Merge branch 'main' into main
Signed-off-by: JoFrost <20685007+JoFrost@users.noreply.github.com>
This commit is contained in:
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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(_));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user