diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 29d0fd691..f24e5d7fd 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -92,7 +92,7 @@ jobs: run: cargo codspeed build -p benchmarks --bench ${{ matrix.benchmark }} --features codspeed - name: Run the benchmarks - uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad + uses: CodSpeedHQ/action@dbda7111f8ac363564b0c51b992d4ce76bb89f2f with: run: cargo codspeed run mode: "instrumentation" diff --git a/Cargo.lock b/Cargo.lock index c961fb3ab..64b998ba1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4809,8 +4809,9 @@ dependencies = [ [[package]] name = "ruma" -version = "0.14.0" -source = "git+https://github.com/ruma/ruma?rev=a67081e402dce14365089b34f50489dacc9c53b5#a67081e402dce14365089b34f50489dacc9c53b5" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9f620a2116d0d3082f9256e61dcdf67f2ec266d3f6bb9d2f9c8a20ec5a1fabb" dependencies = [ "assign", "js_int", @@ -4827,8 +4828,9 @@ dependencies = [ [[package]] name = "ruma-client-api" -version = "0.22.0" -source = "git+https://github.com/ruma/ruma?rev=a67081e402dce14365089b34f50489dacc9c53b5#a67081e402dce14365089b34f50489dacc9c53b5" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc977d1a91ea15dcf896cbd7005ed4a253784468833638998109ffceaee53e7" dependencies = [ "as_variant", "assign", @@ -4850,8 +4852,9 @@ dependencies = [ [[package]] name = "ruma-common" -version = "0.17.0" -source = "git+https://github.com/ruma/ruma?rev=a67081e402dce14365089b34f50489dacc9c53b5#a67081e402dce14365089b34f50489dacc9c53b5" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a01993f22d291320b7c9267675e7395775e95269ff526e2c8c3ed5e13175b" dependencies = [ "as_variant", "base64", @@ -4883,8 +4886,9 @@ dependencies = [ [[package]] name = "ruma-events" -version = "0.32.0" -source = "git+https://github.com/ruma/ruma?rev=a67081e402dce14365089b34f50489dacc9c53b5#a67081e402dce14365089b34f50489dacc9c53b5" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dbdeccb62cb4ffe3282325de8ba28cbc0fdce7c78a3f11b7241fbfdb9cb9907" dependencies = [ "as_variant", "indexmap", @@ -4910,8 +4914,9 @@ dependencies = [ [[package]] name = "ruma-federation-api" -version = "0.13.0" -source = "git+https://github.com/ruma/ruma?rev=a67081e402dce14365089b34f50489dacc9c53b5#a67081e402dce14365089b34f50489dacc9c53b5" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb45c15badbf4299c6113a6b90df3e7cb64edbe756bbd8e0224144b56b38305" dependencies = [ "headers", "http", @@ -4932,7 +4937,8 @@ dependencies = [ [[package]] name = "ruma-html" version = "0.6.0" -source = "git+https://github.com/ruma/ruma?rev=a67081e402dce14365089b34f50489dacc9c53b5#a67081e402dce14365089b34f50489dacc9c53b5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6dcd6e9823e177d15460d3cd3a413f38a2beea381f26aca1001c05cd6954ff" dependencies = [ "as_variant", "html5ever", @@ -4943,7 +4949,8 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.12.0" -source = "git+https://github.com/ruma/ruma?rev=a67081e402dce14365089b34f50489dacc9c53b5#a67081e402dce14365089b34f50489dacc9c53b5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9c6b5643060beec0fc9d7acfb41d2c5d91e1591db440ff62361d178e77c35fe" dependencies = [ "js_int", "thiserror 2.0.17", @@ -4951,9 +4958,11 @@ dependencies = [ [[package]] name = "ruma-macros" -version = "0.17.0" -source = "git+https://github.com/ruma/ruma?rev=a67081e402dce14365089b34f50489dacc9c53b5#a67081e402dce14365089b34f50489dacc9c53b5" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a0753312ad577ac462de1742bf2e326b6ba9856ff6f13343aeb17d423fd5426" dependencies = [ + "as_variant", "cfg-if", "proc-macro-crate", "proc-macro2", @@ -4967,7 +4976,8 @@ dependencies = [ [[package]] name = "ruma-signatures" version = "0.19.0" -source = "git+https://github.com/ruma/ruma?rev=a67081e402dce14365089b34f50489dacc9c53b5#a67081e402dce14365089b34f50489dacc9c53b5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146ace2cd59b60ec80d3e801a84e7e6a91e3e01d18a9f5d896ea7ca16a6b8e08" dependencies = [ "base64", "ed25519-dalek", diff --git a/Cargo.toml b/Cargo.toml index cf4b8b3e8..f8441bd15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,7 +68,7 @@ rand = "0.8.5" regex = "1.12.2" reqwest = { version = "0.12.24", default-features = false } rmp-serde = "1.3.0" -ruma = { git = "https://github.com/ruma/ruma", rev = "a67081e402dce14365089b34f50489dacc9c53b5", features = [ +ruma = { version = "0.14.1", features = [ "client-api-c", "compat-upload-signatures", "compat-arbitrary-length-ids", diff --git a/bindings/matrix-sdk-ffi/CHANGELOG.md b/bindings/matrix-sdk-ffi/CHANGELOG.md index 9a2cf5255..bcf288a08 100644 --- a/bindings/matrix-sdk-ffi/CHANGELOG.md +++ b/bindings/matrix-sdk-ffi/CHANGELOG.md @@ -15,6 +15,11 @@ All notable changes to this project will be documented in this file. ### Features +- [**breaking**] `LazyTimelineItemProvider::get_shields` no longer returns an + an `Option`: the `ShieldState` type contains a `None` variant, so the + `Option` was redundant. The `message` field has also been removed: since there + was no way to localise the returned string, applications should not be using it. + ([#5959](https://github.com/matrix-org/matrix-rust-sdk/pull/5959)) - Add `SpaceService::get_space_room` to get a space given its id from the space graph if available. [#5944](https://github.com/matrix-org/matrix-rust-sdk/pull/5944) - Add `QrCodeData::to_bytes()` to allow generation of a QR code. @@ -25,9 +30,13 @@ All notable changes to this project will be documented in this file. [#5624](https://github.com/matrix-org/matrix-rust-sdk/pull/5624/) - Created `RoomPowerLevels::events` function which returns a `HashMap` with all the power levels per event type. ([#5937](https://github.com/matrix-org/matrix-rust-sdk/pull/5937)) +- Expose `EventTimelineItem::forwarder` and `forwarder_profile`, which, if present, provide the ID and profile of + the user who forwarded the keys used to decrypt the event as part of an [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268) + key bundle. + ([#6000](https://github.com/matrix-org/matrix-rust-sdk/pull/6000)) - Expose room power level thresholds in `OtherState::RoomPowerLevels` (ban, kick, invite, redact, state & events defaults, per-event overrides, notifications), so clients can compute the required power level - for actions and compare with previous values. + for actions and compare with previous values. ([#5931](https://github.com/matrix-org/matrix-rust-sdk/pull/5931)) ### Refactor diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index db1be1ee5..cc36c0e2e 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -21,7 +21,6 @@ use matrix_sdk::{ attachment::{ AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo, Thumbnail, }, - deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode}, event_cache::RoomPaginationStatus, room::edit::EditedContent as SdkEditedContent, }; @@ -33,6 +32,7 @@ use matrix_sdk_ui::timeline::{ self, AttachmentConfig, AttachmentSource, EventItemOrigin, LatestEventValue as UiLatestEventValue, LatestEventValueLocalState, MediaUploadProgress as SdkMediaUploadProgress, Profile, TimelineDetails, + TimelineEventShieldState as SdkShieldState, TimelineEventShieldStateCode, TimelineUniqueId as SdkTimelineUniqueId, }; use mime::Mime; @@ -980,12 +980,12 @@ impl From<&matrix_sdk_ui::timeline::EventSendState> for EventSendState { /// authenticity properties. #[derive(uniffi::Enum, Clone)] pub enum ShieldState { - /// A red shield with a tooltip containing the associated message should be - /// presented. - Red { code: ShieldStateCode, message: String }, - /// A grey shield with a tooltip containing the associated message should be - /// presented. - Grey { code: ShieldStateCode, message: String }, + /// A red shield with a tooltip containing a message appropriate to the + /// associated code should be presented. + Red { code: TimelineEventShieldStateCode }, + /// A grey shield with a tooltip containing a message appropriate to the + /// associated code should be presented. + Grey { code: TimelineEventShieldStateCode }, /// No shield should be presented. None, } @@ -993,12 +993,8 @@ pub enum ShieldState { impl From for ShieldState { fn from(value: SdkShieldState) -> Self { match value { - SdkShieldState::Red { code, message } => { - Self::Red { code, message: message.to_owned() } - } - SdkShieldState::Grey { code, message } => { - Self::Grey { code, message: message.to_owned() } - } + SdkShieldState::Red { code } => Self::Red { code }, + SdkShieldState::Grey { code } => Self::Grey { code }, SdkShieldState::None => Self::None, } } @@ -1011,6 +1007,8 @@ pub struct EventTimelineItem { event_or_transaction_id: EventOrTransactionId, sender: String, sender_profile: ProfileDetails, + forwarder: Option, + forwarder_profile: Option, is_own: bool, is_editable: bool, content: TimelineItemContent, @@ -1034,6 +1032,8 @@ impl From for EventTimelineItem { event_or_transaction_id: item.identifier().into(), sender: item.sender().to_string(), sender_profile: item.sender_profile().clone().into(), + forwarder: item.forwarder().map(ToString::to_string), + forwarder_profile: item.forwarder_profile().map(Into::into), is_own: item.is_own(), is_editable: item.is_editable(), content: item.content().clone().into(), @@ -1089,6 +1089,21 @@ impl From> for ProfileDetails { } } +impl From<&TimelineDetails> for ProfileDetails { + fn from(details: &TimelineDetails) -> Self { + match details { + TimelineDetails::Unavailable => Self::Unavailable, + TimelineDetails::Pending => Self::Pending, + TimelineDetails::Ready(profile) => Self::Ready { + display_name: profile.display_name.clone(), + display_name_ambiguous: profile.display_name_ambiguous, + avatar_url: profile.avatar_url.as_ref().map(ToString::to_string), + }, + TimelineDetails::Error(e) => Self::Error { message: e.to_string() }, + } + } +} + #[derive(Clone, uniffi::Record)] pub struct PollData { question: String, @@ -1277,8 +1292,8 @@ pub struct LazyTimelineItemProvider(Arc Option { - self.0.get_shield(strict).map(Into::into) + fn get_shields(&self, strict: bool) -> ShieldState { + self.0.get_shield(strict).into() } /// Returns some debug information for this event timeline item. diff --git a/crates/matrix-sdk-common/CHANGELOG.md b/crates/matrix-sdk-common/CHANGELOG.md index 034190dae..6361ec993 100644 --- a/crates/matrix-sdk-common/CHANGELOG.md +++ b/crates/matrix-sdk-common/CHANGELOG.md @@ -6,6 +6,18 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +### Features + +- [**breaking**] `ShieldStateCode` no longer includes + `SentInClear`. `VeificationState::to_shield_state_{lax,strict}` never + returned that code, ans so having it in the enum was somewhat misleading. + ([#5959](https://github.com/matrix-org/matrix-rust-sdk/pull/5959)) +- Add field `forwarder` of type `ForwarderInfo` to `EncryptionInfo`, which + exposes information about the forwarder of the keys with which an event was + encrypted if they were shared as part of an [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268) + room key bundle. + ([#5945](https://github.com/matrix-org/matrix-rust-sdk/pull/5945)). + ### Bug Fixes - Fix `TimelineEvent::from_bundled_latest_event` sometimes removing the `session_id` of UTDs. This broken event could later be saved to the event cache and become an unresolvable UTD. ([#5970](https://github.com/matrix-org/matrix-rust-sdk/pull/5970)). @@ -14,8 +26,6 @@ All notable changes to this project will be documented in this file. ### Features -- Add field `forwarder` of type `ForwarderInfo` to `EncryptionInfo`, which which exposes information about the forwarder of the keys with which an event was encrypted if they were shared as part of an [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268) room key bundle. - ([#5945](https://github.com/matrix-org/matrix-rust-sdk/pull/5945)). - [**breaking**] Cross-process lock can be dirty. The `CrossProcess::try_lock_once` now returns a new type `CrossProcessResult`, which is an enum with `Clean`, `Dirty` or `Unobtained` variants. When the lock is dirty it means it's been acquired once, then acquired another time from another holder, so the current holder may want to refresh its internal state. ([#5672](https://github.com/matrix-org/matrix-rust-sdk/pull/5672)). diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index f00fff36b..4151eb010 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -46,7 +46,6 @@ const UNKNOWN_DEVICE: &str = "Encrypted by an unknown or deleted device."; const MISMATCHED_SENDER: &str = "\ The sender of the event does not match the owner of the device \ that created the Megolm session."; -pub const SENT_IN_CLEAR: &str = "Not encrypted."; /// Represents the state of verification for a decrypted message sent by a /// device. @@ -283,8 +282,6 @@ pub enum ShieldStateCode { UnsignedDevice, /// The sender hasn't been verified by the Client's user. UnverifiedIdentity, - /// An unencrypted event in an encrypted room. - SentInClear, /// The sender was previously verified but changed their identity. #[serde(alias = "PreviouslyVerified")] VerificationViolation, @@ -2008,7 +2005,6 @@ mod tests { 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); }); } diff --git a/crates/matrix-sdk-common/src/snapshots/snapshot_test_shield_codes-5.snap b/crates/matrix-sdk-common/src/snapshots/snapshot_test_shield_codes-5.snap index 7b294abf0..753b4b444 100644 --- a/crates/matrix-sdk-common/src/snapshots/snapshot_test_shield_codes-5.snap +++ b/crates/matrix-sdk-common/src/snapshots/snapshot_test_shield_codes-5.snap @@ -1,5 +1,5 @@ --- source: crates/matrix-sdk-common/src/deserialized_responses.rs -expression: "ShieldStateCode::SentInClear" +expression: "ShieldStateCode::VerificationViolation" --- -"SentInClear" +"VerificationViolation" diff --git a/crates/matrix-sdk-indexeddb/Cargo.toml b/crates/matrix-sdk-indexeddb/Cargo.toml index 8cdfd88ec..62fad4b80 100644 --- a/crates/matrix-sdk-indexeddb/Cargo.toml +++ b/crates/matrix-sdk-indexeddb/Cargo.toml @@ -4,7 +4,7 @@ version = "0.16.0" repository = "https://github.com/matrix-org/matrix-rust-sdk" description = "Web's IndexedDB Storage backend for matrix-sdk" license = "Apache-2.0" -edition = "2021" +edition = "2024" rust-version.workspace = true readme = "README.md" 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 6ed4f1783..58ea72f6f 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs @@ -25,7 +25,7 @@ use indexed_db_futures::{ }; use tracing::info; -use crate::{crypto_store::Result, serializer::SafeEncodeSerializer, IndexeddbCryptoStoreError}; +use crate::{IndexeddbCryptoStoreError, crypto_store::Result, serializer::SafeEncodeSerializer}; mod old_keys; mod v0_to_v5; @@ -274,13 +274,13 @@ mod tests { }; use matrix_sdk_crypto::{ olm::{InboundGroupSession, SenderData, SessionKey}, - store::{types::RoomKeyWithheldEntry, CryptoStore}, - types::{events::room_key_withheld::RoomKeyWithheldContent, EventEncryptionAlgorithm}, + store::{CryptoStore, types::RoomKeyWithheldEntry}, + types::{EventEncryptionAlgorithm, events::room_key_withheld::RoomKeyWithheldContent}, vodozemac::{Curve25519PublicKey, Curve25519SecretKey, Ed25519PublicKey, Ed25519SecretKey}, }; use matrix_sdk_store_encryption::StoreCipher; use matrix_sdk_test::async_test; - use ruma::{device_id, owned_user_id, room_id, OwnedRoomId, RoomId}; + use ruma::{OwnedRoomId, RoomId, device_id, owned_user_id, room_id}; use serde::Serialize; use tracing_subscriber::util::SubscriberInitExt; use wasm_bindgen::JsValue; @@ -288,8 +288,8 @@ mod tests { use super::{v0_to_v5, v7::InboundGroupSessionIndexedDbObject2}; use crate::{ - crypto_store::{keys, migrations::*, InboundGroupSessionIndexedDbObject}, IndexeddbCryptoStore, + crypto_store::{InboundGroupSessionIndexedDbObject, keys, migrations::*}, }; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -658,10 +658,12 @@ 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()); + assert!( + raw_store + .index_names() + .find(|idx| idx == "inbound_group_session_sender_key_sender_data_type_idx") + .is_some() + ); transaction.commit().await.unwrap(); db.close(); diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v0_to_v5.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v0_to_v5.rs index 444d7e952..e30772542 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v0_to_v5.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v0_to_v5.rs @@ -16,15 +16,14 @@ //! the first version of `inbound_group_sessions`. use indexed_db_futures::{ + Build, database::Database, error::{Error, OpenDbError}, - Build, }; use crate::crypto_store::{ - keys, + Result, keys, migrations::{add_nonunique_index, add_unique_index, do_schema_upgrade, old_keys}, - Result, }; /// Perform schema migrations as needed, up to schema version 5. diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v101_to_v102.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v101_to_v102.rs index 9ee0d9be0..04b6865d9 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v101_to_v102.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v101_to_v102.rs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use indexed_db_futures::{error::OpenDbError, Build}; +use indexed_db_futures::{Build, error::OpenDbError}; -use crate::crypto_store::{keys, migrations::do_schema_upgrade, Result}; +use crate::crypto_store::{Result, keys, migrations::do_schema_upgrade}; /// Perform the schema upgrade v101 to v102, add the `lease_locks` table. /// diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v10_to_v11.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v10_to_v11.rs index 8ae0cd5b9..0a5039a7a 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v10_to_v11.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v10_to_v11.rs @@ -16,14 +16,14 @@ //! `backup_keys.backup_version_v1`, switching to a new serialization format. use indexed_db_futures::{ - error::OpenDbError, query_source::QuerySource, transaction::TransactionMode, Build, + Build, error::OpenDbError, query_source::QuerySource, transaction::TransactionMode, }; use wasm_bindgen::JsValue; use crate::{ crypto_store::{ keys, - migrations::{do_schema_upgrade, old_keys, MigrationDb}, + migrations::{MigrationDb, do_schema_upgrade, old_keys}, }, serializer::SafeEncodeSerializer, }; 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 index 256947904..ec5f6ded0 100644 --- 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 @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use indexed_db_futures::{error::OpenDbError, Build}; +use indexed_db_futures::{Build, error::OpenDbError}; -use crate::crypto_store::{keys, migrations::do_schema_upgrade, Result}; +use crate::crypto_store::{Result, keys, migrations::do_schema_upgrade}; /// Perform the schema upgrade v11 to v12, adding an index on /// `(curve_key, sender_data_type, session_id)` to `inbound_group_sessions3`. diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v12_to_v13.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v12_to_v13.rs index e17a3a4f6..fafa3f538 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v12_to_v13.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v12_to_v13.rs @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -use indexed_db_futures::{error::OpenDbError, Build}; +use indexed_db_futures::{Build, error::OpenDbError}; -use crate::crypto_store::{keys, migrations::do_schema_upgrade, Result}; +use crate::crypto_store::{Result, keys, migrations::do_schema_upgrade}; /// Perform the schema upgrade v12 to v13, adding the /// `received_room_key_bundles` store. diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v13_to_v14.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v13_to_v14.rs index ac9084fcf..3fba7d514 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v13_to_v14.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v13_to_v14.rs @@ -14,11 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -use indexed_db_futures::{error::OpenDbError, transaction::TransactionMode, Build}; +use indexed_db_futures::{Build, error::OpenDbError, transaction::TransactionMode}; use super::MigrationDb; use crate::{ - crypto_store::{keys, migrations::do_schema_upgrade, Result}, + crypto_store::{Result, keys, migrations::do_schema_upgrade}, serializer::SafeEncodeSerializer, }; diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v14_to_v101.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v14_to_v101.rs index b17c98aaf..883a13e63 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v14_to_v101.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v14_to_v101.rs @@ -19,15 +19,15 @@ limitations under the License. //! the key around; finally, we drop the old table. use indexed_db_futures::{ - error::OpenDbError, query_source::QuerySource, transaction::TransactionMode, Build, + Build, error::OpenDbError, query_source::QuerySource, transaction::TransactionMode, }; use matrix_sdk_crypto::store::types::RoomKeyWithheldEntry; use tracing::{debug, info, warn}; use wasm_bindgen::JsValue; -use super::{old_keys, MigrationDb}; +use super::{MigrationDb, old_keys}; use crate::{ - crypto_store::{keys, migrations::do_schema_upgrade, Result}, + crypto_store::{Result, keys, migrations::do_schema_upgrade}, serializer::SafeEncodeSerializer, }; diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v5_to_v7.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v5_to_v7.rs index fd3e803f4..df33f846f 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v5_to_v7.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v5_to_v7.rs @@ -20,20 +20,19 @@ //! The migration 6->7 deletes the old store inbound_group_sessions. use indexed_db_futures::{ - error::OpenDbError, query_source::QuerySource, transaction::TransactionMode, Build, + Build, error::OpenDbError, query_source::QuerySource, transaction::TransactionMode, }; use matrix_sdk_crypto::olm::InboundGroupSession; use tracing::{debug, info}; use wasm_bindgen::JsValue; use crate::{ + IndexeddbCryptoStoreError, crypto_store::{ - keys, - migrations::{add_nonunique_index, do_schema_upgrade, old_keys, v7, MigrationDb}, - Result, + Result, keys, + migrations::{MigrationDb, add_nonunique_index, do_schema_upgrade, old_keys, v7}, }, serializer::SafeEncodeSerializer, - IndexeddbCryptoStoreError, }; /// Perform the schema upgrade v5 to v6, creating `inbound_group_sessions2`. diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v7_to_v8.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v7_to_v8.rs index e07f68ce8..3ccb87a97 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v7_to_v8.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v7_to_v8.rs @@ -16,19 +16,19 @@ //! ensuring that the keys are correctly encoded for this new store name. use indexed_db_futures::{ - error::OpenDbError, query_source::QuerySource, transaction::TransactionMode, Build, + Build, error::OpenDbError, query_source::QuerySource, transaction::TransactionMode, }; use matrix_sdk_crypto::olm::InboundGroupSession; use tracing::{debug, info}; use wasm_bindgen::JsValue; use crate::{ + IndexeddbCryptoStoreError, crypto_store::{ - migrations::{do_schema_upgrade, old_keys, v7, MigrationDb}, Result, + migrations::{MigrationDb, do_schema_upgrade, old_keys, v7}, }, serializer::SafeEncodeSerializer, - IndexeddbCryptoStoreError, }; /// In the migration v5 to v7, we incorrectly copied the keys in diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v8_to_v10.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v8_to_v10.rs index a7ff3ff0f..613d6e7f0 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v8_to_v10.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v8_to_v10.rs @@ -16,23 +16,22 @@ //! inbound_group_sessions3, shrinking the values stored in each record. use indexed_db_futures::{ - error::OpenDbError, query_source::QuerySource, transaction::TransactionMode, Build, + Build, error::OpenDbError, query_source::QuerySource, transaction::TransactionMode, }; use matrix_sdk_crypto::olm::InboundGroupSession; use tracing::{debug, info}; use wasm_bindgen::JsValue; use crate::{ + IndexeddbCryptoStoreError, crypto_store::{ - keys, + InboundGroupSessionIndexedDbObject, Result, keys, migrations::{ - add_nonunique_index, do_schema_upgrade, old_keys, - v7::InboundGroupSessionIndexedDbObject2, MigrationDb, + MigrationDb, add_nonunique_index, do_schema_upgrade, old_keys, + v7::InboundGroupSessionIndexedDbObject2, }, - InboundGroupSessionIndexedDbObject, Result, }, serializer::SafeEncodeSerializer, - IndexeddbCryptoStoreError, }; /// Perform the schema upgrade v8 to v9, creating `inbound_group_sessions3`. diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs index 2fbe6f33f..879817b8e 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs @@ -21,38 +21,38 @@ use async_trait::async_trait; use gloo_utils::format::JsValueSerdeExt; use hkdf::Hkdf; use indexed_db_futures::{ + KeyRange, cursor::Cursor, database::Database, internals::SystemRepr, object_store::ObjectStore, prelude::*, transaction::{Transaction, TransactionMode}, - KeyRange, }; use js_sys::Array; use matrix_sdk_base::cross_process_lock::{ CrossProcessLockGeneration, FIRST_CROSS_PROCESS_LOCK_GENERATION, }; use matrix_sdk_crypto::{ + Account, DeviceData, GossipRequest, GossippedSecret, SecretInfo, TrackedUser, UserIdentityData, olm::{ Curve25519PublicKey, InboundGroupSession, OlmMessageHash, OutboundGroupSession, PickledInboundGroupSession, PrivateCrossSigningIdentity, SenderDataType, Session, StaticAccountData, }, store::{ + CryptoStore, CryptoStoreError, types::{ BackupKeys, Changes, DehydratedDeviceKey, PendingChanges, RoomKeyCounts, RoomKeyWithheldEntry, RoomSettings, StoredRoomKeyBundleData, }, - CryptoStore, CryptoStoreError, }, vodozemac::base64_encode, - Account, DeviceData, GossipRequest, GossippedSecret, SecretInfo, TrackedUser, UserIdentityData, }; use matrix_sdk_store_encryption::StoreCipher; use ruma::{ - events::secret::request::SecretName, DeviceId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, - RoomId, TransactionId, UserId, + DeviceId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, RoomId, TransactionId, UserId, + events::secret::request::SecretName, }; use serde::{Deserialize, Serialize}; use sha2::Sha256; @@ -313,11 +313,7 @@ impl PendingIndexeddbChanges { .iter() .filter_map( |(store, pending_operations)| { - if !pending_operations.is_empty() { - Some(*store) - } else { - None - } + if !pending_operations.is_empty() { Some(*store) } else { None } }, ) .collect() @@ -2004,9 +2000,9 @@ mod unit_tests { // Testing the exact JSON here is theoretically flaky in the face of // serialization changes in serde_json but it seems unlikely, and it's // simple enough to fix if we need to. - assert!(serde_json::to_string(&session_needs_backup) - .unwrap() - .contains(r#""needs_backup":1"#),); + assert!( + serde_json::to_string(&session_needs_backup).unwrap().contains(r#""needs_backup":1"#), + ); } #[test] @@ -2178,7 +2174,7 @@ mod encrypted_tests { use matrix_sdk_crypto::{ cryptostore_integration_tests, olm::Account, - store::{types::PendingChanges, CryptoStore}, + store::{CryptoStore, types::PendingChanges}, vodozemac::base64_encode, }; use matrix_sdk_test::async_test; diff --git a/crates/matrix-sdk-indexeddb/src/error.rs b/crates/matrix-sdk-indexeddb/src/error.rs index 0bd42147a..4384736b4 100644 --- a/crates/matrix-sdk-indexeddb/src/error.rs +++ b/crates/matrix-sdk-indexeddb/src/error.rs @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License +#[cfg(feature = "state-store")] +use matrix_sdk_base::StoreError; #[cfg(feature = "event-cache-store")] use matrix_sdk_base::event_cache::store::EventCacheStoreError; #[cfg(feature = "media-store")] use matrix_sdk_base::media::store::MediaStoreError; -#[cfg(feature = "state-store")] -use matrix_sdk_base::StoreError; #[cfg(any(feature = "event-cache-store", feature = "media-store"))] use matrix_sdk_base::{SendOutsideWasm, SyncOutsideWasm}; #[cfg(feature = "e2e-encryption")] diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/builder.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/builder.rs index 6d8bcd862..16703d494 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/builder.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/builder.rs @@ -23,8 +23,8 @@ use matrix_sdk_store_encryption::StoreCipher; use crate::{ event_cache_store::{ - error::IndexeddbEventCacheStoreError, migrations::open_and_upgrade_db, - IndexeddbEventCacheStore, + IndexeddbEventCacheStore, error::IndexeddbEventCacheStoreError, + migrations::open_and_upgrade_db, }, serializer::{indexed_type::IndexedTypeSerializer, safe_encode::types::SafeEncodeSerializer}, }; diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/integration_tests.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/integration_tests.rs index 02781b0e5..09dba1150 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/integration_tests.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/integration_tests.rs @@ -15,11 +15,11 @@ use assert_matches::assert_matches; use matrix_sdk_base::{ event_cache::{ - store::{ - integration_tests::{check_test_event, make_test_event}, - EventCacheStore, - }, Gap, + store::{ + EventCacheStore, + integration_tests::{check_test_event, make_test_event}, + }, }, linked_chunk::{ChunkContent, ChunkIdentifier, LinkedChunkId, Position, Update}, }; @@ -573,8 +573,8 @@ pub async fn test_load_previous_chunk(store: IndexeddbEventCacheStore) { /// mod tests { /// use super::{EventCacheStore, EventCacheStoreResult, MyStore}; /// -/// async fn get_event_cache_store( -/// ) -> Result { +/// async fn get_event_cache_store() +/// -> Result { /// Ok(MyStore::new()) /// } /// diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/migrations.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/migrations.rs index c82a24472..91140762c 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/migrations.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/migrations.rs @@ -21,7 +21,7 @@ use thiserror::Error; /// The current version and keys used in the database. pub mod current { - use super::{v2, Version}; + use super::{Version, v2}; pub const VERSION: Version = Version::V2; pub use v2::keys; diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs index 6b9696ef5..efa87d9f0 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs @@ -16,13 +16,13 @@ use std::{rc::Rc, time::Duration}; -use indexed_db_futures::{database::Database, Build}; +use indexed_db_futures::{Build, database::Database}; #[cfg(target_family = "wasm")] use matrix_sdk_base::cross_process_lock::{ CrossProcessLockGeneration, FIRST_CROSS_PROCESS_LOCK_GENERATION, }; use matrix_sdk_base::{ - event_cache::{store::EventCacheStore, Event, Gap}, + event_cache::{Event, Gap, store::EventCacheStore}, linked_chunk::{ ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId, Position, RawChunk, Update, @@ -30,7 +30,7 @@ use matrix_sdk_base::{ timer, }; use ruma::{ - events::relation::RelationType, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, + EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, events::relation::RelationType, }; use tracing::{error, instrument, trace}; use web_sys::IdbTransactionMode; @@ -41,7 +41,7 @@ use crate::{ transaction::IndexeddbEventCacheStoreTransaction, types::{ChunkType, InBandEvent, Lease, OutOfBandEvent}, }, - serializer::indexed_type::{traits::Indexed, IndexedTypeSerializer}, + serializer::indexed_type::{IndexedTypeSerializer, traits::Indexed}, transaction::TransactionError, }; @@ -406,15 +406,13 @@ impl EventCacheStore for IndexeddbEventCacheStore { )?; if let Some(chunk) = transaction.get_chunk_by_id(linked_chunk_id, before_chunk_identifier).await? + && let Some(previous_identifier) = chunk.previous { - if let Some(previous_identifier) = chunk.previous { - let previous_identifier = ChunkIdentifier::new(previous_identifier); - return Ok(transaction - .load_chunk_by_id(linked_chunk_id, previous_identifier) - .await?); - } + let previous_identifier = ChunkIdentifier::new(previous_identifier); + Ok(transaction.load_chunk_by_id(linked_chunk_id, previous_identifier).await?) + } else { + Ok(None) } - Ok(None) } #[instrument(skip(self))] diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/indexed_types.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/indexed_types.rs index d3454669f..f8455b0ea 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/indexed_types.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/indexed_types.rs @@ -29,7 +29,7 @@ use matrix_sdk_base::linked_chunk::{ChunkIdentifier, LinkedChunkId}; use matrix_sdk_crypto::CryptoStoreError; -use ruma::{events::relation::RelationType, EventId, RoomId}; +use ruma::{EventId, RoomId, events::relation::RelationType}; use serde::{Deserialize, Serialize}; use thiserror::Error; diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/transaction.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/transaction.rs index 19b8e1411..74dd55eca 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/transaction.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/transaction.rs @@ -19,8 +19,8 @@ use matrix_sdk_base::{ event_cache::{Event as RawEvent, Gap as RawGap}, linked_chunk::{ChunkContent, ChunkIdentifier, LinkedChunkId, RawChunk}, }; -use ruma::{events::relation::RelationType, EventId, RoomId}; -use serde::{de::DeserializeOwned, Serialize}; +use ruma::{EventId, RoomId, events::relation::RelationType}; +use serde::{Serialize, de::DeserializeOwned}; use crate::{ error::AsyncErrorDeps, @@ -33,9 +33,9 @@ use crate::{ types::{Chunk, ChunkType, Event, Gap, Lease, Position}, }, serializer::indexed_type::{ + IndexedTypeSerializer, range::IndexedKeyRange, traits::{Indexed, IndexedPrefixKeyBounds, IndexedPrefixKeyComponentBounds}, - IndexedTypeSerializer, }, transaction::{Transaction, TransactionError}, }; diff --git a/crates/matrix-sdk-indexeddb/src/media_store/builder.rs b/crates/matrix-sdk-indexeddb/src/media_store/builder.rs index 1dede46d0..1d1526b96 100644 --- a/crates/matrix-sdk-indexeddb/src/media_store/builder.rs +++ b/crates/matrix-sdk-indexeddb/src/media_store/builder.rs @@ -19,7 +19,7 @@ use matrix_sdk_store_encryption::StoreCipher; use crate::{ media_store::{ - error::IndexeddbMediaStoreError, migrations::open_and_upgrade_db, IndexeddbMediaStore, + IndexeddbMediaStore, error::IndexeddbMediaStoreError, migrations::open_and_upgrade_db, }, serializer::{indexed_type::IndexedTypeSerializer, safe_encode::types::SafeEncodeSerializer}, }; diff --git a/crates/matrix-sdk-indexeddb/src/media_store/migrations.rs b/crates/matrix-sdk-indexeddb/src/media_store/migrations.rs index 5d115498e..3bcc2fbe4 100644 --- a/crates/matrix-sdk-indexeddb/src/media_store/migrations.rs +++ b/crates/matrix-sdk-indexeddb/src/media_store/migrations.rs @@ -21,7 +21,7 @@ use thiserror::Error; /// The current version and keys used in the database. pub mod current { - use super::{v2, Version}; + use super::{Version, v2}; pub const VERSION: Version = Version::V2; pub use v2::keys; diff --git a/crates/matrix-sdk-indexeddb/src/media_store/mod.rs b/crates/matrix-sdk-indexeddb/src/media_store/mod.rs index 92b8dc778..c9d51a597 100644 --- a/crates/matrix-sdk-indexeddb/src/media_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/media_store/mod.rs @@ -29,7 +29,7 @@ use std::{rc::Rc, time::Duration}; pub use builder::IndexeddbMediaStoreBuilder; pub use error::IndexeddbMediaStoreError; use indexed_db_futures::{ - cursor::CursorDirection, database::Database, transaction::TransactionMode, Build, + Build, cursor::CursorDirection, database::Database, transaction::TransactionMode, }; #[cfg(target_family = "wasm")] use matrix_sdk_base::cross_process_lock::{ @@ -37,15 +37,15 @@ use matrix_sdk_base::cross_process_lock::{ }; use matrix_sdk_base::{ media::{ + MediaRequestParameters, store::{ IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService, MediaStore, MediaStoreInner, }, - MediaRequestParameters, }, timer, }; -use ruma::{time::SystemTime, MilliSecondsSinceUnixEpoch, MxcUri}; +use ruma::{MilliSecondsSinceUnixEpoch, MxcUri, time::SystemTime}; use tracing::instrument; use crate::{ @@ -53,7 +53,7 @@ use crate::{ transaction::IndexeddbMediaStoreTransaction, types::{Lease, Media, MediaCleanupTime, MediaContent, MediaMetadata, UnixTime}, }, - serializer::indexed_type::{traits::Indexed, IndexedTypeSerializer}, + serializer::indexed_type::{IndexedTypeSerializer, traits::Indexed}, transaction::TransactionError, }; @@ -345,12 +345,12 @@ impl MediaStoreInner for IndexeddbMediaStore { let transaction = self.transaction(&[MediaMetadata::OBJECT_STORE], TransactionMode::Readwrite)?; - if let Some(mut metadata) = transaction.get_media_metadata_by_id(request).await? { - if metadata.ignore_policy != ignore_policy { - metadata.ignore_policy = ignore_policy; - transaction.put_media_metadata(&metadata).await?; - transaction.commit().await?; - } + if let Some(mut metadata) = transaction.get_media_metadata_by_id(request).await? + && metadata.ignore_policy != ignore_policy + { + metadata.ignore_policy = ignore_policy; + transaction.put_media_metadata(&metadata).await?; + transaction.commit().await?; } Ok(()) } diff --git a/crates/matrix-sdk-indexeddb/src/media_store/serializer/indexed_types.rs b/crates/matrix-sdk-indexeddb/src/media_store/serializer/indexed_types.rs index fc6ed3681..13d25364d 100644 --- a/crates/matrix-sdk-indexeddb/src/media_store/serializer/indexed_types.rs +++ b/crates/matrix-sdk-indexeddb/src/media_store/serializer/indexed_types.rs @@ -30,8 +30,8 @@ use std::ops::Deref; use matrix_sdk_base::media::{ - store::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy}, MediaRequestParameters, UniqueKey, + store::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy}, }; use matrix_sdk_crypto::CryptoStoreError; use ruma::MxcUri; diff --git a/crates/matrix-sdk-indexeddb/src/media_store/transaction.rs b/crates/matrix-sdk-indexeddb/src/media_store/transaction.rs index e492d4aaf..d7dc9abdd 100644 --- a/crates/matrix-sdk-indexeddb/src/media_store/transaction.rs +++ b/crates/matrix-sdk-indexeddb/src/media_store/transaction.rs @@ -16,8 +16,8 @@ use std::ops::Deref; use indexed_db_futures::{cursor::CursorDirection, transaction as inner}; use matrix_sdk_base::media::{ - store::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy}, MediaRequestParameters, + store::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy}, }; use ruma::MxcUri; use uuid::Uuid; @@ -34,7 +34,7 @@ use crate::{ types::{Lease, Media, MediaCleanupTime, MediaContent, MediaMetadata, UnixTime}, }, serializer::indexed_type::{ - range::IndexedKeyRange, traits::IndexedPrefixKeyComponentBounds, IndexedTypeSerializer, + IndexedTypeSerializer, range::IndexedKeyRange, traits::IndexedPrefixKeyComponentBounds, }, transaction::{Transaction, TransactionError}, }; diff --git a/crates/matrix-sdk-indexeddb/src/media_store/types.rs b/crates/matrix-sdk-indexeddb/src/media_store/types.rs index 204afba9e..06986fed4 100644 --- a/crates/matrix-sdk-indexeddb/src/media_store/types.rs +++ b/crates/matrix-sdk-indexeddb/src/media_store/types.rs @@ -19,7 +19,7 @@ use std::{ use matrix_sdk_base::{ cross_process_lock::CrossProcessLockGeneration, - media::{store::IgnoreMediaRetentionPolicy, MediaRequestParameters}, + media::{MediaRequestParameters, store::IgnoreMediaRetentionPolicy}, }; use ruma::time::{SystemTime, UNIX_EPOCH}; use serde::{Deserialize, Serialize}; diff --git a/crates/matrix-sdk-indexeddb/src/serializer/indexed_type/mod.rs b/crates/matrix-sdk-indexeddb/src/serializer/indexed_type/mod.rs index 66182ee3d..1423152bd 100644 --- a/crates/matrix-sdk-indexeddb/src/serializer/indexed_type/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/serializer/indexed_type/mod.rs @@ -25,7 +25,7 @@ pub mod traits; use gloo_utils::format::JsValueSerdeExt; use indexed_db_futures::KeyRange; use range::IndexedKeyRange; -use serde::{de::DeserializeOwned, Serialize}; +use serde::{Serialize, de::DeserializeOwned}; use thiserror::Error; use traits::{Indexed, IndexedKey}; use wasm_bindgen::JsValue; diff --git a/crates/matrix-sdk-indexeddb/src/serializer/safe_encode/traits.rs b/crates/matrix-sdk-indexeddb/src/serializer/safe_encode/traits.rs index aa4bf32c9..a5c276e50 100644 --- a/crates/matrix-sdk-indexeddb/src/serializer/safe_encode/traits.rs +++ b/crates/matrix-sdk-indexeddb/src/serializer/safe_encode/traits.rs @@ -1,18 +1,17 @@ //! Helpers for wasm32/browser environments use base64::{ - alphabet, - engine::{general_purpose, GeneralPurpose}, - Engine, + Engine, alphabet, + engine::{GeneralPurpose, general_purpose}, }; use indexed_db_futures::KeyRange; use matrix_sdk_store_encryption::StoreCipher; use ruma::{ - events::{ - receipt::ReceiptType, GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType, - }, DeviceId, EventId, MxcUri, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, TransactionId, UserId, + events::{ + GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType, receipt::ReceiptType, + }, }; use wasm_bindgen::JsValue; diff --git a/crates/matrix-sdk-indexeddb/src/serializer/safe_encode/types.rs b/crates/matrix-sdk-indexeddb/src/serializer/safe_encode/types.rs index 348c899c4..c17e97707 100644 --- a/crates/matrix-sdk-indexeddb/src/serializer/safe_encode/types.rs +++ b/crates/matrix-sdk-indexeddb/src/serializer/safe_encode/types.rs @@ -15,15 +15,14 @@ use std::sync::Arc; use base64::{ - alphabet, - engine::{general_purpose, GeneralPurpose}, - Engine, + Engine, alphabet, + engine::{GeneralPurpose, general_purpose}, }; use gloo_utils::format::JsValueSerdeExt; use indexed_db_futures::KeyRange; use matrix_sdk_crypto::CryptoStoreError; use matrix_sdk_store_encryption::{EncryptedValueBase64, StoreCipher}; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; use wasm_bindgen::JsValue; use zeroize::Zeroizing; diff --git a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs index d066d12d2..664d32561 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs @@ -28,17 +28,17 @@ use indexed_db_futures::{ }; use js_sys::Date as JsDate; use matrix_sdk_base::{ - deserialized_responses::SyncOrStrippedState, store::migration_helpers::RoomInfoV1, - StateStoreDataKey, + StateStoreDataKey, deserialized_responses::SyncOrStrippedState, + store::migration_helpers::RoomInfoV1, }; use matrix_sdk_store_encryption::StoreCipher; use ruma::{ events::{ + StateEventType, room::{ create::RoomCreateEventContent, member::{StrippedRoomMemberEvent, SyncRoomMemberEvent}, }, - StateEventType, }, serde::Raw, }; @@ -47,8 +47,8 @@ use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue}; use wasm_bindgen::JsValue; use super::{ - deserialize_value, encode_key, encode_to_range, keys, serialize_value, Result, RoomMember, - ALL_STORES, + ALL_STORES, Result, RoomMember, deserialize_value, encode_key, encode_to_range, keys, + serialize_value, }; use crate::IndexeddbStateStoreError; @@ -408,11 +408,11 @@ async fn v3_fix_store(store: &ObjectStore<'_>, store_cipher: Option<&StoreCipher if json.contains(r#""content":null"#) { let mut value: JsonValue = serde_json::from_str(json)?; - if let Some(content) = value.get_mut("content") { - if matches!(content, JsonValue::Null) { - *content = JsonValue::Object(Default::default()); - return Ok(Some(value)); - } + if let Some(content) = value.get_mut("content") + && matches!(content, JsonValue::Null) + { + *content = JsonValue::Object(Default::default()); + return Ok(Some(value)); } } @@ -854,33 +854,34 @@ mod tests { transaction::{Transaction, TransactionMode}, }; use matrix_sdk_base::{ + RoomMemberships, RoomState, StateStore, StateStoreDataKey, StoreError, deserialized_responses::RawMemberEvent, store::{RoomLoadSettings, StateStoreExt}, sync::UnreadNotificationsCount, - RoomMemberships, RoomState, StateStore, StateStoreDataKey, StoreError, }; use matrix_sdk_test::{async_test, test_json}; use ruma::{ + EventId, MilliSecondsSinceUnixEpoch, OwnedUserId, RoomId, UserId, events::{ + AnySyncStateEvent, StateEventType, room::{ create::RoomCreateEventContent, member::{StrippedRoomMemberEvent, SyncRoomMemberEvent}, }, - AnySyncStateEvent, StateEventType, }, owned_user_id, room_id, serde::Raw, - server_name, user_id, EventId, MilliSecondsSinceUnixEpoch, OwnedUserId, RoomId, UserId, + server_name, user_id, }; use serde_json::json; use uuid::Uuid; use wasm_bindgen::JsValue; - use super::{old_keys, MigrationConflictStrategy, CURRENT_DB_VERSION, CURRENT_META_DB_VERSION}; + use super::{CURRENT_DB_VERSION, CURRENT_META_DB_VERSION, MigrationConflictStrategy, old_keys}; use crate::{ - serializer::safe_encode::traits::SafeEncode, - state_store::{encode_key, keys, serialize_value, Result}, IndexeddbStateStore, IndexeddbStateStoreError, + serializer::safe_encode::traits::SafeEncode, + state_store::{Result, encode_key, keys, serialize_value}, }; const CUSTOM_DATA_KEY: &[u8] = b"custom_data_key"; diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index a90a3cc8b..ce3a3f30c 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -22,38 +22,38 @@ use async_trait::async_trait; use gloo_utils::format::JsValueSerdeExt; use growable_bloom_filter::GrowableBloom; use indexed_db_futures::{ - cursor::CursorDirection, database::Database, error::OpenDbError, prelude::*, - transaction::TransactionMode, KeyRange, + KeyRange, cursor::CursorDirection, database::Database, error::OpenDbError, prelude::*, + transaction::TransactionMode, }; use matrix_sdk_base::{ + MinimalRoomMemberEvent, ROOM_VERSION_FALLBACK, ROOM_VERSION_RULES_FALLBACK, RoomInfo, + RoomMemberships, StateStoreDataKey, StateStoreDataValue, ThreadSubscriptionCatchupToken, deserialized_responses::{DisplayName, RawAnySyncOrStrippedState}, store::{ - compare_thread_subscription_bump_stamps, ChildTransactionId, ComposerDraft, - DependentQueuedRequest, DependentQueuedRequestKind, QueuedRequest, QueuedRequestKind, - RoomLoadSettings, SentRequestKey, SerializableEventContent, StateChanges, StateStore, - StoreError, StoredThreadSubscription, SupportedVersionsResponse, ThreadSubscriptionStatus, - TtlStoreValue, WellKnownResponse, + ChildTransactionId, ComposerDraft, DependentQueuedRequest, DependentQueuedRequestKind, + QueuedRequest, QueuedRequestKind, RoomLoadSettings, SentRequestKey, + SerializableEventContent, StateChanges, StateStore, StoreError, StoredThreadSubscription, + SupportedVersionsResponse, ThreadSubscriptionStatus, TtlStoreValue, WellKnownResponse, + compare_thread_subscription_bump_stamps, }, - MinimalRoomMemberEvent, RoomInfo, RoomMemberships, StateStoreDataKey, StateStoreDataValue, - ThreadSubscriptionCatchupToken, ROOM_VERSION_FALLBACK, ROOM_VERSION_RULES_FALLBACK, }; use matrix_sdk_store_encryption::{Error as EncryptionError, StoreCipher}; use ruma::{ - canonical_json::{redact, RedactedBecause}, + CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, + OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId, + canonical_json::{RedactedBecause, redact}, events::{ + AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnySyncStateEvent, + GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType, SyncStateEvent, presence::PresenceEvent, receipt::{Receipt, ReceiptThread, ReceiptType}, room::member::{ MembershipState, RoomMemberEventContent, StrippedRoomMemberEvent, SyncRoomMemberEvent, }, - AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnySyncStateEvent, - GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType, SyncStateEvent, }, serde::Raw, - CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, - OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId, }; -use serde::{de::DeserializeOwned, ser::Error, Deserialize, Serialize}; +use serde::{Deserialize, Serialize, de::DeserializeOwned, ser::Error}; use tracing::{debug, warn}; use wasm_bindgen::JsValue; @@ -372,12 +372,12 @@ impl IndexeddbStateStore { .open_cursor() .with_direction(CursorDirection::Prev) .await? + && let Some(record) = cursor.next_record::().await? { - if let Some(record) = cursor.next_record::().await? { - return Ok(record.as_string()); - } + Ok(record.as_string()) + } else { + Ok(None) } - Ok(None) } /// Encrypt (if needs be) then JSON-serialize a value. @@ -1044,32 +1044,38 @@ impl_state_store!({ }; let raw_evt = self.deserialize_value::>(&value)?; - if let Ok(Some(event_id)) = raw_evt.get_field::("event_id") { - if let Some(redaction) = redactions.get(&event_id) { - let redaction_rules = { - if redaction_rules.is_none() { - redaction_rules.replace(room_info - .get(&self.encode_key(keys::ROOM_INFOS, room_id)) - .await? - .and_then(|f| self.deserialize_value::(&f).ok()) - .map(|info| info.room_version_rules_or_default()) - .unwrap_or_else(|| { - warn!(?room_id, "Unable to get the room version rules, defaulting to rules for room version {ROOM_VERSION_FALLBACK}"); - ROOM_VERSION_RULES_FALLBACK - }).redaction - ); - } - redaction_rules.as_ref().unwrap() - }; + if let Ok(Some(event_id)) = raw_evt.get_field::("event_id") + && let Some(redaction) = redactions.get(&event_id) + { + let redaction_rules = match &redaction_rules { + Some(r) => r, + None => { + let value = room_info + .get(&self.encode_key(keys::ROOM_INFOS, room_id)) + .await? + .and_then(|f| self.deserialize_value::(&f).ok()) + .map(|info| info.room_version_rules_or_default()) + .unwrap_or_else(|| { + warn!( + ?room_id, + "Unable to get the room version rules, \ + defaulting to rules for room version \ + {ROOM_VERSION_FALLBACK}" + ); + ROOM_VERSION_RULES_FALLBACK + }) + .redaction; + redaction_rules.get_or_insert(value) + } + }; - let redacted = redact( - raw_evt.deserialize_as::()?, - redaction_rules, - Some(RedactedBecause::from_raw_event(redaction)?), - ) - .map_err(StoreError::Redaction)?; - state.put(&self.serialize_value(&redacted)?).with_key(key).build()?; - } + let redacted = redact( + raw_evt.deserialize_as::()?, + redaction_rules, + Some(RedactedBecause::from_raw_event(redaction)?), + ) + .map_err(StoreError::Redaction)?; + state.put(&self.serialize_value(&redacted)?).with_key(key).build()?; } } } @@ -2103,8 +2109,8 @@ mod migration_tests { use assert_matches2::assert_matches; use matrix_sdk_base::store::{QueuedRequestKind, SerializableEventContent}; use ruma::{ - events::room::message::RoomMessageEventContent, room_id, OwnedRoomId, OwnedTransactionId, - TransactionId, + OwnedRoomId, OwnedTransactionId, TransactionId, + events::room::message::RoomMessageEventContent, room_id, }; use serde::{Deserialize, Serialize}; diff --git a/crates/matrix-sdk-indexeddb/src/transaction/mod.rs b/crates/matrix-sdk-indexeddb/src/transaction/mod.rs index 03074190e..ac1836a7d 100644 --- a/crates/matrix-sdk-indexeddb/src/transaction/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/transaction/mod.rs @@ -20,12 +20,12 @@ use futures_util::TryStreamExt; use indexed_db_futures::{ - cursor::CursorDirection, internals::SystemRepr, query_source::QuerySource, - transaction as inner, BuildSerde, + BuildSerde, cursor::CursorDirection, internals::SystemRepr, query_source::QuerySource, + transaction as inner, }; use serde::{ - de::{DeserializeOwned, Error}, Serialize, + de::{DeserializeOwned, Error}, }; use thiserror::Error; use wasm_bindgen::JsValue; @@ -33,9 +33,9 @@ use wasm_bindgen::JsValue; use crate::{ error::{AsyncErrorDeps, GenericError}, serializer::indexed_type::{ + IndexedTypeSerializer, range::IndexedKeyRange, traits::{Indexed, IndexedKey}, - IndexedTypeSerializer, }, }; diff --git a/crates/matrix-sdk-search/src/encrypted/encrypted_dir.rs b/crates/matrix-sdk-search/src/encrypted/encrypted_dir.rs index a00ea91d1..ce3afa541 100644 --- a/crates/matrix-sdk-search/src/encrypted/encrypted_dir.rs +++ b/crates/matrix-sdk-search/src/encrypted/encrypted_dir.rs @@ -13,7 +13,7 @@ // limitations under the License. use std::{ - fs::File, + fs::{File, create_dir_all}, io::{BufWriter, Cursor, Error as IoError, ErrorKind, Read, Write}, path::Path, sync::Arc, @@ -413,10 +413,13 @@ impl EncryptedMmapDirectory { passphrase: &str, pbkdf_count: u32, ) -> Result { + let dir_path = key_path.parent().unwrap_or(key_path); + + create_dir_all(dir_path).map_err(|err| err.into_tv_err(dir_path))?; // Derive a AES key from our passphrase using a randomly generated salt // to prevent bruteforce attempts using rainbow tables. let (key, hmac_key, salt) = EncryptedMmapDirectory::derive_key(passphrase, pbkdf_count) - .map_err(|err| err.into_tv_err(key_path))?; + .map_err(|err| err.into_tv_err(dir_path))?; // Generate a new random store key. This key will encrypt our Tantivy // indexing files. The key itself is stored encrypted using the derived // key. @@ -696,4 +699,13 @@ mod tests { let _ = EncryptedMmapDirectory::open(tmpdir.path(), "password") .expect("Can't open the store with the new passphrase"); } + + #[test] + fn create_store_in_nonexistent_directory() { + let tmpdir = tempdir().unwrap(); + let nested_path = tmpdir.path().join("nested").join("directory"); + let dir = EncryptedMmapDirectory::open_or_create(&nested_path, "password", PBKDF_COUNT) + .expect("Should create store in non-existent nested directory"); + drop(dir); + } } diff --git a/crates/matrix-sdk-sqlite/src/crypto_store.rs b/crates/matrix-sdk-sqlite/src/crypto_store.rs index 28fecd934..ef24833fb 100644 --- a/crates/matrix-sdk-sqlite/src/crypto_store.rs +++ b/crates/matrix-sdk-sqlite/src/crypto_store.rs @@ -125,6 +125,8 @@ impl SqliteCryptoStore { debug!("Opened sqlite store with version {}", version); run_migrations(&conn, version).await?; + conn.wal_checkpoint().await; + let store_cipher = match secret { Some(s) => Some(Arc::new(conn.get_or_create_store_cipher(s).await?)), None => None, diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index bad1489a5..73eee0a9e 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -149,8 +149,11 @@ impl SqliteEventCacheStore { let conn = pool.get().await?; let version = conn.db_version().await?; + run_migrations(&conn, version).await?; + conn.wal_checkpoint().await; + let store_cipher = match secret { Some(s) => Some(Arc::new(conn.get_or_create_store_cipher(s).await?)), None => None, diff --git a/crates/matrix-sdk-sqlite/src/media_store.rs b/crates/matrix-sdk-sqlite/src/media_store.rs index 9584b0365..ae6c229bd 100644 --- a/crates/matrix-sdk-sqlite/src/media_store.rs +++ b/crates/matrix-sdk-sqlite/src/media_store.rs @@ -143,6 +143,8 @@ impl SqliteMediaStore { let version = conn.db_version().await?; run_migrations(&conn, version).await?; + conn.wal_checkpoint().await; + let store_cipher = match secret { Some(s) => Some(Arc::new(conn.get_or_create_store_cipher(s).await?)), None => None, diff --git a/crates/matrix-sdk-sqlite/src/state_store.rs b/crates/matrix-sdk-sqlite/src/state_store.rs index 5dbc235b9..a760781d6 100644 --- a/crates/matrix-sdk-sqlite/src/state_store.rs +++ b/crates/matrix-sdk-sqlite/src/state_store.rs @@ -160,6 +160,8 @@ impl SqliteStateStore { }; this.run_migrations(version, None).await?; + this.read().await?.wal_checkpoint().await; + Ok(this) } diff --git a/crates/matrix-sdk-sqlite/src/utils.rs b/crates/matrix-sdk-sqlite/src/utils.rs index d9265a77c..06a33a562 100644 --- a/crates/matrix-sdk-sqlite/src/utils.rs +++ b/crates/matrix-sdk-sqlite/src/utils.rs @@ -187,6 +187,8 @@ pub(crate) trait SqliteAsyncConnExt { /// /// Only returns an error in tests, otherwise the error is only logged. async fn vacuum(&self) -> Result<()> { + // Truncate the WAL file before vacuuming so it has room to grow. + self.wal_checkpoint().await; if let Err(error) = self.execute_batch("VACUUM").await { // Since this is an optimisation step, do not propagate the error // but log it. @@ -198,11 +200,24 @@ pub(crate) trait SqliteAsyncConnExt { return Err(error.into()); } else { trace!("VACUUM complete"); + // Once vacuumed, truncate the WAL file again to purge the copied DB contents. + self.wal_checkpoint().await; } Ok(()) } + /// Adds a manual [WAL checkpoint] to copy back the contents of the WAL + /// files into the actual database, resetting the write-ahead log. + /// + /// [WAL checkpoint]: https://sqlite.org/c3ref/wal_checkpoint.html + async fn wal_checkpoint(&self) { + match self.execute_batch("PRAGMA wal_checkpoint(TRUNCATE);").await { + Ok(_) => trace!("WAL checkpoint completed"), + Err(error) => error!(?error, "WAL checkpoint error"), + } + } + async fn get_db_size(&self) -> Result { let page_size = self.query_row("PRAGMA page_size;", (), |row| row.get::<_, usize>(0)).await?; diff --git a/crates/matrix-sdk-ui/CHANGELOG.md b/crates/matrix-sdk-ui/CHANGELOG.md index 73ff21474..668b79f97 100644 --- a/crates/matrix-sdk-ui/CHANGELOG.md +++ b/crates/matrix-sdk-ui/CHANGELOG.md @@ -18,6 +18,10 @@ All notable changes to this project will be documented in this file. ### Features +- [**breaking**] `EventTimelineItem::get_shield` now returns a new type, + `TimelineEventShieldState`, which extends the old `ShieldState` with a code + for `SentInClear`, now that the latter has been removed from `ShieldState`. + ([#5959](https://github.com/matrix-org/matrix-rust-sdk/pull/5959)) - Add `SpaceService::get_space_room` to get a space given its id from the space graph if available. ([#5944](https://github.com/matrix-org/matrix-rust-sdk/pull/5944)) @@ -28,7 +32,11 @@ All notable changes to this project will be documented in this file. ([#5624](https://github.com/matrix-org/matrix-rust-sdk/pull/5624/)) - `Room::load_event_with_relations` now also calls `/relations` to fetch related events when falling back to network mode after a cache miss. - ([#5930](https://github.com/matrix-org/matrix-rust-sdk/pull/5930)) + ([#5930](https://github.com/matrix-org/matrix-rust-sdk/pull/5930)) +- Expose `EventTimelineItem::forwarder` and `forwarder_profile`, which, if present, provide the ID and profile of + the user who forwarded the keys used to decrypt the event as part of an [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268) + key bundle. + ([#6000](https://github.com/matrix-org/matrix-rust-sdk/pull/6000)) ### Refactor diff --git a/crates/matrix-sdk-ui/src/timeline/controller/aggregations.rs b/crates/matrix-sdk-ui/src/timeline/controller/aggregations.rs index 47d3d9df5..a44210ea3 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/aggregations.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/aggregations.rs @@ -387,7 +387,24 @@ impl Aggregations { } self.inverted_map.insert(aggregation.own_id.clone(), related_to.clone()); - self.related_events.entry(related_to).or_default().push(aggregation); + + // We can have 3 different states for the same aggregation in related_events, in + // chronological order: + // + // 1. The local echo with a transaction ID. + // 2. The local echo with the event ID returned by the server after sending the + // event. + // 3. The remote echo received via sync. + // + // The transition from states 1 to 2 is handled in `mark_aggregation_as_sent()`. + // So here we need to handle the transition from states 2 to 3. We need to + // replace the local echo by the remote echo, which might have more data, like + // the raw JSON. + let related_events = self.related_events.entry(related_to).or_default(); + if let Some(pos) = related_events.iter().position(|agg| agg.own_id == aggregation.own_id) { + related_events.remove(pos); + } + related_events.push(aggregation); } /// Is the given id one for a known aggregation to another event? diff --git a/crates/matrix-sdk-ui/src/timeline/controller/decryption_retry_task.rs b/crates/matrix-sdk-ui/src/timeline/controller/decryption_retry_task.rs index 68fa53bb0..c88c82956 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/decryption_retry_task.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/decryption_retry_task.rs @@ -236,6 +236,8 @@ mod tests { TimelineItemKind::Event(EventTimelineItem::new( owned_user_id!("@u:s.to"), TimelineDetails::Pending, + None, + None, timestamp(), TimelineItemContent::MsgLike(MsgLikeContent::redacted()), event_kind, @@ -262,6 +264,8 @@ mod tests { TimelineItemKind::Event(EventTimelineItem::new( owned_user_id!("@u:s.to"), TimelineDetails::Pending, + None, + None, timestamp(), TimelineItemContent::MsgLike(MsgLikeContent::unable_to_decrypt( EncryptedMessage::from_content( @@ -315,6 +319,8 @@ mod tests { TimelineItemKind::Event(EventTimelineItem::new( owned_user_id!("@u:s.to"), TimelineDetails::Pending, + None, + None, timestamp(), TimelineItemContent::message( content.msgtype, 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 409679fdb..76735ffb4 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -734,6 +734,8 @@ mod observable_items_tests { EventTimelineItem::new( owned_user_id!("@ivan:mnt.io"), TimelineDetails::Unavailable, + None, + None, MilliSecondsSinceUnixEpoch(0u32.into()), TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(Message { @@ -768,6 +770,8 @@ mod observable_items_tests { EventTimelineItem::new( owned_user_id!("@ivan:mnt.io"), TimelineDetails::Unavailable, + None, + None, MilliSecondsSinceUnixEpoch(0u32.into()), TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(Message { diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index ba0e813b3..47838fb5a 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -180,6 +180,8 @@ impl TimelineState

{ let ctx = TimelineEventContext { sender: own_user_id, sender_profile: own_profile, + forwarder: None, + forwarder_profile: None, timestamp: MilliSecondsSinceUnixEpoch::now(), read_receipts: Default::default(), // An event sent by ourselves is never matched against push rules. diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs b/crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs index 5c257d69e..f836a4dee 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs @@ -226,9 +226,15 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> { | Some(action @ TimelineAction::HandleAggregation { .. }) => { let encryption_info = event.kind.encryption_info().cloned(); let sender_profile = room_data_provider.profile_from_user_id(&sender).await; + + let (forwarder, forwarder_profile) = + get_forwarder_info(&event, room_data_provider).await; + let mut ctx = TimelineEventContext { sender, sender_profile, + forwarder, + forwarder_profile, timestamp, // These are not used when handling an aggregation. read_receipts: Default::default(), @@ -680,9 +686,9 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> { let is_highlighted = event.push_actions().is_some_and(|actions| actions.iter().any(Action::is_highlight)); - let thread_summary = if let ThreadSummaryStatus::Some(summary) = event.thread_summary { - let latest_reply_item = if let Some(latest_reply) = summary.latest_reply { - self.fetch_latest_thread_reply(&latest_reply, room_data_provider).await + let thread_summary = if let ThreadSummaryStatus::Some(ref summary) = event.thread_summary { + let latest_reply_item = if let Some(ref latest_reply) = summary.latest_reply { + self.fetch_latest_thread_reply(latest_reply, room_data_provider).await } else { None }; @@ -700,6 +706,8 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> { map.get(&UnsignedEventLocation::RelationsReplace)?.encryption_info().cloned() }); + let (forwarder, forwarder_profile) = get_forwarder_info(&event, room_data_provider).await; + let (raw, utd_info) = match event.kind { TimelineEventKind::UnableToDecrypt { utd_info, event } => (event, Some(utd_info)), _ => (event.kind.into_raw(), None), @@ -794,6 +802,8 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> { let ctx = TimelineEventContext { sender, sender_profile, + forwarder, + forwarder_profile, timestamp, read_receipts: if settings.track_read_receipts.is_enabled() && should_add @@ -1029,3 +1039,34 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> { } } } + +/// Retrieves the forwarder information for a given timeline event. +/// +/// # Parameters +/// +/// - `event`: The timeline event to extract forwarder information from. +/// - `room_data_provider`: A reference to the room data provider. +/// +/// # Returns +/// +/// A tuple containing: +/// - `Option`: The user ID of the forwarder, if available. +/// - `Option`: The profile of the forwarder, if available. +async fn get_forwarder_info( + event: &TimelineEvent, + room_data_provider: &P, +) -> (Option, Option) { + let forwarder = event + .kind + .encryption_info() + .and_then(|info| info.forwarder.as_ref()) + .map(|info| info.user_id.clone()); + + let forwarder_profile = if let Some(ref forwarder_id) = forwarder { + Some(room_data_provider.profile_from_user_id(forwarder_id).await) + } else { + None + }; + + (forwarder, forwarder_profile.flatten()) +} diff --git a/crates/matrix-sdk-ui/src/timeline/date_dividers.rs b/crates/matrix-sdk-ui/src/timeline/date_dividers.rs index 54d766220..6d7b4d842 100644 --- a/crates/matrix-sdk-ui/src/timeline/date_dividers.rs +++ b/crates/matrix-sdk-ui/src/timeline/date_dividers.rs @@ -683,6 +683,8 @@ mod tests { EventTimelineItem::new( owned_user_id!("@alice:example.org"), crate::timeline::TimelineDetails::Pending, + None, + None, timestamp, TimelineItemContent::MsgLike(MsgLikeContent::redacted()), event_kind, diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 454fe9660..48f93dcc7 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -108,6 +108,16 @@ impl Flow { pub(super) struct TimelineEventContext { pub(super) sender: OwnedUserId, pub(super) sender_profile: Option, + /// If the keys used to decrypt this event were shared-on-invite as part of + /// an [MSC4268] key bundle, the user ID of the forwarder. + /// + /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268 + pub(super) forwarder: Option, + /// If the keys used to decrypt this event were shared-on-invite as part of + /// an [MSC4268] key bundle, the forwarder's profile. + /// + /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268 + pub(super) forwarder_profile: Option, /// The event's `origin_server_ts` field (or creation time for local echo). pub(super) timestamp: MilliSecondsSinceUnixEpoch, pub(super) read_receipts: IndexMap, @@ -762,6 +772,14 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { fn add_item(&mut self, content: TimelineItemContent) { let sender = self.ctx.sender.to_owned(); let sender_profile = TimelineDetails::from_initial_value(self.ctx.sender_profile.clone()); + + let forwarder = self.ctx.forwarder.to_owned(); + let forwarder_profile = self + .ctx + .forwarder + .as_ref() + .map(|_| TimelineDetails::from_initial_value(self.ctx.forwarder_profile.clone())); + let timestamp = self.ctx.timestamp; let kind: EventTimelineItemKind = match &self.ctx.flow { @@ -808,6 +826,8 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { let item = EventTimelineItem::new( sender, sender_profile, + forwarder, + forwarder_profile, timestamp, content, kind, 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 97595a4fb..e2afdb31b 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -24,7 +24,7 @@ use matrix_sdk::{ deserialized_responses::{EncryptionInfo, ShieldState}, send_queue::{SendHandle, SendReactionHandle}, }; -use matrix_sdk_base::deserialized_responses::{SENT_IN_CLEAR, ShieldStateCode}; +use matrix_sdk_base::deserialized_responses::ShieldStateCode; use once_cell::sync::Lazy; use ruma::{ EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedTransactionId, @@ -67,6 +67,16 @@ pub struct EventTimelineItem { pub(super) sender: OwnedUserId, /// The sender's profile of the event. pub(super) sender_profile: TimelineDetails, + /// If the keys used to decrypt this event were shared-on-invite as part of + /// an [MSC4268] key bundle, the user ID of the forwarder. + /// + /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268 + pub(super) forwarder: Option, + /// If the keys used to decrypt this event were shared-on-invite as part of + /// an [MSC4268] key bundle, the forwarder's profile, if present. + /// + /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268 + pub(super) forwarder_profile: Option>, /// The timestamp of the event. pub(super) timestamp: MilliSecondsSinceUnixEpoch, /// The content of the event. @@ -108,15 +118,27 @@ pub(crate) enum TimelineItemHandle<'a> { } impl EventTimelineItem { + #[allow(clippy::too_many_arguments)] pub(super) fn new( sender: OwnedUserId, sender_profile: TimelineDetails, + forwarder: Option, + forwarder_profile: Option>, timestamp: MilliSecondsSinceUnixEpoch, content: TimelineItemContent, kind: EventTimelineItemKind, is_room_encrypted: bool, ) -> Self { - Self { sender, sender_profile, timestamp, content, kind, is_room_encrypted } + Self { + sender, + sender_profile, + forwarder, + forwarder_profile, + timestamp, + content, + kind, + is_room_encrypted, + } } /// Check whether this item is a local echo. @@ -216,6 +238,22 @@ impl EventTimelineItem { &self.sender_profile } + /// If the keys used to decrypt this event were shared-on-invite as part of + /// an [MSC4268] key bundle, returns the user ID of the forwarder. + /// + /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268 + pub fn forwarder(&self) -> Option<&UserId> { + self.forwarder.as_deref() + } + + /// If the keys used to decrypt this event were shared-on-invite as part of + /// an [MSC4268] key bundle, returns the profile of the forwarder. + /// + /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268 + pub fn forwarder_profile(&self) -> Option<&TimelineDetails> { + self.forwarder_profile.as_ref() + } + /// Get the content of this item. pub fn content(&self) -> &TimelineItemContent { &self.content @@ -309,30 +347,29 @@ impl EventTimelineItem { } } - /// Gets the [`ShieldState`] which can be used to decorate messages in the - /// recommended way. - pub fn get_shield(&self, strict: bool) -> Option { + /// Gets the [`TimelineEventShieldState`] which can be used to decorate + /// messages in the recommended way. + pub fn get_shield(&self, strict: bool) -> TimelineEventShieldState { if !self.is_room_encrypted || self.is_local_echo() { - return None; + return TimelineEventShieldState::None; } // An unable-to-decrypt message has no authenticity shield. if self.content().is_unable_to_decrypt() { - return None; + return TimelineEventShieldState::None; } match self.encryption_info() { Some(info) => { if strict { - Some(info.verification_state.to_shield_state_strict()) + info.verification_state.to_shield_state_strict().into() } else { - Some(info.verification_state.to_shield_state_lax()) + info.verification_state.to_shield_state_lax().into() } } - None => Some(ShieldState::Red { - code: ShieldStateCode::SentInClear, - message: SENT_IN_CLEAR, - }), + None => { + TimelineEventShieldState::Red { code: TimelineEventShieldStateCode::SentInClear } + } } } @@ -450,6 +487,8 @@ impl EventTimelineItem { Self { sender: self.sender.clone(), sender_profile: self.sender_profile.clone(), + forwarder: self.forwarder.clone(), + forwarder_profile: self.forwarder_profile.clone(), timestamp: self.timestamp, content, kind, @@ -693,3 +732,71 @@ impl ReactionsByKeyBySender { None } } + +/// Extends [`ShieldState`] to allow for a `SentInClear` code. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TimelineEventShieldState { + /// A red shield with a tooltip containing a message appropriate to the + /// associated code should be presented. + Red { + /// A machine-readable representation. + code: TimelineEventShieldStateCode, + }, + /// A grey shield with a tooltip containing a message appropriate to the + /// associated code should be presented. + Grey { + /// A machine-readable representation. + code: TimelineEventShieldStateCode, + }, + /// No shield should be presented. + None, +} + +impl From for TimelineEventShieldState { + fn from(value: ShieldState) -> Self { + match value { + ShieldState::Red { code, message: _ } => { + TimelineEventShieldState::Red { code: code.into() } + } + ShieldState::Grey { code, message: _ } => { + TimelineEventShieldState::Grey { code: code.into() } + } + ShieldState::None => TimelineEventShieldState::None, + } + } +} + +/// Extends [`ShieldStateCode`] to allow for a `SentInClear` code. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] +pub enum TimelineEventShieldStateCode { + /// Not enough information available to check the authenticity. + AuthenticityNotGuaranteed, + /// The sending device isn't yet known by the Client. + UnknownDevice, + /// The sending device hasn't been verified by the sender. + UnsignedDevice, + /// The sender hasn't been verified by the Client's user. + UnverifiedIdentity, + /// The sender was previously verified but changed their identity. + VerificationViolation, + /// The `sender` field on the event does not match the owner of the device + /// that established the Megolm session. + MismatchedSender, + /// An unencrypted event in an encrypted room. + SentInClear, +} + +impl From for TimelineEventShieldStateCode { + fn from(value: ShieldStateCode) -> Self { + use TimelineEventShieldStateCode::*; + match value { + ShieldStateCode::AuthenticityNotGuaranteed => AuthenticityNotGuaranteed, + ShieldStateCode::UnknownDevice => UnknownDevice, + ShieldStateCode::UnsignedDevice => UnsignedDevice, + ShieldStateCode::UnverifiedIdentity => UnverifiedIdentity, + ShieldStateCode::VerificationViolation => VerificationViolation, + ShieldStateCode::MismatchedSender => MismatchedSender, + } + } +} diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index af8bec3fc..741da8f15 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -97,7 +97,8 @@ pub use self::{ MemberProfileChange, MembershipChange, Message, MsgLikeContent, MsgLikeKind, OtherMessageLike, OtherState, PollResult, PollState, Profile, ReactionInfo, ReactionStatus, ReactionsByKeyBySender, RoomMembershipChange, RoomPinnedEventsChange, Sticker, - ThreadSummary, TimelineDetails, TimelineEventItemId, TimelineItemContent, + ThreadSummary, TimelineDetails, TimelineEventItemId, TimelineEventShieldState, + TimelineEventShieldStateCode, TimelineItemContent, }, event_type_filter::TimelineEventTypeFilter, item::{TimelineItem, TimelineItemKind, TimelineUniqueId}, diff --git a/crates/matrix-sdk-ui/src/timeline/tests/shields.rs b/crates/matrix-sdk-ui/src/timeline/tests/shields.rs index ee25822fc..25901d21d 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/shields.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/shields.rs @@ -1,6 +1,5 @@ use assert_matches::assert_matches; use eyeball_im::VectorDiff; -use matrix_sdk_base::deserialized_responses::{ShieldState, ShieldStateCode}; use matrix_sdk_test::{ALICE, async_test, event_factory::EventFactory}; use ruma::{ event_id, @@ -17,7 +16,7 @@ use ruma::{ use stream_assert::{assert_next_matches, assert_pending}; use crate::timeline::{ - EventSendState, + EventSendState, TimelineEventShieldState, TimelineEventShieldStateCode, tests::{TestTimeline, TestTimelineBuilder}, }; @@ -31,7 +30,7 @@ async fn test_no_shield_in_unencrypted_room() { let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); let shield = item.as_event().unwrap().get_shield(false); - assert!(shield.is_none()); + assert_eq!(shield, TimelineEventShieldState::None); } #[async_test] @@ -46,7 +45,7 @@ async fn test_sent_in_clear_shield() { let shield = item.as_event().unwrap().get_shield(false); assert_eq!( shield, - Some(ShieldState::Red { code: ShieldStateCode::SentInClear, message: "Not encrypted." }) + TimelineEventShieldState::Red { code: TimelineEventShieldStateCode::SentInClear } ); } @@ -75,7 +74,7 @@ async fn test_local_sent_in_clear_shield() { // available). assert!(event_item.is_local_echo()); let shield = event_item.get_shield(false); - assert_eq!(shield, None); + assert_eq!(shield, TimelineEventShieldState::None); { // The date divider comes in late. @@ -96,7 +95,7 @@ async fn test_local_sent_in_clear_shield() { // Then the local echo still should not have a shield. assert!(event_item.is_local_echo()); let shield = event_item.get_shield(false); - assert_eq!(shield, None); + assert_eq!(shield, TimelineEventShieldState::None); // When the remote echo comes in. timeline @@ -118,7 +117,7 @@ async fn test_local_sent_in_clear_shield() { let shield = event_item.get_shield(false); assert_eq!( shield, - Some(ShieldState::Red { code: ShieldStateCode::SentInClear, message: "Not encrypted." }) + TimelineEventShieldState::Red { code: TimelineEventShieldStateCode::SentInClear } ); // Date divider is adjusted. @@ -168,5 +167,5 @@ async fn test_utd_shield() { // 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()); + assert_eq!(shield, TimelineEventShieldState::None); } 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 c0a698245..30af36bc7 100644 --- a/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs @@ -47,14 +47,14 @@ async fn test_smoke_encryption_sync_works() -> anyhow::Result<()> { // Requests enable the e2ee and to_device extensions on the first run. sliding_sync_then_assert_request_and_fake_response! { [server, stream] - assert request = { + assert request >= { "conn_id": "encryption", "extensions": { "e2ee": { - "enabled": true + "enabled": true, }, "to_device": { - "enabled": true + "enabled": true, } } }, @@ -64,33 +64,39 @@ async fn test_smoke_encryption_sync_works() -> anyhow::Result<()> { }; // The request then passes the `pos`ition marker to the next request, as usual - // in sliding sync. The extensions haven't changed, so they're not updated - // (sticky parameters ftw). + // in sliding sync. sliding_sync_then_assert_request_and_fake_response! { [server, stream] - assert request = { + assert request >= { "conn_id": "encryption", + "extensions": { + "e2ee": { + "enabled": true, + }, + "to_device": { + "enabled": true, + } + } }, respond with = { "pos": "1", "extensions": { "to_device": { - "next_batch": "nb0" + "next_batch": "nb0", } } }, }; // The to-device since token is passed from the previous request. - // The extensions haven't changed, so they're not updated (sticky parameters - // ftw). sliding_sync_then_assert_request_and_fake_response! { [server, stream] - assert request = { + assert request >= { "conn_id": "encryption", "extensions": { "to_device": { - "since": "nb0" + "enabled": true, + "since": "nb0", } } }, @@ -105,18 +111,15 @@ async fn test_smoke_encryption_sync_works() -> anyhow::Result<()> { }; // The to-device since token is passed from the previous request. - // The extensions haven't changed, so they're not updated (sticky parameters - // ftw)... in the first request. Then, the sliding sync instance will retry - // those requests, so it will include them again; as a matter of fact, the - // last request that we assert against will contain those. sliding_sync_then_assert_request_and_fake_response! { [server, stream] sync matches Some(Err(_)), - assert request = { + assert request >= { "conn_id": "encryption", "extensions": { "to_device": { - "since": "nb1" + "enabled": true, + "since": "nb1", } } }, @@ -134,15 +137,12 @@ async fn test_smoke_encryption_sync_works() -> anyhow::Result<()> { let stream = encryption_sync.sync(sync_permit_guard); pin_mut!(stream); - // The next request will contain sticky parameters again. + // The next request will contain extensions again. sliding_sync_then_assert_request_and_fake_response! { [server, stream] - assert request = { + assert request >= { "conn_id": "encryption", "extensions": { - "e2ee": { - "enabled": true - }, "to_device": { "enabled": true, "since": "nb1" @@ -219,8 +219,6 @@ async fn test_encryption_sync_two_fixed_iterations() -> anyhow::Result<()> { encryption_sync.run_fixed_iterations(2, sync_permit_guard).await?; - // First iteration fills the whole request. - // Second iteration only sends non-sticky parameters. let expected_requests = [ json!({ "conn_id": "encryption", @@ -235,6 +233,14 @@ async fn test_encryption_sync_two_fixed_iterations() -> anyhow::Result<()> { }), json!({ "conn_id": "encryption", + "extensions": { + "e2ee": { + "enabled": true + }, + "to_device": { + "enabled": true + } + } }), ]; @@ -280,14 +286,17 @@ async fn test_encryption_sync_always_reloads_todevice_token() -> anyhow::Result< }, }; - // Second iteration only sends non-sticky parameters, plus the to-device token - // from the previous request. + // Second iteration contains the to-device token from the previous request. sliding_sync_then_assert_request_and_fake_response! { [server, stream] assert request = { "conn_id": "encryption", "extensions": { + "e2ee": { + "enabled": true + }, "to_device": { + "enabled": true, "since": "nb0", }, } @@ -321,7 +330,11 @@ async fn test_encryption_sync_always_reloads_todevice_token() -> anyhow::Result< assert request = { "conn_id": "encryption", "extensions": { + "e2ee": { + "enabled": true + }, "to_device": { + "enabled": true, "since": "nb2", }, } @@ -453,7 +466,7 @@ async fn test_notification_client_does_not_upload_duplicate_one_time_keys() -> a sliding_sync_then_assert_request_and_fake_response! { [server, stream] - assert request = { + assert request >= { "conn_id": "encryption", "extensions": { "to_device": { @@ -504,7 +517,7 @@ async fn test_notification_client_does_not_upload_duplicate_one_time_keys() -> a sliding_sync_then_assert_request_and_fake_response! { [server, stream] - assert request = { + assert request >= { "conn_id": "encryption", "extensions": { "to_device": { @@ -529,7 +542,7 @@ async fn test_notification_client_does_not_upload_duplicate_one_time_keys() -> a sliding_sync_then_assert_request_and_fake_response! { [server, stream] - assert request = { + assert request >= { "conn_id": "encryption", "extensions": { "to_device": { 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 82d0eeb95..7dddc47e7 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -417,7 +417,7 @@ async fn test_sync_all_states() -> Result<(), Error> { assert pos Some("0"), // Still no long-polling because the list isn't fully-loaded. assert timeout None, - assert request = { + assert request >= { "conn_id": "room-list", "lists": { ALL_ROOMS: { @@ -445,7 +445,7 @@ async fn test_sync_all_states() -> Result<(), Error> { assert pos Some("1"), // Still no long-polling because the list isn't fully-loaded. assert timeout None, - assert request = { + assert request >= { "conn_id": "room-list", "lists": { ALL_ROOMS: { @@ -474,7 +474,7 @@ async fn test_sync_all_states() -> Result<(), Error> { // Still no long-polling because the list isn't fully-loaded, // but it's about to be! assert timeout None, - assert request = { + assert request >= { "conn_id": "room-list", "lists": { ALL_ROOMS: { @@ -502,7 +502,7 @@ async fn test_sync_all_states() -> Result<(), Error> { assert pos Some("3"), // The list is fully-loaded, we can start long-polling. assert timeout Some(30000), - assert request = { + assert request >= { "conn_id": "room-list", "lists": { ALL_ROOMS: { @@ -2414,6 +2414,25 @@ async fn test_room_subscription() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 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.join_rules", ""], + ["m.room.tombstone", ""], + ["m.room.create", ""], + ["m.room.history_visibility", ""], + ["io.element.functional_members", ""], + ["m.space.parent", "*"], + ["m.space.child", "*"], + ], + "filters": {}, "timeline_limit": 1, }, }, diff --git a/crates/matrix-sdk-ui/tests/integration/sliding_sync.rs b/crates/matrix-sdk-ui/tests/integration/sliding_sync.rs index e31ccf3d7..81a10f989 100644 --- a/crates/matrix-sdk-ui/tests/integration/sliding_sync.rs +++ b/crates/matrix-sdk-ui/tests/integration/sliding_sync.rs @@ -27,7 +27,10 @@ pub(crate) async fn check_requests(server: MockServer, expected_requests: &[serd &expected_requests[num_requests], assert_json_diff::Config::new(assert_json_diff::CompareMode::Strict), ) { - panic!("{error}\n\njson_value = {json_value:?}"); + panic!( + "{error}\n\nexpected_requests[{num_requests}] = {expected_request}\n\njson_value = {json_value:?}", + expected_request = expected_requests[num_requests], + ); } num_requests += 1; diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index 55b634ea0..bf271f2db 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -312,16 +312,14 @@ async fn test_send_edit() { let hello_world_message = hello_world_item.content().as_message().unwrap(); assert!(!hello_world_message.is_edited()); assert!(hello_world_item.is_editable()); + assert_matches!(hello_world_item.original_json(), Some(_)); + assert_matches!(hello_world_item.latest_edit_json(), None); server.mock_room_send().ok(event_id!("$edit_event")).mock_once().mount().await; + let edit = RoomMessageEventContentWithoutRelation::text_plain("Hello, Room!"); timeline - .edit( - &hello_world_item.identifier(), - EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( - "Hello, Room!", - )), - ) + .edit(&hello_world_item.identifier(), EditedContent::RoomMessage(edit.clone())) .await .unwrap(); @@ -337,6 +335,31 @@ async fn test_send_edit() { let edit_message = edit_item.content().as_message().unwrap(); assert_eq!(edit_message.body(), "Hello, Room!"); assert!(edit_message.is_edited()); + assert_matches!(edit_item.original_json(), Some(_)); + // The local echo doesn't have the edit's JSON yet. + assert_matches!(edit_item.latest_edit_json(), None); + + // We receive the remote echo for the edit. + server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id).add_timeline_event( + f.text_msg("*Hello, Room!") + .sender(client.user_id().unwrap()) + .event_id(event_id!("$edit_event")) + .edit(hello_world_item.event_id().unwrap(), edit), + ), + ) + .await; + + let edit_item = + assert_next_matches!(timeline_stream, VectorDiff::Set { index: 0, value } => value); + let edit_message = edit_item.content().as_message().unwrap(); + assert_eq!(edit_message.body(), "Hello, Room!"); + assert!(edit_message.is_edited()); + assert_matches!(edit_item.original_json(), Some(_)); + // The remote echo populated the edit's JSON. + assert_matches!(edit_item.latest_edit_json(), Some(_)); // The response to the mocked endpoint does not generate further timeline // updates, so just wait for a bit before verifying that the endpoint was diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/media.rs b/crates/matrix-sdk-ui/tests/integration/timeline/media.rs index 9c408b7a8..aeada71c8 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/media.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/media.rs @@ -483,7 +483,7 @@ async fn test_send_gallery_from_bytes() -> TestResult { assert_let!(MediaSource::Plain(uri) = &file.source); assert!(uri.to_string().contains("localhost")); - (*index, progress.clone()) + (*index, *progress) }; // Eventually, the media is updated with the final MXC IDs… diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs index 4a38f8eb9..0a21f5cf8 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs @@ -30,8 +30,8 @@ use matrix_sdk_ui::{ Timeline, timeline::{ AnyOtherFullStateEventContent, Error, EventSendState, MsgLikeKind, OtherMessageLike, - RedactError, RoomExt, TimelineBuilder, TimelineEventItemId, TimelineFocus, - TimelineItemContent, VirtualTimelineItem, default_event_filter, + RedactError, RoomExt, TimelineBuilder, TimelineEventItemId, TimelineEventShieldState, + TimelineFocus, TimelineItemContent, VirtualTimelineItem, default_event_filter, }, }; use ruma::{ @@ -757,7 +757,7 @@ async fn test_timeline_without_encryption_info() { 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()); + assert_eq!(items[1].as_event().unwrap().get_shield(false), TimelineEventShieldState::None); } #[async_test] @@ -787,7 +787,7 @@ async fn test_timeline_without_encryption_can_update() { 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()); + assert_eq!(items[1].as_event().unwrap().get_shield(false), TimelineEventShieldState::None); let encryption_event_content = RoomEncryptionEventContent::with_recommended_defaults(); server @@ -805,17 +805,17 @@ async fn test_timeline_without_encryption_can_update() { // Previous timeline event now has a shield. assert_let!(VectorDiff::Set { index, value } = &timeline_updates[0]); assert_eq!(*index, 1); - assert!(value.as_event().unwrap().get_shield(false).is_some()); + assert_ne!(value.as_event().unwrap().get_shield(false), TimelineEventShieldState::None); // Room encryption event is received. assert_let!(VectorDiff::PushBack { value } = &timeline_updates[1]); 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()); + assert_ne!(value.as_event().unwrap().get_shield(false), TimelineEventShieldState::None); // New message event is received and has a shield. assert_let!(VectorDiff::PushBack { value } = &timeline_updates[2]); - assert!(value.as_event().unwrap().get_shield(false).is_some()); + assert_ne!(value.as_event().unwrap().get_shield(false), TimelineEventShieldState::None); assert_pending!(stream); } diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 8d126d565..473460f76 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -26,9 +26,13 @@ All notable changes to this project will be documented in this file. - Replace in-memory stores with IndexedDB implementations when initializing `Client` with `BuilderStoreConfig::IndexedDb`. [#5946](https://github.com/matrix-org/matrix-rust-sdk/pull/5946) +- Sliding Sync room subscriptions are sent once to save bandwidth. + ([#6002](https://github.com/matrix-org/matrix-rust-sdk/pull/6002)) ### Bugfix +- Add manual WAL checkpoints when opening Sqlite DBs and when vacuuming them, since the WAL files aren't automatically shrinking. ([#6004](https://github.com/matrix-org/matrix-rust-sdk/pull/6004)) +- Use the server name extracted from the user id in `Client::fetch_client_well_known` as a fallback value. Otherwise, sometimes the server name is not available and we can't reload the well-known contents. ([#5996](https://github.com/matrix-org/matrix-rust-sdk/pull/5996)) - Latest Event is lazier: a `RoomLatestEvents` can be registered even if its associated `RoomEventCache` isn't created yet. ([#5947](https://github.com/matrix-org/matrix-rust-sdk/pull/5947)) diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index f48102a55..dd18c8002 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -2045,23 +2045,61 @@ impl Client { } /// Fetches client well_known from network; no caching. + /// + /// 1. If the [`Client::server`] value is available, we use it to fetch the + /// well-known contents. + /// 2. If it's not, we try extracting the server name from the + /// [`Client::user_id`] and building the server URL from it. + /// 3. If we couldn't get the well-known contents with either the explicit + /// server name or the implicit extracted one, we try the homeserver URL + /// as a last resort. pub async fn fetch_client_well_known(&self) -> Option { - let server_url_string = self - .server() - .unwrap_or( - // Sometimes people configure their well-known directly on the homeserver so use - // this as a fallback when the server name is unknown. - &self.homeserver(), - ) - .to_string(); + let homeserver = self.homeserver(); + let scheme = homeserver.scheme(); + // Use the server name, either an explicit one or an implicit one taken from + // the user id: sometimes we'll have only the homeserver url available and no + // server name, but the server name can be extracted from the current user id. + let server_url = self + .server() + .map(|server| server.to_string()) + // If the server name wasn't available, extract it from the user id and build a URL: + // Reuse the same scheme as the homeserver url does, assuming if it's `http` there it + // will be the same for the public server url, lacking a better candidate. + .or_else(|| self.user_id().map(|id| format!("{}://{}", scheme, id.server_name()))); + + // If the server name is available, first try using it + let response = if let Some(server_url) = server_url { + // First try using the server name + self.fetch_client_well_known_with_url(server_url).await + } else { + None + }; + + // If we didn't get a well-known value yet, try with the homeserver url instead: + if response.is_none() { + // Sometimes people configure their well-known directly on the homeserver so use + // this as a fallback when the server name is unknown. + warn!( + "Fetching the well-known from the server name didn't work, using the homeserver url instead" + ); + self.fetch_client_well_known_with_url(homeserver.to_string()).await + } else { + response + } + } + + async fn fetch_client_well_known_with_url( + &self, + url: String, + ) -> Option { let well_known = self .inner .http_client .send( discover_homeserver::Request::new(), Some(RequestConfig::short_retry()), - server_url_string, + url, None, (), Default::default(), @@ -4524,4 +4562,43 @@ pub(crate) mod tests { assert_matches!(client.device_exists(owned_device_id!("ABCDEF")).await, Err(_)); } + + #[async_test] + async fn test_fetching_well_known_with_homeserver_url() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + server.mock_well_known().ok().mount().await; + + assert_matches!(client.fetch_client_well_known().await, Some(_)); + } + + #[async_test] + async fn test_fetching_well_known_with_server_name() { + let server = MatrixMockServer::new().await; + let server_name = ServerName::parse(server.server().address().to_string()).unwrap(); + + server.mock_well_known().ok().mount().await; + + let client = MockClientBuilder::new(None) + .on_builder(|builder| builder.insecure_server_name_no_tls(&server_name)) + .build() + .await; + + assert_matches!(client.fetch_client_well_known().await, Some(_)); + } + + #[async_test] + async fn test_fetching_well_known_with_domain_part_of_user_id() { + let server = MatrixMockServer::new().await; + server.mock_well_known().ok().mount().await; + + let user_id = + UserId::parse(format!("@user:{}", server.server().address())).expect("Invalid user id"); + let client = MockClientBuilder::new(None) + .logged_in_with_token("A_TOKEN".to_owned(), user_id, owned_device_id!("ABCDEF")) + .build() + .await; + + assert_matches!(client.fetch_client_well_known().await, Some(_)); + } } diff --git a/crates/matrix-sdk/src/sliding_sync/builder.rs b/crates/matrix-sdk/src/sliding_sync/builder.rs index 0d25319c7..888af82f3 100644 --- a/crates/matrix-sdk/src/sliding_sync/builder.rs +++ b/crates/matrix-sdk/src/sliding_sync/builder.rs @@ -11,10 +11,10 @@ use ruma::{OwnedRoomId, api::client::sync::sync_events::v5 as http}; use tokio::sync::{Mutex as AsyncMutex, RwLock as AsyncRwLock, broadcast::channel}; use super::{ - Error, SlidingSync, SlidingSyncInner, SlidingSyncListBuilder, SlidingSyncPositionMarkers, - Version, cache::format_storage_key_prefix, sticky_parameters::SlidingSyncStickyManager, + Error, RoomSubscriptionState, SlidingSync, SlidingSyncInner, SlidingSyncListBuilder, + SlidingSyncPositionMarkers, Version, cache::format_storage_key_prefix, }; -use crate::{Client, Result, sliding_sync::SlidingSyncStickyParameters}; +use crate::{Client, Result}; /// Configuration for a Sliding Sync instance. /// @@ -28,7 +28,7 @@ pub struct SlidingSyncBuilder { client: Client, lists: Vec, extensions: Option, - subscriptions: BTreeMap, + room_subscriptions: BTreeMap, poll_timeout: Duration, network_timeout: Duration, #[cfg(feature = "e2e-encryption")] @@ -50,7 +50,7 @@ impl SlidingSyncBuilder { client, lists: Vec::new(), extensions: None, - subscriptions: BTreeMap::new(), + room_subscriptions: BTreeMap::new(), poll_timeout: Duration::from_secs(30), network_timeout: Duration::from_secs(30), #[cfg(feature = "e2e-encryption")] @@ -288,12 +288,15 @@ impl SlidingSyncBuilder { position: Arc::new(AsyncMutex::new(SlidingSyncPositionMarkers { pos })), - sticky: StdRwLock::new(SlidingSyncStickyManager::new( - SlidingSyncStickyParameters::new( - self.subscriptions, - self.extensions.unwrap_or_default(), - ), - )), + room_subscriptions: StdRwLock::new( + self.room_subscriptions + .into_iter() + .map(|(room_id, room_subscription)| { + (room_id, (RoomSubscriptionState::Pending, room_subscription)) + }) + .collect(), + ), + extensions: self.extensions.unwrap_or_default(), internal_channel: internal_channel_sender, diff --git a/crates/matrix-sdk/src/sliding_sync/list/builder.rs b/crates/matrix-sdk/src/sliding_sync/list/builder.rs index 8f2ad1567..3ba5895f4 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/builder.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/builder.rs @@ -11,14 +11,11 @@ use ruma::{api::client::sync::sync_events::v5 as http, events::StateEventType}; use tokio::sync::broadcast::Sender; use super::{ - super::SlidingSyncInternalMessage, Bound, SlidingSyncList, SlidingSyncListCachePolicy, - SlidingSyncListInner, SlidingSyncListLoadingState, SlidingSyncListRequestGenerator, - SlidingSyncListStickyParameters, SlidingSyncMode, -}; -use crate::{ - Client, - sliding_sync::{cache::restore_sliding_sync_list, sticky_parameters::SlidingSyncStickyManager}, + super::{SlidingSyncInternalMessage, cache::restore_sliding_sync_list}, + Bound, SlidingSyncList, SlidingSyncListCachePolicy, SlidingSyncListInner, + SlidingSyncListLoadingState, SlidingSyncListRequestGenerator, SlidingSyncMode, }; +use crate::Client; /// Data that might have been read from the cache. #[derive(Clone)] @@ -213,9 +210,8 @@ impl SlidingSyncListBuilder { sync_mode: StdRwLock::new(self.sync_mode.clone()), // From the builder - sticky: StdRwLock::new(SlidingSyncStickyManager::new( - SlidingSyncListStickyParameters::new(self.required_state, self.filters), - )), + filters: self.filters, + required_state: self.required_state, 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 654307569..2fe7441e4 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/mod.rs @@ -1,7 +1,6 @@ mod builder; mod frozen; mod request_generator; -mod sticky; use std::{ fmt, @@ -11,18 +10,14 @@ use std::{ use eyeball::{SharedObservable, Subscriber}; use futures_core::Stream; -use ruma::{TransactionId, api::client::sync::sync_events::v5 as http, assign}; +use ruma::{api::client::sync::sync_events::v5 as http, assign, events::StateEventType}; use serde::{Deserialize, Serialize}; use tokio::sync::broadcast::Sender; use tracing::{instrument, warn}; pub use self::builder::*; -use self::sticky::SlidingSyncListStickyParameters; pub(super) use self::{frozen::FrozenSlidingSyncList, request_generator::*}; -use super::{ - Error, SlidingSyncInternalMessage, - sticky_parameters::{LazyTransactionId, SlidingSyncStickyManager}, -}; +use super::{Error, SlidingSyncInternalMessage}; use crate::Result; /// Should this [`SlidingSyncList`] be stored in the cache, and automatically @@ -149,11 +144,8 @@ impl SlidingSyncList { /// /// The next request is entirely calculated based on the request generator /// ([`SlidingSyncListRequestGenerator`]). - pub(super) fn next_request( - &self, - txn_id: &mut LazyTransactionId, - ) -> Result { - self.inner.next_request(txn_id) + pub(super) fn next_request(&self) -> Result { + self.inner.next_request() } /// Returns the current cache policy for this list. @@ -181,17 +173,6 @@ impl SlidingSyncList { Ok(new_changes) } - /// Commit the set of sticky parameters for this list. - pub fn maybe_commit_sticky(&mut self, txn_id: &TransactionId) { - self.inner.sticky.write().unwrap().maybe_commit(txn_id); - } - - /// Manually invalidate the sticky data, so the sticky parameters are - /// re-sent next time. - pub(super) fn invalidate_sticky_data(&self) { - let _ = self.inner.sticky.write().unwrap().data_mut(); - } - /// Get the sync-mode. #[cfg(feature = "testing")] pub fn sync_mode(&self) -> SlidingSyncMode { @@ -217,10 +198,11 @@ pub(super) struct SlidingSyncListInner { #[cfg(target_family = "wasm")] requires_timeout: Arc bool>, - /// Parameters that are sticky, and can be sent only once per session (until - /// the connection is dropped or the server invalidates what the client - /// knows). - sticky: StdRwLock>, + /// Any filters to apply to the query. + filters: Option, + + /// Required states to return per room. + required_state: Vec<(StateEventType, String)>, /// The maximum number of timeline events to query for. timeline_limit: StdRwLock, @@ -289,7 +271,7 @@ impl SlidingSyncListInner { } /// Update the state to the next request, and return it. - fn next_request(&self, txn_id: &mut LazyTransactionId) -> Result { + fn next_request(&self) -> Result { let ranges = { // Use a dedicated scope to ensure the lock is released before continuing. let mut request_generator = self.request_generator.write().unwrap(); @@ -297,22 +279,19 @@ impl SlidingSyncListInner { }; // Here we go. - Ok(self.request(ranges, txn_id)) + Ok(self.request(ranges)) } /// Build a [`http::request::List`] based on the current state of the /// request generator. #[instrument(skip(self), fields(name = self.name))] - fn request(&self, ranges: Ranges, txn_id: &mut LazyTransactionId) -> http::request::List { + fn request(&self, ranges: Ranges) -> http::request::List { 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); - } + request.filters = self.filters.clone(); + request.room_details.required_state = self.required_state.clone(); request } @@ -534,7 +513,7 @@ mod tests { use tokio::sync::broadcast::{channel, error::TryRecvError}; use super::{SlidingSyncList, SlidingSyncListLoadingState, SlidingSyncMode}; - use crate::sliding_sync::{SlidingSyncInternalMessage, sticky_parameters::LazyTransactionId}; + use crate::sliding_sync::SlidingSyncInternalMessage; macro_rules! assert_json_roundtrip { (from $type:ty: $rust_value:expr => $json_value:expr) => { @@ -625,7 +604,7 @@ mod tests { $( { // Generate a new request. - let request = $list.next_request(&mut LazyTransactionId::new()).unwrap(); + let request = $list.next_request().unwrap(); assert_eq!( request.ranges, @@ -1136,7 +1115,7 @@ mod tests { assert!(list.maximum_number_of_rooms().is_none()); // Simulate a request. - let _ = list.next_request(&mut LazyTransactionId::new()); + let _ = list.next_request(); let new_changes = list.update(Some(5)).unwrap(); assert!(new_changes); @@ -1144,7 +1123,7 @@ mod tests { assert_eq!(list.maximum_number_of_rooms(), Some(5)); // Simulate another request. - let _ = list.next_request(&mut LazyTransactionId::new()); + let _ = list.next_request(); let new_changes = list.update(Some(5)).unwrap(); assert!(!new_changes); diff --git a/crates/matrix-sdk/src/sliding_sync/list/sticky.rs b/crates/matrix-sdk/src/sliding_sync/list/sticky.rs deleted file mode 100644 index 960afa400..000000000 --- a/crates/matrix-sdk/src/sliding_sync/list/sticky.rs +++ /dev/null @@ -1,34 +0,0 @@ -use ruma::{api::client::sync::sync_events::v5 as http, events::StateEventType}; - -use crate::sliding_sync::sticky_parameters::StickyData; - -/// The set of `SlidingSyncList` request parameters that are *sticky*, as -/// defined by the [Sliding Sync MSC](https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/sync-v3/proposals/3575-sync.md). -#[derive(Debug)] -pub(super) struct SlidingSyncListStickyParameters { - /// Required states to return per room. - required_state: Vec<(StateEventType, String)>, - - /// Any filters to apply to the query. - filters: Option, -} - -impl SlidingSyncListStickyParameters { - pub fn new( - required_state: Vec<(StateEventType, String)>, - filters: Option, - ) -> Self { - // Consider that each list will have at least one parameter set, so invalidate - // it by default. - Self { required_state, filters } - } -} - -impl StickyData for SlidingSyncListStickyParameters { - type Request = http::request::List; - - fn apply(&self, request: &mut Self::Request) { - request.room_details.required_state = self.required_state.to_vec(); - request.filters = self.filters.clone(); - } -} diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 92c8db59b..866eeb6ca 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -20,7 +20,6 @@ mod cache; mod client; mod error; mod list; -mod sticky_parameters; use std::{ collections::{BTreeMap, btree_map::Entry}, @@ -49,11 +48,7 @@ use tokio::{ use tracing::{Instrument, Span, debug, error, info, instrument, trace, warn}; pub use self::{builder::*, client::VersionBuilderError, error::*, list::*}; -use self::{ - cache::restore_sliding_sync_state, - client::SlidingSyncResponseProcessor, - sticky_parameters::{LazyTransactionId, SlidingSyncStickyManager, StickyData}, -}; +use self::{cache::restore_sliding_sync_state, client::SlidingSyncResponseProcessor}; use crate::{Client, Result, config::RequestConfig}; /// The Sliding Sync instance. @@ -110,8 +105,14 @@ pub(super) struct SlidingSyncInner { /// The lists of this Sliding Sync instance. lists: AsyncRwLock>, - /// Request parameters that are sticky. - sticky: StdRwLock>, + /// Room subscriptions, i.e. rooms that may be out-of-scope of all lists + /// but one wants to receive updates. + room_subscriptions: + StdRwLock>, + + /// The intended state of the extensions being supplied to sliding /sync + /// calls. + extensions: http::request::Extensions, /// Internal channel used to pass messages between Sliding Sync and other /// types. @@ -145,8 +146,7 @@ impl SlidingSync { 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 room_subscriptions = &mut self.inner.room_subscriptions.write().unwrap(); let mut skip_over_current_sync_loop_iteration = false; @@ -305,12 +305,15 @@ impl SlidingSync { debug!("Sliding Sync response has been handled by the client"); trace!(?sync_response); - // Commit sticky parameters, if needed. - if let Some(ref txn_id) = sliding_sync_response.txn_id { - let txn_id = txn_id.as_str().into(); - self.inner.sticky.write().unwrap().maybe_commit(txn_id); - let mut lists = self.inner.lists.write().await; - lists.values_mut().for_each(|list| list.maybe_commit_sticky(txn_id)); + // All room subscriptions are marked as `Applied`. + { + let mut room_subscriptions = self.inner.room_subscriptions.write().unwrap(); + + for (state, _room_subscription) in room_subscriptions.values_mut() { + if matches!(state, RoomSubscriptionState::Pending) { + *state = RoomSubscriptionState::Applied; + } + } } let update_summary = { @@ -385,7 +388,6 @@ impl SlidingSync { #[instrument(skip_all)] async fn generate_sync_request( &self, - txn_id: &mut LazyTransactionId, ) -> Result<(http::Request, RequestConfig, OwnedMutexGuard)> { // Collect requests for lists. let mut requests_lists = BTreeMap::new(); @@ -397,7 +399,7 @@ impl SlidingSync { let mut require_timeout = true; for (name, list) in lists.iter() { - requests_lists.insert(name.clone(), list.next_request(txn_id)?); + requests_lists.insert(name.clone(), list.next_request()?); require_timeout = require_timeout && list.requires_timeout(); } @@ -421,8 +423,7 @@ impl SlidingSync { debug!(pos = ?position_guard.pos, "Got a position"); - let to_device_enabled = - self.inner.sticky.read().unwrap().data().extensions.to_device.enabled == Some(true); + let to_device_enabled = self.inner.extensions.to_device.enabled == Some(true); let restored_fields = if self.inner.share_pos || to_device_enabled { restore_sliding_sync_state(&self.inner.client, &self.inner.storage_key).await? @@ -483,22 +484,26 @@ impl SlidingSync { lists: requests_lists, }); - // Apply sticky parameters, if needs be. - self.inner.sticky.write().unwrap().maybe_apply(&mut request, txn_id); + // Add room subscriptions. + request.room_subscriptions = self + .inner + .room_subscriptions + .read() + .unwrap() + .iter() + .filter(|(_, (state, _))| matches!(state, RoomSubscriptionState::Pending)) + .map(|(room_id, (_, room_subscription))| (room_id.clone(), room_subscription.clone())) + .collect(); + + // Add extensions. + request.extensions = self.inner.extensions.clone(); - // 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); } - // Apply the transaction id if one was generated. - if let Some(txn_id) = txn_id.get() { - request.txn_id = Some(txn_id.to_string()); - } - Ok(( // The request itself. request, @@ -630,14 +635,13 @@ impl SlidingSync { /// Is the e2ee extension enabled for this sliding sync instance? #[cfg(feature = "e2e-encryption")] fn is_e2ee_enabled(&self) -> bool { - self.inner.sticky.read().unwrap().data().extensions.e2ee.enabled == Some(true) + self.inner.extensions.e2ee.enabled == Some(true) } /// Is the thread subscriptions extension enabled for this sliding sync /// instance? fn is_thread_subscriptions_enabled(&self) -> bool { - self.inner.sticky.read().unwrap().data().extensions.thread_subscriptions.enabled - == Some(true) + self.inner.extensions.thread_subscriptions.enabled == Some(true) } #[cfg(not(feature = "e2e-encryption"))] @@ -649,7 +653,7 @@ impl SlidingSync { async fn must_process_rooms_response(&self) -> bool { // We consider that we must, if there's any room subscription or there's any // list. - !self.inner.sticky.read().unwrap().data().room_subscriptions.is_empty() + !self.inner.room_subscriptions.read().unwrap().is_empty() || !self.inner.lists.read().await.is_empty() } @@ -659,8 +663,7 @@ impl SlidingSync { #[doc(hidden)] #[instrument(skip_all, fields(pos, conn_id = self.inner.id))] pub async fn sync_once(&self) -> Result { - let (request, request_config, position_guard) = - self.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, request_config, position_guard) = self.generate_sync_request().await?; // Send the request. let summaries = self.send_sync_request(request, request_config, position_guard).await?; @@ -718,7 +721,7 @@ impl SlidingSync { // Here, errors we **cannot** ignore, and that must stop the sync loop. Err(error) => { if error.client_api_error_kind() == Some(&ErrorKind::UnknownPos) { - // The Sliding Sync session has expired. Let's reset `pos` and sticky parameters. + // The Sliding Sync session has expired. Let's reset `pos`. self.expire_session().await; } @@ -750,8 +753,7 @@ impl SlidingSync { /// Expire the current Sliding Sync session on the client-side. /// - /// Expiring a Sliding Sync session means: resetting `pos`. It also resets - /// sticky parameters. + /// Expiring a Sliding Sync session means: resetting `pos`. /// /// This should only be used when it's clear that this session was about to /// expire anyways, and should be used only in very specific cases (e.g. @@ -761,16 +763,14 @@ impl SlidingSync { /// This method **MUST** be called when the sync loop is stopped. #[doc(hidden)] pub async fn expire_session(&self) { - info!("Session expired; resetting `pos` and sticky parameters"); + info!("Session expired; resetting `pos`"); { let lists = self.inner.lists.read().await; + for list in lists.values() { // Invalidate in-memory data that would be persisted on disk. list.set_maximum_number_of_rooms(None); - - // Invalidate the sticky data for this list. - list.invalidate_sticky_data(); } } @@ -790,11 +790,9 @@ impl SlidingSync { } { - let mut sticky = self.inner.sticky.write().unwrap(); - // Clear all room subscriptions: we don't want to resend all room subscriptions // when the session will restart. - sticky.data_mut().room_subscriptions.clear(); + self.inner.room_subscriptions.write().unwrap().clear(); } } } @@ -832,16 +830,6 @@ impl SlidingSync { let mut position_lock = self.inner.position.lock().await; position_lock.pos = Some(new_pos); } - - /// Read the static extension configuration for this Sliding Sync. - /// - /// Note: this is not the next content of the sticky parameters, but rightly - /// the static configuration that was set during creation of this - /// Sliding Sync. - pub fn extensions_config(&self) -> http::request::Extensions { - let sticky = self.inner.sticky.read().unwrap(); - sticky.data().extensions.clone() - } } #[derive(Clone, Debug)] @@ -862,76 +850,29 @@ pub struct UpdateSummary { } /// A very basic bool-ish enum to represent the state of a -/// [`http::request::RoomSubscription`]. A `RoomSubscription` that has been sent -/// once should ideally not being sent again, to mostly save bandwidth. +/// [`RoomSubscription`]. +/// +/// Once a [`RoomSubscription`] has beent sent, it's not removed from the list +/// of room subscriptions, but instead is marked as [`Self::Applied`], so that +/// it cannot be sent again, mostly to save bandwidth. +/// +/// [`RoomSubscription`]: http::request::RoomSubscription #[derive(Debug, Default)] enum RoomSubscriptionState { - /// The `RoomSubscription` has not been sent or received correctly from the - /// server, i.e. the `RoomSubscription` —which is part of the sticky - /// parameters— has not been committed. + /// The [`RoomSubscription`] has not been sent to or received correctly by + /// the server. + /// + /// [`RoomSubscription`]: http::request::RoomSubscription #[default] Pending, - /// The `RoomSubscription` has been sent and received correctly by the + /// The [`RoomSubscription`] has been sent and received correctly by the /// server. + /// + /// [`RoomSubscription`]: http::request::RoomSubscription Applied, } -/// The set of sticky parameters owned by the `SlidingSyncInner` instance, and -/// sent in the request. -#[derive(Debug)] -pub(super) struct SlidingSyncStickyParameters { - /// Room subscriptions, i.e. rooms that may be out-of-scope of all lists - /// but one wants to receive updates. - room_subscriptions: - BTreeMap, - - /// The intended state of the extensions being supplied to sliding /sync - /// calls. - extensions: http::request::Extensions, -} - -impl SlidingSyncStickyParameters { - /// Create a new set of sticky parameters. - pub fn new( - room_subscriptions: BTreeMap, - extensions: http::request::Extensions, - ) -> Self { - Self { - room_subscriptions: room_subscriptions - .into_iter() - .map(|(room_id, room_subscription)| { - (room_id, (RoomSubscriptionState::Pending, room_subscription)) - }) - .collect(), - extensions, - } - } -} - -impl StickyData for SlidingSyncStickyParameters { - type Request = http::Request; - - fn apply(&self, request: &mut Self::Request) { - request.room_subscriptions = self - .room_subscriptions - .iter() - .filter(|(_, (state, _))| matches!(state, RoomSubscriptionState::Pending)) - .map(|(room_id, (_, room_subscription))| (room_id.clone(), room_subscription.clone())) - .collect(); - request.extensions = self.extensions.clone(); - } - - fn on_commit(&mut self) { - // All room subscriptions are marked as `Applied`. - for (state, _room_subscription) in self.room_subscriptions.values_mut() { - if matches!(state, RoomSubscriptionState::Pending) { - *state = RoomSubscriptionState::Applied; - } - } - } -} - #[cfg(all(test, not(target_family = "wasm")))] #[allow(clippy::dbg_macro)] mod tests { @@ -950,9 +891,7 @@ mod tests { use matrix_sdk_common::executor::spawn; use matrix_sdk_test::{ALICE, async_test, event_factory::EventFactory}; use ruma::{ - OwnedRoomId, TransactionId, - api::client::error::ErrorKind, - assign, + OwnedRoomId, assign, events::{direct::DirectEvent, room::member::MembershipState}, owned_room_id, room_id, serde::Raw, @@ -965,13 +904,11 @@ mod tests { }; use super::{ - SlidingSync, SlidingSyncList, SlidingSyncListBuilder, SlidingSyncMode, - SlidingSyncStickyParameters, http, - sticky_parameters::{LazyTransactionId, SlidingSyncStickyManager}, + RoomSubscriptionState, SlidingSync, SlidingSyncBuilder, SlidingSyncList, + SlidingSyncListBuilder, SlidingSyncMode, cache::restore_sliding_sync_state, http, }; use crate::{ Client, Result, - sliding_sync::cache::restore_sliding_sync_state, test_utils::{logged_in_client, mocks::MatrixMockServer}, }; @@ -1081,8 +1018,7 @@ mod tests { assert!(room0.are_members_synced().not()); { - let sticky = sliding_sync.inner.sticky.read().unwrap(); - let room_subscriptions = &sticky.data().room_subscriptions; + let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap(); assert!(room_subscriptions.contains_key(room_id_0)); assert!(room_subscriptions.contains_key(room_id_1)); @@ -1140,8 +1076,7 @@ mod tests { sliding_sync.subscribe_to_rooms(&[room_id_0, room_id_1], None, false); { - let sticky = sliding_sync.inner.sticky.read().unwrap(); - let room_subscriptions = &sticky.data().room_subscriptions; + let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap(); assert!(room_subscriptions.contains_key(room_id_0)); assert!(room_subscriptions.contains_key(room_id_1)); @@ -1152,8 +1087,7 @@ mod tests { sliding_sync.subscribe_to_rooms(&[room_id_2], None, false); { - let sticky = sliding_sync.inner.sticky.read().unwrap(); - let room_subscriptions = &sticky.data().room_subscriptions; + let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap(); assert!(room_subscriptions.contains_key(room_id_0)); assert!(room_subscriptions.contains_key(room_id_1)); @@ -1164,8 +1098,7 @@ mod tests { sliding_sync.expire_session().await; { - let sticky = sliding_sync.inner.sticky.read().unwrap(); - let room_subscriptions = &sticky.data().room_subscriptions; + let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap(); assert!(room_subscriptions.is_empty()); } @@ -1174,8 +1107,7 @@ mod tests { sliding_sync.subscribe_to_rooms(&[room_id_2], None, false); { - let sticky = sliding_sync.inner.sticky.read().unwrap(); - let room_subscriptions = &sticky.data().room_subscriptions; + let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap(); assert!(room_subscriptions.contains_key(room_id_0).not()); assert!(room_subscriptions.contains_key(room_id_1).not()); @@ -1213,320 +1145,232 @@ mod tests { Ok(()) } - #[test] - fn test_sticky_parameters_api_invalidated_flow() { - let r0 = room_id!("!r0.matrix.org"); - let r1 = room_id!("!r1:matrix.org"); - - let mut room_subscriptions = BTreeMap::new(); - room_subscriptions.insert(r0.to_owned(), Default::default()); - - // At first it's invalidated. - let mut sticky = SlidingSyncStickyManager::new(SlidingSyncStickyParameters::new( - room_subscriptions, - Default::default(), - )); - assert!(sticky.is_invalidated()); - - // Then when we create a request, the sticky parameters are applied. - let txn_id: &TransactionId = "tid123".into(); - - let mut request = http::Request::default(); - request.txn_id = Some(txn_id.to_string()); - - sticky.maybe_apply(&mut request, &mut LazyTransactionId::from_owned(txn_id.to_owned())); - - assert!(request.txn_id.is_some()); - assert_eq!(request.room_subscriptions.len(), 1); - assert!(request.room_subscriptions.contains_key(r0)); - - let tid = request.txn_id.unwrap(); - - sticky.maybe_commit(tid.as_str().into()); - assert!(!sticky.is_invalidated()); - - // Applying new parameters will invalidate again. - sticky - .data_mut() - .room_subscriptions - .insert(r1.to_owned(), (Default::default(), Default::default())); - assert!(sticky.is_invalidated()); - - // Committing with the wrong transaction id will keep it invalidated. - sticky.maybe_commit("wrong tid today, my love has gone away 🎵".into()); - assert!(sticky.is_invalidated()); - - // Restarting a request will only remember the last generated transaction id. - let txn_id1: &TransactionId = "tid456".into(); - let mut request1 = http::Request::default(); - request1.txn_id = Some(txn_id1.to_string()); - sticky.maybe_apply(&mut request1, &mut LazyTransactionId::from_owned(txn_id1.to_owned())); - - assert!(sticky.is_invalidated()); - // The first room subscription has been applied to `request`, so it's not - // reapplied here. It's a particular logic of `room_subscriptions`, it's not - // related to the sticky design. - assert_eq!(request1.room_subscriptions.len(), 1); - assert!(request1.room_subscriptions.contains_key(r1)); - - let txn_id2: &TransactionId = "tid789".into(); - let mut request2 = http::Request::default(); - request2.txn_id = Some(txn_id2.to_string()); - - sticky.maybe_apply(&mut request2, &mut LazyTransactionId::from_owned(txn_id2.to_owned())); - assert!(sticky.is_invalidated()); - // `request2` contains `r1` because the sticky parameters have not been - // committed, so it's still marked as pending. - assert_eq!(request2.room_subscriptions.len(), 1); - assert!(request2.room_subscriptions.contains_key(r1)); - - // Here we commit with the not most-recent TID, so it keeps the invalidated - // status. - sticky.maybe_commit(txn_id1); - assert!(sticky.is_invalidated()); - - // But here we use the latest TID, so the commit is effective. - sticky.maybe_commit(txn_id2); - assert!(!sticky.is_invalidated()); - } - - #[test] - fn test_room_subscriptions_are_sticky() { - let r0 = room_id!("!r0.matrix.org"); - let r1 = room_id!("!r1:matrix.org"); - - let mut sticky = SlidingSyncStickyManager::new(SlidingSyncStickyParameters::new( - BTreeMap::new(), - Default::default(), - )); - - // A room subscription is added, applied, and committed. - { - // Insert `r0`. - sticky - .data_mut() - .room_subscriptions - .insert(r0.to_owned(), (Default::default(), Default::default())); - - // Then the sticky parameters are applied. - let txn_id: &TransactionId = "tid0".into(); - let mut request = http::Request::default(); - request.txn_id = Some(txn_id.to_string()); - - sticky.maybe_apply(&mut request, &mut LazyTransactionId::from_owned(txn_id.to_owned())); - - assert!(request.txn_id.is_some()); - assert_eq!(request.room_subscriptions.len(), 1); - assert!(request.room_subscriptions.contains_key(r0)); - - // Then the sticky parameters are committed. - let tid = request.txn_id.unwrap(); - - sticky.maybe_commit(tid.as_str().into()); - } - - // A room subscription is added, applied, but NOT committed. - { - // Insert `r1`. - sticky - .data_mut() - .room_subscriptions - .insert(r1.to_owned(), (Default::default(), Default::default())); - - // Then the sticky parameters are applied. - let txn_id: &TransactionId = "tid1".into(); - let mut request = http::Request::default(); - request.txn_id = Some(txn_id.to_string()); - - sticky.maybe_apply(&mut request, &mut LazyTransactionId::from_owned(txn_id.to_owned())); - - assert!(request.txn_id.is_some()); - assert_eq!(request.room_subscriptions.len(), 1); - // `r0` is not present, it's only `r1`. - assert!(request.room_subscriptions.contains_key(r1)); - - // Then the sticky parameters are NOT committed. - // It can happen if the request has failed to be sent for example, - // or if the response didn't match. - } - - // A previously added room subscription is re-added, applied, and committed. - { - // Then the sticky parameters are applied. - let txn_id: &TransactionId = "tid2".into(); - let mut request = http::Request::default(); - request.txn_id = Some(txn_id.to_string()); - - sticky.maybe_apply(&mut request, &mut LazyTransactionId::from_owned(txn_id.to_owned())); - - assert!(request.txn_id.is_some()); - assert_eq!(request.room_subscriptions.len(), 1); - // `r0` is not present, it's only `r1`. - assert!(request.room_subscriptions.contains_key(r1)); - - // Then the sticky parameters are committed. - let tid = request.txn_id.unwrap(); - - sticky.maybe_commit(tid.as_str().into()); - } - - // All room subscriptions have been committed. - { - // Then the sticky parameters are applied. - let txn_id: &TransactionId = "tid3".into(); - let mut request = http::Request::default(); - request.txn_id = Some(txn_id.to_string()); - - sticky.maybe_apply(&mut request, &mut LazyTransactionId::from_owned(txn_id.to_owned())); - - assert!(request.txn_id.is_some()); - // All room subscriptions have been sent. - assert!(request.room_subscriptions.is_empty()); - } - } - - #[test] - fn test_extensions_are_sticky() { - let mut extensions = http::request::Extensions::default(); - extensions.account_data.enabled = Some(true); - - // At first it's invalidated. - let mut sticky = SlidingSyncStickyManager::new(SlidingSyncStickyParameters::new( - Default::default(), - extensions, - )); - - assert!(sticky.is_invalidated(), "invalidated because of non default parameters"); - - // `StickyParameters::new` follows its caller's intent when it comes to e2ee and - // 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); - - // 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(); - request.txn_id = Some(txn_id.to_string()); - sticky.maybe_apply(&mut request, &mut LazyTransactionId::from_owned(txn_id.to_owned())); - assert!(sticky.is_invalidated()); - assert_eq!(request.extensions.to_device.enabled, None); - assert_eq!(request.extensions.to_device.since, None); - assert_eq!(request.extensions.e2ee.enabled, None); - assert_eq!(request.extensions.account_data.enabled, Some(true)); - } - + #[cfg(feature = "e2e-encryption")] #[async_test] - async fn test_sticky_extensions_plus_since() -> Result<()> { - let server = MockServer::start().await; - let client = logged_in_client(Some(server.uri())).await; + async fn test_extensions_to_device_since_is_set() { + use matrix_sdk_base::crypto::store::types::Changes; - let sync = client - .sliding_sync("test-slidingsync")? - .add_list(SlidingSyncList::builder("new_list")) + let client = logged_in_client(None).await; + let sliding_sync = SlidingSyncBuilder::new("foo".to_owned(), client.clone()) + .unwrap() + .with_to_device_extension(assign!( + http::request::ToDevice::default(), + { + enabled: Some(true), + } + )) .build() - .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.e2ee.enabled, None); - assert_eq!(sync.inner.sticky.read().unwrap().data().extensions.account_data.enabled, None); - - // Now enable e2ee and to-device. - let sync = client - .sliding_sync("test-slidingsync")? - .add_list(SlidingSyncList::builder("new_list")) - .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?; - - // Even without a since token, the first request will contain the extensions - // configuration, at least. - let txn_id = TransactionId::new(); - let (request, _, _) = sync - .generate_sync_request(&mut LazyTransactionId::from_owned(txn_id.to_owned())) - .await?; - - assert_eq!(request.extensions.e2ee.enabled, Some(true)); - assert_eq!(request.extensions.to_device.enabled, Some(true)); - assert!(request.extensions.to_device.since.is_none()); + .await + .unwrap(); + // Test `SlidingSyncInner::extensions`. { - // Committing with another transaction id doesn't validate anything. - let mut sticky = sync.inner.sticky.write().unwrap(); - assert!(sticky.is_invalidated()); - sticky.maybe_commit( - "hopefully the rng won't generate this very specific transaction id".into(), - ); - assert!(sticky.is_invalidated()); + let to_device = &sliding_sync.inner.extensions.to_device; + + assert_eq!(to_device.enabled, Some(true)); + assert!(to_device.since.is_none()); } - // Regenerating a request will yield the same one. - let txn_id2 = TransactionId::new(); - let (request, _, _) = sync - .generate_sync_request(&mut LazyTransactionId::from_owned(txn_id2.to_owned())) - .await?; - - assert_eq!(request.extensions.e2ee.enabled, Some(true)); - assert_eq!(request.extensions.to_device.enabled, Some(true)); - assert!(request.extensions.to_device.since.is_none()); - - assert!(txn_id != txn_id2, "the two requests must not share the same transaction id"); - + // Test `Request::extensions`. { - // Committing with the expected transaction id will validate it. - let mut sticky = sync.inner.sticky.write().unwrap(); - assert!(sticky.is_invalidated()); - sticky.maybe_commit(txn_id2.as_str().into()); - assert!(!sticky.is_invalidated()); + let (request, _, _) = sliding_sync.generate_sync_request().await.unwrap(); + + let to_device = &request.extensions.to_device; + + assert_eq!(to_device.enabled, Some(true)); + assert!(to_device.since.is_none()); } - // The next request should contain no sticky parameters. - let txn_id = TransactionId::new(); - let (request, _, _) = sync - .generate_sync_request(&mut LazyTransactionId::from_owned(txn_id.to_owned())) - .await?; - assert!(request.extensions.e2ee.enabled.is_none()); - assert!(request.extensions.to_device.enabled.is_none()); - assert!(request.extensions.to_device.since.is_none()); + // Define a `since` token. + let since_token = "depuis".to_owned(); - // If there's a to-device `since` token, we make sure we put the token - // into the extension config. The rest doesn't need to be re-enabled due to - // stickiness. - let _since_token = "since"; - - #[cfg(feature = "e2e-encryption")] { - use matrix_sdk_base::crypto::store::types::Changes; if let Some(olm_machine) = &*client.olm_machine().await { olm_machine .store() .save_changes(Changes { - next_batch_token: Some(_since_token.to_owned()), + next_batch_token: Some(since_token.clone()), ..Default::default() }) - .await?; + .await + .unwrap(); + } else { + panic!("Where is the Olm machine?"); } } - let txn_id = TransactionId::new(); - let (request, _, _) = sync - .generate_sync_request(&mut LazyTransactionId::from_owned(txn_id.to_owned())) - .await?; + // Test `Request::extensions` again. + { + let (request, _, _) = sliding_sync.generate_sync_request().await.unwrap(); - assert!(request.extensions.e2ee.enabled.is_none()); - assert!(request.extensions.to_device.enabled.is_none()); + let to_device = &request.extensions.to_device; - #[cfg(feature = "e2e-encryption")] - assert_eq!(request.extensions.to_device.since.as_deref(), Some(_since_token)); + assert_eq!(to_device.enabled, Some(true)); + assert_eq!(to_device.since, Some(since_token)); + } + } - Ok(()) + #[async_test] + async fn test_room_subscriptions_are_sticky() { + let r0 = room_id!("!r0.matrix.org"); + let r1 = room_id!("!r1:matrix.org"); + + let client = logged_in_client(None).await; + let sliding_sync = + SlidingSyncBuilder::new("foo".to_owned(), client).unwrap().build().await.unwrap(); + + // A room subscription is added. The request is sent and received. + { + // Insert `r0`. + sliding_sync.subscribe_to_rooms(&[r0], None, false); + + // The room subscription is marked as `Pending`. + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r0), + Some((RoomSubscriptionState::Pending, _)) + ); + + // Generate the request. + let (request, _, mut position_markers) = + sliding_sync.generate_sync_request().await.unwrap(); + + assert_eq!(request.room_subscriptions.len(), 1); + assert!(request.room_subscriptions.contains_key(r0)); + + // The room subscription is marked as `Pending`. + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r0), + Some((RoomSubscriptionState::Pending, _)) + ); + + // Receive a response (simulate the request is sent and a response is received). + sliding_sync + .handle_response( + http::Response::new("pos0".to_owned()), + &mut position_markers, + Default::default(), + ) + .await + .unwrap(); + + // The room subscription is marked as `Applied`. + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r0), + Some((RoomSubscriptionState::Applied, _)) + ); + } + + // A room subscription is re-added. + { + // Insert `r0`. + sliding_sync.subscribe_to_rooms(&[r0], None, false); + + // The room subscription is still marked as `Applied`. + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r0), + Some((RoomSubscriptionState::Applied, _)) + ); + } + + // A new room subscription is added. + { + // Insert `r1`. + sliding_sync.subscribe_to_rooms(&[r1], None, false); + + // This room subscription is still marked as `Applied`. + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r0), + Some((RoomSubscriptionState::Applied, _)) + ); + // This room subscription is marked as `Pending`. + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r1), + Some((RoomSubscriptionState::Pending, _)) + ); + + // Generate the request. + let (request, _, mut position_markers) = + sliding_sync.generate_sync_request().await.unwrap(); + + assert_eq!(request.room_subscriptions.len(), 1); + assert!(request.room_subscriptions.contains_key(r1)); + + // This room subscription is still marked as `Applied`. + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r0), + Some((RoomSubscriptionState::Applied, _)) + ); + // This room subscription is still marked as `Pending`. + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r1), + Some((RoomSubscriptionState::Pending, _)) + ); + + // Receive a response (simulate the request is sent and a response is received). + sliding_sync + .handle_response( + http::Response::new("pos1".to_owned()), + &mut position_markers, + Default::default(), + ) + .await + .unwrap(); + + // This room subscription is marked as `Applied`. + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r1), + Some((RoomSubscriptionState::Applied, _)) + ); + } + + // No new room subscription is added. + { + // Room subscriptions are still marked as `Applied`. + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r0), + Some((RoomSubscriptionState::Applied, _)) + ); + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r1), + Some((RoomSubscriptionState::Applied, _)) + ); + + // Generate the request. + let (request, _, mut position_markers) = + sliding_sync.generate_sync_request().await.unwrap(); + + assert!(request.room_subscriptions.is_empty()); + + // Room subscriptions are still marked as `Applied`. + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r0), + Some((RoomSubscriptionState::Applied, _)) + ); + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r1), + Some((RoomSubscriptionState::Applied, _)) + ); + + // Receive a response (simulate the request is sent and a response is received). + sliding_sync + .handle_response( + http::Response::new("pos1".to_owned()), + &mut position_markers, + Default::default(), + ) + .await + .unwrap(); + + // Room subscriptions are still marked as `Applied`. + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r0), + Some((RoomSubscriptionState::Applied, _)) + ); + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r1), + Some((RoomSubscriptionState::Applied, _)) + ); + } } // With MSC4186, with the `e2ee` extension enabled, if a request has no `pos`, @@ -1616,10 +1460,7 @@ mod tests { .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?; + let (_request, _, _) = sync.generate_sync_request().await?; // Now, tracked users must be dirty. { @@ -1657,10 +1498,7 @@ mod tests { // 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?; + let (_request, _, _) = sync.generate_sync_request().await?; // Tracked users are not marked as dirty. { @@ -1676,141 +1514,6 @@ mod tests { Ok(()) } - #[async_test] - async fn test_unknown_pos_resets_pos_and_sticky_parameters() -> Result<()> { - let server = MockServer::start().await; - let client = logged_in_client(Some(server.uri())).await; - - let sliding_sync = client - .sliding_sync("test-slidingsync")? - .with_to_device_extension( - assign!(http::request::ToDevice::default(), { enabled: Some(true) }), - ) - .build() - .await?; - - // First request asks to enable the extension. - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; - assert!(request.extensions.to_device.enabled.is_some()); - - let sync = sliding_sync.sync(); - pin_mut!(sync); - - // `pos` is `None` to start with. - assert!(sliding_sync.inner.position.lock().await.pos.is_none()); - - #[derive(Deserialize)] - struct PartialRequest { - txn_id: Option, - } - - { - let _mock_guard = Mock::given(SlidingSyncMatcher) - .respond_with(|request: &Request| { - // Repeat the txn_id in the response, if set. - let request: PartialRequest = request.body_json().unwrap(); - - ResponseTemplate::new(200).set_body_json(json!({ - "txn_id": request.txn_id, - "pos": "0", - })) - }) - .mount_as_scoped(&server) - .await; - - let next = sync.next().await; - assert_matches!(next, Some(Ok(_update_summary))); - - // `pos` has been updated. - assert_eq!(sliding_sync.inner.position.lock().await.pos, Some("0".to_owned())); - } - - // Next request doesn't ask to enable the extension. - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; - assert!(request.extensions.to_device.enabled.is_none()); - - // Next request is successful. - { - let _mock_guard = Mock::given(SlidingSyncMatcher) - .respond_with(|request: &Request| { - // Repeat the txn_id in the response, if set. - let request: PartialRequest = request.body_json().unwrap(); - - ResponseTemplate::new(200).set_body_json(json!({ - "txn_id": request.txn_id, - "pos": "1", - })) - }) - .mount_as_scoped(&server) - .await; - - let next = sync.next().await; - assert_matches!(next, Some(Ok(_update_summary))); - - // `pos` has been updated. - assert_eq!(sliding_sync.inner.position.lock().await.pos, Some("1".to_owned())); - } - - // Next request is successful despite it receives an already - // received `pos` from the server. - { - let _mock_guard = Mock::given(SlidingSyncMatcher) - .respond_with(|request: &Request| { - // Repeat the txn_id in the response, if set. - let request: PartialRequest = request.body_json().unwrap(); - - ResponseTemplate::new(200).set_body_json(json!({ - "txn_id": request.txn_id, - "pos": "0", // <- already received! - })) - }) - .up_to_n_times(1) // run this mock only once. - .mount_as_scoped(&server) - .await; - - let next = sync.next().await; - assert_matches!(next, Some(Ok(_update_summary))); - - // `pos` has been updated. - assert_eq!(sliding_sync.inner.position.lock().await.pos, Some("0".to_owned())); - } - - // Stop responding with successful requests! - // - // When responding with `M_UNKNOWN_POS`, that regenerates the sticky parameters, - // so they're reset. It also resets the `pos`. - { - let _mock_guard = Mock::given(SlidingSyncMatcher) - .respond_with(ResponseTemplate::new(400).set_body_json(json!({ - "error": "foo", - "errcode": "M_UNKNOWN_POS", - }))) - .mount_as_scoped(&server) - .await; - - let next = sync.next().await; - - // The expected error is returned. - assert_matches!(next, Some(Err(err)) if err.client_api_error_kind() == Some(&ErrorKind::UnknownPos)); - - // `pos` has been reset. - assert!(sliding_sync.inner.position.lock().await.pos.is_none()); - - // Next request asks to enable the extension again. - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; - - assert!(request.extensions.to_device.enabled.is_some()); - - // `sync` has been stopped. - assert!(sync.next().await.is_none()); - } - - Ok(()) - } - #[cfg(feature = "e2e-encryption")] #[async_test] async fn test_sliding_sync_doesnt_remember_pos() -> Result<()> { @@ -1849,8 +1552,7 @@ mod tests { { assert!(sliding_sync.inner.position.lock().await.pos.is_none()); - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; assert!(request.pos.is_none()); } @@ -1887,8 +1589,7 @@ mod tests { // It's still 0, not "yolo". { assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("0")); - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; assert_eq!(request.pos.as_deref(), Some("0")); } @@ -1938,8 +1639,7 @@ mod tests { // `pos` is `None` to start with. { - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; assert!(request.pos.is_none()); assert!(sliding_sync.inner.position.lock().await.pos.is_none()); @@ -1975,8 +1675,7 @@ mod tests { // It's alright, the next request will load it from the database. { - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; assert_eq!(request.pos.as_deref(), Some("42")); assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("42")); } @@ -1986,8 +1685,7 @@ mod tests { let sliding_sync = client.sliding_sync("elephant-sync")?.share_pos().build().await?; assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("42")); - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; assert_eq!(request.pos.as_deref(), Some("42")); } @@ -1998,8 +1696,7 @@ mod tests { { assert!(sliding_sync.inner.position.lock().await.pos.is_none()); - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; assert!(request.pos.is_none()); } @@ -2008,8 +1705,7 @@ mod tests { let sliding_sync = client.sliding_sync("elephant-sync")?.share_pos().build().await?; assert!(sliding_sync.inner.position.lock().await.pos.is_none()); - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; assert!(request.pos.is_none()); } @@ -2638,8 +2334,7 @@ mod tests { async fn test_timeout_zero_list() -> Result<()> { let (_server, sliding_sync) = new_sliding_sync(vec![]).await?; - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; // Zero list means sliding sync is fully loaded, so there is a timeout to wait // on new update to pop. @@ -2655,8 +2350,7 @@ mod tests { ]) .await?; - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; // The list does not require a timeout. assert!(request.timeout.is_none()); @@ -2684,8 +2378,7 @@ mod tests { }; } - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; // The list is now fully loaded, so it requires a timeout. assert!(request.timeout.is_some()); @@ -2703,8 +2396,7 @@ mod tests { ]) .await?; - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; // Two lists don't require a timeout. assert!(request.timeout.is_none()); @@ -2732,8 +2424,7 @@ mod tests { }; } - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; // One don't require a timeout. assert!(request.timeout.is_none()); @@ -2761,8 +2452,7 @@ mod tests { }; } - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; // All lists require a timeout. assert!(request.timeout.is_some()); diff --git a/crates/matrix-sdk/src/sliding_sync/sticky_parameters.rs b/crates/matrix-sdk/src/sliding_sync/sticky_parameters.rs deleted file mode 100644 index acacbc0ee..000000000 --- a/crates/matrix-sdk/src/sliding_sync/sticky_parameters.rs +++ /dev/null @@ -1,194 +0,0 @@ -//! Sticky parameters are a way to spare bandwidth on the network, by sending -//! request parameters once and have the server remember them. -//! -//! The set of sticky parameters have to be agreed upon by the server and the -//! client; this is defined in the -//! [MSC](https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/sync-v3/proposals/3575-sync.md). - -use ruma::{OwnedTransactionId, TransactionId}; - -/// An `OwnedTransactionId` that is either initialized at creation, or -/// lazily-generated once. -#[derive(Debug)] -pub struct LazyTransactionId { - txn_id: Option, -} - -impl LazyTransactionId { - /// Create a new `LazyTransactionId`, not set. - pub fn new() -> Self { - Self { txn_id: None } - } - - /// Get (or create it, if never set) a `TransactionId`. - pub fn get_or_create(&mut self) -> &TransactionId { - self.txn_id.get_or_insert_with(TransactionId::new) - } - - /// Attempt to get the underlying `TransactionId` without creating it, if - /// missing. - pub fn get(&self) -> Option<&TransactionId> { - self.txn_id.as_deref() - } -} - -#[cfg(test)] -impl LazyTransactionId { - /// Create a `LazyTransactionId` for a given known transaction id. For - /// testing only. - pub fn from_owned(owned: OwnedTransactionId) -> Self { - Self { txn_id: Some(owned) } - } -} - -/// A trait to implement for data that can be sticky, given a context. -pub trait StickyData { - /// Request type that will be applied to, if the sticky parameters have been - /// invalidated before. - type Request; - - /// Apply the current data onto the request. - fn apply(&self, request: &mut Self::Request); - - /// When the current are committed, i.e. when the request has been validated - /// by a response. - fn on_commit(&mut self) { - // noop - } -} - -/// Helper data structure to manage sticky parameters, for any kind of data. -/// -/// Initially, the provided data is considered to be invalidated, so it's -/// applied onto the request the first time it's sent. Any changes to the -/// wrapped data happen via [`Self::data_mut`], which invalidates the sticky -/// parameters; they will be applied automatically to the next request. -/// -/// When applying sticky parameters, we will also remember the transaction id -/// that was generated for us, stash it, so we can match the response against -/// the transaction id later, and only consider the data isn't invalidated -/// anymore (we say it's "committed" in that case) if the response's transaction -/// id match what we expect. -#[derive(Debug)] -pub struct SlidingSyncStickyManager { - /// The data managed by this sticky manager. - data: D, - - /// Was any of the parameters invalidated? If yes, reinitialize them. - invalidated: bool, - - /// If the sticky parameters were applied to a given request, this is - /// the transaction id generated for that request, that must be matched - /// upon in the next call to `commit()`. - txn_id: Option, -} - -impl SlidingSyncStickyManager { - /// Create a new `StickyManager` for the given data. - /// - /// Always assume the initial data invalidates the request, at first. - pub fn new(data: D) -> Self { - Self { data, txn_id: None, invalidated: true } - } - - /// Get a mutable reference to the managed data. - /// - /// Will invalidate the sticky set by default. If you don't need to modify - /// the data, use `Self::data()`; if you're not sure you're going to modify - /// the data, it's best to first use `Self::data()` then `Self::data_mut()` - /// when you're sure. - pub fn data_mut(&mut self) -> &mut D { - self.invalidated = true; - &mut self.data - } - - /// Returns a non-invalidating reference to the managed data. - pub fn data(&self) -> &D { - &self.data - } - - /// May apply some the managed sticky parameters to the given request. - /// - /// After receiving the response from this sliding sync, the caller MUST - /// also call [`Self::maybe_commit`] with the transaction id from the - /// server's response. - /// - /// If no `txn_id` is provided, it will generate one that can be reused - /// later. - pub fn maybe_apply(&mut self, req: &mut D::Request, txn_id: &mut LazyTransactionId) { - if self.invalidated { - let txn_id = txn_id.get_or_create(); - self.txn_id = Some(txn_id.to_owned()); - self.data.apply(req); - } - } - - /// May mark the managed data as not invalidated anymore, if the transaction - /// id received from the response matches the one received from the request. - pub fn maybe_commit(&mut self, txn_id: &TransactionId) { - if self.invalidated && self.txn_id.as_deref() == Some(txn_id) { - self.invalidated = false; - self.data.on_commit(); - } - } - - #[cfg(test)] - pub fn is_invalidated(&self) -> bool { - self.invalidated - } -} - -#[cfg(test)] -mod tests { - use super::{LazyTransactionId, SlidingSyncStickyManager, StickyData}; - - struct EmptyStickyData(u8); - - impl StickyData for EmptyStickyData { - type Request = bool; - - fn apply(&self, req: &mut Self::Request) { - // Mark that applied has had an effect. - *req = true; - } - - fn on_commit(&mut self) { - self.0 += 1; - } - } - - #[test] - fn test_sticky_parameters_api_non_invalidated_no_effect() { - let mut sticky = SlidingSyncStickyManager::new(EmptyStickyData(0)); - - // At first, it's always invalidated. - assert!(sticky.is_invalidated()); - - let mut applied = false; - let mut txn_id = LazyTransactionId::new(); - sticky.maybe_apply(&mut applied, &mut txn_id); - assert!(applied); - assert!(sticky.is_invalidated()); - assert!(txn_id.get().is_some(), "a transaction id was lazily generated"); - - // Committing with the wrong transaction id won't commit. - sticky.maybe_commit("tid456".into()); - assert_eq!(sticky.data.0, 0); - assert!(sticky.is_invalidated()); - - // Providing the correct transaction id will commit. - sticky.maybe_commit(txn_id.get().unwrap()); - assert_eq!(sticky.data.0, 1); - assert!(!sticky.is_invalidated()); - - // Applying without being invalidated won't do anything, and not generate a - // transaction id. - let mut txn_id = LazyTransactionId::new(); - let mut applied = false; - sticky.maybe_apply(&mut applied, &mut txn_id); - - assert!(!applied); - assert!(!sticky.is_invalidated()); - assert!(txn_id.get().is_none()); - } -} diff --git a/testing/matrix-sdk-integration-testing/src/tests/e2ee/shared_history.rs b/testing/matrix-sdk-integration-testing/src/tests/e2ee/shared_history.rs index 89e3a670e..5f23935b0 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/e2ee/shared_history.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/e2ee/shared_history.rs @@ -6,7 +6,7 @@ use assign::assign; use eyeball_im::VectorDiff; use futures::{FutureExt, StreamExt, future, pin_mut}; use matrix_sdk::{ - assert_decrypted_message_eq, assert_next_with_timeout, + Client, assert_decrypted_message_eq, assert_next_with_timeout, deserialized_responses::TimelineEventKind, encryption::EncryptionSettings, room::power_levels::RoomPowerLevelChanges, @@ -32,7 +32,8 @@ use matrix_sdk_ui::{ Timeline, sync_service::SyncService, timeline::{ - EncryptedMessage, MsgLikeContent, MsgLikeKind, RoomExt, TimelineItem, TimelineItemContent, + EncryptedMessage, MsgLikeContent, MsgLikeKind, RoomExt, TimelineDetails, TimelineItem, + TimelineItemContent, }, }; use similar_asserts::assert_eq; @@ -65,35 +66,15 @@ async fn test_history_share_on_invite_helper(exclude_insecure_devices: bool) -> let alice_span = tracing::info_span!("alice"); let bob_span = tracing::info_span!("bob"); - let encryption_settings = - EncryptionSettings { auto_enable_cross_signing: true, ..Default::default() }; - - let alice = TestClientBuilder::new("alice") - .use_sqlite() - .encryption_settings(encryption_settings) - .enable_share_history_on_invite(true) - .exclude_insecure_devices(exclude_insecure_devices) - .build() + let alice = create_encryption_enabled_client("alice", exclude_insecure_devices) + .instrument(alice_span.clone()) .await?; - let sync_service_span = tracing::info_span!(parent: &alice_span, "sync_service"); - let alice_sync_service = SyncService::builder(alice.clone()) - .with_parent_span(sync_service_span) - .build() - .await - .expect("Could not build alice sync service"); + let alice_sync_service = start_client_sync_service(&alice_span, &alice).await; - alice.encryption().wait_for_e2ee_initialization_tasks().await; - alice_sync_service.start().await; - - let bob = SyncTokenAwareClient::new( - TestClientBuilder::new("bob") - .encryption_settings(encryption_settings) - .enable_share_history_on_invite(true) - .exclude_insecure_devices(exclude_insecure_devices) - .build() - .await?, - ); + let bob = create_encryption_enabled_client("bob", exclude_insecure_devices) + .instrument(bob_span.clone()) + .await?; // Alice creates a room ... let alice_room = alice @@ -139,13 +120,7 @@ async fn test_history_share_on_invite_helper(exclude_insecure_devices: bool) -> let bob_response = bob.sync_once().instrument(bob_span.clone()).await?; // Bob should have received a to-device event with the payload - assert_eq!(bob_response.to_device.len(), 1); - let to_device_event = &bob_response.to_device[0]; - assert_let!(ProcessedToDeviceEvent::Decrypted { raw, .. } = to_device_event); - assert_eq!( - raw.get_field::("type").unwrap().unwrap(), - "io.element.msc4268.room_key_bundle" - ); + assert_received_room_key_bundle(bob_response); bob.get_room(alice_room.room_id()).expect("Bob should have received the invite"); @@ -178,6 +153,35 @@ async fn test_history_share_on_invite_helper(exclude_insecure_devices: bool) -> "The decrypted event should match the message Alice has sent" ); + // We should be able to find the event using the high level timeline API, and + // inspect who forwarded us the keys to decrypt. + + let alice_id = alice.user_id().unwrap(); + let alice_display_name = + alice.account().get_display_name().await?.expect("Alice should have a display name"); + + let bob_timeline = bob_room.timeline().await?; + bob.sync_once().instrument(bob_span.clone()).await?; + + let item = assert_event_received(&bob_timeline, &event_id, "Hello Bob").await; + let event = item.as_event().expect("The timeline item should be an event"); + + assert_eq!( + event.forwarder().expect("We should be able to access the forwarder's ID"), + alice_id.as_str() + ); + assert_let!( + Some(TimelineDetails::Ready(profile)) = event.forwarder_profile(), + "We should be able to access the forwarder's profile" + ); + assert_eq!( + profile + .display_name + .as_ref() + .expect("We should be able to access the forwarder's display name"), + &alice_display_name + ); + Ok(()) } @@ -407,11 +411,13 @@ async fn test_transitive_history_share_with_withhelds() -> Result<()> { let charlie_span = tracing::info_span!("charlie"); let derek_span = tracing::info_span!("derek"); - let alice = create_encryption_enabled_client("alice").instrument(alice_span.clone()).await?; - let bob = create_encryption_enabled_client("bob").instrument(bob_span.clone()).await?; + let alice = + create_encryption_enabled_client("alice", false).instrument(alice_span.clone()).await?; + let bob = create_encryption_enabled_client("bob", false).instrument(bob_span.clone()).await?; let charlie = - create_encryption_enabled_client("charlie").instrument(charlie_span.clone()).await?; - let derek = create_encryption_enabled_client("derek").instrument(derek_span.clone()).await?; + create_encryption_enabled_client("charlie", false).instrument(charlie_span.clone()).await?; + let derek = + create_encryption_enabled_client("derek", false).instrument(derek_span.clone()).await?; // 1. Alice creates a room, and enables encryption let alice_room = alice @@ -572,10 +578,11 @@ async fn test_history_sharing_session_merging() -> Result<()> { let bob_span = tracing::info_span!("bob"); let charlie_span = tracing::info_span!("charlie"); - let alice = create_encryption_enabled_client("alice").instrument(alice_span.clone()).await?; - let bob = create_encryption_enabled_client("bob").instrument(bob_span.clone()).await?; + let alice = + create_encryption_enabled_client("alice", false).instrument(alice_span.clone()).await?; + let bob = create_encryption_enabled_client("bob", false).instrument(bob_span.clone()).await?; let charlie = - create_encryption_enabled_client("charlie").instrument(charlie_span.clone()).await?; + create_encryption_enabled_client("charlie", false).instrument(charlie_span.clone()).await?; // 1. Alice creates a room, and enables encryption let alice_room = alice @@ -700,7 +707,159 @@ async fn test_history_sharing_session_merging() -> Result<()> { Ok(()) } -async fn create_encryption_enabled_client(username: &str) -> Result { +/// This is a very similar test to [`test_history_share_on_invite`], but we send +/// a second message once Bob has fully joined. +/// +/// We can't combine this with the above since: +/// +/// - We want to test that history sharing works when Alice's device is deleted, +/// which prevents Alice from sending; +/// - Sending a message after we invite Bob but before they join causes the +/// sessions to be merged, so we lose the forwarder info on the first event as +/// intended. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn test_history_share_on_invite_no_forwarder_info_for_normal_events() -> Result<()> { + let alice_span = tracing::info_span!("alice"); + let bob_span = tracing::info_span!("bob"); + + let alice = create_encryption_enabled_client("alice", false).await?; + let alice_sync_service = start_client_sync_service(&alice_span, &alice).await; + + alice.encryption().wait_for_e2ee_initialization_tasks().await; + alice_sync_service.start().await; + + let bob = create_encryption_enabled_client("bob", false).await?; + + // Alice creates a room ... + let alice_room = alice + .create_room(assign!(CreateRoomRequest::new(), { + preset: Some(RoomPreset::PublicChat), + })) + .await?; + alice_room.enable_encryption().await?; + + info!(room_id = ?alice_room.room_id(), "Alice has created and enabled encryption in the room"); + + // ... and sends a message + let event_id = alice_room + .send(RoomMessageEventContent::text_plain("Hello Bob")) + .await + .expect("We should be able to send a message to the room") + .response + .event_id; + + let bundle_stream = bob + .encryption() + .historic_room_key_stream() + .await + .expect("We should be able to get the bundle stream"); + + // Alice invites Bob to the room + alice_room.invite_user_by_id(bob.user_id().unwrap()).await?; + + // Workaround for https://github.com/matrix-org/matrix-rust-sdk/issues/5770: Bob needs a copy of + // Alice's identity. + bob.encryption() + .request_user_identity(alice.user_id().unwrap()) + .instrument(bob_span.clone()) + .await?; + + // Bob should have received a to-device event with the payload + assert_received_room_key_bundle(bob.sync_once().instrument(bob_span.clone()).await?); + + bob.get_room(alice_room.room_id()).expect("Bob should have received the invite"); + + pin_mut!(bundle_stream); + + let info = bundle_stream + .next() + .now_or_never() + .flatten() + .expect("We should be notified about the received bundle"); + + assert_eq!(Some(info.sender.deref()), alice.user_id()); + assert_eq!(info.room_id, alice_room.room_id()); + + let bob_room = bob + .join_room_by_id(alice_room.room_id()) + .instrument(bob_span.clone()) + .await + .expect("Bob should be able to accept the invitation from Alice"); + + let event = bob_room + .event(&event_id, None) + .instrument(bob_span.clone()) + .await + .expect("Bob should be able to fetch the historic event"); + + assert_decrypted_message_eq!( + event, + "Hello Bob", + "The decrypted event should match the message Alice has sent" + ); + + // We should be able to find the event using the high level timeline API, and + // inspect who forwarded us the keys to decrypt. + + let alice_id = alice.user_id().unwrap(); + let alice_display_name = + alice.account().get_display_name().await?.expect("Alice should have a display name"); + + let bob_timeline = bob_room.timeline().await?; + bob.sync_once().instrument(bob_span.clone()).await?; + + let item = assert_event_received(&bob_timeline, &event_id, "Hello Bob").await; + let event = item.as_event().expect("The timeline item should be an event"); + + assert_eq!( + event.forwarder().expect("We should be able to access the forwarder's ID"), + alice_id.as_str() + ); + assert_let!( + Some(TimelineDetails::Ready(profile)) = event.forwarder_profile(), + "We should be able to access the forwarder's profile" + ); + assert_eq!( + profile + .display_name + .as_ref() + .expect("We should be able to access the forwarder's display name"), + &alice_display_name + ); + + // Alice sends a second message, which Bob should receive, but have no forwarder + // info for as it was sent as part of a session they already have. + + let event_id = alice_room + .send(RoomMessageEventContent::text_plain("I said Hello, Bob")) + .await + .expect("We should be able to send a message to the room") + .response + .event_id; + + bob.sync_once().instrument(bob_span.clone()).await?; + + let item = assert_event_received(&bob_timeline, &event_id, "I said Hello, Bob").await; + assert!( + item.as_event().expect("The timeline item should be an event").forwarder().is_none(), + "There should be no forwarder for the second message" + ); + + Ok(()) +} + +/// Creates a new encryption-enabled client with the given username and +/// settings. +/// +/// # Arguments +/// +/// * `username` - The username for the client. +/// * `exclude_insecure_devices` - A boolean indicating whether to exclude +/// insecure devices. +async fn create_encryption_enabled_client( + username: &str, + exclude_insecure_devices: bool, +) -> Result { let encryption_settings = EncryptionSettings { auto_enable_cross_signing: true, ..Default::default() }; @@ -709,6 +868,7 @@ async fn create_encryption_enabled_client(username: &str) -> Result("type").unwrap().unwrap(), + "io.element.msc4268.room_key_bundle", + "Expected the event type to be 'io.element.msc4268.room_key_bundle'" + ); +} + +/// Start the given client's sync service and attach a new span to track logs. +async fn start_client_sync_service( + span: &tracing::Span, + client: &impl Deref, +) -> SyncService { + let sync_service_span = tracing::info_span!(parent: span, "sync_service"); + let sync_service = SyncService::builder(client.deref().clone()) + .with_parent_span(sync_service_span) + .build() + .await + .expect("Could not build sync service"); + + client.encryption().wait_for_e2ee_initialization_tasks().await; + sync_service.start().await; + sync_service +}