Compare commits

...

21 Commits

Author SHA1 Message Date
ganfra f92ccd8d40 feat(ffi): expose BeaconError from sdk to ffi
Benchmarks / Run Benchmarks (crypto_bench) (push) Failing after 1m29s
Benchmarks / Run Benchmarks (event_cache) (push) Failing after 6s
Benchmarks / Run Benchmarks (linked_chunk) (push) Failing after 6s
Benchmarks / Run Benchmarks (room_list) (push) Failing after 7s
Benchmarks / Run Benchmarks (store_bench) (push) Failing after 7s
Benchmarks / Run Benchmarks (timeline) (push) Failing after 7s
Bindings tests / xtask (push) Failing after 2m6s
Bindings tests / Run Complement Crypto tests (push) Failing after 0s
Rust tests / xtask (push) Failing after 42s
Rust tests / 🐧 [m], experimental-encrypted-state-events (push) Has been skipped
Rust tests / 🐧 [m], markdown (push) Has been skipped
Rust tests / 🐧 [m], no-encryption (push) Has been skipped
Rust tests / 🐧 [m], no-encryption-and-sqlite (push) Has been skipped
Rust tests / 🐧 [m], no-sqlite (push) Has been skipped
Rust tests / 🐧 [m], search (push) Has been skipped
Rust tests / 🐧 [m], socks (push) Has been skipped
Rust tests / 🐧 [m], sqlite-cryptostore (push) Has been skipped
Rust tests / 🐧 [m], sso-login (push) Has been skipped
Rust tests / 🐧 [m]-examples (push) Has been skipped
Rust tests / 🐧 [m]-crypto (push) Has been skipped
Rust tests / 🕸️ [m]-indexeddb (push) Has been skipped
Rust tests / 🕸️ [m]-base (push) Has been skipped
Rust tests / 🕸️ [m]-common (push) Has been skipped
Rust tests / 🕸️ [m], indexeddb stores (push) Has been skipped
Rust tests / 🕸️ [m], indexeddb stores, no crypto (push) Has been skipped
Rust tests / 🕸️ [m], no-default (push) Has been skipped
Rust tests / 🕸️ [m]-qrcode (push) Has been skipped
Rust tests / 🕸️ [m]-ui (push) Has been skipped
Rust tests / Lint (push) Has been skipped
Rust tests / 🐧 all crates, 🦀 beta (push) Failing after 1m4s
Rust tests / 🐧 all crates, 🦀 stable (push) Failing after 31s
Rust tests / Spell Check with Typos (push) Failing after 1m43s
Rust tests / Integration test (features: default) (push) Failing after 1m20s
Rust tests / Integration test (features: experimental-encrypted-state-events) (push) Failing after 36s
Code Coverage / xtask (push) Failing after 29s
Code Coverage / Code Coverage (push) Has been skipped
Documentation / All crates (push) Failing after 2m1s
Rust version / msrv (push) Failing after 40s
Lint GHA workflows with zizmor / Run zizmor (push) Failing after 9m53s
Bindings tests / Test UniFFI bindings generation (push) Has been skipped
Bindings tests / matrix-rust-components-kotlin (push) Has been skipped
Bindings tests / Generate Crypto FFI Apple XCFramework (push) Has been cancelled
Rust tests / 🍏 all crates, 🦀 stable (push) Has been cancelled
Bindings tests / matrix-rust-components-swift (push) Has been cancelled
2026-04-23 11:25:24 +02:00
Jorge Martín 2c2e0f1b15 doc: Add doc comments and changelog entries 2026-04-22 17:26:51 +02:00
Jorge Martín f314578df5 feat(ffi): Add Client::get_dm_rooms
Expose the new function in the FFI layer
2026-04-22 17:26:51 +02:00
Jorge Martín 739289cd82 feat(ui): Add Client::get_dm_rooms
This function returns an iterator with all the available rooms that match the behaviour previously in `Client::get_dm`. `Client::get_dm` now just calls this function and returns the first item.
2026-04-22 17:26:51 +02:00
Kévin Commaille 1d7d6c943b feat(sdk): Add an expiry to the cached homeserver capabilities
I believe that it is a footgun to cache the data indefinitely by default
so this copies the same behavior as for the supported versions in the
ClientCaches: it sets an expiry duration of 1 day and refreshes the data
in the background when it has expired.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-22 14:21:34 +03:00
Damir Jelić 917f9ee298 chore: Bump vodozemac 2026-04-22 12:30:38 +02:00
Andy Balaam c7d44ddad3 Change in to into
Signed-off-by: Andy Balaam <mail@artificialworlds.net>
2026-04-22 10:52:25 +01:00
Kévin Commaille 1a170ddf90 fix(common): The expiry duration of TtlValue is 1 day
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-22 10:52:25 +01:00
Kévin Commaille 696d1cacbe refactor(common): Don't check expiry for TtlValue::into_data()
This API is no longer in use so we can remove it. We also rename
`into_data_unchecked()` to `into_data()`.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-22 10:52:25 +01:00
Kévin Commaille c6d1cf20b6 feat(sdk): Refresh well-known cache in the background
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-22 10:52:25 +01:00
Jonas Platte 38f34e66eb Collapse an else { if ... }
as suggested by clippy.
2026-04-22 08:52:04 +02:00
Jonas Platte 8fa411aaa0 Clean up a lint 2026-04-22 08:52:04 +02:00
Jonas Platte b4b3b5258c Clean up some imports 2026-04-22 08:52:04 +02:00
Andy Balaam fd6cb6647f Support stable prefix for MSC4287 m.key_backup 2026-04-21 14:17:26 +01:00
Andy Balaam 103ff7d3ec Use stable identifier for m.shared_history 2026-04-21 13:47:19 +01:00
Andy Balaam efa28a1ffd Use stable identifiers for m.room_key_bundle and m.history_not_shared 2026-04-21 13:47:19 +01:00
Andy Balaam c4640897d4 Test for serialization and deserialization of a room key bundle 2026-04-21 13:47:19 +01:00
Benjamin Bouvier 0487a143ed test(integration): workaround non-working subscription to make the test pass in CI 2026-04-21 14:43:55 +02:00
Benjamin Bouvier 7ab97d59b8 test(integration): add an integration test for computing latest events in a few rooms 2026-04-21 14:43:55 +02:00
Benjamin Bouvier 940862ebed chore: bump the integration testing synapse version to v1.151.0 2026-04-21 14:43:55 +02:00
Jorge Martín e507eaabf6 feat(ffi): Expose ffi::NotificationRoomInfo::service_members
This is needed in some clients to know if a direct room is a DM or not
2026-04-21 13:22:58 +02:00
58 changed files with 1081 additions and 328 deletions
Generated
+46 -45
View File
@@ -453,9 +453,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.6.0"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "basic-toml"
@@ -642,7 +642,7 @@ dependencies = [
"semver",
"serde",
"serde_json",
"thiserror 2.0.17",
"thiserror 2.0.18",
]
[[package]]
@@ -3239,7 +3239,7 @@ dependencies = [
"similar-asserts",
"stream_assert",
"tempfile",
"thiserror 2.0.17",
"thiserror 2.0.18",
"tokio",
"tokio-stream",
"tokio-test",
@@ -3290,7 +3290,7 @@ dependencies = [
"serde_json",
"similar-asserts",
"stream_assert",
"thiserror 2.0.17",
"thiserror 2.0.18",
"tokio",
"tracing",
"unicode-normalization",
@@ -3320,7 +3320,7 @@ dependencies = [
"ruma",
"serde",
"serde_json",
"thiserror 2.0.17",
"thiserror 2.0.18",
"tokio",
"tracing",
"tracing-subscriber",
@@ -3371,7 +3371,7 @@ dependencies = [
"similar-asserts",
"stream_assert",
"subtle",
"thiserror 2.0.17",
"thiserror 2.0.18",
"time",
"tokio",
"tokio-stream",
@@ -3404,7 +3404,7 @@ dependencies = [
"serde_json",
"sha2",
"tempfile",
"thiserror 2.0.17",
"thiserror 2.0.18",
"tokio",
"tracing-subscriber",
"uniffi",
@@ -3446,7 +3446,7 @@ dependencies = [
"serde_json",
"similar-asserts",
"tempfile",
"thiserror 2.0.17",
"thiserror 2.0.18",
"tokio",
"tracing",
"tracing-appender",
@@ -3496,7 +3496,7 @@ dependencies = [
"serde-wasm-bindgen",
"serde_json",
"sha2",
"thiserror 2.0.17",
"thiserror 2.0.18",
"tokio",
"tracing",
"tracing-subscriber",
@@ -3547,7 +3547,7 @@ dependencies = [
"image",
"qrcode",
"ruma",
"thiserror 2.0.17",
"thiserror 2.0.18",
"vodozemac",
]
@@ -3567,7 +3567,7 @@ dependencies = [
"sha2",
"tantivy",
"tempfile",
"thiserror 2.0.17",
"thiserror 2.0.18",
"tracing",
"zeroize",
]
@@ -3598,7 +3598,7 @@ dependencies = [
"serde_path_to_error",
"similar-asserts",
"tempfile",
"thiserror 2.0.17",
"thiserror 2.0.18",
"tokio",
"tracing",
"vodozemac",
@@ -3621,7 +3621,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"thiserror 2.0.17",
"thiserror 2.0.18",
"zeroize",
]
@@ -3701,7 +3701,7 @@ dependencies = [
"serde_json",
"stream_assert",
"tempfile",
"thiserror 2.0.17",
"thiserror 2.0.18",
"tokio",
"tokio-stream",
"tracing",
@@ -3730,7 +3730,7 @@ dependencies = [
"serde",
"serde-wasm-bindgen",
"smallvec",
"thiserror 2.0.17",
"thiserror 2.0.18",
"tokio",
"wasm-bindgen",
"wasm-bindgen-futures",
@@ -4124,7 +4124,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6"
dependencies = [
"memchr",
"thiserror 2.0.17",
"thiserror 2.0.18",
"ucd-trie",
]
@@ -4358,9 +4358,9 @@ dependencies = [
[[package]]
name = "prost"
version = "0.13.4"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec"
checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568"
dependencies = [
"bytes",
"prost-derive",
@@ -4368,12 +4368,12 @@ dependencies = [
[[package]]
name = "prost-derive"
version = "0.13.4"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3"
checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
dependencies = [
"anyhow",
"itertools 0.13.0",
"itertools 0.14.0",
"proc-macro2",
"quote",
"syn 2.0.101",
@@ -4429,7 +4429,7 @@ dependencies = [
"rustc-hash",
"rustls",
"socket2 0.6.0",
"thiserror 2.0.17",
"thiserror 2.0.18",
"tokio",
"tracing",
"web-time",
@@ -4451,7 +4451,7 @@ dependencies = [
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.17",
"thiserror 2.0.18",
"tinyvec",
"tracing",
"web-time",
@@ -4668,7 +4668,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
dependencies = [
"getrandom 0.2.15",
"libredox",
"thiserror 2.0.17",
"thiserror 2.0.18",
]
[[package]]
@@ -4834,7 +4834,7 @@ dependencies = [
"serde",
"serde_html_form",
"serde_json",
"thiserror 2.0.17",
"thiserror 2.0.18",
"url",
"web-time",
]
@@ -4862,7 +4862,7 @@ dependencies = [
"serde",
"serde_html_form",
"serde_json",
"thiserror 2.0.17",
"thiserror 2.0.18",
"time",
"tracing",
"url",
@@ -4888,7 +4888,7 @@ dependencies = [
"ruma-macros",
"serde",
"serde_json",
"thiserror 2.0.17",
"thiserror 2.0.18",
"tracing",
"web-time",
"wildmatch",
@@ -4912,7 +4912,7 @@ dependencies = [
"ruma-signatures",
"serde",
"serde_json",
"thiserror 2.0.17",
"thiserror 2.0.18",
"tracing",
]
@@ -4933,7 +4933,7 @@ version = "0.12.0"
source = "git+https://github.com/ruma/ruma?rev=7680eebd9586669e1a4e5b1fd1c2c691221369d4#7680eebd9586669e1a4e5b1fd1c2c691221369d4"
dependencies = [
"js_int",
"thiserror 2.0.17",
"thiserror 2.0.18",
]
[[package]]
@@ -4964,7 +4964,7 @@ dependencies = [
"ruma-common",
"serde_json",
"sha2",
"thiserror 2.0.17",
"thiserror 2.0.18",
]
[[package]]
@@ -5320,7 +5320,7 @@ dependencies = [
"rand 0.9.2",
"serde",
"serde_json",
"thiserror 2.0.17",
"thiserror 2.0.18",
"time",
"url",
"uuid",
@@ -5349,11 +5349,12 @@ dependencies = [
[[package]]
name = "serde_bytes"
version = "0.11.15"
version = "0.11.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a"
checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
dependencies = [
"serde",
"serde_core",
]
[[package]]
@@ -5800,7 +5801,7 @@ dependencies = [
"tantivy-stacker",
"tantivy-tokenizer-api",
"tempfile",
"thiserror 2.0.17",
"thiserror 2.0.18",
"time",
"uuid",
"winapi",
@@ -5945,11 +5946,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.17"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl 2.0.17",
"thiserror-impl 2.0.18",
]
[[package]]
@@ -5965,9 +5966,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
version = "2.0.17"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
@@ -6282,7 +6283,7 @@ version = "0.2.3"
source = "git+https://github.com/tokio-rs/tracing.git?rev=20f5b3d8ba057ca9c4ae00ad30dda3dce8a71c05#20f5b3d8ba057ca9c4ae00ad30dda3dce8a71c05"
dependencies = [
"crossbeam-channel",
"thiserror 2.0.17",
"thiserror 2.0.18",
"time",
"tracing-subscriber",
]
@@ -6361,7 +6362,7 @@ dependencies = [
"itertools 0.14.0",
"ratatui",
"strum 0.27.2",
"thiserror 2.0.17",
"thiserror 2.0.18",
]
[[package]]
@@ -6761,9 +6762,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "vodozemac"
version = "0.9.0"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c022a277687e4e8685d72b95a7ca3ccfec907daa946678e715f8badaa650883d"
checksum = "b98bf83c0992966775b8012f194b07b44928996163e5a05b741b43891571ae5b"
dependencies = [
"aes",
"arrayvec",
@@ -6784,7 +6785,7 @@ dependencies = [
"serde_json",
"sha2",
"subtle",
"thiserror 2.0.17",
"thiserror 2.0.18",
"x25519-dalek",
"zeroize",
]
+1 -1
View File
@@ -119,7 +119,7 @@ uniffi_bindgen = { version = "0.31.0", default-features = false, features = ["ca
url = { version = "2.5.7", default-features = false }
uuid = { version = "1.18.1", default-features = false }
vergen-gitcl = { version = "1.0.8", default-features = false }
vodozemac = { version = "0.9.0", default-features = false, features = ["libolm-compat", "insecure-pk-encryption"] }
vodozemac = { version = "0.10.0", default-features = false, features = ["libolm-compat", "insecure-pk-encryption", "experimental-session-config"] }
wasm-bindgen = { version = "0.2.105", default-features = false }
wasm-bindgen-test = { version = "0.3.55", default-features = false, features = ["std"] }
web-sys = { version = "0.3.82", default-features = false }
+4 -4
View File
@@ -1015,18 +1015,18 @@ impl PkEncryption {
}
/// Encrypt a message using this [`PkEncryption`] object.
pub fn encrypt(&self, plaintext: &str) -> PkMessage {
pub fn encrypt(&self, plaintext: &str) -> Option<PkMessage> {
use vodozemac::base64_encode;
let message = self.inner.encrypt(plaintext.as_ref());
let message = self.inner.encrypt(plaintext.as_ref()).ok()?;
let vodozemac::pk_encryption::Message { ciphertext, mac, ephemeral_key } = message;
PkMessage {
Some(PkMessage {
ciphertext: base64_encode(ciphertext),
mac: base64_encode(mac),
ephemeral_key: ephemeral_key.to_base64(),
}
})
}
}
+5
View File
@@ -44,6 +44,11 @@ All notable changes to this project will be documented in this file.
### Features
- Add `Client::get_dm_rooms` function to get a list with the DMs for the provided user id.
([#6487](https://github.com/matrix-org/matrix-rust-sdk/pull/6487))
- Expose `ffi::NotificationRoomInfo::service_members` so clients can use the list of service
members to calculate if a room is a DM from the notification info.
([#6474](https://github.com/matrix-org/matrix-rust-sdk/pull/6474))
- Enable `experimental-push-secrets` feature by default.
([#6473](https://github.com/matrix-org/matrix-rust-sdk/pull/6394))
- Add new high-level search helpers `RoomSearchIterator` and `GlobalSearchIterator` to perform
+17
View File
@@ -1483,6 +1483,7 @@ impl Client {
Ok(room)
}
/// Get the first existing DM room with the given user, if any.
pub fn get_dm_room(&self, user_id: String) -> Result<Option<Arc<Room>>, ClientError> {
let user_id = UserId::parse(user_id)?;
let sdk_room = self.inner.get_dm_room(&user_id);
@@ -1491,6 +1492,16 @@ impl Client {
Ok(dm)
}
/// Get an iterator with the existing DM rooms for the given user.
pub fn get_dm_rooms(&self, user_id: String) -> Result<Vec<Arc<Room>>, ClientError> {
let user_id = UserId::parse(user_id)?;
let sdk_rooms = self.inner.get_dm_rooms(&user_id);
let dms = sdk_rooms
.map(|room| Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())))
.collect();
Ok(dms)
}
pub async fn search_users(
&self,
search_term: String,
@@ -2174,6 +2185,12 @@ async fn notification_handler(
topic: room.topic(),
join_rule: room.join_rule().map(TryInto::try_into).transpose().ok().flatten(),
joined_members_count: room.joined_members_count(),
service_members: room
.service_members()
.unwrap_or_default()
.iter()
.map(ToString::to_string)
.collect(),
is_encrypted: Some(room.encryption_state().is_encrypted()),
is_direct,
is_space: room.is_space(),
+33
View File
@@ -345,6 +345,39 @@ pub enum RoomError {
FailedSendingAttachment,
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum LiveLocationError {
#[error("Network error")]
Network,
#[error("Existing beacon information not found")]
NotFound,
#[error("Beacon event is redacted and cannot be processed")]
Redacted,
#[error("Must join the room to access beacon information")]
Stripped,
#[error("The beacon event has expired")]
NotLive,
#[error("Deserialization error")]
Deserialization,
#[error("Other error: {msg}")]
Other { msg: String },
}
impl From<matrix_sdk::BeaconError> for LiveLocationError {
fn from(value: matrix_sdk::BeaconError) -> Self {
match value {
matrix_sdk::BeaconError::Network(_) => Self::Network,
matrix_sdk::BeaconError::NotFound => Self::NotFound,
matrix_sdk::BeaconError::Redacted => Self::Redacted,
matrix_sdk::BeaconError::Stripped => Self::Stripped,
matrix_sdk::BeaconError::Deserialization(_) => Self::Deserialization,
matrix_sdk::BeaconError::NotLive => Self::NotLive,
matrix_sdk::BeaconError::Other(err) => Self::Other { msg: err.to_string() },
}
}
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum MediaInfoError {
@@ -49,6 +49,7 @@ pub struct NotificationRoomInfo {
pub topic: Option<String>,
pub join_rule: Option<JoinRule>,
pub joined_members_count: u64,
pub service_members: Vec<String>,
pub is_encrypted: Option<bool>,
pub is_direct: bool,
pub is_space: bool,
@@ -106,6 +107,7 @@ impl NotificationItem {
topic: item.room_topic,
join_rule: item.room_join_rule.map(TryInto::try_into).transpose().ok().flatten(),
joined_members_count: item.joined_members_count,
service_members: item.service_members,
is_encrypted: item.is_room_encrypted,
is_direct: item.is_direct_message_room,
is_space: item.is_space,
+8 -8
View File
@@ -53,7 +53,10 @@ use crate::{
TaskHandle,
chunk_iterator::ChunkIterator,
client::{JoinRule, RoomVisibility},
error::{ClientError, MediaInfoError, NotYetImplemented, QueueWedgeError, RoomError},
error::{
ClientError, LiveLocationError, MediaInfoError, NotYetImplemented, QueueWedgeError,
RoomError,
},
event::TimelineEvent,
identity_status_change::IdentityStatusChange,
live_location_share::LiveLocationShares,
@@ -1084,17 +1087,14 @@ impl Room {
}
/// Stop the current users live location share in the room.
pub async fn stop_live_location_share(&self) -> Result<(), ClientError> {
self.inner.stop_live_location_share().await.expect("Unable to stop live location share");
pub async fn stop_live_location_share(&self) -> Result<(), LiveLocationError> {
self.inner.stop_live_location_share().await?;
Ok(())
}
/// Send the current users live location beacon in the room.
pub async fn send_live_location(&self, geo_uri: String) -> Result<(), ClientError> {
self.inner
.send_location_beacon(geo_uri)
.await
.expect("Unable to send live location beacon");
pub async fn send_live_location(&self, geo_uri: String) -> Result<(), LiveLocationError> {
self.inner.send_location_beacon(geo_uri).await?;
Ok(())
}
@@ -540,7 +540,7 @@ impl StateStoreIntegrationTests for DynStateStore {
Ok(Some(StateStoreDataValue::SupportedVersions(stored_supported_versions))) =
self.get_kv_data(StateStoreDataKey::SupportedVersions).await
);
assert_let!(Some(stored_supported_versions) = stored_supported_versions.into_data());
let stored_supported_versions = stored_supported_versions.into_data();
assert_eq!(supported_versions, stored_supported_versions);
let stored_supported = stored_supported_versions.supported_versions();
@@ -572,7 +572,7 @@ impl StateStoreIntegrationTests for DynStateStore {
Ok(Some(StateStoreDataValue::WellKnown(stored_well_known))) =
self.get_kv_data(StateStoreDataKey::WellKnown).await
);
assert_let!(Some(stored_well_known) = stored_well_known.into_data());
let stored_well_known = stored_well_known.into_data();
assert_eq!(stored_well_known, Some(well_known));
self.remove_kv_data(StateStoreDataKey::WellKnown).await?;
@@ -588,7 +588,7 @@ impl StateStoreIntegrationTests for DynStateStore {
Ok(Some(StateStoreDataValue::WellKnown(stored_well_known))) =
self.get_kv_data(StateStoreDataKey::WellKnown).await
);
assert_let!(Some(stored_well_known) = stored_well_known.into_data());
let stored_well_known = stored_well_known.into_data();
assert_eq!(stored_well_known, None);
Ok(())
@@ -93,7 +93,7 @@ struct MemoryStoreInner {
seen_knock_requests: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, OwnedUserId>>,
thread_subscriptions: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, StoredThreadSubscription>>,
thread_subscriptions_catchup_tokens: Option<Vec<ThreadSubscriptionCatchupToken>>,
homeserver_capabilities: Option<Capabilities>,
homeserver_capabilities: Option<TtlValue<Capabilities>>,
}
/// In-memory, non-persistent implementation of the `StateStore`.
+2 -2
View File
@@ -1145,7 +1145,7 @@ pub enum StateStoreDataValue {
ThreadSubscriptionsCatchupTokens(Vec<ThreadSubscriptionCatchupToken>),
/// The capabilities the homeserver supports or disables.
HomeserverCapabilities(Capabilities),
HomeserverCapabilities(TtlValue<Capabilities>),
}
/// Tokens to use when catching up on thread subscriptions.
@@ -1350,7 +1350,7 @@ impl StateStoreDataValue {
/// Get this value if it is the data for the capabilities the homeserver
/// supports or disables.
pub fn into_homeserver_capabilities(self) -> Option<Capabilities> {
pub fn into_homeserver_capabilities(self) -> Option<TtlValue<Capabilities>> {
as_variant!(self, Self::HomeserverCapabilities)
}
}
+3
View File
@@ -8,6 +8,9 @@ All notable changes to this project will be documented in this file.
### Features
- [**breaking**] Change to the stable identifiers for `m.history_not_shared`.
We still support reading the unstable identifier.
([#6467](https://github.com/matrix-org/matrix-rust-sdk/pull/6467))
- Add a method to check the validity of edits.
([#6454](https://github.com/matrix-org/matrix-rust-sdk/pull/6454))
- A background task monitor has been added, that can spawn background tasks and monitor their
@@ -1249,7 +1249,7 @@ pub enum WithheldCode {
/// that the session was not marked as "shared_history".
///
/// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
#[ruma_enum(rename = "io.element.msc4268.history_not_shared", alias = "m.history_not_shared")]
#[ruma_enum(rename = "m.history_not_shared", alias = "io.element.msc4268.history_not_shared")]
HistoryNotShared,
#[doc(hidden)]
+2 -7
View File
@@ -147,7 +147,7 @@ pub struct TtlValue<T> {
impl<T> TtlValue<T> {
/// The number of milliseconds after which the data is considered stale.
///
/// This matches 7 days.
/// This matches 1 day.
pub const STALE_THRESHOLD: f64 = (1000 * 60 * 60 * 24) as _;
/// Construct a new `TtlValue` with the given data.
@@ -191,14 +191,9 @@ impl<T> TtlValue<T> {
}
/// Get the data of this value.
pub fn into_data_unchecked(self) -> T {
pub fn into_data(self) -> T {
self.data
}
/// Get the data of this value if it hasn't expired.
pub fn into_data(self) -> Option<T> {
(!self.has_expired()).then_some(self.data)
}
}
/// Get the current timestamp as the number of milliseconds since Unix Epoch.
+6 -6
View File
@@ -8,12 +8,15 @@ All notable changes to this project will be documented in this file.
### Features
- [**breaking**] Change to the stable identifiers for `m.room_key_bundle`,
`m.history_not_shared` and `m.shared_history`. We still support reading the
unstable identifiers.
([#6467](https://github.com/matrix-org/matrix-rust-sdk/pull/6467))
- Add support for MSC4385.
([#6164](https://github.com/matrix-org/matrix-rust-sdk/pull/6164))
- Add new method `OlmMachine::push_secret_to_verified_devices`.
- Pushed secrets that we receive from verified devices are added to the
secrets inbox.
- Add `Store::{store,clear}_room_pending_key_bundle`,
`CryptoStore::get_pending_key_bundle_details_for_room` and
`CryptoStore::get_all_rooms_pending_key_bundle`, which can be used by
@@ -40,28 +43,26 @@ All notable changes to this project will be documented in this file.
### Refactor
- [**breaking**] The `MegolmV1BackupKey::encrypt` now returns a `Result`
([#6477](https://github.com/matrix-org/matrix-rust-sdk/pull/6477))
- [**breaking**] `CryptoStore::get_secrets_from_inbox` now returns a `Vec` of
the secrets as strings, rather than a `Vec` of `GossippedSecret` structs.
([#6164](https://github.com/matrix-org/matrix-rust-sdk/pull/6164))
- [**breaking**] `store::types::Changes::sessions` now stores a `Vec` of
`SecretsInboxItem`.
([#6164](https://github.com/matrix-org/matrix-rust-sdk/pull/6164))
- **breaking** The `BackupDecryptionKey::new` and `DehydratedDeviceKey::new`
methods became infallible, they don't return a `Result` anymore.
([#5502](https://github.com/matrix-org/matrix-rust-sdk/pull/5502))
- [**breaking**] Remove cross-process lock generation logic from `OlmMachine`, which is now
implemented more generally in `matrix_sdk_common::cross_process_lock::CrossProcessLock`.
([#6326](https://github.com/matrix-org/matrix-rust-sdk/pull/6326))
- [**breaking**] The `MediaEncryptionInfo` fields changed to match the new fields of `EncryptedFile`
from Ruma. The serialized JSON format did not change and still matches the format of
`EncryptedFile` defined in the spec, without the `url` field. The `DecryptorError::KeyNonceLength`
variant was removed because the length of the key and nonce are now enforced in
`MediaEncryptionInfo`.
([#6346](https://github.com/matrix-org/matrix-rust-sdk/pull/6346))
- [**breaking**] Removed `WithLocking` from `EncryptionSyncService` and replaced it with `CrossProcessLockConfig`.
([#6160](https://github.com/matrix-org/matrix-rust-sdk/pull/6160))
- [**breaking**] The QrcodeData struct has been reworked in preparation to
@@ -70,7 +71,6 @@ All notable changes to this project will be documented in this file.
returns an MSC-specific struct now. The `rendezvous_url()` method has been
removed.
([#6081](https://github.com/matrix-org/matrix-rust-sdk/pull/6081))
- [**breaking**] The `message-ids` feature has been removed. It was already a no-op and has now
been eliminated entirely.
([#5963](https://github.com/matrix-org/matrix-rust-sdk/pull/5963))
@@ -101,7 +101,10 @@ impl MegolmV1BackupKey {
/// Export the given inbound group session, and encrypt the data, ready for
/// writing to the backup.
pub async fn encrypt(&self, session: InboundGroupSession) -> KeyBackupData {
pub async fn encrypt(
&self,
session: InboundGroupSession,
) -> Result<KeyBackupData, vodozemac::pk_encryption::Error> {
let pk = PkEncryption::from_key(self.inner.key);
// The forwarding chains don't mean much, we only care whether we received the
@@ -117,7 +120,7 @@ impl MegolmV1BackupKey {
let key =
Zeroizing::new(serde_json::to_vec(&key).expect("Can't serialize exported room key"));
let message = pk.encrypt(&key);
let message = pk.encrypt(&key)?;
let session_data = EncryptedSessionDataInit {
ephemeral: Base64::new(message.ephemeral_key.to_vec()),
@@ -126,7 +129,7 @@ impl MegolmV1BackupKey {
}
.into();
KeyBackupDataInit {
Ok(KeyBackupDataInit {
first_message_index,
forwarded_count,
// TODO: is this actually used anywhere? seems to be completely
@@ -136,6 +139,6 @@ impl MegolmV1BackupKey {
is_verified: false,
session_data,
}
.into()
.into())
}
}
@@ -397,7 +397,7 @@ mod tests {
let decryption_key = BackupDecryptionKey::new();
let encryption_key = decryption_key.megolm_v1_public_key();
let encrypted = encryption_key.encrypt(session).await;
let encrypted = encryption_key.encrypt(session).await.unwrap();
let _ = decryption_key
.decrypt_session_data(encrypted.session_data)
+10 -7
View File
@@ -534,7 +534,7 @@ impl BackupMachine {
}
let key_count = sessions.len();
let (backup, session_record) = Self::backup_keys(sessions, backup_key).await;
let (backup, session_record) = Self::backup_keys(sessions, backup_key).await?;
info!(
key_count = key_count,
@@ -556,10 +556,13 @@ impl BackupMachine {
async fn backup_keys(
sessions: Vec<InboundGroupSession>,
backup_key: &MegolmV1BackupKey,
) -> (
BTreeMap<OwnedRoomId, RoomKeyBackup>,
BTreeMap<OwnedRoomId, BTreeMap<SenderKey, BTreeSet<SessionId>>>,
) {
) -> Result<
(
BTreeMap<OwnedRoomId, RoomKeyBackup>,
BTreeMap<OwnedRoomId, BTreeMap<SenderKey, BTreeSet<SessionId>>>,
),
vodozemac::pk_encryption::Error,
> {
let mut backup: BTreeMap<OwnedRoomId, RoomKeyBackup> = BTreeMap::new();
let mut session_record: BTreeMap<OwnedRoomId, BTreeMap<SenderKey, BTreeSet<SessionId>>> =
BTreeMap::new();
@@ -568,7 +571,7 @@ impl BackupMachine {
let room_id = session.room_id().to_owned();
let session_id = session.session_id().to_owned();
let sender_key = session.sender_key().to_owned();
let session = backup_key.encrypt(session).await;
let session = backup_key.encrypt(session).await?;
session_record
.entry(room_id.to_owned())
@@ -586,7 +589,7 @@ impl BackupMachine {
.insert(session_id, session);
}
(backup, session_record)
Ok((backup, session_record))
}
/// Import the given room keys into our store.
+5
View File
@@ -73,6 +73,11 @@ pub enum OlmError {
)]
MissingSession,
/// Encrypting of an Olm message failed because of a low-level cryptographic
/// issue occurred.
#[error(transparent)]
Encryption(#[from] vodozemac::olm::EncryptionError),
/// Encryption failed due to an error collecting the recipient devices.
#[error("encryption failed due to an error collecting the recipient devices: {0}")]
SessionRecipientCollectionError(SessionRecipientCollectionError),
+1 -2
View File
@@ -960,8 +960,7 @@ impl OlmMachine {
}
}
/// Handle a received, decrypted, `io.element.msc4268.room_key_bundle`
/// to-device event.
/// Handle a received, decrypted, `m.room_key_bundle` to-device event.
#[instrument()]
async fn receive_room_key_bundle_data(
&self,
@@ -272,7 +272,7 @@ pub async fn build_encrypted_to_device_content_without_sender_data(
}))
.unwrap();
let ciphertext = olm_session.encrypt_helper(&plaintext).await;
let ciphertext = olm_session.encrypt_helper(&plaintext).await.unwrap();
let content =
olm_session.build_encrypted_event(ciphertext, None).await.expect("could not encrypt");
@@ -290,7 +290,7 @@ async fn create_and_share_session_without_sender_data(
}))
.unwrap();
let ciphertext = olm_session.encrypt_helper(&plaintext).await;
let ciphertext = olm_session.encrypt_helper(&plaintext).await.unwrap();
ToDeviceEvent::new(
alice.user_id().to_owned(),
olm_session.build_encrypted_event(ciphertext, None).await.unwrap(),
@@ -296,7 +296,7 @@ async fn test_decrypt_to_device_message_with_unsigned_sender_keys() {
}))
.unwrap();
let ciphertext = alice_session.encrypt_helper(&plaintext).await;
let ciphertext = alice_session.encrypt_helper(&plaintext).await.unwrap();
let event = ToDeviceEvent::new(
alice.user_id().to_owned(),
alice_session.build_encrypted_event(ciphertext, None).await.unwrap(),
+14 -8
View File
@@ -25,8 +25,6 @@ use js_option::JsOption;
use matrix_sdk_common::deserialized_responses::{
AlgorithmInfo, DeviceLinkProblem, EncryptionInfo, VerificationLevel, VerificationState,
};
#[cfg(test)]
use ruma::api::client::dehydrated_device::DehydratedDeviceV1;
use ruma::{
CanonicalJsonValue, DeviceId, DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch,
OneTimeKeyAlgorithm, OneTimeKeyId, OwnedDeviceId, OwnedDeviceKeyId, OwnedOneTimeKeyId,
@@ -745,6 +743,8 @@ impl Account {
/// of MSC3814.
#[cfg(test)]
pub(crate) fn legacy_dehydrate(&self, pickle_key: &[u8; 32]) -> Raw<DehydratedDeviceData> {
use ruma::api::client::dehydrated_device::DehydratedDeviceV1;
let pickle_key = expand_legacy_pickle_key(pickle_key, &self.device_id);
let device_pickle = self
.inner
@@ -966,13 +966,13 @@ impl Account {
one_time_key: Curve25519PublicKey,
fallback_used: bool,
our_device_keys: DeviceKeys,
) -> Session {
let session = self.inner.create_outbound_session(config, identity_key, one_time_key);
) -> Result<Session, vodozemac::olm::SessionCreationError> {
let session = self.inner.create_outbound_session(config, identity_key, one_time_key)?;
let now = SecondsSinceUnixEpoch::now();
let session_id = session.session_id();
Session {
Ok(Session {
inner: Arc::new(Mutex::new(session)),
session_id: session_id.into(),
sender_key: identity_key,
@@ -980,7 +980,7 @@ impl Account {
created_using_fallback_key: fallback_used,
creation_time: now,
last_use_time: now,
}
})
}
#[instrument(
@@ -1066,7 +1066,7 @@ impl Account {
one_time_key,
is_fallback,
our_device_keys,
))
)?)
}
}
}
@@ -1094,7 +1094,13 @@ impl Account {
Span::current().record("session_id", debug(message.session_id()));
trace!("Creating a new Olm session from a pre-key message");
let result = self.inner.create_inbound_session(their_identity_key, message)?;
#[cfg(not(feature = "experimental-algorithms"))]
let config = SessionConfig::version_1();
#[cfg(feature = "experimental-algorithms")]
let config = SessionConfig::version_2();
let result = self.inner.create_inbound_session(config, their_identity_key, message)?;
let now = SecondsSinceUnixEpoch::now();
let session_id = result.session.session_id();
@@ -1223,12 +1223,14 @@ mod tests {
.unwrap()
}
#[async_test]
async fn test_shared_history_from_m_room_key_content() {
let content = json!({
fn key_json(stable: bool) -> serde_json::Value {
let shared_history =
if stable { "m.shared_history" } else { "org.matrix.msc3061.shared_history" };
json!({
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!Cuyf34gef24t:localhost",
"org.matrix.msc3061.shared_history": true,
shared_history: true,
"session_id": "ZFD6+OmV7fVCsJ7Gap8UnORH8EnmiAkes8FAvQuCw/I",
"session_key": "AgAAAADNp1EbxXYOGmJtyX4AkD1bvJvAUyPkbIaKxtnGKjv\
SQ3E/4mnuqdM4vsmNzpO1EeWzz1rDkUpYhYE9kP7sJhgLXi\
@@ -1237,7 +1239,12 @@ mod tests {
QrCexmqfFJzkR/BJ5ogJHrPBQL0LgsPyglIbMTLg7qygIaY\
U5Fe2QdKMH7nTZPNIRHh1RaMfHVETAUJBax88EWZBoifk80\
gdHUwHSgMk77vCc2a5KHKLDA",
});
})
}
#[async_test]
async fn test_shared_history_from_m_room_key_content_stable() {
let content = key_json(true);
let sender_key = Curve25519PublicKey::from_bytes([0; 32]);
let signing_key = Ed25519PublicKey::from_slice(&[0; 32]).expect("");
@@ -1267,20 +1274,57 @@ mod tests {
}
#[async_test]
async fn test_shared_history_from_exported_room_key() {
let content = json!({
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room:id",
"sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw",
"session_id": "/2K+V777vipCxPZ0gpY9qcpz1DYaXwuMRIu0UEP0Wa0",
"session_key": "AQAAAAAclzWVMeWBKH+B/WMowa3rb4ma3jEl6n5W4GCs9ue65CruzD3ihX+85pZ9hsV9Bf6fvhjp76WNRajoJYX0UIt7aosjmu0i+H+07hEQ0zqTKpVoSH0ykJ6stAMhdr6Q4uW5crBmdTTBIsqmoWsNJZKKoE2+ldYrZ1lrFeaJbjBIY/9ivle++74qQsT2dIKWPanKc9Q2Gl8LjESLtFBD9Fmt",
"sender_claimed_keys": {
"ed25519": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA"
},
"forwarding_curve25519_key_chain": [],
"org.matrix.msc3061.shared_history": true
async fn test_shared_history_from_m_room_key_content_unstable() {
let content = key_json(false);
});
let sender_key = Curve25519PublicKey::from_bytes([0; 32]);
let signing_key = Ed25519PublicKey::from_slice(&[0; 32]).expect("");
let mut content: room_key::MegolmV1AesSha2Content = serde_json::from_value(content)
.expect("We should be able to deserialize the m.room_key content");
let session = InboundGroupSession::from_room_key_content(sender_key, signing_key, &content)
.expect(
"We should be able to create an inbound group session from the room key content",
);
assert!(
session.shared_history,
"The shared history flag should be set as it was set in the m.room_key content"
);
content.shared_history = false;
let session = InboundGroupSession::from_room_key_content(sender_key, signing_key, &content)
.expect(
"We should be able to create an inbound group session from the room key content",
);
assert!(
!session.shared_history,
"The shared history flag should not be set as it was not set in the m.room_key content"
);
}
fn exported_key_json(stable: bool) -> serde_json::Value {
let shared_history =
if stable { "m.shared_history" } else { "org.matrix.msc3061.shared_history" };
json!({
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room:id",
"sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw",
"session_id": "/2K+V777vipCxPZ0gpY9qcpz1DYaXwuMRIu0UEP0Wa0",
"session_key": "AQAAAAAclzWVMeWBKH+B/WMowa3rb4ma3jEl6n5W4GCs9ue65CruzD3ihX+85pZ9hsV9Bf6fvhjp76WNRajoJYX0UIt7aosjmu0i+H+07hEQ0zqTKpVoSH0ykJ6stAMhdr6Q4uW5crBmdTTBIsqmoWsNJZKKoE2+ldYrZ1lrFeaJbjBIY/9ivle++74qQsT2dIKWPanKc9Q2Gl8LjESLtFBD9Fmt",
"sender_claimed_keys": {
"ed25519": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA"
},
"forwarding_curve25519_key_chain": [],
shared_history: true
})
}
#[async_test]
async fn test_shared_history_from_exported_room_key_stable() {
let content = exported_key_json(true);
let mut content: ExportedRoomKey = serde_json::from_value(content)
.expect("We should be able to deserialize the m.room_key content");
@@ -1305,8 +1349,36 @@ mod tests {
}
#[async_test]
async fn test_shared_history_from_backed_up_room_key() {
let content = json!({
async fn test_shared_history_from_exported_room_key_unstable() {
let content = exported_key_json(false);
let mut content: ExportedRoomKey = serde_json::from_value(content)
.expect("We should be able to deserialize the m.room_key content");
let session = InboundGroupSession::from_export(&content).expect(
"We should be able to create an inbound group session from the room key export",
);
assert!(
session.shared_history,
"The shared history flag should be set as it was set in the exported room key"
);
content.shared_history = false;
let session = InboundGroupSession::from_export(&content).expect(
"We should be able to create an inbound group session from the room key export",
);
assert!(
!session.shared_history,
"The shared history flag should not be set as it was not set in the exported room key"
);
}
fn backed_up_room_key(stable: bool) -> serde_json::Value {
let shared_history =
if stable { "m.shared_history" } else { "org.matrix.msc3061.shared_history" };
json!({
"algorithm": "m.megolm.v1.aes-sha2",
"sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw",
"session_key": "AQAAAAAclzWVMeWBKH+B/WMowa3rb4ma3jEl6n5W4GCs9ue65CruzD3ihX+85pZ9hsV9Bf6fvhjp76WNRajoJYX0UIt7aosjmu0i+H+07hEQ0zqTKpVoSH0ykJ6stAMhdr6Q4uW5crBmdTTBIsqmoWsNJZKKoE2+ldYrZ1lrFeaJbjBIY/9ivle++74qQsT2dIKWPanKc9Q2Gl8LjESLtFBD9Fmt",
@@ -1314,9 +1386,34 @@ mod tests {
"ed25519": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA"
},
"forwarding_curve25519_key_chain": [],
"org.matrix.msc3061.shared_history": true
shared_history: true
})
}
});
#[async_test]
async fn test_shared_history_from_backed_up_room_key_stable() {
let content = backed_up_room_key(true);
let session_id = "/2K+V777vipCxPZ0gpY9qcpz1DYaXwuMRIu0UEP0Wa0";
let room_id = owned_room_id!("!room:id");
let room_key: BackedUpRoomKey = serde_json::from_value(content)
.expect("We should be able to deserialize the backed up room key");
let room_key =
ExportedRoomKey::from_backed_up_room_key(room_id, session_id.to_owned(), room_key);
let session = InboundGroupSession::from_export(&room_key).expect(
"We should be able to create an inbound group session from the room key export",
);
assert!(
session.shared_history,
"The shared history flag should be set as it was set in the backed up room key"
);
}
#[async_test]
async fn test_shared_history_from_backed_up_room_key_unstable() {
let content = backed_up_room_key(false);
let session_id = "/2K+V777vipCxPZ0gpY9qcpz1DYaXwuMRIu0UEP0Wa0";
let room_id = owned_room_id!("!room:id");
@@ -109,7 +109,7 @@ pub struct ExportedRoomKey {
/// defined in [MSC3061].
///
/// [MSC3061]: https://github.com/matrix-org/matrix-spec-proposals/pull/3061
#[serde(default, rename = "org.matrix.msc3061.shared_history")]
#[serde(default, rename = "m.shared_history", alias = "org.matrix.msc3061.shared_history")]
pub shared_history: bool,
}
@@ -196,7 +196,7 @@ pub struct BackedUpRoomKey {
/// defined in [MSC3061].
///
/// [MSC3061]: https://github.com/matrix-org/matrix-spec-proposals/pull/3061
#[serde(default, rename = "org.matrix.msc3061.shared_history")]
#[serde(default, rename = "m.shared_history", alias = "org.matrix.msc3061.shared_history")]
pub shared_history: bool,
}
+25 -15
View File
@@ -89,13 +89,15 @@ pub(crate) mod tests {
bob.generate_one_time_keys(1);
let one_time_key = *bob.one_time_keys().values().next().unwrap();
let sender_key = bob.identity_keys().curve25519;
let session = alice.create_outbound_session_helper(
SessionConfig::default(),
sender_key,
one_time_key,
false,
alice.device_keys(),
);
let session = alice
.create_outbound_session_helper(
SessionConfig::default(),
sender_key,
one_time_key,
false,
alice.device_keys(),
)
.unwrap();
(alice, session)
}
@@ -141,17 +143,25 @@ pub(crate) mod tests {
let one_time_key = *one_time_keys.values().next().unwrap();
let mut bob_session = bob.create_outbound_session_helper(
SessionConfig::default(),
alice_keys.curve25519,
one_time_key,
false,
bob.device_keys(),
);
#[cfg(not(feature = "experimental-algorithms"))]
let config = SessionConfig::version_1();
#[cfg(feature = "experimental-algorithms")]
let config = SessionConfig::version_2();
let mut bob_session = bob
.create_outbound_session_helper(
config,
alice_keys.curve25519,
one_time_key,
false,
bob.device_keys(),
)
.unwrap();
let plaintext = "Hello world";
let message = bob_session.encrypt_helper(plaintext).await;
let message = bob_session.encrypt_helper(plaintext).await.unwrap();
let prekey_message = match message {
OlmMessage::PreKey(m) => m,
+21 -11
View File
@@ -123,12 +123,14 @@ impl Session {
/// # Arguments
///
/// * `plaintext` - The plaintext that should be encrypted.
pub(crate) async fn encrypt_helper(&mut self, plaintext: &str) -> OlmMessage {
pub(crate) async fn encrypt_helper(&mut self, plaintext: &str) -> OlmResult<OlmMessage> {
let mut session = self.inner.lock().await;
let message = session.encrypt(plaintext);
let message = session.encrypt(plaintext)?;
self.last_use_time = SecondsSinceUnixEpoch::now();
debug!(?session, "Successfully encrypted an event");
message
Ok(message)
}
/// Encrypt the given event content as an m.room.encrypted event
@@ -206,7 +208,7 @@ impl Session {
serde_json::to_string(&content)?
};
let ciphertext = self.encrypt_helper(&plaintext).await;
let ciphertext = self.encrypt_helper(&plaintext).await?;
let content = self.build_encrypted_event(ciphertext, message_id).await?;
let content = Raw::new(&content)?;
@@ -364,17 +366,25 @@ mod tests {
Account::with_device_id(user_id!("@alice:localhost"), device_id!("ALICEDEVICE"));
let mut bob = Account::with_device_id(user_id!("@bob:localhost"), device_id!("BOBDEVICE"));
#[cfg(not(feature = "experimental-algorithms"))]
let config = SessionConfig::version_1();
#[cfg(feature = "experimental-algorithms")]
let config = SessionConfig::version_2();
// When Alice creates an Olm session with Bob
bob.generate_one_time_keys(1);
let one_time_key = *bob.one_time_keys().values().next().unwrap();
let sender_key = bob.identity_keys().curve25519;
let mut alice_session = alice.create_outbound_session_helper(
SessionConfig::default(),
sender_key,
one_time_key,
false,
alice.device_keys(),
);
let mut alice_session = alice
.create_outbound_session_helper(
config,
sender_key,
one_time_key,
false,
alice.device_keys(),
)
.unwrap();
let alice_device = DeviceData::from_account(&alice);
@@ -56,6 +56,10 @@ pub enum CryptoStoreError {
#[error(transparent)]
Pickle(#[from] vodozemac::PickleError),
/// Backing up a room key has failed.
#[error(transparent)]
Backup(#[from] vodozemac::pk_encryption::Error),
/// The received room key couldn't be converted into a valid Megolm session.
#[error(transparent)]
SessionCreation(#[from] SessionCreationError),
@@ -127,7 +127,7 @@ macro_rules! cryptostore_integration_tests {
one_time_key,
false,
alice.device_keys(),
);
).unwrap();
(alice, session)
}
@@ -28,7 +28,7 @@ expression: bundle
"withheld": [
{
"algorithm": "[algorithm]",
"code": "io.element.msc4268.history_not_shared",
"code": "m.history_not_shared",
"from_device": "BOB",
"reason": "The sender disabled sharing encrypted history.",
"room_id": "!room1:localhost",
@@ -83,7 +83,7 @@ impl DecryptedForwardedRoomKeyEvent {
/// `m.olm.v1.curve25519-aes-sha2` algorithm
pub type DecryptedSecretSendEvent = DecryptedOlmV1Event<SecretSendContent>;
/// An `io.element.msc4268.room_key_bundle` to-device event which has
/// An `m.room_key_bundle` to-device event which has
/// been decrypted using using the `m.olm.v1.curve25519-aes-sha2` algorithm
pub type DecryptedRoomKeyBundleEvent = DecryptedOlmV1Event<RoomKeyBundleContent>;
@@ -104,7 +104,7 @@ pub enum AnyDecryptedOlmEvent {
SecretSend(DecryptedSecretSendEvent),
/// The `m.dummy` decrypted to-device event.
Dummy(DecryptedDummyEvent),
/// The `io.element.msc4268.room_key_bundle` decrypted to-device event.
/// The `m.room_key_bundle` decrypted to-device event.
RoomKeyBundle(DecryptedRoomKeyBundleEvent),
/// The `io.element.msc4385.secret.push` decrypted to-device event.
#[cfg(feature = "experimental-push-secrets")]
@@ -352,7 +352,7 @@ impl<'de> Deserialize<'de> for AnyDecryptedOlmEvent {
}
SecretSendContent::EVENT_TYPE => AnyDecryptedOlmEvent::SecretSend(from_str(json)?),
DummyEventContent::EVENT_TYPE => AnyDecryptedOlmEvent::Dummy(from_str(json)?),
RoomKeyBundleContent::EVENT_TYPE => {
RoomKeyBundleContent::EVENT_TYPE | RoomKeyBundleContent::UNSTABLE_EVENT_TYPE => {
AnyDecryptedOlmEvent::RoomKeyBundle(from_str(json)?)
}
#[cfg(feature = "experimental-push-secrets")]
@@ -368,7 +368,11 @@ mod tests {
use assert_matches::assert_matches;
use insta::{assert_json_snapshot, with_settings};
use ruma::{KeyId, device_id, owned_user_id};
use ruma::{
KeyId, OwnedMxcUri, device_id,
events::room::{EncryptedFile, V2EncryptedFileInfo},
owned_room_id, owned_user_id, user_id,
};
use serde_json::{Value, json};
use similar_asserts::assert_eq;
use vodozemac::{Curve25519PublicKey, Ed25519PublicKey, Ed25519Signature};
@@ -376,7 +380,10 @@ mod tests {
use super::AnyDecryptedOlmEvent;
use crate::types::{
DeviceKey, DeviceKeys, EventEncryptionAlgorithm, Signatures,
events::olm_v1::DecryptedRoomKeyEvent,
events::{
olm_v1::{DecryptedRoomKeyBundleEvent, DecryptedRoomKeyEvent},
room_key_bundle::RoomKeyBundleContent,
},
};
const ED25519_KEY: &str = "aOfOnlaeMb5GW1TxkZ8pXnblkGMgAvps+lAukrdYaZk";
@@ -472,6 +479,47 @@ mod tests {
})
}
fn room_key_bundle_event(stable: bool) -> Value {
json!({
"sender": "@u:s.co",
"recipient": "@x:s.co",
"keys": {
"ed25519": "ee3Ek+J2LkkPmjGPGLhMxiKnhiX//xcqaVL4RP6EypE"
},
"recipient_keys": {
"ed25519": "ee3Ek+J2LkkPmjGPGLhMxiKnhiX//xcqaVL4RP6EypE"
},
"content": {
"room_id": "!r:s.co",
"file": {
"url": "test",
"v": "v2",
"key": {
"kty": "oct",
"key_ops": [
"decrypt",
"encrypt"
],
"alg": "A256CTR",
"k": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"ext": true
},
"iv": "AAAAAAAAAAAAAAAAAAAAAA",
"hashes": {}
}
},
"type": if stable { "m.room_key_bundle" } else { "io.element.msc4268.room_key_bundle" }
})
}
fn room_key_bundle_event_stable() -> Value {
room_key_bundle_event(true)
}
fn room_key_bundle_event_unstable() -> Value {
room_key_bundle_event(false)
}
/// Return the JSON for creating sender device keys, and the matching
/// `DeviceKeys` object that should be created when the JSON is
/// deserialized.
@@ -582,6 +630,12 @@ mod tests {
// `m.dummy`
dummy_event => Dummy,
// `m.room_key_bundle`
room_key_bundle_event_stable => RoomKeyBundle,
// `m.io.element.msc4268.room_key_bundle`
room_key_bundle_event_unstable => RoomKeyBundle,
);
Ok(())
@@ -630,6 +684,29 @@ mod tests {
});
}
#[test]
fn serialize_room_key_bundle() {
let room_id = owned_room_id!("!r:s.co");
let sender = user_id!("@u:s.co");
let recipient = user_id!("@x:s.co");
let key =
Ed25519PublicKey::from_base64("ee3Ek+J2LkkPmjGPGLhMxiKnhiX//xcqaVL4RP6EypE").unwrap();
let content = RoomKeyBundleContent {
room_id,
file: EncryptedFile::new(
OwnedMxcUri::from("test"),
V2EncryptedFileInfo::encode([0; 32], [0; 16]).into(),
Default::default(),
),
};
let event = DecryptedRoomKeyBundleEvent::new(sender, recipient, key, None, content);
assert_eq!(serde_json::to_value(&event).unwrap(), room_key_bundle_event_stable());
}
#[test]
fn test_serialization_cycle() {
let event_json = json!({
@@ -644,7 +721,7 @@ mod tests {
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!Cuyf34gef24t:localhost",
"org.matrix.msc3061.shared_history": true,
"m.shared_history": true,
"session_id": "ZFD6+OmV7fVCsJ7Gap8UnORH8EnmiAkes8FAvQuCw/I",
"session_key": "AgAAAADNp1EbxXYOGmJtyX4AkD1bvJvAUyPkbIaKxtnGKjv\
SQ3E/4mnuqdM4vsmNzpO1EeWzz1rDkUpYhYE9kP7sJhgLXi\
@@ -124,7 +124,7 @@ pub struct MegolmV1AesSha2Content {
/// [MSC3061].
///
/// [MSC3061]: https://github.com/matrix-org/matrix-spec-proposals/pull/3061
#[serde(default, rename = "org.matrix.msc3061.shared_history")]
#[serde(default, rename = "m.shared_history", alias = "org.matrix.msc3061.shared_history")]
pub shared_history: bool,
/// Any other, custom and non-specced fields of the content.
#[serde(flatten)]
@@ -229,14 +229,22 @@ pub(super) mod tests {
use super::RoomKeyEvent;
use crate::types::events::room_key::RoomKeyContent;
pub fn json() -> Value {
pub fn json_stable() -> Value {
json(true)
}
pub fn json_unstable() -> Value {
json(false)
}
pub fn json(stable: bool) -> Value {
json!({
"sender": "@alice:example.org",
"content": {
"m.custom": "something custom",
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!Cuyf34gef24t:localhost",
"org.matrix.msc3061.shared_history": false,
if stable { "m.shared_history" } else { "org.matrix.msc3061.shared_history" }: false,
"session_id": "ZFD6+OmV7fVCsJ7Gap8UnORH8EnmiAkes8FAvQuCw/I",
"session_key": "AgAAAADNp1EbxXYOGmJtyX4AkD1bvJvAUyPkbIaKxtnGKjv\
SQ3E/4mnuqdM4vsmNzpO1EeWzz1rDkUpYhYE9kP7sJhgLXi\
@@ -252,8 +260,8 @@ pub(super) mod tests {
}
#[test]
fn deserialization() -> Result<(), serde_json::Error> {
let json = json();
fn deserialization_stable() -> Result<(), serde_json::Error> {
let json = json_stable();
let event: RoomKeyEvent = serde_json::from_value(json.clone())?;
assert_matches!(event.content, RoomKeyContent::MegolmV1AesSha2(_));
@@ -262,4 +270,14 @@ pub(super) mod tests {
Ok(())
}
#[test]
fn deserialization_unstable() -> Result<(), serde_json::Error> {
let json = json_unstable();
let event: RoomKeyEvent = serde_json::from_value(json)?;
assert_matches!(event.content, RoomKeyContent::MegolmV1AesSha2(_));
Ok(())
}
}
@@ -12,8 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//! Types for `io.element.msc4268.room_key_bundle` to-device events, per
//! [MSC4268].
//! Types for `m.room_key_bundle` to-device events, per [MSC4268].
//!
//! [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
@@ -22,7 +21,7 @@ use serde::{Deserialize, Serialize};
use super::EventType;
/// The `io.element.msc4268.room_key_bundle` event content. See [MSC4268].
/// The `m.room_key_bundle` event content. See [MSC4268].
///
/// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -34,6 +33,11 @@ pub struct RoomKeyBundleContent {
pub file: EncryptedFile,
}
impl EventType for RoomKeyBundleContent {
const EVENT_TYPE: &'static str = "io.element.msc4268.room_key_bundle";
impl RoomKeyBundleContent {
/// The unstable event type for MSC4268 m.room_key_bundle
pub const UNSTABLE_EVENT_TYPE: &'static str = "io.element.msc4268.room_key_bundle";
}
impl EventType for RoomKeyBundleContent {
const EVENT_TYPE: &'static str = "m.room_key_bundle";
}
@@ -13,7 +13,7 @@ expression: event
},
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"org.matrix.msc3061.shared_history": false,
"m.shared_history": false,
"room_id": "!Cuyf34gef24t:localhost",
"session_id": "ZFD6+OmV7fVCsJ7Gap8UnORH8EnmiAkes8FAvQuCw/I",
"session_key": "AgAAAADNp1EbxXYOGmJtyX4AkD1bvJvAUyPkbIaKxtnGKjvSQ3E/4mnuqdM4vsmNzpO1EeWzz1rDkUpYhYE9kP7sJhgLXijVv80fMPHfGc49hPdu8A+xnwD4SQiYdFmSWJOIqsxeo/fiHtino//CDQENtcKuEt0I9s0+Kk4YSH310Szse2RQ+vjple31QrCexmqfFJzkR/BJ5ogJHrPBQL0LgsPyglIbMTLg7qygIaYU5Fe2QdKMH7nTZPNIRHh1RaMfHVETAUJBax88EWZBoifk80gdHUwHSgMk77vCc2a5KHKLDA"
@@ -30,7 +30,7 @@ expression: event
},
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"org.matrix.msc3061.shared_history": false,
"m.shared_history": false,
"room_id": "!Cuyf34gef24t:localhost",
"session_id": "ZFD6+OmV7fVCsJ7Gap8UnORH8EnmiAkes8FAvQuCw/I",
"session_key": "AgAAAADNp1EbxXYOGmJtyX4AkD1bvJvAUyPkbIaKxtnGKjvSQ3E/4mnuqdM4vsmNzpO1EeWzz1rDkUpYhYE9kP7sJhgLXijVv80fMPHfGc49hPdu8A+xnwD4SQiYdFmSWJOIqsxeo/fiHtino//CDQENtcKuEt0I9s0+Kk4YSH310Szse2RQ+vjple31QrCexmqfFJzkR/BJ5ogJHrPBQL0LgsPyglIbMTLg7qygIaYU5Fe2QdKMH7nTZPNIRHh1RaMfHVETAUJBax88EWZBoifk80gdHUwHSgMk77vCc2a5KHKLDA"
@@ -520,7 +520,7 @@ mod tests {
assert_serialization_roundtrip!(
// `m.room_key
crate::types::events::room_key::tests::json => RoomKey,
crate::types::events::room_key::tests::json_stable => RoomKey,
// `m.forwarded_room_key`
forwarded_room_key_event => ForwardedRoomKey,
@@ -570,7 +570,7 @@ mod tests {
assert_serialization_roundtrip!(
// `m.room_key
crate::types::events::room_key::tests::json => RoomKey,
crate::types::events::room_key::tests::json_stable => RoomKey,
// `m.forwarded_room_key`
forwarded_room_key_event => ForwardedRoomKey,
@@ -662,7 +662,7 @@ mod tests {
}
#[cfg(not(target_os = "macos"))]
#[allow(unknown_lints, clippy::unchecked_time_subtraction)]
#[expect(clippy::unchecked_time_subtraction)]
#[async_test]
async fn test_timing_out() {
use std::time::Duration;
@@ -674,7 +674,7 @@ impl_state_store!({
.transpose()?
.map(StateStoreDataValue::ThreadSubscriptionsCatchupTokens),
StateStoreDataKey::HomeserverCapabilities => value
.map(|f| self.deserialize_value::<Capabilities>(&f))
.map(|f| self.deserialize_value::<TtlValue<Capabilities>>(&f))
.transpose()?
.map(StateStoreDataValue::HomeserverCapabilities),
};
@@ -20,6 +20,7 @@ use std::{
};
use futures_util::{StreamExt as _, pin_mut};
use itertools::Itertools;
use matrix_sdk::{
Client, ClientBuildError, SlidingSyncList, SlidingSyncMode, room::Room, sleep::sleep,
};
@@ -938,6 +939,8 @@ pub struct NotificationItem {
pub is_direct_message_room: bool,
/// Numbers of members who joined the room.
pub joined_members_count: u64,
/// Number of service members in the room.
pub service_members: Vec<String>,
/// Is the room a space?
pub is_space: bool,
@@ -1022,6 +1025,12 @@ impl NotificationItem {
let is_noisy = push_actions.map(|actions| actions.iter().any(|a| a.sound().is_some()));
let has_mention = push_actions.map(|actions| actions.iter().any(|a| a.is_highlight()));
let thread_id = event.thread_id().clone();
let service_members = room
.service_members()
.unwrap_or_default()
.iter()
.map(ToString::to_string)
.collect_vec();
let item = NotificationItem {
event,
@@ -1041,6 +1050,7 @@ impl NotificationItem {
.map(|state| state.is_encrypted())
.ok(),
joined_members_count: room.joined_members_count(),
service_members,
is_space: room.is_space(),
is_noisy,
has_mention,
@@ -51,6 +51,16 @@
//! }
//! ```
#[cfg(test)]
use matrix_sdk::{Client, test_utils::mocks::MatrixMockServer};
#[cfg(test)]
use matrix_sdk_test::JoinedRoomBuilder;
#[cfg(test)]
use ruma::RoomId;
use unicode_normalization::{UnicodeNormalization, char::is_combining_mark};
use super::RoomListItem;
mod all;
mod any;
mod category;
@@ -68,31 +78,24 @@ mod not;
mod space;
mod unread;
pub use all::new_filter as new_filter_all;
pub use any::new_filter as new_filter_any;
pub use category::{RoomCategory, new_filter as new_filter_category};
pub use deduplicate_versions::new_filter as new_filter_deduplicate_versions;
pub use favourite::new_filter as new_filter_favourite;
pub use fuzzy_match_room_name::new_filter as new_filter_fuzzy_match_room_name;
pub use identifiers::new_filter as new_filter_identifiers;
pub use invite::new_filter as new_filter_invite;
pub use joined::new_filter as new_filter_joined;
pub use low_priority::new_filter as new_filter_low_priority;
#[cfg(test)]
use matrix_sdk::{Client, test_utils::mocks::MatrixMockServer};
#[cfg(test)]
use matrix_sdk_test::JoinedRoomBuilder;
pub use non_left::new_filter as new_filter_non_left;
pub use none::new_filter as new_filter_none;
pub use normalized_match_room_name::new_filter as new_filter_normalized_match_room_name;
pub use not::new_filter as new_filter_not;
#[cfg(test)]
use ruma::RoomId;
pub use space::new_filter as new_filter_space;
use unicode_normalization::{UnicodeNormalization, char::is_combining_mark};
pub use unread::new_filter as new_filter_unread;
use super::RoomListItem;
pub use self::{
all::new_filter as new_filter_all,
any::new_filter as new_filter_any,
category::{RoomCategory, new_filter as new_filter_category},
deduplicate_versions::new_filter as new_filter_deduplicate_versions,
favourite::new_filter as new_filter_favourite,
fuzzy_match_room_name::new_filter as new_filter_fuzzy_match_room_name,
identifiers::new_filter as new_filter_identifiers,
invite::new_filter as new_filter_invite,
joined::new_filter as new_filter_joined,
low_priority::new_filter as new_filter_low_priority,
non_left::new_filter as new_filter_non_left,
none::new_filter as new_filter_none,
normalized_match_room_name::new_filter as new_filter_normalized_match_room_name,
not::new_filter as new_filter_not,
space::new_filter as new_filter_space,
unread::new_filter as new_filter_unread,
};
/// A trait “alias” that represents a _filter_.
///
@@ -23,8 +23,6 @@ use eyeball_im::{VectorDiff, VectorSubscriberStream};
use eyeball_im_util::vector::{FilterMap, VectorObserverExt};
use futures_core::Stream;
use imbl::Vector;
#[cfg(test)]
use matrix_sdk::Result;
use matrix_sdk::{
deserialized_responses::TimelineEvent,
event_cache::{
+5
View File
@@ -8,6 +8,11 @@ All notable changes to this project will be documented in this file.
### Features
- Add `Client::get_dm_rooms` function to get an iterator with the DMs for the provided user id.
([#6487](https://github.com/matrix-org/matrix-rust-sdk/pull/6487))
- Support the stable `m.key_backup` prefix for MSC4287: Sharing key backup
preference between clients.
([#6410](https://github.com/matrix-org/matrix-rust-sdk/pull/6410))
- Add new high-level search helpers in `matrix_sdk_ui::search` to perform searches for messages in
a room or across all rooms.
([#6394](https://github.com/matrix-org/matrix-rust-sdk/pull/6394))
+1 -1
View File
@@ -613,7 +613,7 @@ impl ClientBuilder {
None => NotSet,
};
let well_known = match well_known {
Some(well_known) => Cached(Some(well_known.into())),
Some(well_known) => Cached(TtlValue::new(Some(well_known.into()))),
None => NotSet,
};
+13 -3
View File
@@ -18,9 +18,12 @@ use matrix_sdk_base::{store::WellKnownResponse, ttl_cache::TtlCache};
use matrix_sdk_common::{locks::Mutex, ttl_cache::TtlValue};
use ruma::api::{
SupportedVersions,
client::discovery::get_authorization_server_metadata::v1::AuthorizationServerMetadata,
client::discovery::{
get_authorization_server_metadata::v1::AuthorizationServerMetadata,
get_capabilities::v3::Capabilities,
},
};
use tokio::sync::{Mutex as AsyncMutex, RwLock};
use tokio::sync::Mutex as AsyncMutex;
use crate::HttpError;
@@ -35,8 +38,10 @@ pub(crate) struct ClientCaches {
/// - The versions fetched from an *authenticated* request to the server.
pub(crate) supported_versions: Cache<SupportedVersions>,
/// Well-known information.
pub(super) well_known: RwLock<CachedValue<Option<WellKnownResponse>>>,
pub(super) well_known: Cache<Option<WellKnownResponse>>,
pub(crate) server_metadata: AsyncMutex<TtlCache<String, AuthorizationServerMetadata>>,
/// Homeserver capabilities.
pub(crate) homeserver_capabilities: Cache<Capabilities>,
}
/// A cached value that can either be set or not set, used to avoid confusion
@@ -74,6 +79,11 @@ pub(crate) struct Cache<Value> {
}
impl<Value> Cache<Value> {
/// Construct a new empty `Cache`.
pub(crate) fn new() -> Self {
Self::with_value(CachedValue::NotSet)
}
/// Construct a new `Cache` with the given value.
pub(crate) fn with_value(value: CachedValue<TtlValue<Value>>) -> Self {
Self { value: Mutex::new(value), refresh_lock: AsyncMutex::new(Ok(())) }
@@ -1,4 +1,6 @@
use matrix_sdk_base::{StateStoreDataKey, StateStoreDataValue};
use std::sync::Arc;
use matrix_sdk_base::{StateStoreDataKey, StateStoreDataValue, StoreError, ttl_cache::TtlValue};
use ruma::{
api::{
Metadata,
@@ -15,9 +17,9 @@ use ruma::{
},
profile::ProfileFieldName,
};
use tracing::warn;
use tracing::{debug, warn};
use crate::{Client, HttpResult};
use crate::{Client, HttpError, HttpResult, client::caches::CachedValue};
/// Helper to check what [`Capabilities`] are supported by the homeserver.
///
@@ -135,6 +137,47 @@ impl HomeserverCapabilities {
Ok(capabilities.forget_forced_upon_leave.enabled)
}
/// Gets the supported [`Capabilities`] from the local cache.
async fn homeserver_capabilities_cached(&self) -> Result<Option<Capabilities>, StoreError> {
let capabilities_cache = &self.client.inner.caches.homeserver_capabilities;
let value = if let CachedValue::Cached(cached) = capabilities_cache.value() {
cached
} else if let Some(stored) = self
.client
.state_store()
.get_kv_data(StateStoreDataKey::HomeserverCapabilities)
.await?
.and_then(|value| value.into_homeserver_capabilities())
{
// Copy the data from the store in the in-memory cache.
capabilities_cache.set_value(stored.clone());
stored
} else {
return Ok(None);
};
// Spawn a task to refresh the cache if it has expired.
if value.has_expired() {
debug!("spawning task to refresh homeserver capabilities cache");
let homeserver_capabilities = self.clone();
self.client.task_monitor().spawn_finite_task(
"refresh homeserver capabilities cache",
async move {
if let Err(error) =
homeserver_capabilities.get_and_cache_remote_capabilities().await
{
warn!("failed to refresh homeserver capabilities cache: {error}");
}
},
);
}
Ok(Some(value.into_data()))
}
/// Gets the supported [`Capabilities`] either from the local cache or from
/// the homeserver using the `/capabilities` endpoint if the data is not
/// cached.
@@ -142,12 +185,9 @@ impl HomeserverCapabilities {
/// To ensure you get updated values, you should call [`Self::refresh`]
/// instead.
async fn load_or_fetch_homeserver_capabilities(&self) -> crate::Result<Capabilities> {
match self.client.state_store().get_kv_data(StateStoreDataKey::HomeserverCapabilities).await
{
Ok(Some(stored)) => {
if let Some(capabilities) = stored.into_homeserver_capabilities() {
return Ok(capabilities);
}
match self.homeserver_capabilities_cached().await {
Ok(Some(capabilities)) => {
return Ok(capabilities);
}
Ok(None) => {
// fallthrough: cache is empty
@@ -163,21 +203,58 @@ impl HomeserverCapabilities {
/// Gets and caches the capabilities of the homeserver.
async fn get_and_cache_remote_capabilities(&self) -> HttpResult<Capabilities> {
let res = self.client.send(get_capabilities::v3::Request::new()).await?;
let capabilities_cache = &self.client.inner.caches.homeserver_capabilities;
let mut capabilities_guard = match capabilities_cache.refresh_lock.try_lock() {
Ok(guard) => guard,
Err(_) => {
// There is already a refresh in progress, wait for it to finish.
let guard = capabilities_cache.refresh_lock.lock().await;
if let Err(error) = guard.as_ref() {
// There was an error in the previous refresh, return it.
return Err(HttpError::Cached(error.clone()));
}
// Reuse the data if it was cached and it hasn't expired.
if let CachedValue::Cached(value) = capabilities_cache.value()
&& !value.has_expired()
{
return Ok(value.into_data());
}
// The data wasn't cached or has expired, we need to make another request.
guard
}
};
let capabilities = match self.client.send(get_capabilities::v3::Request::new()).await {
Ok(response) => {
*capabilities_guard = Ok(());
TtlValue::new(response.capabilities)
}
Err(error) => {
let error = Arc::new(error);
*capabilities_guard = Err(error.clone());
return Err(HttpError::Cached(error));
}
};
if let Err(err) = self
.client
.state_store()
.set_kv_data(
StateStoreDataKey::HomeserverCapabilities,
StateStoreDataValue::HomeserverCapabilities(res.capabilities.clone()),
StateStoreDataValue::HomeserverCapabilities(capabilities.clone()),
)
.await
{
warn!("error when caching homeserver capabilities: {err}");
}
Ok(res.capabilities)
capabilities_cache.set_value(capabilities.clone());
Ok(capabilities.into_data())
}
/// Gets or computes the supported [`ProfileCapabilities`].
@@ -234,6 +311,10 @@ struct ProfileCapabilities {
#[cfg(all(not(target_family = "wasm"), test))]
mod tests {
use std::time::Duration;
use assert_matches::assert_matches;
use matrix_sdk_base::sleep::sleep;
use matrix_sdk_test::async_test;
#[allow(deprecated)]
use ruma::api::{
@@ -321,14 +402,27 @@ mod tests {
server
.mock_get_homeserver_capabilities()
.ok_with_capabilities(expected_capabilities)
// Ensure it's not called, since we'd be using the cached values instead
.never()
.expect(1)
.mount()
.await;
// Check the values we get are not updated without a refresh, they're loaded
// from the cache
assert!(capabilities.can_change_displayname().await.expect("checking capabilities failed"));
// Force an expiry of the data.
let capabilities_data =
capabilities.homeserver_capabilities_cached().await.unwrap().unwrap();
let mut ttl_value = TtlValue::new(capabilities_data);
ttl_value.expire();
client.inner.caches.homeserver_capabilities.set_value(ttl_value);
// Call a method to trigger a cache refresh background task.
capabilities.homeserver_capabilities_cached().await.unwrap().unwrap();
// We wait for the task to finish, the endpoint should have been called again.
sleep(Duration::from_secs(1)).await;
assert_matches!(client.inner.caches.homeserver_capabilities.value(), CachedValue::Cached(value) if !value.has_expired());
}
#[async_test]
+118 -51
View File
@@ -391,7 +391,7 @@ impl ClientInner {
http_client: HttpClient,
base_client: BaseClient,
supported_versions: CachedValue<TtlValue<SupportedVersions>>,
well_known: CachedValue<Option<WellKnownResponse>>,
well_known: CachedValue<TtlValue<Option<WellKnownResponse>>>,
respect_login_well_known: bool,
event_cache: OnceCell<EventCache>,
send_queue: Arc<SendQueueData>,
@@ -404,8 +404,9 @@ impl ClientInner {
) -> Arc<Self> {
let caches = ClientCaches {
supported_versions: Cache::with_value(supported_versions),
well_known: well_known.into(),
well_known: Cache::with_value(well_known),
server_metadata: Mutex::new(TtlCache::new()),
homeserver_capabilities: Cache::new(),
};
let client = Self {
@@ -542,7 +543,7 @@ impl Client {
) -> Result<()> {
self.set_homeserver(homeserver_url);
self.reset_well_known().await?;
if let Some(well_known) = self.load_or_fetch_well_known().await? {
if let Some(well_known) = self.well_known().await {
self.set_homeserver(Url::parse(&well_known.homeserver.base_url)?);
}
Ok(())
@@ -1790,18 +1791,23 @@ impl Client {
self.create_room(request).await
}
/// Get the existing DM room with the given user, if any.
/// Get the first existing DM room with the given user, if any.
pub fn get_dm_room(&self, user_id: &UserId) -> Option<Room> {
self.get_dm_rooms(user_id).next()
}
/// Get an iterator with the existing DM rooms for the given user.
pub fn get_dm_rooms(&self, user_id: &UserId) -> impl Iterator<Item = Room> {
let rooms = self.joined_rooms();
// Find the room we share with the `user_id` and only with `user_id`
let room = rooms.into_iter().find(|r| {
let rooms = rooms.into_iter().filter(move |r| {
let targets = r.direct_targets();
targets.len() == 1 && targets.contains(<&DirectUserIdentifier>::from(user_id))
});
trace!(?user_id, ?room, "Found DM room with user");
room
trace!(?user_id, ?rooms, "Found DM rooms with user");
rooms
}
/// Search the homeserver's directory for public rooms with a filter.
@@ -2188,14 +2194,14 @@ impl Client {
if let Err(error) = guard.as_ref() {
// There was an error in the previous refresh, return it.
return Err(HttpError::SupportedVersions(error.clone()));
return Err(HttpError::Cached(error.clone()));
}
// Reuse the data if it was cached and it hasn't expired.
if let CachedValue::Cached(value) = cached_supported_versions.value()
&& !value.has_expired()
{
return Ok(value.into_data_unchecked());
return Ok(value.into_data());
}
// The data wasn't cached or has expired, we need to make another request.
@@ -2211,7 +2217,7 @@ impl Client {
Err(error) => {
let error = Arc::new(error);
*supported_versions_guard = Err(error.clone());
return Err(HttpError::SupportedVersions(error));
return Err(HttpError::Cached(error));
}
};
@@ -2233,7 +2239,7 @@ impl Client {
cached_supported_versions.set_value(supported_versions.clone());
}
Ok(supported_versions.into_data_unchecked())
Ok(supported_versions.into_data())
}
/// Get the Matrix versions and features supported by the homeserver by
@@ -2313,7 +2319,7 @@ impl Client {
});
}
Ok(Some(value.into_data_unchecked()))
Ok(Some(value.into_data()))
}
/// Get the Matrix versions supported by the homeserver by fetching them
@@ -2374,58 +2380,104 @@ impl Client {
Ok(self.state_store().remove_kv_data(StateStoreDataKey::SupportedVersions).await?)
}
/// Load well-known from storage, or fetch it from network and cache it.
async fn load_or_fetch_well_known(&self) -> HttpResult<Option<WellKnownResponse>> {
match self.state_store().get_kv_data(StateStoreDataKey::WellKnown).await {
Ok(Some(stored)) => {
if let Some(well_known) =
stored.into_well_known().and_then(|value| value.into_data())
{
return Ok(well_known);
}
}
Ok(None) => {
// fallthrough: cache is empty
}
Err(err) => {
warn!("error when loading cached well-known: {err}");
// fallthrough to network.
}
/// Get the well-known file of the homeserver from the cache.
///
/// If the data in the cache has expired, this will trigger a background
/// task to refresh it.
async fn well_known_cached(
&self,
) -> Result<CachedValue<Option<WellKnownResponse>>, StoreError> {
let well_known_cache = &self.inner.caches.well_known;
let value = if let CachedValue::Cached(cached) = well_known_cache.value() {
cached
} else if let Some(stored) = self
.state_store()
.get_kv_data(StateStoreDataKey::WellKnown)
.await?
.and_then(|value| value.into_well_known())
{
// Copy the data from the store into the in-memory cache.
well_known_cache.set_value(stored.clone());
stored
} else {
return Ok(CachedValue::NotSet);
};
// Spawn a task to refresh the cache if it has expired.
if value.has_expired() {
debug!("spawning task to refresh well-known cache");
let client = self.clone();
self.task_monitor().spawn_finite_task("refresh well-known cache", async move {
client.refresh_well_known_cache().await;
});
}
let well_known = self.fetch_client_well_known().await.map(Into::into);
Ok(CachedValue::Cached(value.into_data()))
}
/// Refresh the well-known file of the homeserver in the cache.
async fn refresh_well_known_cache(&self) -> Option<WellKnownResponse> {
let well_known_cache = &self.inner.caches.well_known;
let _well_known_guard = match well_known_cache.refresh_lock.try_lock() {
Ok(guard) => guard,
Err(_) => {
// There is already a refresh in progress, wait for it to finish.
let guard = well_known_cache.refresh_lock.lock().await;
// A refresh can't fail because we ignore failures, so there shouldn't be an
// error in the refresh lock.
// Reuse the data if it was cached and it hasn't expired.
if let CachedValue::Cached(value) = well_known_cache.value()
&& !value.has_expired()
{
return value.into_data();
}
// The data wasn't cached or has expired, we need to make another request.
guard
}
};
let well_known = TtlValue::new(self.fetch_client_well_known().await.map(Into::into));
// Attempt to cache the result in storage.
if let Err(err) = self
.state_store()
.set_kv_data(
StateStoreDataKey::WellKnown,
StateStoreDataValue::WellKnown(TtlValue::new(well_known.clone())),
StateStoreDataValue::WellKnown(well_known.clone()),
)
.await
{
warn!("error when caching well-known: {err}");
}
Ok(well_known)
well_known_cache.set_value(well_known.clone());
well_known.into_data()
}
async fn get_or_load_and_cache_well_known(&self) -> HttpResult<Option<WellKnownResponse>> {
let well_known = &self.inner.caches.well_known;
if let CachedValue::Cached(val) = &*well_known.read().await {
return Ok(val.clone());
/// Get the well-known file of the homeserver by fetching it from the server
/// or the cache.
async fn well_known(&self) -> Option<WellKnownResponse> {
match self.well_known_cached().await {
Ok(CachedValue::Cached(value)) => {
return value;
}
Ok(CachedValue::NotSet) => {
// The cache is empty, make a request.
}
Err(error) => {
warn!("error when loading cached well-known: {error}");
// Fallthrough to make a request.
}
}
let mut guarded_well_known = well_known.write().await;
if let CachedValue::Cached(val) = &*guarded_well_known {
return Ok(val.clone());
}
let well_known = self.load_or_fetch_well_known().await?;
*guarded_well_known = CachedValue::Cached(well_known.clone());
Ok(well_known)
self.refresh_well_known_cache().await
}
/// Get information about the homeserver's advertised RTC foci by fetching
@@ -2449,7 +2501,7 @@ impl Client {
/// # anyhow::Ok(()) };
/// ```
pub async fn rtc_foci(&self) -> HttpResult<Vec<RtcFocusInfo>> {
let well_known = self.get_or_load_and_cache_well_known().await?;
let well_known = self.well_known().await;
Ok(well_known.map(|well_known| well_known.rtc_foci).unwrap_or_default())
}
@@ -2460,7 +2512,7 @@ impl Client {
/// in the cache. This functions makes it possible to force reset it.
pub async fn reset_well_known(&self) -> Result<()> {
// Empty the in-memory caches.
self.inner.caches.well_known.write().await.take();
self.inner.caches.well_known.reset();
// Empty the store cache.
Ok(self.state_store().remove_kv_data(StateStoreDataKey::WellKnown).await?)
@@ -3117,7 +3169,7 @@ impl Client {
.clone_with_in_memory_state_store(cross_process_lock_config.clone(), false)
.await?,
self.inner.caches.supported_versions.value(),
self.inner.caches.well_known.read().await.clone(),
self.inner.caches.well_known.value(),
self.inner.respect_login_well_known,
self.inner.event_cache.clone(),
self.inner.send_queue_data.clone(),
@@ -3976,12 +4028,27 @@ pub(crate) mod tests {
// Now, reset the cache, and observe the endpoints being called again once.
client.reset_well_known().await.unwrap();
server.mock_well_known().ok().named("second well known mock").expect(1).mount().await;
server.mock_well_known().ok().named("second well known mock").expect(2).mount().await;
// Hits network again.
assert_eq!(client.rtc_foci().await.unwrap(), rtc_foci);
// Hits in-memory cache again.
assert_eq!(client.rtc_foci().await.unwrap(), rtc_foci);
// Force an expiry of the data.
let well_known = client.well_known().await;
let mut ttl_value = TtlValue::new(well_known);
ttl_value.expire();
client.inner.caches.well_known.set_value(ttl_value);
// Call the method again to trigger a cache refresh background task.
client.well_known().await;
// We wait for the task to finish, the endpoint should have been called again.
// We need to wait a bit because the first requests using the server name of the
// user will fail, only the requests using the homeserver URL will succeed.
sleep(Duration::from_secs(5)).await;
assert_matches!(client.inner.caches.well_known.value(), CachedValue::Cached(value) if !value.has_expired());
}
#[async_test]
@@ -120,7 +120,7 @@ mod types;
pub use self::types::{EnableProgress, RecoveryError, RecoveryState, Result};
use self::{
futures::{Enable, RecoverAndReset, Reset},
types::{BackupDisabledContent, SecretStorageDisabledContent},
types::{BackupDisabledContent, KeyBackupContent, SecretStorageDisabledContent},
};
use crate::encryption::{AuthData, CrossSigningResetAuthType, CrossSigningResetHandle};
@@ -325,6 +325,8 @@ impl Recovery {
// Now let's "delete" the actual `m.secret.storage.default_key` event.
self.client.account().set_account_data(SecretStorageDisabledContent {}).await?;
// Make sure that we don't re-enable backups automatically.
self.client.account().set_account_data(KeyBackupContent { enabled: false }).await?;
// (Unstable prefix version of KeyBackupContent)
self.client.account().set_account_data(BackupDisabledContent { disabled: true }).await?;
// Finally, "delete" all the known secrets we have in the account data.
self.delete_all_known_secrets().await?;
@@ -651,16 +653,26 @@ impl Recovery {
/// Run a network request to figure whether backups have been disabled at
/// the account level.
async fn are_backups_marked_as_disabled(&self) -> Result<bool> {
Ok(self
.client
.account()
.fetch_account_data_static::<BackupDisabledContent>()
.await?
.map(|event| event.deserialize().map(|event| event.disabled).unwrap_or(false))
.unwrap_or(false))
if let Some(key_backup_content) =
self.client.account().fetch_account_data_static::<KeyBackupContent>().await?
{
Ok(key_backup_content.deserialize().map(|event| !event.enabled).unwrap_or(false))
} else {
Ok(self
.client
.account()
.fetch_account_data_static::<BackupDisabledContent>()
.await?
.map(|event| event.deserialize().map(|event| event.disabled).unwrap_or(false))
.unwrap_or(false))
}
}
async fn mark_backup_as_enabled(&self) -> Result<()> {
self.client.account().set_account_data(KeyBackupContent { enabled: true }).await?;
// Unstable prefix: will be removed when sufficient time has passed for clients
// to use the stable prefix.
self.client.account().set_account_data(BackupDisabledContent { disabled: false }).await?;
Ok(())
@@ -105,13 +105,22 @@ pub enum RecoveryState {
#[ruma_event(type = "m.secret_storage.default_key", kind = GlobalAccountData)]
pub(super) struct SecretStorageDisabledContent {}
/// A custom global account data event which tells us that a new backup should
/// not be automatically created.
/// A global account data event which tells us that a new backup should
/// be automatically created.
///
/// This event is defined in [MSC4287].
///
/// [MSC4287]: https://github.com/matrix-org/matrix-spec-proposals/pull/4287
#[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)]
#[ruma_event(type = "m.key_backup", kind = GlobalAccountData)]
pub(super) struct KeyBackupContent {
pub enabled: bool,
}
/// Unstable prefix form of [MSC4287].
///
/// [MSC4287]: https://github.com/matrix-org/matrix-spec-proposals/pull/4287
#[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)]
#[ruma_event(type = "m.org.matrix.custom.backup_disabled", kind = GlobalAccountData)]
pub(super) struct BackupDisabledContent {
pub disabled: bool,
+12 -9
View File
@@ -106,9 +106,12 @@ pub enum HttpError {
#[error(transparent)]
RefreshToken(RefreshTokenError),
/// Error while fetching the versions supported by the homeserver.
/// Error while fetching data that is cached.
///
/// This variant is present for convenience because cached data wraps
/// [`HttpError`] into an [`Arc`] to be able to clone it.
#[error(transparent)]
SupportedVersions(Arc<HttpError>),
Cached(Arc<HttpError>),
/// Error creating the TLS verifier.
#[cfg(target_os = "android")]
@@ -633,31 +636,31 @@ impl From<ReqwestError> for Error {
/// Errors that can happen when interacting with the beacon API.
#[derive(Debug, Error)]
pub enum BeaconError {
// A network error occurred.
/// A network error occurred.
#[error("Network error: {0}")]
Network(#[from] HttpError),
// The beacon information is not found.
/// The beacon information is not found.
#[error("Existing beacon information not found.")]
NotFound,
// The redacted event is not an error, but it's not useful for the client.
/// The redacted event is not an error, but it's not useful for the client.
#[error("Beacon event is redacted and cannot be processed.")]
Redacted,
// The client must join the room to access the beacon information.
/// The client must join the room to access the beacon information.
#[error("Must join the room to access beacon information.")]
Stripped,
// The beacon event could not be deserialized.
/// The beacon event could not be deserialized.
#[error("Deserialization error: {0}")]
Deserialization(#[from] serde_json::Error),
// The beacon event is expired.
/// The beacon event is expired.
#[error("The beacon event has expired.")]
NotLive,
// Allow for other errors to be wrapped.
/// Allow for other errors to be wrapped.
#[error("Other error: {0}")]
Other(Box<Error>),
}
+2 -2
View File
@@ -76,8 +76,8 @@ pub use client::{
sanitize_server_name,
};
pub use error::{
Error, HttpError, HttpResult, NotificationSettingsError, RefreshTokenError, Result,
RumaApiError,
BeaconError, Error, HttpError, HttpResult, NotificationSettingsError, RefreshTokenError,
Result, RumaApiError,
};
pub use http_client::TransmissionProgress;
#[cfg(all(feature = "e2e-encryption", feature = "sqlite"))]
@@ -1661,7 +1661,8 @@ async fn mock_download_session_from_key_backup(
.unwrap()
.megolm_v1_public_key()
.encrypt(inbound_group_session)
.await;
.await
.unwrap();
Mock::given(method("GET"))
.and(path(format!(
@@ -226,6 +226,26 @@ async fn enable(
) {
let recovery = client.encryption().recovery();
let key_backup_content = Arc::new(Mutex::new(None));
let _quard = Mock::given(method("PUT"))
.and(path(format!("_matrix/client/r0/user/{user_id}/account_data/m.key_backup")))
.and(header("authorization", "Bearer 1234"))
.and({
let key_backup_content = key_backup_content.clone();
move |request: &wiremock::Request| {
let content: Value = request.body_json().expect("The body should be a JSON body");
*key_backup_content.lock().unwrap() = Some(content);
true
}
})
.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
.expect(1)
.mount_as_scoped(server)
.await;
let backup_disabled_content = Arc::new(Mutex::new(None));
let _quard = Mock::given(method("PUT"))
@@ -493,6 +513,26 @@ async fn test_backups_enabling() {
.mount(&server)
.await;
Mock::given(method("PUT"))
.and(path(format!("_matrix/client/r0/user/{user_id}/account_data/m.key_backup")))
.and(header("authorization", "Bearer 1234"))
.and(|request: &wiremock::Request| {
#[derive(Deserialize)]
struct Enabled {
enabled: bool,
}
let content: Enabled = request.body_json().expect("The body should be a JSON body");
assert!(content.enabled, "The backup support should be marked as enabled.");
true
})
.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
.expect(1)
.mount(&server)
.await;
Mock::given(method("PUT"))
.and(path(format!(
"_matrix/client/r0/user/{user_id}/account_data/m.org.matrix.custom.backup_disabled"
@@ -612,6 +652,26 @@ async fn test_recovery_disabling() {
.mount(&server)
.await;
Mock::given(method("PUT"))
.and(path(format!("_matrix/client/r0/user/{user_id}/account_data/m.key_backup")))
.and(header("authorization", "Bearer 1234"))
.and(|request: &wiremock::Request| {
#[derive(Deserialize)]
struct Enabled {
enabled: bool,
}
let content: Enabled = request.body_json().expect("The body should be a JSON body");
assert!(!content.enabled, "The backup support should be marked as disabled.");
true
})
.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
.expect(1)
.mount(&server)
.await;
Mock::given(method("PUT"))
.and(path(format!(
"_matrix/client/r0/user/{user_id}/account_data/m.org.matrix.custom.backup_disabled"
@@ -907,6 +967,15 @@ async fn test_reset_identity() {
.await;
// Re-enable backups
Mock::given(method("PUT"))
.and(path(format!("_matrix/client/r0/user/{user_id}/account_data/m.key_backup")))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
.expect(1)
.named("m.key_backup PUT")
.mount(&server)
.await;
Mock::given(method("PUT"))
.and(path(format!(
"_matrix/client/r0/user/{user_id}/account_data/m.org.matrix.custom.backup_disabled"
@@ -919,18 +988,14 @@ async fn test_reset_identity() {
.await;
Mock::given(method("GET"))
.and(path(format!(
"_matrix/client/r0/user/{user_id}/account_data/m.org.matrix.custom.backup_disabled"
)))
.and(path(format!("_matrix/client/r0/user/{user_id}/account_data/m.key_backup")))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(
json!({"type": "m.org.matrix.custom.backup_disabled",
"content": {
"disabled": false
}}),
))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"type": "m.key_backup",
"content": {
"enabled": true
}})))
.expect(1)
.named("m.org.matrix.custom.backup_disabled GET")
.named("m.key_backup GET")
.mount(&server)
.await;
+10 -13
View File
@@ -770,21 +770,18 @@ impl App {
})
.collect(),
);
} else {
if let Some((query, room)) =
view.get_text().zip(self.room_view.room())
{
let mut room_search =
room.search_messages(query, 5);
} else if let Some((query, room)) =
view.get_text().zip(self.room_view.room())
{
let mut room_search = room.search_messages(query, 5);
let mut all_results = Vec::new();
while let Some(results) =
room_search.next_events().await?
{
all_results.extend(results);
}
view.set_results(vec![(None, all_results)]);
let mut all_results = Vec::new();
while let Some(results) =
room_search.next_events().await?
{
all_results.extend(results);
}
view.set_results(vec![(None, all_results)]);
}
}
}
@@ -1,4 +1,4 @@
FROM docker.io/matrixdotorg/synapse:v1.117.0
FROM docker.io/matrixdotorg/synapse:v1.151.0
ADD ci-start.sh /ci-start.sh
RUN chmod 770 /ci-start.sh
ENTRYPOINT /ci-start.sh
@@ -1648,9 +1648,9 @@ fn assert_received_room_key_bundle(sync_response: matrix_sdk::sync::SyncResponse
"Expected the to-device event to be decrypted"
);
assert_eq!(
"io.element.msc4268.room_key_bundle",
"m.room_key_bundle",
raw.get_field::<String>("type").unwrap().unwrap(),
"Expected the event type to be 'io.element.msc4268.room_key_bundle'"
"Expected the event type to be 'm.room_key_bundle'"
);
}
@@ -99,10 +99,7 @@ async fn test_e2ee_state_events() -> Result<()> {
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_eq!(raw.get_field::<String>("type").unwrap().unwrap(), "m.room_key_bundle");
bob.get_room(alice_room.room_id()).expect("Bob should have received the invite");
@@ -2,18 +2,23 @@ use std::{ops::Not as _, time::Duration};
use anyhow::Result;
use assert_matches2::{assert_let, assert_matches};
use eyeball::Subscriber;
use futures::FutureExt as _;
use matrix_sdk::{
RoomState,
Room, RoomState, assert_let_timeout,
encryption::{BackupDownloadStrategy, EncryptionSettings, recovery::RecoveryState},
event_cache::RoomEventCacheUpdate,
latest_events::LatestEventValue,
room::{MessagesOptions, edit::EditedContent::RoomMessage},
ruma::{
EventId,
api::client::room::create_room::v3::{Request as CreateRoomRequest, RoomPreset},
assign, event_id,
events::{
self, AnyRoomAccountDataEventContent, AnySyncStateEvent, AnySyncTimelineEvent,
Mentions, RoomAccountDataEventContent,
reaction::ReactionEventContent,
relation::Annotation,
room::message::{
OriginalSyncRoomMessageEvent, RoomMessageEventContent,
RoomMessageEventContentWithoutRelation,
@@ -25,9 +30,9 @@ use matrix_sdk::{
test_utils::assert_event_matches_msg,
};
use matrix_sdk_test::TestResult;
use matrix_sdk_ui::sync_service::SyncService;
use matrix_sdk_ui::{sync_service::SyncService, timeline::TimelineBuilder};
use tokio::{spawn, time::sleep};
use tracing::{debug, error};
use tracing::{debug, error, warn};
use crate::helpers::{TestClientBuilder, wait_for_room};
@@ -465,3 +470,188 @@ async fn test_unread_counts_get_updated_after_decryption() -> TestResult {
Ok(())
}
#[tokio::test]
async fn test_latest_event_few_rooms() -> Result<()> {
// Test plan:
// - Create a user
// - Create two rooms
// - one room with a message;
// - one room with a message and a reaction to it.
// - Create another client for that same user, and sync.
// - Check that each room has their own latest event.
let bob = TestClientBuilder::new("bob").use_sqlite().build().await?;
bob.event_cache().subscribe()?;
// Spawn sync for bob.
let bob_sync_service = SyncService::builder(bob.clone()).build().await?;
bob_sync_service.start().await;
// Bob creates two rooms.
let room1 = bob
.create_room(assign!(CreateRoomRequest::new(), {
preset: Some(RoomPreset::PrivateChat)
}))
.await?;
let room2 = bob
.create_room(assign!(CreateRoomRequest::new(), {
preset: Some(RoomPreset::PrivateChat)
}))
.await?;
while bob.rooms().len() < 2 {
sleep(Duration::from_millis(100)).await;
}
// Subscribe to the latest events for both rooms.
let mut room1_sub = bob
.latest_events()
.await
.listen_and_subscribe_to_room(room1.room_id())
.await?
.expect("room 1 exists");
let mut room2_sub = bob
.latest_events()
.await
.listen_and_subscribe_to_room(room2.room_id())
.await?
.expect("room 2 exists");
// Send messages in both rooms, and a reaction in the second one.
let room1_msg_event_id =
room1.send(RoomMessageEventContent::text_plain("Hello room 1!")).await?.response.event_id;
let room2_msg_event_id =
room2.send(RoomMessageEventContent::text_plain("Hello room 2!")).await?.response.event_id;
let reaction_event_id = room2
.send(ReactionEventContent::new(Annotation::new(
room2_msg_event_id.clone(),
"👋".to_owned(),
)))
.await?
.response
.event_id;
warn!(%room2_msg_event_id, %reaction_event_id, "Sent messages and reaction, waiting for them to be processed…");
// Wait for both latest events to be updated.
async fn assert_latest_event_is_remote_event(
room: &Room,
room_sub: &mut Subscriber<LatestEventValue, eyeball::AsyncLock>,
event_id: &EventId,
) {
debug!(room_id = %room.room_id(), %event_id, "Checking if the latest event is the expected one…");
let mut run_loop = true;
if let LatestEventValue::Remote(e) = room.latest_event()
&& e.event_id().unwrap() == event_id
{
run_loop = false;
}
if run_loop {
loop {
assert_let_timeout!(Duration::from_secs(5), Some(latest_event) = room_sub.next());
if matches!(latest_event, LatestEventValue::Remote(event) if event.event_id().as_deref() == Some(event_id))
{
break;
}
}
}
}
debug!("Running check for first client, room1");
assert_latest_event_is_remote_event(&room1, &mut room1_sub, &room1_msg_event_id).await;
debug!("Running check for first client, room2");
assert_latest_event_is_remote_event(&room2, &mut room2_sub, &room2_msg_event_id).await;
sleep(Duration::from_secs(1)).await;
while let Some(Some(up)) = room2_sub.next().now_or_never() {
assert_let!(LatestEventValue::Remote(event) = up);
assert_eq!(event.event_id().as_ref(), Some(&room2_msg_event_id));
}
bob_sync_service.stop().await;
// Relog Bob on another device.
let bob_user_id = bob.user_id().unwrap().to_owned();
let bob2 = TestClientBuilder::with_exact_username(bob_user_id.localpart().to_owned())
.use_sqlite()
.build()
.await?;
bob2.event_cache().subscribe()?;
let bob2_sync_service = SyncService::builder(bob2.clone()).build().await?;
bob2_sync_service.start().await;
// Wait for the rooms to be loaded.
while bob2.rooms().len() < 2 {
sleep(Duration::from_millis(100)).await;
}
let mut room1_sub = bob2
.latest_events()
.await
.listen_and_subscribe_to_room(room1.room_id())
.await?
.expect("room 1 exists");
let mut room2_sub = bob2
.latest_events()
.await
.listen_and_subscribe_to_room(room2.room_id())
.await?
.expect("room 2 exists");
let room1 = bob2.get_room(room1.room_id()).unwrap();
let room2 = bob2.get_room(room2.room_id()).unwrap();
debug!("Running check for second client, room1");
assert_latest_event_is_remote_event(&room1, &mut room1_sub, &room1_msg_event_id).await;
// Test passes if we uncomment this line, since more events will be fetched from
// the server and there will be a latest event update.
room2.event_cache().await?.0.pagination().run_backwards_until(100).await?;
//warn!("Subscribing to rooms on second client…");
//bob2_sync_service
//.room_list_service()
//.subscribe_to_rooms(&[room1.room_id(), room2.room_id()])
//.await;
//// Wait for a bit, for a sync response to be returned.
//sleep(Duration::from_secs(3)).await;
warn!("Rendering timeline.");
let timeline = TimelineBuilder::new(&room2).build().await?;
let items = timeline.items().await;
// Basically render all items
for item in items {
let Some(ev_item) = item.as_event() else { continue };
let Some(event_id) = ev_item.event_id() else { continue };
match ev_item.content() {
matrix_sdk_ui::timeline::TimelineItemContent::MsgLike(msglikecontent) => {
match &msglikecontent.kind {
matrix_sdk_ui::timeline::MsgLikeKind::Message(msg) => {
debug!(%event_id, "event: {}", msg.body());
}
_ => unreachable!(),
}
}
_ => debug!(%event_id, "event: non-msglike"),
}
}
drop(timeline);
warn!("Done rendering timeline.");
debug!("Running check for second client, room2");
assert_latest_event_is_remote_event(&room2, &mut room2_sub, &room2_msg_event_id).await;
Ok(())
}