Merge branch 'main' into main

Signed-off-by: JoFrost <20685007+JoFrost@users.noreply.github.com>
This commit is contained in:
JoFrost
2026-01-07 19:58:20 +02:00
committed by GitHub
72 changed files with 1276 additions and 1223 deletions
+1 -1
View File
@@ -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"
Generated
+25 -15
View File
@@ -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",
+1 -1
View File
@@ -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",
+10 -1
View File
@@ -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<TimelineEventType, i64>` 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
+30 -15
View File
@@ -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<SdkShieldState> 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<String>,
forwarder_profile: Option<ProfileDetails>,
is_own: bool,
is_editable: bool,
content: TimelineItemContent,
@@ -1034,6 +1032,8 @@ impl From<matrix_sdk_ui::timeline::EventTimelineItem> 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<TimelineDetails<Profile>> for ProfileDetails {
}
}
impl From<&TimelineDetails<Profile>> for ProfileDetails {
fn from(details: &TimelineDetails<Profile>) -> 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<matrix_sdk_ui::timeline::EventTimelineIt
#[matrix_sdk_ffi_macros::export]
impl LazyTimelineItemProvider {
/// Returns the shields for this event timeline item.
fn get_shields(&self, strict: bool) -> Option<ShieldState> {
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.
+12 -2
View File
@@ -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)).
@@ -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);
});
}
@@ -1,5 +1,5 @@
---
source: crates/matrix-sdk-common/src/deserialized_responses.rs
expression: "ShieldStateCode::SentInClear"
expression: "ShieldStateCode::VerificationViolation"
---
"SentInClear"
"VerificationViolation"
+1 -1
View File
@@ -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"
@@ -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();
@@ -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.
@@ -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.
///
@@ -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,
};
@@ -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`.
@@ -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.
@@ -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,
};
@@ -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,
};
@@ -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`.
@@ -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
@@ -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`.
@@ -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;
+2 -2
View File
@@ -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")]
@@ -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},
};
@@ -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<impl EventCacheStore, EventCacheStoreError> {
/// async fn get_event_cache_store()
/// -> Result<impl EventCacheStore, EventCacheStoreError> {
/// Ok(MyStore::new())
/// }
///
@@ -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;
@@ -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))]
@@ -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;
@@ -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},
};
@@ -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},
};
@@ -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;
@@ -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(())
}
@@ -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;
@@ -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},
};
@@ -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};
@@ -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;
@@ -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;
@@ -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;
@@ -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";
@@ -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::<JsValue>().await?
{
if let Some(record) = cursor.next_record::<JsValue>().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::<Raw<AnySyncStateEvent>>(&value)?;
if let Ok(Some(event_id)) = raw_evt.get_field::<OwnedEventId>("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::<RoomInfo>(&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::<OwnedEventId>("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::<RoomInfo>(&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::<CanonicalJsonObject>()?,
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::<CanonicalJsonObject>()?,
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};
@@ -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,
},
};
@@ -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<KeyBuffer, OpenDirectoryError> {
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);
}
}
@@ -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,
@@ -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,
@@ -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,
@@ -160,6 +160,8 @@ impl SqliteStateStore {
};
this.run_migrations(version, None).await?;
this.read().await?.wal_checkpoint().await;
Ok(this)
}
+15
View File
@@ -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<usize> {
let page_size =
self.query_row("PRAGMA page_size;", (), |row| row.get::<_, usize>(0)).await?;
+9 -1
View File
@@ -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
@@ -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?
@@ -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,
@@ -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 {
@@ -180,6 +180,8 @@ impl<P: RoomDataProvider> TimelineState<P> {
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.
@@ -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<OwnedUserId>`: The user ID of the forwarder, if available.
/// - `Option<Profile>`: The profile of the forwarder, if available.
async fn get_forwarder_info<P: RoomDataProvider>(
event: &TimelineEvent,
room_data_provider: &P,
) -> (Option<OwnedUserId>, Option<Profile>) {
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())
}
@@ -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,
@@ -108,6 +108,16 @@ impl Flow {
pub(super) struct TimelineEventContext {
pub(super) sender: OwnedUserId,
pub(super) sender_profile: Option<Profile>,
/// 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<OwnedUserId>,
/// 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<Profile>,
/// The event's `origin_server_ts` field (or creation time for local echo).
pub(super) timestamp: MilliSecondsSinceUnixEpoch,
pub(super) read_receipts: IndexMap<OwnedUserId, Receipt>,
@@ -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,
@@ -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<Profile>,
/// 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<OwnedUserId>,
/// 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<TimelineDetails<Profile>>,
/// 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<Profile>,
forwarder: Option<OwnedUserId>,
forwarder_profile: Option<TimelineDetails<Profile>>,
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<Profile>> {
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<ShieldState> {
/// 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<ShieldState> 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<ShieldStateCode> 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,
}
}
}
+2 -1
View File
@@ -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},
@@ -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);
}
@@ -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": {
@@ -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,
},
},
@@ -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;
@@ -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
@@ -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…
@@ -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);
}
+4
View File
@@ -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))
+86 -9
View File
@@ -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<discover_homeserver::Response> {
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<discover_homeserver::Response> {
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(_));
}
}
+14 -11
View File
@@ -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<SlidingSyncListBuilder>,
extensions: Option<http::request::Extensions>,
subscriptions: BTreeMap<OwnedRoomId, http::request::RoomSubscription>,
room_subscriptions: BTreeMap<OwnedRoomId, http::request::RoomSubscription>,
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,
@@ -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,
+18 -39
View File
@@ -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<http::request::List, Error> {
self.inner.next_request(txn_id)
pub(super) fn next_request(&self) -> Result<http::request::List, Error> {
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<dyn Fn(&SlidingSyncListRequestGenerator) -> 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<SlidingSyncStickyManager<SlidingSyncListStickyParameters>>,
/// Any filters to apply to the query.
filters: Option<http::request::ListFilters>,
/// Required states to return per room.
required_state: Vec<(StateEventType, String)>,
/// The maximum number of timeline events to query for.
timeline_limit: StdRwLock<Bound>,
@@ -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<http::request::List, Error> {
fn next_request(&self) -> Result<http::request::List, Error> {
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);
@@ -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<http::request::ListFilters>,
}
impl SlidingSyncListStickyParameters {
pub fn new(
required_state: Vec<(StateEventType, String)>,
filters: Option<http::request::ListFilters>,
) -> 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();
}
}
File diff suppressed because it is too large Load Diff
@@ -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<OwnedTransactionId>,
}
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<D: StickyData> {
/// 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<OwnedTransactionId>,
}
impl<D: StickyData> SlidingSyncStickyManager<D> {
/// 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());
}
}
@@ -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::<String>("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<SyncTokenAwareClient> {
/// 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<SyncTokenAwareClient> {
let encryption_settings =
EncryptionSettings { auto_enable_cross_signing: true, ..Default::default() };
@@ -709,6 +868,7 @@ async fn create_encryption_enabled_client(username: &str) -> Result<SyncTokenAwa
.use_sqlite()
.encryption_settings(encryption_settings)
.enable_share_history_on_invite(true)
.exclude_insecure_devices(exclude_insecure_devices)
.build()
.await?,
);
@@ -825,3 +985,36 @@ async fn assert_utd_history_not_shared(timeline: &Timeline, event_id: &EventId)
MissingMegolmSession { withheld_code: Some(WithheldCode::HistoryNotShared) }
);
}
/// Asserts that the given `sync_response` contains exactly one to-device event
/// and that the event is a decrypted room key bundle.
fn assert_received_room_key_bundle(sync_response: matrix_sdk::sync::SyncResponse) {
assert_eq!(sync_response.to_device.len(), 1, "Expected exactly one to-device event");
let to_device_event = &sync_response.to_device[0];
assert_let!(
ProcessedToDeviceEvent::Decrypted { raw, .. } = to_device_event,
"Expected the to-device event to be decrypted"
);
assert_eq!(
raw.get_field::<String>("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<Target = Client>,
) -> 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
}