Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f92ccd8d40 | |||
| 2c2e0f1b15 | |||
| f314578df5 | |||
| 739289cd82 | |||
| 1d7d6c943b | |||
| 917f9ee298 | |||
| c7d44ddad3 | |||
| 1a170ddf90 | |||
| 696d1cacbe | |||
| c6d1cf20b6 | |||
| 38f34e66eb | |||
| 8fa411aaa0 | |||
| b4b3b5258c | |||
| fd6cb6647f | |||
| 103ff7d3ec | |||
| efa28a1ffd | |||
| c4640897d4 | |||
| 0487a143ed | |||
| 7ab97d59b8 | |||
| 940862ebed | |||
| e507eaabf6 |
Generated
+46
-45
@@ -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
@@ -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 }
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+1
-1
@@ -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";
|
||||
}
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
+1
-1
@@ -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::{
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>),
|
||||
}
|
||||
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user