Compare commits
326 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e17fc2072 | |||
| f08e978540 | |||
| 40b09cda2f | |||
| 2710a85897 | |||
| 315e6c9d85 | |||
| 2822f2471a | |||
| b0530ba3a6 | |||
| 844212e965 | |||
| eb33c3754a | |||
| cf57992346 | |||
| d9f99f84f5 | |||
| afd05a24df | |||
| 0f91eebc96 | |||
| 0f8b99b744 | |||
| 30714c3f92 | |||
| 96b427a332 | |||
| 1c7bf820bf | |||
| 24b879bbc0 | |||
| 51a0bb6f3b | |||
| 9eca314511 | |||
| c4724c082e | |||
| 3d9d3b7ca6 | |||
| 541d9184c6 | |||
| 45bdbf5067 | |||
| 04ef3d9f95 | |||
| 75fe874cae | |||
| 2a77aaa068 | |||
| 4a686229e1 | |||
| d64fb8241d | |||
| 870d1eafb4 | |||
| a9905eaedd | |||
| f1c15da87d | |||
| f2b53080b6 | |||
| 7fe4e22076 | |||
| b6dab1a3a5 | |||
| ec833c81e0 | |||
| de0574aa14 | |||
| fb1ff70538 | |||
| d6bcbf2281 | |||
| cbc832411d | |||
| 843fcac3c1 | |||
| b3a8f34655 | |||
| 73af3d9cfa | |||
| 09e355a7bc | |||
| 9ba09b2ae8 | |||
| c9b02ad068 | |||
| 07c428ec56 | |||
| d4b8f88e10 | |||
| 28a58479e7 | |||
| 320b868694 | |||
| 8615b1283a | |||
| 9a4d539428 | |||
| 4d3fc44425 | |||
| 8b878c6591 | |||
| 738f7f0336 | |||
| f5fb44bd80 | |||
| e0ba9f5a22 | |||
| b5aeea0a3b | |||
| 19526cea6b | |||
| 99d0b37914 | |||
| b21a438b7e | |||
| f3101baa08 | |||
| 0cf99db001 | |||
| a032d33d21 | |||
| 92212cc328 | |||
| 6ffb0181e4 | |||
| 614bf942c0 | |||
| 512509ce8b | |||
| dfb33c9534 | |||
| 03aad4e965 | |||
| 9e67b6fcb1 | |||
| 9443f455a4 | |||
| 384deec1c8 | |||
| 5310f41cda | |||
| 95a1db8fc7 | |||
| 3d9dffa3b7 | |||
| 3d7e1d5989 | |||
| a46bf76d74 | |||
| 326935db63 | |||
| 416bc8b0e4 | |||
| edd113a17c | |||
| 66411d7b2e | |||
| 4a53bf1f3d | |||
| a3aa55ce91 | |||
| 0066ae6614 | |||
| 17ebc23719 | |||
| cdb3449ce2 | |||
| c5b11fc2f8 | |||
| 2ad6acb930 | |||
| dac779f4fc | |||
| e2ea19ee77 | |||
| cc38768bf4 | |||
| c87bd4d4ec | |||
| 5700c700f0 | |||
| fff9882792 | |||
| 45f8ff11c2 | |||
| 7c9d842d05 | |||
| c3706d7ca0 | |||
| b314da37b8 | |||
| 02ba6c0dbe | |||
| 22d9c62262 | |||
| 18b6387a7a | |||
| c5ddba2e13 | |||
| 9756ed28cf | |||
| 5701cea51e | |||
| 794b7ead7a | |||
| f879f3d866 | |||
| 4ebb8e29b4 | |||
| d764fca7da | |||
| 3b7f9f7361 | |||
| 73770b78bb | |||
| 7cffc34984 | |||
| c4ef967523 | |||
| f0b378179e | |||
| ee344112f3 | |||
| 1f52aca210 | |||
| 74091de8ef | |||
| 0f6efc391a | |||
| c707e1f17e | |||
| 5ab69f7400 | |||
| e652069896 | |||
| 5337c9d9ea | |||
| 9fd52e5df7 | |||
| 4ab79a4085 | |||
| a132c0a885 | |||
| 5081802177 | |||
| 736188811f | |||
| 1337fdf0b8 | |||
| 0573137835 | |||
| 21ce2b07a7 | |||
| 6451a9dffe | |||
| a6af31984a | |||
| 2de45bcb51 | |||
| 7bdc1448bd | |||
| 511c44c588 | |||
| a915900580 | |||
| e12f6fcbb7 | |||
| 72dfd3d1fd | |||
| a6c206118d | |||
| 2536373546 | |||
| 045d94ab4b | |||
| d8c02d7e55 | |||
| b20313c492 | |||
| c3ea2c3736 | |||
| 2c1377b2b5 | |||
| bb85af9279 | |||
| 58ab6704ff | |||
| 94a64296d1 | |||
| 7d6c16956f | |||
| 916bf69e5c | |||
| 915c10e1b4 | |||
| 5e122c5c5e | |||
| 49e5461ef7 | |||
| 27927c676d | |||
| ac5a9e106b | |||
| 94c4e685fc | |||
| f248e272e9 | |||
| 97026fc3a6 | |||
| 1fcd5af526 | |||
| 8b04db666c | |||
| 1ff2c5bb3e | |||
| 05f0106e06 | |||
| fd0f369f75 | |||
| 39fc283353 | |||
| 3ebd8afa49 | |||
| 2483ba2cc6 | |||
| 2d3a458a08 | |||
| 246a128ec3 | |||
| 932f12e76d | |||
| bfe79468c6 | |||
| 9503eb49c7 | |||
| ded854425a | |||
| 959e90252b | |||
| e761ad8f97 | |||
| ceeb5e78b6 | |||
| 04c4284b33 | |||
| bae191b4ed | |||
| ea2e85c5f5 | |||
| d6401ef278 | |||
| 9bba437fdd | |||
| 19e65c05cf | |||
| 369ca7024f | |||
| e958b1ce28 | |||
| 4912cd8a40 | |||
| c99b0e8344 | |||
| b38f501902 | |||
| f37467f81f | |||
| 6239231ba0 | |||
| aa1623b891 | |||
| 99131d0d7a | |||
| c5c62d8fda | |||
| b909f4400d | |||
| 18d69f7515 | |||
| 456d8bb4f2 | |||
| b277423237 | |||
| 4621dd4317 | |||
| 17797da71a | |||
| eec4227dfc | |||
| 64ddfd872c | |||
| 19a990ad41 | |||
| 53ad6f5fe5 | |||
| 8038414de2 | |||
| 560d71ceea | |||
| 6591a6ef04 | |||
| 35bac2a6c3 | |||
| 7455b90d24 | |||
| 1e359576ad | |||
| fe02752f29 | |||
| aa7d2a21a3 | |||
| 337f2ad415 | |||
| c5c3850edf | |||
| 086e988e68 | |||
| 5c37acb81c | |||
| 2aaa709b0e | |||
| 8fe501c4ed | |||
| 79c020f169 | |||
| bb4254b9c9 | |||
| ee0010e831 | |||
| c9ddf92e25 | |||
| c7712b52e6 | |||
| ead29388f2 | |||
| 06c9a1a355 | |||
| 0ed4a9950c | |||
| 844340e0f7 | |||
| 692be61043 | |||
| d0b6771251 | |||
| 47298b9200 | |||
| a262cb23d9 | |||
| a881cd0712 | |||
| c536760fc1 | |||
| 894f4c218d | |||
| 6472ba5109 | |||
| bcf33c0bcb | |||
| fe541bc601 | |||
| 2d3ee89477 | |||
| cb1827f151 | |||
| 4299697935 | |||
| 7e53c6821b | |||
| f21a9b704b | |||
| cb08e5a103 | |||
| e43a25a703 | |||
| ee9af5f501 | |||
| f9aeb590e0 | |||
| 2d2eef8b0e | |||
| bfd2a1c445 | |||
| 6e5682d8d2 | |||
| 4a428b4731 | |||
| c24830d794 | |||
| 5786b1a631 | |||
| 445bf3b02a | |||
| 71a2d23bf3 | |||
| 955d611aaa | |||
| 0f4a175a99 | |||
| b553dbb31c | |||
| 1abe039582 | |||
| 6c490b7aec | |||
| c17acd5fe3 | |||
| 8c05d09e4d | |||
| 72254caf08 | |||
| b232cd37e8 | |||
| 6baf092bc8 | |||
| c109119b35 | |||
| 8bce5e2416 | |||
| a03b29ac70 | |||
| c7f7941734 | |||
| 8895ce40d1 | |||
| edf32e8941 | |||
| 73ddd34cb1 | |||
| 0916c93641 | |||
| 8a65e32e7e | |||
| 91c4bc0b3e | |||
| 6f710063b5 | |||
| e788a6b099 | |||
| 8c1baf6ba8 | |||
| 99032511aa | |||
| 645bbd67df | |||
| 26ede608b0 | |||
| 554aa0404a | |||
| 3438e7de9b | |||
| 44bb9a3d8f | |||
| bc5b190509 | |||
| 2e20f00d99 | |||
| 760053b15d | |||
| e599fa1ccf | |||
| c7fbfd4db8 | |||
| ad392a1977 | |||
| 57136247bf | |||
| e5b06bd6d8 | |||
| beb01eacfa | |||
| 1e9fab1e4e | |||
| 1be7fab4fd | |||
| ad5761bfb5 | |||
| efb72063ac | |||
| 9ef6103912 | |||
| 71c6b98d6a | |||
| dc86835ae2 | |||
| ab4c524212 | |||
| c2f422209f | |||
| d90c623375 | |||
| abd6779210 | |||
| d48c27dc86 | |||
| 303417eae2 | |||
| 017d72e80f | |||
| d7f6231acd | |||
| 6a27fc2e04 | |||
| 3481cde1dc | |||
| d821d611ba | |||
| 4485abbfdf | |||
| 2d19c7ad65 | |||
| 1692460b30 | |||
| 250d63c6da | |||
| 0a33642851 | |||
| 64c9fa5542 | |||
| d042bcce04 | |||
| 7d52322687 | |||
| b797635255 | |||
| d9845c1658 | |||
| 4428b525ea | |||
| b67ca2c123 | |||
| 7be84ca71a | |||
| 9956b56a2c | |||
| afcc7022a2 | |||
| 8de33f68f3 | |||
| 2efb09907b | |||
| 91e7f2f722 | |||
| 7e1eaddf5d |
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2023-07-03
|
||||
toolchain: nightly-2023-11-08
|
||||
components: rustfmt
|
||||
|
||||
- name: Run Benchmarks
|
||||
|
||||
@@ -281,7 +281,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2023-07-03
|
||||
toolchain: nightly-2023-11-08
|
||||
components: rustfmt
|
||||
|
||||
- name: Cargo fmt
|
||||
@@ -298,7 +298,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Check the spelling of the files in our repo
|
||||
uses: crate-ci/typos@v1.16.17
|
||||
uses: crate-ci/typos@v1.17.0
|
||||
|
||||
clippy:
|
||||
name: Run clippy
|
||||
@@ -318,7 +318,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2023-07-03
|
||||
toolchain: nightly-2023-11-08
|
||||
components: clippy
|
||||
|
||||
- name: Load cache
|
||||
|
||||
@@ -17,6 +17,9 @@ concurrency:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
# without matrix_sdk=trace, expressions in `trace!` fields are not evaluated
|
||||
# when the `trace!` statement is hit, and thus not covered
|
||||
RUST_LOG: info,matrix_sdk=trace
|
||||
|
||||
jobs:
|
||||
code_coverage:
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2023-07-03
|
||||
toolchain: nightly-2023-11-08
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
|
||||
@@ -21,6 +21,7 @@ WeeChat = "WeeChat"
|
||||
sing = "sign"
|
||||
singed = "signed"
|
||||
singing = "signing"
|
||||
Nd = "Nd"
|
||||
|
||||
[files]
|
||||
# Our json files contain a bunch of base64 encoded ed25519 keys which aren't
|
||||
|
||||
Generated
+1013
-1083
File diff suppressed because it is too large
Load Diff
+19
-7
@@ -27,7 +27,6 @@ async-trait = "0.1.60"
|
||||
as_variant = "1.2.0"
|
||||
base64 = "0.21.0"
|
||||
byteorder = "1.4.3"
|
||||
ctor = "0.2.0"
|
||||
eyeball = { version = "0.8.7", features = ["tracing"] }
|
||||
eyeball-im = { version = "0.4.1", features = ["tracing"] }
|
||||
eyeball-im-util = "0.5.1"
|
||||
@@ -35,10 +34,11 @@ futures-core = "0.3.28"
|
||||
futures-executor = "0.3.21"
|
||||
futures-util = { version = "0.3.26", default-features = false, features = ["alloc"] }
|
||||
http = "0.2.6"
|
||||
itertools = "0.11.0"
|
||||
ruma = { version = "0.9.2", features = ["client-api-c", "compat-upload-signatures", "compat-user-id", "compat-arbitrary-length-ids", "unstable-msc3401"] }
|
||||
itertools = "0.12.0"
|
||||
ruma = { version = "0.9.3", features = ["client-api-c", "compat-upload-signatures", "compat-user-id", "compat-arbitrary-length-ids", "unstable-msc3401"] }
|
||||
ruma-common = "0.12.0"
|
||||
once_cell = "1.16.0"
|
||||
rand = "0.8.5"
|
||||
serde = "1.0.151"
|
||||
serde_html_form = "0.2.0"
|
||||
serde_json = "1.0.91"
|
||||
@@ -46,13 +46,25 @@ sha2 = "0.10.8"
|
||||
stream_assert = "0.1.1"
|
||||
thiserror = "1.0.38"
|
||||
tokio = { version = "1.30.0", default-features = false, features = ["sync"] }
|
||||
tracing = { version = "0.1.36", default-features = false, features = ["std"] }
|
||||
tracing-core = "0.1.30"
|
||||
uniffi = { git = "https://github.com/mozilla/uniffi-rs", rev = "48b1f51f6e5406cab20f502f535b30a589a5b140" }
|
||||
uniffi_bindgen = { git = "https://github.com/mozilla/uniffi-rs", rev = "48b1f51f6e5406cab20f502f535b30a589a5b140" }
|
||||
tokio-stream = "0.1.14"
|
||||
tracing = { version = "0.1.40", default-features = false, features = ["std"] }
|
||||
tracing-core = "0.1.32"
|
||||
uniffi = { version = "0.25.3", git = "https://github.com/mozilla/uniffi-rs", rev = "0d58c94cbd2ef63554f3388d03d55984be76bb1f" }
|
||||
uniffi_bindgen = { version = "0.25.3", git = "https://github.com/mozilla/uniffi-rs", rev = "0d58c94cbd2ef63554f3388d03d55984be76bb1f" }
|
||||
vodozemac = "0.5.0"
|
||||
zeroize = "1.6.0"
|
||||
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.7.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.7.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.7.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.7.0" }
|
||||
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.7.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.7.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.7.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.7.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.7.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.7.0", default-features = false }
|
||||
|
||||
# Default release profile, select with `--release`
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
@@ -9,18 +9,18 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
criterion = { version = "0.5.1", features = ["async", "async_tokio", "html_reports"] }
|
||||
matrix-sdk-base = { path = "../crates/matrix-sdk-base" }
|
||||
matrix-sdk-crypto = { path = "../crates/matrix-sdk-crypto", version = "0.6.0"}
|
||||
matrix-sdk-sqlite = { path = "../crates/matrix-sdk-sqlite", version = "0.1.0", default-features = false, features = ["crypto-store"] }
|
||||
matrix-sdk-test = { path = "../testing/matrix-sdk-test", version = "0.6.0"}
|
||||
matrix-sdk = { path = "../crates/matrix-sdk" }
|
||||
matrix-sdk-base = { workspace = true }
|
||||
matrix-sdk-crypto = { workspace = true }
|
||||
matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
|
||||
matrix-sdk-test = { workspace = true }
|
||||
matrix-sdk = { workspace = true }
|
||||
ruma = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tempfile = "3.3.0"
|
||||
tokio = { version = "1.24.2", default-features = false, features = ["rt-multi-thread"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
pprof = { version = "0.12.0", features = ["flamegraph", "criterion"] }
|
||||
pprof = { version = "0.13.0", features = ["flamegraph", "criterion"] }
|
||||
|
||||
[[bench]]
|
||||
name = "crypto_bench"
|
||||
|
||||
@@ -30,7 +30,7 @@ fn keys_query_response() -> get_keys::v3::Response {
|
||||
let data: Value = serde_json::from_slice(data).unwrap();
|
||||
let data = response_from_file(&data);
|
||||
get_keys::v3::Response::try_from_http_response(data)
|
||||
.expect("Can't parse the keys upload response")
|
||||
.expect("Can't parse the `/keys/upload` response")
|
||||
}
|
||||
|
||||
fn keys_claim_response() -> claim_keys::v3::Response {
|
||||
@@ -38,7 +38,7 @@ fn keys_claim_response() -> claim_keys::v3::Response {
|
||||
let data: Value = serde_json::from_slice(data).unwrap();
|
||||
let data = response_from_file(&data);
|
||||
claim_keys::v3::Response::try_from_http_response(data)
|
||||
.expect("Can't parse the keys upload response")
|
||||
.expect("Can't parse the `/keys/upload` response")
|
||||
}
|
||||
|
||||
fn huge_keys_query_response() -> get_keys::v3::Response {
|
||||
@@ -46,7 +46,7 @@ fn huge_keys_query_response() -> get_keys::v3::Response {
|
||||
let data: Value = serde_json::from_slice(data).unwrap();
|
||||
let data = response_from_file(&data);
|
||||
get_keys::v3::Response::try_from_http_response(data)
|
||||
.expect("Can't parse the keys query response")
|
||||
.expect("Can't parse the `/keys/query` response")
|
||||
}
|
||||
|
||||
pub fn keys_query(c: &mut Criterion) {
|
||||
|
||||
@@ -21,8 +21,9 @@ anyhow = { workspace = true }
|
||||
futures-util = "0.3.28"
|
||||
hmac = "0.12.1"
|
||||
http = { workspace = true }
|
||||
matrix-sdk-common = { workspace = true }
|
||||
pbkdf2 = "0.12.2"
|
||||
rand = "0.8.5"
|
||||
rand = { workspace = true }
|
||||
ruma = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
@@ -38,20 +39,12 @@ zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
version = "0.2.2"
|
||||
features = ["lax_deserialize"]
|
||||
|
||||
[dependencies.matrix-sdk-common]
|
||||
path = "../../crates/matrix-sdk-common"
|
||||
version = "0.6.0"
|
||||
|
||||
[dependencies.matrix-sdk-crypto]
|
||||
path = "../../crates/matrix-sdk-crypto"
|
||||
version = "0.6.0"
|
||||
default_features = false
|
||||
features = ["qrcode", "backups_v1", "automatic-room-key-forwarding"]
|
||||
workspace = true
|
||||
features = ["qrcode", "automatic-room-key-forwarding"]
|
||||
|
||||
[dependencies.matrix-sdk-sqlite]
|
||||
path = "../../crates/matrix-sdk-sqlite"
|
||||
version = "0.1.0"
|
||||
default_features = false
|
||||
workspace = true
|
||||
features = ["crypto-store"]
|
||||
|
||||
[dependencies.tokio]
|
||||
|
||||
@@ -149,7 +149,22 @@ impl BackupRecoveryKey {
|
||||
let signatures: HashMap<String, HashMap<String, String>> = public_key
|
||||
.signatures()
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_string(), v.into_iter().map(|(k, v)| (k.to_string(), v)).collect()))
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k.to_string(),
|
||||
v.into_iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k.to_string(),
|
||||
match v {
|
||||
Ok(s) => s.to_base64(),
|
||||
Err(s) => s.source,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
MegolmV1BackupKey {
|
||||
|
||||
@@ -46,9 +46,10 @@ pub struct DehydratedDevices {
|
||||
impl Drop for DehydratedDevices {
|
||||
fn drop(&mut self) {
|
||||
// See the drop implementation for the `crate::OlmMachine` for an explanation.
|
||||
let inner = unsafe { ManuallyDrop::take(&mut self.inner) };
|
||||
let _guard = self.runtime.enter();
|
||||
drop(inner);
|
||||
unsafe {
|
||||
ManuallyDrop::drop(&mut self.inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,9 +100,10 @@ pub struct RehydratedDevice {
|
||||
impl Drop for RehydratedDevice {
|
||||
fn drop(&mut self) {
|
||||
// See the drop implementation for the `crate::OlmMachine` for an explanation.
|
||||
let inner = unsafe { ManuallyDrop::take(&mut self.inner) };
|
||||
let _guard = self.runtime.enter();
|
||||
drop(inner);
|
||||
unsafe {
|
||||
ManuallyDrop::drop(&mut self.inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,9 +126,10 @@ pub struct DehydratedDevice {
|
||||
impl Drop for DehydratedDevice {
|
||||
fn drop(&mut self) {
|
||||
// See the drop implementation for the `crate::OlmMachine` for an explanation.
|
||||
let inner = unsafe { ManuallyDrop::take(&mut self.inner) };
|
||||
let _guard = self.runtime.enter();
|
||||
drop(inner);
|
||||
unsafe {
|
||||
ManuallyDrop::drop(&mut self.inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -460,7 +460,7 @@ fn collect_sessions(
|
||||
.signing_key
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
let algorithm = DeviceKeyAlgorithm::try_from(k)?;
|
||||
let algorithm = DeviceKeyAlgorithm::from(k);
|
||||
let key = SigningKey::from_parts(&algorithm, v)?;
|
||||
|
||||
Ok((algorithm, key))
|
||||
|
||||
@@ -113,14 +113,15 @@ pub struct OlmMachine {
|
||||
|
||||
impl Drop for OlmMachine {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: self.inner is never used again, which is the only requirement
|
||||
// for ManuallyDrop::take to be used safely.
|
||||
let inner = unsafe { ManuallyDrop::take(&mut self.inner) };
|
||||
let _guard = self.runtime.enter();
|
||||
// Dropping the inner OlmMachine must happen within a tokio context
|
||||
// because deadpool drops sqlite connections in the DB pool on tokio's
|
||||
// blocking threadpool to avoid blocking async worker threads.
|
||||
drop(inner);
|
||||
let _guard = self.runtime.enter();
|
||||
// SAFETY: self.inner is never used again, which is the only requirement
|
||||
// for ManuallyDrop::drop to be used safely.
|
||||
unsafe {
|
||||
ManuallyDrop::drop(&mut self.inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,7 +567,7 @@ impl OlmMachine {
|
||||
///
|
||||
/// *Note*: Only users that aren't already tracked will be considered for an
|
||||
/// update. It's safe to call this with already tracked users, it won't
|
||||
/// result in excessive keys query requests.
|
||||
/// result in excessive `/keys/query` requests.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@@ -789,11 +790,11 @@ impl OlmMachine {
|
||||
content: String,
|
||||
) -> Result<String, CryptoStoreError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
let content: Value = serde_json::from_str(&content)?;
|
||||
let content = serde_json::from_str(&content)?;
|
||||
|
||||
let encrypted_content = self
|
||||
.runtime
|
||||
.block_on(self.inner.encrypt_room_event_raw(&room_id, content, &event_type))
|
||||
.block_on(self.inner.encrypt_room_event_raw(&room_id, &event_type, &content))
|
||||
.expect("Encrypting an event produced an error");
|
||||
|
||||
Ok(serde_json::to_string(&encrypted_content)?)
|
||||
@@ -1464,6 +1465,7 @@ impl OlmMachine {
|
||||
progress_listener.on_progress(progress as i32, total as i32)
|
||||
};
|
||||
|
||||
#[allow(deprecated)]
|
||||
let result =
|
||||
self.runtime.block_on(self.inner.import_room_keys(keys, from_backup, listener))?;
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ impl From<OutgoingRequest> for Request {
|
||||
Request::KeysUpload {
|
||||
request_id: r.request_id().to_string(),
|
||||
body: serde_json::to_string(&body)
|
||||
.expect("Can't serialize keys upload request"),
|
||||
.expect("Can't serialize `/keys/upload` request"),
|
||||
}
|
||||
}
|
||||
KeysQuery(k) => {
|
||||
|
||||
@@ -29,11 +29,12 @@ eyeball-im = { workspace = true }
|
||||
extension-trait = "1.0.1"
|
||||
futures-core = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
matrix-sdk-ui = { path = "../../crates/matrix-sdk-ui", default-features = false, features = ["e2e-encryption"] }
|
||||
matrix-sdk-ui = { workspace = true, features = ["e2e-encryption", "uniffi"] }
|
||||
mime = "0.3.16"
|
||||
once_cell = { workspace = true }
|
||||
opentelemetry = { version = "0.20.0", features = ["rt-tokio"] }
|
||||
opentelemetry-otlp = { version = "0.13.0", features = ["tokio", "reqwest-client", "http-proto"] }
|
||||
opentelemetry = "0.21.0"
|
||||
opentelemetry_sdk = { version = "0.21.0", features = ["rt-tokio"] }
|
||||
opentelemetry-otlp = { version = "0.14.0", features = ["tokio", "reqwest-client", "http-proto"] }
|
||||
ruma = { workspace = true, features = ["html", "unstable-unspecified", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat"] }
|
||||
sanitize-filename-reader-friendly = "2.2.1"
|
||||
serde = { workspace = true }
|
||||
@@ -41,11 +42,11 @@ serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-core = { workspace = true }
|
||||
tracing-opentelemetry = { version = "0.20.0" }
|
||||
tracing-opentelemetry = "0.22.0"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tracing-appender = { version = "0.2.2" }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
tokio-stream = "0.1.8"
|
||||
tokio-stream = { workspace = true, features = ["time"] }
|
||||
uniffi = { workspace = true, features = ["tokio"] }
|
||||
url = "2.2.2"
|
||||
zeroize = { workspace = true }
|
||||
@@ -57,8 +58,7 @@ log-panics = { version = "2", features = ["with-backtrace"] }
|
||||
paranoid-android = "0.2.1"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies.matrix-sdk]
|
||||
path = "../../crates/matrix-sdk"
|
||||
default-features = false
|
||||
workspace = true
|
||||
features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
@@ -72,8 +72,7 @@ features = [
|
||||
]
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies.matrix-sdk]
|
||||
path = "../../crates/matrix-sdk"
|
||||
default-features = false
|
||||
workspace = true
|
||||
features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
|
||||
@@ -20,15 +20,3 @@ interface MediaSource {
|
||||
string to_json();
|
||||
string url();
|
||||
};
|
||||
|
||||
enum EventItemOrigin {
|
||||
"Local",
|
||||
"Sync",
|
||||
"Pagination",
|
||||
};
|
||||
|
||||
enum BackPaginationStatus {
|
||||
"Idle",
|
||||
"Paginating",
|
||||
"TimelineStartReached",
|
||||
};
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
mem::ManuallyDrop,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use matrix_sdk::{
|
||||
@@ -36,6 +40,7 @@ use matrix_sdk_ui::notification_client::NotificationProcessSetup as MatrixNotifi
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
api::client::discovery::discover_homeserver::AuthenticationServerInfo,
|
||||
events::room::power_levels::RoomPowerLevelsEventContent,
|
||||
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -47,9 +52,11 @@ use url::Url;
|
||||
use super::{room::Room, session_verification::SessionVerificationController, RUNTIME};
|
||||
use crate::{
|
||||
client,
|
||||
encryption::Encryption,
|
||||
notification::NotificationClientBuilder,
|
||||
notification_settings::NotificationSettings,
|
||||
sync_service::{SyncService, SyncServiceBuilder},
|
||||
task_handle::TaskHandle,
|
||||
ClientError,
|
||||
};
|
||||
|
||||
@@ -147,12 +154,26 @@ impl From<matrix_sdk::TransmissionProgress> for TransmissionProgress {
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Client {
|
||||
pub(crate) inner: MatrixClient,
|
||||
pub(crate) inner: ManuallyDrop<MatrixClient>,
|
||||
delegate: RwLock<Option<Arc<dyn ClientDelegate>>>,
|
||||
session_verification_controller:
|
||||
Arc<tokio::sync::RwLock<Option<SessionVerificationController>>>,
|
||||
}
|
||||
|
||||
impl Drop for Client {
|
||||
fn drop(&mut self) {
|
||||
// Dropping the inner OlmMachine must happen within a tokio context
|
||||
// because deadpool drops sqlite connections in the DB pool on tokio's
|
||||
// blocking threadpool to avoid blocking async worker threads.
|
||||
let _guard = RUNTIME.enter();
|
||||
// SAFETY: self.inner is never used again, which is the only requirement
|
||||
// for ManuallyDrop::drop to be used safely.
|
||||
unsafe {
|
||||
ManuallyDrop::drop(&mut self.inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(
|
||||
sdk_client: MatrixClient,
|
||||
@@ -173,26 +194,11 @@ impl Client {
|
||||
});
|
||||
|
||||
let client = Arc::new(Client {
|
||||
inner: sdk_client,
|
||||
inner: ManuallyDrop::new(sdk_client),
|
||||
delegate: RwLock::new(None),
|
||||
session_verification_controller,
|
||||
});
|
||||
|
||||
let mut session_change_receiver = client.inner.subscribe_to_session_changes();
|
||||
let client_clone = client.clone();
|
||||
RUNTIME.spawn(async move {
|
||||
loop {
|
||||
match session_change_receiver.recv().await {
|
||||
Ok(session_change) => client_clone.process_session_change(session_change),
|
||||
Err(receive_error) => {
|
||||
if let RecvError::Closed = receive_error {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(process_id) = cross_process_refresh_lock_id {
|
||||
if session_delegate.is_none() {
|
||||
return Err(anyhow::anyhow!(
|
||||
@@ -230,7 +236,7 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl Client {
|
||||
/// Login using a username and password.
|
||||
pub fn login(
|
||||
@@ -253,31 +259,30 @@ impl Client {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_media_file(
|
||||
pub async fn get_media_file(
|
||||
&self,
|
||||
media_source: Arc<MediaSource>,
|
||||
body: Option<String>,
|
||||
mime_type: String,
|
||||
use_cache: bool,
|
||||
temp_dir: Option<String>,
|
||||
) -> Result<Arc<MediaFileHandle>, ClientError> {
|
||||
let client = self.inner.clone();
|
||||
let source = (*media_source).clone();
|
||||
let mime_type: mime::Mime = mime_type.parse()?;
|
||||
|
||||
RUNTIME.block_on(async move {
|
||||
let handle = client
|
||||
.media()
|
||||
.get_media_file(
|
||||
&MediaRequest { source, format: MediaFormat::File },
|
||||
body,
|
||||
&mime_type,
|
||||
true,
|
||||
temp_dir,
|
||||
)
|
||||
.await?;
|
||||
let handle = self
|
||||
.inner
|
||||
.media()
|
||||
.get_media_file(
|
||||
&MediaRequest { source, format: MediaFormat::File },
|
||||
body,
|
||||
&mime_type,
|
||||
use_cache,
|
||||
temp_dir,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Arc::new(MediaFileHandle { inner: handle }))
|
||||
})
|
||||
Ok(Arc::new(MediaFileHandle::new(handle)))
|
||||
}
|
||||
|
||||
/// Restores the client from a `Session`.
|
||||
@@ -343,14 +348,35 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl Client {
|
||||
pub fn set_delegate(&self, delegate: Option<Box<dyn ClientDelegate>>) {
|
||||
*self.delegate.write().unwrap() = delegate.map(Arc::from);
|
||||
pub fn set_delegate(
|
||||
self: Arc<Self>,
|
||||
delegate: Option<Box<dyn ClientDelegate>>,
|
||||
) -> Option<Arc<TaskHandle>> {
|
||||
delegate.map(|delegate| {
|
||||
let mut session_change_receiver = self.inner.subscribe_to_session_changes();
|
||||
let client_clone = self.clone();
|
||||
let session_change_task = RUNTIME.spawn(async move {
|
||||
loop {
|
||||
match session_change_receiver.recv().await {
|
||||
Ok(session_change) => client_clone.process_session_change(session_change),
|
||||
Err(receive_error) => {
|
||||
if let RecvError::Closed = receive_error {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
*self.delegate.write().unwrap() = Some(Arc::from(delegate));
|
||||
Arc::new(TaskHandle::new(session_change_task))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn session(&self) -> Result<Session, ClientError> {
|
||||
RUNTIME.block_on(async move { Self::session_inner(self.inner.clone()).await })
|
||||
RUNTIME.block_on(async move { Self::session_inner((*self.inner).clone()).await })
|
||||
}
|
||||
|
||||
pub fn account_url(
|
||||
@@ -460,68 +486,65 @@ impl Client {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn upload_media(
|
||||
pub async fn upload_media(
|
||||
&self,
|
||||
mime_type: String,
|
||||
data: Vec<u8>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Result<String, ClientError> {
|
||||
let l = self.inner.clone();
|
||||
let mime_type: mime::Mime = mime_type.parse().context("Parsing mime type")?;
|
||||
let request = self.inner.media().upload(&mime_type, data);
|
||||
|
||||
RUNTIME.block_on(async move {
|
||||
let mime_type: mime::Mime = mime_type.parse().context("Parsing mime type")?;
|
||||
let request = l.media().upload(&mime_type, data);
|
||||
if let Some(progress_watcher) = progress_watcher {
|
||||
let mut subscriber = request.subscribe_to_send_progress();
|
||||
RUNTIME.spawn(async move {
|
||||
while let Some(progress) = subscriber.next().await {
|
||||
progress_watcher.transmission_progress(progress.into());
|
||||
}
|
||||
});
|
||||
}
|
||||
let response = request.await?;
|
||||
Ok(String::from(response.content_uri))
|
||||
})
|
||||
if let Some(progress_watcher) = progress_watcher {
|
||||
let mut subscriber = request.subscribe_to_send_progress();
|
||||
RUNTIME.spawn(async move {
|
||||
while let Some(progress) = subscriber.next().await {
|
||||
progress_watcher.transmission_progress(progress.into());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let response = request.await?;
|
||||
|
||||
Ok(String::from(response.content_uri))
|
||||
}
|
||||
|
||||
pub fn get_media_content(
|
||||
pub async fn get_media_content(
|
||||
&self,
|
||||
media_source: Arc<MediaSource>,
|
||||
) -> Result<Vec<u8>, ClientError> {
|
||||
let l = self.inner.clone();
|
||||
let source = (*media_source).clone();
|
||||
|
||||
RUNTIME.block_on(async move {
|
||||
Ok(l.media()
|
||||
.get_media_content(&MediaRequest { source, format: MediaFormat::File }, true)
|
||||
.await?)
|
||||
})
|
||||
Ok(self
|
||||
.inner
|
||||
.media()
|
||||
.get_media_content(&MediaRequest { source, format: MediaFormat::File }, true)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub fn get_media_thumbnail(
|
||||
pub async fn get_media_thumbnail(
|
||||
&self,
|
||||
media_source: Arc<MediaSource>,
|
||||
width: u64,
|
||||
height: u64,
|
||||
) -> Result<Vec<u8>, ClientError> {
|
||||
let l = self.inner.clone();
|
||||
let source = (*media_source).clone();
|
||||
|
||||
RUNTIME.block_on(async move {
|
||||
Ok(l.media()
|
||||
.get_media_content(
|
||||
&MediaRequest {
|
||||
source,
|
||||
format: MediaFormat::Thumbnail(MediaThumbnailSize {
|
||||
method: Method::Scale,
|
||||
width: UInt::new(width).unwrap(),
|
||||
height: UInt::new(height).unwrap(),
|
||||
}),
|
||||
},
|
||||
true,
|
||||
)
|
||||
.await?)
|
||||
})
|
||||
Ok(self
|
||||
.inner
|
||||
.media()
|
||||
.get_media_content(
|
||||
&MediaRequest {
|
||||
source,
|
||||
format: MediaFormat::Thumbnail(MediaThumbnailSize {
|
||||
method: Method::Scale,
|
||||
width: UInt::new(width).unwrap(),
|
||||
height: UInt::new(height).unwrap(),
|
||||
}),
|
||||
},
|
||||
true,
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub fn get_session_verification_controller(
|
||||
@@ -665,24 +688,28 @@ impl Client {
|
||||
}
|
||||
|
||||
pub fn notification_client(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
process_setup: NotificationProcessSetup,
|
||||
) -> Result<Arc<NotificationClientBuilder>, ClientError> {
|
||||
NotificationClientBuilder::new(self.inner.clone(), process_setup.into())
|
||||
NotificationClientBuilder::new(self.clone(), process_setup.into())
|
||||
}
|
||||
|
||||
pub fn sync_service(&self) -> Arc<SyncServiceBuilder> {
|
||||
SyncServiceBuilder::new(self.inner.clone())
|
||||
SyncServiceBuilder::new((*self.inner).clone())
|
||||
}
|
||||
|
||||
pub fn get_notification_settings(&self) -> Arc<NotificationSettings> {
|
||||
RUNTIME.block_on(async move {
|
||||
Arc::new(NotificationSettings::new(
|
||||
self.inner.clone(),
|
||||
(*self.inner).clone(),
|
||||
self.inner.notification_settings().await,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encryption(&self) -> Arc<Encryption> {
|
||||
Arc::new(self.inner.encryption().into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
@@ -782,6 +809,86 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct NotificationPowerLevels {
|
||||
pub room: i32,
|
||||
}
|
||||
|
||||
impl From<NotificationPowerLevels> for ruma::power_levels::NotificationPowerLevels {
|
||||
fn from(value: NotificationPowerLevels) -> Self {
|
||||
let mut notification_power_levels = Self::new();
|
||||
notification_power_levels.room = value.room.into();
|
||||
notification_power_levels
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct PowerLevels {
|
||||
pub users_default: Option<i32>,
|
||||
pub events_default: Option<i32>,
|
||||
pub state_default: Option<i32>,
|
||||
pub ban: Option<i32>,
|
||||
pub kick: Option<i32>,
|
||||
pub redact: Option<i32>,
|
||||
pub invite: Option<i32>,
|
||||
pub notifications: Option<NotificationPowerLevels>,
|
||||
pub users: HashMap<String, i32>,
|
||||
pub events: HashMap<String, i32>,
|
||||
}
|
||||
|
||||
impl From<PowerLevels> for RoomPowerLevelsEventContent {
|
||||
fn from(value: PowerLevels) -> Self {
|
||||
let mut power_levels = RoomPowerLevelsEventContent::new();
|
||||
|
||||
if let Some(users_default) = value.users_default {
|
||||
power_levels.users_default = users_default.into();
|
||||
}
|
||||
if let Some(state_default) = value.state_default {
|
||||
power_levels.state_default = state_default.into();
|
||||
}
|
||||
if let Some(events_default) = value.events_default {
|
||||
power_levels.events_default = events_default.into();
|
||||
}
|
||||
if let Some(ban) = value.ban {
|
||||
power_levels.ban = ban.into();
|
||||
}
|
||||
if let Some(kick) = value.kick {
|
||||
power_levels.kick = kick.into();
|
||||
}
|
||||
if let Some(redact) = value.redact {
|
||||
power_levels.redact = redact.into();
|
||||
}
|
||||
if let Some(invite) = value.invite {
|
||||
power_levels.invite = invite.into();
|
||||
}
|
||||
if let Some(notifications) = value.notifications {
|
||||
power_levels.notifications = notifications.into()
|
||||
}
|
||||
power_levels.users = value
|
||||
.users
|
||||
.iter()
|
||||
.filter_map(|(user_id, power_level)| match UserId::parse(user_id) {
|
||||
Ok(id) => Some((id, (*power_level).into())),
|
||||
Err(e) => {
|
||||
error!(user_id, "Skipping invalid user ID, error: {e}");
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
power_levels.events = value
|
||||
.events
|
||||
.iter()
|
||||
.map(|(event_type, power_level)| {
|
||||
let event_type: ruma::events::TimelineEventType = event_type.as_str().into();
|
||||
(event_type, (*power_level).into())
|
||||
})
|
||||
.collect();
|
||||
|
||||
power_levels
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct CreateRoomParameters {
|
||||
pub name: Option<String>,
|
||||
@@ -796,6 +903,8 @@ pub struct CreateRoomParameters {
|
||||
pub invite: Option<Vec<String>>,
|
||||
#[uniffi(default = None)]
|
||||
pub avatar: Option<String>,
|
||||
#[uniffi(default = None)]
|
||||
pub power_level_content_override: Option<PowerLevels>,
|
||||
}
|
||||
|
||||
impl From<CreateRoomParameters> for create_room::v3::Request {
|
||||
@@ -833,9 +942,19 @@ impl From<CreateRoomParameters> for create_room::v3::Request {
|
||||
content.url = Some(url.into());
|
||||
initial_state.push(InitialStateEvent::new(content).to_raw_any());
|
||||
}
|
||||
|
||||
request.initial_state = initial_state;
|
||||
|
||||
if let Some(power_levels) = value.power_level_content_override {
|
||||
match Raw::new(&power_levels.into()) {
|
||||
Ok(power_levels) => {
|
||||
request.power_level_content_override = Some(power_levels);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to serialize power levels, error: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
request
|
||||
}
|
||||
}
|
||||
@@ -1101,13 +1220,45 @@ fn gen_transaction_id() -> String {
|
||||
/// is dropped, the file will be removed from the disk.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct MediaFileHandle {
|
||||
inner: SdkMediaFileHandle,
|
||||
inner: RwLock<Option<SdkMediaFileHandle>>,
|
||||
}
|
||||
|
||||
impl MediaFileHandle {
|
||||
fn new(handle: SdkMediaFileHandle) -> Self {
|
||||
Self { inner: RwLock::new(Some(handle)) }
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl MediaFileHandle {
|
||||
/// Get the media file's path.
|
||||
pub fn path(&self) -> String {
|
||||
self.inner.path().to_str().unwrap().to_owned()
|
||||
pub fn path(&self) -> Result<String, ClientError> {
|
||||
Ok(self
|
||||
.inner
|
||||
.read()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.context("MediaFileHandle must not be used after calling persist")?
|
||||
.path()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_owned())
|
||||
}
|
||||
|
||||
pub fn persist(&self, path: String) -> Result<bool, ClientError> {
|
||||
let mut guard = self.inner.write().unwrap();
|
||||
Ok(
|
||||
match guard
|
||||
.take()
|
||||
.context("MediaFileHandle was already persisted")?
|
||||
.persist(path.as_ref())
|
||||
{
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
*guard = Some(e.file);
|
||||
false
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::{fs, path::PathBuf, sync::Arc};
|
||||
|
||||
use matrix_sdk::{
|
||||
encryption::{BackupDownloadStrategy, EncryptionSettings},
|
||||
ruma::{
|
||||
api::{error::UnknownVersionError, MatrixVersion},
|
||||
ServerName, UserId,
|
||||
@@ -249,6 +250,13 @@ impl ClientBuilder {
|
||||
|
||||
impl Default for ClientBuilder {
|
||||
fn default() -> Self {
|
||||
let encryption_settings = EncryptionSettings {
|
||||
auto_enable_cross_signing: true,
|
||||
auto_enable_backups: true,
|
||||
backup_download_strategy: BackupDownloadStrategy::AfterDecryptionFailure,
|
||||
};
|
||||
let inner = MatrixClient::builder().with_encryption_settings(encryption_settings);
|
||||
|
||||
Self {
|
||||
base_path: None,
|
||||
username: None,
|
||||
@@ -261,7 +269,7 @@ impl Default for ClientBuilder {
|
||||
proxy: None,
|
||||
disable_ssl_verification: false,
|
||||
disable_automatic_token_refresh: false,
|
||||
inner: MatrixClient::builder(),
|
||||
inner,
|
||||
cross_process_refresh_lock_id: None,
|
||||
session_delegate: None,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::encryption::{backups, recovery};
|
||||
use thiserror::Error;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::{error::ClientError, task_handle::TaskHandle};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Encryption {
|
||||
inner: matrix_sdk::encryption::Encryption,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::encryption::Encryption> for Encryption {
|
||||
fn from(value: matrix_sdk::encryption::Encryption) -> Self {
|
||||
Self { inner: value }
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait BackupStateListener: Sync + Send {
|
||||
fn on_update(&self, status: BackupState);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait BackupSteadyStateListener: Sync + Send {
|
||||
fn on_update(&self, status: BackupUploadState);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait RecoveryStateListener: Sync + Send {
|
||||
fn on_update(&self, status: RecoveryState);
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum BackupUploadState {
|
||||
Waiting,
|
||||
Uploading { backed_up_count: u32, total_count: u32 },
|
||||
Error,
|
||||
Done,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum SteadyStateError {
|
||||
#[error("The backup got disabled while waiting for the room keys to be uploaded.")]
|
||||
BackupDisabled,
|
||||
#[error("There was a connection error.")]
|
||||
Connection,
|
||||
#[error("We couldn't read status updates from the upload task quickly enough.")]
|
||||
Lagged,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error, uniffi::Error)]
|
||||
pub enum RecoveryError {
|
||||
/// A backup already exists on the homeserver, the recovery subsystem does
|
||||
/// not allow backups to be overwritten, disable recovery first.
|
||||
#[error(
|
||||
"A backup already exists on the homeserver and the method does not allow to overwrite it"
|
||||
)]
|
||||
BackupExistsOnServer,
|
||||
|
||||
/// A typical SDK error.
|
||||
#[error(transparent)]
|
||||
Client { source: crate::ClientError },
|
||||
|
||||
/// Error in the secret storage subsystem.
|
||||
#[error("Error in the secret-storage subsystem: {error_message}")]
|
||||
SecretStorage { error_message: String },
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::encryption::recovery::RecoveryError> for RecoveryError {
|
||||
fn from(value: matrix_sdk::encryption::recovery::RecoveryError) -> Self {
|
||||
match value {
|
||||
recovery::RecoveryError::BackupExistsOnServer => Self::BackupExistsOnServer,
|
||||
recovery::RecoveryError::Sdk(e) => Self::Client { source: ClientError::from(e) },
|
||||
recovery::RecoveryError::SecretStorage(e) => {
|
||||
Self::SecretStorage { error_message: e.to_string() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<A, E = RecoveryError> = std::result::Result<A, E>;
|
||||
|
||||
impl From<matrix_sdk::encryption::backups::futures::SteadyStateError> for SteadyStateError {
|
||||
fn from(value: matrix_sdk::encryption::backups::futures::SteadyStateError) -> Self {
|
||||
match value {
|
||||
backups::futures::SteadyStateError::BackupDisabled => Self::BackupDisabled,
|
||||
backups::futures::SteadyStateError::Connection => Self::Connection,
|
||||
backups::futures::SteadyStateError::Lagged => Self::Lagged,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum BackupState {
|
||||
Unknown,
|
||||
Creating,
|
||||
Enabling,
|
||||
Resuming,
|
||||
Enabled,
|
||||
Downloading,
|
||||
Disabling,
|
||||
}
|
||||
|
||||
impl From<backups::BackupState> for BackupState {
|
||||
fn from(value: backups::BackupState) -> Self {
|
||||
match value {
|
||||
backups::BackupState::Unknown => Self::Unknown,
|
||||
backups::BackupState::Creating => Self::Creating,
|
||||
backups::BackupState::Enabling => Self::Enabling,
|
||||
backups::BackupState::Resuming => Self::Resuming,
|
||||
backups::BackupState::Enabled => Self::Enabled,
|
||||
backups::BackupState::Downloading => Self::Downloading,
|
||||
backups::BackupState::Disabling => Self::Disabling,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<backups::UploadState> for BackupUploadState {
|
||||
fn from(value: backups::UploadState) -> Self {
|
||||
match value {
|
||||
backups::UploadState::Idle => Self::Waiting,
|
||||
backups::UploadState::Uploading(count) => Self::Uploading {
|
||||
backed_up_count: count.backed_up.try_into().unwrap_or(u32::MAX),
|
||||
total_count: count.total.try_into().unwrap_or(u32::MAX),
|
||||
},
|
||||
backups::UploadState::Error => Self::Error,
|
||||
backups::UploadState::Done => Self::Done,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RecoveryState {
|
||||
Unknown,
|
||||
Enabled,
|
||||
Disabled,
|
||||
Incomplete,
|
||||
}
|
||||
|
||||
impl From<recovery::RecoveryState> for RecoveryState {
|
||||
fn from(value: recovery::RecoveryState) -> Self {
|
||||
match value {
|
||||
recovery::RecoveryState::Unknown => Self::Unknown,
|
||||
recovery::RecoveryState::Enabled => Self::Enabled,
|
||||
recovery::RecoveryState::Disabled => Self::Disabled,
|
||||
recovery::RecoveryState::Incomplete => Self::Incomplete,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait EnableRecoveryProgressListener: Sync + Send {
|
||||
fn on_update(&self, status: EnableRecoveryProgress);
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum EnableRecoveryProgress {
|
||||
Starting,
|
||||
CreatingBackup,
|
||||
CreatingRecoveryKey,
|
||||
BackingUp { backed_up_count: u32, total_count: u32 },
|
||||
RoomKeyUploadError,
|
||||
Done { recovery_key: String },
|
||||
}
|
||||
|
||||
impl From<recovery::EnableProgress> for EnableRecoveryProgress {
|
||||
fn from(value: recovery::EnableProgress) -> Self {
|
||||
match &value {
|
||||
recovery::EnableProgress::Starting => Self::Starting,
|
||||
recovery::EnableProgress::CreatingBackup => Self::CreatingBackup,
|
||||
recovery::EnableProgress::CreatingRecoveryKey => Self::CreatingRecoveryKey,
|
||||
recovery::EnableProgress::BackingUp(counts) => Self::BackingUp {
|
||||
backed_up_count: counts.backed_up.try_into().unwrap_or(u32::MAX),
|
||||
total_count: counts.backed_up.try_into().unwrap_or(u32::MAX),
|
||||
},
|
||||
recovery::EnableProgress::RoomKeyUploadError => Self::RoomKeyUploadError,
|
||||
recovery::EnableProgress::Done { recovery_key } => {
|
||||
Self::Done { recovery_key: recovery_key.to_owned() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl Encryption {
|
||||
pub fn backup_state_listener(&self, listener: Box<dyn BackupStateListener>) -> Arc<TaskHandle> {
|
||||
let mut stream = self.inner.backups().state_stream();
|
||||
|
||||
let stream_task = TaskHandle::new(RUNTIME.spawn(async move {
|
||||
while let Some(state) = stream.next().await {
|
||||
let Ok(state) = state else { continue };
|
||||
listener.on_update(state.into());
|
||||
}
|
||||
}));
|
||||
|
||||
stream_task.into()
|
||||
}
|
||||
|
||||
pub fn backup_state(&self) -> BackupState {
|
||||
self.inner.backups().state().into()
|
||||
}
|
||||
|
||||
/// Does a backup exist on the server?
|
||||
///
|
||||
/// Because the homeserver doesn't notify us about changes to the backup
|
||||
/// version, the [`BackupState`] and its listener are a bit crippled.
|
||||
/// The `BackupState::Unknown` state might mean there is no backup at all or
|
||||
/// a backup exists but we don't have access to it.
|
||||
///
|
||||
/// Therefore it is necessary to poll the server for an answer every time
|
||||
/// you want to differentiate between those two states.
|
||||
pub async fn backup_exists_on_server(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.backups().exists_on_server().await?)
|
||||
}
|
||||
|
||||
pub fn recovery_state(&self) -> RecoveryState {
|
||||
self.inner.recovery().state().into()
|
||||
}
|
||||
|
||||
pub fn recovery_state_listener(
|
||||
&self,
|
||||
listener: Box<dyn RecoveryStateListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let mut stream = self.inner.recovery().state_stream();
|
||||
|
||||
let stream_task = TaskHandle::new(RUNTIME.spawn(async move {
|
||||
while let Some(state) = stream.next().await {
|
||||
listener.on_update(state.into());
|
||||
}
|
||||
}));
|
||||
|
||||
stream_task.into()
|
||||
}
|
||||
|
||||
pub async fn enable_backups(&self) -> Result<()> {
|
||||
Ok(self.inner.recovery().enable_backup().await?)
|
||||
}
|
||||
|
||||
pub async fn is_last_device(&self) -> Result<bool> {
|
||||
Ok(self.inner.recovery().are_we_the_last_man_standing().await?)
|
||||
}
|
||||
|
||||
pub async fn wait_for_backup_upload_steady_state(
|
||||
&self,
|
||||
progress_listener: Option<Box<dyn BackupSteadyStateListener>>,
|
||||
) -> Result<(), SteadyStateError> {
|
||||
let backups = self.inner.backups();
|
||||
let wait_for_steady_state = backups.wait_for_steady_state();
|
||||
|
||||
let task = if let Some(listener) = progress_listener {
|
||||
let mut progress_stream = wait_for_steady_state.subscribe_to_progress();
|
||||
|
||||
Some(RUNTIME.spawn(async move {
|
||||
while let Some(progress) = progress_stream.next().await {
|
||||
let Ok(progress) = progress else { continue };
|
||||
listener.on_update(progress.into());
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let result = wait_for_steady_state.await;
|
||||
|
||||
if let Some(task) = task {
|
||||
task.abort();
|
||||
}
|
||||
|
||||
Ok(result?)
|
||||
}
|
||||
|
||||
pub async fn enable_recovery(
|
||||
&self,
|
||||
wait_for_backups_to_upload: bool,
|
||||
progress_listener: Box<dyn EnableRecoveryProgressListener>,
|
||||
) -> Result<String> {
|
||||
let recovery = self.inner.recovery();
|
||||
|
||||
let enable = if wait_for_backups_to_upload {
|
||||
recovery.enable().wait_for_backups_to_upload()
|
||||
} else {
|
||||
recovery.enable()
|
||||
};
|
||||
|
||||
let mut progress_stream = enable.subscribe_to_progress();
|
||||
|
||||
let task = RUNTIME.spawn(async move {
|
||||
while let Some(progress) = progress_stream.next().await {
|
||||
let Ok(progress) = progress else { continue };
|
||||
progress_listener.on_update(progress.into());
|
||||
}
|
||||
});
|
||||
|
||||
let ret = enable.await?;
|
||||
|
||||
task.abort();
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
pub async fn disable_recovery(&self) -> Result<()> {
|
||||
Ok(self.inner.recovery().disable().await?)
|
||||
}
|
||||
|
||||
pub async fn reset_recovery_key(&self) -> Result<String> {
|
||||
Ok(self.inner.recovery().reset_key().await?)
|
||||
}
|
||||
|
||||
pub async fn recover_and_reset(&self, mut old_recovery_key: String) -> Result<String> {
|
||||
let result = self.inner.recovery().recover_and_reset(&old_recovery_key).await;
|
||||
|
||||
old_recovery_key.zeroize();
|
||||
|
||||
Ok(result?)
|
||||
}
|
||||
|
||||
pub async fn recover(&self, mut recovery_key: String) -> Result<()> {
|
||||
let result = self.inner.recovery().recover(&recovery_key).await;
|
||||
|
||||
recovery_key.zeroize();
|
||||
|
||||
Ok(result?)
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use ruma::events::{
|
||||
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
|
||||
};
|
||||
|
||||
use crate::{room_member::MembershipState, timeline::MessageType, ClientError};
|
||||
use crate::{room_member::MembershipState, ruma::MessageType, ClientError};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct TimelineEvent(pub(crate) AnySyncTimelineEvent);
|
||||
|
||||
@@ -24,6 +24,7 @@ mod authentication_service;
|
||||
mod chunk_iterator;
|
||||
mod client;
|
||||
mod client_builder;
|
||||
mod encryption;
|
||||
mod error;
|
||||
mod event;
|
||||
mod helpers;
|
||||
@@ -34,23 +35,24 @@ mod room;
|
||||
mod room_info;
|
||||
mod room_list;
|
||||
mod room_member;
|
||||
mod ruma;
|
||||
mod session_verification;
|
||||
mod sync_service;
|
||||
mod task_handle;
|
||||
mod timeline;
|
||||
mod tracing;
|
||||
mod utils;
|
||||
mod widget;
|
||||
|
||||
use async_compat::TOKIO1 as RUNTIME;
|
||||
use matrix_sdk::ruma::events::room::{
|
||||
message::RoomMessageEventContentWithoutRelation, MediaSource,
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{BackPaginationStatus, EventItemOrigin};
|
||||
|
||||
use self::{
|
||||
error::ClientError,
|
||||
ruma::{MediaSourceExt, Mentions, RoomMessageEventContentWithoutRelationExt},
|
||||
task_handle::TaskHandle,
|
||||
timeline::{MediaSourceExt, Mentions, RoomMessageEventContentWithoutRelationExt},
|
||||
};
|
||||
|
||||
uniffi::include_scaffolding!("api");
|
||||
|
||||
@@ -7,7 +7,9 @@ use matrix_sdk_ui::notification_client::{
|
||||
};
|
||||
use ruma::{EventId, RoomId};
|
||||
|
||||
use crate::{error::ClientError, event::TimelineEvent, helpers::unwrap_or_clone_arc, RUNTIME};
|
||||
use crate::{
|
||||
client::Client, error::ClientError, event::TimelineEvent, helpers::unwrap_or_clone_arc, RUNTIME,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum NotificationEvent {
|
||||
@@ -42,6 +44,7 @@ pub struct NotificationItem {
|
||||
/// Can be `None` if we couldn't determine this, because we lacked
|
||||
/// information to create a push context.
|
||||
pub is_noisy: Option<bool>,
|
||||
pub has_mention: Option<bool>,
|
||||
}
|
||||
|
||||
impl NotificationItem {
|
||||
@@ -70,23 +73,26 @@ impl NotificationItem {
|
||||
is_direct: item.is_direct_message_room,
|
||||
},
|
||||
is_noisy: item.is_noisy,
|
||||
has_mention: item.has_mention,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct NotificationClientBuilder {
|
||||
client: Arc<Client>,
|
||||
builder: MatrixNotificationClientBuilder,
|
||||
}
|
||||
|
||||
impl NotificationClientBuilder {
|
||||
pub(crate) fn new(
|
||||
client: matrix_sdk::Client,
|
||||
client: Arc<Client>,
|
||||
process_setup: NotificationProcessSetup,
|
||||
) -> Result<Arc<Self>, ClientError> {
|
||||
let builder = RUNTIME
|
||||
.block_on(async { MatrixNotificationClient::builder(client, process_setup).await })?;
|
||||
Ok(Arc::new(Self { builder }))
|
||||
let builder = RUNTIME.block_on(async {
|
||||
MatrixNotificationClient::builder((*client.inner).clone(), process_setup).await
|
||||
})?;
|
||||
Ok(Arc::new(Self { builder, client }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,18 +103,25 @@ impl NotificationClientBuilder {
|
||||
pub fn filter_by_push_rules(self: Arc<Self>) -> Arc<Self> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
let builder = this.builder.filter_by_push_rules();
|
||||
Arc::new(Self { builder })
|
||||
Arc::new(Self { builder, client: this.client })
|
||||
}
|
||||
|
||||
pub fn finish(self: Arc<Self>) -> Arc<NotificationClient> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
Arc::new(NotificationClient { inner: this.builder.build() })
|
||||
Arc::new(NotificationClient { inner: this.builder.build(), _client: this.client })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct NotificationClient {
|
||||
inner: MatrixNotificationClient,
|
||||
|
||||
/// A reference to the FFI client.
|
||||
///
|
||||
/// Note: we do this to make it so that the FFI `NotificationClient` keeps
|
||||
/// the FFI `Client` and thus the SDK `Client` alive. Otherwise, we
|
||||
/// would need to repeat the hack done in the FFI `Client::drop` method.
|
||||
_client: Arc<Client>,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
|
||||
@@ -267,10 +267,7 @@ impl NotificationSettings {
|
||||
pub async fn is_room_mention_enabled(&self) -> Result<bool, NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
let enabled = notification_settings
|
||||
.is_push_rule_enabled(
|
||||
RuleKind::Override,
|
||||
PredefinedOverrideRuleId::IsRoomMention.as_str(),
|
||||
)
|
||||
.is_push_rule_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention)
|
||||
.await?;
|
||||
Ok(enabled)
|
||||
}
|
||||
@@ -284,7 +281,7 @@ impl NotificationSettings {
|
||||
notification_settings
|
||||
.set_push_rule_enabled(
|
||||
RuleKind::Override,
|
||||
PredefinedOverrideRuleId::IsRoomMention.as_str(),
|
||||
PredefinedOverrideRuleId::IsRoomMention,
|
||||
enabled,
|
||||
)
|
||||
.await?;
|
||||
@@ -295,14 +292,33 @@ impl NotificationSettings {
|
||||
pub async fn is_user_mention_enabled(&self) -> Result<bool, NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
let enabled = notification_settings
|
||||
.is_push_rule_enabled(
|
||||
RuleKind::Override,
|
||||
PredefinedOverrideRuleId::IsUserMention.as_str(),
|
||||
)
|
||||
.is_push_rule_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention)
|
||||
.await?;
|
||||
Ok(enabled)
|
||||
}
|
||||
|
||||
/// Check if [MSC 4028 push rule][rule] is enabled.
|
||||
///
|
||||
/// [rule]: https://github.com/matrix-org/matrix-spec-proposals/blob/giomfo/push_encrypted_events/proposals/4028-push-all-encrypted-events-except-for-muted-rooms.md
|
||||
pub async fn can_homeserver_push_encrypted_event_to_device(&self) -> bool {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
// Check stable identifier
|
||||
if let Ok(enabled) = notification_settings
|
||||
.is_push_rule_enabled(RuleKind::Override, ".m.rule.encrypted_event")
|
||||
.await
|
||||
{
|
||||
enabled
|
||||
// Check unstable identifier
|
||||
} else if let Ok(enabled) = notification_settings
|
||||
.is_push_rule_enabled(RuleKind::Override, ".org.matrix.msc4028.encrypted_event")
|
||||
.await
|
||||
{
|
||||
enabled
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Set whether user mentions are enabled.
|
||||
pub async fn set_user_mention_enabled(
|
||||
&self,
|
||||
@@ -312,7 +328,7 @@ impl NotificationSettings {
|
||||
notification_settings
|
||||
.set_push_rule_enabled(
|
||||
RuleKind::Override,
|
||||
PredefinedOverrideRuleId::IsUserMention.as_str(),
|
||||
PredefinedOverrideRuleId::IsUserMention,
|
||||
enabled,
|
||||
)
|
||||
.await?;
|
||||
@@ -323,18 +339,42 @@ impl NotificationSettings {
|
||||
pub async fn is_call_enabled(&self) -> Result<bool, NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
let enabled = notification_settings
|
||||
.is_push_rule_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::Call.as_str())
|
||||
.is_push_rule_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::Call)
|
||||
.await?;
|
||||
Ok(enabled)
|
||||
}
|
||||
|
||||
/// Set whether the `.m.rule.call` push rule is enabled
|
||||
pub async fn set_call_enabled(&self, enabled: bool) -> Result<(), NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
notification_settings
|
||||
.set_push_rule_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::Call, enabled)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get whether the `.m.rule.invite_for_me` push rule is enabled
|
||||
pub async fn is_invite_for_me_enabled(&self) -> Result<bool, NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
let enabled = notification_settings
|
||||
.is_push_rule_enabled(
|
||||
RuleKind::Override,
|
||||
PredefinedOverrideRuleId::InviteForMe.as_str(),
|
||||
)
|
||||
.await?;
|
||||
Ok(enabled)
|
||||
}
|
||||
|
||||
/// Set whether the `.m.rule.invite_for_me` push rule is enabled
|
||||
pub async fn set_invite_for_me_enabled(
|
||||
&self,
|
||||
enabled: bool,
|
||||
) -> Result<(), NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
notification_settings
|
||||
.set_push_rule_enabled(
|
||||
RuleKind::Underride,
|
||||
PredefinedUnderrideRuleId::Call.as_str(),
|
||||
RuleKind::Override,
|
||||
PredefinedOverrideRuleId::InviteForMe.as_str(),
|
||||
enabled,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
use std::{collections::HashMap, fmt::Debug};
|
||||
use std::{collections::HashMap, fmt::Debug, pin::Pin};
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||
use futures_core::future::BoxFuture;
|
||||
use opentelemetry::{
|
||||
sdk::{runtime::RuntimeChannel, trace::Tracer, util::tokio_interval_stream, Resource},
|
||||
KeyValue,
|
||||
};
|
||||
use opentelemetry::KeyValue;
|
||||
use opentelemetry_otlp::{Protocol, WithExportConfig};
|
||||
use opentelemetry_sdk::{runtime::RuntimeChannel, trace::Tracer, Resource};
|
||||
use tokio::runtime::Handle;
|
||||
use tracing_core::Subscriber;
|
||||
use tracing_subscriber::{
|
||||
@@ -20,17 +18,17 @@ use tracing_subscriber::{
|
||||
use crate::RUNTIME;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct TracingRuntime {
|
||||
struct TokioRuntime {
|
||||
runtime: Handle,
|
||||
}
|
||||
|
||||
impl opentelemetry::runtime::Runtime for TracingRuntime {
|
||||
impl opentelemetry_sdk::runtime::Runtime for TokioRuntime {
|
||||
type Interval = tokio_stream::wrappers::IntervalStream;
|
||||
type Delay = ::std::pin::Pin<Box<tokio::time::Sleep>>;
|
||||
type Delay = Pin<Box<tokio::time::Sleep>>;
|
||||
|
||||
fn interval(&self, duration: std::time::Duration) -> Self::Interval {
|
||||
fn interval(&self, period: std::time::Duration) -> Self::Interval {
|
||||
let _guard = self.runtime.enter();
|
||||
tokio_interval_stream(duration)
|
||||
tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(period))
|
||||
}
|
||||
|
||||
fn spawn(&self, future: BoxFuture<'static, ()>) {
|
||||
@@ -44,11 +42,14 @@ impl opentelemetry::runtime::Runtime for TracingRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Debug + Send> RuntimeChannel<T> for TracingRuntime {
|
||||
type Receiver = tokio_stream::wrappers::ReceiverStream<T>;
|
||||
type Sender = tokio::sync::mpsc::Sender<T>;
|
||||
impl RuntimeChannel for TokioRuntime {
|
||||
type Receiver<T: Debug + Send> = tokio_stream::wrappers::ReceiverStream<T>;
|
||||
type Sender<T: Debug + Send> = tokio::sync::mpsc::Sender<T>;
|
||||
|
||||
fn batch_message_channel(&self, capacity: usize) -> (Self::Sender, Self::Receiver) {
|
||||
fn batch_message_channel<T: Debug + Send>(
|
||||
&self,
|
||||
capacity: usize,
|
||||
) -> (Self::Sender<T>, Self::Receiver<T>) {
|
||||
let (sender, receiver) = tokio::sync::mpsc::channel(capacity);
|
||||
(sender, tokio_stream::wrappers::ReceiverStream::new(receiver))
|
||||
}
|
||||
@@ -73,14 +74,14 @@ pub fn create_otlp_tracer(
|
||||
.with_endpoint(otlp_endpoint)
|
||||
.with_headers(headers);
|
||||
|
||||
let tracer_runtime = TracingRuntime { runtime: runtime.to_owned() };
|
||||
let tracer_runtime = TokioRuntime { runtime: runtime.to_owned() };
|
||||
|
||||
let _guard = runtime.enter();
|
||||
let tracer = opentelemetry_otlp::new_pipeline()
|
||||
.tracing()
|
||||
.with_exporter(exporter)
|
||||
.with_trace_config(
|
||||
opentelemetry::sdk::trace::config()
|
||||
opentelemetry_sdk::trace::config()
|
||||
.with_resource(Resource::new(vec![KeyValue::new("service.name", client_name)])),
|
||||
)
|
||||
.install_batch(tracer_runtime)?;
|
||||
|
||||
@@ -1,66 +1,27 @@
|
||||
use std::{convert::TryFrom, fs, sync::Arc};
|
||||
use std::{convert::TryFrom, sync::Arc};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use matrix_sdk::{
|
||||
attachment::{
|
||||
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
|
||||
BaseThumbnailInfo, BaseVideoInfo, Thumbnail,
|
||||
},
|
||||
room::Room as SdkRoom,
|
||||
ruma::{
|
||||
api::client::{receipt::create_receipt::v3::ReceiptType, room::report_content},
|
||||
events::{
|
||||
location::{AssetType as RumaAssetType, LocationContent, ZoomLevel},
|
||||
poll::unstable_start::{
|
||||
UnstablePollAnswer, UnstablePollAnswers, UnstablePollStartContentBlock,
|
||||
},
|
||||
receipt::ReceiptThread,
|
||||
relation::Annotation,
|
||||
room::{
|
||||
avatar::ImageInfo as RumaAvatarImageInfo,
|
||||
message::{
|
||||
ForwardThread, LocationMessageEventContent, MessageType,
|
||||
RoomMessageEventContentWithoutRelation,
|
||||
},
|
||||
},
|
||||
AnyMessageLikeEventContent,
|
||||
},
|
||||
EventId, UserId,
|
||||
},
|
||||
RoomMemberships, RoomState,
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{BackPaginationStatus, RoomExt, Timeline};
|
||||
use anyhow::{Context, Result};
|
||||
use matrix_sdk::{room::Room as SdkRoom, RoomMemberships, RoomState};
|
||||
use matrix_sdk_ui::timeline::RoomExt;
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
api::client::room::report_content,
|
||||
assign,
|
||||
events::{
|
||||
poll::{
|
||||
unstable_end::UnstablePollEndEventContent,
|
||||
unstable_response::UnstablePollResponseEventContent,
|
||||
unstable_start::NewUnstablePollStartEventContent,
|
||||
},
|
||||
room::MediaSource,
|
||||
},
|
||||
events::room::{avatar::ImageInfo as RumaAvatarImageInfo, MediaSource},
|
||||
EventId, UserId,
|
||||
};
|
||||
use tokio::{
|
||||
sync::{Mutex, RwLock},
|
||||
task::{AbortHandle, JoinHandle},
|
||||
};
|
||||
use tracing::{error, info};
|
||||
use uuid::Uuid;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::error;
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::{
|
||||
chunk_iterator::ChunkIterator,
|
||||
client::ProgressWatcher,
|
||||
error::{ClientError, MediaInfoError, RoomError},
|
||||
room_info::RoomInfo,
|
||||
room_member::{MessageLikeEventType, RoomMember, StateEventType},
|
||||
timeline::{
|
||||
u64_to_uint, AudioInfo, EventTimelineItem, FileInfo, ImageInfo, PollKind, ThumbnailInfo,
|
||||
TimelineDiff, TimelineItem, TimelineListener, VideoInfo,
|
||||
},
|
||||
ruma::ImageInfo,
|
||||
timeline::{EventTimelineItem, Timeline},
|
||||
utils::u64_to_uint,
|
||||
TaskHandle,
|
||||
};
|
||||
|
||||
@@ -178,41 +139,19 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the timeline.
|
||||
///
|
||||
/// Timeline items cached in memory as well as timeline listeners are
|
||||
/// dropped.
|
||||
pub fn remove_timeline(&self) {
|
||||
RUNTIME.block_on(async {
|
||||
*self.timeline.write().await = None;
|
||||
});
|
||||
pub async fn timeline(&self) -> Arc<Timeline> {
|
||||
let mut write_guard = self.timeline.write().await;
|
||||
if let Some(timeline) = &*write_guard {
|
||||
timeline.clone()
|
||||
} else {
|
||||
let timeline = Timeline::new(self.inner.timeline().await);
|
||||
*write_guard = Some(timeline.clone());
|
||||
timeline
|
||||
}
|
||||
}
|
||||
|
||||
pub fn retry_decryption(&self, session_ids: Vec<String>) {
|
||||
let timeline = match &*RUNTIME.block_on(self.timeline.read()) {
|
||||
Some(t) => Arc::clone(t),
|
||||
None => {
|
||||
error!("Timeline not set up, can't retry decryption");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
RUNTIME.spawn(async move {
|
||||
timeline.retry_decryption(&session_ids).await;
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn fetch_members(&self) -> Result<(), ClientError> {
|
||||
let timeline = self
|
||||
.timeline
|
||||
.read()
|
||||
.await
|
||||
.clone()
|
||||
.context("Timeline not set up, can't fetch members")?;
|
||||
|
||||
timeline.fetch_members().await;
|
||||
|
||||
Ok(())
|
||||
pub async fn poll_history(&self) -> Arc<Timeline> {
|
||||
Timeline::new(self.inner.poll_history().await)
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> Result<String, ClientError> {
|
||||
@@ -258,37 +197,6 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn add_timeline_listener(
|
||||
&self,
|
||||
listener: Box<dyn TimelineListener>,
|
||||
) -> RoomTimelineListenerResult {
|
||||
let timeline = {
|
||||
let mut write_guard = self.timeline.write().await;
|
||||
if let Some(timeline) = &*write_guard {
|
||||
timeline.clone()
|
||||
} else {
|
||||
let timeline = Arc::new(self.inner.timeline().await);
|
||||
*write_guard = Some(timeline.clone());
|
||||
timeline
|
||||
}
|
||||
};
|
||||
|
||||
let (timeline_items, timeline_stream) = timeline.subscribe_batched().await;
|
||||
let timeline_stream = TaskHandle::new(RUNTIME.spawn(async move {
|
||||
pin_mut!(timeline_stream);
|
||||
|
||||
while let Some(diffs) = timeline_stream.next().await {
|
||||
listener
|
||||
.on_update(diffs.into_iter().map(|d| Arc::new(TimelineDiff::new(d))).collect());
|
||||
}
|
||||
}));
|
||||
|
||||
RoomTimelineListenerResult {
|
||||
items: timeline_items.into_iter().map(TimelineItem::from_arc).collect(),
|
||||
items_stream: Arc::new(timeline_stream),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn room_info(&self) -> Result<RoomInfo, ClientError> {
|
||||
let avatar_url = self.inner.avatar_url();
|
||||
|
||||
@@ -297,7 +205,7 @@ impl Room {
|
||||
// First off, let's see if a `Timeline` exists…
|
||||
if let Some(timeline) = self.timeline.read().await.clone() {
|
||||
// If it contains a `latest_event`…
|
||||
if let Some(timeline_last_event) = timeline.latest_event().await {
|
||||
if let Some(timeline_last_event) = timeline.inner.latest_event().await {
|
||||
// If it's a local echo…
|
||||
if timeline_last_event.is_local_echo() {
|
||||
return Ok(RoomInfo::new(
|
||||
@@ -342,200 +250,6 @@ impl Room {
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn subscribe_to_back_pagination_status(
|
||||
&self,
|
||||
listener: Box<dyn BackPaginationStatusListener>,
|
||||
) -> Result<Arc<TaskHandle>, ClientError> {
|
||||
let mut subscriber = match &*RUNTIME.block_on(self.timeline.read()) {
|
||||
Some(t) => t.back_pagination_status(),
|
||||
None => {
|
||||
return Err(anyhow!(
|
||||
"Timeline not set up, can't subscribe to back-pagination status"
|
||||
)
|
||||
.into());
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
// Send the current state even if it hasn't changed right away.
|
||||
listener.on_update(subscriber.next_now());
|
||||
|
||||
while let Some(status) = subscriber.next().await {
|
||||
listener.on_update(status);
|
||||
}
|
||||
}))))
|
||||
}
|
||||
|
||||
/// Loads older messages into the timeline.
|
||||
///
|
||||
/// Raises an exception if there are no timeline listeners.
|
||||
pub fn paginate_backwards(&self, opts: PaginationOptions) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let timeline: Arc<_> = self
|
||||
.timeline
|
||||
.read()
|
||||
.await
|
||||
.clone()
|
||||
.context("No timeline listeners registered, can't paginate")?;
|
||||
Ok(timeline.paginate_backwards(opts.into()).await?)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send_read_receipt(&self, event_id: String) -> Result<(), ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
|
||||
RUNTIME.block_on(async move {
|
||||
self.timeline
|
||||
.read()
|
||||
.await
|
||||
.clone()
|
||||
.context("Timeline not set up, can't send read receipt")?
|
||||
.send_single_receipt(ReceiptType::Read, ReceiptThread::Unthreaded, event_id)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send(&self, msg: Arc<RoomMessageEventContentWithoutRelation>) {
|
||||
let timeline = match &*RUNTIME.block_on(self.timeline.read()) {
|
||||
Some(t) => Arc::clone(t),
|
||||
None => {
|
||||
error!("Timeline not set up, can't send message");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
RUNTIME.spawn(async move {
|
||||
timeline.send((*msg).to_owned().with_relation(None).into()).await;
|
||||
});
|
||||
}
|
||||
|
||||
pub fn create_poll(
|
||||
&self,
|
||||
question: String,
|
||||
answers: Vec<String>,
|
||||
max_selections: u8,
|
||||
poll_kind: PollKind,
|
||||
) -> Result<(), ClientError> {
|
||||
let timeline = match &*RUNTIME.block_on(self.timeline.read()) {
|
||||
Some(t) => Arc::clone(t),
|
||||
None => {
|
||||
return Err(anyhow!("Timeline not set up, can't send the poll").into());
|
||||
}
|
||||
};
|
||||
|
||||
let poll_answers_vec: Vec<UnstablePollAnswer> = answers
|
||||
.iter()
|
||||
.map(|answer| UnstablePollAnswer::new(Uuid::new_v4().to_string(), answer))
|
||||
.collect();
|
||||
|
||||
let poll_answers = UnstablePollAnswers::try_from(poll_answers_vec)
|
||||
.context("Failed to create poll answers")?;
|
||||
|
||||
let mut poll_content_block =
|
||||
UnstablePollStartContentBlock::new(question.clone(), poll_answers);
|
||||
poll_content_block.kind = poll_kind.into();
|
||||
poll_content_block.max_selections = max_selections.into();
|
||||
|
||||
let fallback_text = answers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold(question, |acc, (index, answer)| format!("{acc}\n{}. {answer}", index + 1));
|
||||
|
||||
let poll_start_event_content =
|
||||
NewUnstablePollStartEventContent::plain_text(fallback_text, poll_content_block);
|
||||
let event_content =
|
||||
AnyMessageLikeEventContent::UnstablePollStart(poll_start_event_content.into());
|
||||
|
||||
RUNTIME.spawn(async move {
|
||||
timeline.send(event_content).await;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_poll_response(
|
||||
&self,
|
||||
poll_start_id: String,
|
||||
answers: Vec<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let timeline = match &*RUNTIME.block_on(self.timeline.read()) {
|
||||
Some(t) => Arc::clone(t),
|
||||
None => {
|
||||
return Err(anyhow!("Timeline not set up, can't send the poll vote").into());
|
||||
}
|
||||
};
|
||||
|
||||
let poll_start_event_id =
|
||||
EventId::parse(poll_start_id).context("Failed to parse EventId")?;
|
||||
let poll_response_event_content =
|
||||
UnstablePollResponseEventContent::new(answers, poll_start_event_id);
|
||||
let event_content =
|
||||
AnyMessageLikeEventContent::UnstablePollResponse(poll_response_event_content);
|
||||
|
||||
RUNTIME.spawn(async move {
|
||||
timeline.send(event_content).await;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn end_poll(&self, poll_start_id: String, text: String) -> Result<(), ClientError> {
|
||||
let timeline = match &*RUNTIME.block_on(self.timeline.read()) {
|
||||
Some(t) => Arc::clone(t),
|
||||
None => {
|
||||
return Err(anyhow!("Timeline not set up, can't end the poll").into());
|
||||
}
|
||||
};
|
||||
|
||||
let poll_start_event_id =
|
||||
EventId::parse(poll_start_id).context("Failed to parse EventId")?;
|
||||
let poll_end_event_content = UnstablePollEndEventContent::new(text, poll_start_event_id);
|
||||
let event_content = AnyMessageLikeEventContent::UnstablePollEnd(poll_end_event_content);
|
||||
|
||||
RUNTIME.spawn(async move {
|
||||
timeline.send(event_content).await;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_reply(
|
||||
&self,
|
||||
msg: Arc<RoomMessageEventContentWithoutRelation>,
|
||||
reply_item: Arc<EventTimelineItem>,
|
||||
) -> Result<(), ClientError> {
|
||||
let timeline = match &*RUNTIME.block_on(self.timeline.read()) {
|
||||
Some(t) => Arc::clone(t),
|
||||
None => return Err(anyhow!("Timeline not set up, can't send message").into()),
|
||||
};
|
||||
|
||||
RUNTIME.block_on(async move {
|
||||
timeline.send_reply((*msg).clone(), &reply_item.0, ForwardThread::Yes).await?;
|
||||
anyhow::Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn edit(
|
||||
&self,
|
||||
new_content: Arc<RoomMessageEventContentWithoutRelation>,
|
||||
edit_item: Arc<EventTimelineItem>,
|
||||
) -> Result<(), ClientError> {
|
||||
let timeline = match &*RUNTIME.block_on(self.timeline.read()) {
|
||||
Some(t) => Arc::clone(t),
|
||||
None => return Err(anyhow!("Timeline not set up, can't send message").into()),
|
||||
};
|
||||
|
||||
RUNTIME.block_on(async move {
|
||||
timeline.edit((*new_content).clone().with_relation(None), &edit_item.0).await?;
|
||||
anyhow::Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Redacts an event from the room.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -552,19 +266,6 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn toggle_reaction(&self, event_id: String, key: String) -> Result<(), ClientError> {
|
||||
let timeline = match &*RUNTIME.block_on(self.timeline.read()) {
|
||||
Some(t) => Arc::clone(t),
|
||||
None => return Err(anyhow!("Timeline not set up, can't send message").into()),
|
||||
};
|
||||
|
||||
RUNTIME.block_on(async move {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
timeline.toggle_reaction(&Annotation::new(event_id, key)).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn active_members_count(&self) -> u64 {
|
||||
self.inner.active_members_count()
|
||||
}
|
||||
@@ -713,262 +414,6 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn fetch_details_for_event(&self, event_id: String) -> Result<(), ClientError> {
|
||||
let timeline = RUNTIME
|
||||
.block_on(self.timeline.read())
|
||||
.as_ref()
|
||||
.context("Timeline not set up, can't fetch event details")?
|
||||
.clone();
|
||||
|
||||
RUNTIME.block_on(async move {
|
||||
let event_id = <&EventId>::try_from(event_id.as_str())?;
|
||||
timeline.fetch_details_for_event(event_id).await.context("Fetching event details")?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send_image(
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
thumbnail_url: String,
|
||||
image_info: ImageInfo,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
let mime_str =
|
||||
image_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let base_image_info = BaseImageInfo::try_from(&image_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
|
||||
let attachment_info = AttachmentInfo::Image(base_image_info);
|
||||
|
||||
let attachment_config = match image_info.thumbnail_info {
|
||||
Some(thumbnail_image_info) => {
|
||||
let thumbnail =
|
||||
self.build_thumbnail_info(thumbnail_url, thumbnail_image_info)?;
|
||||
AttachmentConfig::with_thumbnail(thumbnail).info(attachment_info)
|
||||
}
|
||||
None => AttachmentConfig::new().info(attachment_info),
|
||||
};
|
||||
|
||||
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn send_video(
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
thumbnail_url: String,
|
||||
video_info: VideoInfo,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
let mime_str =
|
||||
video_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let base_video_info: BaseVideoInfo = BaseVideoInfo::try_from(&video_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
|
||||
let attachment_info = AttachmentInfo::Video(base_video_info);
|
||||
|
||||
let attachment_config = match video_info.thumbnail_info {
|
||||
Some(thumbnail_image_info) => {
|
||||
let thumbnail =
|
||||
self.build_thumbnail_info(thumbnail_url, thumbnail_image_info)?;
|
||||
AttachmentConfig::with_thumbnail(thumbnail).info(attachment_info)
|
||||
}
|
||||
None => AttachmentConfig::new().info(attachment_info),
|
||||
};
|
||||
|
||||
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn send_audio(
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
audio_info: AudioInfo,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
let mime_str =
|
||||
audio_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
|
||||
let attachment_info = AttachmentInfo::Audio(base_audio_info);
|
||||
let attachment_config = AttachmentConfig::new().info(attachment_info);
|
||||
|
||||
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn send_voice_message(
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
audio_info: AudioInfo,
|
||||
waveform: Vec<u16>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
let mime_str =
|
||||
audio_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
|
||||
let attachment_info =
|
||||
AttachmentInfo::Voice { audio_info: base_audio_info, waveform: Some(waveform) };
|
||||
let attachment_config = AttachmentConfig::new().info(attachment_info);
|
||||
|
||||
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn send_file(
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
file_info: FileInfo,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
let mime_str =
|
||||
file_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let base_file_info: BaseFileInfo =
|
||||
BaseFileInfo::try_from(&file_info).map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
|
||||
let attachment_info = AttachmentInfo::File(base_file_info);
|
||||
let attachment_config = AttachmentConfig::new().info(attachment_info);
|
||||
|
||||
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn retry_send(&self, txn_id: String) {
|
||||
let timeline = match &*RUNTIME.block_on(self.timeline.read()) {
|
||||
Some(t) => Arc::clone(t),
|
||||
None => {
|
||||
error!("Timeline not set up, can't retry sending message");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
RUNTIME.spawn(async move {
|
||||
if let Err(e) = timeline.retry_send(txn_id.as_str().into()).await {
|
||||
error!(txn_id, "Failed to retry sending: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn send_location(
|
||||
&self,
|
||||
body: String,
|
||||
geo_uri: String,
|
||||
description: Option<String>,
|
||||
zoom_level: Option<u8>,
|
||||
asset_type: Option<AssetType>,
|
||||
) {
|
||||
let mut location_event_message_content =
|
||||
LocationMessageEventContent::new(body, geo_uri.clone());
|
||||
|
||||
if let Some(asset_type) = asset_type {
|
||||
location_event_message_content =
|
||||
location_event_message_content.with_asset_type(RumaAssetType::from(asset_type));
|
||||
}
|
||||
|
||||
let mut location_content = LocationContent::new(geo_uri);
|
||||
location_content.description = description;
|
||||
location_content.zoom_level = zoom_level.and_then(ZoomLevel::new);
|
||||
location_event_message_content.location = Some(location_content);
|
||||
|
||||
let room_message_event_content = RoomMessageEventContentWithoutRelation::new(
|
||||
MessageType::Location(location_event_message_content),
|
||||
);
|
||||
self.send(Arc::new(room_message_event_content))
|
||||
}
|
||||
|
||||
pub fn cancel_send(&self, txn_id: String) {
|
||||
let timeline = match &*RUNTIME.block_on(self.timeline.read()) {
|
||||
Some(t) => Arc::clone(t),
|
||||
None => {
|
||||
error!("Timeline not set up, can't retry sending message");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
RUNTIME.spawn(async move {
|
||||
if !timeline.cancel_send(txn_id.as_str().into()).await {
|
||||
info!(txn_id, "Failed to discard local echo: Not found");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_event_timeline_item_by_event_id(
|
||||
&self,
|
||||
event_id: String,
|
||||
) -> Result<Arc<EventTimelineItem>, ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let timeline = self
|
||||
.timeline
|
||||
.read()
|
||||
.await
|
||||
.clone()
|
||||
.context("Timeline not set up, can't get event ")?;
|
||||
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
|
||||
let item = timeline
|
||||
.item_by_event_id(&event_id)
|
||||
.await
|
||||
.context("Item with given event ID not found")?;
|
||||
|
||||
Ok(Arc::new(EventTimelineItem(item)))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_timeline_event_content_by_event_id(
|
||||
&self,
|
||||
event_id: String,
|
||||
) -> Result<Arc<RoomMessageEventContentWithoutRelation>, ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let timeline = self
|
||||
.timeline
|
||||
.read()
|
||||
.await
|
||||
.clone()
|
||||
.context("Timeline not set up, can't get event content")?;
|
||||
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
|
||||
let item = timeline
|
||||
.item_by_event_id(&event_id)
|
||||
.await
|
||||
.context("Item with given event ID not found")?;
|
||||
|
||||
let msgtype = item
|
||||
.content()
|
||||
.as_message()
|
||||
.context("Item with given event ID is not a message")?
|
||||
.msgtype()
|
||||
.to_owned();
|
||||
|
||||
Ok(Arc::new(RoomMessageEventContentWithoutRelation::new(msgtype)))
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn can_user_redact(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_redact(&user_id).await?)
|
||||
@@ -979,6 +424,24 @@ impl Room {
|
||||
Ok(self.inner.can_user_ban(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn ban_user(
|
||||
&self,
|
||||
user_id: String,
|
||||
reason: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.ban_user(&user_id, reason.as_deref()).await?)
|
||||
}
|
||||
|
||||
pub async fn unban_user(
|
||||
&self,
|
||||
user_id: String,
|
||||
reason: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.unban_user(&user_id, reason.as_deref()).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_invite(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_invite(&user_id).await?)
|
||||
@@ -989,6 +452,15 @@ impl Room {
|
||||
Ok(self.inner.can_user_kick(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn kick_user(
|
||||
&self,
|
||||
user_id: String,
|
||||
reason: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.kick_user(&user_id, reason.as_deref()).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_send_state(
|
||||
&self,
|
||||
user_id: String,
|
||||
@@ -1018,86 +490,10 @@ impl Room {
|
||||
pub fn own_user_id(&self) -> String {
|
||||
self.inner.own_user_id().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl Room {
|
||||
fn build_thumbnail_info(
|
||||
&self,
|
||||
thumbnail_url: String,
|
||||
thumbnail_info: ThumbnailInfo,
|
||||
) -> Result<Thumbnail, RoomError> {
|
||||
let thumbnail_data =
|
||||
fs::read(thumbnail_url).map_err(|_| RoomError::InvalidThumbnailData)?;
|
||||
|
||||
let base_thumbnail_info = BaseThumbnailInfo::try_from(&thumbnail_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
|
||||
let mime_str =
|
||||
thumbnail_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
Ok(Thumbnail {
|
||||
data: thumbnail_data,
|
||||
content_type: mime_type,
|
||||
info: Some(base_thumbnail_info),
|
||||
})
|
||||
pub async fn typing_notice(&self, is_typing: bool) -> Result<(), ClientError> {
|
||||
Ok(self.inner.typing_notice(is_typing).await?)
|
||||
}
|
||||
|
||||
async fn send_attachment(
|
||||
&self,
|
||||
url: String,
|
||||
mime_type: Mime,
|
||||
attachment_config: AttachmentConfig,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Result<(), RoomError> {
|
||||
let timeline = self.timeline.read().await.clone().ok_or(RoomError::TimelineUnavailable)?;
|
||||
|
||||
let request = timeline.send_attachment(url, mime_type, attachment_config);
|
||||
if let Some(progress_watcher) = progress_watcher {
|
||||
let mut subscriber = request.subscribe_to_send_progress();
|
||||
RUNTIME.spawn(async move {
|
||||
while let Some(progress) = subscriber.next().await {
|
||||
progress_watcher.transmission_progress(progress.into());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
request.await.map_err(|_| RoomError::FailedSendingAttachment)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SendAttachmentJoinHandle {
|
||||
join_hdl: Arc<Mutex<JoinHandle<Result<(), RoomError>>>>,
|
||||
abort_hdl: AbortHandle,
|
||||
}
|
||||
|
||||
impl SendAttachmentJoinHandle {
|
||||
fn new(join_hdl: JoinHandle<Result<(), RoomError>>) -> Arc<Self> {
|
||||
let abort_hdl = join_hdl.abort_handle();
|
||||
let join_hdl = Arc::new(Mutex::new(join_hdl));
|
||||
Arc::new(Self { join_hdl, abort_hdl })
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl SendAttachmentJoinHandle {
|
||||
pub async fn join(&self) -> Result<(), RoomError> {
|
||||
let join_hdl = self.join_hdl.clone();
|
||||
RUNTIME.spawn(async move { (&mut *join_hdl.lock().await).await.unwrap() }).await.unwrap()
|
||||
}
|
||||
|
||||
pub fn cancel(&self) {
|
||||
self.abort_hdl.abort();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomTimelineListenerResult {
|
||||
pub items: Vec<Arc<TimelineItem>>,
|
||||
pub items_stream: Arc<TaskHandle>,
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
@@ -1105,52 +501,6 @@ pub trait RoomInfoListener: Sync + Send {
|
||||
fn call(&self, room_info: RoomInfo);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait BackPaginationStatusListener: Sync + Send {
|
||||
fn on_update(&self, status: BackPaginationStatus);
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum PaginationOptions {
|
||||
SingleRequest { event_limit: u16, wait_for_token: bool },
|
||||
UntilNumItems { event_limit: u16, items: u16, wait_for_token: bool },
|
||||
}
|
||||
|
||||
impl From<PaginationOptions> for matrix_sdk_ui::timeline::PaginationOptions<'static> {
|
||||
fn from(value: PaginationOptions) -> Self {
|
||||
use matrix_sdk_ui::timeline::PaginationOptions as Opts;
|
||||
let (wait_for_token, mut opts) = match value {
|
||||
PaginationOptions::SingleRequest { event_limit, wait_for_token } => {
|
||||
(wait_for_token, Opts::single_request(event_limit))
|
||||
}
|
||||
PaginationOptions::UntilNumItems { event_limit, items, wait_for_token } => {
|
||||
(wait_for_token, Opts::until_num_items(event_limit, items))
|
||||
}
|
||||
};
|
||||
|
||||
if wait_for_token {
|
||||
opts = opts.wait_for_token();
|
||||
}
|
||||
|
||||
opts
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum AssetType {
|
||||
Sender,
|
||||
Pin,
|
||||
}
|
||||
|
||||
impl From<AssetType> for RumaAssetType {
|
||||
fn from(value: AssetType) -> Self {
|
||||
match value {
|
||||
AssetType::Sender => Self::Self_,
|
||||
AssetType::Pin => Self::Pin,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomMembersIterator {
|
||||
chunk_iterator: ChunkIterator<matrix_sdk::room::RoomMember>,
|
||||
|
||||
@@ -31,6 +31,15 @@ pub struct RoomInfo {
|
||||
user_defined_notification_mode: Option<RoomNotificationMode>,
|
||||
has_room_call: bool,
|
||||
active_room_call_participants: Vec<String>,
|
||||
/// "Interesting" messages received in that room, independently of the
|
||||
/// notification settings.
|
||||
num_unread_messages: u64,
|
||||
/// Events that will notify the user, according to their
|
||||
/// notification settings.
|
||||
num_unread_notifications: u64,
|
||||
/// Events causing mentions/highlights for the user, according to their
|
||||
/// notification settings.
|
||||
num_unread_mentions: u64,
|
||||
}
|
||||
|
||||
impl RoomInfo {
|
||||
@@ -75,6 +84,9 @@ impl RoomInfo {
|
||||
.iter()
|
||||
.map(|u| u.to_string())
|
||||
.collect(),
|
||||
num_unread_messages: room.num_unread_messages(),
|
||||
num_unread_notifications: room.num_unread_notifications(),
|
||||
num_unread_mentions: room.num_unread_mentions(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,14 +13,17 @@ use matrix_sdk::{
|
||||
RoomListEntry as MatrixRoomListEntry,
|
||||
};
|
||||
use matrix_sdk_ui::room_list_service::filters::{
|
||||
new_filter_all, new_filter_fuzzy_match_room_name, new_filter_none,
|
||||
new_filter_all, new_filter_all_non_left, new_filter_fuzzy_match_room_name, new_filter_none,
|
||||
new_filter_normalized_match_room_name,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
error::ClientError, room::Room, room_info::RoomInfo, timeline::EventTimelineItem, TaskHandle,
|
||||
RUNTIME,
|
||||
error::ClientError,
|
||||
room::Room,
|
||||
room_info::RoomInfo,
|
||||
timeline::{EventTimelineItem, Timeline},
|
||||
TaskHandle, RUNTIME,
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
@@ -376,6 +379,7 @@ impl RoomListDynamicEntriesController {
|
||||
|
||||
match kind {
|
||||
Kind::All => self.inner.set_filter(new_filter_all()),
|
||||
Kind::AllNonLeft => self.inner.set_filter(new_filter_all_non_left(&self.client)),
|
||||
Kind::None => self.inner.set_filter(new_filter_none()),
|
||||
Kind::NormalizedMatchRoomName { pattern } => {
|
||||
self.inner.set_filter(new_filter_normalized_match_room_name(&self.client, &pattern))
|
||||
@@ -398,6 +402,7 @@ impl RoomListDynamicEntriesController {
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RoomListEntriesDynamicFilterKind {
|
||||
All,
|
||||
AllNonLeft,
|
||||
None,
|
||||
NormalizedMatchRoomName { pattern: String },
|
||||
FuzzyMatchRoomName { pattern: String },
|
||||
@@ -443,7 +448,7 @@ impl RoomListItem {
|
||||
async fn full_room(&self) -> Arc<Room> {
|
||||
Arc::new(Room::with_timeline(
|
||||
self.inner.inner_room().clone(),
|
||||
Arc::new(RwLock::new(Some(self.inner.timeline().await))),
|
||||
Arc::new(RwLock::new(Some(Timeline::from_arc(self.inner.timeline().await)))),
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -140,6 +140,7 @@ impl RoomMember {
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum StateEventType {
|
||||
CallMember,
|
||||
PolicyRuleRoom,
|
||||
PolicyRuleServer,
|
||||
PolicyRuleUser,
|
||||
@@ -166,6 +167,7 @@ pub enum StateEventType {
|
||||
impl From<StateEventType> for ruma::events::StateEventType {
|
||||
fn from(val: StateEventType) -> Self {
|
||||
match val {
|
||||
StateEventType::CallMember => Self::CallMember,
|
||||
StateEventType::PolicyRuleRoom => Self::PolicyRuleRoom,
|
||||
StateEventType::PolicyRuleServer => Self::PolicyRuleServer,
|
||||
StateEventType::PolicyRuleUser => Self::PolicyRuleUser,
|
||||
|
||||
@@ -0,0 +1,712 @@
|
||||
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{collections::BTreeSet, sync::Arc, time::Duration};
|
||||
|
||||
use extension_trait::extension_trait;
|
||||
use matrix_sdk::attachment::{
|
||||
BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo,
|
||||
};
|
||||
use ruma::{
|
||||
assign,
|
||||
events::{
|
||||
location::AssetType as RumaAssetType,
|
||||
poll::start::PollKind as RumaPollKind,
|
||||
room::{
|
||||
message::{
|
||||
AudioInfo as RumaAudioInfo,
|
||||
AudioMessageEventContent as RumaAudioMessageEventContent,
|
||||
EmoteMessageEventContent as RumaEmoteMessageEventContent, FileInfo as RumaFileInfo,
|
||||
FileMessageEventContent as RumaFileMessageEventContent,
|
||||
FormattedBody as RumaFormattedBody,
|
||||
ImageMessageEventContent as RumaImageMessageEventContent,
|
||||
LocationMessageEventContent as RumaLocationMessageEventContent,
|
||||
MessageType as RumaMessageType,
|
||||
NoticeMessageEventContent as RumaNoticeMessageEventContent,
|
||||
RoomMessageEventContentWithoutRelation,
|
||||
TextMessageEventContent as RumaTextMessageEventContent,
|
||||
UnstableAudioDetailsContentBlock as RumaUnstableAudioDetailsContentBlock,
|
||||
UnstableVoiceContentBlock as RumaUnstableVoiceContentBlock,
|
||||
VideoInfo as RumaVideoInfo,
|
||||
VideoMessageEventContent as RumaVideoMessageEventContent,
|
||||
},
|
||||
ImageInfo as RumaImageInfo, MediaSource, ThumbnailInfo as RumaThumbnailInfo,
|
||||
},
|
||||
},
|
||||
serde::JsonObject,
|
||||
OwnedUserId, UInt, UserId,
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
error::{ClientError, MediaInfoError},
|
||||
helpers::unwrap_or_clone_arc,
|
||||
utils::u64_to_uint,
|
||||
};
|
||||
|
||||
#[uniffi::export]
|
||||
pub fn media_source_from_url(url: String) -> Arc<MediaSource> {
|
||||
Arc::new(MediaSource::Plain(url.into()))
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
pub fn message_event_content_new(
|
||||
msgtype: MessageType,
|
||||
) -> Result<Arc<RoomMessageEventContentWithoutRelation>, ClientError> {
|
||||
Ok(Arc::new(RoomMessageEventContentWithoutRelation::new(msgtype.try_into()?)))
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
pub fn message_event_content_from_markdown(
|
||||
md: String,
|
||||
) -> Arc<RoomMessageEventContentWithoutRelation> {
|
||||
Arc::new(RoomMessageEventContentWithoutRelation::new(RumaMessageType::text_markdown(md)))
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
pub fn message_event_content_from_markdown_as_emote(
|
||||
md: String,
|
||||
) -> Arc<RoomMessageEventContentWithoutRelation> {
|
||||
Arc::new(RoomMessageEventContentWithoutRelation::new(RumaMessageType::emote_markdown(md)))
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
pub fn message_event_content_from_html(
|
||||
body: String,
|
||||
html_body: String,
|
||||
) -> Arc<RoomMessageEventContentWithoutRelation> {
|
||||
Arc::new(RoomMessageEventContentWithoutRelation::new(RumaMessageType::text_html(
|
||||
body, html_body,
|
||||
)))
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
pub fn message_event_content_from_html_as_emote(
|
||||
body: String,
|
||||
html_body: String,
|
||||
) -> Arc<RoomMessageEventContentWithoutRelation> {
|
||||
Arc::new(RoomMessageEventContentWithoutRelation::new(RumaMessageType::emote_html(
|
||||
body, html_body,
|
||||
)))
|
||||
}
|
||||
|
||||
#[extension_trait]
|
||||
pub impl MediaSourceExt for MediaSource {
|
||||
fn from_json(json: String) -> Result<MediaSource, ClientError> {
|
||||
let res = serde_json::from_str(&json)?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn to_json(&self) -> String {
|
||||
serde_json::to_string(self).expect("Media source should always be serializable ")
|
||||
}
|
||||
|
||||
fn url(&self) -> String {
|
||||
match self {
|
||||
MediaSource::Plain(url) => url.to_string(),
|
||||
MediaSource::Encrypted(file) => file.url.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[extension_trait]
|
||||
pub impl RoomMessageEventContentWithoutRelationExt for RoomMessageEventContentWithoutRelation {
|
||||
fn with_mentions(self: Arc<Self>, mentions: Mentions) -> Arc<Self> {
|
||||
let mut content = unwrap_or_clone_arc(self);
|
||||
content.mentions = Some(mentions.into());
|
||||
Arc::new(content)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Mentions {
|
||||
pub user_ids: Vec<String>,
|
||||
pub room: bool,
|
||||
}
|
||||
|
||||
impl From<Mentions> for ruma::events::Mentions {
|
||||
fn from(value: Mentions) -> Self {
|
||||
let mut user_ids = BTreeSet::<OwnedUserId>::new();
|
||||
for user_id in value.user_ids {
|
||||
if let Ok(user_id) = UserId::parse(user_id) {
|
||||
user_ids.insert(user_id);
|
||||
}
|
||||
}
|
||||
let mut result = Self::default();
|
||||
result.user_ids = user_ids;
|
||||
result.room = value.room;
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum MessageType {
|
||||
Emote { content: EmoteMessageContent },
|
||||
Image { content: ImageMessageContent },
|
||||
Audio { content: AudioMessageContent },
|
||||
Video { content: VideoMessageContent },
|
||||
File { content: FileMessageContent },
|
||||
Notice { content: NoticeMessageContent },
|
||||
Text { content: TextMessageContent },
|
||||
Location { content: LocationContent },
|
||||
Other { msgtype: String, body: String },
|
||||
}
|
||||
|
||||
impl TryFrom<MessageType> for RumaMessageType {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_from(value: MessageType) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
MessageType::Emote { content } => {
|
||||
Self::Emote(assign!(RumaEmoteMessageEventContent::plain(content.body), {
|
||||
formatted: content.formatted.map(Into::into),
|
||||
}))
|
||||
}
|
||||
MessageType::Image { content } => Self::Image(
|
||||
RumaImageMessageEventContent::new(content.body, (*content.source).clone())
|
||||
.info(content.info.map(Into::into).map(Box::new)),
|
||||
),
|
||||
MessageType::Audio { content } => Self::Audio(
|
||||
RumaAudioMessageEventContent::new(content.body, (*content.source).clone())
|
||||
.info(content.info.map(Into::into).map(Box::new)),
|
||||
),
|
||||
MessageType::Video { content } => Self::Video(
|
||||
RumaVideoMessageEventContent::new(content.body, (*content.source).clone())
|
||||
.info(content.info.map(Into::into).map(Box::new)),
|
||||
),
|
||||
MessageType::File { content } => Self::File(
|
||||
RumaFileMessageEventContent::new(content.body, (*content.source).clone())
|
||||
.filename(content.filename)
|
||||
.info(content.info.map(Into::into).map(Box::new)),
|
||||
),
|
||||
MessageType::Notice { content } => {
|
||||
Self::Notice(assign!(RumaNoticeMessageEventContent::plain(content.body), {
|
||||
formatted: content.formatted.map(Into::into),
|
||||
}))
|
||||
}
|
||||
MessageType::Text { content } => {
|
||||
Self::Text(assign!(RumaTextMessageEventContent::plain(content.body), {
|
||||
formatted: content.formatted.map(Into::into),
|
||||
}))
|
||||
}
|
||||
MessageType::Location { content } => {
|
||||
Self::Location(RumaLocationMessageEventContent::new(content.body, content.geo_uri))
|
||||
}
|
||||
MessageType::Other { msgtype, body } => {
|
||||
Self::new(&msgtype, body, JsonObject::default())?
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RumaMessageType> for MessageType {
|
||||
fn from(value: RumaMessageType) -> Self {
|
||||
match value {
|
||||
RumaMessageType::Emote(c) => MessageType::Emote {
|
||||
content: EmoteMessageContent {
|
||||
body: c.body.clone(),
|
||||
formatted: c.formatted.as_ref().map(Into::into),
|
||||
},
|
||||
},
|
||||
RumaMessageType::Image(c) => MessageType::Image {
|
||||
content: ImageMessageContent {
|
||||
body: c.body.clone(),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
},
|
||||
},
|
||||
RumaMessageType::Audio(c) => MessageType::Audio {
|
||||
content: AudioMessageContent {
|
||||
body: c.body.clone(),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
audio: c.audio.map(Into::into),
|
||||
voice: c.voice.map(Into::into),
|
||||
},
|
||||
},
|
||||
RumaMessageType::Video(c) => MessageType::Video {
|
||||
content: VideoMessageContent {
|
||||
body: c.body.clone(),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
},
|
||||
},
|
||||
RumaMessageType::File(c) => MessageType::File {
|
||||
content: FileMessageContent {
|
||||
body: c.body.clone(),
|
||||
filename: c.filename.clone(),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
},
|
||||
},
|
||||
RumaMessageType::Notice(c) => MessageType::Notice {
|
||||
content: NoticeMessageContent {
|
||||
body: c.body.clone(),
|
||||
formatted: c.formatted.as_ref().map(Into::into),
|
||||
},
|
||||
},
|
||||
RumaMessageType::Text(c) => MessageType::Text {
|
||||
content: TextMessageContent {
|
||||
body: c.body.clone(),
|
||||
formatted: c.formatted.as_ref().map(Into::into),
|
||||
},
|
||||
},
|
||||
RumaMessageType::Location(c) => {
|
||||
let (description, zoom_level) =
|
||||
c.location.map(|loc| (loc.description, loc.zoom_level)).unwrap_or((None, None));
|
||||
MessageType::Location {
|
||||
content: LocationContent {
|
||||
body: c.body,
|
||||
geo_uri: c.geo_uri,
|
||||
description,
|
||||
zoom_level: zoom_level.and_then(|z| z.get().try_into().ok()),
|
||||
asset: c.asset.and_then(|a| match a.type_ {
|
||||
RumaAssetType::Self_ => Some(AssetType::Sender),
|
||||
RumaAssetType::Pin => Some(AssetType::Pin),
|
||||
_ => None,
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
_ => MessageType::Other {
|
||||
msgtype: value.msgtype().to_owned(),
|
||||
body: value.body().to_owned(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct EmoteMessageContent {
|
||||
pub body: String,
|
||||
pub formatted: Option<FormattedBody>,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct ImageMessageContent {
|
||||
pub body: String,
|
||||
pub source: Arc<MediaSource>,
|
||||
pub info: Option<ImageInfo>,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct AudioMessageContent {
|
||||
pub body: String,
|
||||
pub source: Arc<MediaSource>,
|
||||
pub info: Option<AudioInfo>,
|
||||
pub audio: Option<UnstableAudioDetailsContent>,
|
||||
pub voice: Option<UnstableVoiceContent>,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct VideoMessageContent {
|
||||
pub body: String,
|
||||
pub source: Arc<MediaSource>,
|
||||
pub info: Option<VideoInfo>,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct FileMessageContent {
|
||||
pub body: String,
|
||||
pub filename: Option<String>,
|
||||
pub source: Arc<MediaSource>,
|
||||
pub info: Option<FileInfo>,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct ImageInfo {
|
||||
pub height: Option<u64>,
|
||||
pub width: Option<u64>,
|
||||
pub mimetype: Option<String>,
|
||||
pub size: Option<u64>,
|
||||
pub thumbnail_info: Option<ThumbnailInfo>,
|
||||
pub thumbnail_source: Option<Arc<MediaSource>>,
|
||||
pub blurhash: Option<String>,
|
||||
}
|
||||
|
||||
impl From<ImageInfo> for RumaImageInfo {
|
||||
fn from(value: ImageInfo) -> Self {
|
||||
assign!(RumaImageInfo::new(), {
|
||||
height: value.height.map(u64_to_uint),
|
||||
width: value.width.map(u64_to_uint),
|
||||
mimetype: value.mimetype,
|
||||
size: value.size.map(u64_to_uint),
|
||||
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()),
|
||||
blurhash: value.blurhash,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&ImageInfo> for BaseImageInfo {
|
||||
type Error = MediaInfoError;
|
||||
|
||||
fn try_from(value: &ImageInfo) -> Result<Self, MediaInfoError> {
|
||||
let height = UInt::try_from(value.height.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
let width = UInt::try_from(value.width.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
let size = UInt::try_from(value.size.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
let blurhash = value.blurhash.clone().ok_or(MediaInfoError::MissingField)?;
|
||||
|
||||
Ok(BaseImageInfo {
|
||||
height: Some(height),
|
||||
width: Some(width),
|
||||
size: Some(size),
|
||||
blurhash: Some(blurhash),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct AudioInfo {
|
||||
pub duration: Option<Duration>,
|
||||
pub size: Option<u64>,
|
||||
pub mimetype: Option<String>,
|
||||
}
|
||||
|
||||
impl From<AudioInfo> for RumaAudioInfo {
|
||||
fn from(value: AudioInfo) -> Self {
|
||||
assign!(RumaAudioInfo::new(), {
|
||||
duration: value.duration,
|
||||
size: value.size.map(u64_to_uint),
|
||||
mimetype: value.mimetype,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&AudioInfo> for BaseAudioInfo {
|
||||
type Error = MediaInfoError;
|
||||
|
||||
fn try_from(value: &AudioInfo) -> Result<Self, MediaInfoError> {
|
||||
let duration = value.duration.ok_or(MediaInfoError::MissingField)?;
|
||||
let size = UInt::try_from(value.size.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
|
||||
Ok(BaseAudioInfo { duration: Some(duration), size: Some(size) })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct UnstableAudioDetailsContent {
|
||||
pub duration: Duration,
|
||||
pub waveform: Vec<u16>,
|
||||
}
|
||||
|
||||
impl From<RumaUnstableAudioDetailsContentBlock> for UnstableAudioDetailsContent {
|
||||
fn from(details: RumaUnstableAudioDetailsContentBlock) -> Self {
|
||||
Self {
|
||||
duration: details.duration,
|
||||
waveform: details
|
||||
.waveform
|
||||
.iter()
|
||||
.map(|x| u16::try_from(x.get()).unwrap_or(0))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct UnstableVoiceContent {}
|
||||
|
||||
impl From<RumaUnstableVoiceContentBlock> for UnstableVoiceContent {
|
||||
fn from(_details: RumaUnstableVoiceContentBlock) -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct VideoInfo {
|
||||
pub duration: Option<Duration>,
|
||||
pub height: Option<u64>,
|
||||
pub width: Option<u64>,
|
||||
pub mimetype: Option<String>,
|
||||
pub size: Option<u64>,
|
||||
pub thumbnail_info: Option<ThumbnailInfo>,
|
||||
pub thumbnail_source: Option<Arc<MediaSource>>,
|
||||
pub blurhash: Option<String>,
|
||||
}
|
||||
|
||||
impl From<VideoInfo> for RumaVideoInfo {
|
||||
fn from(value: VideoInfo) -> Self {
|
||||
assign!(RumaVideoInfo::new(), {
|
||||
duration: value.duration,
|
||||
height: value.height.map(u64_to_uint),
|
||||
width: value.width.map(u64_to_uint),
|
||||
mimetype: value.mimetype,
|
||||
size: value.size.map(u64_to_uint),
|
||||
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()),
|
||||
blurhash: value.blurhash,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&VideoInfo> for BaseVideoInfo {
|
||||
type Error = MediaInfoError;
|
||||
|
||||
fn try_from(value: &VideoInfo) -> Result<Self, MediaInfoError> {
|
||||
let duration = value.duration.ok_or(MediaInfoError::MissingField)?;
|
||||
let height = UInt::try_from(value.height.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
let width = UInt::try_from(value.width.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
let size = UInt::try_from(value.size.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
let blurhash = value.blurhash.clone().ok_or(MediaInfoError::MissingField)?;
|
||||
|
||||
Ok(BaseVideoInfo {
|
||||
duration: Some(duration),
|
||||
height: Some(height),
|
||||
width: Some(width),
|
||||
size: Some(size),
|
||||
blurhash: Some(blurhash),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct FileInfo {
|
||||
pub mimetype: Option<String>,
|
||||
pub size: Option<u64>,
|
||||
pub thumbnail_info: Option<ThumbnailInfo>,
|
||||
pub thumbnail_source: Option<Arc<MediaSource>>,
|
||||
}
|
||||
|
||||
impl From<FileInfo> for RumaFileInfo {
|
||||
fn from(value: FileInfo) -> Self {
|
||||
assign!(RumaFileInfo::new(), {
|
||||
mimetype: value.mimetype,
|
||||
size: value.size.map(u64_to_uint),
|
||||
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&FileInfo> for BaseFileInfo {
|
||||
type Error = MediaInfoError;
|
||||
|
||||
fn try_from(value: &FileInfo) -> Result<Self, MediaInfoError> {
|
||||
let size = UInt::try_from(value.size.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
|
||||
Ok(BaseFileInfo { size: Some(size) })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct ThumbnailInfo {
|
||||
pub height: Option<u64>,
|
||||
pub width: Option<u64>,
|
||||
pub mimetype: Option<String>,
|
||||
pub size: Option<u64>,
|
||||
}
|
||||
|
||||
impl From<ThumbnailInfo> for RumaThumbnailInfo {
|
||||
fn from(value: ThumbnailInfo) -> Self {
|
||||
assign!(RumaThumbnailInfo::new(), {
|
||||
height: value.height.map(u64_to_uint),
|
||||
width: value.width.map(u64_to_uint),
|
||||
mimetype: value.mimetype,
|
||||
size: value.size.map(u64_to_uint),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&ThumbnailInfo> for BaseThumbnailInfo {
|
||||
type Error = MediaInfoError;
|
||||
|
||||
fn try_from(value: &ThumbnailInfo) -> Result<Self, MediaInfoError> {
|
||||
let height = UInt::try_from(value.height.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
let width = UInt::try_from(value.width.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
let size = UInt::try_from(value.size.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
|
||||
Ok(BaseThumbnailInfo { height: Some(height), width: Some(width), size: Some(size) })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct NoticeMessageContent {
|
||||
pub body: String,
|
||||
pub formatted: Option<FormattedBody>,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct TextMessageContent {
|
||||
pub body: String,
|
||||
pub formatted: Option<FormattedBody>,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct LocationContent {
|
||||
pub body: String,
|
||||
pub geo_uri: String,
|
||||
pub description: Option<String>,
|
||||
pub zoom_level: Option<u8>,
|
||||
pub asset: Option<AssetType>,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum AssetType {
|
||||
Sender,
|
||||
Pin,
|
||||
}
|
||||
|
||||
impl From<AssetType> for RumaAssetType {
|
||||
fn from(value: AssetType) -> Self {
|
||||
match value {
|
||||
AssetType::Sender => Self::Self_,
|
||||
AssetType::Pin => Self::Pin,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct FormattedBody {
|
||||
pub format: MessageFormat,
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
impl From<FormattedBody> for RumaFormattedBody {
|
||||
fn from(f: FormattedBody) -> Self {
|
||||
Self {
|
||||
format: match f.format {
|
||||
MessageFormat::Html => matrix_sdk::ruma::events::room::message::MessageFormat::Html,
|
||||
MessageFormat::Unknown { format } => format.into(),
|
||||
},
|
||||
body: f.body,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RumaFormattedBody> for FormattedBody {
|
||||
fn from(f: &RumaFormattedBody) -> Self {
|
||||
Self {
|
||||
format: match &f.format {
|
||||
matrix_sdk::ruma::events::room::message::MessageFormat::Html => MessageFormat::Html,
|
||||
_ => MessageFormat::Unknown { format: f.format.to_string() },
|
||||
},
|
||||
body: f.body.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum MessageFormat {
|
||||
Html,
|
||||
Unknown { format: String },
|
||||
}
|
||||
|
||||
impl From<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
|
||||
fn from(info: &matrix_sdk::ruma::events::room::ImageInfo) -> Self {
|
||||
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
mimetype: info.mimetype.clone(),
|
||||
size: info.size.map(Into::into),
|
||||
});
|
||||
|
||||
Self {
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
mimetype: info.mimetype.clone(),
|
||||
size: info.size.map(Into::into),
|
||||
thumbnail_info,
|
||||
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
|
||||
blurhash: info.blurhash.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RumaAudioInfo> for AudioInfo {
|
||||
fn from(info: &RumaAudioInfo) -> Self {
|
||||
Self {
|
||||
duration: info.duration,
|
||||
size: info.size.map(Into::into),
|
||||
mimetype: info.mimetype.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RumaVideoInfo> for VideoInfo {
|
||||
fn from(info: &RumaVideoInfo) -> Self {
|
||||
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
mimetype: info.mimetype.clone(),
|
||||
size: info.size.map(Into::into),
|
||||
});
|
||||
|
||||
Self {
|
||||
duration: info.duration,
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
mimetype: info.mimetype.clone(),
|
||||
size: info.size.map(Into::into),
|
||||
thumbnail_info,
|
||||
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
|
||||
blurhash: info.blurhash.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RumaFileInfo> for FileInfo {
|
||||
fn from(info: &RumaFileInfo) -> Self {
|
||||
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
mimetype: info.mimetype.clone(),
|
||||
size: info.size.map(Into::into),
|
||||
});
|
||||
|
||||
Self {
|
||||
mimetype: info.mimetype.clone(),
|
||||
size: info.size.map(Into::into),
|
||||
thumbnail_info,
|
||||
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum PollKind {
|
||||
Disclosed,
|
||||
Undisclosed,
|
||||
}
|
||||
|
||||
impl From<PollKind> for RumaPollKind {
|
||||
fn from(value: PollKind) -> Self {
|
||||
match value {
|
||||
PollKind::Disclosed => Self::Disclosed,
|
||||
PollKind::Undisclosed => Self::Undisclosed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RumaPollKind> for PollKind {
|
||||
fn from(value: RumaPollKind) -> Self {
|
||||
match value {
|
||||
RumaPollKind::Disclosed => Self::Disclosed,
|
||||
RumaPollKind::Undisclosed => Self::Undisclosed,
|
||||
_ => {
|
||||
info!("Unknown poll kind, defaulting to undisclosed");
|
||||
Self::Undisclosed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,11 +31,17 @@ impl SessionVerificationEmoji {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum SessionVerificationData {
|
||||
Emojis { emojis: Vec<Arc<SessionVerificationEmoji>>, indices: Vec<u8> },
|
||||
Decimals { values: Vec<u16> },
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait SessionVerificationControllerDelegate: Sync + Send {
|
||||
fn did_accept_verification_request(&self);
|
||||
fn did_start_sas_verification(&self);
|
||||
fn did_receive_verification_data(&self, data: Vec<Arc<SessionVerificationEmoji>>);
|
||||
fn did_receive_verification_data(&self, data: SessionVerificationData);
|
||||
fn did_fail(&self);
|
||||
fn did_cancel(&self);
|
||||
fn did_finish(&self);
|
||||
@@ -199,25 +205,31 @@ impl SessionVerificationController {
|
||||
|
||||
while let Some(state) = stream.next().await {
|
||||
match state {
|
||||
SasState::KeysExchanged { emojis, decimals: _ } => {
|
||||
// TODO: If emojis is None, decimals should be used.
|
||||
if let Some(emojis) = emojis {
|
||||
if let Some(delegate) = &*delegate.read().unwrap() {
|
||||
let emojis = emojis
|
||||
.emojis
|
||||
.iter()
|
||||
.map(|e| {
|
||||
Arc::new(SessionVerificationEmoji {
|
||||
symbol: e.symbol.to_owned(),
|
||||
description: e.description.to_owned(),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
delegate.did_receive_verification_data(emojis);
|
||||
SasState::KeysExchanged { emojis, decimals } => {
|
||||
if let Some(delegate) = &*delegate.read().unwrap() {
|
||||
if let Some(emojis) = emojis {
|
||||
delegate.did_receive_verification_data(
|
||||
SessionVerificationData::Emojis {
|
||||
emojis: emojis
|
||||
.emojis
|
||||
.into_iter()
|
||||
.map(|emoji| {
|
||||
Arc::new(SessionVerificationEmoji {
|
||||
symbol: emoji.symbol.to_owned(),
|
||||
description: emoji.description.to_owned(),
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
indices: emojis.indices.to_vec(),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
delegate.did_receive_verification_data(
|
||||
SessionVerificationData::Decimals {
|
||||
values: vec![decimals.0, decimals.1, decimals.2],
|
||||
},
|
||||
)
|
||||
}
|
||||
} else if let Some(delegate) = &*delegate.read().unwrap() {
|
||||
delegate.did_fail()
|
||||
}
|
||||
}
|
||||
SasState::Done { .. } => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,399 @@
|
||||
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use matrix_sdk_ui::timeline::{PollResult, TimelineDetails};
|
||||
use tracing::warn;
|
||||
|
||||
use super::ProfileDetails;
|
||||
use crate::ruma::{ImageInfo, MessageType, PollKind};
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct TimelineItemContent(pub(crate) matrix_sdk_ui::timeline::TimelineItemContent);
|
||||
|
||||
#[uniffi::export]
|
||||
impl TimelineItemContent {
|
||||
pub fn kind(&self) -> TimelineItemContentKind {
|
||||
use matrix_sdk_ui::timeline::TimelineItemContent as Content;
|
||||
|
||||
match &self.0 {
|
||||
Content::Message(_) => TimelineItemContentKind::Message,
|
||||
Content::RedactedMessage => TimelineItemContentKind::RedactedMessage,
|
||||
Content::Sticker(sticker) => {
|
||||
let content = sticker.content();
|
||||
TimelineItemContentKind::Sticker {
|
||||
body: content.body.clone(),
|
||||
info: (&content.info).into(),
|
||||
url: content.url.to_string(),
|
||||
}
|
||||
}
|
||||
Content::Poll(poll_state) => TimelineItemContentKind::from(poll_state.results()),
|
||||
Content::UnableToDecrypt(msg) => {
|
||||
TimelineItemContentKind::UnableToDecrypt { msg: EncryptedMessage::new(msg) }
|
||||
}
|
||||
Content::MembershipChange(membership) => TimelineItemContentKind::RoomMembership {
|
||||
user_id: membership.user_id().to_string(),
|
||||
change: membership.change().map(Into::into),
|
||||
},
|
||||
Content::ProfileChange(profile) => {
|
||||
let (display_name, prev_display_name) = profile
|
||||
.displayname_change()
|
||||
.map(|change| (change.new.clone(), change.old.clone()))
|
||||
.unzip();
|
||||
let (avatar_url, prev_avatar_url) = profile
|
||||
.avatar_url_change()
|
||||
.map(|change| {
|
||||
(
|
||||
change.new.as_ref().map(ToString::to_string),
|
||||
change.old.as_ref().map(ToString::to_string),
|
||||
)
|
||||
})
|
||||
.unzip();
|
||||
TimelineItemContentKind::ProfileChange {
|
||||
display_name: display_name.flatten(),
|
||||
prev_display_name: prev_display_name.flatten(),
|
||||
avatar_url: avatar_url.flatten(),
|
||||
prev_avatar_url: prev_avatar_url.flatten(),
|
||||
}
|
||||
}
|
||||
Content::OtherState(state) => TimelineItemContentKind::State {
|
||||
state_key: state.state_key().to_owned(),
|
||||
content: state.content().into(),
|
||||
},
|
||||
Content::FailedToParseMessageLike { event_type, error } => {
|
||||
TimelineItemContentKind::FailedToParseMessageLike {
|
||||
event_type: event_type.to_string(),
|
||||
error: error.to_string(),
|
||||
}
|
||||
}
|
||||
Content::FailedToParseState { event_type, state_key, error } => {
|
||||
TimelineItemContentKind::FailedToParseState {
|
||||
event_type: event_type.to_string(),
|
||||
state_key: state_key.to_string(),
|
||||
error: error.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_message(self: Arc<Self>) -> Option<Arc<Message>> {
|
||||
use matrix_sdk_ui::timeline::TimelineItemContent as Content;
|
||||
unwrap_or_clone_arc_into_variant!(self, .0, Content::Message(msg) => Arc::new(Message(msg)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum TimelineItemContentKind {
|
||||
Message,
|
||||
RedactedMessage,
|
||||
Sticker {
|
||||
body: String,
|
||||
info: ImageInfo,
|
||||
url: String,
|
||||
},
|
||||
Poll {
|
||||
question: String,
|
||||
kind: PollKind,
|
||||
max_selections: u64,
|
||||
answers: Vec<PollAnswer>,
|
||||
votes: HashMap<String, Vec<String>>,
|
||||
end_time: Option<u64>,
|
||||
has_been_edited: bool,
|
||||
},
|
||||
UnableToDecrypt {
|
||||
msg: EncryptedMessage,
|
||||
},
|
||||
RoomMembership {
|
||||
user_id: String,
|
||||
change: Option<MembershipChange>,
|
||||
},
|
||||
ProfileChange {
|
||||
display_name: Option<String>,
|
||||
prev_display_name: Option<String>,
|
||||
avatar_url: Option<String>,
|
||||
prev_avatar_url: Option<String>,
|
||||
},
|
||||
State {
|
||||
state_key: String,
|
||||
content: OtherState,
|
||||
},
|
||||
FailedToParseMessageLike {
|
||||
event_type: String,
|
||||
error: String,
|
||||
},
|
||||
FailedToParseState {
|
||||
event_type: String,
|
||||
state_key: String,
|
||||
error: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct Message(matrix_sdk_ui::timeline::Message);
|
||||
|
||||
#[uniffi::export]
|
||||
impl Message {
|
||||
pub fn msgtype(&self) -> MessageType {
|
||||
self.0.msgtype().clone().into()
|
||||
}
|
||||
|
||||
pub fn body(&self) -> String {
|
||||
self.0.msgtype().body().to_owned()
|
||||
}
|
||||
|
||||
pub fn in_reply_to(&self) -> Option<InReplyToDetails> {
|
||||
self.0.in_reply_to().map(InReplyToDetails::from)
|
||||
}
|
||||
|
||||
pub fn is_threaded(&self) -> bool {
|
||||
self.0.is_threaded()
|
||||
}
|
||||
|
||||
pub fn is_edited(&self) -> bool {
|
||||
self.0.is_edited()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct InReplyToDetails {
|
||||
event_id: String,
|
||||
event: RepliedToEventDetails,
|
||||
}
|
||||
|
||||
impl From<&matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
|
||||
fn from(inner: &matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
|
||||
let event_id = inner.event_id.to_string();
|
||||
let event = match &inner.event {
|
||||
TimelineDetails::Unavailable => RepliedToEventDetails::Unavailable,
|
||||
TimelineDetails::Pending => RepliedToEventDetails::Pending,
|
||||
TimelineDetails::Ready(event) => RepliedToEventDetails::Ready {
|
||||
content: Arc::new(TimelineItemContent(event.content().to_owned())),
|
||||
sender: event.sender().to_string(),
|
||||
sender_profile: event.sender_profile().into(),
|
||||
},
|
||||
TimelineDetails::Error(err) => {
|
||||
RepliedToEventDetails::Error { message: err.to_string() }
|
||||
}
|
||||
};
|
||||
|
||||
Self { event_id, event }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RepliedToEventDetails {
|
||||
Unavailable,
|
||||
Pending,
|
||||
Ready { content: Arc<TimelineItemContent>, sender: String, sender_profile: ProfileDetails },
|
||||
Error { message: String },
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum EncryptedMessage {
|
||||
OlmV1Curve25519AesSha2 {
|
||||
/// The Curve25519 key of the sender.
|
||||
sender_key: String,
|
||||
},
|
||||
// Other fields not included because UniFFI doesn't have the concept of
|
||||
// deprecated fields right now.
|
||||
MegolmV1AesSha2 {
|
||||
/// The ID of the session used to encrypt the message.
|
||||
session_id: String,
|
||||
},
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl EncryptedMessage {
|
||||
fn new(msg: &matrix_sdk_ui::timeline::EncryptedMessage) -> Self {
|
||||
use matrix_sdk_ui::timeline::EncryptedMessage as Message;
|
||||
|
||||
match msg {
|
||||
Message::OlmV1Curve25519AesSha2 { sender_key } => {
|
||||
let sender_key = sender_key.clone();
|
||||
Self::OlmV1Curve25519AesSha2 { sender_key }
|
||||
}
|
||||
Message::MegolmV1AesSha2 { session_id, .. } => {
|
||||
let session_id = session_id.clone();
|
||||
Self::MegolmV1AesSha2 { session_id }
|
||||
}
|
||||
Message::Unknown => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct Reaction {
|
||||
pub key: String,
|
||||
pub count: u64,
|
||||
pub senders: Vec<ReactionSenderData>,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct ReactionSenderData {
|
||||
pub sender_id: String,
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum MembershipChange {
|
||||
None,
|
||||
Error,
|
||||
Joined,
|
||||
Left,
|
||||
Banned,
|
||||
Unbanned,
|
||||
Kicked,
|
||||
Invited,
|
||||
KickedAndBanned,
|
||||
InvitationAccepted,
|
||||
InvitationRejected,
|
||||
InvitationRevoked,
|
||||
Knocked,
|
||||
KnockAccepted,
|
||||
KnockRetracted,
|
||||
KnockDenied,
|
||||
NotImplemented,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::MembershipChange> for MembershipChange {
|
||||
fn from(membership_change: matrix_sdk_ui::timeline::MembershipChange) -> Self {
|
||||
use matrix_sdk_ui::timeline::MembershipChange as Change;
|
||||
match membership_change {
|
||||
Change::None => Self::None,
|
||||
Change::Error => Self::Error,
|
||||
Change::Joined => Self::Joined,
|
||||
Change::Left => Self::Left,
|
||||
Change::Banned => Self::Banned,
|
||||
Change::Unbanned => Self::Unbanned,
|
||||
Change::Kicked => Self::Kicked,
|
||||
Change::Invited => Self::Invited,
|
||||
Change::KickedAndBanned => Self::KickedAndBanned,
|
||||
Change::InvitationAccepted => Self::InvitationAccepted,
|
||||
Change::InvitationRejected => Self::InvitationRejected,
|
||||
Change::InvitationRevoked => Self::InvitationRevoked,
|
||||
Change::Knocked => Self::Knocked,
|
||||
Change::KnockAccepted => Self::KnockAccepted,
|
||||
Change::KnockRetracted => Self::KnockRetracted,
|
||||
Change::KnockDenied => Self::KnockDenied,
|
||||
Change::NotImplemented => Self::NotImplemented,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum OtherState {
|
||||
PolicyRuleRoom,
|
||||
PolicyRuleServer,
|
||||
PolicyRuleUser,
|
||||
RoomAliases,
|
||||
RoomAvatar { url: Option<String> },
|
||||
RoomCanonicalAlias,
|
||||
RoomCreate,
|
||||
RoomEncryption,
|
||||
RoomGuestAccess,
|
||||
RoomHistoryVisibility,
|
||||
RoomJoinRules,
|
||||
RoomName { name: Option<String> },
|
||||
RoomPinnedEvents,
|
||||
RoomPowerLevels,
|
||||
RoomServerAcl,
|
||||
RoomThirdPartyInvite { display_name: Option<String> },
|
||||
RoomTombstone,
|
||||
RoomTopic { topic: Option<String> },
|
||||
SpaceChild,
|
||||
SpaceParent,
|
||||
Custom { event_type: String },
|
||||
}
|
||||
|
||||
impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherState {
|
||||
fn from(content: &matrix_sdk_ui::timeline::AnyOtherFullStateEventContent) -> Self {
|
||||
use matrix_sdk::ruma::events::FullStateEventContent as FullContent;
|
||||
use matrix_sdk_ui::timeline::AnyOtherFullStateEventContent as Content;
|
||||
|
||||
match content {
|
||||
Content::PolicyRuleRoom(_) => Self::PolicyRuleRoom,
|
||||
Content::PolicyRuleServer(_) => Self::PolicyRuleServer,
|
||||
Content::PolicyRuleUser(_) => Self::PolicyRuleUser,
|
||||
Content::RoomAliases(_) => Self::RoomAliases,
|
||||
Content::RoomAvatar(c) => {
|
||||
let url = match c {
|
||||
FullContent::Original { content, .. } => {
|
||||
content.url.as_ref().map(ToString::to_string)
|
||||
}
|
||||
FullContent::Redacted(_) => None,
|
||||
};
|
||||
Self::RoomAvatar { url }
|
||||
}
|
||||
Content::RoomCanonicalAlias(_) => Self::RoomCanonicalAlias,
|
||||
Content::RoomCreate(_) => Self::RoomCreate,
|
||||
Content::RoomEncryption(_) => Self::RoomEncryption,
|
||||
Content::RoomGuestAccess(_) => Self::RoomGuestAccess,
|
||||
Content::RoomHistoryVisibility(_) => Self::RoomHistoryVisibility,
|
||||
Content::RoomJoinRules(_) => Self::RoomJoinRules,
|
||||
Content::RoomName(c) => {
|
||||
let name = match c {
|
||||
FullContent::Original { content, .. } => Some(content.name.clone()),
|
||||
FullContent::Redacted(_) => None,
|
||||
};
|
||||
Self::RoomName { name }
|
||||
}
|
||||
Content::RoomPinnedEvents(_) => Self::RoomPinnedEvents,
|
||||
Content::RoomPowerLevels(_) => Self::RoomPowerLevels,
|
||||
Content::RoomServerAcl(_) => Self::RoomServerAcl,
|
||||
Content::RoomThirdPartyInvite(c) => {
|
||||
let display_name = match c {
|
||||
FullContent::Original { content, .. } => Some(content.display_name.clone()),
|
||||
FullContent::Redacted(_) => None,
|
||||
};
|
||||
Self::RoomThirdPartyInvite { display_name }
|
||||
}
|
||||
Content::RoomTombstone(_) => Self::RoomTombstone,
|
||||
Content::RoomTopic(c) => {
|
||||
let topic = match c {
|
||||
FullContent::Original { content, .. } => Some(content.topic.clone()),
|
||||
FullContent::Redacted(_) => None,
|
||||
};
|
||||
Self::RoomTopic { topic }
|
||||
}
|
||||
Content::SpaceChild(_) => Self::SpaceChild,
|
||||
Content::SpaceParent(_) => Self::SpaceParent,
|
||||
Content::_Custom { event_type, .. } => Self::Custom { event_type: event_type.clone() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct PollAnswer {
|
||||
pub id: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
impl From<PollResult> for TimelineItemContentKind {
|
||||
fn from(value: PollResult) -> Self {
|
||||
TimelineItemContentKind::Poll {
|
||||
question: value.question,
|
||||
kind: PollKind::from(value.kind),
|
||||
max_selections: value.max_selections,
|
||||
answers: value
|
||||
.answers
|
||||
.into_iter()
|
||||
.map(|i| PollAnswer { id: i.id, text: i.text })
|
||||
.collect(),
|
||||
votes: value.votes,
|
||||
end_time: value.end_time,
|
||||
has_been_edited: value.has_been_edited,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,998 @@
|
||||
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{collections::HashMap, fmt::Write as _, fs, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use as_variant::as_variant;
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use matrix_sdk::attachment::{
|
||||
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
|
||||
BaseThumbnailInfo, BaseVideoInfo, Thumbnail,
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{BackPaginationStatus, EventItemOrigin, Profile, TimelineDetails};
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
events::{
|
||||
location::{AssetType as RumaAssetType, LocationContent, ZoomLevel},
|
||||
poll::{
|
||||
unstable_end::UnstablePollEndEventContent,
|
||||
unstable_response::UnstablePollResponseEventContent,
|
||||
unstable_start::{
|
||||
NewUnstablePollStartEventContent, UnstablePollAnswer, UnstablePollAnswers,
|
||||
UnstablePollStartContentBlock,
|
||||
},
|
||||
},
|
||||
receipt::ReceiptThread,
|
||||
relation::Annotation,
|
||||
room::message::{
|
||||
ForwardThread, LocationMessageEventContent, MessageType,
|
||||
RoomMessageEventContentWithoutRelation,
|
||||
},
|
||||
AnyMessageLikeEventContent,
|
||||
},
|
||||
EventId,
|
||||
};
|
||||
use tokio::{
|
||||
sync::Mutex,
|
||||
task::{AbortHandle, JoinHandle},
|
||||
};
|
||||
use tracing::{error, info, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
client::ProgressWatcher,
|
||||
error::{ClientError, RoomError},
|
||||
helpers::unwrap_or_clone_arc,
|
||||
ruma::{AssetType, AudioInfo, FileInfo, ImageInfo, PollKind, ThumbnailInfo, VideoInfo},
|
||||
task_handle::TaskHandle,
|
||||
RUNTIME,
|
||||
};
|
||||
|
||||
mod content;
|
||||
|
||||
pub use self::content::{Reaction, ReactionSenderData, TimelineItemContent};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
#[repr(transparent)]
|
||||
pub struct Timeline {
|
||||
pub(crate) inner: matrix_sdk_ui::timeline::Timeline,
|
||||
}
|
||||
|
||||
impl Timeline {
|
||||
pub(crate) fn new(inner: matrix_sdk_ui::timeline::Timeline) -> Arc<Self> {
|
||||
Arc::new(Self { inner })
|
||||
}
|
||||
|
||||
pub(crate) fn from_arc(inner: Arc<matrix_sdk_ui::timeline::Timeline>) -> Arc<Self> {
|
||||
// SAFETY: repr(transparent) means transmuting the arc this way is allowed
|
||||
unsafe { Arc::from_raw(Arc::into_raw(inner) as _) }
|
||||
}
|
||||
|
||||
fn build_thumbnail_info(
|
||||
&self,
|
||||
thumbnail_url: String,
|
||||
thumbnail_info: ThumbnailInfo,
|
||||
) -> Result<Thumbnail, RoomError> {
|
||||
let thumbnail_data =
|
||||
fs::read(thumbnail_url).map_err(|_| RoomError::InvalidThumbnailData)?;
|
||||
|
||||
let base_thumbnail_info = BaseThumbnailInfo::try_from(&thumbnail_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
|
||||
let mime_str =
|
||||
thumbnail_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
Ok(Thumbnail {
|
||||
data: thumbnail_data,
|
||||
content_type: mime_type,
|
||||
info: Some(base_thumbnail_info),
|
||||
})
|
||||
}
|
||||
|
||||
async fn send_attachment(
|
||||
&self,
|
||||
url: String,
|
||||
mime_type: Mime,
|
||||
attachment_config: AttachmentConfig,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Result<(), RoomError> {
|
||||
let request = self.inner.send_attachment(url, mime_type, attachment_config);
|
||||
if let Some(progress_watcher) = progress_watcher {
|
||||
let mut subscriber = request.subscribe_to_send_progress();
|
||||
RUNTIME.spawn(async move {
|
||||
while let Some(progress) = subscriber.next().await {
|
||||
progress_watcher.transmission_progress(progress.into());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
request.await.map_err(|_| RoomError::FailedSendingAttachment)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl Timeline {
|
||||
pub async fn add_listener(
|
||||
&self,
|
||||
listener: Box<dyn TimelineListener>,
|
||||
) -> RoomTimelineListenerResult {
|
||||
let (timeline_items, timeline_stream) = self.inner.subscribe_batched().await;
|
||||
let timeline_stream = TaskHandle::new(RUNTIME.spawn(async move {
|
||||
pin_mut!(timeline_stream);
|
||||
|
||||
while let Some(diffs) = timeline_stream.next().await {
|
||||
listener
|
||||
.on_update(diffs.into_iter().map(|d| Arc::new(TimelineDiff::new(d))).collect());
|
||||
}
|
||||
}));
|
||||
|
||||
RoomTimelineListenerResult {
|
||||
items: timeline_items.into_iter().map(TimelineItem::from_arc).collect(),
|
||||
items_stream: Arc::new(timeline_stream),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn retry_decryption(self: Arc<Self>, session_ids: Vec<String>) {
|
||||
RUNTIME.spawn(async move {
|
||||
self.inner.retry_decryption(&session_ids).await;
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn fetch_members(&self) {
|
||||
self.inner.fetch_members().await
|
||||
}
|
||||
|
||||
pub fn subscribe_to_back_pagination_status(
|
||||
&self,
|
||||
listener: Box<dyn BackPaginationStatusListener>,
|
||||
) -> Result<Arc<TaskHandle>, ClientError> {
|
||||
let mut subscriber = self.inner.back_pagination_status();
|
||||
|
||||
Ok(Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
// Send the current state even if it hasn't changed right away.
|
||||
listener.on_update(subscriber.next_now());
|
||||
|
||||
while let Some(status) = subscriber.next().await {
|
||||
listener.on_update(status);
|
||||
}
|
||||
}))))
|
||||
}
|
||||
|
||||
/// Loads older messages into the timeline.
|
||||
///
|
||||
/// Raises an exception if there are no timeline listeners.
|
||||
pub fn paginate_backwards(&self, opts: PaginationOptions) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async { Ok(self.inner.paginate_backwards(opts.into()).await?) })
|
||||
}
|
||||
|
||||
pub fn send_read_receipt(
|
||||
&self,
|
||||
receipt_type: ReceiptType,
|
||||
event_id: String,
|
||||
) -> Result<(), ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
|
||||
RUNTIME.block_on(async {
|
||||
self.inner
|
||||
.send_single_receipt(receipt_type.into(), ReceiptThread::Unthreaded, event_id)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send(self: Arc<Self>, msg: Arc<RoomMessageEventContentWithoutRelation>) {
|
||||
RUNTIME.spawn(async move {
|
||||
self.inner.send((*msg).to_owned().with_relation(None).into()).await;
|
||||
});
|
||||
}
|
||||
|
||||
pub fn send_image(
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
thumbnail_url: Option<String>,
|
||||
image_info: ImageInfo,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
let mime_str =
|
||||
image_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let base_image_info = BaseImageInfo::try_from(&image_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
|
||||
let attachment_info = AttachmentInfo::Image(base_image_info);
|
||||
|
||||
let attachment_config = match (thumbnail_url, image_info.thumbnail_info) {
|
||||
(Some(thumbnail_url), Some(thumbnail_image_info)) => {
|
||||
let thumbnail =
|
||||
self.build_thumbnail_info(thumbnail_url, thumbnail_image_info)?;
|
||||
AttachmentConfig::with_thumbnail(thumbnail).info(attachment_info)
|
||||
}
|
||||
_ => AttachmentConfig::new().info(attachment_info),
|
||||
};
|
||||
|
||||
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn send_video(
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
thumbnail_url: Option<String>,
|
||||
video_info: VideoInfo,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
let mime_str =
|
||||
video_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let base_video_info: BaseVideoInfo = BaseVideoInfo::try_from(&video_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
|
||||
let attachment_info = AttachmentInfo::Video(base_video_info);
|
||||
|
||||
let attachment_config = match (thumbnail_url, video_info.thumbnail_info) {
|
||||
(Some(thumbnail_url), Some(thumbnail_image_info)) => {
|
||||
let thumbnail =
|
||||
self.build_thumbnail_info(thumbnail_url, thumbnail_image_info)?;
|
||||
AttachmentConfig::with_thumbnail(thumbnail).info(attachment_info)
|
||||
}
|
||||
_ => AttachmentConfig::new().info(attachment_info),
|
||||
};
|
||||
|
||||
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn send_audio(
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
audio_info: AudioInfo,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
let mime_str =
|
||||
audio_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
|
||||
let attachment_info = AttachmentInfo::Audio(base_audio_info);
|
||||
let attachment_config = AttachmentConfig::new().info(attachment_info);
|
||||
|
||||
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn send_voice_message(
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
audio_info: AudioInfo,
|
||||
waveform: Vec<u16>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
let mime_str =
|
||||
audio_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
|
||||
let attachment_info =
|
||||
AttachmentInfo::Voice { audio_info: base_audio_info, waveform: Some(waveform) };
|
||||
let attachment_config = AttachmentConfig::new().info(attachment_info);
|
||||
|
||||
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn send_file(
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
file_info: FileInfo,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
let mime_str =
|
||||
file_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let base_file_info: BaseFileInfo =
|
||||
BaseFileInfo::try_from(&file_info).map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
|
||||
let attachment_info = AttachmentInfo::File(base_file_info);
|
||||
let attachment_config = AttachmentConfig::new().info(attachment_info);
|
||||
|
||||
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn create_poll(
|
||||
self: Arc<Self>,
|
||||
question: String,
|
||||
answers: Vec<String>,
|
||||
max_selections: u8,
|
||||
poll_kind: PollKind,
|
||||
) -> Result<(), ClientError> {
|
||||
let poll_data = PollData { question, answers, max_selections, poll_kind };
|
||||
|
||||
let poll_start_event_content = NewUnstablePollStartEventContent::plain_text(
|
||||
poll_data.fallback_text(),
|
||||
poll_data.try_into()?,
|
||||
);
|
||||
let event_content =
|
||||
AnyMessageLikeEventContent::UnstablePollStart(poll_start_event_content.into());
|
||||
|
||||
RUNTIME.spawn(async move {
|
||||
self.inner.send(event_content).await;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_poll_response(
|
||||
self: Arc<Self>,
|
||||
poll_start_id: String,
|
||||
answers: Vec<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let poll_start_event_id =
|
||||
EventId::parse(poll_start_id).context("Failed to parse EventId")?;
|
||||
let poll_response_event_content =
|
||||
UnstablePollResponseEventContent::new(answers, poll_start_event_id);
|
||||
let event_content =
|
||||
AnyMessageLikeEventContent::UnstablePollResponse(poll_response_event_content);
|
||||
|
||||
RUNTIME.spawn(async move {
|
||||
self.inner.send(event_content).await;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn end_poll(
|
||||
self: Arc<Self>,
|
||||
poll_start_id: String,
|
||||
text: String,
|
||||
) -> Result<(), ClientError> {
|
||||
let poll_start_event_id =
|
||||
EventId::parse(poll_start_id).context("Failed to parse EventId")?;
|
||||
let poll_end_event_content = UnstablePollEndEventContent::new(text, poll_start_event_id);
|
||||
let event_content = AnyMessageLikeEventContent::UnstablePollEnd(poll_end_event_content);
|
||||
|
||||
RUNTIME.spawn(async move {
|
||||
self.inner.send(event_content).await;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_reply(
|
||||
&self,
|
||||
msg: Arc<RoomMessageEventContentWithoutRelation>,
|
||||
reply_item: Arc<EventTimelineItem>,
|
||||
) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async {
|
||||
self.inner.send_reply((*msg).clone(), &reply_item.0, ForwardThread::Yes).await?;
|
||||
anyhow::Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn edit(
|
||||
&self,
|
||||
new_content: Arc<RoomMessageEventContentWithoutRelation>,
|
||||
edit_item: Arc<EventTimelineItem>,
|
||||
) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async {
|
||||
self.inner.edit((*new_content).clone().with_relation(None), &edit_item.0).await?;
|
||||
anyhow::Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn edit_poll(
|
||||
&self,
|
||||
question: String,
|
||||
answers: Vec<String>,
|
||||
max_selections: u8,
|
||||
poll_kind: PollKind,
|
||||
edit_item: Arc<EventTimelineItem>,
|
||||
) -> Result<(), ClientError> {
|
||||
let poll_data = PollData { question, answers, max_selections, poll_kind };
|
||||
|
||||
RUNTIME.block_on(async {
|
||||
self.inner
|
||||
.edit_poll(poll_data.fallback_text(), poll_data.try_into()?, &edit_item.0)
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_location(
|
||||
self: Arc<Self>,
|
||||
body: String,
|
||||
geo_uri: String,
|
||||
description: Option<String>,
|
||||
zoom_level: Option<u8>,
|
||||
asset_type: Option<AssetType>,
|
||||
) {
|
||||
let mut location_event_message_content =
|
||||
LocationMessageEventContent::new(body, geo_uri.clone());
|
||||
|
||||
if let Some(asset_type) = asset_type {
|
||||
location_event_message_content =
|
||||
location_event_message_content.with_asset_type(RumaAssetType::from(asset_type));
|
||||
}
|
||||
|
||||
let mut location_content = LocationContent::new(geo_uri);
|
||||
location_content.description = description;
|
||||
location_content.zoom_level = zoom_level.and_then(ZoomLevel::new);
|
||||
location_event_message_content.location = Some(location_content);
|
||||
|
||||
let room_message_event_content = RoomMessageEventContentWithoutRelation::new(
|
||||
MessageType::Location(location_event_message_content),
|
||||
);
|
||||
self.send(Arc::new(room_message_event_content))
|
||||
}
|
||||
|
||||
pub fn toggle_reaction(&self, event_id: String, key: String) -> Result<(), ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
RUNTIME.block_on(async {
|
||||
self.inner.toggle_reaction(&Annotation::new(event_id, key)).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn fetch_details_for_event(&self, event_id: String) -> Result<(), ClientError> {
|
||||
let event_id = <&EventId>::try_from(event_id.as_str())?;
|
||||
RUNTIME.block_on(async {
|
||||
self.inner.fetch_details_for_event(event_id).await.context("Fetching event details")?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn retry_send(self: Arc<Self>, txn_id: String) {
|
||||
RUNTIME.spawn(async move {
|
||||
if let Err(e) = self.inner.retry_send(txn_id.as_str().into()).await {
|
||||
error!(txn_id, "Failed to retry sending: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn cancel_send(self: Arc<Self>, txn_id: String) {
|
||||
RUNTIME.spawn(async move {
|
||||
if !self.inner.cancel_send(txn_id.as_str().into()).await {
|
||||
info!(txn_id, "Failed to discard local echo: Not found");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_event_timeline_item_by_event_id(
|
||||
&self,
|
||||
event_id: String,
|
||||
) -> Result<Arc<EventTimelineItem>, ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
RUNTIME.block_on(async {
|
||||
let item = self
|
||||
.inner
|
||||
.item_by_event_id(&event_id)
|
||||
.await
|
||||
.context("Item with given event ID not found")?;
|
||||
|
||||
Ok(Arc::new(EventTimelineItem(item)))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_timeline_event_content_by_event_id(
|
||||
&self,
|
||||
event_id: String,
|
||||
) -> Result<Arc<RoomMessageEventContentWithoutRelation>, ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
RUNTIME.block_on(async {
|
||||
let item = self
|
||||
.inner
|
||||
.item_by_event_id(&event_id)
|
||||
.await
|
||||
.context("Item with given event ID not found")?;
|
||||
|
||||
let msgtype = item
|
||||
.content()
|
||||
.as_message()
|
||||
.context("Item with given event ID is not a message")?
|
||||
.msgtype()
|
||||
.to_owned();
|
||||
|
||||
Ok(Arc::new(RoomMessageEventContentWithoutRelation::new(msgtype)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomTimelineListenerResult {
|
||||
pub items: Vec<Arc<TimelineItem>>,
|
||||
pub items_stream: Arc<TaskHandle>,
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait TimelineListener: Sync + Send {
|
||||
fn on_update(&self, diff: Vec<Arc<TimelineDiff>>);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait BackPaginationStatusListener: Sync + Send {
|
||||
fn on_update(&self, status: BackPaginationStatus);
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub enum TimelineDiff {
|
||||
Append { values: Vec<Arc<TimelineItem>> },
|
||||
Clear,
|
||||
PushFront { value: Arc<TimelineItem> },
|
||||
PushBack { value: Arc<TimelineItem> },
|
||||
PopFront,
|
||||
PopBack,
|
||||
Insert { index: usize, value: Arc<TimelineItem> },
|
||||
Set { index: usize, value: Arc<TimelineItem> },
|
||||
Remove { index: usize },
|
||||
Truncate { length: usize },
|
||||
Reset { values: Vec<Arc<TimelineItem>> },
|
||||
}
|
||||
|
||||
impl TimelineDiff {
|
||||
pub(crate) fn new(inner: VectorDiff<Arc<matrix_sdk_ui::timeline::TimelineItem>>) -> Self {
|
||||
match inner {
|
||||
VectorDiff::Append { values } => {
|
||||
Self::Append { values: values.into_iter().map(TimelineItem::from_arc).collect() }
|
||||
}
|
||||
VectorDiff::Clear => Self::Clear,
|
||||
VectorDiff::Insert { index, value } => {
|
||||
Self::Insert { index, value: TimelineItem::from_arc(value) }
|
||||
}
|
||||
VectorDiff::Set { index, value } => {
|
||||
Self::Set { index, value: TimelineItem::from_arc(value) }
|
||||
}
|
||||
VectorDiff::Truncate { length } => Self::Truncate { length },
|
||||
VectorDiff::Remove { index } => Self::Remove { index },
|
||||
VectorDiff::PushBack { value } => {
|
||||
Self::PushBack { value: TimelineItem::from_arc(value) }
|
||||
}
|
||||
VectorDiff::PushFront { value } => {
|
||||
Self::PushFront { value: TimelineItem::from_arc(value) }
|
||||
}
|
||||
VectorDiff::PopBack => Self::PopBack,
|
||||
VectorDiff::PopFront => Self::PopFront,
|
||||
VectorDiff::Reset { values } => {
|
||||
warn!("Timeline subscriber lagged behind and was reset");
|
||||
Self::Reset { values: values.into_iter().map(TimelineItem::from_arc).collect() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl TimelineDiff {
|
||||
pub fn change(&self) -> TimelineChange {
|
||||
match self {
|
||||
Self::Append { .. } => TimelineChange::Append,
|
||||
Self::Insert { .. } => TimelineChange::Insert,
|
||||
Self::Set { .. } => TimelineChange::Set,
|
||||
Self::Remove { .. } => TimelineChange::Remove,
|
||||
Self::PushBack { .. } => TimelineChange::PushBack,
|
||||
Self::PushFront { .. } => TimelineChange::PushFront,
|
||||
Self::PopBack => TimelineChange::PopBack,
|
||||
Self::PopFront => TimelineChange::PopFront,
|
||||
Self::Clear => TimelineChange::Clear,
|
||||
Self::Truncate { .. } => TimelineChange::Truncate,
|
||||
Self::Reset { .. } => TimelineChange::Reset,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append(self: Arc<Self>) -> Option<Vec<Arc<TimelineItem>>> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
as_variant!(this, Self::Append { values } => values)
|
||||
}
|
||||
|
||||
pub fn insert(self: Arc<Self>) -> Option<InsertData> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
as_variant!(this, Self::Insert { index, value } => {
|
||||
InsertData { index: index.try_into().unwrap(), item: value }
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set(self: Arc<Self>) -> Option<SetData> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
as_variant!(this, Self::Set { index, value } => {
|
||||
SetData { index: index.try_into().unwrap(), item: value }
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remove(&self) -> Option<u32> {
|
||||
as_variant!(self, Self::Remove { index } => (*index).try_into().unwrap())
|
||||
}
|
||||
|
||||
pub fn push_back(self: Arc<Self>) -> Option<Arc<TimelineItem>> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
as_variant!(this, Self::PushBack { value } => value)
|
||||
}
|
||||
|
||||
pub fn push_front(self: Arc<Self>) -> Option<Arc<TimelineItem>> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
as_variant!(this, Self::PushFront { value } => value)
|
||||
}
|
||||
|
||||
pub fn reset(self: Arc<Self>) -> Option<Vec<Arc<TimelineItem>>> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
as_variant!(this, Self::Reset { values } => values)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct InsertData {
|
||||
pub index: u32,
|
||||
pub item: Arc<TimelineItem>,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct SetData {
|
||||
pub index: u32,
|
||||
pub item: Arc<TimelineItem>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, uniffi::Enum)]
|
||||
pub enum TimelineChange {
|
||||
Append,
|
||||
Clear,
|
||||
Insert,
|
||||
Set,
|
||||
Remove,
|
||||
PushBack,
|
||||
PushFront,
|
||||
PopBack,
|
||||
PopFront,
|
||||
Truncate,
|
||||
Reset,
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct TimelineItem(pub(crate) matrix_sdk_ui::timeline::TimelineItem);
|
||||
|
||||
impl TimelineItem {
|
||||
pub(crate) fn from_arc(arc: Arc<matrix_sdk_ui::timeline::TimelineItem>) -> Arc<Self> {
|
||||
// SAFETY: This is valid because Self is a repr(transparent) wrapper
|
||||
// around the other Timeline type.
|
||||
unsafe { Arc::from_raw(Arc::into_raw(arc) as _) }
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl TimelineItem {
|
||||
pub fn as_event(self: Arc<Self>) -> Option<Arc<EventTimelineItem>> {
|
||||
let event_item = self.0.as_event()?;
|
||||
Some(Arc::new(EventTimelineItem(event_item.clone())))
|
||||
}
|
||||
|
||||
pub fn as_virtual(self: Arc<Self>) -> Option<VirtualTimelineItem> {
|
||||
use matrix_sdk_ui::timeline::VirtualTimelineItem as VItem;
|
||||
match self.0.as_virtual()? {
|
||||
VItem::DayDivider(ts) => Some(VirtualTimelineItem::DayDivider { ts: ts.0.into() }),
|
||||
VItem::ReadMarker => Some(VirtualTimelineItem::ReadMarker),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unique_id(&self) -> u64 {
|
||||
self.0.unique_id()
|
||||
}
|
||||
|
||||
pub fn fmt_debug(&self) -> String {
|
||||
format!("{:#?}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// This type represents the “send state” of a local event timeline item.
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum EventSendState {
|
||||
/// The local event has not been sent yet.
|
||||
NotSentYet,
|
||||
/// The local event has been sent to the server, but unsuccessfully: The
|
||||
/// sending has failed.
|
||||
SendingFailed { error: String },
|
||||
/// Sending has been cancelled because an earlier event in the
|
||||
/// message-sending queue failed.
|
||||
Cancelled,
|
||||
/// The local event has been sent successfully to the server.
|
||||
Sent { event_id: String },
|
||||
}
|
||||
|
||||
impl From<&matrix_sdk_ui::timeline::EventSendState> for EventSendState {
|
||||
fn from(value: &matrix_sdk_ui::timeline::EventSendState) -> Self {
|
||||
use matrix_sdk_ui::timeline::EventSendState::*;
|
||||
|
||||
match value {
|
||||
NotSentYet => Self::NotSentYet,
|
||||
SendingFailed { error } => Self::SendingFailed { error: error.to_string() },
|
||||
Cancelled => Self::Cancelled,
|
||||
Sent { event_id } => Self::Sent { event_id: event_id.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct EventTimelineItem(pub(crate) matrix_sdk_ui::timeline::EventTimelineItem);
|
||||
|
||||
#[uniffi::export]
|
||||
impl EventTimelineItem {
|
||||
pub fn is_local(&self) -> bool {
|
||||
self.0.is_local_echo()
|
||||
}
|
||||
|
||||
pub fn is_remote(&self) -> bool {
|
||||
!self.0.is_local_echo()
|
||||
}
|
||||
|
||||
pub fn transaction_id(&self) -> Option<String> {
|
||||
self.0.transaction_id().map(ToString::to_string)
|
||||
}
|
||||
|
||||
pub fn event_id(&self) -> Option<String> {
|
||||
self.0.event_id().map(ToString::to_string)
|
||||
}
|
||||
|
||||
pub fn sender(&self) -> String {
|
||||
self.0.sender().to_string()
|
||||
}
|
||||
|
||||
pub fn sender_profile(&self) -> ProfileDetails {
|
||||
self.0.sender_profile().into()
|
||||
}
|
||||
|
||||
pub fn is_own(&self) -> bool {
|
||||
self.0.is_own()
|
||||
}
|
||||
|
||||
pub fn is_editable(&self) -> bool {
|
||||
self.0.is_editable()
|
||||
}
|
||||
|
||||
pub fn content(&self) -> Arc<TimelineItemContent> {
|
||||
Arc::new(TimelineItemContent(self.0.content().clone()))
|
||||
}
|
||||
|
||||
pub fn timestamp(&self) -> u64 {
|
||||
self.0.timestamp().0.into()
|
||||
}
|
||||
|
||||
pub fn reactions(&self) -> Vec<Reaction> {
|
||||
self.0
|
||||
.reactions()
|
||||
.iter()
|
||||
.map(|(k, v)| Reaction {
|
||||
key: k.to_owned(),
|
||||
count: v.len().try_into().unwrap(),
|
||||
senders: v
|
||||
.senders()
|
||||
.map(|v| ReactionSenderData {
|
||||
sender_id: v.sender_id.to_string(),
|
||||
timestamp: v.timestamp.0.into(),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn debug_info(&self) -> EventTimelineItemDebugInfo {
|
||||
EventTimelineItemDebugInfo {
|
||||
model: format!("{:#?}", self.0),
|
||||
original_json: self.0.original_json().map(|raw| raw.json().get().to_owned()),
|
||||
latest_edit_json: self.0.latest_edit_json().map(|raw| raw.json().get().to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn local_send_state(&self) -> Option<EventSendState> {
|
||||
self.0.send_state().map(Into::into)
|
||||
}
|
||||
|
||||
pub fn read_receipts(&self) -> HashMap<String, Receipt> {
|
||||
self.0.read_receipts().iter().map(|(k, v)| (k.to_string(), v.clone().into())).collect()
|
||||
}
|
||||
|
||||
pub fn origin(&self) -> Option<EventItemOrigin> {
|
||||
self.0.origin()
|
||||
}
|
||||
|
||||
pub fn can_be_replied_to(&self) -> bool {
|
||||
self.0.can_be_replied_to()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct Receipt {
|
||||
pub timestamp: Option<u64>,
|
||||
}
|
||||
|
||||
impl From<ruma::events::receipt::Receipt> for Receipt {
|
||||
fn from(value: ruma::events::receipt::Receipt) -> Self {
|
||||
Receipt { timestamp: value.ts.map(|ts| ts.0.into()) }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct EventTimelineItemDebugInfo {
|
||||
model: String,
|
||||
original_json: Option<String>,
|
||||
latest_edit_json: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum ProfileDetails {
|
||||
Unavailable,
|
||||
Pending,
|
||||
Ready { display_name: Option<String>, display_name_ambiguous: bool, avatar_url: Option<String> },
|
||||
Error { message: String },
|
||||
}
|
||||
|
||||
impl From<&TimelineDetails<Profile>> for ProfileDetails {
|
||||
fn from(details: &TimelineDetails<Profile>) -> Self {
|
||||
match details {
|
||||
TimelineDetails::Unavailable => Self::Unavailable,
|
||||
TimelineDetails::Pending => Self::Pending,
|
||||
TimelineDetails::Ready(profile) => Self::Ready {
|
||||
display_name: profile.display_name.clone(),
|
||||
display_name_ambiguous: profile.display_name_ambiguous,
|
||||
avatar_url: profile.avatar_url.as_ref().map(ToString::to_string),
|
||||
},
|
||||
TimelineDetails::Error(e) => Self::Error { message: e.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PollData {
|
||||
question: String,
|
||||
answers: Vec<String>,
|
||||
max_selections: u8,
|
||||
poll_kind: PollKind,
|
||||
}
|
||||
|
||||
impl PollData {
|
||||
fn fallback_text(&self) -> String {
|
||||
self.answers.iter().enumerate().fold(self.question.clone(), |mut acc, (index, answer)| {
|
||||
write!(&mut acc, "\n{}. {answer}", index + 1).unwrap();
|
||||
acc
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<PollData> for UnstablePollStartContentBlock {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: PollData) -> Result<Self, Self::Error> {
|
||||
let poll_answers_vec: Vec<UnstablePollAnswer> = value
|
||||
.answers
|
||||
.iter()
|
||||
.map(|answer| UnstablePollAnswer::new(Uuid::new_v4().to_string(), answer))
|
||||
.collect();
|
||||
|
||||
let poll_answers = UnstablePollAnswers::try_from(poll_answers_vec)
|
||||
.context("Failed to create poll answers")?;
|
||||
|
||||
let mut poll_content_block =
|
||||
UnstablePollStartContentBlock::new(value.question.clone(), poll_answers);
|
||||
poll_content_block.kind = value.poll_kind.into();
|
||||
poll_content_block.max_selections = value.max_selections.into();
|
||||
|
||||
Ok(poll_content_block)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SendAttachmentJoinHandle {
|
||||
join_hdl: Arc<Mutex<JoinHandle<Result<(), RoomError>>>>,
|
||||
abort_hdl: AbortHandle,
|
||||
}
|
||||
|
||||
impl SendAttachmentJoinHandle {
|
||||
fn new(join_hdl: JoinHandle<Result<(), RoomError>>) -> Arc<Self> {
|
||||
let abort_hdl = join_hdl.abort_handle();
|
||||
let join_hdl = Arc::new(Mutex::new(join_hdl));
|
||||
Arc::new(Self { join_hdl, abort_hdl })
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl SendAttachmentJoinHandle {
|
||||
pub async fn join(&self) -> Result<(), RoomError> {
|
||||
let join_hdl = self.join_hdl.clone();
|
||||
RUNTIME.spawn(async move { (&mut *join_hdl.lock().await).await.unwrap() }).await.unwrap()
|
||||
}
|
||||
|
||||
pub fn cancel(&self) {
|
||||
self.abort_hdl.abort();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum PaginationOptions {
|
||||
SimpleRequest { event_limit: u16, wait_for_token: bool },
|
||||
UntilNumItems { event_limit: u16, items: u16, wait_for_token: bool },
|
||||
}
|
||||
|
||||
impl From<PaginationOptions> for matrix_sdk_ui::timeline::PaginationOptions<'static> {
|
||||
fn from(value: PaginationOptions) -> Self {
|
||||
use matrix_sdk_ui::timeline::PaginationOptions as Opts;
|
||||
let (wait_for_token, mut opts) = match value {
|
||||
PaginationOptions::SimpleRequest { event_limit, wait_for_token } => {
|
||||
(wait_for_token, Opts::simple_request(event_limit))
|
||||
}
|
||||
PaginationOptions::UntilNumItems { event_limit, items, wait_for_token } => {
|
||||
(wait_for_token, Opts::until_num_items(event_limit, items))
|
||||
}
|
||||
};
|
||||
|
||||
if wait_for_token {
|
||||
opts = opts.wait_for_token();
|
||||
}
|
||||
|
||||
opts
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`TimelineItem`](super::TimelineItem) that doesn't correspond to an event.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum VirtualTimelineItem {
|
||||
/// A divider between messages of two days.
|
||||
DayDivider {
|
||||
/// A timestamp in milliseconds since Unix Epoch on that day in local
|
||||
/// time.
|
||||
ts: u64,
|
||||
},
|
||||
|
||||
/// The user's own read marker.
|
||||
ReadMarker,
|
||||
}
|
||||
|
||||
/// A [`TimelineItem`](super::TimelineItem) that doesn't correspond to an event.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum ReceiptType {
|
||||
Read,
|
||||
ReadPrivate,
|
||||
FullyRead,
|
||||
}
|
||||
|
||||
impl From<ReceiptType> for ruma::api::client::receipt::create_receipt::v3::ReceiptType {
|
||||
fn from(value: ReceiptType) -> Self {
|
||||
match value {
|
||||
ReceiptType::Read => Self::Read,
|
||||
ReceiptType::ReadPrivate => Self::Read,
|
||||
ReceiptType::FullyRead => Self::FullyRead,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use ruma::UInt;
|
||||
use tracing::warn;
|
||||
|
||||
pub(crate) fn u64_to_uint(u: u64) -> UInt {
|
||||
UInt::new(u).unwrap_or_else(|| {
|
||||
warn!("u64 -> UInt conversion overflowed, falling back to UInt::MAX");
|
||||
UInt::MAX
|
||||
})
|
||||
}
|
||||
@@ -111,6 +111,34 @@ pub async fn generate_webview_url(
|
||||
.map(|url| url.to_string())?)
|
||||
}
|
||||
|
||||
/// Defines if a call is encrypted and which encryption system should be used.
|
||||
///
|
||||
/// This controls the url parameters: `perParticipantE2EE`, `password`.
|
||||
#[derive(uniffi::Enum, Clone)]
|
||||
pub enum EncryptionSystem {
|
||||
/// Equivalent to the element call url parameter: `enableE2EE=false`
|
||||
Unencrypted,
|
||||
/// Equivalent to the element call url parameter:
|
||||
/// `perParticipantE2EE=true`
|
||||
PerParticipantKeys,
|
||||
/// Equivalent to the element call url parameter:
|
||||
/// `password={secret}`
|
||||
SharedSecret {
|
||||
/// The secret/password which is used in the url.
|
||||
secret: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<EncryptionSystem> for matrix_sdk::widget::EncryptionSystem {
|
||||
fn from(value: EncryptionSystem) -> Self {
|
||||
match value {
|
||||
EncryptionSystem::Unencrypted => Self::Unencrypted,
|
||||
EncryptionSystem::PerParticipantKeys => Self::PerParticipantKeys,
|
||||
EncryptionSystem::SharedSecret { secret } => Self::SharedSecret { secret },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Properties to create a new virtual Element Call widget.
|
||||
#[derive(uniffi::Record, Clone)]
|
||||
pub struct VirtualElementCallWidgetOptions {
|
||||
@@ -174,6 +202,11 @@ pub struct VirtualElementCallWidgetOptions {
|
||||
|
||||
/// Can be used to pass a PostHog id to element call.
|
||||
pub analytics_id: Option<String>,
|
||||
|
||||
/// The encryption system to use.
|
||||
///
|
||||
/// Use `EncryptionSystem::Unencrypted` to disable encryption.
|
||||
pub encryption: EncryptionSystem,
|
||||
}
|
||||
|
||||
impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElementCallWidgetOptions {
|
||||
@@ -190,6 +223,7 @@ impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElemen
|
||||
confine_to_room: value.confine_to_room,
|
||||
font: value.font,
|
||||
analytics_id: value.analytics_id,
|
||||
encryption: value.encryption.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,6 +247,46 @@ pub fn new_virtual_element_call_widget(
|
||||
.map(|w| w.into())?)
|
||||
}
|
||||
|
||||
/// The Capabilities required to run a element call widget.
|
||||
///
|
||||
/// This is intended to be used in combination with: `acquire_capabilities` of
|
||||
/// the `CapabilitiesProvider`.
|
||||
///
|
||||
/// `acquire_capabilities` can simply return the `WidgetCapabilities` from this
|
||||
/// function. Even if there are non intersecting permissions to what the widget
|
||||
/// requested.
|
||||
///
|
||||
/// Editing and extending the capabilities from this function is also possible,
|
||||
/// but should only be done as temporal workarounds until this function is
|
||||
/// adjusted
|
||||
#[uniffi::export]
|
||||
pub fn get_element_call_required_permissions() -> WidgetCapabilities {
|
||||
use ruma::events::StateEventType;
|
||||
|
||||
WidgetCapabilities {
|
||||
read: vec![
|
||||
WidgetEventFilter::StateWithType { event_type: StateEventType::CallMember.to_string() },
|
||||
WidgetEventFilter::StateWithType { event_type: StateEventType::RoomMember.to_string() },
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "org.matrix.rageshake_request".to_owned(),
|
||||
},
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "io.element.call.encryption_keys".to_owned(),
|
||||
},
|
||||
],
|
||||
send: vec![
|
||||
WidgetEventFilter::StateWithType { event_type: StateEventType::CallMember.to_string() },
|
||||
WidgetEventFilter::StateWithType {
|
||||
event_type: "org.matrix.rageshake_request".to_owned(),
|
||||
},
|
||||
WidgetEventFilter::StateWithType {
|
||||
event_type: "io.element.call.encryption_keys".to_owned(),
|
||||
},
|
||||
],
|
||||
requires_client: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct ClientProperties {
|
||||
/// The client_id provides the widget with the option to behave differently
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"rust-analyzer.checkOnSave.command": "clippy",
|
||||
"rust-analyzer.rustfmt.extraArgs": ["+nightly"]
|
||||
}
|
||||
@@ -36,7 +36,7 @@ FAILURES = {
|
||||
"Success": (lambda x: None, 50),
|
||||
"Gateway error":
|
||||
(lambda _: http.Response.make(500, b"Gateway error"), 20),
|
||||
"Limit exeeded": (lambda _: http.Response.make(
|
||||
"Limit exceeded": (lambda _: http.Response.make(
|
||||
429,
|
||||
json.dumps({
|
||||
"errcode": "M_LIMIT_EXCEEDED",
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
# Changelog
|
||||
|
||||
## unreleased
|
||||
# 0.7.0
|
||||
|
||||
- Rename `RoomType` to `RoomState`
|
||||
- Add `RoomInfo::state` accessor
|
||||
@@ -33,9 +31,9 @@
|
||||
- Move `Session`, `SessionTokens` and associated methods to the `matrix-sdk` crate.
|
||||
- Add `Room::subscribe_info`
|
||||
|
||||
## 0.5.1
|
||||
# 0.5.1
|
||||
|
||||
### Bug Fixes
|
||||
## Bug Fixes
|
||||
- #664: Fix regression with push rules being applied to the own user_id only instead of all but the own user_id
|
||||
|
||||
## 0.5.0
|
||||
# 0.5.0
|
||||
@@ -9,7 +9,7 @@ name = "matrix-sdk-base"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = { workspace = true }
|
||||
version = "0.6.1"
|
||||
version = "0.7.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
@@ -40,12 +40,13 @@ assert_matches2 = { workspace = true, optional = true }
|
||||
async-trait = { workspace = true }
|
||||
bitflags = "2.1.0"
|
||||
eyeball = { workspace = true }
|
||||
eyeball-im = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
http = { workspace = true, optional = true }
|
||||
matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" }
|
||||
matrix-sdk-crypto = { version = "0.6.0", path = "../matrix-sdk-crypto", optional = true, default-features = false }
|
||||
matrix-sdk-store-encryption = { version = "0.2.0", path = "../matrix-sdk-store-encryption" }
|
||||
matrix-sdk-test = { version = "0.6.0", path = "../../testing/matrix-sdk-test", optional = true }
|
||||
matrix-sdk-common = { workspace = true }
|
||||
matrix-sdk-crypto = { workspace = true, optional = true }
|
||||
matrix-sdk-store-encryption = { workspace = true }
|
||||
matrix-sdk-test = { workspace = true, optional = true }
|
||||
once_cell = { workspace = true }
|
||||
ruma = { workspace = true, features = ["canonical-json", "unstable-msc3381"] }
|
||||
serde = { workspace = true, features = ["rc"] }
|
||||
@@ -58,11 +59,10 @@ tracing = { workspace = true }
|
||||
assert_matches = { workspace = true }
|
||||
assert_matches2 = { workspace = true }
|
||||
assign = "1.1.1"
|
||||
ctor = { workspace = true }
|
||||
futures-executor = { workspace = true }
|
||||
http = { workspace = true }
|
||||
matrix-sdk-test = { version = "0.6.0", path = "../../testing/matrix-sdk-test" }
|
||||
tracing-subscriber = { version = "0.3.11", features = ["env-filter"] }
|
||||
matrix-sdk-test = { workspace = true }
|
||||
stream_assert = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
@@ -46,7 +46,7 @@ use ruma::{
|
||||
},
|
||||
push::{Action, PushConditionRoomCtx, Ruleset},
|
||||
serde::Raw,
|
||||
MilliSecondsSinceUnixEpoch, OwnedUserId, RoomId, RoomVersionId, UInt, UserId,
|
||||
MilliSecondsSinceUnixEpoch, OwnedRoomId, OwnedUserId, RoomId, RoomVersionId, UInt, UserId,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
@@ -283,6 +283,7 @@ impl BaseClient {
|
||||
user_ids: &mut BTreeSet<OwnedUserId>,
|
||||
room_info: &mut RoomInfo,
|
||||
changes: &mut StateChanges,
|
||||
notifications: &mut BTreeMap<OwnedRoomId, Vec<Notification>>,
|
||||
ambiguity_cache: &mut AmbiguityCache,
|
||||
) -> Result<Timeline> {
|
||||
let mut timeline = Timeline::new(limited, prev_batch);
|
||||
@@ -397,8 +398,7 @@ impl BaseClient {
|
||||
let actions = push_rules.get_actions(&event.event, context);
|
||||
|
||||
if actions.iter().any(Action::should_notify) {
|
||||
changes.add_notification(
|
||||
room.room_id(),
|
||||
notifications.entry(room.room_id().to_owned()).or_default().push(
|
||||
Notification::new(
|
||||
actions.to_owned(),
|
||||
event.event.clone(),
|
||||
@@ -541,8 +541,7 @@ impl BaseClient {
|
||||
for (user_id, rooms) in e.content.iter() {
|
||||
for room_id in rooms {
|
||||
trace!(
|
||||
room_id = room_id.as_str(),
|
||||
target = user_id.as_str(),
|
||||
?room_id, target = ?user_id,
|
||||
"Marking room as direct room"
|
||||
);
|
||||
|
||||
@@ -608,8 +607,7 @@ impl BaseClient {
|
||||
// event. If we found one, set it as the latest and delete any older
|
||||
// encrypted events
|
||||
if let Some((found, found_index)) = self.decrypt_latest_suitable_event(room).await {
|
||||
room.on_latest_event_decrypted(found, found_index);
|
||||
changes.room_infos.insert(room.room_id().to_owned(), room.clone_info());
|
||||
room.on_latest_event_decrypted(found, found_index, changes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -620,12 +618,21 @@ impl BaseClient {
|
||||
/// decrypted event if we found one, along with its index in the
|
||||
/// latest_encrypted_events list, or None if we didn't find one.
|
||||
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
|
||||
async fn decrypt_latest_suitable_event(&self, room: &Room) -> Option<(LatestEvent, usize)> {
|
||||
async fn decrypt_latest_suitable_event(
|
||||
&self,
|
||||
room: &Room,
|
||||
) -> Option<(Box<LatestEvent>, usize)> {
|
||||
let enc_events = room.latest_encrypted_events();
|
||||
|
||||
// Walk backwards through the encrypted events, looking for one we can decrypt
|
||||
for (i, event) in enc_events.iter().enumerate().rev() {
|
||||
if let Ok(Some(decrypted)) = self.decrypt_sync_room_event(event, room.room_id()).await {
|
||||
// Size of the decrypt_sync_room_event future should not impact this
|
||||
// async fn since it is likely that there aren't even any encrypted
|
||||
// events when calling it.
|
||||
let decrypt_sync_room_event =
|
||||
Box::pin(self.decrypt_sync_room_event(event, room.room_id()));
|
||||
|
||||
if let Ok(Some(decrypted)) = decrypt_sync_room_event.await {
|
||||
// We found an event we can decrypt
|
||||
if let Ok(any_sync_event) = decrypted.event.deserialize() {
|
||||
// We can deserialize it to find its type
|
||||
@@ -633,7 +640,7 @@ impl BaseClient {
|
||||
is_suitable_for_latest_event(&any_sync_event)
|
||||
{
|
||||
// The event is the right type for us to use as latest_event
|
||||
return Some((LatestEvent::new(decrypted), i));
|
||||
return Some((Box::new(LatestEvent::new(decrypted)), i));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -656,7 +663,7 @@ impl BaseClient {
|
||||
let mut changes = StateChanges::default();
|
||||
changes.add_room(room_info.clone());
|
||||
self.store.save_changes(&changes).await?; // Update the store
|
||||
room.update_summary(room_info); // Update the cached room handle
|
||||
room.set_room_info(room_info); // Update the cached room handle
|
||||
}
|
||||
|
||||
Ok(room)
|
||||
@@ -677,7 +684,7 @@ impl BaseClient {
|
||||
let mut changes = StateChanges::default();
|
||||
changes.add_room(room_info.clone());
|
||||
self.store.save_changes(&changes).await?; // Update the store
|
||||
room.update_summary(room_info); // Update the cached room handle
|
||||
room.set_room_info(room_info); // Update the cached room handle
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -733,6 +740,7 @@ impl BaseClient {
|
||||
let push_rules = self.get_push_rules(&changes).await?;
|
||||
|
||||
let mut new_rooms = Rooms::default();
|
||||
let mut notifications = Default::default();
|
||||
|
||||
for (room_id, new_info) in response.rooms.join {
|
||||
let room = self.store.get_or_create_room(&room_id, RoomState::Joined);
|
||||
@@ -788,6 +796,7 @@ impl BaseClient {
|
||||
&mut user_ids,
|
||||
&mut room_info,
|
||||
&mut changes,
|
||||
&mut notifications,
|
||||
&mut ambiguity_cache,
|
||||
)
|
||||
.await?;
|
||||
@@ -858,6 +867,7 @@ impl BaseClient {
|
||||
&mut user_ids,
|
||||
&mut room_info,
|
||||
&mut changes,
|
||||
&mut notifications,
|
||||
&mut ambiguity_cache,
|
||||
)
|
||||
.await?;
|
||||
@@ -906,7 +916,7 @@ impl BaseClient {
|
||||
let sync_lock = self.sync_lock().write().await;
|
||||
self.store.save_changes(&changes).await?;
|
||||
*self.store.sync_token.write().await = Some(response.next_batch.clone());
|
||||
self.apply_changes(&changes).await;
|
||||
self.apply_changes(&changes);
|
||||
drop(sync_lock);
|
||||
|
||||
info!("Processed a sync response in {:?}", now.elapsed());
|
||||
@@ -917,20 +927,20 @@ impl BaseClient {
|
||||
account_data: response.account_data.events,
|
||||
to_device,
|
||||
ambiguity_changes: AmbiguityChanges { changes: ambiguity_cache.changes },
|
||||
notifications: changes.notifications,
|
||||
notifications,
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub(crate) async fn apply_changes(&self, changes: &StateChanges) {
|
||||
pub(crate) fn apply_changes(&self, changes: &StateChanges) {
|
||||
if changes.account_data.contains_key(&GlobalAccountDataEventType::IgnoredUserList) {
|
||||
self.ignore_user_list_changes.set(());
|
||||
}
|
||||
|
||||
for (room_id, room_info) in &changes.room_infos {
|
||||
if let Some(room) = self.store.get_room(room_id) {
|
||||
room.update_summary(room_info.clone())
|
||||
room.set_room_info(room_info.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1024,7 +1034,7 @@ impl BaseClient {
|
||||
changes.add_room(room_info);
|
||||
|
||||
self.store.save_changes(&changes).await?;
|
||||
self.apply_changes(&changes).await;
|
||||
self.apply_changes(&changes);
|
||||
}
|
||||
|
||||
Ok(MembersResponse {
|
||||
|
||||
@@ -27,6 +27,7 @@ use ruma::{
|
||||
/// that only prints the event type of each item.
|
||||
pub struct DebugListOfRawEventsNoId<'a, T>(pub &'a [Raw<T>]);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a, T> fmt::Debug for DebugListOfRawEventsNoId<'a, T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut list = f.debug_list();
|
||||
|
||||
@@ -30,8 +30,12 @@ mod error;
|
||||
pub mod latest_event;
|
||||
pub mod media;
|
||||
mod rooms;
|
||||
|
||||
mod read_receipts;
|
||||
pub use read_receipts::PreviousEventsProvider;
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
mod sliding_sync;
|
||||
|
||||
pub mod store;
|
||||
pub mod sync;
|
||||
mod utils;
|
||||
@@ -51,15 +55,8 @@ pub use utils::{
|
||||
MinimalRoomMemberEvent, MinimalStateEvent, OriginalMinimalStateEvent, RedactedMinimalStateEvent,
|
||||
};
|
||||
|
||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||
#[ctor::ctor]
|
||||
fn init_logging() {
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::EnvFilter::from_default_env())
|
||||
.with(tracing_subscriber::fmt::layer().with_test_writer())
|
||||
.init();
|
||||
}
|
||||
#[cfg(test)]
|
||||
matrix_sdk_test::init_tracing_for_tests!();
|
||||
|
||||
/// The Matrix user session info.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
|
||||
|
||||
@@ -12,7 +12,7 @@ use ruma::{
|
||||
},
|
||||
sticker::StickerEventContent,
|
||||
},
|
||||
UInt,
|
||||
MxcUri, UInt,
|
||||
};
|
||||
|
||||
const UNIQUE_SEPARATOR: &str = "_";
|
||||
@@ -83,11 +83,22 @@ pub struct MediaRequest {
|
||||
pub format: MediaFormat,
|
||||
}
|
||||
|
||||
impl MediaRequest {
|
||||
/// Get the [`MxcUri`] from `Self`.
|
||||
pub fn uri(&self) -> &MxcUri {
|
||||
match &self.source {
|
||||
MediaSource::Plain(url) => url.as_ref(),
|
||||
MediaSource::Encrypted(file) => file.url.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UniqueKey for MediaRequest {
|
||||
fn unique_key(&self) -> String {
|
||||
format!("{}{UNIQUE_SEPARATOR}{}", self.source.unique_key(), self.format.unique_key())
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for media event content.
|
||||
pub trait MediaEventContent {
|
||||
/// Get the source of the file for `Self`.
|
||||
@@ -166,3 +177,47 @@ impl MediaEventContent for LocationMessageEventContent {
|
||||
self.info.as_ref()?.thumbnail_source.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruma::mxc_uri;
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_media_request_url() {
|
||||
let mxc_uri = mxc_uri!("mxc://homeserver/media");
|
||||
|
||||
let plain = MediaRequest {
|
||||
source: MediaSource::Plain(mxc_uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
|
||||
assert_eq!(plain.uri(), mxc_uri);
|
||||
|
||||
let file = MediaRequest {
|
||||
source: MediaSource::Encrypted(Box::new(
|
||||
serde_json::from_value(json!({
|
||||
"url": mxc_uri,
|
||||
"key": {
|
||||
"kty": "oct",
|
||||
"key_ops": ["encrypt", "decrypt"],
|
||||
"alg": "A256CTR",
|
||||
"k": "b50ACIv6LMn9AfMCFD1POJI_UAFWIclxAN1kWrEO2X8",
|
||||
"ext": true,
|
||||
},
|
||||
"iv": "AK1wyzigZtQAAAABAAAAKK",
|
||||
"hashes": {
|
||||
"sha256": "foobar",
|
||||
},
|
||||
"v": "v2",
|
||||
}))
|
||||
.unwrap(),
|
||||
)),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
|
||||
assert_eq!(file.uri(), mxc_uri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,658 @@
|
||||
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! # Client-side read receipts computation
|
||||
//!
|
||||
//! While Matrix servers have the ability to provide basic information about the
|
||||
//! unread status of rooms, via [`matrix_sdk::ruma::UnreadNotificationCounts`],
|
||||
//! it's not reliable for encrypted rooms. Indeed, the server doesn't have
|
||||
//! access to the content of encrypted events, so it can only makes guesses when
|
||||
//! estimating unread and highlight counts.
|
||||
//!
|
||||
//! Instead, this module provides facilities to compute the number of unread
|
||||
//! messages, unread notifications and unread highlights in a room.
|
||||
//!
|
||||
//! Counting unread messages is performed by looking at the latest receipt of
|
||||
//! the current user, and inferring which events are following it, according to
|
||||
//! the sync ordering.
|
||||
//!
|
||||
//! For notifications and highlights to be precisely accounted for, we also need
|
||||
//! to pay attention to the user's notification settings. Fortunately, this is
|
||||
//! also something we need to for notifications, so we can reuse this code.
|
||||
//!
|
||||
//! Of course, not all events are created equal, and some are less interesting
|
||||
//! than others, and shouldn't cause a room to be marked unread. This module's
|
||||
//! `marks_as_unread` function shows the opiniated set of rules that will filter
|
||||
//! out uninterested events.
|
||||
//!
|
||||
//! The only public method in that module is [`compute_notifications`], which
|
||||
//! updates the `RoomInfo` in place according to the new counts.
|
||||
#![allow(dead_code)] // too many different build configurations, I give up
|
||||
|
||||
use eyeball_im::Vector;
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
use ruma::{
|
||||
events::{
|
||||
poll::{start::PollStartEventContent, unstable_start::UnstablePollStartEventContent},
|
||||
receipt::{ReceiptEventContent, ReceiptThread, ReceiptType},
|
||||
room::message::Relation,
|
||||
AnySyncMessageLikeEvent, AnySyncTimelineEvent, OriginalSyncMessageLikeEvent,
|
||||
SyncMessageLikeEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
EventId, OwnedEventId, RoomId, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{instrument, trace};
|
||||
|
||||
use crate::error::Result;
|
||||
|
||||
/// Information about read receipts collected during processing of that room.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
pub(crate) struct RoomReadReceipts {
|
||||
/// Does the room have unread messages?
|
||||
pub num_unread: u64,
|
||||
|
||||
/// Does the room have unread events that should notify?
|
||||
pub num_notifications: u64,
|
||||
|
||||
/// Does the room have messages causing highlights for the users? (aka
|
||||
/// mentions)
|
||||
pub num_mentions: u64,
|
||||
|
||||
/// The id of the event the last unthreaded (or main-threaded, for better
|
||||
/// compatibility with clients that have thread support) read receipt is
|
||||
/// attached to.
|
||||
latest_read_receipt_event_id: Option<OwnedEventId>,
|
||||
}
|
||||
|
||||
impl RoomReadReceipts {
|
||||
/// Update the [`RoomReadReceipts`] unread counts according to the new
|
||||
/// event.
|
||||
///
|
||||
/// Returns whether a new event triggered a new unread/notification/mention.
|
||||
#[inline(always)]
|
||||
fn update_for_event(&mut self, event: &SyncTimelineEvent, user_id: &UserId) -> bool {
|
||||
let mut has_unread = false;
|
||||
|
||||
if marks_as_unread(&event.event, user_id) {
|
||||
self.num_unread += 1;
|
||||
has_unread = true
|
||||
}
|
||||
|
||||
let mut has_notify = false;
|
||||
let mut has_mention = false;
|
||||
|
||||
for action in &event.push_actions {
|
||||
if !has_notify && action.should_notify() {
|
||||
self.num_notifications += 1;
|
||||
has_notify = true;
|
||||
}
|
||||
if !has_mention && action.is_highlight() {
|
||||
self.num_mentions += 1;
|
||||
has_mention = true;
|
||||
}
|
||||
}
|
||||
|
||||
has_unread || has_notify || has_mention
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn reset(&mut self) {
|
||||
self.num_unread = 0;
|
||||
self.num_notifications = 0;
|
||||
self.num_mentions = 0;
|
||||
}
|
||||
|
||||
/// Try to find the event to which the receipt attaches to, and if found,
|
||||
/// will update the notification count in the room.
|
||||
fn find_and_count_events<'a>(
|
||||
&mut self,
|
||||
receipt_event_id: &EventId,
|
||||
user_id: &UserId,
|
||||
events: impl IntoIterator<Item = &'a SyncTimelineEvent>,
|
||||
) -> bool {
|
||||
let mut counting_receipts = false;
|
||||
|
||||
for event in events {
|
||||
if counting_receipts {
|
||||
self.update_for_event(event, user_id);
|
||||
} else if let Ok(Some(event_id)) = event.event.get_field::<OwnedEventId>("event_id") {
|
||||
if event_id == receipt_event_id {
|
||||
// Bingo! Switch over to the counting state, after resetting the
|
||||
// previous counts.
|
||||
trace!("Found the event the receipt was referring to! Starting to count.");
|
||||
self.reset();
|
||||
counting_receipts = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
counting_receipts
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider for timeline events prior to the current sync.
|
||||
pub trait PreviousEventsProvider: Send + Sync {
|
||||
/// Returns the list of known timeline events, in sync order, for the given
|
||||
/// room.
|
||||
fn for_room(&self, room_id: &RoomId) -> Vector<SyncTimelineEvent>;
|
||||
}
|
||||
|
||||
impl PreviousEventsProvider for () {
|
||||
fn for_room(&self, _: &RoomId) -> Vector<SyncTimelineEvent> {
|
||||
Vector::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a set of events coming from sync, for a room, update the
|
||||
/// [`RoomReadReceipts`]'s counts of unread messages, notifications and
|
||||
/// highlights' in place.
|
||||
///
|
||||
/// A provider of previous events may be required to reconcile a read receipt
|
||||
/// that has been just received for an event that came in a previous sync.
|
||||
///
|
||||
/// See this module's documentation for more information.
|
||||
///
|
||||
/// Returns a boolean indicating if a field changed value in the read receipts.
|
||||
#[instrument(skip_all, fields(room_id = %room_id, ?read_receipts))]
|
||||
pub(crate) fn compute_notifications<PEP: PreviousEventsProvider>(
|
||||
user_id: &UserId,
|
||||
room_id: &RoomId,
|
||||
receipt_event: Option<&ReceiptEventContent>,
|
||||
previous_events_provider: &PEP,
|
||||
new_events: &[SyncTimelineEvent],
|
||||
read_receipts: &mut RoomReadReceipts,
|
||||
) -> Result<bool> {
|
||||
let prev_latest_receipt_event_id = read_receipts.latest_read_receipt_event_id.clone();
|
||||
|
||||
if let Some(receipt_event) = receipt_event {
|
||||
trace!("Got a new receipt event!");
|
||||
|
||||
// Find a private or public read receipt for the current user.
|
||||
let mut receipt_event_id = None;
|
||||
if let Some((event_id, receipt)) = receipt_event
|
||||
.user_receipt(user_id, ReceiptType::Read)
|
||||
.or_else(|| receipt_event.user_receipt(user_id, ReceiptType::ReadPrivate))
|
||||
{
|
||||
if receipt.thread == ReceiptThread::Unthreaded || receipt.thread == ReceiptThread::Main
|
||||
{
|
||||
receipt_event_id = Some(event_id.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(receipt_event_id) = receipt_event_id {
|
||||
// We've found the id of an event to which the receipt attaches. The associated
|
||||
// event may either come from the new batch of events associated to
|
||||
// this sync, or it may live in the past timeline events we know
|
||||
// about.
|
||||
|
||||
// First, save the event id as the latest one that has a read receipt.
|
||||
read_receipts.latest_read_receipt_event_id = Some(receipt_event_id.clone());
|
||||
|
||||
// Try to find if the read receipt refers to an event from the current sync, to
|
||||
// avoid searching the cached timeline events.
|
||||
trace!("We got a new event with a read receipt: {receipt_event_id}. Search in new events...");
|
||||
if read_receipts.find_and_count_events(&receipt_event_id, user_id, new_events) {
|
||||
// It did, so our work here is done.
|
||||
// Always return true here; we saved at least the latest read receipt.
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// We didn't find the event attached to the receipt in the new batches of
|
||||
// events. It's possible it's referring to an event we've already
|
||||
// seen. In that case, try to find it.
|
||||
let previous_events = previous_events_provider.for_room(room_id);
|
||||
|
||||
trace!("Couldn't find the event attached to the receipt in the new events; looking in past events too now...");
|
||||
if read_receipts.find_and_count_events(
|
||||
&receipt_event_id,
|
||||
user_id,
|
||||
previous_events.iter().chain(new_events.iter()),
|
||||
) {
|
||||
// It did refer to an old event, so our work here is done.
|
||||
// Always return true here; we saved at least the latest read receipt.
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(receipt_event_id) = prev_latest_receipt_event_id {
|
||||
// There's no new read-receipt here. We assume the cached events have been
|
||||
// properly processed, and we only need to process the new events based
|
||||
// on the previous receipt.
|
||||
trace!("No new receipts, or couldn't find attached event; looking if the past latest known receipt refers to a new event...");
|
||||
if read_receipts.find_and_count_events(&receipt_event_id, user_id, new_events) {
|
||||
// We found the event to which the previous receipt attached to (so we at least
|
||||
// reset the counts once), our work is done here.
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
// If we haven't returned at this point, it means that either we had no previous
|
||||
// read receipt, or the previous read receipt was not attached to any new
|
||||
// event.
|
||||
//
|
||||
// In that case, accumulate all events as part of the current batch, and wait
|
||||
// for the next receipt.
|
||||
trace!("Default path: including all new events for the receipts count.");
|
||||
let mut new_receipt = false;
|
||||
for event in new_events {
|
||||
if read_receipts.update_for_event(event, user_id) {
|
||||
new_receipt = true;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(new_receipt)
|
||||
}
|
||||
|
||||
/// Is the event worth marking a room as unread?
|
||||
fn marks_as_unread(event: &Raw<AnySyncTimelineEvent>, user_id: &UserId) -> bool {
|
||||
let event = match event.deserialize() {
|
||||
Ok(event) => event,
|
||||
Err(err) => {
|
||||
tracing::debug!(
|
||||
"couldn't deserialize event {:?}: {err}",
|
||||
event.get_field::<String>("event_id").ok().flatten()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if event.sender() == user_id {
|
||||
// Not interested in one's own events.
|
||||
return false;
|
||||
}
|
||||
|
||||
match event {
|
||||
ruma::events::AnySyncTimelineEvent::MessageLike(event) => {
|
||||
// Filter out redactions.
|
||||
let Some(content) = event.original_content() else {
|
||||
tracing::trace!("not interesting because redacted");
|
||||
return false;
|
||||
};
|
||||
|
||||
// Filter out edits.
|
||||
if matches!(
|
||||
content.relation(),
|
||||
Some(ruma::events::room::encrypted::Relation::Replacement(..))
|
||||
) {
|
||||
tracing::trace!("not interesting because edited");
|
||||
return false;
|
||||
}
|
||||
|
||||
match event {
|
||||
AnySyncMessageLikeEvent::CallAnswer(_)
|
||||
| AnySyncMessageLikeEvent::CallInvite(_)
|
||||
| AnySyncMessageLikeEvent::CallHangup(_)
|
||||
| AnySyncMessageLikeEvent::CallCandidates(_)
|
||||
| AnySyncMessageLikeEvent::CallNegotiate(_)
|
||||
| AnySyncMessageLikeEvent::CallReject(_)
|
||||
| AnySyncMessageLikeEvent::CallSelectAnswer(_)
|
||||
| AnySyncMessageLikeEvent::PollResponse(_)
|
||||
| AnySyncMessageLikeEvent::UnstablePollResponse(_)
|
||||
| AnySyncMessageLikeEvent::Reaction(_)
|
||||
| AnySyncMessageLikeEvent::RoomRedaction(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationStart(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationReady(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationCancel(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationAccept(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationDone(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationMac(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationKey(_) => false,
|
||||
|
||||
// For some reason, Ruma doesn't handle these two in `content.relation()` above.
|
||||
AnySyncMessageLikeEvent::PollStart(SyncMessageLikeEvent::Original(
|
||||
OriginalSyncMessageLikeEvent {
|
||||
content:
|
||||
PollStartEventContent { relates_to: Some(Relation::Replacement(_)), .. },
|
||||
..
|
||||
},
|
||||
))
|
||||
| AnySyncMessageLikeEvent::UnstablePollStart(SyncMessageLikeEvent::Original(
|
||||
OriginalSyncMessageLikeEvent {
|
||||
content: UnstablePollStartEventContent::Replacement(_),
|
||||
..
|
||||
},
|
||||
)) => false,
|
||||
|
||||
AnySyncMessageLikeEvent::Message(_)
|
||||
| AnySyncMessageLikeEvent::PollStart(_)
|
||||
| AnySyncMessageLikeEvent::UnstablePollStart(_)
|
||||
| AnySyncMessageLikeEvent::PollEnd(_)
|
||||
| AnySyncMessageLikeEvent::UnstablePollEnd(_)
|
||||
| AnySyncMessageLikeEvent::RoomEncrypted(_)
|
||||
| AnySyncMessageLikeEvent::RoomMessage(_)
|
||||
| AnySyncMessageLikeEvent::Sticker(_) => true,
|
||||
|
||||
_ => {
|
||||
// What I don't know about, I don't care about.
|
||||
tracing::debug!("unhandled timeline event type: {}", event.event_type());
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ruma::events::AnySyncTimelineEvent::State(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::ops::Not as _;
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
use matrix_sdk_test::sync_timeline_event;
|
||||
use ruma::{event_id, push::Action, user_id, EventId, UserId};
|
||||
|
||||
use crate::read_receipts::{marks_as_unread, RoomReadReceipts};
|
||||
|
||||
#[test]
|
||||
fn test_room_message_marks_as_unread() {
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
let other_user_id = user_id!("@bob:example.org");
|
||||
|
||||
// A message from somebody else marks the room as unread...
|
||||
let ev = sync_timeline_event!({
|
||||
"sender": other_user_id,
|
||||
"type": "m.room.message",
|
||||
"event_id": "$ida",
|
||||
"origin_server_ts": 12344446,
|
||||
"content": { "body":"A", "msgtype": "m.text" },
|
||||
});
|
||||
assert!(marks_as_unread(&ev, user_id));
|
||||
|
||||
// ... but a message from ourselves doesn't.
|
||||
let ev = sync_timeline_event!({
|
||||
"sender": user_id,
|
||||
"type": "m.room.message",
|
||||
"event_id": "$ida",
|
||||
"origin_server_ts": 12344446,
|
||||
"content": { "body":"A", "msgtype": "m.text" },
|
||||
});
|
||||
assert!(marks_as_unread(&ev, user_id).not());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_room_edit_doesnt_mark_as_unread() {
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
let other_user_id = user_id!("@bob:example.org");
|
||||
|
||||
// An edit to a message from somebody else doesn't mark the room as unread.
|
||||
let ev = sync_timeline_event!({
|
||||
"sender": other_user_id,
|
||||
"type": "m.room.message",
|
||||
"event_id": "$ida",
|
||||
"origin_server_ts": 12344446,
|
||||
"content": {
|
||||
"body": " * edited message",
|
||||
"m.new_content": {
|
||||
"body": "edited message",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"m.relates_to": {
|
||||
"event_id": "$someeventid:localhost",
|
||||
"rel_type": "m.replace"
|
||||
},
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
});
|
||||
assert!(marks_as_unread(&ev, user_id).not());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_redaction_doesnt_mark_room_as_unread() {
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
let other_user_id = user_id!("@bob:example.org");
|
||||
|
||||
// A redact of a message from somebody else doesn't mark the room as unread.
|
||||
let ev = sync_timeline_event!({
|
||||
"content": {
|
||||
"reason": "🛑"
|
||||
},
|
||||
"event_id": "$151957878228ssqrJ:localhost",
|
||||
"origin_server_ts": 151957878000000_u64,
|
||||
"sender": other_user_id,
|
||||
"type": "m.room.redaction",
|
||||
"redacts": "$151957878228ssqrj:localhost",
|
||||
"unsigned": {
|
||||
"age": 85
|
||||
}
|
||||
});
|
||||
|
||||
assert!(marks_as_unread(&ev, user_id).not());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reaction_doesnt_mark_room_as_unread() {
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
let other_user_id = user_id!("@bob:example.org");
|
||||
|
||||
// A reaction from somebody else to a message doesn't mark the room as unread.
|
||||
let ev = sync_timeline_event!({
|
||||
"content": {
|
||||
"m.relates_to": {
|
||||
"event_id": "$15275047031IXQRi:localhost",
|
||||
"key": "👍",
|
||||
"rel_type": "m.annotation"
|
||||
}
|
||||
},
|
||||
"event_id": "$15275047031IXQRi:localhost",
|
||||
"origin_server_ts": 159027581000000_u64,
|
||||
"sender": other_user_id,
|
||||
"type": "m.reaction",
|
||||
"unsigned": {
|
||||
"age": 85
|
||||
}
|
||||
});
|
||||
|
||||
assert!(marks_as_unread(&ev, user_id).not());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_state_event_doesnt_mark_as_unread() {
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
let event_id = event_id!("$1");
|
||||
let ev = sync_timeline_event!({
|
||||
"content": {
|
||||
"displayname": "Alice",
|
||||
"membership": "join",
|
||||
},
|
||||
"event_id": event_id,
|
||||
"origin_server_ts": 1432135524678u64,
|
||||
"sender": user_id,
|
||||
"state_key": user_id,
|
||||
"type": "m.room.member",
|
||||
});
|
||||
|
||||
assert!(marks_as_unread(&ev, user_id).not());
|
||||
|
||||
let other_user_id = user_id!("@bob:example.org");
|
||||
assert!(marks_as_unread(&ev, other_user_id).not());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_count_unread_and_mentions() {
|
||||
fn make_event(user_id: &UserId, push_actions: Vec<Action>) -> SyncTimelineEvent {
|
||||
SyncTimelineEvent {
|
||||
event: sync_timeline_event!({
|
||||
"sender": user_id,
|
||||
"type": "m.room.message",
|
||||
"event_id": "$ida",
|
||||
"origin_server_ts": 12344446,
|
||||
"content": { "body":"A", "msgtype": "m.text" },
|
||||
}),
|
||||
encryption_info: None,
|
||||
push_actions,
|
||||
}
|
||||
}
|
||||
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
|
||||
// An interesting event from oneself doesn't count as a new unread message.
|
||||
let event = make_event(user_id, Vec::new());
|
||||
let mut receipts = RoomReadReceipts::default();
|
||||
receipts.update_for_event(&event, user_id);
|
||||
assert_eq!(receipts.num_unread, 0);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
|
||||
// An interesting event from someone else does count as a new unread message.
|
||||
let event = make_event(user_id!("@bob:example.org"), Vec::new());
|
||||
let mut receipts = RoomReadReceipts::default();
|
||||
receipts.update_for_event(&event, user_id);
|
||||
assert_eq!(receipts.num_unread, 1);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
|
||||
// Push actions computed beforehand are respected.
|
||||
let event = make_event(user_id!("@bob:example.org"), vec![Action::Notify]);
|
||||
let mut receipts = RoomReadReceipts::default();
|
||||
receipts.update_for_event(&event, user_id);
|
||||
assert_eq!(receipts.num_unread, 1);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
assert_eq!(receipts.num_notifications, 1);
|
||||
|
||||
let event = make_event(
|
||||
user_id!("@bob:example.org"),
|
||||
vec![Action::SetTweak(ruma::push::Tweak::Highlight(true))],
|
||||
);
|
||||
let mut receipts = RoomReadReceipts::default();
|
||||
receipts.update_for_event(&event, user_id);
|
||||
assert_eq!(receipts.num_unread, 1);
|
||||
assert_eq!(receipts.num_mentions, 1);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
|
||||
let event = make_event(
|
||||
user_id!("@bob:example.org"),
|
||||
vec![Action::SetTweak(ruma::push::Tweak::Highlight(true)), Action::Notify],
|
||||
);
|
||||
let mut receipts = RoomReadReceipts::default();
|
||||
receipts.update_for_event(&event, user_id);
|
||||
assert_eq!(receipts.num_unread, 1);
|
||||
assert_eq!(receipts.num_mentions, 1);
|
||||
assert_eq!(receipts.num_notifications, 1);
|
||||
|
||||
// Technically this `push_actions` set would be a bug somewhere else, but let's
|
||||
// make sure to resist against it.
|
||||
let event = make_event(user_id!("@bob:example.org"), vec![Action::Notify, Action::Notify]);
|
||||
let mut receipts = RoomReadReceipts::default();
|
||||
receipts.update_for_event(&event, user_id);
|
||||
assert_eq!(receipts.num_unread, 1);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
assert_eq!(receipts.num_notifications, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_and_count_events() {
|
||||
let ev0 = event_id!("$0");
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
|
||||
// When provided with no events, we report not finding the event to which the
|
||||
// receipt relates.
|
||||
let mut receipts = RoomReadReceipts::default();
|
||||
assert!(receipts.find_and_count_events(ev0, user_id, &[]).not());
|
||||
assert_eq!(receipts.num_unread, 0);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
|
||||
// When provided with one event, that's not the receipt event, we don't count
|
||||
// it.
|
||||
fn make_event(event_id: &EventId) -> SyncTimelineEvent {
|
||||
SyncTimelineEvent {
|
||||
event: sync_timeline_event!({
|
||||
"sender": "@bob:example.org",
|
||||
"type": "m.room.message",
|
||||
"event_id": event_id,
|
||||
"origin_server_ts": 12344446,
|
||||
"content": { "body":"A", "msgtype": "m.text" },
|
||||
}),
|
||||
encryption_info: None,
|
||||
push_actions: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
let mut receipts = RoomReadReceipts {
|
||||
num_unread: 42,
|
||||
num_notifications: 13,
|
||||
num_mentions: 37,
|
||||
latest_read_receipt_event_id: None,
|
||||
};
|
||||
assert!(receipts
|
||||
.find_and_count_events(ev0, user_id, &[make_event(event_id!("$1"))],)
|
||||
.not());
|
||||
assert_eq!(receipts.num_unread, 42);
|
||||
assert_eq!(receipts.num_notifications, 13);
|
||||
assert_eq!(receipts.num_mentions, 37);
|
||||
|
||||
// When provided with one event that's the receipt target, we find it, reset the
|
||||
// count, and since there's nothing else, we stop there and end up with
|
||||
// zero counts.
|
||||
let mut receipts = RoomReadReceipts {
|
||||
num_unread: 42,
|
||||
num_notifications: 13,
|
||||
num_mentions: 37,
|
||||
latest_read_receipt_event_id: None,
|
||||
};
|
||||
assert!(receipts.find_and_count_events(ev0, user_id, &[make_event(ev0)]));
|
||||
assert_eq!(receipts.num_unread, 0);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
|
||||
// When provided with multiple events and not the receipt event, we do not count
|
||||
// anything..
|
||||
let mut receipts = RoomReadReceipts {
|
||||
num_unread: 42,
|
||||
num_notifications: 13,
|
||||
num_mentions: 37,
|
||||
latest_read_receipt_event_id: None,
|
||||
};
|
||||
assert!(receipts
|
||||
.find_and_count_events(
|
||||
ev0,
|
||||
user_id,
|
||||
&[
|
||||
make_event(event_id!("$1")),
|
||||
make_event(event_id!("$2")),
|
||||
make_event(event_id!("$3"))
|
||||
],
|
||||
)
|
||||
.not());
|
||||
assert_eq!(receipts.num_unread, 42);
|
||||
assert_eq!(receipts.num_notifications, 13);
|
||||
assert_eq!(receipts.num_mentions, 37);
|
||||
|
||||
// When provided with multiple events including one that's the receipt event, we
|
||||
// find it and count from it.
|
||||
let mut receipts = RoomReadReceipts {
|
||||
num_unread: 42,
|
||||
num_notifications: 13,
|
||||
num_mentions: 37,
|
||||
latest_read_receipt_event_id: None,
|
||||
};
|
||||
assert!(receipts.find_and_count_events(
|
||||
ev0,
|
||||
user_id,
|
||||
&[
|
||||
make_event(event_id!("$1")),
|
||||
make_event(ev0),
|
||||
make_event(event_id!("$2")),
|
||||
make_event(event_id!("$3"))
|
||||
],
|
||||
));
|
||||
assert_eq!(receipts.num_unread, 2);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
}
|
||||
}
|
||||
@@ -100,7 +100,7 @@ pub struct BaseRoomInfo {
|
||||
pub(crate) tombstone: Option<MinimalStateEvent<RoomTombstoneEventContent>>,
|
||||
/// The topic of this room.
|
||||
pub(crate) topic: Option<MinimalStateEvent<RoomTopicEventContent>>,
|
||||
/// All Minimal state events that containing one or more running matrixRTC
|
||||
/// All minimal state events that containing one or more running matrixRTC
|
||||
/// memberships.
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
|
||||
pub(crate) rtc_member: BTreeMap<OwnedUserId, MinimalStateEvent<CallMemberEventContent>>,
|
||||
|
||||
@@ -63,6 +63,7 @@ use super::{
|
||||
use crate::latest_event::LatestEvent;
|
||||
use crate::{
|
||||
deserialized_responses::MemberEvent,
|
||||
read_receipts::RoomReadReceipts,
|
||||
store::{DynStateStore, Result as StoreResult, StateStoreExt},
|
||||
sync::UnreadNotificationsCount,
|
||||
MinimalStateEvent, OriginalMinimalStateEvent, RoomMemberships,
|
||||
@@ -187,6 +188,31 @@ impl Room {
|
||||
self.inner.read().notification_counts
|
||||
}
|
||||
|
||||
/// Get the number of unread messages (computed client-side).
|
||||
///
|
||||
/// This might be more precise than [`Self::unread_notification_counts`] for
|
||||
/// encrypted rooms.
|
||||
pub fn num_unread_messages(&self) -> u64 {
|
||||
self.inner.read().read_receipts.num_unread
|
||||
}
|
||||
|
||||
/// Get the number of unread notifications (computed client-side).
|
||||
///
|
||||
/// This might be more precise than [`Self::unread_notification_counts`] for
|
||||
/// encrypted rooms.
|
||||
pub fn num_unread_notifications(&self) -> u64 {
|
||||
self.inner.read().read_receipts.num_notifications
|
||||
}
|
||||
|
||||
/// Get the number of unread mentions (computed client-side), that is,
|
||||
/// messages causing a highlight in a room.
|
||||
///
|
||||
/// This might be more precise than [`Self::unread_notification_counts`] for
|
||||
/// encrypted rooms.
|
||||
pub fn num_unread_mentions(&self) -> u64 {
|
||||
self.inner.read().read_receipts.num_mentions
|
||||
}
|
||||
|
||||
/// Check if the room has its members fully synced.
|
||||
///
|
||||
/// Members might be missing if lazy member loading was enabled for the
|
||||
@@ -403,13 +429,7 @@ impl Room {
|
||||
/// sliding sync.
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
pub fn latest_event(&self) -> Option<LatestEvent> {
|
||||
self.inner.read().latest_event.clone()
|
||||
}
|
||||
|
||||
/// Update the last event in the room
|
||||
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
|
||||
pub(crate) fn set_latest_event(&self, latest_event: Option<LatestEvent>) {
|
||||
self.inner.update(|info| info.latest_event = latest_event);
|
||||
self.inner.read().latest_event.as_deref().cloned()
|
||||
}
|
||||
|
||||
/// Return the most recent few encrypted events. When the keys come through
|
||||
@@ -428,10 +448,23 @@ impl Room {
|
||||
///
|
||||
/// Panics if index is not a valid index in the latest_encrypted_events
|
||||
/// list.
|
||||
///
|
||||
/// It is the responsibility of the caller to apply the changes into the
|
||||
/// state store after calling this function.
|
||||
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
|
||||
pub(crate) fn on_latest_event_decrypted(&self, latest_event: LatestEvent, index: usize) {
|
||||
self.set_latest_event(Some(latest_event));
|
||||
pub(crate) fn on_latest_event_decrypted(
|
||||
&self,
|
||||
latest_event: Box<LatestEvent>,
|
||||
index: usize,
|
||||
changes: &mut crate::StateChanges,
|
||||
) {
|
||||
self.latest_encrypted_events.write().unwrap().drain(0..=index);
|
||||
|
||||
let room_info = changes
|
||||
.room_infos
|
||||
.entry(self.room_id().to_owned())
|
||||
.or_insert_with(|| self.clone_info());
|
||||
room_info.latest_event = Some(latest_event);
|
||||
}
|
||||
|
||||
/// Get the list of users ids that are considered to be joined members of
|
||||
@@ -577,8 +610,8 @@ impl Room {
|
||||
};
|
||||
|
||||
debug!(
|
||||
room_id = self.room_id().as_str(),
|
||||
own_user = self.own_user_id.as_str(),
|
||||
room_id = ?self.room_id(),
|
||||
own_user = ?self.own_user_id,
|
||||
joined, invited,
|
||||
heroes = ?members,
|
||||
"Calculating name for a room",
|
||||
@@ -597,9 +630,10 @@ impl Room {
|
||||
self.inner.get()
|
||||
}
|
||||
|
||||
/// Update the summary with given RoomInfo
|
||||
pub fn update_summary(&self, summary: RoomInfo) {
|
||||
self.inner.set(summary);
|
||||
/// Update the inner summary with the given RoomInfo, and notify
|
||||
/// subscribers.
|
||||
pub fn set_room_info(&self, room_info: RoomInfo) {
|
||||
self.inner.set(room_info);
|
||||
}
|
||||
|
||||
/// Get the `RoomMember` with the given `user_id`.
|
||||
@@ -686,7 +720,7 @@ impl Room {
|
||||
|
||||
/// Get the receipt as an `OwnedEventId` and `Receipt` tuple for the given
|
||||
/// `receipt_type`, `thread` and `user_id` in this room.
|
||||
pub async fn user_receipt(
|
||||
pub async fn load_user_receipt(
|
||||
&self,
|
||||
receipt_type: ReceiptType,
|
||||
thread: ReceiptThread,
|
||||
@@ -695,9 +729,10 @@ impl Room {
|
||||
self.store.get_user_room_receipt_event(self.room_id(), receipt_type, thread, user_id).await
|
||||
}
|
||||
|
||||
/// Get the receipts as a list of `OwnedUserId` and `Receipt` tuples for the
|
||||
/// given `receipt_type`, `thread` and `event_id` in this room.
|
||||
pub async fn event_receipts(
|
||||
/// Load from storage the receipts as a list of `OwnedUserId` and `Receipt`
|
||||
/// tuples for the given `receipt_type`, `thread` and `event_id` in this
|
||||
/// room.
|
||||
pub async fn load_event_receipts(
|
||||
&self,
|
||||
receipt_type: ReceiptType,
|
||||
thread: ReceiptThread,
|
||||
@@ -716,26 +751,42 @@ impl Room {
|
||||
pub struct RoomInfo {
|
||||
/// The unique room id of the room.
|
||||
pub(crate) room_id: OwnedRoomId,
|
||||
|
||||
/// The state of the room.
|
||||
pub(crate) room_state: RoomState,
|
||||
/// The unread notifications counts.
|
||||
|
||||
/// The unread notifications counts, as returned by the server.
|
||||
///
|
||||
/// These might be incorrect for encrypted rooms, since the server doesn't
|
||||
/// have access to the content of the encrypted events.
|
||||
pub(crate) notification_counts: UnreadNotificationsCount,
|
||||
|
||||
/// The summary of this room.
|
||||
pub(crate) summary: RoomSummary,
|
||||
|
||||
/// Flag remembering if the room members are synced.
|
||||
pub(crate) members_synced: bool,
|
||||
|
||||
/// The prev batch of this room we received during the last sync.
|
||||
pub(crate) last_prev_batch: Option<String>,
|
||||
|
||||
/// How much we know about this room.
|
||||
pub(crate) sync_info: SyncInfo,
|
||||
|
||||
/// Whether or not the encryption info was been synced.
|
||||
pub(crate) encryption_state_synced: bool,
|
||||
|
||||
/// The last event send by sliding sync
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
pub(crate) latest_event: Option<LatestEvent>,
|
||||
pub(crate) latest_event: Option<Box<LatestEvent>>,
|
||||
|
||||
/// Information about read receipts for this room.
|
||||
#[serde(default)]
|
||||
pub(crate) read_receipts: RoomReadReceipts,
|
||||
|
||||
/// Base room info which holds some basic event contents important for the
|
||||
/// room state.
|
||||
pub(crate) base_info: BaseRoomInfo,
|
||||
pub(crate) base_info: Box<BaseRoomInfo>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
@@ -769,7 +820,8 @@ impl RoomInfo {
|
||||
encryption_state_synced: false,
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
latest_event: None,
|
||||
base_info: BaseRoomInfo::new(),
|
||||
read_receipts: Default::default(),
|
||||
base_info: Box::new(BaseRoomInfo::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1246,10 +1298,11 @@ mod tests {
|
||||
last_prev_batch: Some("pb".to_owned()),
|
||||
sync_info: SyncInfo::FullySynced,
|
||||
encryption_state_synced: true,
|
||||
latest_event: Some(LatestEvent::new(
|
||||
latest_event: Some(Box::new(LatestEvent::new(
|
||||
Raw::from_json_string(json!({"sender": "@u:i.uk"}).to_string()).unwrap().into(),
|
||||
)),
|
||||
base_info: BaseRoomInfo::new(),
|
||||
))),
|
||||
base_info: Box::new(BaseRoomInfo::new()),
|
||||
read_receipts: Default::default(),
|
||||
};
|
||||
|
||||
let info_json = json!({
|
||||
@@ -1289,6 +1342,12 @@ mod tests {
|
||||
"name": null,
|
||||
"tombstone": null,
|
||||
"topic": null,
|
||||
},
|
||||
"read_receipts": {
|
||||
"num_unread": 0,
|
||||
"num_mentions": 0,
|
||||
"num_notifications": 0,
|
||||
"latest_read_receipt_event_id": null,
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1626,6 +1685,50 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
async fn test_setting_the_latest_event_doesnt_cause_a_room_info_update() {
|
||||
// Given a room,
|
||||
let client = crate::BaseClient::new();
|
||||
|
||||
client
|
||||
.set_session_meta(crate::SessionMeta {
|
||||
user_id: user_id!("@alice:example.org").into(),
|
||||
device_id: ruma::device_id!("AYEAYEAYE").into(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let room_id = room_id!("!test:localhost");
|
||||
let room = client.get_or_create_room(room_id, RoomState::Joined);
|
||||
|
||||
// That has an encrypted event,
|
||||
add_encrypted_event(&room, "$A");
|
||||
// Sanity: it has no latest_event
|
||||
assert!(room.latest_event().is_none());
|
||||
|
||||
// When I set up an observer on the latest_event,
|
||||
let mut room_info_subscriber = room.subscribe_info();
|
||||
|
||||
// And I provide a decrypted event to replace the encrypted one,
|
||||
let event = make_latest_event("$A");
|
||||
|
||||
let mut changes = StateChanges::default();
|
||||
room.on_latest_event_decrypted(event.clone(), 0, &mut changes);
|
||||
|
||||
// The subscriber isn't notified at this point.
|
||||
stream_assert::assert_pending!(room_info_subscriber);
|
||||
|
||||
// Then updating the room info will store the event,
|
||||
client.apply_changes(&changes);
|
||||
assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
|
||||
|
||||
// And wake up the subscriber.
|
||||
use futures_util::FutureExt as _;
|
||||
assert!(room_info_subscriber.next().now_or_never().is_some());
|
||||
stream_assert::assert_pending!(room_info_subscriber);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
fn when_we_provide_a_newly_decrypted_event_it_replaces_latest_event() {
|
||||
@@ -1637,7 +1740,9 @@ mod tests {
|
||||
|
||||
// When I provide a decrypted event to replace the encrypted one
|
||||
let event = make_latest_event("$A");
|
||||
room.on_latest_event_decrypted(event.clone(), 0);
|
||||
let mut changes = StateChanges::default();
|
||||
room.on_latest_event_decrypted(event.clone(), 0, &mut changes);
|
||||
room.set_room_info(changes.room_infos.get(room.room_id()).cloned().unwrap());
|
||||
|
||||
// Then is it stored
|
||||
assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
|
||||
@@ -1657,13 +1762,15 @@ mod tests {
|
||||
// When I provide a latest event
|
||||
let new_event = make_latest_event("$1");
|
||||
let new_event_index = 1;
|
||||
room.on_latest_event_decrypted(new_event.clone(), new_event_index);
|
||||
let mut changes = StateChanges::default();
|
||||
room.on_latest_event_decrypted(new_event.clone(), new_event_index, &mut changes);
|
||||
room.set_room_info(changes.room_infos.get(room.room_id()).cloned().unwrap());
|
||||
|
||||
// Then the encrypted events list is shortened to only newer events
|
||||
let enc_evs = room.latest_encrypted_events();
|
||||
assert_eq!(enc_evs.len(), 2);
|
||||
assert_eq!(enc_evs.get(0).unwrap().get_field::<&str>("event_id").unwrap().unwrap(), "$2");
|
||||
assert_eq!(enc_evs.get(1).unwrap().get_field::<&str>("event_id").unwrap().unwrap(), "$3");
|
||||
assert_eq!(enc_evs[0].get_field::<&str>("event_id").unwrap().unwrap(), "$2");
|
||||
assert_eq!(enc_evs[1].get_field::<&str>("event_id").unwrap().unwrap(), "$3");
|
||||
|
||||
// And the event is stored
|
||||
assert_eq!(room.latest_event().unwrap().event_id(), new_event.event_id());
|
||||
@@ -1682,7 +1789,9 @@ mod tests {
|
||||
// When I provide a latest event and say it was the very latest
|
||||
let new_event = make_latest_event("$3");
|
||||
let new_event_index = 3;
|
||||
room.on_latest_event_decrypted(new_event, new_event_index);
|
||||
let mut changes = StateChanges::default();
|
||||
room.on_latest_event_decrypted(new_event, new_event_index, &mut changes);
|
||||
room.set_room_info(changes.room_infos.get(room.room_id()).cloned().unwrap());
|
||||
|
||||
// Then the encrypted events list ie empty
|
||||
let enc_evs = room.latest_encrypted_events();
|
||||
@@ -1698,10 +1807,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
fn make_latest_event(event_id: &str) -> LatestEvent {
|
||||
LatestEvent::new(SyncTimelineEvent::new(
|
||||
fn make_latest_event(event_id: &str) -> Box<LatestEvent> {
|
||||
Box::new(LatestEvent::new(SyncTimelineEvent::new(
|
||||
Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap(),
|
||||
))
|
||||
)))
|
||||
}
|
||||
|
||||
fn timestamp(minutes_ago: u32) -> MilliSecondsSinceUnixEpoch {
|
||||
|
||||
@@ -12,21 +12,24 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use std::ops::Deref;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::events::AnyToDeviceEvent;
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::{
|
||||
v3::{self, InvitedRoom, RoomSummary},
|
||||
v4::{self, AccountData},
|
||||
api::client::{
|
||||
push::get_notifications::v3::Notification,
|
||||
sync::sync_events::{
|
||||
v3::{self, InvitedRoom, RoomSummary},
|
||||
v4,
|
||||
},
|
||||
},
|
||||
events::{AnySyncStateEvent, AnySyncTimelineEvent},
|
||||
serde::Raw,
|
||||
RoomId,
|
||||
OwnedRoomId, RoomId,
|
||||
};
|
||||
use tracing::{instrument, trace, warn};
|
||||
|
||||
@@ -38,6 +41,7 @@ use crate::RoomMemberships;
|
||||
use crate::{
|
||||
deserialized_responses::AmbiguityChanges,
|
||||
error::Result,
|
||||
read_receipts::{compute_notifications, PreviousEventsProvider},
|
||||
rooms::RoomState,
|
||||
store::{ambiguity_map::AmbiguityCache, StateChanges, Store},
|
||||
sync::{JoinedRoom, LeftRoom, Rooms, SyncResponse},
|
||||
@@ -96,7 +100,7 @@ impl BaseClient {
|
||||
|
||||
trace!("ready to submit changes to store");
|
||||
self.store.save_changes(&changes).await?;
|
||||
self.apply_changes(&changes).await;
|
||||
self.apply_changes(&changes);
|
||||
trace!("applied changes");
|
||||
|
||||
Ok(to_device)
|
||||
@@ -109,7 +113,11 @@ impl BaseClient {
|
||||
/// * `response` - The response that we received after a successful sliding
|
||||
/// sync.
|
||||
#[instrument(skip_all, level = "trace")]
|
||||
pub async fn process_sliding_sync(&self, response: &v4::Response) -> Result<SyncResponse> {
|
||||
pub async fn process_sliding_sync<PEP: PreviousEventsProvider>(
|
||||
&self,
|
||||
response: &v4::Response,
|
||||
previous_events_provider: &PEP,
|
||||
) -> Result<SyncResponse> {
|
||||
let v4::Response {
|
||||
// FIXME not yet supported by sliding sync. see
|
||||
// https://github.com/matrix-org/matrix-rust-sdk/issues/1014
|
||||
@@ -135,32 +143,33 @@ impl BaseClient {
|
||||
return Ok(SyncResponse::default());
|
||||
};
|
||||
|
||||
let v4::Extensions { account_data, receipts, .. } = extensions;
|
||||
|
||||
let mut changes = StateChanges::default();
|
||||
|
||||
let store = self.store.clone();
|
||||
let mut ambiguity_cache = AmbiguityCache::new(store.inner.clone());
|
||||
|
||||
let account_data = &extensions.account_data;
|
||||
if !account_data.is_empty() {
|
||||
self.handle_account_data(&account_data.global, &mut changes).await;
|
||||
}
|
||||
|
||||
let mut new_rooms = Rooms::default();
|
||||
let mut notifications = Default::default();
|
||||
|
||||
for (room_id, room_data) in rooms {
|
||||
let (room_to_store, joined_room, left_room, invited_room) = self
|
||||
for (room_id, response_room_data) in rooms {
|
||||
let (room_info, joined_room, left_room, invited_room) = self
|
||||
.process_sliding_sync_room(
|
||||
room_id,
|
||||
room_data,
|
||||
response_room_data,
|
||||
account_data,
|
||||
&store,
|
||||
&mut changes,
|
||||
&mut notifications,
|
||||
&mut ambiguity_cache,
|
||||
account_data,
|
||||
)
|
||||
.await?;
|
||||
|
||||
changes.add_room(room_to_store);
|
||||
changes.add_room(room_info);
|
||||
|
||||
if let Some(joined_room) = joined_room {
|
||||
new_rooms.join.insert(room_id.clone(), joined_room);
|
||||
@@ -175,8 +184,11 @@ impl BaseClient {
|
||||
}
|
||||
}
|
||||
|
||||
// Process receipts now we have rooms
|
||||
for (room_id, raw) in &receipts.rooms {
|
||||
// Handle read receipts and typing notifications independently of the rooms:
|
||||
// these both live in a different subsection of the server's response,
|
||||
// so they may exist without any update for the associated room.
|
||||
|
||||
for (room_id, raw) in &extensions.receipts.rooms {
|
||||
match raw.deserialize() {
|
||||
Ok(event) => {
|
||||
changes.add_receipts(room_id, event.content);
|
||||
@@ -186,10 +198,52 @@ impl BaseClient {
|
||||
#[rustfmt::skip]
|
||||
warn!(
|
||||
?room_id, event_id,
|
||||
"Failed to deserialize ephemeral room event: {e}"
|
||||
"Failed to deserialize read receipt room event: {e}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// We assume this can only happen in joined rooms, or something's very wrong.
|
||||
new_rooms
|
||||
.join
|
||||
.entry(room_id.to_owned())
|
||||
.or_insert_with(JoinedRoom::default)
|
||||
.ephemeral
|
||||
.push(raw.clone().cast());
|
||||
}
|
||||
|
||||
for (room_id, raw) in &extensions.typing.rooms {
|
||||
// We assume this can only happen in joined rooms, or something's very wrong.
|
||||
new_rooms
|
||||
.join
|
||||
.entry(room_id.to_owned())
|
||||
.or_insert_with(JoinedRoom::default)
|
||||
.ephemeral
|
||||
.push(raw.clone().cast());
|
||||
}
|
||||
|
||||
// Rooms in `new_rooms.join` either have a timeline update, or a new read
|
||||
// receipt. Update the read receipt accordingly.
|
||||
let user_id = &self.session_meta().expect("logged in user").user_id;
|
||||
|
||||
for (room_id, joined_room_update) in &mut new_rooms.join {
|
||||
if let Some(mut room_info) = changes
|
||||
.room_infos
|
||||
.get(room_id)
|
||||
.cloned()
|
||||
.or_else(|| self.get_room(room_id).map(|r| r.clone_info()))
|
||||
{
|
||||
if compute_notifications(
|
||||
user_id,
|
||||
room_id,
|
||||
changes.receipts.get(room_id),
|
||||
previous_events_provider,
|
||||
&joined_room_update.timeline.events,
|
||||
&mut room_info.read_receipts,
|
||||
)? {
|
||||
changes.add_room(room_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO remove this, we're processing account data events here again
|
||||
@@ -214,13 +268,13 @@ impl BaseClient {
|
||||
|
||||
trace!("ready to submit changes to store");
|
||||
store.save_changes(&changes).await?;
|
||||
self.apply_changes(&changes).await;
|
||||
self.apply_changes(&changes);
|
||||
trace!("applied changes");
|
||||
|
||||
Ok(SyncResponse {
|
||||
rooms: new_rooms,
|
||||
ambiguity_changes: AmbiguityChanges { changes: ambiguity_cache.changes },
|
||||
notifications: changes.notifications,
|
||||
notifications,
|
||||
// FIXME not yet supported by sliding sync.
|
||||
presence: Default::default(),
|
||||
account_data: account_data.global.clone(),
|
||||
@@ -228,14 +282,16 @@ impl BaseClient {
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn process_sliding_sync_room(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
room_data: &v4::SlidingSyncRoom,
|
||||
account_data: &v4::AccountData,
|
||||
store: &Store,
|
||||
changes: &mut StateChanges,
|
||||
notifications: &mut BTreeMap<OwnedRoomId, Vec<Notification>>,
|
||||
ambiguity_cache: &mut AmbiguityCache,
|
||||
account_data: &AccountData,
|
||||
) -> Result<(RoomInfo, Option<JoinedRoom>, Option<LeftRoom>, Option<InvitedRoom>)> {
|
||||
let mut state_events = Self::deserialize_state_events(&room_data.required_state);
|
||||
state_events.extend(Self::deserialize_state_events_from_timeline(&room_data.timeline));
|
||||
@@ -288,6 +344,7 @@ impl BaseClient {
|
||||
&mut user_ids,
|
||||
&mut room_info,
|
||||
changes,
|
||||
notifications,
|
||||
ambiguity_cache,
|
||||
)
|
||||
.await?;
|
||||
@@ -319,18 +376,25 @@ impl BaseClient {
|
||||
room_info.update_notification_count(notification_count);
|
||||
|
||||
match room_info.state() {
|
||||
RoomState::Joined => Ok((
|
||||
room_info,
|
||||
Some(JoinedRoom::new(
|
||||
timeline,
|
||||
raw_state_events,
|
||||
room_account_data.unwrap_or_default(),
|
||||
Vec::new(),
|
||||
notification_count,
|
||||
)),
|
||||
None,
|
||||
None,
|
||||
)),
|
||||
RoomState::Joined => {
|
||||
// Ephemeral events are added separately, because we might not
|
||||
// have a room subsection in the response, yet we may have receipts for
|
||||
// that room.
|
||||
let ephemeral = Vec::new();
|
||||
|
||||
Ok((
|
||||
room_info,
|
||||
Some(JoinedRoom::new(
|
||||
timeline,
|
||||
raw_state_events,
|
||||
room_account_data.unwrap_or_default(),
|
||||
ephemeral,
|
||||
notification_count,
|
||||
)),
|
||||
None,
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
RoomState::Left => Ok((
|
||||
room_info,
|
||||
@@ -412,16 +476,20 @@ impl BaseClient {
|
||||
state_events: &[AnySyncStateEvent],
|
||||
room_info: &mut RoomInfo,
|
||||
) {
|
||||
for event in state_events {
|
||||
let Some(meta) = self.session_meta() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Start from the last event; the first membership event we see in that order is
|
||||
// the last in the regular order, so that's the only one we need to
|
||||
// consider.
|
||||
for event in state_events.iter().rev() {
|
||||
if let AnySyncStateEvent::RoomMember(member) = &event {
|
||||
// If this event updates the current user's membership, record that in the
|
||||
// room_info.
|
||||
if let Some(meta) = self.session_meta() {
|
||||
if member.sender() == meta.user_id
|
||||
&& member.state_key() == meta.user_id.as_str()
|
||||
{
|
||||
room_info.set_state(member.membership().into());
|
||||
}
|
||||
if member.sender() == meta.user_id && member.state_key() == meta.user_id.as_str() {
|
||||
room_info.set_state(member.membership().into());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -456,8 +524,12 @@ impl BaseClient {
|
||||
}
|
||||
|
||||
/// Find the most recent decrypted event and cache it in the supplied RoomInfo.
|
||||
///
|
||||
/// If any encrypted events are found after that one, store them in the RoomInfo
|
||||
/// too so we can use them when we get the relevant keys.
|
||||
///
|
||||
/// It is the responsibility of the caller to update the `RoomInfo` instance
|
||||
/// stored in the `Room`.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
async fn cache_latest_events(
|
||||
room: &Room,
|
||||
@@ -524,16 +596,14 @@ async fn cache_latest_events(
|
||||
}
|
||||
}
|
||||
|
||||
let latest_event = LatestEvent::new_with_sender_details(
|
||||
let latest_event = Box::new(LatestEvent::new_with_sender_details(
|
||||
event.clone(),
|
||||
sender_profile,
|
||||
sender_name_is_ambiguous,
|
||||
);
|
||||
));
|
||||
|
||||
// Store it in the return RoomInfo, and in the Room, to make sure they are
|
||||
// consistent
|
||||
// Store it in the return RoomInfo (it will be saved for us in the room later).
|
||||
room_info.latest_event = Some(latest_event.clone());
|
||||
room.set_latest_event(Some(latest_event));
|
||||
// We don't need any of the older encrypted events because we have a new
|
||||
// decrypted one.
|
||||
room.latest_encrypted_events.write().unwrap().clear();
|
||||
@@ -601,8 +671,8 @@ mod tests {
|
||||
use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, ring_buffer::RingBuffer};
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::v4,
|
||||
device_id, event_id,
|
||||
api::client::sync::sync_events::{v4, UnreadNotificationsCount},
|
||||
assign, device_id, event_id,
|
||||
events::{
|
||||
direct::DirectEventContent,
|
||||
room::{
|
||||
@@ -623,11 +693,41 @@ mod tests {
|
||||
use super::cache_latest_events;
|
||||
use crate::{store::MemoryStore, BaseClient, Room, RoomState, SessionMeta};
|
||||
|
||||
#[async_test]
|
||||
async fn test_notification_count_set() {
|
||||
let client = logged_in_client().await;
|
||||
|
||||
let mut response = v4::Response::new("42".to_owned());
|
||||
let room_id = room_id!("!room:example.org");
|
||||
let count = assign!(UnreadNotificationsCount::default(), {
|
||||
highlight_count: Some(uint!(13)),
|
||||
notification_count: Some(uint!(37)),
|
||||
});
|
||||
|
||||
response.rooms.insert(
|
||||
room_id.to_owned(),
|
||||
assign!(v4::SlidingSyncRoom::new(), {
|
||||
unread_notifications: count.clone()
|
||||
}),
|
||||
);
|
||||
|
||||
let sync_response =
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
|
||||
// Check it's present in the response.
|
||||
let room = sync_response.rooms.join.get(room_id).unwrap();
|
||||
assert_eq!(room.unread_notifications, count.clone().into());
|
||||
|
||||
// Check it's been updated in the store.
|
||||
let room = client.get_room(room_id).expect("found room");
|
||||
assert_eq!(room.unread_notification_counts(), count.into());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn can_process_empty_sliding_sync_response() {
|
||||
let client = logged_in_client().await;
|
||||
let empty_response = v4::Response::new("5".to_owned());
|
||||
client.process_sliding_sync(&empty_response).await.expect("Failed to process sync");
|
||||
client.process_sliding_sync(&empty_response, &()).await.expect("Failed to process sync");
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
@@ -642,7 +742,7 @@ mod tests {
|
||||
room.joined_count = Some(uint!(41));
|
||||
let response = response_with_room(room_id, room).await;
|
||||
let sync_resp =
|
||||
client.process_sliding_sync(&response).await.expect("Failed to process sync");
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
|
||||
// Then the room appears in the client (with the same joined count)
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
@@ -667,7 +767,7 @@ mod tests {
|
||||
room.name = Some("little room".to_owned());
|
||||
let response = response_with_room(room_id, room).await;
|
||||
let sync_resp =
|
||||
client.process_sliding_sync(&response).await.expect("Failed to process sync");
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
|
||||
// Then the room appears in the client with the expected name
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
@@ -693,7 +793,7 @@ mod tests {
|
||||
room.name = Some("little room".to_owned());
|
||||
let response = response_with_room(room_id, room).await;
|
||||
let sync_resp =
|
||||
client.process_sliding_sync(&response).await.expect("Failed to process sync");
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
|
||||
// Then the room appears in the client with the expected name
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
@@ -717,7 +817,7 @@ mod tests {
|
||||
let mut room = v4::SlidingSyncRoom::new();
|
||||
set_room_joined(&mut room, user_id);
|
||||
let response = response_with_room(room_id, room).await;
|
||||
client.process_sliding_sync(&response).await.expect("Failed to process sync");
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Joined);
|
||||
|
||||
// And then leave with a `required_state` state event…
|
||||
@@ -725,7 +825,7 @@ mod tests {
|
||||
set_room_left(&mut room, user_id);
|
||||
let response = response_with_room(room_id, room).await;
|
||||
let sync_resp =
|
||||
client.process_sliding_sync(&response).await.expect("Failed to process sync");
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
|
||||
// The room is left.
|
||||
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
|
||||
@@ -747,14 +847,14 @@ mod tests {
|
||||
let mut room = v4::SlidingSyncRoom::new();
|
||||
set_room_joined(&mut room, user_id);
|
||||
let response = response_with_room(room_id, room).await;
|
||||
client.process_sliding_sync(&response).await.expect("Failed to process sync");
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Joined);
|
||||
|
||||
// And then leave with a `timeline` state event…
|
||||
let mut room = v4::SlidingSyncRoom::new();
|
||||
set_room_left_as_timeline_event(&mut room, user_id);
|
||||
let response = response_with_room(room_id, room).await;
|
||||
client.process_sliding_sync(&response).await.expect("Failed to process sync");
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
|
||||
// The room is left.
|
||||
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
|
||||
@@ -773,7 +873,7 @@ mod tests {
|
||||
let mut room = v4::SlidingSyncRoom::new();
|
||||
set_room_joined(&mut room, user_id);
|
||||
let response = response_with_room(room_id, room).await;
|
||||
client.process_sliding_sync(&response).await.expect("Failed to process sync");
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
// (sanity: state is join)
|
||||
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Joined);
|
||||
|
||||
@@ -781,7 +881,7 @@ mod tests {
|
||||
let mut room = v4::SlidingSyncRoom::new();
|
||||
set_room_left(&mut room, user_id);
|
||||
let response = response_with_room(room_id, room).await;
|
||||
client.process_sliding_sync(&response).await.expect("Failed to process sync");
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
// (sanity: state is left)
|
||||
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
|
||||
|
||||
@@ -789,7 +889,7 @@ mod tests {
|
||||
let mut room = v4::SlidingSyncRoom::new();
|
||||
set_room_invited(&mut room, user_id);
|
||||
let response = response_with_room(room_id, room).await;
|
||||
client.process_sliding_sync(&response).await.expect("Failed to process sync");
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
|
||||
// Then the room is in the invite state
|
||||
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Invited);
|
||||
@@ -902,7 +1002,7 @@ mod tests {
|
||||
// When I send sliding sync response containing a room with an avatar
|
||||
let room = room_with_avatar(mxc_uri!("mxc://e.uk/med1"), user_id);
|
||||
let response = response_with_room(room_id, room).await;
|
||||
client.process_sliding_sync(&response).await.expect("Failed to process sync");
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
|
||||
// Then the room in the client has the avatar
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
@@ -924,7 +1024,7 @@ mod tests {
|
||||
set_room_invited(&mut room, user_id);
|
||||
let response = response_with_room(room_id, room).await;
|
||||
let sync_resp =
|
||||
client.process_sliding_sync(&response).await.expect("Failed to process sync");
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
|
||||
// Then the room is added to the client
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
@@ -947,7 +1047,7 @@ mod tests {
|
||||
let mut room = room_with_avatar(mxc_uri!("mxc://e.uk/med1"), user_id);
|
||||
set_room_invited(&mut room, user_id);
|
||||
let response = response_with_room(room_id, room).await;
|
||||
client.process_sliding_sync(&response).await.expect("Failed to process sync");
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
|
||||
// Then the room in the client has the avatar
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
@@ -969,7 +1069,7 @@ mod tests {
|
||||
let mut room = room_with_canonical_alias(room_alias_id, user_id);
|
||||
set_room_invited(&mut room, user_id);
|
||||
let response = response_with_room(room_id, room).await;
|
||||
client.process_sliding_sync(&response).await.expect("Failed to process sync");
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
|
||||
// Then the room in the client has the avatar
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
@@ -989,7 +1089,7 @@ mod tests {
|
||||
let mut room = room_with_canonical_alias(room_alias_id, user_id);
|
||||
room.name = Some("This came from the server".to_owned());
|
||||
let response = response_with_room(room_id, room).await;
|
||||
client.process_sliding_sync(&response).await.expect("Failed to process sync");
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
|
||||
// Then the room's name is just exactly what the server supplied
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
@@ -1023,7 +1123,7 @@ mod tests {
|
||||
let events = &[event_a, event_b.clone()];
|
||||
let room = room_with_timeline(events);
|
||||
let response = response_with_room(room_id, room).await;
|
||||
client.process_sliding_sync(&response).await.expect("Failed to process sync");
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
|
||||
// Then the room holds the latest event
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
@@ -1049,7 +1149,7 @@ mod tests {
|
||||
// When the sliding sync response contains a timeline
|
||||
let room = room_with_timeline(&[event_a]);
|
||||
let response = response_with_room(room_id, room).await;
|
||||
client.process_sliding_sync(&response).await.expect("Failed to process sync");
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
|
||||
// Then the room holds the latest event
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
@@ -1070,7 +1170,7 @@ mod tests {
|
||||
// When a redaction for that event is received
|
||||
let room = room_with_timeline(&[redaction]);
|
||||
let response = response_with_room(room_id, room).await;
|
||||
client.process_sliding_sync(&response).await.expect("Failed to process sync");
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
|
||||
// Then the room still holds the latest event
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
@@ -1145,9 +1245,11 @@ mod tests {
|
||||
|
||||
// The latest message is stored
|
||||
assert_eq!(
|
||||
ev_id(room_info.latest_event.map(|latest_event| latest_event.event().clone())),
|
||||
ev_id(room_info.latest_event.as_ref().map(|latest_event| latest_event.event().clone())),
|
||||
rawev_id(event2.clone())
|
||||
);
|
||||
|
||||
room.set_room_info(room_info);
|
||||
assert_eq!(
|
||||
ev_id(room.latest_event().map(|latest_event| latest_event.event().clone())),
|
||||
rawev_id(event2)
|
||||
@@ -1169,6 +1271,7 @@ mod tests {
|
||||
let room = make_room();
|
||||
let mut room_info = room.clone_info();
|
||||
cache_latest_events(&room, &mut room_info, events, None, None).await;
|
||||
room.set_room_info(room_info);
|
||||
|
||||
// The latest message is stored
|
||||
assert_eq!(
|
||||
@@ -1195,6 +1298,7 @@ mod tests {
|
||||
let room = make_room();
|
||||
let mut room_info = room.clone_info();
|
||||
cache_latest_events(&room, &mut room_info, events, None, None).await;
|
||||
room.set_room_info(room_info);
|
||||
|
||||
// The latest message is stored, ignoring the receipt
|
||||
assert_eq!(
|
||||
@@ -1247,6 +1351,7 @@ mod tests {
|
||||
let room = make_room();
|
||||
let mut room_info = room.clone_info();
|
||||
cache_latest_events(&room, &mut room_info, events, None, None).await;
|
||||
room.set_room_info(room_info);
|
||||
|
||||
// The latest message is stored, ignoring encrypted and receipts
|
||||
assert_eq!(
|
||||
@@ -1287,6 +1392,8 @@ mod tests {
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
room.set_room_info(room_info);
|
||||
|
||||
// Sanity: room_info has 10 encrypted events inside it
|
||||
assert_eq!(room.latest_encrypted_events.read().unwrap().len(), 10);
|
||||
|
||||
@@ -1294,6 +1401,7 @@ mod tests {
|
||||
let eventa = make_encrypted_event("$a");
|
||||
let mut room_info = room.clone_info();
|
||||
cache_latest_events(&room, &mut room_info, &[eventa], None, None).await;
|
||||
room.set_room_info(room_info);
|
||||
|
||||
// The oldest event is gone
|
||||
assert!(!rawevs_ids(&room.latest_encrypted_events).contains(&"$0".to_owned()));
|
||||
@@ -1315,11 +1423,13 @@ mod tests {
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
room.set_room_info(room_info.clone());
|
||||
|
||||
// When I ask to cache an unecnrypted event, and some more encrypted events
|
||||
// When I ask to cache an unencrypted event, and some more encrypted events
|
||||
let eventa = make_event("m.room.message", "$a");
|
||||
let eventb = make_encrypted_event("$b");
|
||||
cache_latest_events(&room, &mut room_info, &[eventa, eventb], None, None).await;
|
||||
room.set_room_info(room_info);
|
||||
|
||||
// The only encrypted events stored are the ones after the decrypted one
|
||||
assert_eq!(rawevs_ids(&room.latest_encrypted_events), &["$b"]);
|
||||
@@ -1332,6 +1442,7 @@ mod tests {
|
||||
let room = make_room();
|
||||
let mut room_info = room.clone_info();
|
||||
cache_latest_events(&room, &mut room_info, events, None, None).await;
|
||||
room.set_room_info(room_info);
|
||||
room.latest_event().map(|latest_event| latest_event.event().clone())
|
||||
}
|
||||
|
||||
@@ -1446,7 +1557,7 @@ mod tests {
|
||||
|
||||
let mut response = response_with_room(room_id, room).await;
|
||||
set_direct_with(&mut response, their_id.to_owned(), vec![room_id.to_owned()]);
|
||||
client.process_sliding_sync(&response).await.expect("Failed to process sync");
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
}
|
||||
|
||||
/// Set this user's membership within this room to new_state
|
||||
@@ -1459,7 +1570,7 @@ mod tests {
|
||||
let mut room = v4::SlidingSyncRoom::new();
|
||||
room.required_state.push(make_membership_event(user_id, new_state));
|
||||
let response = response_with_room(room_id, room).await;
|
||||
client.process_sliding_sync(&response).await.expect("Failed to process sync");
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
}
|
||||
|
||||
fn set_direct_with(
|
||||
|
||||
@@ -200,11 +200,8 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
|
||||
async fn test_media_content(&self) {
|
||||
let uri = mxc_uri!("mxc://localhost/media");
|
||||
let content: Vec<u8> = "somebinarydata".into();
|
||||
|
||||
let request_file =
|
||||
MediaRequest { source: MediaSource::Plain(uri.to_owned()), format: MediaFormat::File };
|
||||
|
||||
let request_thumbnail = MediaRequest {
|
||||
source: MediaSource::Plain(uri.to_owned()),
|
||||
format: MediaFormat::Thumbnail(MediaThumbnailSize {
|
||||
@@ -214,6 +211,17 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
}),
|
||||
};
|
||||
|
||||
let other_uri = mxc_uri!("mxc://localhost/media-other");
|
||||
let request_other_file = MediaRequest {
|
||||
source: MediaSource::Plain(other_uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
|
||||
let content: Vec<u8> = "hello".into();
|
||||
let thumbnail_content: Vec<u8> = "world".into();
|
||||
let other_content: Vec<u8> = "foo".into();
|
||||
|
||||
// Media isn't present in the cache.
|
||||
assert!(
|
||||
self.get_media_content(&request_file).await.unwrap().is_none(),
|
||||
"unexpected media found"
|
||||
@@ -223,35 +231,63 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
"media not found"
|
||||
);
|
||||
|
||||
// Let's add the media.
|
||||
self.add_media_content(&request_file, content.clone()).await.expect("adding media failed");
|
||||
assert!(
|
||||
self.get_media_content(&request_file).await.unwrap().is_some(),
|
||||
|
||||
// Media is present in the cache.
|
||||
assert_eq!(
|
||||
self.get_media_content(&request_file).await.unwrap().as_ref(),
|
||||
Some(&content),
|
||||
"media not found though added"
|
||||
);
|
||||
|
||||
// Let's remove the media.
|
||||
self.remove_media_content(&request_file).await.expect("removing media failed");
|
||||
|
||||
// Media isn't present in the cache.
|
||||
assert!(
|
||||
self.get_media_content(&request_file).await.unwrap().is_none(),
|
||||
"media still there after removing"
|
||||
);
|
||||
|
||||
// Let's add the media again.
|
||||
self.add_media_content(&request_file, content.clone())
|
||||
.await
|
||||
.expect("adding media again failed");
|
||||
assert!(
|
||||
self.get_media_content(&request_file).await.unwrap().is_some(),
|
||||
|
||||
assert_eq!(
|
||||
self.get_media_content(&request_file).await.unwrap().as_ref(),
|
||||
Some(&content),
|
||||
"media not found after adding again"
|
||||
);
|
||||
|
||||
self.add_media_content(&request_thumbnail, content.clone())
|
||||
// Let's add the thumbnail media.
|
||||
self.add_media_content(&request_thumbnail, thumbnail_content.clone())
|
||||
.await
|
||||
.expect("adding thumbnail failed");
|
||||
assert!(
|
||||
self.get_media_content(&request_thumbnail).await.unwrap().is_some(),
|
||||
|
||||
// Media's thumbnail is present.
|
||||
assert_eq!(
|
||||
self.get_media_content(&request_thumbnail).await.unwrap().as_ref(),
|
||||
Some(&thumbnail_content),
|
||||
"thumbnail not found"
|
||||
);
|
||||
|
||||
// Let's add another media with a different URI.
|
||||
self.add_media_content(&request_other_file, other_content.clone())
|
||||
.await
|
||||
.expect("adding other media failed");
|
||||
|
||||
// Other file is present.
|
||||
assert_eq!(
|
||||
self.get_media_content(&request_other_file).await.unwrap().as_ref(),
|
||||
Some(&other_content),
|
||||
"other file not found"
|
||||
);
|
||||
|
||||
// Let's remove media based on URI.
|
||||
self.remove_media_content_for_uri(uri).await.expect("removing all media for uri failed");
|
||||
|
||||
assert!(
|
||||
self.get_media_content(&request_file).await.unwrap().is_none(),
|
||||
"media wasn't removed"
|
||||
@@ -260,6 +296,10 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
self.get_media_content(&request_thumbnail).await.unwrap().is_none(),
|
||||
"thumbnail wasn't removed"
|
||||
);
|
||||
assert!(
|
||||
self.get_media_content(&request_other_file).await.unwrap().is_some(),
|
||||
"other media was removed"
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_topic_redaction(&self) -> Result<()> {
|
||||
|
||||
@@ -18,7 +18,7 @@ use std::{
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::instant::Instant;
|
||||
use matrix_sdk_common::{instant::Instant, ring_buffer::RingBuffer};
|
||||
use ruma::{
|
||||
canonical_json::{redact, RedactedBecause},
|
||||
events::{
|
||||
@@ -29,15 +29,16 @@ use ruma::{
|
||||
AnySyncStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType,
|
||||
},
|
||||
serde::Raw,
|
||||
CanonicalJsonObject, EventId, MxcUri, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId,
|
||||
RoomVersionId, UserId,
|
||||
CanonicalJsonObject, EventId, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedUserId,
|
||||
RoomId, RoomVersionId, UserId,
|
||||
};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use super::{Result, RoomInfo, StateChanges, StateStore, StoreError};
|
||||
use crate::{
|
||||
deserialized_responses::RawAnySyncOrStrippedState, media::MediaRequest, MinimalRoomMemberEvent,
|
||||
RoomMemberships, RoomState, StateStoreDataKey, StateStoreDataValue,
|
||||
deserialized_responses::RawAnySyncOrStrippedState,
|
||||
media::{MediaRequest, UniqueKey as _},
|
||||
MinimalRoomMemberEvent, RoomMemberships, RoomState, StateStoreDataKey, StateStoreDataValue,
|
||||
};
|
||||
|
||||
/// In-Memory, non-persistent implementation of the `StateStore`
|
||||
@@ -77,13 +78,14 @@ pub struct MemoryStore {
|
||||
HashMap<(String, Option<String>), HashMap<OwnedEventId, HashMap<OwnedUserId, Receipt>>>,
|
||||
>,
|
||||
>,
|
||||
media: StdRwLock<RingBuffer<(OwnedMxcUri, String /* unique key */, Vec<u8>)>>,
|
||||
custom: StdRwLock<HashMap<Vec<u8>, Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl MemoryStore {
|
||||
/// Create a new empty MemoryStore
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
Self { media: StdRwLock::new(RingBuffer::new(20)), ..Default::default() }
|
||||
}
|
||||
|
||||
fn get_user_room_receipt_event_impl(
|
||||
@@ -700,17 +702,55 @@ impl StateStore for MemoryStore {
|
||||
Ok(self.custom.write().unwrap().remove(key))
|
||||
}
|
||||
|
||||
// The in-memory store doesn't cache media
|
||||
async fn add_media_content(&self, _request: &MediaRequest, _data: Vec<u8>) -> Result<()> {
|
||||
async fn add_media_content(&self, request: &MediaRequest, data: Vec<u8>) -> Result<()> {
|
||||
// Avoid duplication. Let's try to remove it first.
|
||||
self.remove_media_content(request).await?;
|
||||
// Now, let's add it.
|
||||
self.media.write().unwrap().push((request.uri().to_owned(), request.unique_key(), data));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
async fn get_media_content(&self, _request: &MediaRequest) -> Result<Option<Vec<u8>>> {
|
||||
Ok(None)
|
||||
|
||||
async fn get_media_content(&self, request: &MediaRequest) -> Result<Option<Vec<u8>>> {
|
||||
let media = self.media.read().unwrap();
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
Ok(media.iter().find_map(|(_media_uri, media_key, media_content)| {
|
||||
(media_key == &expected_key).then(|| media_content.to_owned())
|
||||
}))
|
||||
}
|
||||
async fn remove_media_content(&self, _request: &MediaRequest) -> Result<()> {
|
||||
|
||||
async fn remove_media_content(&self, request: &MediaRequest) -> Result<()> {
|
||||
let mut media = self.media.write().unwrap();
|
||||
let expected_key = request.unique_key();
|
||||
let Some(index) = media
|
||||
.iter()
|
||||
.position(|(_media_uri, media_key, _media_content)| media_key == &expected_key)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
media.remove(index);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
async fn remove_media_content_for_uri(&self, _uri: &MxcUri) -> Result<()> {
|
||||
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
|
||||
let mut media = self.media.write().unwrap();
|
||||
let expected_key = uri.to_owned();
|
||||
let positions = media
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(position, (media_uri, _media_key, _media_content))| {
|
||||
(media_uri == &expected_key).then_some(position)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Iterate in reverse-order so that positions stay valid after first removals.
|
||||
for position in positions.into_iter().rev() {
|
||||
media.remove(position);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -738,5 +778,5 @@ mod tests {
|
||||
Ok(MemoryStore::new())
|
||||
}
|
||||
|
||||
statestore_integration_tests!();
|
||||
statestore_integration_tests!(with_media_tests);
|
||||
}
|
||||
|
||||
@@ -117,7 +117,8 @@ impl RoomInfoV1 {
|
||||
sync_info,
|
||||
encryption_state_synced,
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
latest_event: latest_event.map(LatestEvent::new),
|
||||
latest_event: latest_event.map(|ev| Box::new(LatestEvent::new(ev))),
|
||||
read_receipts: Default::default(),
|
||||
base_info: base_info.migrate(create),
|
||||
}
|
||||
}
|
||||
@@ -157,7 +158,10 @@ struct BaseRoomInfoV1 {
|
||||
|
||||
impl BaseRoomInfoV1 {
|
||||
/// Migrate this to a [`BaseRoomInfo`].
|
||||
fn migrate(self, create: Option<&SyncOrStrippedState<RoomCreateEventContent>>) -> BaseRoomInfo {
|
||||
fn migrate(
|
||||
self,
|
||||
create: Option<&SyncOrStrippedState<RoomCreateEventContent>>,
|
||||
) -> Box<BaseRoomInfo> {
|
||||
let BaseRoomInfoV1 {
|
||||
avatar,
|
||||
canonical_alias,
|
||||
@@ -186,7 +190,7 @@ impl BaseRoomInfoV1 {
|
||||
MinimalStateEvent::Redacted(ev) => MinimalStateEvent::Redacted(ev),
|
||||
});
|
||||
|
||||
BaseRoomInfo {
|
||||
Box::new(BaseRoomInfo {
|
||||
avatar,
|
||||
canonical_alias,
|
||||
create,
|
||||
@@ -200,7 +204,7 @@ impl BaseRoomInfoV1 {
|
||||
tombstone,
|
||||
topic,
|
||||
rtc_member: BTreeMap::new(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ mod traits;
|
||||
use matrix_sdk_crypto::store::{DynCryptoStore, IntoCryptoStore};
|
||||
pub use matrix_sdk_store_encryption::Error as StoreEncryptionError;
|
||||
use ruma::{
|
||||
api::client::push::get_notifications::v3::Notification,
|
||||
events::{
|
||||
presence::PresenceEvent,
|
||||
receipt::ReceiptEventContent,
|
||||
@@ -291,8 +290,6 @@ pub struct StateChanges {
|
||||
/// A map from room id to a map of a display name and a set of user ids that
|
||||
/// share that display name in the given room.
|
||||
pub ambiguity_maps: BTreeMap<OwnedRoomId, BTreeMap<String, BTreeSet<OwnedUserId>>>,
|
||||
/// A map of `RoomId` to a vector of `Notification`s
|
||||
pub notifications: BTreeMap<OwnedRoomId, Vec<Notification>>,
|
||||
}
|
||||
|
||||
impl StateChanges {
|
||||
@@ -379,12 +376,6 @@ impl StateChanges {
|
||||
.insert(redacted_event_id.to_owned(), redaction);
|
||||
}
|
||||
|
||||
/// Update the `StateChanges` struct with the given room with a new
|
||||
/// `Notification`.
|
||||
pub fn add_notification(&mut self, room_id: &RoomId, notification: Notification) {
|
||||
self.notifications.entry(room_id.to_owned()).or_default().push(notification);
|
||||
}
|
||||
|
||||
/// Update the `StateChanges` struct with the given room with a new
|
||||
/// `Receipts`.
|
||||
pub fn add_receipts(&mut self, room_id: &RoomId, event: ReceiptEventContent) {
|
||||
|
||||
@@ -371,6 +371,7 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
#[repr(transparent)]
|
||||
struct EraseStateStoreError<T>(T);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<T: fmt::Debug> fmt::Debug for EraseStateStoreError<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
|
||||
@@ -96,7 +96,7 @@ impl fmt::Debug for Rooms {
|
||||
}
|
||||
|
||||
/// Updates to joined rooms.
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct JoinedRoom {
|
||||
/// Counts of unread notifications for this room.
|
||||
pub unread_notifications: UnreadNotificationsCount,
|
||||
@@ -114,6 +114,7 @@ pub struct JoinedRoom {
|
||||
pub ephemeral: Vec<Raw<AnySyncEphemeralRoomEvent>>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for JoinedRoom {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("JoinedRoom")
|
||||
@@ -139,7 +140,7 @@ impl JoinedRoom {
|
||||
}
|
||||
|
||||
/// Counts of unread notifications for a room.
|
||||
#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
|
||||
pub struct UnreadNotificationsCount {
|
||||
/// The number of unread notifications for this room with the highlight flag
|
||||
/// set.
|
||||
@@ -182,6 +183,7 @@ impl LeftRoom {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for LeftRoom {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("JoinedRoom")
|
||||
|
||||
@@ -9,14 +9,14 @@ name = "matrix-sdk-common"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = { workspace = true }
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
default-target = "x86_64-unknown-linux-gnu"
|
||||
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
|
||||
|
||||
[features]
|
||||
js = ["instant/wasm-bindgen", "instant/inaccurate", "wasm-bindgen-futures"]
|
||||
js = ["instant/wasm-bindgen", "wasm-bindgen-futures"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
@@ -27,21 +27,21 @@ serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt", "time", "sync"] }
|
||||
tokio = { workspace = true, features = ["rt", "time"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
futures-util = { workspace = true, features = ["channel"] }
|
||||
wasm-bindgen-futures = { version = "0.4.33", optional = true }
|
||||
gloo-timers = { version = "0.3.0", features = ["futures"] }
|
||||
web-sys = {version = "0.3.60", features = ["console"] }
|
||||
web-sys = { version = "0.3.60", features = ["console"] }
|
||||
tracing-subscriber = { version = "0.3.14", default-features = false, features = ["fmt", "ansi"] }
|
||||
wasm-bindgen = "0.2.84"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = { workspace = true }
|
||||
matrix-sdk-test = { path = "../../testing/matrix-sdk-test/", version= "0.6.0"}
|
||||
proptest = { version = "1.4.0", default-features = false, features = ["std"] }
|
||||
matrix-sdk-test = { workspace = true }
|
||||
wasm-bindgen-test = "0.3.33"
|
||||
tracing-subscriber = "0.3.15"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
js-sys = "0.3.64"
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! A TTL cache which can be used to time out repeated operations that might
|
||||
//! experience intermittent failures.
|
||||
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
collections::HashMap,
|
||||
hash::Hash,
|
||||
sync::{Arc, RwLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use super::instant::Instant;
|
||||
|
||||
const MAX_DELAY: u64 = 15 * 60;
|
||||
const MULTIPLIER: u64 = 15;
|
||||
|
||||
/// A TTL cache where items get inactive instead of discarded.
|
||||
///
|
||||
/// The items need to be explicitly removed from the cache. This allows us to
|
||||
/// implement exponential backoff based TTL.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FailuresCache<T: Eq + Hash> {
|
||||
inner: Arc<InnerCache<T>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InnerCache<T: Eq + Hash> {
|
||||
max_delay: Duration,
|
||||
backoff_multiplier: u64,
|
||||
items: RwLock<HashMap<T, FailuresItem>>,
|
||||
}
|
||||
|
||||
impl<T: Eq + Hash> Default for InnerCache<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_delay: Duration::from_secs(MAX_DELAY),
|
||||
backoff_multiplier: MULTIPLIER,
|
||||
items: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct FailuresItem {
|
||||
insertion_time: Instant,
|
||||
duration: Duration,
|
||||
|
||||
/// Number of times that this item has failed after it was first added to
|
||||
/// the cache. (In other words, one less than the total number of
|
||||
/// failures.)
|
||||
failure_count: u8,
|
||||
}
|
||||
|
||||
impl FailuresItem {
|
||||
/// Has the item expired.
|
||||
fn expired(&self) -> bool {
|
||||
self.insertion_time.elapsed() >= self.duration
|
||||
}
|
||||
|
||||
/// Force the expiry of this item.
|
||||
///
|
||||
/// This doesn't reset the failure count, but does mark the item as ready
|
||||
/// for immediate retry.
|
||||
fn expire(&mut self) {
|
||||
self.duration = Duration::from_secs(0);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FailuresCache<T>
|
||||
where
|
||||
T: Eq + Hash,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self { inner: Default::default() }
|
||||
}
|
||||
|
||||
pub fn with_settings(max_delay: Duration, multiplier: u8) -> Self {
|
||||
Self {
|
||||
inner: InnerCache {
|
||||
max_delay,
|
||||
backoff_multiplier: multiplier.into(),
|
||||
items: Default::default(),
|
||||
}
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Is the given key non-expired and part of the cache.
|
||||
pub fn contains<Q>(&self, key: &Q) -> bool
|
||||
where
|
||||
T: Borrow<Q>,
|
||||
Q: Hash + Eq + ?Sized,
|
||||
{
|
||||
let lock = self.inner.items.read().unwrap();
|
||||
|
||||
let contains = if let Some(item) = lock.get(key) { !item.expired() } else { false };
|
||||
|
||||
contains
|
||||
}
|
||||
|
||||
/// Get the failure count for a given key.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `None` if this key is not in the failure cache. (It has never failed,
|
||||
/// or it has been [`FailuresCache::remove()`]d since the last failure.)
|
||||
///
|
||||
/// * `Some(u8)`: the number of times it has failed since it was first
|
||||
/// added to the failure cache. (In other words, one less than the total
|
||||
/// number of failures.)
|
||||
pub fn failure_count<Q>(&self, key: &Q) -> Option<u8>
|
||||
where
|
||||
T: Borrow<Q>,
|
||||
Q: Hash + Eq + ?Sized,
|
||||
{
|
||||
let lock = self.inner.items.read().unwrap();
|
||||
lock.get(key).map(|i| i.failure_count)
|
||||
}
|
||||
|
||||
/// This will calculate a duration that determines how long an item is
|
||||
/// considered to be valid while being in the cache.
|
||||
///
|
||||
/// The returned duration will follow this sequence if the default
|
||||
/// multiplier and `max_delay` values are used, values are in minutes:
|
||||
/// [0.25, 0.5, 1.0, 2.0, 4.0, 8.0, 15.0]
|
||||
fn calculate_delay(&self, failure_count: u8) -> Duration {
|
||||
let exponential_backoff = 2u64.saturating_pow(failure_count.into());
|
||||
let delay = exponential_backoff.saturating_mul(self.inner.backoff_multiplier);
|
||||
|
||||
Duration::from_secs(delay).clamp(Duration::from_secs(1), self.inner.max_delay)
|
||||
}
|
||||
|
||||
/// Add a single item to the cache.
|
||||
pub fn insert(&self, item: T) {
|
||||
self.extend([item]);
|
||||
}
|
||||
|
||||
/// Extend the cache with the given iterator of items.
|
||||
///
|
||||
/// Items that are already part of the cache, whether they are expired or
|
||||
/// not, will have their TTL extended using an exponential backoff
|
||||
/// algorithm.
|
||||
pub fn extend(&self, iterator: impl IntoIterator<Item = T>) {
|
||||
let mut lock = self.inner.items.write().unwrap();
|
||||
|
||||
let now = Instant::now();
|
||||
|
||||
for key in iterator {
|
||||
let failure_count = if let Some(value) = lock.get(&key) {
|
||||
value.failure_count.saturating_add(1)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let delay = self.calculate_delay(failure_count);
|
||||
|
||||
let item = FailuresItem { insertion_time: now, duration: delay, failure_count };
|
||||
|
||||
lock.insert(key, item);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the items contained in the iterator from the cache.
|
||||
pub fn remove<'a, I, Q>(&'a self, iterator: I)
|
||||
where
|
||||
I: Iterator<Item = &'a Q>,
|
||||
T: Borrow<Q>,
|
||||
Q: Hash + Eq + 'a + ?Sized,
|
||||
{
|
||||
let mut lock = self.inner.items.write().unwrap();
|
||||
|
||||
for item in iterator {
|
||||
lock.remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// Force the expiry of the given item, if it is present in the cache.
|
||||
///
|
||||
/// This doesn't reset the failure count, but does mark the item as ready
|
||||
/// for immediate retry.
|
||||
#[doc(hidden)]
|
||||
pub fn expire(&self, item: &T) {
|
||||
let mut lock = self.inner.items.write().unwrap();
|
||||
lock.get_mut(item).map(FailuresItem::expire);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Eq + Hash> Default for FailuresCache<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use proptest::prelude::*;
|
||||
|
||||
use super::FailuresCache;
|
||||
|
||||
#[test]
|
||||
fn failures_cache() {
|
||||
let cache = FailuresCache::new();
|
||||
|
||||
assert!(!cache.contains(&1));
|
||||
cache.extend([1u8].iter());
|
||||
assert!(cache.contains(&1));
|
||||
|
||||
cache.inner.items.write().unwrap().get_mut(&1).unwrap().duration = Duration::from_secs(0);
|
||||
assert!(!cache.contains(&1));
|
||||
|
||||
cache.remove([1u8].iter());
|
||||
assert!(cache.inner.items.read().unwrap().get(&1).is_none())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failures_cache_timeout() {
|
||||
let cache: FailuresCache<u8> = FailuresCache::new();
|
||||
|
||||
assert_eq!(cache.calculate_delay(0).as_secs(), 15);
|
||||
assert_eq!(cache.calculate_delay(1).as_secs(), 30);
|
||||
assert_eq!(cache.calculate_delay(2).as_secs(), 60);
|
||||
assert_eq!(cache.calculate_delay(3).as_secs(), 120);
|
||||
assert_eq!(cache.calculate_delay(4).as_secs(), 240);
|
||||
assert_eq!(cache.calculate_delay(5).as_secs(), 480);
|
||||
assert_eq!(cache.calculate_delay(6).as_secs(), 900);
|
||||
assert_eq!(cache.calculate_delay(7).as_secs(), 900);
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn failures_cache_proptest_timeout(count in 0..10u8) {
|
||||
let cache: FailuresCache<u8> = FailuresCache::new();
|
||||
let delay = cache.calculate_delay(count).as_secs();
|
||||
|
||||
assert!(delay <= 900);
|
||||
assert!(delay >= 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,10 +23,13 @@ use std::{
|
||||
sync::atomic::{AtomicU32, Ordering},
|
||||
};
|
||||
|
||||
use tracing::{level_filters::LevelFilter, Level, Metadata};
|
||||
use tracing_subscriber::fmt::{
|
||||
format::{DefaultFields, Format, Pretty},
|
||||
MakeWriter, Subscriber,
|
||||
use tracing::{field::Field, level_filters::LevelFilter, Event, Level, Metadata};
|
||||
use tracing_subscriber::{
|
||||
fmt::{
|
||||
format::{DefaultFields, Writer},
|
||||
FmtContext, FormatEvent, FormatFields, FormattedFields, MakeWriter, Subscriber,
|
||||
},
|
||||
registry::LookupSpan,
|
||||
};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
@@ -199,9 +202,130 @@ fn write_message_to_console(level: Level, message: &JsValue) {
|
||||
};
|
||||
}
|
||||
|
||||
/// An implementation of [`FormatEvent`] which formats events in a sensible way
|
||||
/// for sending events to the JS console.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct JsEventFormatter {}
|
||||
|
||||
impl JsEventFormatter {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, N> FormatEvent<C, N> for JsEventFormatter
|
||||
where
|
||||
C: tracing::Subscriber + for<'a> LookupSpan<'a>,
|
||||
N: for<'a> FormatFields<'a> + 'static,
|
||||
{
|
||||
fn format_event(
|
||||
&self,
|
||||
ctx: &FmtContext<'_, C, N>,
|
||||
mut writer: Writer<'_>,
|
||||
event: &Event<'_>,
|
||||
) -> fmt::Result {
|
||||
let meta = event.metadata();
|
||||
write!(writer, "{} {}: ", meta.level(), meta.target())?;
|
||||
|
||||
// write the message
|
||||
let mut v = FindMessageVisitor::default();
|
||||
event.record(&mut v);
|
||||
if let Some(m) = v.message {
|
||||
writer.write_str(m.as_str())?
|
||||
}
|
||||
|
||||
// write the other fields
|
||||
let mut v = JsFieldVisitor::new(writer.by_ref());
|
||||
event.record(&mut v);
|
||||
|
||||
if let Some(file) = meta.file() {
|
||||
write!(writer, "\n at {file}")?;
|
||||
if let Some(line) = meta.line() {
|
||||
write!(writer, ":{line}")?;
|
||||
}
|
||||
}
|
||||
|
||||
let span = event.parent().and_then(|id| ctx.span(id)).or_else(|| ctx.lookup_current());
|
||||
let scope = span.into_iter().flat_map(|span| span.scope());
|
||||
for span in scope {
|
||||
let meta = span.metadata();
|
||||
write!(writer, "\n in {}::{}", meta.target(), meta.name())?;
|
||||
|
||||
let ext = span.extensions();
|
||||
let fields = &ext
|
||||
.get::<FormattedFields<N>>()
|
||||
.expect("Unable to find FormattedFields in extensions; this is a bug");
|
||||
if !fields.is_empty() {
|
||||
write!(writer, " with {fields}")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A field visitor which is used by [`JsEventFormatter`] to find the "message"
|
||||
/// for the event.
|
||||
#[derive(Debug, Default)]
|
||||
struct FindMessageVisitor {
|
||||
message: Option<String>,
|
||||
}
|
||||
|
||||
impl tracing::field::Visit for FindMessageVisitor {
|
||||
fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
|
||||
if field.name() == "message" {
|
||||
self.message = Some(format!("{value:?}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A field visitor which is used by [`JsEventFormatter`] to print the fields
|
||||
/// other than `message`.
|
||||
struct JsFieldVisitor<'a> {
|
||||
writer: Writer<'a>,
|
||||
result: fmt::Result,
|
||||
is_empty: bool,
|
||||
}
|
||||
|
||||
impl<'a> JsFieldVisitor<'a> {
|
||||
fn new(writer: Writer<'a>) -> Self {
|
||||
Self { writer, result: Ok(()), is_empty: true }
|
||||
}
|
||||
|
||||
fn pad_and_record(&mut self, name: &str, value: &dyn Debug) -> fmt::Result {
|
||||
// If this is the first field since the message, make a new line. Otherwise,
|
||||
// just print a space.
|
||||
if self.is_empty {
|
||||
self.is_empty = false;
|
||||
write!(self.writer, "\n ")?;
|
||||
} else {
|
||||
write!(self.writer, " ")?;
|
||||
}
|
||||
|
||||
write!(self.writer, "{name}={value:?}")
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> tracing::field::Visit for JsFieldVisitor<'a> {
|
||||
fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
|
||||
if self.result.is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
let name = field.name();
|
||||
|
||||
if name == "message" {
|
||||
// Already handled by FindMessageVisitor.
|
||||
return;
|
||||
}
|
||||
|
||||
self.result = self.pad_and_record(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of [`Subscriber`] returned by [`make_tracing_subscriber`]
|
||||
pub type JsLoggingSubscriber =
|
||||
Subscriber<DefaultFields, Format<Pretty, ()>, LevelFilter, MakeJsLogWriter>;
|
||||
Subscriber<DefaultFields, JsEventFormatter, LevelFilter, MakeJsLogWriter>;
|
||||
|
||||
/// Construct a [`tracing::Subscriber`] which will format logs and send them to
|
||||
/// the Javascript console or the given logging object.
|
||||
@@ -217,13 +341,11 @@ pub fn make_tracing_subscriber(logger: Option<JsLogger>) -> JsLoggingSubscriber
|
||||
None => MakeJsLogWriter::new(),
|
||||
};
|
||||
|
||||
let format = tracing_subscriber::fmt::format().without_time().pretty();
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(Level::TRACE)
|
||||
.with_writer(make_writer)
|
||||
.with_ansi(false)
|
||||
.event_format(format)
|
||||
.event_format(JsEventFormatter::new())
|
||||
.finish()
|
||||
}
|
||||
|
||||
@@ -250,7 +372,7 @@ pub(crate) mod tests {
|
||||
|
||||
// log something to it
|
||||
with_default(subscriber, || {
|
||||
debug!("Test message");
|
||||
debug!(value = 1, "Test message");
|
||||
});
|
||||
|
||||
// inspect the call log
|
||||
@@ -268,7 +390,8 @@ pub(crate) mod tests {
|
||||
assert_eq!(call_args.length(), 1, "Expected 1 argument, got {}", call_args.length());
|
||||
|
||||
let message_string = call_args.get(0).as_string().unwrap();
|
||||
let expected_prefix = " DEBUG matrix_sdk_common::js_tracing::tests: Test message";
|
||||
let expected_prefix =
|
||||
"DEBUG matrix_sdk_common::js_tracing::tests: Test message\n value=1\n";
|
||||
assert!(
|
||||
message_string.starts_with(expected_prefix),
|
||||
"Expected log message to start with '{}', but was '{}'",
|
||||
|
||||
@@ -23,12 +23,16 @@ pub use ruma;
|
||||
pub mod debug;
|
||||
pub mod deserialized_responses;
|
||||
pub mod executor;
|
||||
pub mod failures_cache;
|
||||
pub mod ring_buffer;
|
||||
pub mod store_locks;
|
||||
pub mod timeout;
|
||||
pub mod tracing_timer;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
// We cannot currently measure test coverage in the WASM environment, so
|
||||
// js_tracing is incorrectly flagged as untested. Disable coverage checking for
|
||||
// it.
|
||||
#[cfg(all(target_arch = "wasm32", not(tarpaulin_include)))]
|
||||
pub mod js_tracing;
|
||||
|
||||
pub use store_locks::LEASE_DURATION_MS;
|
||||
|
||||
@@ -75,12 +75,19 @@ impl<T> RingBuffer<T> {
|
||||
self.inner.pop_front()
|
||||
}
|
||||
|
||||
/// Removes and returns one specific element at `index` if it exists,
|
||||
/// otherwise it returns `None`.
|
||||
pub fn remove(&mut self, index: usize) -> Option<T> {
|
||||
self.inner.remove(index)
|
||||
}
|
||||
|
||||
/// Returns an iterator that provides elements in front-to-back order, i.e.
|
||||
/// the same order you would get if you repeatedly called pop().
|
||||
pub fn iter(&self) -> Iter<'_, T> {
|
||||
self.inner.iter()
|
||||
}
|
||||
|
||||
/// Returns an iterator that drains its items.
|
||||
pub fn drain<R>(&mut self, range: R) -> Drain<'_, T>
|
||||
where
|
||||
R: RangeBounds<usize>,
|
||||
@@ -155,7 +162,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_push_and_pop_and_length() {
|
||||
pub fn test_push_and_pop_and_remove_and_length() {
|
||||
let mut ring_buffer = RingBuffer::new(3);
|
||||
|
||||
ring_buffer.push(1);
|
||||
@@ -167,23 +174,48 @@ mod tests {
|
||||
ring_buffer.push(3);
|
||||
assert_eq!(ring_buffer.len(), 3);
|
||||
|
||||
ring_buffer.pop();
|
||||
assert_eq!(ring_buffer.pop(), Some(1));
|
||||
assert_eq!(ring_buffer.len(), 2);
|
||||
assert_eq!(ring_buffer.get(0), Some(&2));
|
||||
assert_eq!(ring_buffer.get(1), Some(&3));
|
||||
assert_eq!(ring_buffer.get(2), None);
|
||||
|
||||
ring_buffer.pop();
|
||||
assert_eq!(ring_buffer.pop(), Some(2));
|
||||
assert_eq!(ring_buffer.len(), 1);
|
||||
assert_eq!(ring_buffer.get(0), Some(&3));
|
||||
assert_eq!(ring_buffer.get(1), None);
|
||||
assert_eq!(ring_buffer.get(2), None);
|
||||
|
||||
ring_buffer.pop();
|
||||
assert_eq!(ring_buffer.pop(), Some(3));
|
||||
assert_eq!(ring_buffer.len(), 0);
|
||||
assert_eq!(ring_buffer.get(0), None);
|
||||
assert_eq!(ring_buffer.get(1), None);
|
||||
assert_eq!(ring_buffer.get(2), None);
|
||||
|
||||
assert_eq!(ring_buffer.pop(), None);
|
||||
|
||||
ring_buffer.push(1);
|
||||
ring_buffer.push(2);
|
||||
ring_buffer.push(3);
|
||||
assert_eq!(ring_buffer.len(), 3);
|
||||
assert_eq!(ring_buffer.get(0), Some(&1));
|
||||
assert_eq!(ring_buffer.get(1), Some(&2));
|
||||
assert_eq!(ring_buffer.get(2), Some(&3));
|
||||
|
||||
assert_eq!(ring_buffer.remove(1), Some(2));
|
||||
assert_eq!(ring_buffer.len(), 2);
|
||||
assert_eq!(ring_buffer.get(0), Some(&1));
|
||||
assert_eq!(ring_buffer.get(1), Some(&3));
|
||||
assert_eq!(ring_buffer.get(2), None);
|
||||
|
||||
assert_eq!(ring_buffer.remove(0), Some(1));
|
||||
assert_eq!(ring_buffer.len(), 1);
|
||||
assert_eq!(ring_buffer.get(0), Some(&3));
|
||||
assert_eq!(ring_buffer.get(1), None);
|
||||
assert_eq!(ring_buffer.get(2), None);
|
||||
|
||||
assert_eq!(ring_buffer.remove(1), None);
|
||||
assert_eq!(ring_buffer.remove(10), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -109,8 +109,6 @@ mod tests {
|
||||
async fn test_timer_name() {
|
||||
use tracing::{span, Level};
|
||||
|
||||
tracing_subscriber::fmt().with_max_level(tracing::Level::DEBUG).init();
|
||||
|
||||
tracing::warn!("Starting test...");
|
||||
|
||||
mod time123 {
|
||||
|
||||
@@ -1,4 +1,78 @@
|
||||
# unreleased
|
||||
# 0.7.0
|
||||
|
||||
- Add method to mark a list of inbound group sessions as backed up:
|
||||
`CryptoStore::mark_inbound_group_sessions_as_backed_up`
|
||||
|
||||
- `OlmMachine::toggle_room_key_forwarding` is replaced by two separate methods:
|
||||
|
||||
* `OlmMachine::set_room_key_requests_enabled`, which controls whether
|
||||
outgoing room key requests are enabled, and:
|
||||
|
||||
* `OlmMachine::set_room_key_forwarding_enabled`, which controls whether we
|
||||
automatically reply to incoming room key requests.
|
||||
|
||||
`OlmMachine::is_room_key_forwarding_enabled` is updated to return the setting
|
||||
of `OlmMachine::set_room_key_forwarding_enabled`, while
|
||||
`OlmMachine::are_room_key_requests_enabled` is added to return the setting of
|
||||
`OlmMachine::set_room_key_requests_enabled`.
|
||||
|
||||
([#2902](https://github.com/matrix-org/matrix-rust-sdk/pull/2902))
|
||||
|
||||
- Improve performance of `share_room_key`.
|
||||
([#2862](https://github.com/matrix-org/matrix-rust-sdk/pull/2862))
|
||||
|
||||
- `get_missing_sessions`: Don't block waiting for `/keys/query` requests on
|
||||
blacklisted servers, and improve performance.
|
||||
([#2845](https://github.com/matrix-org/matrix-rust-sdk/pull/2845))
|
||||
|
||||
- Generalize `olm::Session::encrypt` to accept any value implementing
|
||||
`Serialize` for the `value` parameter, instead of specifically
|
||||
`serde_json::Value`. Note that references to `Serialize`-implementing types
|
||||
themselves implement `Serialize`.
|
||||
|
||||
- Change the argument to `OlmMachine::receive_sync_changes` to be an
|
||||
`EncryptionSyncChanges` struct packing all the arguments instead of many
|
||||
single arguments. The new `next_batch_token` field there should be the
|
||||
`next_batch` value read from the latest sync response.
|
||||
|
||||
- Handle missing devices in `/keys/claim` responses.
|
||||
([#2805](https://github.com/matrix-org/matrix-rust-sdk/pull/2805))
|
||||
|
||||
- Add the higher level decryption method `decrypt_session_data` to the
|
||||
`BackupDecryptionKey` type.
|
||||
|
||||
- Add a higher level method to create signatures for the backup info. The
|
||||
`OlmMachine::backup_machine()::sign_backup()` method can be used to add
|
||||
signatures to a `RoomKeyBackupInfo`.
|
||||
|
||||
- Remove the `backups_v1` feature, backups support is now enabled by default.
|
||||
|
||||
- Use the `Signatures` type as the return value for the
|
||||
`MegolmV1BackupKey::signatures()` method.
|
||||
|
||||
- Add two new methods to import room keys,
|
||||
`OlmMachine::store()::import_exported_room_keys()` for file exports and
|
||||
`OlmMachine::backup_machine()::import_backed_up_room_keys()` for backups. The
|
||||
`OlmMachine::import_room_keys()` method is now deprecated.
|
||||
|
||||
- The parameter order of `OlmMachine::encrypt_room_event_raw` and
|
||||
`OutboundGroupSession::encrypt` has changed, `content` is now last
|
||||
- The parameter type of `content` has also changed, from `serde_json::Value`
|
||||
to `&Raw<AnyMessageLikeEventContent>`
|
||||
|
||||
- Change the return value of `bootstrap_cross_signing` so it returns an extra
|
||||
keys upload request. The three requests must be sent in the order they
|
||||
appear in the return tuple.
|
||||
|
||||
- Stop logging large quantities of data about the `Store` during olm
|
||||
decryption.
|
||||
|
||||
- Remove spurious "Unknown outgoing secret request" warning which was logged
|
||||
for every outgoing secret request.
|
||||
|
||||
- Clean up the logging of to-device messages in `share_room_key`.
|
||||
|
||||
- Expose new `OlmMachine::get_room_event_encryption_info` method.
|
||||
|
||||
- Add support for secret storage.
|
||||
|
||||
@@ -63,16 +137,3 @@
|
||||
|
||||
- Change the returned success value type of `BackupMachine::backup` from
|
||||
`OutgoingRequest` to `(OwnedTransactionId, KeysBackupRequest)`.
|
||||
|
||||
- Expose new `OlmMachine::get_room_event_encryption_info` method.
|
||||
|
||||
- Clean up the logging of to-device messages in `share_room_key`.
|
||||
|
||||
- Remove spurious "Unknown outgoing secret request" warning which was logged
|
||||
for every outgoing secret request.
|
||||
|
||||
- Stop logging large quantities of data about the `Store` during olm
|
||||
decryption.
|
||||
|
||||
- Change the return value of `bootstrap_cross_signing` so it returns an extra keys upload request.
|
||||
The three requests must be sent in the order they appear in the return tuple.
|
||||
|
||||
@@ -9,7 +9,7 @@ name = "matrix-sdk-crypto"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = { workspace = true }
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
@@ -19,7 +19,6 @@ default = []
|
||||
automatic-room-key-forwarding = []
|
||||
js = ["ruma/js", "vodozemac/js"]
|
||||
qrcode = ["dep:matrix-sdk-qrcode"]
|
||||
backups_v1 = ["dep:cbc"]
|
||||
message-ids = ["dep:ulid"]
|
||||
experimental-algorithms = []
|
||||
|
||||
@@ -29,11 +28,10 @@ testing = ["dep:http"]
|
||||
[dependencies]
|
||||
aes = "0.8.1"
|
||||
as_variant = { workspace = true }
|
||||
async-std = { version = "1.12.0", features = ["unstable"] }
|
||||
async-trait = { workspace = true }
|
||||
bs58 = { version = "0.5.0" }
|
||||
byteorder = { workspace = true }
|
||||
cbc = { version = "0.1.2", features = ["std"], optional = true }
|
||||
cbc = { version = "0.1.2", features = ["std"] }
|
||||
cfg-if = "1.0"
|
||||
ctr = "0.9.1"
|
||||
eyeball = { workspace = true }
|
||||
@@ -43,18 +41,18 @@ hkdf = "0.12.3"
|
||||
hmac = "0.12.1"
|
||||
http = { workspace = true, optional = true } # feature = testing only
|
||||
itertools = { workspace = true }
|
||||
matrix-sdk-qrcode = { version = "0.4.0", path = "../matrix-sdk-qrcode", optional = true }
|
||||
matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" }
|
||||
matrix-sdk-qrcode = { workspace = true, optional = true }
|
||||
matrix-sdk-common = { workspace = true }
|
||||
pbkdf2 = { version = "0.12.2", default-features = false }
|
||||
rand = "0.8.5"
|
||||
rand = { workspace = true }
|
||||
rmp-serde = "1.1.1"
|
||||
ruma = { workspace = true, features = ["rand", "canonical-json", "unstable-msc3814"] }
|
||||
serde = { workspace = true, features = ["derive", "rc"] }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
subtle = "2.5.0"
|
||||
tokio-stream = { version = "0.1.12", features = ["sync"] }
|
||||
tokio = { workspace = true, default-features = false, features = ["sync"] }
|
||||
tokio-stream = { workspace = true, features = ["sync"] }
|
||||
tokio = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true, features = ["attributes"] }
|
||||
ulid = { version = "1.0.0", optional = true }
|
||||
@@ -71,14 +69,12 @@ tokio = { workspace = true, features = ["time"] }
|
||||
anyhow = { workspace = true }
|
||||
assert_matches = { workspace = true }
|
||||
assert_matches2 = { workspace = true }
|
||||
ctor = { workspace = true }
|
||||
futures-executor = { workspace = true }
|
||||
http = { workspace = true }
|
||||
indoc = "2.0.1"
|
||||
matrix-sdk-test = { version = "0.6.0", path = "../../testing/matrix-sdk-test" }
|
||||
matrix-sdk-test = { workspace = true }
|
||||
olm-rs = { version = "2.2.0", features = ["serde"] }
|
||||
proptest = { version = "1.0.0", default-features = false, features = ["std"] }
|
||||
# required for async_test macro
|
||||
stream_assert = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||
|
||||
@@ -12,26 +12,22 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use ruma::{
|
||||
api::client::backup::{EncryptedSessionDataInit, KeyBackupData, KeyBackupDataInit},
|
||||
serde::Base64,
|
||||
OwnedDeviceKeyId, OwnedUserId,
|
||||
};
|
||||
use vodozemac::Curve25519PublicKey;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use super::{compat::PkEncryption, decryption::DecodeError};
|
||||
use crate::olm::InboundGroupSession;
|
||||
use crate::{olm::InboundGroupSession, types::Signatures};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InnerBackupKey {
|
||||
key: Curve25519PublicKey,
|
||||
signatures: BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceKeyId, String>>,
|
||||
signatures: Signatures,
|
||||
version: Mutex<Option<String>>,
|
||||
}
|
||||
|
||||
@@ -41,6 +37,7 @@ pub struct MegolmV1BackupKey {
|
||||
inner: Arc<InnerBackupKey>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl std::fmt::Debug for MegolmV1BackupKey {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter
|
||||
@@ -69,7 +66,7 @@ impl MegolmV1BackupKey {
|
||||
}
|
||||
|
||||
/// Get all the signatures of this `MegolmV1BackupKey`.
|
||||
pub fn signatures(&self) -> BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceKeyId, String>> {
|
||||
pub fn signatures(&self) -> Signatures {
|
||||
self.inner.signatures.to_owned()
|
||||
}
|
||||
|
||||
|
||||
@@ -207,6 +207,13 @@ pub enum Error {
|
||||
/// The message failed to be decoded.
|
||||
#[error("The message could not been decoded: {0}")]
|
||||
Decoding(#[from] MessageDecodeError),
|
||||
/// The message's Curve25519 key failed to be decoded.
|
||||
#[error("The message's ephemeral Curve25519 key could not been decoded: {0}")]
|
||||
InvalidCurveKey(#[from] KeyError),
|
||||
/// The decrypted message should contain a backed up room key, but the
|
||||
/// plaintext isn't valid JSON.
|
||||
#[error("The decrypted message isn't valid JSON: {0}")]
|
||||
Json(#[from] serde_json::error::Error),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -18,14 +18,20 @@ use std::{
|
||||
};
|
||||
|
||||
use bs58;
|
||||
use ruma::api::client::backup::EncryptedSessionData;
|
||||
use thiserror::Error;
|
||||
use zeroize::Zeroizing;
|
||||
use vodozemac::Curve25519PublicKey;
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
|
||||
use super::{
|
||||
compat::{Error as DecryptionError, Message, PkDecryption},
|
||||
MegolmV1BackupKey,
|
||||
};
|
||||
use crate::store::BackupDecryptionKey;
|
||||
use crate::{
|
||||
olm::BackedUpRoomKey,
|
||||
store::BackupDecryptionKey,
|
||||
types::{MegolmV1AuthData, RoomKeyBackupInfo},
|
||||
};
|
||||
|
||||
/// Error type for the decoding of a [`BackupDecryptionKey`].
|
||||
#[derive(Debug, Error)]
|
||||
@@ -53,16 +59,6 @@ pub enum DecodeError {
|
||||
PublicKey(#[from] vodozemac::KeyError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum UnpicklingError {
|
||||
#[error(transparent)]
|
||||
Json(#[from] serde_json::Error),
|
||||
// #[error("Couldn't decrypt the pickle: {0}")]
|
||||
// Decryption(String),
|
||||
#[error(transparent)]
|
||||
Decode(#[from] DecodeError),
|
||||
}
|
||||
|
||||
impl TryFrom<String> for BackupDecryptionKey {
|
||||
type Error = DecodeError;
|
||||
|
||||
@@ -187,6 +183,17 @@ impl BackupDecryptionKey {
|
||||
MegolmV1BackupKey::new(pk.public_key(), None)
|
||||
}
|
||||
|
||||
/// Get the [`RoomKeyBackupInfo`] for this [`BackupDecryptionKey`].
|
||||
///
|
||||
/// The [`RoomKeyBackupInfo`] can be uploaded to the homeserver to activate
|
||||
/// a new backup version.
|
||||
pub fn to_backup_info(&self) -> RoomKeyBackupInfo {
|
||||
let pk = self.get_pk_decryption();
|
||||
let auth_data = MegolmV1AuthData::new(pk.public_key(), Default::default());
|
||||
|
||||
RoomKeyBackupInfo::MegolmBackupV1Curve25519AesSha2(auth_data)
|
||||
}
|
||||
|
||||
/// Try to decrypt the given ciphertext using this [`BackupDecryptionKey`].
|
||||
///
|
||||
/// This will use the [`m.megolm_backup.v1.curve25519-aes-sha2`] algorithm
|
||||
@@ -207,15 +214,52 @@ impl BackupDecryptionKey {
|
||||
|
||||
Ok(String::from_utf8_lossy(&decrypted).to_string())
|
||||
}
|
||||
|
||||
/// Try to decrypt the given [`EncryptedSessionData`] using this
|
||||
/// [`BackupDecryptionKey`].
|
||||
pub fn decrypt_session_data(
|
||||
&self,
|
||||
session_data: EncryptedSessionData,
|
||||
) -> Result<BackedUpRoomKey, DecryptionError> {
|
||||
let message = Message {
|
||||
ciphertext: session_data.ciphertext.into_inner(),
|
||||
mac: session_data.mac.into_inner(),
|
||||
ephemeral_key: Curve25519PublicKey::from_slice(session_data.ephemeral.as_bytes())?,
|
||||
};
|
||||
|
||||
let pk = self.get_pk_decryption();
|
||||
|
||||
let mut decrypted = pk.decrypt(&message)?;
|
||||
let result = serde_json::from_slice(&decrypted);
|
||||
|
||||
decrypted.zeroize();
|
||||
|
||||
Ok(result?)
|
||||
}
|
||||
|
||||
/// Check if the given public key from the [`RoomKeyBackupInfo`] matches to
|
||||
/// this [`BackupDecryptionKey`].
|
||||
pub fn backup_key_matches(&self, info: &RoomKeyBackupInfo) -> bool {
|
||||
match info {
|
||||
RoomKeyBackupInfo::MegolmBackupV1Curve25519AesSha2(info) => {
|
||||
let pk = self.get_pk_decryption();
|
||||
let public_key = pk.public_key();
|
||||
|
||||
info.public_key == public_key
|
||||
}
|
||||
RoomKeyBackupInfo::Other { .. } => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::api::client::backup::KeyBackupData;
|
||||
use serde_json::json;
|
||||
|
||||
use super::{BackupDecryptionKey, DecodeError};
|
||||
use crate::olm::BackedUpRoomKey;
|
||||
use crate::olm::{BackedUpRoomKey, ExportedRoomKey, InboundGroupSession};
|
||||
|
||||
const TEST_KEY: [u8; 32] = [
|
||||
0x77, 0x07, 0x6D, 0x0A, 0x73, 0x18, 0xA5, 0x7D, 0x3C, 0x16, 0xC1, 0x72, 0x51, 0xB2, 0x66,
|
||||
@@ -223,6 +267,24 @@ mod tests {
|
||||
0x2C, 0x2A,
|
||||
];
|
||||
|
||||
fn room_key() -> ExportedRoomKey {
|
||||
let json = json!({
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"sender_key": "DeHIg4gwhClxzFYcmNntPNF9YtsdZbmMy8+3kzCMXHA",
|
||||
"session_id": "gM8i47Xhu0q52xLfgUXzanCMpLinoyVyH7R58cBuVBU",
|
||||
"room_id": "!DovneieKSTkdHKpIXy:morpheus.localhost",
|
||||
"session_key": "AQAAAABvWMNZjKFtebYIePKieQguozuoLgzeY6wKcyJjLJcJtQgy1dPqTBD12U+XrYLrRHn\
|
||||
lKmxoozlhFqJl456+9hlHCL+yq+6ScFuBHtJepnY1l2bdLb4T0JMDkNsNErkiLiLnD6yp3J\
|
||||
DSjIhkdHxmup/huygrmroq6/L5TaThEoqvW4DPIuO14btKudsS34FF82pwjKS4p6Mlch+0e\
|
||||
fHAblQV",
|
||||
"sender_claimed_keys":{},
|
||||
"forwarding_curve25519_key_chain":[]
|
||||
});
|
||||
|
||||
serde_json::from_value(json)
|
||||
.expect("We should be able to deserialize our backed up room key")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64_decoding() -> Result<(), DecodeError> {
|
||||
let key = BackupDecryptionKey::new().expect("Can't create a new recovery key");
|
||||
@@ -306,5 +368,35 @@ mod tests {
|
||||
|
||||
let _: BackedUpRoomKey = serde_json::from_str(&decrypted)
|
||||
.expect("The decrypted payload should contain valid JSON");
|
||||
|
||||
let _ = decryption_key
|
||||
.decrypt_session_data(key_backup_data.session_data)
|
||||
.expect("The backed up key should be decrypted successfully");
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_encryption_cycle() {
|
||||
let session = InboundGroupSession::from_export(&room_key()).unwrap();
|
||||
|
||||
let decryption_key = BackupDecryptionKey::new().unwrap();
|
||||
let encryption_key = decryption_key.megolm_v1_public_key();
|
||||
|
||||
let encrypted = encryption_key.encrypt(session).await;
|
||||
|
||||
let _ = decryption_key
|
||||
.decrypt_session_data(encrypted.session_data)
|
||||
.expect("We should be able to decrypt a just encrypted room key");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_matches() {
|
||||
let decryption_key = BackupDecryptionKey::new().unwrap();
|
||||
|
||||
let key_info = decryption_key.to_backup_info();
|
||||
|
||||
assert!(
|
||||
decryption_key.backup_key_matches(&key_info),
|
||||
"The backup info should match the decryption key"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,5 +52,5 @@ mod compat;
|
||||
mod decryption;
|
||||
|
||||
pub use backup::MegolmV1BackupKey;
|
||||
pub use compat::{Error as DecryptionError, MessageDecodeError};
|
||||
pub use compat::Error as DecryptionError;
|
||||
pub use decryption::DecodeError;
|
||||
|
||||
@@ -30,16 +30,17 @@ use std::{
|
||||
|
||||
use ruma::{
|
||||
api::client::backup::RoomKeyBackup, serde::Raw, DeviceId, DeviceKeyAlgorithm, OwnedDeviceId,
|
||||
OwnedRoomId, OwnedTransactionId, TransactionId,
|
||||
OwnedRoomId, OwnedTransactionId, RoomId, TransactionId,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{debug, info, instrument, trace, warn};
|
||||
|
||||
use crate::{
|
||||
olm::{InboundGroupSession, SignedJsonObject},
|
||||
olm::{BackedUpRoomKey, ExportedRoomKey, InboundGroupSession, SignedJsonObject},
|
||||
store::{BackupDecryptionKey, BackupKeys, Changes, RoomKeyCounts, Store},
|
||||
types::{MegolmV1AuthData, RoomKeyBackupInfo, Signatures},
|
||||
CryptoStoreError, Device, KeysBackupRequest, OutgoingRequest,
|
||||
CryptoStoreError, Device, KeysBackupRequest, OutgoingRequest, RoomKeyImportResult,
|
||||
SignatureError,
|
||||
};
|
||||
|
||||
mod keys;
|
||||
@@ -59,22 +60,14 @@ pub struct BackupMachine {
|
||||
pending_backup: Arc<RwLock<Option<PendingBackup>>>,
|
||||
}
|
||||
|
||||
type SenderKey = String;
|
||||
type SessionId = String;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PendingBackup {
|
||||
request_id: OwnedTransactionId,
|
||||
request: KeysBackupRequest,
|
||||
sessions: BTreeMap<OwnedRoomId, BTreeMap<String, BTreeSet<String>>>,
|
||||
}
|
||||
|
||||
impl PendingBackup {
|
||||
fn session_was_part_of_the_backup(&self, session: &InboundGroupSession) -> bool {
|
||||
self.sessions
|
||||
.get(session.room_id())
|
||||
.and_then(|r| {
|
||||
r.get(&session.sender_key().to_base64()).map(|s| s.contains(session.session_id()))
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
sessions: BTreeMap<OwnedRoomId, BTreeMap<SenderKey, BTreeSet<SessionId>>>,
|
||||
}
|
||||
|
||||
impl From<PendingBackup> for OutgoingRequest {
|
||||
@@ -325,7 +318,7 @@ impl BackupMachine {
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `backup_version`: The backup version that should be verified. Should
|
||||
/// * `backup_info`: The backup info that should be verified. Should
|
||||
/// be fetched from the server using the [`/room_keys/version`] endpoint.
|
||||
///
|
||||
/// * `compute_all_signatures`: *Useful for debugging only*. If this
|
||||
@@ -349,6 +342,46 @@ impl BackupMachine {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sign a [`RoomKeyBackupInfo`] using the device's identity key and, if
|
||||
/// available, the cross-signing master key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `backup_info`: The backup version that should be verified. Should
|
||||
/// be created from the [`BackupDecryptionKey`] using the
|
||||
/// [`BackupDecryptionKey::to_backup_info()`] method.
|
||||
pub async fn sign_backup(
|
||||
&self,
|
||||
backup_info: &mut RoomKeyBackupInfo,
|
||||
) -> Result<(), SignatureError> {
|
||||
if let RoomKeyBackupInfo::MegolmBackupV1Curve25519AesSha2(data) = backup_info {
|
||||
let canonical_json = data.to_canonical_json()?;
|
||||
|
||||
let private_identity = self.store.private_identity();
|
||||
let identity = private_identity.lock().await;
|
||||
|
||||
if let Some(key_id) = identity.master_key_id().await {
|
||||
if let Ok(signature) = identity.sign(&canonical_json).await {
|
||||
data.signatures.add_signature(
|
||||
self.store.user_id().to_owned(),
|
||||
key_id,
|
||||
signature,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let cache = self.store.cache().await?;
|
||||
let account = cache.account().await?;
|
||||
let key_id = account.signing_key_id();
|
||||
let signature = account.sign(&canonical_json);
|
||||
data.signatures.add_signature(self.store.user_id().to_owned(), key_id, signature);
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SignatureError::UnsupportedAlgorithm)
|
||||
}
|
||||
}
|
||||
|
||||
/// Activate the given backup key to be used to encrypt and backup room
|
||||
/// keys.
|
||||
///
|
||||
@@ -436,44 +469,38 @@ impl BackupMachine {
|
||||
request_id: &TransactionId,
|
||||
) -> Result<(), CryptoStoreError> {
|
||||
let mut request = self.pending_backup.write().await;
|
||||
|
||||
if let Some(r) = &*request {
|
||||
if r.request_id == request_id {
|
||||
let sessions: Vec<_> = self
|
||||
.store
|
||||
.get_inbound_group_sessions()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|s| r.session_was_part_of_the_backup(s))
|
||||
let room_and_session_ids: Vec<(&RoomId, &str)> = r
|
||||
.sessions
|
||||
.iter()
|
||||
.flat_map(|(room_id, sender_key_to_session_ids)| {
|
||||
std::iter::repeat(room_id).zip(sender_key_to_session_ids.values().flatten())
|
||||
})
|
||||
.map(|(room_id, session_id)| (room_id.as_ref(), session_id.as_str()))
|
||||
.collect();
|
||||
|
||||
for session in &sessions {
|
||||
session.mark_as_backed_up();
|
||||
}
|
||||
|
||||
trace!(request_id = ?r.request_id, keys = ?r.sessions, "Marking room keys as backed up");
|
||||
|
||||
let changes = Changes { inbound_group_sessions: sessions, ..Default::default() };
|
||||
self.store.save_changes(changes).await?;
|
||||
|
||||
let counts = self.store.inbound_group_session_counts().await?;
|
||||
self.store.mark_inbound_group_sessions_as_backed_up(&room_and_session_ids).await?;
|
||||
|
||||
trace!(
|
||||
room_key_counts = ?counts,
|
||||
request_id = ?r.request_id, keys = ?r.sessions, "Marked room keys as backed up"
|
||||
request_id = ?r.request_id,
|
||||
keys = ?r.sessions,
|
||||
"Marked room keys as backed up"
|
||||
);
|
||||
|
||||
*request = None;
|
||||
} else {
|
||||
warn!(
|
||||
expected = r.request_id.to_string().as_str(),
|
||||
got = request_id.to_string().as_str(),
|
||||
expected = ?r.request_id,
|
||||
got = ?request_id,
|
||||
"Tried to mark a pending backup as sent but the request id didn't match"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
request_id = request_id.to_string().as_str(),
|
||||
?request_id,
|
||||
"Tried to mark a pending backup as sent but there isn't a backup pending"
|
||||
);
|
||||
};
|
||||
@@ -525,10 +552,10 @@ impl BackupMachine {
|
||||
backup_key: &MegolmV1BackupKey,
|
||||
) -> (
|
||||
BTreeMap<OwnedRoomId, RoomKeyBackup>,
|
||||
BTreeMap<OwnedRoomId, BTreeMap<String, BTreeSet<String>>>,
|
||||
BTreeMap<OwnedRoomId, BTreeMap<SenderKey, BTreeSet<SessionId>>>,
|
||||
) {
|
||||
let mut backup: BTreeMap<OwnedRoomId, RoomKeyBackup> = BTreeMap::new();
|
||||
let mut session_record: BTreeMap<OwnedRoomId, BTreeMap<String, BTreeSet<String>>> =
|
||||
let mut session_record: BTreeMap<OwnedRoomId, BTreeMap<SenderKey, BTreeSet<SessionId>>> =
|
||||
BTreeMap::new();
|
||||
|
||||
for session in sessions {
|
||||
@@ -555,15 +582,69 @@ impl BackupMachine {
|
||||
|
||||
(backup, session_record)
|
||||
}
|
||||
|
||||
/// Import the given room keys into our store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_keys` - A list of previously exported keys that should be
|
||||
/// imported into our store. If we already have a better version of a key
|
||||
/// the key will *not* be imported.
|
||||
///
|
||||
/// Returns a [`RoomKeyImportResult`] containing information about room keys
|
||||
/// which were imported.
|
||||
pub async fn import_backed_up_room_keys(
|
||||
&self,
|
||||
room_keys: BTreeMap<OwnedRoomId, BTreeMap<String, BackedUpRoomKey>>,
|
||||
progress_listener: impl Fn(usize, usize),
|
||||
) -> Result<RoomKeyImportResult, CryptoStoreError> {
|
||||
let mut decrypted_room_keys = vec![];
|
||||
|
||||
for (room_id, room_keys) in room_keys {
|
||||
for (session_id, room_key) in room_keys {
|
||||
let room_key = ExportedRoomKey::from_backed_up_room_key(
|
||||
room_id.to_owned(),
|
||||
session_id,
|
||||
room_key,
|
||||
);
|
||||
|
||||
decrypted_room_keys.push(room_key);
|
||||
}
|
||||
}
|
||||
|
||||
self.store.import_room_keys(decrypted_room_keys, true, progress_listener).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use assert_matches2::assert_let;
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{device_id, room_id, user_id, CanonicalJsonValue, DeviceId, RoomId, UserId};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{store::BackupDecryptionKey, types::RoomKeyBackupInfo, OlmError, OlmMachine};
|
||||
use crate::{
|
||||
olm::BackedUpRoomKey, store::BackupDecryptionKey, types::RoomKeyBackupInfo, OlmError,
|
||||
OlmMachine,
|
||||
};
|
||||
|
||||
fn room_key() -> BackedUpRoomKey {
|
||||
let json = json!({
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"sender_key": "DeHIg4gwhClxzFYcmNntPNF9YtsdZbmMy8+3kzCMXHA",
|
||||
"session_key": "AQAAAABvWMNZjKFtebYIePKieQguozuoLgzeY6wKcyJjLJcJtQgy1dPqTBD12U+XrYLrRHn\
|
||||
lKmxoozlhFqJl456+9hlHCL+yq+6ScFuBHtJepnY1l2bdLb4T0JMDkNsNErkiLiLnD6yp3J\
|
||||
DSjIhkdHxmup/huygrmroq6/L5TaThEoqvW4DPIuO14btKudsS34FF82pwjKS4p6Mlch+0e\
|
||||
fHAblQV",
|
||||
"sender_claimed_keys":{},
|
||||
"forwarding_curve25519_key_chain":[]
|
||||
});
|
||||
|
||||
serde_json::from_value(json)
|
||||
.expect("We should be able to deserialize our backed up room key")
|
||||
}
|
||||
|
||||
fn alice_id() -> &'static UserId {
|
||||
user_id!("@alice:example.org")
|
||||
@@ -588,8 +669,8 @@ mod tests {
|
||||
assert_eq!(counts.total, 0, "Initially no keys exist");
|
||||
assert_eq!(counts.backed_up, 0, "Initially no backed up keys exist");
|
||||
|
||||
machine.create_outbound_group_session_with_defaults(room_id()).await?;
|
||||
machine.create_outbound_group_session_with_defaults(room_id2()).await?;
|
||||
machine.create_outbound_group_session_with_defaults_test_helper(room_id()).await?;
|
||||
machine.create_outbound_group_session_with_defaults_test_helper(room_id2()).await?;
|
||||
|
||||
let counts = backup_machine.store.inbound_group_session_counts().await?;
|
||||
assert_eq!(counts.total, 2, "Two room keys need to exist in the store");
|
||||
@@ -717,4 +798,56 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn import_backed_up_room_keys() {
|
||||
let machine = OlmMachine::new(alice_id(), alice_device_id()).await;
|
||||
let backup_machine = machine.backup_machine();
|
||||
|
||||
let room_id = room_id!("!DovneieKSTkdHKpIXy:morpheus.localhost");
|
||||
let session_id = "gM8i47Xhu0q52xLfgUXzanCMpLinoyVyH7R58cBuVBU";
|
||||
let room_key = room_key();
|
||||
|
||||
let room_keys: BTreeMap<_, BTreeMap<_, _>> = BTreeMap::from([(
|
||||
room_id.to_owned(),
|
||||
BTreeMap::from([(session_id.to_owned(), room_key)]),
|
||||
)]);
|
||||
|
||||
let session = machine.store().get_inbound_group_session(room_id, session_id).await.unwrap();
|
||||
|
||||
assert!(session.is_none(), "Initially we should not have the session in the store");
|
||||
|
||||
backup_machine
|
||||
.import_backed_up_room_keys(room_keys, |_, _| {})
|
||||
.await
|
||||
.expect("We should be able to import a room key");
|
||||
|
||||
let session = machine.store().get_inbound_group_session(room_id, session_id).await.unwrap();
|
||||
|
||||
assert_let!(Some(session) = session);
|
||||
assert!(
|
||||
session.backed_up(),
|
||||
"If a session was imported from a backup, it should be considered to be backed up"
|
||||
);
|
||||
assert!(session.has_been_imported());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn sign_backup_info() {
|
||||
let machine = OlmMachine::new(alice_id(), alice_device_id()).await;
|
||||
let backup_machine = machine.backup_machine();
|
||||
|
||||
let decryption_key = BackupDecryptionKey::new().unwrap();
|
||||
let mut backup_info = decryption_key.to_backup_info();
|
||||
|
||||
let result = backup_machine.verify_backup(backup_info.to_owned(), false).await.unwrap();
|
||||
|
||||
assert!(!result.trusted());
|
||||
|
||||
backup_machine.sign_backup(&mut backup_info).await.unwrap();
|
||||
|
||||
let result = backup_machine.verify_backup(backup_info, false).await.unwrap();
|
||||
|
||||
assert!(result.trusted());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ pub struct AttachmentDecryptor<'a, R: Read> {
|
||||
aes: Aes256Ctr,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a, R: 'a + Read + std::fmt::Debug> std::fmt::Debug for AttachmentDecryptor<'a, R> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("AttachmentDecryptor")
|
||||
@@ -165,6 +166,7 @@ pub struct AttachmentEncryptor<'a, R: Read + ?Sized> {
|
||||
sha: Sha256,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a, R: 'a + Read + std::fmt::Debug + ?Sized> std::fmt::Debug for AttachmentEncryptor<'a, R> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("AttachmentEncryptor")
|
||||
|
||||
@@ -292,7 +292,7 @@ mod tests {
|
||||
let (machine, _) = get_prepared_machine_test_helper(user_id, false).await;
|
||||
let room_id = room_id!("!test:localhost");
|
||||
|
||||
machine.create_outbound_group_session_with_defaults(room_id).await.unwrap();
|
||||
machine.create_outbound_group_session_with_defaults_test_helper(room_id).await.unwrap();
|
||||
let export = machine.export_room_keys(|s| s.room_id() == room_id).await.unwrap();
|
||||
|
||||
assert!(!export.is_empty());
|
||||
@@ -305,7 +305,7 @@ mod tests {
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
machine.import_room_keys(decrypted, false, |_, _| {}).await.unwrap(),
|
||||
machine.store().import_exported_room_keys(decrypted, |_, _| {}).await.unwrap(),
|
||||
RoomKeyImportResult::new(0, 1, BTreeMap::new())
|
||||
);
|
||||
}
|
||||
@@ -316,7 +316,7 @@ mod tests {
|
||||
|
||||
let (machine, _) = get_prepared_machine_test_helper(user_id, false).await;
|
||||
let room_id = room_id!("!test:localhost");
|
||||
let session = machine.create_inbound_session(room_id).await?;
|
||||
let session = machine.create_inbound_session_test_helper(room_id).await?;
|
||||
|
||||
let export = vec![session.export_at_index(10).await];
|
||||
|
||||
@@ -332,19 +332,22 @@ mod tests {
|
||||
)]),
|
||||
);
|
||||
|
||||
assert_eq!(machine.import_room_keys(export, false, |_, _| {}).await?, keys);
|
||||
assert_eq!(machine.store().import_exported_room_keys(export, |_, _| {}).await?, keys);
|
||||
|
||||
let export = vec![session.export_at_index(10).await];
|
||||
assert_eq!(
|
||||
machine.import_room_keys(export, false, |_, _| {}).await?,
|
||||
machine.store().import_exported_room_keys(export, |_, _| {}).await?,
|
||||
RoomKeyImportResult::new(0, 1, BTreeMap::new())
|
||||
);
|
||||
|
||||
let better_export = vec![session.export().await];
|
||||
|
||||
assert_eq!(machine.import_room_keys(better_export, false, |_, _| {}).await?, keys);
|
||||
assert_eq!(
|
||||
machine.store().import_exported_room_keys(better_export, |_, _| {}).await?,
|
||||
keys
|
||||
);
|
||||
|
||||
let another_session = machine.create_inbound_session(room_id).await?;
|
||||
let another_session = machine.create_inbound_session_test_helper(room_id).await?;
|
||||
let export = vec![another_session.export_at_index(10).await];
|
||||
|
||||
let keys = RoomKeyImportResult::new(
|
||||
@@ -359,7 +362,7 @@ mod tests {
|
||||
)]),
|
||||
);
|
||||
|
||||
assert_eq!(machine.import_room_keys(export, false, |_, _| {}).await?, keys);
|
||||
assert_eq!(machine.store().import_exported_room_keys(export, |_, _| {}).await?, keys);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -73,7 +73,13 @@ pub(crate) struct GossipMachineInner {
|
||||
incoming_key_requests: StdRwLock<BTreeMap<RequestInfo, RequestEvent>>,
|
||||
wait_queue: WaitQueue,
|
||||
users_for_key_claim: Arc<StdRwLock<BTreeMap<OwnedUserId, BTreeSet<OwnedDeviceId>>>>,
|
||||
|
||||
/// Whether we should respond to incoming `m.room_key_request` messages.
|
||||
room_key_forwarding_enabled: AtomicBool,
|
||||
|
||||
/// Whether we should send out `m.room_key_request` messages.
|
||||
room_key_requests_enabled: AtomicBool,
|
||||
|
||||
identity_manager: IdentityManager,
|
||||
}
|
||||
|
||||
@@ -87,6 +93,9 @@ impl GossipMachine {
|
||||
let room_key_forwarding_enabled =
|
||||
AtomicBool::new(cfg!(feature = "automatic-room-key-forwarding"));
|
||||
|
||||
let room_key_requests_enabled =
|
||||
AtomicBool::new(cfg!(feature = "automatic-room-key-forwarding"));
|
||||
|
||||
Self {
|
||||
inner: Arc::new(GossipMachineInner {
|
||||
store,
|
||||
@@ -97,6 +106,7 @@ impl GossipMachine {
|
||||
wait_queue: WaitQueue::new(),
|
||||
users_for_key_claim,
|
||||
room_key_forwarding_enabled,
|
||||
room_key_requests_enabled,
|
||||
identity_manager,
|
||||
}),
|
||||
}
|
||||
@@ -107,7 +117,7 @@ impl GossipMachine {
|
||||
}
|
||||
|
||||
#[cfg(feature = "automatic-room-key-forwarding")]
|
||||
pub fn toggle_room_key_forwarding(&self, enabled: bool) {
|
||||
pub fn set_room_key_forwarding_enabled(&self, enabled: bool) {
|
||||
self.inner.room_key_forwarding_enabled.store(enabled, Ordering::SeqCst)
|
||||
}
|
||||
|
||||
@@ -115,6 +125,19 @@ impl GossipMachine {
|
||||
self.inner.room_key_forwarding_enabled.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// Configure whether we should send outgoing `m.room_key_request`s on
|
||||
/// decryption failure.
|
||||
#[cfg(feature = "automatic-room-key-forwarding")]
|
||||
pub fn set_room_key_requests_enabled(&self, enabled: bool) {
|
||||
self.inner.room_key_requests_enabled.store(enabled, Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// Query whether we should send outgoing `m.room_key_request`s on
|
||||
/// decryption failure.
|
||||
pub fn are_room_key_requests_enabled(&self) -> bool {
|
||||
self.inner.room_key_requests_enabled.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// Load stored outgoing requests that were not yet sent out.
|
||||
async fn load_outgoing_requests(&self) -> Result<Vec<OutgoingRequest>, CryptoStoreError> {
|
||||
Ok(self
|
||||
@@ -294,8 +317,8 @@ impl GossipMachine {
|
||||
if device.user_id() == self.user_id() {
|
||||
if device.is_verified() {
|
||||
info!(
|
||||
user_id = device.user_id().as_str(),
|
||||
device_id = device.device_id().as_str(),
|
||||
user_id = ?device.user_id(),
|
||||
device_id = ?device.device_id(),
|
||||
?secret_name,
|
||||
"Sharing a secret with a device",
|
||||
);
|
||||
@@ -304,9 +327,9 @@ impl GossipMachine {
|
||||
Ok(s) => Ok(Some(s)),
|
||||
Err(OlmError::MissingSession) => {
|
||||
info!(
|
||||
user_id = device.user_id().as_str(),
|
||||
device_id = device.device_id().as_str(),
|
||||
secret_name = secret_name.as_ref(),
|
||||
user_id = ?device.user_id(),
|
||||
device_id = ?device.device_id(),
|
||||
?secret_name,
|
||||
"Secret request is missing an Olm session, \
|
||||
putting the request in the wait queue",
|
||||
);
|
||||
@@ -318,8 +341,8 @@ impl GossipMachine {
|
||||
}?
|
||||
} else {
|
||||
info!(
|
||||
user_id = device.user_id().as_str(),
|
||||
device_id = device.device_id().as_str(),
|
||||
user_id = ?device.user_id(),
|
||||
device_id = ?device.device_id(),
|
||||
?secret_name,
|
||||
"Received a secret request that we won't serve, the device isn't trusted",
|
||||
);
|
||||
@@ -328,8 +351,8 @@ impl GossipMachine {
|
||||
}
|
||||
} else {
|
||||
info!(
|
||||
user_id = device.user_id().as_str(),
|
||||
device_id = device.device_id().as_str(),
|
||||
user_id = ?device.user_id(),
|
||||
device_id = ?device.device_id(),
|
||||
?secret_name,
|
||||
"Received a secret request that we won't serve, the device doesn't belong to us",
|
||||
);
|
||||
@@ -338,8 +361,8 @@ impl GossipMachine {
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
user_id = event.sender.as_str(),
|
||||
device_id = event.content.requesting_device_id.as_str(),
|
||||
user_id = ?event.sender,
|
||||
device_id = ?event.content.requesting_device_id,
|
||||
?secret_name,
|
||||
"Received a secret request from an unknown device",
|
||||
);
|
||||
@@ -445,9 +468,9 @@ impl GossipMachine {
|
||||
#[tracing::instrument(
|
||||
skip_all,
|
||||
fields(
|
||||
user_id = %event.sender,
|
||||
device_id = %event.content.requesting_device_id,
|
||||
%room_id,
|
||||
user_id = ?event.sender,
|
||||
device_id = ?event.content.requesting_device_id,
|
||||
?room_id,
|
||||
session_id
|
||||
)
|
||||
)]
|
||||
@@ -516,7 +539,6 @@ impl GossipMachine {
|
||||
content: SecretSendContent,
|
||||
) -> OlmResult<Session> {
|
||||
let event_type = content.event_type();
|
||||
let content = serde_json::to_value(content)?;
|
||||
let (used_session, content) = device.encrypt(event_type, content).await?;
|
||||
|
||||
let request = ToDeviceRequest::new(
|
||||
@@ -606,8 +628,9 @@ impl GossipMachine {
|
||||
let outbound_session = self
|
||||
.inner
|
||||
.outbound_group_sessions
|
||||
.get_with_id(session.room_id(), session.session_id())
|
||||
.await;
|
||||
.get_or_load(session.room_id())
|
||||
.await
|
||||
.filter(|outgoing_session| outgoing_session.session_id() == session.session_id());
|
||||
|
||||
// If this is our own, verified device, we share the entire session from the
|
||||
// earliest known index.
|
||||
@@ -618,7 +641,7 @@ impl GossipMachine {
|
||||
// at. For this, we need an outbound session because this
|
||||
// information is recorded there.
|
||||
} else if let Some(outbound) = outbound_session {
|
||||
match outbound.is_shared_with(device) {
|
||||
match outbound.is_shared_with(&device.inner) {
|
||||
ShareState::Shared(message_index) => Ok(Some(message_index)),
|
||||
ShareState::SharedButChangedSenderKey => Err(KeyForwardDecision::ChangedSenderKey),
|
||||
ShareState::NotShared => Err(KeyForwardDecision::OutboundSessionNotShared),
|
||||
@@ -641,7 +664,7 @@ impl GossipMachine {
|
||||
/// the key we wish to request.
|
||||
#[cfg(feature = "automatic-room-key-forwarding")]
|
||||
async fn should_request_key(&self, key_info: &SecretInfo) -> Result<bool, CryptoStoreError> {
|
||||
if self.inner.room_key_forwarding_enabled.load(Ordering::SeqCst) {
|
||||
if self.inner.room_key_requests_enabled.load(Ordering::SeqCst) {
|
||||
let request = self.inner.store.get_secret_request_by_info(key_info).await?;
|
||||
|
||||
// Don't send out duplicate requests, users can re-request them if they
|
||||
@@ -754,7 +777,10 @@ impl GossipMachine {
|
||||
) -> Result<bool, CryptoStoreError> {
|
||||
if let Some(info) = event.room_key_info(room_id).map(|i| i.into()) {
|
||||
if self.should_request_key(&info).await? {
|
||||
self.request_key_helper(info).await?;
|
||||
// Size of the request_key_helper future should not impact this
|
||||
// async fn since it is likely enough that this branch won't be
|
||||
// entered.
|
||||
Box::pin(self.request_key_helper(info)).await?;
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
@@ -785,9 +811,9 @@ impl GossipMachine {
|
||||
|
||||
if let Some(mut info) = info {
|
||||
trace!(
|
||||
recipient = info.request_recipient.as_str(),
|
||||
recipient = ?info.request_recipient,
|
||||
request_type = info.request_type(),
|
||||
request_id = info.request_id.to_string().as_str(),
|
||||
request_id = ?info.request_id,
|
||||
"Marking outgoing secret request as sent"
|
||||
);
|
||||
info.sent_out = true;
|
||||
@@ -804,9 +830,9 @@ impl GossipMachine {
|
||||
/// This will queue up a request cancellation.
|
||||
async fn mark_as_done(&self, key_info: &GossipRequest) -> Result<(), CryptoStoreError> {
|
||||
trace!(
|
||||
recipient = key_info.request_recipient.as_str(),
|
||||
recipient = ?key_info.request_recipient,
|
||||
request_type = key_info.request_type(),
|
||||
request_id = key_info.request_id.to_string().as_str(),
|
||||
request_id = ?key_info.request_id,
|
||||
"Successfully received a secret, removing the request"
|
||||
);
|
||||
|
||||
@@ -835,7 +861,7 @@ impl GossipMachine {
|
||||
// secret.
|
||||
Err(e) => {
|
||||
warn!(
|
||||
secret_name = %secret.secret_name,
|
||||
secret_name = ?secret.secret_name,
|
||||
error = ?e,
|
||||
"Error while importing a secret"
|
||||
);
|
||||
@@ -948,7 +974,7 @@ impl GossipMachine {
|
||||
info!(
|
||||
?sender_key,
|
||||
claimed_sender_key = ?session.sender_key(),
|
||||
room_id = session.room_id().as_str(),
|
||||
room_id = ?session.room_id(),
|
||||
session_id = session.session_id(),
|
||||
algorithm = ?session.algorithm(),
|
||||
"Received a forwarded room key",
|
||||
@@ -1069,7 +1095,7 @@ mod tests {
|
||||
|
||||
#[cfg(feature = "automatic-room-key-forwarding")]
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk_test::async_test;
|
||||
use matrix_sdk_test::{async_test, message_like_event_content};
|
||||
use ruma::{
|
||||
device_id, event_id,
|
||||
events::{
|
||||
@@ -1080,7 +1106,6 @@ mod tests {
|
||||
serde::Raw,
|
||||
user_id, DeviceId, RoomId, UserId,
|
||||
};
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::GossipMachine;
|
||||
@@ -1101,12 +1126,12 @@ mod tests {
|
||||
identities::{IdentityManager, LocalTrust, ReadOnlyDevice},
|
||||
olm::{Account, PrivateCrossSigningIdentity},
|
||||
session_manager::GroupSessionCache,
|
||||
store::{CryptoStoreWrapper, MemoryStore, PendingChanges, Store},
|
||||
types::events::room::encrypted::{EncryptedEvent, RoomEncryptedEventContent},
|
||||
store::{Changes, CryptoStoreWrapper, MemoryStore, PendingChanges, Store},
|
||||
types::events::room::encrypted::{
|
||||
EncryptedEvent, EncryptedToDeviceEvent, RoomEncryptedEventContent,
|
||||
},
|
||||
verification::VerificationMachine,
|
||||
};
|
||||
#[cfg(any(feature = "automatic-room-key-forwarding", feature = "backups_v1"))]
|
||||
use crate::{store::Changes, types::events::room::encrypted::EncryptedToDeviceEvent};
|
||||
|
||||
fn alice_id() -> &'static UserId {
|
||||
user_id!("@alice:example.org")
|
||||
@@ -1253,7 +1278,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let content = group_session.encrypt(json!({}), "m.dummy").await;
|
||||
let content = group_session.encrypt("m.dummy", &message_like_event_content!({})).await;
|
||||
let event = wrap_encrypted_content(bob_machine.user_id(), content);
|
||||
|
||||
// Alice wants to request the outbound group session from bob.
|
||||
@@ -1276,7 +1301,6 @@ mod tests {
|
||||
(alice_machine, group_session, bob_machine)
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "automatic-room-key-forwarding", feature = "backups_v1"))]
|
||||
fn extract_content<'a>(
|
||||
recipient: &UserId,
|
||||
request: &'a crate::OutgoingRequest,
|
||||
@@ -1309,7 +1333,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "automatic-room-key-forwarding", feature = "backups_v1"))]
|
||||
fn request_to_event<C>(
|
||||
recipient: &UserId,
|
||||
sender: &UserId,
|
||||
@@ -1343,7 +1366,7 @@ mod tests {
|
||||
|
||||
let (outbound, session) = account.create_group_session_pair_with_defaults(room_id()).await;
|
||||
|
||||
let content = outbound.encrypt(json!({}), "m.dummy").await;
|
||||
let content = outbound.encrypt("m.dummy", &message_like_event_content!({})).await;
|
||||
let event = wrap_encrypted_content(machine.user_id(), content);
|
||||
|
||||
assert!(machine.outgoing_to_device_requests().await.unwrap().is_empty());
|
||||
@@ -1371,7 +1394,7 @@ mod tests {
|
||||
machine.inner.store.save_devices(&[alice_device]).await.unwrap();
|
||||
|
||||
let (outbound, session) = account.create_group_session_pair_with_defaults(room_id()).await;
|
||||
let content = outbound.encrypt(json!({}), "m.dummy").await;
|
||||
let content = outbound.encrypt("m.dummy", &message_like_event_content!({})).await;
|
||||
let event = wrap_encrypted_content(machine.user_id(), content);
|
||||
|
||||
assert!(machine.outgoing_to_device_requests().await.unwrap().is_empty());
|
||||
@@ -1384,12 +1407,41 @@ mod tests {
|
||||
let requests = machine.outgoing_to_device_requests().await.unwrap();
|
||||
assert_eq!(requests.len(), 1);
|
||||
|
||||
let request = requests.get(0).unwrap();
|
||||
let request = &requests[0];
|
||||
|
||||
machine.mark_outgoing_request_as_sent(&request.request_id).await.unwrap();
|
||||
assert!(machine.outgoing_to_device_requests().await.unwrap().is_empty());
|
||||
}
|
||||
|
||||
/// We should *not* request keys if that has been disabled
|
||||
#[async_test]
|
||||
#[cfg(feature = "automatic-room-key-forwarding")]
|
||||
async fn create_key_request_requests_disabled() {
|
||||
let machine = get_machine_test_helper().await;
|
||||
let account = account();
|
||||
let second_account = alice_2_account();
|
||||
let alice_device = ReadOnlyDevice::from_account(&second_account);
|
||||
|
||||
// We need a trusted device, otherwise we won't request keys
|
||||
alice_device.set_trust_state(LocalTrust::Verified);
|
||||
machine.inner.store.save_devices(&[alice_device]).await.unwrap();
|
||||
|
||||
// Disable key requests
|
||||
assert!(machine.are_room_key_requests_enabled());
|
||||
machine.set_room_key_requests_enabled(false);
|
||||
assert!(!machine.are_room_key_requests_enabled());
|
||||
|
||||
let (outbound, session) = account.create_group_session_pair_with_defaults(room_id()).await;
|
||||
let content = outbound.encrypt("m.dummy", &message_like_event_content!({})).await;
|
||||
let event = wrap_encrypted_content(machine.user_id(), content);
|
||||
|
||||
// The outgoing to-device requests should be empty before and after
|
||||
// `create_outgoing_key_request`.
|
||||
assert!(machine.outgoing_to_device_requests().await.unwrap().is_empty());
|
||||
machine.create_outgoing_key_request(session.room_id(), &event).await.unwrap();
|
||||
assert!(machine.outgoing_to_device_requests().await.unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
#[cfg(feature = "automatic-room-key-forwarding")]
|
||||
async fn receive_forwarded_key() {
|
||||
@@ -1404,13 +1456,13 @@ mod tests {
|
||||
machine.inner.store.save_devices(&[alice_device.clone()]).await.unwrap();
|
||||
|
||||
let (outbound, session) = account.create_group_session_pair_with_defaults(room_id()).await;
|
||||
let content = outbound.encrypt(json!({}), "m.dummy").await;
|
||||
let content = outbound.encrypt("m.dummy", &message_like_event_content!({})).await;
|
||||
let room_event = wrap_encrypted_content(machine.user_id(), content);
|
||||
|
||||
machine.create_outgoing_key_request(session.room_id(), &room_event).await.unwrap();
|
||||
|
||||
let requests = machine.outgoing_to_device_requests().await.unwrap();
|
||||
let request = requests.get(0).unwrap();
|
||||
let request = &requests[0];
|
||||
let id = &request.request_id;
|
||||
|
||||
machine.mark_outgoing_request_as_sent(id).await.unwrap();
|
||||
@@ -1501,7 +1553,7 @@ mod tests {
|
||||
|
||||
#[async_test]
|
||||
#[cfg(feature = "automatic-room-key-forwarding")]
|
||||
async fn should_share_key_test() {
|
||||
async fn test_should_share_key() {
|
||||
let machine = get_machine_test_helper().await;
|
||||
let account = account();
|
||||
|
||||
@@ -1869,7 +1921,6 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
#[cfg(feature = "backups_v1")]
|
||||
async fn test_secret_broadcasting() {
|
||||
use futures_util::{pin_mut, FutureExt};
|
||||
use ruma::api::client::to_device::send_event_to_device::v3::Response as ToDeviceResponse;
|
||||
@@ -1890,7 +1941,7 @@ mod tests {
|
||||
vec![SecretName::RecoveryKey],
|
||||
);
|
||||
let mut changes = Changes::default();
|
||||
let request_id = key_requests.first().unwrap().request_id.to_owned();
|
||||
let request_id = key_requests[0].request_id.to_owned();
|
||||
changes.key_requests = key_requests;
|
||||
bob_machine.store().save_changes(changes).await.unwrap();
|
||||
for request in bob_machine.outgoing_requests().await.unwrap() {
|
||||
@@ -2001,7 +2052,7 @@ mod tests {
|
||||
// Bob only has a keys claim request, since we're lacking a session
|
||||
assert_eq!(bob_machine.outgoing_to_device_requests().await.unwrap().len(), 1);
|
||||
assert_matches!(
|
||||
bob_machine.outgoing_to_device_requests().await.unwrap().first().unwrap().request(),
|
||||
bob_machine.outgoing_to_device_requests().await.unwrap()[0].request(),
|
||||
OutgoingRequests::KeysClaim(_)
|
||||
);
|
||||
assert!(!bob_machine.inner.users_for_key_claim.read().unwrap().is_empty());
|
||||
|
||||
@@ -109,12 +109,9 @@ impl SecretInfo {
|
||||
/// comparison.
|
||||
pub fn as_key(&self) -> String {
|
||||
match &self {
|
||||
SecretInfo::KeyRequest(info) => format!(
|
||||
"keyRequest:{}:{}:{}",
|
||||
info.room_id().as_str(),
|
||||
info.session_id(),
|
||||
&info.algorithm(),
|
||||
),
|
||||
SecretInfo::KeyRequest(info) => {
|
||||
format!("keyRequest:{}:{}:{}", info.room_id(), info.session_id(), info.algorithm())
|
||||
}
|
||||
SecretInfo::SecretRequest(sname) => format!("secretName:{sname}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ use ruma::{
|
||||
OwnedDeviceKeyId, UInt, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{instrument, trace, warn};
|
||||
use vodozemac::{olm::SessionConfig, Curve25519PublicKey, Ed25519PublicKey};
|
||||
@@ -96,6 +95,7 @@ fn default_timestamp() -> MilliSecondsSinceUnixEpoch {
|
||||
MilliSecondsSinceUnixEpoch(UInt::default())
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl std::fmt::Debug for ReadOnlyDevice {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ReadOnlyDevice")
|
||||
@@ -119,6 +119,7 @@ pub struct Device {
|
||||
pub(crate) device_owner_identity: Option<ReadOnlyUserIdentities>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl std::fmt::Debug for Device {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Device").field("device", &self.inner).finish()
|
||||
@@ -411,63 +412,12 @@ impl Device {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `content` - The content of the event that should be encrypted.
|
||||
#[instrument(
|
||||
skip_all,
|
||||
fields(
|
||||
recipient = %self.user_id(),
|
||||
recipient_device = %self.device_id(),
|
||||
recipient_key = ?self.curve25519_key(),
|
||||
event_type,
|
||||
session,
|
||||
message_id,
|
||||
))
|
||||
]
|
||||
pub(crate) async fn encrypt(
|
||||
&self,
|
||||
event_type: &str,
|
||||
content: Value,
|
||||
content: impl Serialize,
|
||||
) -> OlmResult<(Session, Raw<ToDeviceEncryptedEventContent>)> {
|
||||
#[cfg(feature = "message-ids")]
|
||||
let message_id = {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let id = ulid::Ulid::new().to_string();
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let id = ruma::TransactionId::new().to_string();
|
||||
|
||||
tracing::Span::current().record("message_id", &id);
|
||||
Some(id)
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "message-ids"))]
|
||||
let message_id = None;
|
||||
|
||||
self.inner
|
||||
.encrypt(self.verification_machine.store.inner(), event_type, content, message_id)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn maybe_encrypt_room_key(
|
||||
&self,
|
||||
session: OutboundGroupSession,
|
||||
) -> OlmResult<MaybeEncryptedRoomKey> {
|
||||
let content = session.as_content().await;
|
||||
let message_index = session.message_index().await;
|
||||
let event_type = content.event_type();
|
||||
let content =
|
||||
serde_json::to_value(content).expect("We can always serialize our own room key");
|
||||
|
||||
match self.encrypt(event_type, content).await {
|
||||
Ok((session, encrypted)) => Ok(MaybeEncryptedRoomKey::Encrypted {
|
||||
share_info: ShareInfo::new_shared(session.sender_key().to_owned(), message_index),
|
||||
used_session: session,
|
||||
message: encrypted.cast(),
|
||||
}),
|
||||
|
||||
Err(OlmError::MissingSession | OlmError::EventError(EventError::MissingSenderKey)) => {
|
||||
Ok(MaybeEncryptedRoomKey::Withheld { code: WithheldCode::NoOlm })
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
self.inner.encrypt(self.verification_machine.store.inner(), event_type, content).await
|
||||
}
|
||||
|
||||
/// Encrypt the given inbound group session as a forwarded room key for this
|
||||
@@ -485,7 +435,7 @@ impl Device {
|
||||
};
|
||||
let content: ForwardedRoomKeyContent = export.try_into()?;
|
||||
|
||||
(content.event_type(), serde_json::to_value(content)?)
|
||||
(content.event_type(), content)
|
||||
};
|
||||
|
||||
self.encrypt(event_type, content).await
|
||||
@@ -770,28 +720,89 @@ impl ReadOnlyDevice {
|
||||
)
|
||||
}
|
||||
|
||||
/// Encrypt the given content for this device.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The crypto store. Used to find an established Olm session
|
||||
/// for this device.
|
||||
/// * `event_type` - The type of the event that should be encrypted.
|
||||
/// * `content` - The content of the event that should be encrypted.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// On success, a tuple `(session, content)`, where `session` is the Olm
|
||||
/// [`Session`] that was used to encrypt the content, and `content` is
|
||||
/// the content for the `m.room.encrypted` to-device event.
|
||||
///
|
||||
/// If an Olm session has not already been established with this device,
|
||||
/// returns `Err(OlmError::MissingSession)`.
|
||||
#[instrument(
|
||||
skip_all,
|
||||
fields(
|
||||
recipient = ?self.user_id(),
|
||||
recipient_device = ?self.device_id(),
|
||||
recipient_key = ?self.curve25519_key(),
|
||||
event_type,
|
||||
session,
|
||||
message_id,
|
||||
))
|
||||
]
|
||||
pub(crate) async fn encrypt(
|
||||
&self,
|
||||
store: &CryptoStoreWrapper,
|
||||
event_type: &str,
|
||||
content: Value,
|
||||
message_id: Option<String>,
|
||||
content: impl Serialize,
|
||||
) -> OlmResult<(Session, Raw<ToDeviceEncryptedEventContent>)> {
|
||||
#[cfg(feature = "message-ids")]
|
||||
let message_id = {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let id = ulid::Ulid::new().to_string();
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let id = ruma::TransactionId::new().to_string();
|
||||
|
||||
tracing::Span::current().record("message_id", &id);
|
||||
Some(id)
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "message-ids"))]
|
||||
let message_id = None;
|
||||
|
||||
let session = self.get_most_recent_session(store).await?;
|
||||
|
||||
if let Some(mut session) = session {
|
||||
let message = session.encrypt(self, event_type, content, message_id).await?;
|
||||
|
||||
trace!("Successfully encrypted an event");
|
||||
|
||||
Ok((session, message))
|
||||
} else {
|
||||
warn!("Trying to encrypt an event for a device, but no Olm session is found.",);
|
||||
|
||||
trace!("Trying to encrypt an event for a device, but no Olm session is found.");
|
||||
Err(OlmError::MissingSession)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn maybe_encrypt_room_key(
|
||||
&self,
|
||||
store: &CryptoStoreWrapper,
|
||||
session: OutboundGroupSession,
|
||||
) -> OlmResult<MaybeEncryptedRoomKey> {
|
||||
let content = session.as_content().await;
|
||||
let message_index = session.message_index().await;
|
||||
let event_type = content.event_type();
|
||||
|
||||
match self.encrypt(store, event_type, content).await {
|
||||
Ok((session, encrypted)) => Ok(MaybeEncryptedRoomKey::Encrypted {
|
||||
share_info: ShareInfo::new_shared(session.sender_key().to_owned(), message_index),
|
||||
used_session: session,
|
||||
message: encrypted.cast(),
|
||||
}),
|
||||
|
||||
Err(OlmError::MissingSession | OlmError::EventError(EventError::MissingSenderKey)) => {
|
||||
Ok(MaybeEncryptedRoomKey::Withheld { code: WithheldCode::NoOlm })
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update a device with a new device keys struct.
|
||||
pub(crate) fn update_device(&mut self, device_keys: &DeviceKeys) -> Result<(), SignatureError> {
|
||||
self.verify_device_keys(device_keys)?;
|
||||
@@ -823,7 +834,6 @@ impl ReadOnlyDevice {
|
||||
/// **Note**: Use this method with caution, the `canonical_json` needs to be
|
||||
/// correctly canonicalized and make sure that the object you are checking
|
||||
/// the signature for is allowed to be signed by a device.
|
||||
#[cfg(feature = "backups_v1")]
|
||||
pub(crate) fn has_signed_raw(
|
||||
&self,
|
||||
signatures: &Signatures,
|
||||
@@ -874,14 +884,14 @@ impl ReadOnlyDevice {
|
||||
|
||||
/// Create a `ReadOnlyDevice` from an `Account`
|
||||
///
|
||||
/// We will have our own device in the store once we receive a keys/query
|
||||
/// We will have our own device in the store once we receive a `/keys/query`
|
||||
/// response, but this is useful to create it before we receive such a
|
||||
/// response.
|
||||
///
|
||||
/// It also makes it easier to check that the server doesn't lie about our
|
||||
/// own device.
|
||||
///
|
||||
/// *Don't* use this after we received a keys/query response, other
|
||||
/// *Don't* use this after we received a `/keys/query` response, other
|
||||
/// users/devices might add signatures to our own device, which can't be
|
||||
/// replicated locally.
|
||||
pub fn from_account(account: &Account) -> ReadOnlyDevice {
|
||||
|
||||
@@ -13,14 +13,15 @@
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashSet},
|
||||
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
|
||||
ops::Deref,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use futures_util::future::join_all;
|
||||
use itertools::Itertools;
|
||||
use matrix_sdk_common::executor::spawn;
|
||||
use matrix_sdk_common::{executor::spawn, failures_cache::FailuresCache};
|
||||
use ruma::{
|
||||
api::client::keys::get_keys::v3::Response as KeysQueryResponse, serde::Raw, OwnedDeviceId,
|
||||
OwnedServerName, OwnedTransactionId, OwnedUserId, ServerName, TransactionId, UserId,
|
||||
@@ -37,11 +38,10 @@ use crate::{
|
||||
requests::KeysQueryRequest,
|
||||
store::{
|
||||
caches::SequenceNumber, Changes, DeviceChanges, IdentityChanges, KeyQueryManager,
|
||||
Result as StoreResult, Store, StoreCache,
|
||||
Result as StoreResult, Store, StoreCache, UserKeyQueryResult,
|
||||
},
|
||||
types::{CrossSigningKey, DeviceKeys, MasterPubkey, SelfSigningPubkey, UserSigningPubkey},
|
||||
utilities::FailuresCache,
|
||||
LocalTrust, SignatureError,
|
||||
CryptoStoreError, LocalTrust, SignatureError,
|
||||
};
|
||||
|
||||
enum DeviceChange {
|
||||
@@ -61,6 +61,10 @@ enum IdentityUpdateResult {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct IdentityManager {
|
||||
/// Servers that have previously appeared in the `failures` section of a
|
||||
/// `/keys/query` response.
|
||||
///
|
||||
/// See also [`crate::session_manager::SessionManager::failures`].
|
||||
failures: FailuresCache<OwnedServerName>,
|
||||
store: Store,
|
||||
|
||||
@@ -101,7 +105,7 @@ impl IdentityManager {
|
||||
&self.store.static_account().user_id
|
||||
}
|
||||
|
||||
/// Receive a successful keys query response.
|
||||
/// Receive a successful `/keys/query` response.
|
||||
///
|
||||
/// Returns a list of devices newly discovered devices and devices that
|
||||
/// changed.
|
||||
@@ -110,7 +114,7 @@ impl IdentityManager {
|
||||
///
|
||||
/// * `request_id` - The request_id returned by `users_for_key_query` or
|
||||
/// `build_key_query_for_users`
|
||||
/// * `response` - The keys query response of the request that the client
|
||||
/// * `response` - The response of the `/keys/query` request that the client
|
||||
/// performed.
|
||||
pub async fn receive_keys_query_response(
|
||||
&self,
|
||||
@@ -121,7 +125,7 @@ impl IdentityManager {
|
||||
?request_id,
|
||||
users = ?response.device_keys.keys().collect::<BTreeSet<_>>(),
|
||||
failures = ?response.failures,
|
||||
"Handling a keys query response"
|
||||
"Handling a `/keys/query` response"
|
||||
);
|
||||
|
||||
// Parse the strings into server names and filter out our own server. We should
|
||||
@@ -209,7 +213,7 @@ impl IdentityManager {
|
||||
?deleted_devices,
|
||||
?new_identities,
|
||||
?changed_identities,
|
||||
"Finished handling of the keys/query response"
|
||||
"Finished handling of the `/keys/query` response"
|
||||
);
|
||||
|
||||
Ok((devices, identities))
|
||||
@@ -225,8 +229,8 @@ impl IdentityManager {
|
||||
if let Some(mut device) = old_device {
|
||||
if let Err(e) = device.update_device(&device_keys) {
|
||||
warn!(
|
||||
user_id = device.user_id().as_str(),
|
||||
device_id = device.device_id().as_str(),
|
||||
user_id = ?device.user_id(),
|
||||
device_id = ?device.device_id(),
|
||||
error = ?e,
|
||||
"Failed to update device keys",
|
||||
);
|
||||
@@ -248,8 +252,8 @@ impl IdentityManager {
|
||||
d.set_trust_state(LocalTrust::Verified);
|
||||
|
||||
trace!(
|
||||
user_id = d.user_id().as_str(),
|
||||
device_id = d.device_id().as_str(),
|
||||
user_id = ?d.user_id(),
|
||||
device_id = ?d.device_id(),
|
||||
keys = ?d.keys(),
|
||||
"Adding our own device to the device store, \
|
||||
marking it as locally verified",
|
||||
@@ -261,8 +265,8 @@ impl IdentityManager {
|
||||
}
|
||||
} else {
|
||||
trace!(
|
||||
user_id = d.user_id().as_str(),
|
||||
device_id = d.device_id().as_str(),
|
||||
user_id = ?d.user_id(),
|
||||
device_id = ?d.device_id(),
|
||||
keys = ?d.keys(),
|
||||
"Adding a new device to the device store",
|
||||
);
|
||||
@@ -272,8 +276,8 @@ impl IdentityManager {
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
user_id = device_keys.user_id.as_str(),
|
||||
device_id = device_keys.device_id.as_str(),
|
||||
user_id = ?device_keys.user_id,
|
||||
device_id = ?device_keys.device_id,
|
||||
error = ?e,
|
||||
"Failed to create a new device",
|
||||
);
|
||||
@@ -301,10 +305,10 @@ impl IdentityManager {
|
||||
Ok(device_keys) => {
|
||||
if user_id != device_keys.user_id || device_id != device_keys.device_id {
|
||||
warn!(
|
||||
user_id = user_id.as_str(),
|
||||
device_id = device_id.as_str(),
|
||||
device_key_user = device_keys.user_id.as_str(),
|
||||
device_key_device_id = device_keys.device_id.as_str(),
|
||||
?user_id,
|
||||
?device_id,
|
||||
device_key_user = ?device_keys.user_id,
|
||||
device_key_device_id = ?device_keys.device_id,
|
||||
"Mismatch in the device keys payload",
|
||||
);
|
||||
None
|
||||
@@ -314,9 +318,7 @@ impl IdentityManager {
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
user_id = user_id.as_str(),
|
||||
device_id = device_id.as_str(),
|
||||
error = ?e,
|
||||
?user_id, ?device_id, error = ?e,
|
||||
"Device keys failed to deserialize",
|
||||
);
|
||||
None
|
||||
@@ -346,8 +348,8 @@ impl IdentityManager {
|
||||
let identity_keys = store.static_account().identity_keys();
|
||||
|
||||
warn!(
|
||||
user_id = own_user_id.as_str(),
|
||||
device_id = own_device_id.as_str(),
|
||||
user_id = ?own_user_id,
|
||||
device_id = ?own_device_id,
|
||||
curve25519_key = ?identity_keys.curve25519,
|
||||
ed25519_key = ?identity_keys.ed25519,
|
||||
"Our own device might have been deleted"
|
||||
@@ -544,7 +546,7 @@ impl IdentityManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to deserialize the the master key and self-signing key of an
|
||||
/// Try to deserialize the master key and self-signing key of an
|
||||
/// identity from a `/keys/query` response.
|
||||
///
|
||||
/// Each user identity *must* at least contain a master and self-signing
|
||||
@@ -878,6 +880,137 @@ impl IdentityManager {
|
||||
let cache = self.store.cache().await?;
|
||||
self.key_query_manager.synced(&cache).await?.update_tracked_users(users.into_iter()).await
|
||||
}
|
||||
|
||||
/// Retrieve a list of a user's current devices, so we can encrypt a message
|
||||
/// to them.
|
||||
///
|
||||
/// If we have not yet seen any devices for the user, and their device list
|
||||
/// has been marked as outdated, then we wait for the `/keys/query` request
|
||||
/// to complete. This helps ensure that we attempt at least once to fetch a
|
||||
/// user's devices before encrypting to them.
|
||||
pub async fn get_user_devices_for_encryption(
|
||||
&self,
|
||||
users: impl Iterator<Item = &UserId>,
|
||||
) -> StoreResult<HashMap<OwnedUserId, HashMap<OwnedDeviceId, ReadOnlyDevice>>> {
|
||||
// How long we wait for /keys/query to complete.
|
||||
const KEYS_QUERY_WAIT_TIME: Duration = Duration::from_secs(5);
|
||||
|
||||
let mut devices_by_user = HashMap::new();
|
||||
let mut users_with_no_devices_on_failed_servers = Vec::new();
|
||||
let mut users_with_no_devices_on_unfailed_servers = Vec::new();
|
||||
|
||||
for user_id in users {
|
||||
// First of all, check the store for this user.
|
||||
let devices = self.store.get_readonly_devices_filtered(user_id).await?;
|
||||
|
||||
// Now, look for users who have no devices at all.
|
||||
//
|
||||
// If a user has no devices at all, that implies we have never (successfully)
|
||||
// done a `/keys/query` for them; we wait for one to complete if it is
|
||||
// in flight. (Of course, the user might genuinely have no devices, but
|
||||
// that's fine, it just means we redundantly grab the cache guard and
|
||||
// check the pending-query flag.)
|
||||
if !devices.is_empty() {
|
||||
// This user has at least one known device.
|
||||
//
|
||||
// The device list may also be outdated in this case; but in this
|
||||
// situation, we are racing between sending a message and retrieving their
|
||||
// device list. That's an inherently racy situation and there is no real
|
||||
// benefit to waiting for the `/keys/query` request to complete. So we don't
|
||||
// bother.
|
||||
//
|
||||
// We just add their devices to the result and carry on.
|
||||
devices_by_user.insert(user_id.to_owned(), devices);
|
||||
continue;
|
||||
}
|
||||
|
||||
// *However*, if the user's server is currently subject to a backoff due to
|
||||
// previous failures, then `users_for_key_query` won't attempt to query
|
||||
// for the user's devices, so there's no point waiting.
|
||||
//
|
||||
// XXX: this is racy. It's possible that:
|
||||
// * `failures` included the user's server when `users_for_key_query` was
|
||||
// called, so the user was not returned in the `KeyQueryRequest`, and:
|
||||
// * The backoff has now expired.
|
||||
//
|
||||
// In that case, we'll end up waiting for the *next* `users_for_key_query` call,
|
||||
// which might not be for 30 seconds or so. (And by then, it might be `failed`
|
||||
// again.)
|
||||
if self.failures.contains(user_id.server_name()) {
|
||||
users_with_no_devices_on_failed_servers.push(user_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
users_with_no_devices_on_unfailed_servers.push(user_id);
|
||||
}
|
||||
|
||||
if !users_with_no_devices_on_failed_servers.is_empty() {
|
||||
info!(
|
||||
?users_with_no_devices_on_failed_servers,
|
||||
"Not waiting for `/keys/query` for users whose server has previously failed"
|
||||
);
|
||||
}
|
||||
|
||||
if !users_with_no_devices_on_unfailed_servers.is_empty() {
|
||||
// For each user with no devices, fire off a task to wait for a `/keys/query`
|
||||
// result if one is pending.
|
||||
//
|
||||
// We don't actually update the `devices_by_user` map here since that could
|
||||
// require concurrent access to it. Instead each task returns a
|
||||
// `(OwnedUserId, HashMap)` pair (or rather, an `Option` of one) so that we can
|
||||
// add the results to the map.
|
||||
let results = join_all(
|
||||
users_with_no_devices_on_unfailed_servers
|
||||
.into_iter()
|
||||
.map(|user_id| self.get_updated_keys_for_user(KEYS_QUERY_WAIT_TIME, user_id)),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Once all the tasks have completed, process the results.
|
||||
let mut updated_users = Vec::new();
|
||||
for result in results {
|
||||
if let Some((user_id, updated_devices)) = result? {
|
||||
devices_by_user.insert(user_id.to_owned(), updated_devices);
|
||||
updated_users.push(user_id);
|
||||
}
|
||||
}
|
||||
|
||||
if !updated_users.is_empty() {
|
||||
info!(
|
||||
?updated_users,
|
||||
"Waited for `/keys/query` to complete for users who have no devices"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(devices_by_user)
|
||||
}
|
||||
|
||||
/// Helper for get_user_devices_for_encryption.
|
||||
///
|
||||
/// Waits for any pending `/keys/query` for the given user. If one was
|
||||
/// pending, reloads the device list and returns `Some(user_id,
|
||||
/// device_list)`. If no request was pending, returns `None`.
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[instrument(skip(self))]
|
||||
async fn get_updated_keys_for_user<'a>(
|
||||
&self,
|
||||
timeout_duration: Duration,
|
||||
user_id: &'a UserId,
|
||||
) -> Result<Option<(&'a UserId, HashMap<OwnedDeviceId, ReadOnlyDevice>)>, CryptoStoreError>
|
||||
{
|
||||
let cache = self.store.cache().await?;
|
||||
match self
|
||||
.key_query_manager
|
||||
.wait_if_user_key_query_pending(cache, timeout_duration, user_id)
|
||||
.await?
|
||||
{
|
||||
UserKeyQueryResult::WasPending => {
|
||||
Ok(Some((user_id, self.store.get_readonly_devices_filtered(user_id).await?)))
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
@@ -987,7 +1120,7 @@ pub(crate) mod testing {
|
||||
"user_signing_keys": {}
|
||||
}));
|
||||
KeyQueryResponse::try_from_http_response(data)
|
||||
.expect("Can't parse the keys upload response")
|
||||
.expect("Can't parse the `/keys/upload` response")
|
||||
}
|
||||
|
||||
// An updated version of `other_key_query` featuring an additional signature on
|
||||
@@ -1053,7 +1186,7 @@ pub(crate) mod testing {
|
||||
"user_signing_keys": {}
|
||||
}));
|
||||
KeyQueryResponse::try_from_http_response(data)
|
||||
.expect("Can't parse the keys upload response")
|
||||
.expect("Can't parse the `/keys/upload` response")
|
||||
}
|
||||
|
||||
/// Mocked response to a /keys/query request.
|
||||
@@ -1155,7 +1288,7 @@ pub(crate) mod testing {
|
||||
}
|
||||
}));
|
||||
KeyQueryResponse::try_from_http_response(data)
|
||||
.expect("Can't parse the keys upload response")
|
||||
.expect("Can't parse the `/keys/upload` response")
|
||||
}
|
||||
|
||||
pub fn own_key_query() -> KeyQueryResponse {
|
||||
@@ -1186,7 +1319,7 @@ pub(crate) mod testing {
|
||||
);
|
||||
|
||||
KeyQueryResponse::try_from_http_response(response_from_file(&json))
|
||||
.expect("Can't parse the keys upload response")
|
||||
.expect("Can't parse the `/keys/upload` response")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1355,7 +1488,7 @@ pub(crate) mod tests {
|
||||
});
|
||||
|
||||
let response = KeysQueryResponse::try_from_http_response(response_from_file(&response))
|
||||
.expect("Can't parse the keys query response");
|
||||
.expect("Can't parse the `/keys/query` response");
|
||||
|
||||
manager.receive_keys_query_response(&TransactionId::new(), &response).await.unwrap();
|
||||
|
||||
@@ -1405,7 +1538,7 @@ pub(crate) mod tests {
|
||||
});
|
||||
|
||||
let response = KeysQueryResponse::try_from_http_response(response_from_file(&response))
|
||||
.expect("Can't parse the keys query response");
|
||||
.expect("Can't parse the `/keys/query` response");
|
||||
|
||||
let (_, private_identity) = manager.handle_cross_signing_keys(&response).await.unwrap();
|
||||
|
||||
@@ -1589,7 +1722,7 @@ pub(crate) mod tests {
|
||||
let (new_request_id, _) =
|
||||
manager.as_ref().unwrap().build_key_query_for_users(vec![user_id()]);
|
||||
|
||||
// A second `keys/query` response with the same result shouldn't fire a change
|
||||
// A second `/keys/query` response with the same result shouldn't fire a change
|
||||
// notification: the identity should be unchanged.
|
||||
manager
|
||||
.as_ref()
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![warn(missing_docs, missing_debug_implementations)]
|
||||
|
||||
#[cfg(feature = "backups_v1")]
|
||||
pub mod backups;
|
||||
mod ciphers;
|
||||
pub mod dehydrated_devices;
|
||||
@@ -104,13 +103,5 @@ pub use vodozemac;
|
||||
/// The version of the matrix-sdk-cypto crate being used
|
||||
pub static VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
// Enable tracing for tests in this crate
|
||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||
#[ctor::ctor]
|
||||
fn init_logging() {
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::EnvFilter::from_default_env())
|
||||
.with(tracing_subscriber::fmt::layer().with_test_writer())
|
||||
.init();
|
||||
}
|
||||
#[cfg(test)]
|
||||
matrix_sdk_test::init_tracing_for_tests!();
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashSet},
|
||||
collections::{BTreeMap, HashSet},
|
||||
sync::{Arc, RwLock as StdRwLock},
|
||||
time::Duration,
|
||||
};
|
||||
@@ -27,7 +27,7 @@ use ruma::{
|
||||
api::client::{
|
||||
dehydrated_device::DehydratedDeviceData,
|
||||
keys::{
|
||||
claim_keys::v3::{Request as KeysClaimRequest, Response as KeysClaimResponse},
|
||||
claim_keys::v3::Request as KeysClaimRequest,
|
||||
get_keys::v3::Response as KeysQueryResponse,
|
||||
upload_keys::v3::{Request as UploadKeysRequest, Response as UploadKeysResponse},
|
||||
upload_signatures::v3::Request as UploadSignaturesRequest,
|
||||
@@ -36,13 +36,14 @@ use ruma::{
|
||||
},
|
||||
assign,
|
||||
events::{
|
||||
secret::request::SecretName, AnyMessageLikeEvent, AnyToDeviceEvent, MessageLikeEventContent,
|
||||
secret::request::SecretName, AnyMessageLikeEvent, AnyMessageLikeEventContent,
|
||||
AnyToDeviceEvent, MessageLikeEventContent,
|
||||
},
|
||||
serde::Raw,
|
||||
DeviceId, DeviceKeyAlgorithm, OwnedDeviceId, OwnedDeviceKeyId, OwnedTransactionId, OwnedUserId,
|
||||
RoomId, TransactionId, UInt, UserId,
|
||||
};
|
||||
use serde_json::{value::to_raw_value, Value};
|
||||
use serde_json::value::to_raw_value;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{
|
||||
debug, error,
|
||||
@@ -54,9 +55,8 @@ use vodozemac::{
|
||||
Curve25519PublicKey, Ed25519Signature,
|
||||
};
|
||||
|
||||
#[cfg(feature = "backups_v1")]
|
||||
use crate::backups::BackupMachine;
|
||||
use crate::{
|
||||
backups::{BackupMachine, MegolmV1BackupKey},
|
||||
dehydrated_devices::{DehydratedDevices, DehydrationError},
|
||||
error::{EventError, MegolmError, MegolmResult, OlmError, OlmResult},
|
||||
gossiping::GossipMachine,
|
||||
@@ -128,7 +128,6 @@ pub struct OlmMachineInner {
|
||||
/// of when a key query needs to be done and handling one.
|
||||
identity_manager: IdentityManager,
|
||||
/// A state machine that handles creating room key backups.
|
||||
#[cfg(feature = "backups_v1")]
|
||||
backup_machine: BackupMachine,
|
||||
}
|
||||
|
||||
@@ -174,7 +173,13 @@ impl OlmMachine {
|
||||
let store = Arc::new(CryptoStoreWrapper::new(self.user_id(), MemoryStore::new()));
|
||||
store.save_pending_changes(PendingChanges { account: Some(account) }).await?;
|
||||
|
||||
Ok(Self::new_helper(device_id, store, static_account, self.store().private_identity()))
|
||||
Ok(Self::new_helper(
|
||||
device_id,
|
||||
store,
|
||||
static_account,
|
||||
self.store().private_identity(),
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
fn new_helper(
|
||||
@@ -182,6 +187,7 @@ impl OlmMachine {
|
||||
store: Arc<CryptoStoreWrapper>,
|
||||
account: StaticAccountData,
|
||||
user_identity: Arc<Mutex<PrivateCrossSigningIdentity>>,
|
||||
maybe_backup_key: Option<MegolmV1BackupKey>,
|
||||
) -> Self {
|
||||
let verification_machine =
|
||||
VerificationMachine::new(account.clone(), user_identity.clone(), store.clone());
|
||||
@@ -202,8 +208,7 @@ impl OlmMachine {
|
||||
let session_manager =
|
||||
SessionManager::new(users_for_key_claim, key_request_machine.clone(), store.clone());
|
||||
|
||||
#[cfg(feature = "backups_v1")]
|
||||
let backup_machine = BackupMachine::new(store.clone(), None);
|
||||
let backup_machine = BackupMachine::new(store.clone(), maybe_backup_key);
|
||||
|
||||
let inner = Arc::new(OlmMachineInner {
|
||||
user_id: store.user_id().to_owned(),
|
||||
@@ -215,7 +220,6 @@ impl OlmMachine {
|
||||
verification_machine,
|
||||
key_request_machine,
|
||||
identity_manager,
|
||||
#[cfg(feature = "backups_v1")]
|
||||
backup_machine,
|
||||
});
|
||||
|
||||
@@ -309,9 +313,21 @@ impl OlmMachine {
|
||||
}
|
||||
};
|
||||
|
||||
let saved_keys = store.load_backup_keys().await?;
|
||||
let maybe_backup_key = saved_keys.decryption_key.and_then(|k| {
|
||||
if let Some(version) = saved_keys.backup_version {
|
||||
MegolmV1BackupKey::from_base64(&k.to_base64()).ok().map(|k| {
|
||||
k.set_version(version);
|
||||
k
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let identity = Arc::new(Mutex::new(identity));
|
||||
let store = Arc::new(CryptoStoreWrapper::new(user_id, store));
|
||||
Ok(OlmMachine::new_helper(device_id, store, static_account, identity))
|
||||
Ok(OlmMachine::new_helper(device_id, store, static_account, identity, maybe_backup_key))
|
||||
}
|
||||
|
||||
/// Get the crypto store associated with this `OlmMachine` instance.
|
||||
@@ -349,17 +365,43 @@ impl OlmMachine {
|
||||
Ok(self.inner.identity_manager.key_query_manager.synced(&cache).await?.tracked_users())
|
||||
}
|
||||
|
||||
/// Enable or disable room key requests.
|
||||
///
|
||||
/// Room key requests allow the device to request room keys that it might
|
||||
/// have missed in the original share using `m.room_key_request`
|
||||
/// events.
|
||||
///
|
||||
/// See also [`OlmMachine::set_room_key_forwarding_enabled`] and
|
||||
/// [`OlmMachine::are_room_key_requests_enabled`].
|
||||
#[cfg(feature = "automatic-room-key-forwarding")]
|
||||
pub fn set_room_key_requests_enabled(&self, enable: bool) {
|
||||
self.inner.key_request_machine.set_room_key_requests_enabled(enable)
|
||||
}
|
||||
|
||||
/// Query whether we should send outgoing `m.room_key_request`s on
|
||||
/// decryption failure.
|
||||
///
|
||||
/// See also [`OlmMachine::set_room_key_requests_enabled`].
|
||||
pub fn are_room_key_requests_enabled(&self) -> bool {
|
||||
self.inner.key_request_machine.are_room_key_requests_enabled()
|
||||
}
|
||||
|
||||
/// Enable or disable room key forwarding.
|
||||
///
|
||||
/// Room key forwarding allows the device to request room keys that it might
|
||||
/// have missend in the original share using `m.room_key_request`
|
||||
/// events.
|
||||
/// If room key forwarding is enabled, we will automatically reply to
|
||||
/// incoming `m.room_key_request` messages from verified devices by
|
||||
/// forwarding the requested key (if we have it).
|
||||
///
|
||||
/// See also [`OlmMachine::set_room_key_requests_enabled`] and
|
||||
/// [`OlmMachine::is_room_key_forwarding_enabled`].
|
||||
#[cfg(feature = "automatic-room-key-forwarding")]
|
||||
pub fn toggle_room_key_forwarding(&self, enable: bool) {
|
||||
self.inner.key_request_machine.toggle_room_key_forwarding(enable)
|
||||
pub fn set_room_key_forwarding_enabled(&self, enable: bool) {
|
||||
self.inner.key_request_machine.set_room_key_forwarding_enabled(enable)
|
||||
}
|
||||
|
||||
/// Is room key forwarding enabled?
|
||||
///
|
||||
/// See also [`OlmMachine::set_room_key_forwarding_enabled`].
|
||||
pub fn is_room_key_forwarding_enabled(&self) -> bool {
|
||||
self.inner.key_request_machine.is_room_key_forwarding_enabled()
|
||||
}
|
||||
@@ -407,6 +449,9 @@ impl OlmMachine {
|
||||
/// This can be useful if we need the results from [`get_identity`] or
|
||||
/// [`get_user_devices`] to be as up-to-date as possible.
|
||||
///
|
||||
/// Note that this request won't be awaited by other calls waiting for a
|
||||
/// user's or device's keys, since this is an out-of-band query.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `users` - list of users whose keys should be queried
|
||||
@@ -442,19 +487,22 @@ impl OlmMachine {
|
||||
) -> OlmResult<()> {
|
||||
match response.into() {
|
||||
IncomingResponse::KeysUpload(response) => {
|
||||
self.receive_keys_upload_response(response).await?;
|
||||
Box::pin(self.receive_keys_upload_response(response)).await?;
|
||||
}
|
||||
IncomingResponse::KeysQuery(response) => {
|
||||
self.receive_keys_query_response(request_id, response).await?;
|
||||
Box::pin(self.receive_keys_query_response(request_id, response)).await?;
|
||||
}
|
||||
IncomingResponse::KeysClaim(response) => {
|
||||
self.receive_keys_claim_response(response).await?;
|
||||
Box::pin(
|
||||
self.inner.session_manager.receive_keys_claim_response(request_id, response),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
IncomingResponse::ToDevice(_) => {
|
||||
self.mark_to_device_request_as_sent(request_id).await?;
|
||||
Box::pin(self.mark_to_device_request_as_sent(request_id)).await?;
|
||||
}
|
||||
IncomingResponse::SigningKeysUpload(_) => {
|
||||
self.receive_cross_signing_upload_response().await?;
|
||||
Box::pin(self.receive_cross_signing_upload_response()).await?;
|
||||
}
|
||||
IncomingResponse::SignatureUpload(_) => {
|
||||
self.inner.verification_machine.mark_request_as_sent(request_id);
|
||||
@@ -463,8 +511,7 @@ impl OlmMachine {
|
||||
self.inner.verification_machine.mark_request_as_sent(request_id);
|
||||
}
|
||||
IncomingResponse::KeysBackup(_) => {
|
||||
#[cfg(feature = "backups_v1")]
|
||||
self.inner.backup_machine.mark_request_as_sent(request_id).await?;
|
||||
Box::pin(self.inner.backup_machine.mark_request_as_sent(request_id)).await?;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -563,12 +610,12 @@ impl OlmMachine {
|
||||
})
|
||||
}
|
||||
|
||||
/// Receive a successful keys upload response.
|
||||
/// Receive a successful `/keys/upload` response.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `response` - The keys upload response of the request that the client
|
||||
/// performed.
|
||||
/// * `response` - The response of the `/keys/upload` request that the
|
||||
/// client performed.
|
||||
async fn receive_keys_upload_response(&self, response: &UploadKeysResponse) -> OlmResult<()> {
|
||||
self.inner
|
||||
.store
|
||||
@@ -607,6 +654,7 @@ impl OlmMachine {
|
||||
/// this method between sync requests.
|
||||
///
|
||||
/// [`mark_request_as_sent`]: #method.mark_request_as_sent
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_missing_sessions(
|
||||
&self,
|
||||
users: impl Iterator<Item = &UserId>,
|
||||
@@ -614,25 +662,15 @@ impl OlmMachine {
|
||||
self.inner.session_manager.get_missing_sessions(users).await
|
||||
}
|
||||
|
||||
/// Receive a successful key claim response and create new Olm sessions with
|
||||
/// the claimed keys.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `response` - The response containing the claimed one-time keys.
|
||||
async fn receive_keys_claim_response(&self, response: &KeysClaimResponse) -> OlmResult<()> {
|
||||
self.inner.session_manager.receive_keys_claim_response(response).await
|
||||
}
|
||||
|
||||
/// Receive a successful keys query response.
|
||||
/// Receive a successful `/keys/query` response.
|
||||
///
|
||||
/// Returns a list of devices newly discovered devices and devices that
|
||||
/// changed.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `response` - The keys query response of the request that the client
|
||||
/// performed.
|
||||
/// * `response` - The response of the `/keys/query` request that the client
|
||||
/// performed.
|
||||
async fn receive_keys_query_response(
|
||||
&self,
|
||||
request_id: &TransactionId,
|
||||
@@ -772,7 +810,7 @@ impl OlmMachine {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn create_outbound_group_session_with_defaults(
|
||||
pub(crate) async fn create_outbound_group_session_with_defaults_test_helper(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> OlmResult<()> {
|
||||
@@ -789,7 +827,7 @@ impl OlmMachine {
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn create_inbound_session(
|
||||
pub(crate) async fn create_inbound_session_test_helper(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> OlmResult<InboundGroupSession> {
|
||||
@@ -824,11 +862,11 @@ impl OlmMachine {
|
||||
content: impl MessageLikeEventContent,
|
||||
) -> MegolmResult<Raw<RoomEncryptedEventContent>> {
|
||||
let event_type = content.event_type().to_string();
|
||||
let content = serde_json::to_value(&content)?;
|
||||
self.encrypt_room_event_raw(room_id, content, &event_type).await
|
||||
let content = Raw::new(&content)?.cast();
|
||||
self.encrypt_room_event_raw(room_id, &event_type, &content).await
|
||||
}
|
||||
|
||||
/// Encrypt a json [`Value`] content for the given room.
|
||||
/// Encrypt a raw JSON content for the given room.
|
||||
///
|
||||
/// This method is equivalent to the [`OlmMachine::encrypt_room_event()`]
|
||||
/// method but operates on an arbitrary JSON value instead of strongly-typed
|
||||
@@ -840,7 +878,7 @@ impl OlmMachine {
|
||||
/// encrypted.
|
||||
///
|
||||
/// * `content` - The plaintext content of the message that should be
|
||||
/// encrypted as a json [`Value`].
|
||||
/// encrypted as a raw JSON value.
|
||||
///
|
||||
/// * `event_type` - The plaintext type of the event.
|
||||
///
|
||||
@@ -850,10 +888,10 @@ impl OlmMachine {
|
||||
pub async fn encrypt_room_event_raw(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
content: Value,
|
||||
event_type: &str,
|
||||
content: &Raw<AnyMessageLikeEventContent>,
|
||||
) -> MegolmResult<Raw<RoomEncryptedEventContent>> {
|
||||
self.inner.group_session_manager.encrypt(room_id, content, event_type).await
|
||||
self.inner.group_session_manager.encrypt(room_id, event_type, content).await
|
||||
}
|
||||
|
||||
/// Invalidate the currently active outbound group session for the given
|
||||
@@ -1142,18 +1180,12 @@ impl OlmMachine {
|
||||
/// This will decrypt and handle to-device events returning the decrypted
|
||||
/// versions of them.
|
||||
///
|
||||
/// To decrypt an event from the room timeline call [`decrypt_room_event`].
|
||||
/// To decrypt an event from the room timeline, call [`decrypt_room_event`].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `to_device_events` - The to-device events of the current sync
|
||||
/// response.
|
||||
///
|
||||
/// * `changed_devices` - The list of devices that changed in this sync
|
||||
/// response.
|
||||
///
|
||||
/// * `one_time_keys_count` - The current one-time keys counts that the sync
|
||||
/// response returned.
|
||||
/// * `sync_changes` - an [`EncryptionSyncChanges`] value, constructed from
|
||||
/// a sync response.
|
||||
///
|
||||
/// [`decrypt_room_event`]: #method.decrypt_room_event
|
||||
///
|
||||
@@ -1347,10 +1379,8 @@ impl OlmMachine {
|
||||
/// ```
|
||||
pub async fn query_missing_secrets_from_other_sessions(&self) -> StoreResult<bool> {
|
||||
let identity = self.inner.user_identity.lock().await;
|
||||
#[allow(unused_mut)]
|
||||
let mut secrets = identity.get_missing_secrets().await;
|
||||
|
||||
#[cfg(feature = "backups_v1")]
|
||||
if self.store().load_backup_keys().await?.decryption_key.is_none() {
|
||||
secrets.push(SecretName::RecoveryKey);
|
||||
}
|
||||
@@ -1550,6 +1580,16 @@ impl OlmMachine {
|
||||
result
|
||||
}
|
||||
|
||||
/// Do we have the room key for the given room and with the given session id
|
||||
/// in the store?
|
||||
pub async fn is_room_key_available(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
session_id: &str,
|
||||
) -> Result<bool, CryptoStoreError> {
|
||||
Ok(self.store().get_inbound_group_session(room_id, session_id).await?.is_some())
|
||||
}
|
||||
|
||||
/// Get encryption info for a decrypted timeline event.
|
||||
///
|
||||
/// This recalculates the [`EncryptionInfo`] data that is returned by
|
||||
@@ -1615,10 +1655,8 @@ impl OlmMachine {
|
||||
self.inner
|
||||
.identity_manager
|
||||
.key_query_manager
|
||||
.synced(&cache)
|
||||
.await?
|
||||
.wait_if_user_key_query_pending(timeout, user_id)
|
||||
.await;
|
||||
.wait_if_user_key_query_pending(cache, timeout, user_id)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1652,6 +1690,7 @@ impl OlmMachine {
|
||||
/// println!("{:?}", device);
|
||||
/// # });
|
||||
/// ```
|
||||
#[instrument(skip(self))]
|
||||
pub async fn get_device(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
@@ -1675,6 +1714,7 @@ impl OlmMachine {
|
||||
///
|
||||
/// Returns a `UserIdentities` enum if one is found and the crypto store
|
||||
/// didn't throw an error.
|
||||
#[instrument(skip(self))]
|
||||
pub async fn get_identity(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
@@ -1710,6 +1750,7 @@ impl OlmMachine {
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
#[instrument(skip(self))]
|
||||
pub async fn get_user_devices(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
@@ -1749,77 +1790,17 @@ impl OlmMachine {
|
||||
/// machine.import_room_keys(exported_keys, false, |_, _| {}).await.unwrap();
|
||||
/// # };
|
||||
/// ```
|
||||
#[deprecated(
|
||||
since = "0.7.0",
|
||||
note = "Use the OlmMachine::store::import_exported_room_keys method instead"
|
||||
)]
|
||||
pub async fn import_room_keys(
|
||||
&self,
|
||||
exported_keys: Vec<ExportedRoomKey>,
|
||||
#[allow(unused_variables)] from_backup: bool,
|
||||
from_backup: bool,
|
||||
progress_listener: impl Fn(usize, usize),
|
||||
) -> StoreResult<RoomKeyImportResult> {
|
||||
let mut sessions = Vec::new();
|
||||
|
||||
async fn new_session_better(
|
||||
session: &InboundGroupSession,
|
||||
old_session: Option<InboundGroupSession>,
|
||||
) -> bool {
|
||||
if let Some(old_session) = &old_session {
|
||||
session.compare(old_session).await == SessionOrdering::Better
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
let total_count = exported_keys.len();
|
||||
let mut keys = BTreeMap::new();
|
||||
|
||||
for (i, key) in exported_keys.into_iter().enumerate() {
|
||||
match InboundGroupSession::from_export(&key) {
|
||||
Ok(session) => {
|
||||
let old_session = self
|
||||
.inner
|
||||
.store
|
||||
.get_inbound_group_session(session.room_id(), session.session_id())
|
||||
.await?;
|
||||
|
||||
// Only import the session if we didn't have this session or
|
||||
// if it's a better version of the same session.
|
||||
if new_session_better(&session, old_session).await {
|
||||
#[cfg(feature = "backups_v1")]
|
||||
if from_backup {
|
||||
session.mark_as_backed_up();
|
||||
}
|
||||
|
||||
keys.entry(session.room_id().to_owned())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.entry(session.sender_key().to_base64())
|
||||
.or_insert_with(BTreeSet::new)
|
||||
.insert(session.session_id().to_owned());
|
||||
|
||||
sessions.push(session);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
sender_key= key.sender_key.to_base64(),
|
||||
room_id = ?key.room_id,
|
||||
session_id = key.session_id,
|
||||
error = ?e,
|
||||
"Couldn't import a room key from a file export."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
progress_listener(i, total_count);
|
||||
}
|
||||
|
||||
let imported_count = sessions.len();
|
||||
|
||||
let changes = Changes { inbound_group_sessions: sessions, ..Default::default() };
|
||||
|
||||
self.store().save_changes(changes).await?;
|
||||
|
||||
info!(total_count, imported_count, room_keys = ?keys, "Successfully imported room keys");
|
||||
|
||||
Ok(RoomKeyImportResult::new(imported_count, total_count, keys))
|
||||
self.store().import_room_keys(exported_keys, from_backup, progress_listener).await
|
||||
}
|
||||
|
||||
/// Export the keys that match the given predicate.
|
||||
@@ -1950,7 +1931,6 @@ impl OlmMachine {
|
||||
///
|
||||
/// This state machine can be used to incrementally backup all room keys to
|
||||
/// the server.
|
||||
#[cfg(feature = "backups_v1")]
|
||||
pub fn backup_machine(&self) -> &BackupMachine {
|
||||
&self.inner.backup_machine
|
||||
}
|
||||
@@ -2116,9 +2096,10 @@ pub struct CrossSigningBootstrapRequests {
|
||||
pub struct EncryptionSyncChanges<'a> {
|
||||
/// The list of to-device events received in the sync.
|
||||
pub to_device_events: Vec<Raw<AnyToDeviceEvent>>,
|
||||
/// The mapping of changed and left devices, per user.
|
||||
/// The mapping of changed and left devices, per user, as returned in the
|
||||
/// sync response.
|
||||
pub changed_devices: &'a DeviceLists,
|
||||
/// The number of one time keys.
|
||||
/// The number of one time keys, as returned in the sync response.
|
||||
pub one_time_keys_counts: &'a BTreeMap<DeviceKeyAlgorithm, UInt>,
|
||||
/// An optional list of fallback keys.
|
||||
pub unused_fallback_keys: Option<&'a [DeviceKeyAlgorithm]>,
|
||||
@@ -2152,7 +2133,7 @@ pub(crate) mod tests {
|
||||
use matrix_sdk_common::deserialized_responses::{
|
||||
DeviceLinkProblem, ShieldState, VerificationLevel, VerificationState,
|
||||
};
|
||||
use matrix_sdk_test::{async_test, test_json};
|
||||
use matrix_sdk_test::{async_test, message_like_event_content, test_json};
|
||||
use ruma::{
|
||||
api::{
|
||||
client::{
|
||||
@@ -2226,13 +2207,13 @@ pub(crate) mod tests {
|
||||
fn keys_upload_response() -> upload_keys::v3::Response {
|
||||
let data = response_from_file(&test_json::KEYS_UPLOAD);
|
||||
upload_keys::v3::Response::try_from_http_response(data)
|
||||
.expect("Can't parse the keys upload response")
|
||||
.expect("Can't parse the `/keys/upload` response")
|
||||
}
|
||||
|
||||
fn keys_query_response() -> get_keys::v3::Response {
|
||||
let data = response_from_file(&test_json::KEYS_QUERY);
|
||||
get_keys::v3::Response::try_from_http_response(data)
|
||||
.expect("Can't parse the keys upload response")
|
||||
.expect("Can't parse the `/keys/upload` response")
|
||||
}
|
||||
|
||||
pub fn to_device_requests_to_content(
|
||||
@@ -2315,21 +2296,20 @@ pub(crate) mod tests {
|
||||
bob: &UserId,
|
||||
use_fallback_key: bool,
|
||||
) -> (OlmMachine, OlmMachine) {
|
||||
let (alice, bob, one_time_keys) = get_machine_pair(alice, bob, use_fallback_key).await;
|
||||
let (alice, bob, mut one_time_keys) = get_machine_pair(alice, bob, use_fallback_key).await;
|
||||
|
||||
let mut bob_keys = BTreeMap::new();
|
||||
let (device_key_id, one_time_key) = one_time_keys.pop_first().unwrap();
|
||||
|
||||
let (device_key_id, one_time_key) = one_time_keys.iter().next().unwrap();
|
||||
let mut keys = BTreeMap::new();
|
||||
keys.insert(device_key_id.clone(), one_time_key.clone());
|
||||
bob_keys.insert(bob.device_id().into(), keys);
|
||||
|
||||
let mut one_time_keys = BTreeMap::new();
|
||||
one_time_keys.insert(bob.user_id().to_owned(), bob_keys);
|
||||
let one_time_keys = BTreeMap::from([(
|
||||
bob.user_id().to_owned(),
|
||||
BTreeMap::from([(
|
||||
bob.device_id().to_owned(),
|
||||
BTreeMap::from([(device_key_id, one_time_key)]),
|
||||
)]),
|
||||
)]);
|
||||
|
||||
let response = claim_keys::v3::Response::new(one_time_keys);
|
||||
|
||||
alice.receive_keys_claim_response(&response).await.unwrap();
|
||||
alice.inner.session_manager.create_sessions(&response).await.unwrap();
|
||||
|
||||
(alice, bob)
|
||||
}
|
||||
@@ -2344,10 +2324,8 @@ pub(crate) mod tests {
|
||||
let bob_device =
|
||||
alice.get_device(bob.user_id(), bob.device_id(), None).await.unwrap().unwrap();
|
||||
|
||||
let (session, content) = bob_device
|
||||
.encrypt("m.dummy", serde_json::to_value(ToDeviceDummyEventContent::new()).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let (session, content) =
|
||||
bob_device.encrypt("m.dummy", ToDeviceDummyEventContent::new()).await.unwrap();
|
||||
alice.store().save_sessions(&[session]).await.unwrap();
|
||||
|
||||
let event =
|
||||
@@ -2452,11 +2430,11 @@ pub(crate) mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn tests_session_invalidation() {
|
||||
async fn test_session_invalidation() {
|
||||
let machine = OlmMachine::new(user_id(), alice_device_id()).await;
|
||||
let room_id = room_id!("!test:example.org");
|
||||
|
||||
machine.create_outbound_group_session_with_defaults(room_id).await.unwrap();
|
||||
machine.create_outbound_group_session_with_defaults_test_helper(room_id).await.unwrap();
|
||||
assert!(machine.inner.group_session_manager.get_outbound_group_session(room_id).is_some());
|
||||
|
||||
machine.invalidate_group_session(room_id).await.unwrap();
|
||||
@@ -2631,25 +2609,26 @@ pub(crate) mod tests {
|
||||
key_id: OwnedDeviceKeyId,
|
||||
one_time_key: Raw<OneTimeKey>,
|
||||
) {
|
||||
let keys = BTreeMap::from([(key_id, one_time_key)]);
|
||||
let keys = BTreeMap::from([(device_id.to_owned(), keys)]);
|
||||
let one_time_keys = BTreeMap::from([(user_id.to_owned(), keys)]);
|
||||
let response = claim_keys::v3::Response::new(one_time_keys);
|
||||
let one_time_keys = BTreeMap::from([(
|
||||
user_id.to_owned(),
|
||||
BTreeMap::from([(device_id.to_owned(), BTreeMap::from([(key_id, one_time_key)]))]),
|
||||
)]);
|
||||
|
||||
machine.receive_keys_claim_response(&response).await.unwrap();
|
||||
let response = claim_keys::v3::Response::new(one_time_keys);
|
||||
machine.inner.session_manager.create_sessions(&response).await.unwrap();
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_session_creation() {
|
||||
let (alice_machine, bob_machine, mut one_time_keys) =
|
||||
get_machine_pair(alice_id(), user_id(), false).await;
|
||||
let (device_key_id, one_time_key) = one_time_keys.pop_first().unwrap();
|
||||
let (key_id, one_time_key) = one_time_keys.pop_first().unwrap();
|
||||
|
||||
create_session(
|
||||
&alice_machine,
|
||||
bob_machine.user_id(),
|
||||
bob_machine.device_id(),
|
||||
device_key_id,
|
||||
key_id,
|
||||
one_time_key,
|
||||
)
|
||||
.await;
|
||||
@@ -2670,7 +2649,7 @@ pub(crate) mod tests {
|
||||
async fn test_getting_most_recent_session() {
|
||||
let (alice_machine, bob_machine, mut one_time_keys) =
|
||||
get_machine_pair(alice_id(), user_id(), false).await;
|
||||
let (device_key_id, one_time_key) = one_time_keys.pop_first().unwrap();
|
||||
let (key_id, one_time_key) = one_time_keys.pop_first().unwrap();
|
||||
|
||||
let device = alice_machine
|
||||
.get_device(bob_machine.user_id(), bob_machine.device_id(), None)
|
||||
@@ -2684,19 +2663,19 @@ pub(crate) mod tests {
|
||||
&alice_machine,
|
||||
bob_machine.user_id(),
|
||||
bob_machine.device_id(),
|
||||
device_key_id,
|
||||
key_id,
|
||||
one_time_key.to_owned(),
|
||||
)
|
||||
.await;
|
||||
|
||||
for _ in 0..10 {
|
||||
let (device_key_id, one_time_key) = one_time_keys.pop_first().unwrap();
|
||||
let (key_id, one_time_key) = one_time_keys.pop_first().unwrap();
|
||||
|
||||
create_session(
|
||||
&alice_machine,
|
||||
bob_machine.user_id(),
|
||||
bob_machine.device_id(),
|
||||
device_key_id,
|
||||
key_id,
|
||||
one_time_key.to_owned(),
|
||||
)
|
||||
.await;
|
||||
@@ -2749,7 +2728,7 @@ pub(crate) mod tests {
|
||||
alice.get_device(bob.user_id(), bob.device_id(), None).await.unwrap().unwrap();
|
||||
|
||||
let (_, content) = bob_device
|
||||
.encrypt("m.dummy", serde_json::to_value(ToDeviceDummyEventContent::new()).unwrap())
|
||||
.encrypt("m.dummy", ToDeviceDummyEventContent::new())
|
||||
.await
|
||||
.expect("We should be able to encrypt a dummy event.");
|
||||
|
||||
@@ -2872,11 +2851,7 @@ pub(crate) mod tests {
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
if cfg!(feature = "backups_v1") {
|
||||
assert_eq!(outgoing_to_device.len(), 4);
|
||||
} else {
|
||||
assert_eq!(outgoing_to_device.len(), 3);
|
||||
}
|
||||
assert_eq!(outgoing_to_device.len(), 4);
|
||||
|
||||
// The second time, as there are already in-flight requests, it should have no
|
||||
// effect.
|
||||
@@ -2893,25 +2868,21 @@ pub(crate) mod tests {
|
||||
|
||||
let should_query_secrets = alice.query_missing_secrets_from_other_sessions().await.unwrap();
|
||||
|
||||
if cfg!(feature = "backups_v1") {
|
||||
assert!(should_query_secrets);
|
||||
assert!(should_query_secrets);
|
||||
|
||||
let outgoing_to_device = alice
|
||||
.outgoing_requests()
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter(|outgoing| match outgoing.request.as_ref() {
|
||||
OutgoingRequests::ToDeviceRequest(request) => {
|
||||
request.event_type.to_string() == "m.secret.request"
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
.collect_vec();
|
||||
assert_eq!(outgoing_to_device.len(), 1);
|
||||
} else {
|
||||
assert!(!should_query_secrets);
|
||||
}
|
||||
let outgoing_to_device = alice
|
||||
.outgoing_requests()
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter(|outgoing| match outgoing.request.as_ref() {
|
||||
OutgoingRequests::ToDeviceRequest(request) => {
|
||||
request.event_type.to_string() == "m.secret.request"
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
.collect_vec();
|
||||
assert_eq!(outgoing_to_device.len(), 1);
|
||||
|
||||
// The second time, as there are already in-flight requests, it should have no
|
||||
// effect.
|
||||
@@ -3294,7 +3265,7 @@ pub(crate) mod tests {
|
||||
);
|
||||
|
||||
let kq_response = KeyQueryResponse::try_from_http_response(response_from_file(&json))
|
||||
.expect("Can't parse the keys upload response");
|
||||
.expect("Can't parse the `/keys/upload` response");
|
||||
|
||||
alice.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap();
|
||||
bob.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap();
|
||||
@@ -3362,7 +3333,7 @@ pub(crate) mod tests {
|
||||
);
|
||||
|
||||
let kq_response = KeyQueryResponse::try_from_http_response(response_from_file(&json))
|
||||
.expect("Can't parse the keys upload response");
|
||||
.expect("Can't parse the `/keys/upload` response");
|
||||
|
||||
alice.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap();
|
||||
bob.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap();
|
||||
@@ -3427,7 +3398,7 @@ pub(crate) mod tests {
|
||||
);
|
||||
|
||||
let kq_response = KeyQueryResponse::try_from_http_response(response_from_file(&json))
|
||||
.expect("Can't parse the keys upload response");
|
||||
.expect("Can't parse the `/keys/upload` response");
|
||||
|
||||
alice.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap();
|
||||
bob.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap();
|
||||
@@ -3452,7 +3423,7 @@ pub(crate) mod tests {
|
||||
|
||||
let data = response_from_file(&test_json::KEYS_QUERY_TWO_DEVICES_ONE_SIGNED);
|
||||
let response = get_keys::v3::Response::try_from_http_response(data)
|
||||
.expect("Can't parse the keys upload response");
|
||||
.expect("Can't parse the `/keys/upload` response");
|
||||
|
||||
let (device_change, identity_change) =
|
||||
bob.receive_keys_query_response(&TransactionId::new(), &response).await.unwrap();
|
||||
@@ -3533,7 +3504,7 @@ pub(crate) mod tests {
|
||||
.expect("should exist")
|
||||
.set_trust_state(crate::LocalTrust::Verified);
|
||||
|
||||
alice.create_outbound_group_session_with_defaults(room_id).await.unwrap();
|
||||
alice.create_outbound_group_session_with_defaults_test_helper(room_id).await.unwrap();
|
||||
|
||||
let plaintext = "It is a secret to everybody";
|
||||
|
||||
@@ -3750,7 +3721,7 @@ pub(crate) mod tests {
|
||||
// Alice sends a key
|
||||
let msgs = alice.inner.verification_machine.outgoing_messages();
|
||||
assert!(msgs.len() == 1);
|
||||
let msg = msgs.first().unwrap();
|
||||
let msg = &msgs[0];
|
||||
let event = outgoing_request_to_event(alice.user_id(), msg);
|
||||
alice.inner.verification_machine.mark_request_as_sent(&msg.request_id);
|
||||
|
||||
@@ -3763,7 +3734,7 @@ pub(crate) mod tests {
|
||||
// Now bob sends a key
|
||||
let msgs = bob.inner.verification_machine.outgoing_messages();
|
||||
assert!(msgs.len() == 1);
|
||||
let msg = msgs.first().unwrap();
|
||||
let msg = &msgs[0];
|
||||
let event = outgoing_request_to_event(bob.user_id(), msg);
|
||||
bob.inner.verification_machine.mark_request_as_sent(&msg.request_id);
|
||||
|
||||
@@ -3876,16 +3847,15 @@ pub(crate) mod tests {
|
||||
let session_key = group_session.session_key();
|
||||
let session_id = group_session.session_id();
|
||||
|
||||
let content = json!({
|
||||
let content = message_like_event_content!({
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"room_id": room_id,
|
||||
"session_id": session_id,
|
||||
"session_key": session_key.to_base64(),
|
||||
|
||||
});
|
||||
|
||||
let encrypted_content =
|
||||
alice.encrypt_room_event_raw(room_id, content, "m.room_key").await.unwrap();
|
||||
alice.encrypt_room_event_raw(room_id, "m.room_key", &content).await.unwrap();
|
||||
let event = json!({
|
||||
"sender": alice.user_id(),
|
||||
"content": encrypted_content,
|
||||
@@ -3946,8 +3916,8 @@ pub(crate) mod tests {
|
||||
let signing_keys = SigningKeys::from([(DeviceKeyAlgorithm::Ed25519, fake_key)]);
|
||||
inbound.creator_info.signing_keys = signing_keys.into();
|
||||
|
||||
let content = json!({});
|
||||
let content = outbound.encrypt(content, "m.dummy").await;
|
||||
let content = message_like_event_content!({});
|
||||
let content = outbound.encrypt("m.dummy", &content).await;
|
||||
alice.store().save_inbound_group_sessions(&[inbound]).await.unwrap();
|
||||
|
||||
let event = json!({
|
||||
@@ -4053,4 +4023,52 @@ pub(crate) mod tests {
|
||||
"Our identity should not be verified when there's a mismatch in the cross-signing keys"
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_wait_on_key_query_doesnt_block_store() {
|
||||
// Waiting for a key query shouldn't delay other write attempts to the store.
|
||||
// This test will end immediately if it works, and times out after a few seconds
|
||||
// if it failed.
|
||||
|
||||
let machine = OlmMachine::new(bob_id(), bob_device_id()).await;
|
||||
|
||||
// Mark Alice as a tracked user, so it gets into the groups of users for which
|
||||
// we need to query keys.
|
||||
machine.update_tracked_users([alice_id()]).await.unwrap();
|
||||
|
||||
// Start a background task that will wait for the key query to finish silently
|
||||
// in the background.
|
||||
let machine_cloned = machine.clone();
|
||||
let wait = tokio::spawn(async move {
|
||||
let machine = machine_cloned;
|
||||
let user_devices =
|
||||
machine.get_user_devices(alice_id(), Some(Duration::from_secs(10))).await.unwrap();
|
||||
assert!(user_devices.devices().next().is_some());
|
||||
});
|
||||
|
||||
// Let the background task work first.
|
||||
tokio::task::yield_now().await;
|
||||
|
||||
// Create a key upload request and process it back immediately.
|
||||
let requests = machine.bootstrap_cross_signing(false).await.unwrap();
|
||||
|
||||
let req = requests.upload_keys_req.expect("upload keys request should be there");
|
||||
let response = keys_upload_response();
|
||||
let mark_request_as_sent = machine.mark_request_as_sent(&req.request_id, &response);
|
||||
tokio::time::timeout(Duration::from_secs(5), mark_request_as_sent)
|
||||
.await
|
||||
.expect("no timeout")
|
||||
.expect("the underlying request has been marked as sent");
|
||||
|
||||
// Answer the key query, so the background task completes immediately?
|
||||
let response = keys_query_response();
|
||||
let key_queries = machine.inner.identity_manager.users_for_key_query().await.unwrap();
|
||||
|
||||
for (id, _) in key_queries {
|
||||
machine.mark_request_as_sent(&id, &response).await.unwrap();
|
||||
}
|
||||
|
||||
// The waiting should successfully complete.
|
||||
wait.await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,11 @@ use crate::{
|
||||
OlmError, SignatureError,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum PrekeyBundle {
|
||||
Olm3DH { key: SignedKey },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum SessionType {
|
||||
New(Session),
|
||||
@@ -245,7 +250,6 @@ impl StaticAccountData {
|
||||
/// **Note**: Use this method with caution, the `canonical_json` needs to be
|
||||
/// correctly canonicalized and make sure that the object you are checking
|
||||
/// the signature for is allowed to be signed by our own device.
|
||||
#[cfg(any(test, feature = "backups_v1"))]
|
||||
pub fn has_signed_raw(
|
||||
&self,
|
||||
signatures: &crate::types::Signatures,
|
||||
@@ -314,7 +318,7 @@ impl StaticAccountData {
|
||||
pub struct Account {
|
||||
pub(crate) static_data: StaticAccountData,
|
||||
/// `vodozemac` account.
|
||||
inner: InnerAccount,
|
||||
inner: Box<InnerAccount>,
|
||||
/// Is this account ready to encrypt messages? (i.e. has it shared keys with
|
||||
/// a homeserver)
|
||||
shared: bool,
|
||||
@@ -397,7 +401,7 @@ impl Account {
|
||||
identity_keys: Arc::new(identity_keys),
|
||||
creation_local_time: MilliSecondsSinceUnixEpoch::now(),
|
||||
},
|
||||
inner: account,
|
||||
inner: Box::new(account),
|
||||
shared: false,
|
||||
uploaded_signed_key_count: 0,
|
||||
}
|
||||
@@ -601,7 +605,7 @@ impl Account {
|
||||
.expect("We should be able to convert a freshly created Account into a libolm pickle");
|
||||
|
||||
let data = DehydratedDeviceData::V1(DehydratedDeviceV1::new(device_pickle));
|
||||
Raw::from_json(to_raw_value(&data).expect("Coulnd't our dehydrated device data"))
|
||||
Raw::from_json(to_raw_value(&data).expect("Couldn't serialize our dehydrated device data"))
|
||||
}
|
||||
|
||||
pub(crate) async fn rehydrate(
|
||||
@@ -644,7 +648,7 @@ impl Account {
|
||||
identity_keys: Arc::new(identity_keys),
|
||||
creation_local_time: pickle.creation_local_time,
|
||||
},
|
||||
inner: account,
|
||||
inner: Box::new(account),
|
||||
shared: pickle.shared,
|
||||
uploaded_signed_key_count: pickle.uploaded_signed_key_count,
|
||||
})
|
||||
@@ -836,6 +840,43 @@ impl Account {
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
skip_all,
|
||||
fields(
|
||||
user_id = ?device.user_id(),
|
||||
device_id = ?device.device_id(),
|
||||
algorithms = ?device.algorithms()
|
||||
)
|
||||
)]
|
||||
fn find_pre_key_bundle(
|
||||
device: &ReadOnlyDevice,
|
||||
key_map: &BTreeMap<OwnedDeviceKeyId, Raw<ruma::encryption::OneTimeKey>>,
|
||||
) -> Result<PrekeyBundle, SessionCreationError> {
|
||||
let mut keys = key_map.iter();
|
||||
|
||||
let first_key = keys.next().ok_or_else(|| {
|
||||
SessionCreationError::OneTimeKeyMissing(
|
||||
device.user_id().to_owned(),
|
||||
device.device_id().into(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let first_key_id = first_key.0.to_owned();
|
||||
let first_key = OneTimeKey::deserialize(first_key_id.algorithm(), first_key.1)?;
|
||||
|
||||
let result = match first_key {
|
||||
OneTimeKey::SignedKey(key) => Ok(PrekeyBundle::Olm3DH { key }),
|
||||
_ => Err(SessionCreationError::OneTimeKeyUnknown(
|
||||
device.user_id().to_owned(),
|
||||
device.device_id().into(),
|
||||
)),
|
||||
};
|
||||
|
||||
trace!(?result, "Finished searching for a valid pre-key bundle");
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Create a new session with another account given a one-time key and a
|
||||
/// device.
|
||||
///
|
||||
@@ -853,50 +894,37 @@ impl Account {
|
||||
device: &ReadOnlyDevice,
|
||||
key_map: &BTreeMap<OwnedDeviceKeyId, Raw<ruma::encryption::OneTimeKey>>,
|
||||
) -> Result<Session, SessionCreationError> {
|
||||
let one_time_key = key_map.values().next().ok_or_else(|| {
|
||||
SessionCreationError::OneTimeKeyMissing(
|
||||
device.user_id().to_owned(),
|
||||
device.device_id().into(),
|
||||
)
|
||||
})?;
|
||||
let pre_key_bundle = Self::find_pre_key_bundle(device, key_map)?;
|
||||
|
||||
let one_time_key: SignedKey = match one_time_key.deserialize_as() {
|
||||
Ok(OneTimeKey::SignedKey(k)) => k,
|
||||
Ok(OneTimeKey::Key(_)) => {
|
||||
return Err(SessionCreationError::OneTimeKeyNotSigned(
|
||||
device.user_id().to_owned(),
|
||||
device.device_id().into(),
|
||||
));
|
||||
match pre_key_bundle {
|
||||
PrekeyBundle::Olm3DH { key } => {
|
||||
device.verify_one_time_key(&key).map_err(|error| {
|
||||
SessionCreationError::InvalidSignature {
|
||||
signing_key: device.ed25519_key(),
|
||||
one_time_key: key.clone(),
|
||||
error,
|
||||
}
|
||||
})?;
|
||||
|
||||
let identity_key = device.curve25519_key().ok_or_else(|| {
|
||||
SessionCreationError::DeviceMissingCurveKey(
|
||||
device.user_id().to_owned(),
|
||||
device.device_id().into(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let is_fallback = key.fallback();
|
||||
let one_time_key = key.key();
|
||||
let config = device.olm_session_config();
|
||||
|
||||
Ok(self.create_outbound_session_helper(
|
||||
config,
|
||||
identity_key,
|
||||
one_time_key,
|
||||
is_fallback,
|
||||
))
|
||||
}
|
||||
Ok(_) => {
|
||||
return Err(SessionCreationError::OneTimeKeyUnknown(
|
||||
device.user_id().to_owned(),
|
||||
device.device_id().into(),
|
||||
));
|
||||
}
|
||||
Err(e) => return Err(SessionCreationError::InvalidJson(e)),
|
||||
};
|
||||
|
||||
device.verify_one_time_key(&one_time_key).map_err(|error| {
|
||||
SessionCreationError::InvalidSignature {
|
||||
signing_key: device.ed25519_key(),
|
||||
one_time_key: one_time_key.clone(),
|
||||
error,
|
||||
}
|
||||
})?;
|
||||
|
||||
let identity_key = device.curve25519_key().ok_or_else(|| {
|
||||
SessionCreationError::DeviceMissingCurveKey(
|
||||
device.user_id().to_owned(),
|
||||
device.device_id().into(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let is_fallback = one_time_key.fallback();
|
||||
let one_time_key = one_time_key.key();
|
||||
let config = device.olm_session_config();
|
||||
|
||||
Ok(self.create_outbound_session_helper(config, identity_key, one_time_key, is_fallback))
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new session with another account given a pre-key Olm message.
|
||||
@@ -956,21 +984,15 @@ impl Account {
|
||||
use ruma::events::dummy::ToDeviceDummyEventContent;
|
||||
|
||||
other.generate_one_time_keys_helper(1);
|
||||
let one_time = other.signed_one_time_keys();
|
||||
|
||||
let one_time_map = other.signed_one_time_keys();
|
||||
let device = ReadOnlyDevice::from_account(other);
|
||||
|
||||
let mut our_session = self.create_outbound_session(&device, &one_time).unwrap();
|
||||
let mut our_session = self.create_outbound_session(&device, &one_time_map).unwrap();
|
||||
|
||||
other.mark_keys_as_published();
|
||||
|
||||
let message = our_session
|
||||
.encrypt(
|
||||
&device,
|
||||
"m.dummy",
|
||||
serde_json::to_value(ToDeviceDummyEventContent::new()).unwrap(),
|
||||
None,
|
||||
)
|
||||
.encrypt(&device, "m.dummy", ToDeviceDummyEventContent::new(), None)
|
||||
.await
|
||||
.unwrap()
|
||||
.deserialize()
|
||||
@@ -1038,7 +1060,7 @@ impl Account {
|
||||
self.decrypt_olm_helper(store, sender, content.sender_key, &content.ciphertext).await
|
||||
}
|
||||
|
||||
#[instrument(skip_all, fields(sender, sender_key = %content.sender_key))]
|
||||
#[instrument(skip_all, fields(sender, sender_key = ?content.sender_key))]
|
||||
async fn decrypt_olm_v1(
|
||||
&mut self,
|
||||
store: &Store,
|
||||
|
||||
@@ -111,7 +111,7 @@ pub struct InboundGroupSession {
|
||||
inner: Arc<Mutex<InnerSession>>,
|
||||
|
||||
/// A copy of [`InnerSession::session_id`] to avoid having to acquire a lock
|
||||
/// to get to the sesison ID.
|
||||
/// to get to the session ID.
|
||||
session_id: Arc<str>,
|
||||
|
||||
/// A copy of [`InnerSession::first_known_index`] to avoid having to acquire
|
||||
@@ -398,7 +398,6 @@ impl InboundGroupSession {
|
||||
|
||||
/// Export the inbound group session into a format that can be uploaded to
|
||||
/// the server as a backup.
|
||||
#[cfg(feature = "backups_v1")]
|
||||
pub async fn to_backup(&self) -> BackedUpRoomKey {
|
||||
self.export().await.into()
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ mod outbound;
|
||||
pub use inbound::{InboundGroupSession, PickledInboundGroupSession};
|
||||
pub(crate) use outbound::ShareState;
|
||||
pub use outbound::{
|
||||
EncryptionSettings, GroupSession, OutboundGroupSession, PickledOutboundGroupSession, ShareInfo,
|
||||
EncryptionSettings, OutboundGroupSession, PickledOutboundGroupSession, ShareInfo,
|
||||
};
|
||||
use thiserror::Error;
|
||||
pub use vodozemac::megolm::{ExportedSessionKey, SessionKey};
|
||||
@@ -97,6 +97,24 @@ pub struct ExportedRoomKey {
|
||||
pub forwarding_curve25519_key_chain: Vec<Curve25519PublicKey>,
|
||||
}
|
||||
|
||||
impl ExportedRoomKey {
|
||||
pub(crate) fn from_backed_up_room_key(
|
||||
room_id: OwnedRoomId,
|
||||
session_id: String,
|
||||
room_key: BackedUpRoomKey,
|
||||
) -> Self {
|
||||
Self {
|
||||
algorithm: room_key.algorithm,
|
||||
room_id,
|
||||
sender_key: room_key.sender_key,
|
||||
session_id,
|
||||
session_key: room_key.session_key,
|
||||
sender_claimed_keys: room_key.sender_claimed_keys,
|
||||
forwarding_curve25519_key_chain: room_key.forwarding_curve25519_key_chain,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A backed up version of an `InboundGroupSession`
|
||||
///
|
||||
/// This can be used to backup the `InboundGroupSession` to the server.
|
||||
|
||||
@@ -24,13 +24,15 @@ use std::{
|
||||
};
|
||||
|
||||
use ruma::{
|
||||
events::room::{encryption::RoomEncryptionEventContent, history_visibility::HistoryVisibility},
|
||||
events::{
|
||||
room::{encryption::RoomEncryptionEventContent, history_visibility::HistoryVisibility},
|
||||
AnyMessageLikeEventContent,
|
||||
},
|
||||
serde::Raw,
|
||||
DeviceId, OwnedDeviceId, OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId,
|
||||
SecondsSinceUnixEpoch, TransactionId, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{debug, error, info};
|
||||
use vodozemac::{megolm::SessionConfig, Curve25519PublicKey};
|
||||
@@ -54,7 +56,7 @@ use crate::{
|
||||
},
|
||||
EventEncryptionAlgorithm,
|
||||
},
|
||||
Device, ToDeviceRequest,
|
||||
ReadOnlyDevice, ToDeviceRequest,
|
||||
};
|
||||
|
||||
const ROTATION_PERIOD: Duration = Duration::from_millis(604800000);
|
||||
@@ -356,30 +358,37 @@ impl OutboundGroupSession {
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `content` - The plaintext content of the message that should be
|
||||
/// encrypted in raw json [`Value`] form.
|
||||
///
|
||||
/// * `event_type` - The plaintext type of the event, the outer type of the
|
||||
/// event will become `m.room.encrypted`.
|
||||
///
|
||||
/// * `content` - The plaintext content of the message that should be
|
||||
/// encrypted in raw JSON form.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the content can't be serialized.
|
||||
pub async fn encrypt(
|
||||
&self,
|
||||
content: Value,
|
||||
event_type: &str,
|
||||
content: &Raw<AnyMessageLikeEventContent>,
|
||||
) -> Raw<RoomEncryptedEventContent> {
|
||||
let json_content = json!({
|
||||
"content": content,
|
||||
"room_id": &*self.room_id,
|
||||
"type": event_type,
|
||||
});
|
||||
#[derive(Serialize)]
|
||||
struct Payload<'a> {
|
||||
#[serde(rename = "type")]
|
||||
event_type: &'a str,
|
||||
content: &'a Raw<AnyMessageLikeEventContent>,
|
||||
room_id: &'a RoomId,
|
||||
}
|
||||
|
||||
let plaintext = json_content.to_string();
|
||||
let relates_to = content.get("m.relates_to").cloned();
|
||||
let payload = Payload { event_type, content, room_id: &self.room_id };
|
||||
let payload_json =
|
||||
serde_json::to_string(&payload).expect("payload serialization never fails");
|
||||
|
||||
let ciphertext = self.encrypt_helper(plaintext).await;
|
||||
let relates_to = content
|
||||
.get_field::<serde_json::Value>("m.relates_to")
|
||||
.expect("serde_json::Value deserialization with valid JSON input never fails");
|
||||
|
||||
let ciphertext = self.encrypt_helper(payload_json).await;
|
||||
let scheme: RoomEventEncryptionScheme = match self.settings.algorithm {
|
||||
EventEncryptionAlgorithm::MegolmV1AesSha2 => MegolmV1AesSha2Content {
|
||||
ciphertext,
|
||||
@@ -495,7 +504,7 @@ impl OutboundGroupSession {
|
||||
}
|
||||
|
||||
/// Has or will the session be shared with the given user/device pair.
|
||||
pub(crate) fn is_shared_with(&self, device: &Device) -> ShareState {
|
||||
pub(crate) fn is_shared_with(&self, device: &ReadOnlyDevice) -> ShareState {
|
||||
// Check if we shared the session.
|
||||
let shared_state =
|
||||
self.shared_with_set.read().unwrap().get(device.user_id()).and_then(|d| {
|
||||
@@ -539,7 +548,7 @@ impl OutboundGroupSession {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_withheld_to(&self, device: &Device, code: &WithheldCode) -> bool {
|
||||
pub(crate) fn is_withheld_to(&self, device: &ReadOnlyDevice, code: &WithheldCode) -> bool {
|
||||
self.shared_with_set
|
||||
.read()
|
||||
.unwrap()
|
||||
@@ -762,7 +771,7 @@ mod tests {
|
||||
#[async_test]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos", target_arch = "wasm32"))]
|
||||
async fn test_expiration() -> Result<(), MegolmError> {
|
||||
use ruma::SecondsSinceUnixEpoch;
|
||||
use ruma::{serde::Raw, SecondsSinceUnixEpoch};
|
||||
|
||||
let settings = EncryptionSettings { rotation_period_msgs: 1, ..Default::default() };
|
||||
|
||||
@@ -777,8 +786,8 @@ mod tests {
|
||||
assert!(!session.expired());
|
||||
let _ = session
|
||||
.encrypt(
|
||||
serde_json::to_value(RoomMessageEventContent::text_plain("Test message"))?,
|
||||
"m.room.message",
|
||||
&Raw::new(&RoomMessageEventContent::text_plain("Test message"))?.cast(),
|
||||
)
|
||||
.await;
|
||||
assert!(session.expired());
|
||||
@@ -810,8 +819,8 @@ mod tests {
|
||||
|
||||
let _ = session
|
||||
.encrypt(
|
||||
serde_json::to_value(RoomMessageEventContent::text_plain("Test message"))?,
|
||||
"m.room.message",
|
||||
&Raw::new(&RoomMessageEventContent::text_plain("Test message"))?.cast(),
|
||||
)
|
||||
.await;
|
||||
assert!(session.expired());
|
||||
|
||||
@@ -39,7 +39,7 @@ pub use vodozemac::{olm::IdentityKeys, Curve25519PublicKey};
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk_test::async_test;
|
||||
use matrix_sdk_test::{async_test, message_like_event_content};
|
||||
use ruma::{
|
||||
device_id, event_id,
|
||||
events::{
|
||||
@@ -47,7 +47,9 @@ pub(crate) mod tests {
|
||||
room::message::{Relation, RoomMessageEventContent},
|
||||
AnyMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
|
||||
},
|
||||
room_id, user_id, DeviceId, UserId,
|
||||
room_id,
|
||||
serde::Raw,
|
||||
user_id, DeviceId, UserId,
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
use vodozemac::{
|
||||
@@ -232,7 +234,7 @@ pub(crate) mod tests {
|
||||
assert_eq!(outbound.session_id(), inbound.session_id());
|
||||
|
||||
let encrypted_content =
|
||||
outbound.encrypt(serde_json::to_value(content).unwrap(), "m.room.message").await;
|
||||
outbound.encrypt("m.room.message", &Raw::new(&content).unwrap().cast()).await;
|
||||
|
||||
let event = json!({
|
||||
"sender": alice.user_id(),
|
||||
@@ -271,10 +273,10 @@ pub(crate) mod tests {
|
||||
|
||||
// We first test that we're copying the relation from the content that
|
||||
// will be encrypted to the content that will stay plaintext.
|
||||
let content = json!({
|
||||
let content = message_like_event_content!({
|
||||
"m.relates_to": relation_json,
|
||||
});
|
||||
let encrypted = outbound.encrypt(content, "m.dummy").await;
|
||||
let encrypted = outbound.encrypt("m.dummy", &content).await;
|
||||
|
||||
let event = json!({
|
||||
"sender": alice.user_id(),
|
||||
@@ -298,8 +300,8 @@ pub(crate) mod tests {
|
||||
let relation = decrypted.get("content").and_then(|c| c.get("m.relates_to"));
|
||||
assert_eq!(relation, Some(&relation_json), "The decrypted event should contain a relation");
|
||||
|
||||
let content = json!({});
|
||||
let encrypted = outbound.encrypt(content, "m.dummy").await;
|
||||
let content = message_like_event_content!({});
|
||||
let encrypted = outbound.encrypt("m.dummy", &content).await;
|
||||
let mut encrypted: Value = json_convert(&encrypted).unwrap();
|
||||
encrypted.as_object_mut().unwrap().insert("m.relates_to".to_owned(), relation_json.clone());
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
use ruma::{serde::Raw, JsOption, OwnedDeviceId, OwnedUserId, SecondsSinceUnixEpoch};
|
||||
use ruma::{serde::Raw, OwnedDeviceId, OwnedUserId, SecondsSinceUnixEpoch};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{field::debug, instrument, trace, Span};
|
||||
use vodozemac::{
|
||||
@@ -73,7 +73,7 @@ impl fmt::Debug for Session {
|
||||
impl Session {
|
||||
/// Decrypt the given Olm message.
|
||||
///
|
||||
/// Returns the decrypted plaintext or an `DecryptionError` if decryption
|
||||
/// Returns the decrypted plaintext or a [`DecryptionError`] if decryption
|
||||
/// failed.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -99,12 +99,12 @@ impl Session {
|
||||
self.sender_key
|
||||
}
|
||||
|
||||
/// Get the `SessionConfig` that this session is using.
|
||||
/// Get the [`SessionConfig`] that this session is using.
|
||||
pub async fn session_config(&self) -> SessionConfig {
|
||||
self.inner.lock().await.session_config()
|
||||
}
|
||||
|
||||
/// Get the `EventEncryptionAlgorithm` of t his `Session`.
|
||||
/// Get the [`EventEncryptionAlgorithm`] of this [`Session`].
|
||||
pub async fn algorithm(&self) -> EventEncryptionAlgorithm {
|
||||
#[cfg(feature = "experimental-algorithms")]
|
||||
if self.session_config().await.version() == 2 {
|
||||
@@ -150,7 +150,7 @@ impl Session {
|
||||
&mut self,
|
||||
recipient_device: &ReadOnlyDevice,
|
||||
event_type: &str,
|
||||
content: Value,
|
||||
content: impl Serialize,
|
||||
message_id: Option<String>,
|
||||
) -> OlmResult<Raw<ToDeviceEncryptedEventContent>> {
|
||||
let plaintext = {
|
||||
@@ -181,14 +181,14 @@ impl Session {
|
||||
ciphertext,
|
||||
recipient_key: self.sender_key,
|
||||
sender_key: self.our_identity_keys.curve25519,
|
||||
message_id: JsOption::from_implicit_option(message_id),
|
||||
message_id,
|
||||
}
|
||||
.into(),
|
||||
#[cfg(feature = "experimental-algorithms")]
|
||||
EventEncryptionAlgorithm::OlmV2Curve25519AesSha2 => OlmV2Curve25519AesSha2Content {
|
||||
ciphertext,
|
||||
sender_key: self.our_identity_keys.curve25519,
|
||||
message_id: JsOption::from_implicit_option(message_id),
|
||||
message_id,
|
||||
}
|
||||
.into(),
|
||||
_ => unreachable!(),
|
||||
|
||||
@@ -51,6 +51,7 @@ pub struct Signing {
|
||||
public_key: Ed25519PublicKey,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl std::fmt::Debug for Signing {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Signing").field("public_key", &self.public_key.to_base64()).finish()
|
||||
|
||||
@@ -192,7 +192,6 @@ impl SignedJsonObject for CrossSigningKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "backups_v1")]
|
||||
impl SignedJsonObject for crate::types::MegolmV1AuthData {
|
||||
fn signatures(&self) -> &Signatures {
|
||||
&self.signatures
|
||||
|
||||
@@ -199,9 +199,9 @@ impl KeysQueryRequest {
|
||||
/// Enum over the different outgoing requests we can have.
|
||||
#[derive(Debug)]
|
||||
pub enum OutgoingRequests {
|
||||
/// The keys upload request, uploading device and one-time keys.
|
||||
/// The `/keys/upload` request, uploading device and one-time keys.
|
||||
KeysUpload(KeysUploadRequest),
|
||||
/// The keys query request, fetching the device and cross signing keys of
|
||||
/// The `/keys/query` request, fetching the device and cross signing keys of
|
||||
/// other users.
|
||||
KeysQuery(KeysQueryRequest),
|
||||
/// The request to claim one-time keys for a user/device pair from the
|
||||
@@ -292,18 +292,18 @@ impl From<KeysUploadRequest> for OutgoingRequest {
|
||||
/// Enum over all the incoming responses we need to receive.
|
||||
#[derive(Debug)]
|
||||
pub enum IncomingResponse<'a> {
|
||||
/// The keys upload response, notifying us about the amount of uploaded
|
||||
/// The `/keys/upload` response, notifying us about the amount of uploaded
|
||||
/// one-time keys.
|
||||
KeysUpload(&'a KeysUploadResponse),
|
||||
/// The keys query response, giving us the device and cross signing keys of
|
||||
/// other users.
|
||||
/// The `/keys/query` response, giving us the device and cross signing keys
|
||||
/// of other users.
|
||||
KeysQuery(&'a KeysQueryResponse),
|
||||
/// The to-device response, an empty response.
|
||||
ToDevice(&'a ToDeviceResponse),
|
||||
/// The key claiming requests, giving us new one-time keys of other users so
|
||||
/// new Olm sessions can be created.
|
||||
KeysClaim(&'a KeysClaimResponse),
|
||||
/// The cross signing keys upload response, marking our private cross
|
||||
/// The cross signing `/keys/upload` response, marking our private cross
|
||||
/// signing identity as shared.
|
||||
SigningKeysUpload(&'a SigningKeysUploadResponse),
|
||||
/// The cross signing signature upload response.
|
||||
|
||||
@@ -23,19 +23,21 @@ use futures_util::future::join_all;
|
||||
use itertools::{Either, Itertools};
|
||||
use matrix_sdk_common::executor::spawn;
|
||||
use ruma::{
|
||||
events::ToDeviceEventType, serde::Raw, to_device::DeviceIdOrAllDevices, DeviceId,
|
||||
OwnedDeviceId, OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId,
|
||||
events::{AnyMessageLikeEventContent, ToDeviceEventType},
|
||||
serde::Raw,
|
||||
to_device::DeviceIdOrAllDevices,
|
||||
DeviceId, OwnedDeviceId, OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId,
|
||||
UserId,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use tracing::{debug, error, info, instrument, trace};
|
||||
|
||||
use crate::{
|
||||
error::{EventError, MegolmResult, OlmResult},
|
||||
identities::device::MaybeEncryptedRoomKey,
|
||||
olm::{InboundGroupSession, OutboundGroupSession, Session, ShareInfo, ShareState},
|
||||
store::{Changes, Result as StoreResult, Store},
|
||||
store::{Changes, CryptoStoreWrapper, Result as StoreResult, Store},
|
||||
types::events::{room::encrypted::RoomEncryptedEventContent, room_key_withheld::WithheldCode},
|
||||
Device, EncryptionSettings, OlmError, ToDeviceRequest,
|
||||
EncryptionSettings, OlmError, ReadOnlyDevice, ToDeviceRequest,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -100,17 +102,17 @@ impl GroupSessionCache {
|
||||
self.sessions.read().unwrap().get(room_id).cloned()
|
||||
}
|
||||
|
||||
/// Get or load the session for the given room with the given session id.
|
||||
///
|
||||
/// This is the same as [get_or_load()](#method.get_or_load) but it will
|
||||
/// filter out the session if it doesn't match the given session id.
|
||||
#[cfg(feature = "automatic-room-key-forwarding")]
|
||||
pub async fn get_with_id(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
session_id: &str,
|
||||
) -> Option<OutboundGroupSession> {
|
||||
self.get_or_load(room_id).await.filter(|o| session_id == o.session_id())
|
||||
/// Returns whether any session is withheld with the given device and code.
|
||||
fn has_session_withheld_to(&self, device: &ReadOnlyDevice, code: &WithheldCode) -> bool {
|
||||
self.sessions.read().unwrap().values().any(|s| s.is_withheld_to(device, code))
|
||||
}
|
||||
|
||||
fn remove_from_being_shared(&self, id: &TransactionId) -> Option<OutboundGroupSession> {
|
||||
self.sessions_being_shared.write().unwrap().remove(id)
|
||||
}
|
||||
|
||||
fn mark_as_being_shared(&self, id: OwnedTransactionId, session: OutboundGroupSession) {
|
||||
self.sessions_being_shared.write().unwrap().insert(id, session);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,18 +123,18 @@ impl GroupSessionCache {
|
||||
/// (`devices`) or not the session, including withheld reason
|
||||
/// `withheld_devices`.
|
||||
#[derive(Debug)]
|
||||
pub struct CollectRecipientsResult {
|
||||
pub(crate) struct CollectRecipientsResult {
|
||||
/// If true the outbound group session should be rotated
|
||||
pub should_rotate: bool,
|
||||
/// The map of user|device that should receive the session
|
||||
pub devices: BTreeMap<OwnedUserId, Vec<Device>>,
|
||||
pub devices: BTreeMap<OwnedUserId, Vec<ReadOnlyDevice>>,
|
||||
/// The map of user|device that won't receive the key with the withheld
|
||||
/// code.
|
||||
pub withheld_devices: Vec<(Device, WithheldCode)>,
|
||||
pub withheld_devices: Vec<(ReadOnlyDevice, WithheldCode)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GroupSessionManager {
|
||||
pub(crate) struct GroupSessionManager {
|
||||
/// Store for the encryption keys.
|
||||
/// Persists all the encryption keys so a client can resume the session
|
||||
/// without the need to create new keys.
|
||||
@@ -144,7 +146,7 @@ pub struct GroupSessionManager {
|
||||
impl GroupSessionManager {
|
||||
const MAX_TO_DEVICE_MESSAGES: usize = 250;
|
||||
|
||||
pub(crate) fn new(store: Store) -> Self {
|
||||
pub fn new(store: Store) -> Self {
|
||||
Self { store: store.clone(), sessions: GroupSessionCache::new(store) }
|
||||
}
|
||||
|
||||
@@ -163,35 +165,33 @@ impl GroupSessionManager {
|
||||
}
|
||||
|
||||
pub async fn mark_request_as_sent(&self, request_id: &TransactionId) -> StoreResult<()> {
|
||||
let removed_session =
|
||||
self.sessions.sessions_being_shared.write().unwrap().remove(request_id);
|
||||
if let Some(session) = removed_session {
|
||||
let no_olm = session.mark_request_as_sent(request_id);
|
||||
let Some(session) = self.sessions.remove_from_being_shared(request_id) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mut changes = Changes::default();
|
||||
let no_olm = session.mark_request_as_sent(request_id);
|
||||
|
||||
for (user_id, devices) in &no_olm {
|
||||
for device_id in devices {
|
||||
let device = self.store.get_device(user_id, device_id).await;
|
||||
let mut changes = Changes::default();
|
||||
|
||||
if let Ok(Some(device)) = device {
|
||||
device.mark_withheld_code_as_sent();
|
||||
changes.devices.changed.push(device.inner.clone());
|
||||
} else {
|
||||
error!(
|
||||
?request_id,
|
||||
"Marking to-device no olm as sent but device not found, might \
|
||||
for (user_id, devices) in &no_olm {
|
||||
for device_id in devices {
|
||||
let device = self.store.get_device(user_id, device_id).await;
|
||||
|
||||
if let Ok(Some(device)) = device {
|
||||
device.mark_withheld_code_as_sent();
|
||||
changes.devices.changed.push(device.inner.clone());
|
||||
} else {
|
||||
error!(
|
||||
?request_id,
|
||||
"Marking to-device no olm as sent but device not found, might \
|
||||
have been deleted?"
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
changes.outbound_group_sessions.push(session.clone());
|
||||
self.store.save_changes(changes).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
changes.outbound_group_sessions.push(session.clone());
|
||||
self.store.save_changes(changes).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -202,15 +202,15 @@ impl GroupSessionManager {
|
||||
pub async fn encrypt(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
content: Value,
|
||||
event_type: &str,
|
||||
content: &Raw<AnyMessageLikeEventContent>,
|
||||
) -> MegolmResult<Raw<RoomEncryptedEventContent>> {
|
||||
let session =
|
||||
self.sessions.get_or_load(room_id).await.expect("Session wasn't created nor shared");
|
||||
|
||||
assert!(!session.expired(), "Session expired");
|
||||
|
||||
let content = session.encrypt(content, event_type).await;
|
||||
let content = session.encrypt(event_type, content).await;
|
||||
|
||||
let mut changes = Changes::default();
|
||||
changes.outbound_group_sessions.push(session);
|
||||
@@ -221,8 +221,7 @@ impl GroupSessionManager {
|
||||
|
||||
/// Create a new outbound group session.
|
||||
///
|
||||
/// This also creates a matching inbound group session and saves that one in
|
||||
/// the store.
|
||||
/// This also creates a matching inbound group session.
|
||||
pub async fn create_outbound_group_session(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
@@ -264,18 +263,19 @@ impl GroupSessionManager {
|
||||
/// Encrypt the given content for the given devices and create a to-device
|
||||
/// requests that sends the encrypted content to them.
|
||||
async fn encrypt_session_for(
|
||||
store: Arc<CryptoStoreWrapper>,
|
||||
group_session: OutboundGroupSession,
|
||||
devices: Vec<Device>,
|
||||
devices: Vec<ReadOnlyDevice>,
|
||||
) -> OlmResult<(
|
||||
OwnedTransactionId,
|
||||
ToDeviceRequest,
|
||||
BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceId, ShareInfo>>,
|
||||
Vec<Session>,
|
||||
Vec<(Device, WithheldCode)>,
|
||||
Vec<(ReadOnlyDevice, WithheldCode)>,
|
||||
)> {
|
||||
// Use a named type instead of a tuple with rather long type name
|
||||
pub struct DeviceResult {
|
||||
device: Device,
|
||||
device: ReadOnlyDevice,
|
||||
maybe_encrypted_room_key: MaybeEncryptedRoomKey,
|
||||
}
|
||||
|
||||
@@ -284,14 +284,20 @@ impl GroupSessionManager {
|
||||
let mut share_infos = BTreeMap::new();
|
||||
let mut withheld_devices = Vec::new();
|
||||
|
||||
let encrypt = |device: Device, session: OutboundGroupSession| async move {
|
||||
let encryption_result = device.maybe_encrypt_room_key(session).await?;
|
||||
// XXX is there a way to do this that doesn't involve cloning the
|
||||
// `Arc<CryptoStoreWrapper>` for each device?
|
||||
let encrypt = |store: Arc<CryptoStoreWrapper>,
|
||||
device: ReadOnlyDevice,
|
||||
session: OutboundGroupSession| async move {
|
||||
let encryption_result = device.maybe_encrypt_room_key(store.as_ref(), session).await?;
|
||||
|
||||
Ok::<_, OlmError>(DeviceResult { device, maybe_encrypted_room_key: encryption_result })
|
||||
};
|
||||
|
||||
let tasks: Vec<_> =
|
||||
devices.iter().map(|d| spawn(encrypt(d.clone(), group_session.clone()))).collect();
|
||||
let tasks: Vec<_> = devices
|
||||
.iter()
|
||||
.map(|d| spawn(encrypt(store.clone(), d.clone(), group_session.clone())))
|
||||
.collect();
|
||||
|
||||
let results = join_all(tasks).await;
|
||||
|
||||
@@ -344,8 +350,8 @@ impl GroupSessionManager {
|
||||
outbound: &OutboundGroupSession,
|
||||
) -> OlmResult<CollectRecipientsResult> {
|
||||
let users: BTreeSet<&UserId> = users.collect();
|
||||
let mut devices: BTreeMap<OwnedUserId, Vec<Device>> = Default::default();
|
||||
let mut withheld_devices: Vec<(Device, WithheldCode)> = Default::default();
|
||||
let mut devices: BTreeMap<OwnedUserId, Vec<ReadOnlyDevice>> = Default::default();
|
||||
let mut withheld_devices: Vec<(ReadOnlyDevice, WithheldCode)> = Default::default();
|
||||
|
||||
trace!(?users, ?settings, "Calculating group session recipients");
|
||||
|
||||
@@ -373,23 +379,37 @@ impl GroupSessionManager {
|
||||
// This is calculated in the following code and stored in this variable.
|
||||
let mut should_rotate = user_left || visibility_changed || algorithm_changed;
|
||||
|
||||
let own_identity =
|
||||
self.store.get_user_identity(self.store.user_id()).await?.and_then(|i| i.into_own());
|
||||
|
||||
for user_id in users {
|
||||
let user_devices = self.store.get_user_devices_filtered(user_id).await?;
|
||||
let user_devices = self.store.get_readonly_devices_filtered(user_id).await?;
|
||||
|
||||
// We only need the user identity if settings.only_allow_trusted_devices is set.
|
||||
let device_owner_identity = if settings.only_allow_trusted_devices {
|
||||
self.store.get_user_identity(user_id).await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// From all the devices a user has, we're splitting them into two
|
||||
// buckets, a bucket of devices that should receive the
|
||||
// room key and a bucket of devices that should receive
|
||||
// a withheld code.
|
||||
let (recipients, withheld_recipients): (Vec<Device>, Vec<(Device, WithheldCode)>) =
|
||||
user_devices.devices().partition_map(|d| {
|
||||
if d.is_blacklisted() {
|
||||
Either::Right((d, WithheldCode::Blacklisted))
|
||||
} else if settings.only_allow_trusted_devices && !d.is_verified() {
|
||||
Either::Right((d, WithheldCode::Unverified))
|
||||
} else {
|
||||
Either::Left(d)
|
||||
}
|
||||
});
|
||||
let (recipients, withheld_recipients): (
|
||||
Vec<ReadOnlyDevice>,
|
||||
Vec<(ReadOnlyDevice, WithheldCode)>,
|
||||
) = user_devices.into_values().partition_map(|d| {
|
||||
if d.is_blacklisted() {
|
||||
Either::Right((d, WithheldCode::Blacklisted))
|
||||
} else if settings.only_allow_trusted_devices
|
||||
&& !d.is_verified(&own_identity, &device_owner_identity)
|
||||
{
|
||||
Either::Right((d, WithheldCode::Unverified))
|
||||
} else {
|
||||
Either::Left(d)
|
||||
}
|
||||
});
|
||||
|
||||
// If we haven't already concluded that the session should be
|
||||
// rotated for other reasons, we also need to check whether any
|
||||
@@ -428,13 +448,14 @@ impl GroupSessionManager {
|
||||
Ok(CollectRecipientsResult { should_rotate, devices, withheld_devices })
|
||||
}
|
||||
|
||||
pub async fn encrypt_request(
|
||||
chunk: Vec<Device>,
|
||||
async fn encrypt_request(
|
||||
store: Arc<CryptoStoreWrapper>,
|
||||
chunk: Vec<ReadOnlyDevice>,
|
||||
outbound: OutboundGroupSession,
|
||||
being_shared: Arc<StdRwLock<BTreeMap<OwnedTransactionId, OutboundGroupSession>>>,
|
||||
) -> OlmResult<(Vec<Session>, Vec<(Device, WithheldCode)>)> {
|
||||
sessions: GroupSessionCache,
|
||||
) -> OlmResult<(Vec<Session>, Vec<(ReadOnlyDevice, WithheldCode)>)> {
|
||||
let (id, request, share_infos, used_sessions, no_olm) =
|
||||
Self::encrypt_session_for(outbound.clone(), chunk).await?;
|
||||
Self::encrypt_session_for(store, outbound.clone(), chunk).await?;
|
||||
|
||||
if !request.messages.is_empty() {
|
||||
trace!(
|
||||
@@ -444,7 +465,7 @@ impl GroupSessionManager {
|
||||
);
|
||||
|
||||
outbound.add_request(id.clone(), request.into(), share_infos);
|
||||
being_shared.write().unwrap().insert(id, outbound.clone());
|
||||
sessions.mark_as_being_shared(id, outbound.clone());
|
||||
}
|
||||
|
||||
Ok((used_sessions, no_olm))
|
||||
@@ -486,10 +507,10 @@ impl GroupSessionManager {
|
||||
|
||||
async fn encrypt_for_devices(
|
||||
&self,
|
||||
recipient_devices: Vec<Device>,
|
||||
recipient_devices: Vec<ReadOnlyDevice>,
|
||||
group_session: &OutboundGroupSession,
|
||||
changes: &mut Changes,
|
||||
) -> OlmResult<Vec<(Device, WithheldCode)>> {
|
||||
) -> OlmResult<Vec<(ReadOnlyDevice, WithheldCode)>> {
|
||||
// If we have some recipients, log them here.
|
||||
if !recipient_devices.is_empty() {
|
||||
#[allow(unknown_lints, clippy::unwrap_or_default)] // false positive
|
||||
@@ -507,7 +528,7 @@ impl GroupSessionManager {
|
||||
info!(
|
||||
?recipients,
|
||||
message_index,
|
||||
room_id = %group_session.room_id(),
|
||||
room_id = ?group_session.room_id(),
|
||||
session_id = group_session.session_id(),
|
||||
"Trying to encrypt a room key",
|
||||
);
|
||||
@@ -521,9 +542,10 @@ impl GroupSessionManager {
|
||||
.chunks(Self::MAX_TO_DEVICE_MESSAGES)
|
||||
.map(|chunk| {
|
||||
spawn(Self::encrypt_request(
|
||||
self.store.crypto_store(),
|
||||
chunk.to_vec(),
|
||||
group_session.clone(),
|
||||
self.sessions.sessions_being_shared.clone(),
|
||||
self.sessions.clone(),
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
@@ -549,7 +571,7 @@ impl GroupSessionManager {
|
||||
fn is_withheld_to(
|
||||
&self,
|
||||
group_session: &OutboundGroupSession,
|
||||
device: &Device,
|
||||
device: &ReadOnlyDevice,
|
||||
code: &WithheldCode,
|
||||
) -> bool {
|
||||
// The `m.no_olm` withheld code is special because it is supposed to be sent
|
||||
@@ -570,14 +592,7 @@ impl GroupSessionManager {
|
||||
// `OutboundGroupSession` and the `Device` both interact with the flag we'll
|
||||
// leave it be.
|
||||
if code == &WithheldCode::NoOlm {
|
||||
device.was_withheld_code_sent()
|
||||
|| self
|
||||
.sessions
|
||||
.sessions
|
||||
.read()
|
||||
.unwrap()
|
||||
.values()
|
||||
.any(|s| s.is_withheld_to(device, code))
|
||||
device.was_withheld_code_sent() || self.sessions.has_session_withheld_to(device, code)
|
||||
} else {
|
||||
group_session.is_withheld_to(device, code)
|
||||
}
|
||||
@@ -586,7 +601,7 @@ impl GroupSessionManager {
|
||||
async fn handle_withheld_devices(
|
||||
&self,
|
||||
group_session: &OutboundGroupSession,
|
||||
withheld_devices: Vec<(Device, WithheldCode)>,
|
||||
withheld_devices: Vec<(ReadOnlyDevice, WithheldCode)>,
|
||||
) -> OlmResult<()> {
|
||||
// Convert a withheld code for the group session into a to-device event content.
|
||||
let to_content = |code| {
|
||||
@@ -601,7 +616,7 @@ impl GroupSessionManager {
|
||||
let mut share_infos = BTreeMap::new();
|
||||
|
||||
for (device, code) in chunk {
|
||||
let device: Device = device;
|
||||
let device: ReadOnlyDevice = device;
|
||||
let code: WithheldCode = code;
|
||||
|
||||
let user_id = device.user_id().to_owned();
|
||||
@@ -640,13 +655,12 @@ impl GroupSessionManager {
|
||||
.map(chunk_to_request)
|
||||
.collect();
|
||||
|
||||
let mut sessions_being_shared = self.sessions.sessions_being_shared.write().unwrap();
|
||||
for (request, share_info) in result {
|
||||
if !request.messages.is_empty() {
|
||||
let txn_id = request.txn_id.to_owned();
|
||||
group_session.add_request(txn_id.to_owned(), request.into(), share_info);
|
||||
|
||||
sessions_being_shared.insert(txn_id, group_session.clone());
|
||||
self.sessions.mark_as_being_shared(txn_id, group_session.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -764,7 +778,7 @@ impl GroupSessionManager {
|
||||
|
||||
// Filter out the devices that already received this room key or have a
|
||||
// to-device message already queued up.
|
||||
let devices: Vec<Device> = devices
|
||||
let devices: Vec<_> = devices
|
||||
.into_iter()
|
||||
.flat_map(|(_, d)| {
|
||||
d.into_iter()
|
||||
@@ -794,10 +808,7 @@ impl GroupSessionManager {
|
||||
|
||||
if requests.is_empty() {
|
||||
if !outbound.shared() {
|
||||
debug!(
|
||||
room_id = room_id.as_str(),
|
||||
"The room key doesn't need to be shared with anyone. Marking as shared."
|
||||
);
|
||||
debug!("The room key doesn't need to be shared with anyone. Marking as shared.");
|
||||
|
||||
outbound.mark_as_shared();
|
||||
changes.outbound_group_sessions.push(outbound.clone());
|
||||
@@ -824,7 +835,7 @@ impl GroupSessionManager {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{collections::BTreeSet, ops::Deref, sync::Arc};
|
||||
use std::{collections::BTreeSet, iter, ops::Deref, sync::Arc};
|
||||
|
||||
use matrix_sdk_test::{async_test, response_from_file};
|
||||
use ruma::{
|
||||
@@ -862,12 +873,13 @@ mod tests {
|
||||
device_id!("JLAFKJWSCS")
|
||||
}
|
||||
|
||||
/// Returns a /keys/query response for user "@example:localhost"
|
||||
fn keys_query_response() -> get_keys::v3::Response {
|
||||
let data = include_bytes!("../../../../benchmarks/benches/crypto_bench/keys_query.json");
|
||||
let data: Value = serde_json::from_slice(data).unwrap();
|
||||
let data = response_from_file(&data);
|
||||
get_keys::v3::Response::try_from_http_response(data)
|
||||
.expect("Can't parse the keys upload response")
|
||||
.expect("Can't parse the `/keys/upload` response")
|
||||
}
|
||||
|
||||
fn bob_keys_query_response() -> get_keys::v3::Response {
|
||||
@@ -898,9 +910,11 @@ mod tests {
|
||||
let data = response_from_file(&data);
|
||||
|
||||
get_keys::v3::Response::try_from_http_response(data)
|
||||
.expect("Can't parse the keys upload response")
|
||||
.expect("Can't parse the `/keys/upload` response")
|
||||
}
|
||||
|
||||
/// Returns a keys claim response for device `BOBDEVICE` of user
|
||||
/// `@bob:localhost`.
|
||||
fn bob_one_time_key() -> claim_keys::v3::Response {
|
||||
let data = json!({
|
||||
"failures": {},
|
||||
@@ -925,6 +939,8 @@ mod tests {
|
||||
.expect("Can't parse the keys claim response")
|
||||
}
|
||||
|
||||
/// Returns a key claim response for device `NMMBNBUSNR` of user
|
||||
/// `@example2:localhost`
|
||||
fn keys_claim_response() -> claim_keys::v3::Response {
|
||||
let data = include_bytes!("../../../../benchmarks/benches/crypto_bench/keys_claim.json");
|
||||
let data: Value = serde_json::from_slice(data).unwrap();
|
||||
@@ -935,14 +951,27 @@ mod tests {
|
||||
|
||||
async fn machine_with_user_test_helper(user_id: &UserId, device_id: &DeviceId) -> OlmMachine {
|
||||
let keys_query = keys_query_response();
|
||||
let keys_claim = keys_claim_response();
|
||||
let txn_id = TransactionId::new();
|
||||
|
||||
let machine = OlmMachine::new(user_id, device_id).await;
|
||||
|
||||
// complete a /keys/query and /keys/claim for @example:localhost
|
||||
machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap();
|
||||
let (txn_id, _keys_claim_request) = machine
|
||||
.get_missing_sessions(iter::once(user_id!("@example:localhost")))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let keys_claim = keys_claim_response();
|
||||
machine.mark_request_as_sent(&txn_id, &keys_claim).await.unwrap();
|
||||
|
||||
// complete a /keys/query and /keys/claim for @bob:localhost
|
||||
machine.mark_request_as_sent(&txn_id, &bob_keys_query_response()).await.unwrap();
|
||||
let (txn_id, _keys_claim_request) = machine
|
||||
.get_missing_sessions(iter::once(user_id!("@bob:localhost")))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
machine.mark_request_as_sent(&txn_id, &bob_one_time_key()).await.unwrap();
|
||||
|
||||
machine
|
||||
@@ -1158,7 +1187,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn key_recipient_collecting() {
|
||||
async fn test_key_recipient_collecting() {
|
||||
// The user id comes from the fact that the keys_query.json file uses
|
||||
// this one.
|
||||
let user_id = user_id!("@example:localhost");
|
||||
|
||||
@@ -13,36 +13,47 @@
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
sync::{Arc, RwLock as StdRwLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use matrix_sdk_common::failures_cache::FailuresCache;
|
||||
use ruma::{
|
||||
api::client::keys::claim_keys::v3::{
|
||||
Request as KeysClaimRequest, Response as KeysClaimResponse,
|
||||
},
|
||||
assign,
|
||||
events::dummy::ToDeviceDummyEventContent,
|
||||
DeviceId, DeviceKeyAlgorithm, OwnedDeviceId, OwnedServerName, OwnedTransactionId, OwnedUserId,
|
||||
SecondsSinceUnixEpoch, ServerName, TransactionId, UserId,
|
||||
DeviceId, DeviceKeyAlgorithm, OwnedDeviceId, OwnedDeviceKeyId, OwnedServerName,
|
||||
OwnedTransactionId, OwnedUserId, SecondsSinceUnixEpoch, ServerName, TransactionId, UserId,
|
||||
};
|
||||
use tracing::{debug, error, info, warn};
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
use vodozemac::Curve25519PublicKey;
|
||||
|
||||
use crate::{
|
||||
error::OlmResult,
|
||||
gossiping::GossipMachine,
|
||||
requests::{OutgoingRequest, ToDeviceRequest},
|
||||
store::{Changes, Result as StoreResult, Store, UserKeyQueryResult},
|
||||
store::{Changes, Result as StoreResult, Store},
|
||||
types::{events::EventType, EventEncryptionAlgorithm},
|
||||
utilities::FailuresCache,
|
||||
ReadOnlyDevice,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct SessionManager {
|
||||
store: Store,
|
||||
|
||||
/// If there is an active /keys/claim request, its details.
|
||||
///
|
||||
/// This is used when processing the response, so that we can spot missing
|
||||
/// users/devices.
|
||||
///
|
||||
/// According to the doc on [`crate::OlmMachine::get_missing_sessions`],
|
||||
/// there should only be one such request active at a time, so we only need
|
||||
/// to keep a record of the most recent.
|
||||
current_key_claim_request: Arc<StdRwLock<Option<(OwnedTransactionId, KeysClaimRequest)>>>,
|
||||
|
||||
/// A map of user/devices that we need to automatically claim keys for.
|
||||
/// Submodules can insert user/device pairs into this map and the
|
||||
/// user/device paris will be added to the list of users when
|
||||
@@ -51,14 +62,19 @@ pub(crate) struct SessionManager {
|
||||
wedged_devices: Arc<StdRwLock<BTreeMap<OwnedUserId, BTreeSet<OwnedDeviceId>>>>,
|
||||
key_request_machine: GossipMachine,
|
||||
outgoing_to_device_requests: Arc<StdRwLock<BTreeMap<OwnedTransactionId, OutgoingRequest>>>,
|
||||
|
||||
/// Servers that have previously appeared in the `failures` section of a
|
||||
/// `/keys/claim` response.
|
||||
///
|
||||
/// See also [`crate::identities::IdentityManager::failures`].
|
||||
failures: FailuresCache<OwnedServerName>,
|
||||
|
||||
failed_devices: Arc<StdRwLock<BTreeMap<OwnedUserId, FailuresCache<OwnedDeviceId>>>>,
|
||||
}
|
||||
|
||||
impl SessionManager {
|
||||
const KEY_CLAIM_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const UNWEDGING_INTERVAL: Duration = Duration::from_secs(60 * 60);
|
||||
const KEYS_QUERY_WAIT_TIME: Duration = Duration::from_secs(5);
|
||||
|
||||
pub fn new(
|
||||
users_for_key_claim: Arc<StdRwLock<BTreeMap<OwnedUserId, BTreeSet<OwnedDeviceId>>>>,
|
||||
@@ -67,6 +83,7 @@ impl SessionManager {
|
||||
) -> Self {
|
||||
Self {
|
||||
store,
|
||||
current_key_claim_request: Default::default(),
|
||||
key_request_machine,
|
||||
users_for_key_claim,
|
||||
wedged_devices: Default::default(),
|
||||
@@ -93,7 +110,7 @@ impl SessionManager {
|
||||
let mut sessions = sessions.lock().await;
|
||||
sessions.sort_by_key(|s| s.creation_time);
|
||||
|
||||
let session = sessions.get(0);
|
||||
let session = sessions.first();
|
||||
|
||||
if let Some(session) = session {
|
||||
info!(sender_key = ?curve_key, "Marking session to be unwedged");
|
||||
@@ -148,8 +165,8 @@ impl SessionManager {
|
||||
.is_some_and(|d| d.remove(device_id))
|
||||
{
|
||||
if let Some(device) = self.store.get_device(user_id, device_id).await? {
|
||||
let content = serde_json::to_value(ToDeviceDummyEventContent::new())?;
|
||||
let (_, content) = device.encrypt("m.dummy", content).await?;
|
||||
let (_, content) =
|
||||
device.encrypt("m.dummy", ToDeviceDummyEventContent::new()).await?;
|
||||
|
||||
let request = ToDeviceRequest::new(
|
||||
device.user_id(),
|
||||
@@ -173,35 +190,6 @@ impl SessionManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_user_devices(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
) -> StoreResult<HashMap<OwnedDeviceId, ReadOnlyDevice>> {
|
||||
use UserKeyQueryResult::*;
|
||||
|
||||
let user_devices = self.store.get_readonly_devices_filtered(user_id).await?;
|
||||
|
||||
let user_devices = if user_devices.is_empty() {
|
||||
let cache = self.store.cache().await?;
|
||||
match self
|
||||
.key_request_machine
|
||||
.identity_manager()
|
||||
.key_query_manager
|
||||
.synced(&cache)
|
||||
.await?
|
||||
.wait_if_user_key_query_pending(Self::KEYS_QUERY_WAIT_TIME, user_id)
|
||||
.await
|
||||
{
|
||||
WasPending => self.store.get_readonly_devices_filtered(user_id).await?,
|
||||
_ => user_devices,
|
||||
}
|
||||
} else {
|
||||
user_devices
|
||||
};
|
||||
|
||||
Ok(user_devices)
|
||||
}
|
||||
|
||||
/// Get a key claiming request for the user/device pairs that we are
|
||||
/// missing Olm sessions for.
|
||||
///
|
||||
@@ -233,23 +221,35 @@ impl SessionManager {
|
||||
&self,
|
||||
users: impl Iterator<Item = &UserId>,
|
||||
) -> StoreResult<Option<(OwnedTransactionId, KeysClaimRequest)>> {
|
||||
let mut missing: BTreeMap<_, BTreeMap<_, _>> = BTreeMap::new();
|
||||
let mut timed_out: BTreeMap<_, BTreeSet<_>> = BTreeMap::new();
|
||||
let mut missing_session_devices_by_user: BTreeMap<_, BTreeMap<_, _>> = BTreeMap::new();
|
||||
let mut timed_out_devices_by_user: BTreeMap<_, BTreeSet<_>> = BTreeMap::new();
|
||||
|
||||
// Add the list of devices that the user wishes to establish sessions
|
||||
// right now.
|
||||
for user_id in users.filter(|u| !self.failures.contains(u.server_name())) {
|
||||
let user_devices = self.get_user_devices(user_id).await?;
|
||||
let unfailed_users = users.filter(|u| !self.failures.contains(u.server_name()));
|
||||
|
||||
// Get the current list of devices for each user.
|
||||
let devices_by_user = Box::pin(
|
||||
self.key_request_machine
|
||||
.identity_manager()
|
||||
.get_user_devices_for_encryption(unfailed_users),
|
||||
)
|
||||
.await?;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct UserFailedDeviceInfo {
|
||||
non_olm_devices: BTreeMap<OwnedDeviceId, Vec<EventEncryptionAlgorithm>>,
|
||||
bad_key_devices: BTreeSet<OwnedDeviceId>,
|
||||
}
|
||||
|
||||
let mut failed_devices_by_user: BTreeMap<_, UserFailedDeviceInfo> = BTreeMap::new();
|
||||
|
||||
for (user_id, user_devices) in devices_by_user {
|
||||
for (device_id, device) in user_devices {
|
||||
if !(device.supports_olm()) {
|
||||
warn!(
|
||||
user_id = device.user_id().as_str(),
|
||||
device_id = device.device_id().as_str(),
|
||||
algorithms = ?device.algorithms(),
|
||||
"Device doesn't support any of our 1-to-1 E2EE \
|
||||
algorithms, can't establish an Olm session"
|
||||
);
|
||||
if !device.supports_olm() {
|
||||
failed_devices_by_user
|
||||
.entry(user_id.clone())
|
||||
.or_default()
|
||||
.non_olm_devices
|
||||
.insert(device_id, Vec::from(device.algorithms()));
|
||||
} else if let Some(sender_key) = device.curve25519_key() {
|
||||
let sessions = self.store.get_sessions(&sender_key.to_base64()).await?;
|
||||
|
||||
@@ -259,23 +259,25 @@ impl SessionManager {
|
||||
true
|
||||
};
|
||||
|
||||
let is_timed_out = self.is_user_timed_out(user_id, &device_id);
|
||||
let is_timed_out = self.is_user_timed_out(&user_id, &device_id);
|
||||
|
||||
if is_missing && is_timed_out {
|
||||
timed_out.entry(user_id.to_owned()).or_default().insert(device_id);
|
||||
timed_out_devices_by_user
|
||||
.entry(user_id.to_owned())
|
||||
.or_default()
|
||||
.insert(device_id);
|
||||
} else if is_missing && !is_timed_out {
|
||||
missing
|
||||
missing_session_devices_by_user
|
||||
.entry(user_id.to_owned())
|
||||
.or_default()
|
||||
.insert(device_id, DeviceKeyAlgorithm::SignedCurve25519);
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
user_id = device.user_id().as_str(),
|
||||
device_id = device.device_id().as_str(),
|
||||
"Device doesn't have a valid Curve25519 key, \
|
||||
can't establish an Olm session"
|
||||
);
|
||||
failed_devices_by_user
|
||||
.entry(user_id.clone())
|
||||
.or_default()
|
||||
.bad_key_devices
|
||||
.insert(device_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -283,43 +285,167 @@ impl SessionManager {
|
||||
// Add the list of sessions that for some reason automatically need to
|
||||
// create an Olm session.
|
||||
for (user, device_ids) in self.users_for_key_claim.read().unwrap().iter() {
|
||||
for device_id in device_ids {
|
||||
missing
|
||||
.entry(user.to_owned())
|
||||
.or_default()
|
||||
.insert(device_id.to_owned(), DeviceKeyAlgorithm::SignedCurve25519);
|
||||
}
|
||||
missing_session_devices_by_user.entry(user.to_owned()).or_default().extend(
|
||||
device_ids
|
||||
.iter()
|
||||
.map(|device_id| (device_id.clone(), DeviceKeyAlgorithm::SignedCurve25519)),
|
||||
);
|
||||
}
|
||||
|
||||
if missing.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
if tracing::level_enabled!(tracing::Level::DEBUG) {
|
||||
// Reformat the map to skip the encryption algorithm, which isn't very useful.
|
||||
let missing_session_devices_by_user = missing_session_devices_by_user
|
||||
.iter()
|
||||
.map(|(user_id, devices)| (user_id, devices.keys().collect::<BTreeSet<_>>()))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
debug!(
|
||||
?missing,
|
||||
?timed_out,
|
||||
?missing_session_devices_by_user,
|
||||
?timed_out_devices_by_user,
|
||||
"Collected user/device pairs that are missing an Olm session"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Some((
|
||||
if !failed_devices_by_user.is_empty() {
|
||||
warn!(
|
||||
?failed_devices_by_user,
|
||||
"Can't establish an Olm session with some devices due to missing Olm support or bad keys",
|
||||
);
|
||||
}
|
||||
|
||||
let result = if missing_session_devices_by_user.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((
|
||||
TransactionId::new(),
|
||||
assign!(KeysClaimRequest::new(missing), {
|
||||
assign!(KeysClaimRequest::new(missing_session_devices_by_user), {
|
||||
timeout: Some(Self::KEY_CLAIM_TIMEOUT),
|
||||
}),
|
||||
)))
|
||||
}
|
||||
))
|
||||
};
|
||||
|
||||
// stash the details of the request so that we can refer to it when handling the
|
||||
// response
|
||||
*(self.current_key_claim_request.write().unwrap()) = result.clone();
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn is_user_timed_out(&self, user_id: &UserId, device_id: &DeviceId) -> bool {
|
||||
self.failed_devices.read().unwrap().get(user_id).is_some_and(|d| d.contains(device_id))
|
||||
}
|
||||
|
||||
/// This method will try to figure out for which devices a one-time key was
|
||||
/// requested but is not present in the response.
|
||||
///
|
||||
/// As per [spec], if a user/device pair does not have any one-time keys on
|
||||
/// the homeserver, the server will just omit the user/device pair from
|
||||
/// the response:
|
||||
///
|
||||
/// > If the homeserver could be reached, but the user or device was
|
||||
/// > unknown, no failure is recorded. Instead, the corresponding user
|
||||
/// > or device is missing from the one_time_keys result.
|
||||
///
|
||||
/// The user/device pairs which are missing from the response are going to
|
||||
/// be put in the failures cache so we don't retry to claim a one-time
|
||||
/// key right away next time the user tries to send a message.
|
||||
///
|
||||
/// [spec]: https://spec.matrix.org/unstable/client-server-api/#post_matrixclientv3keysclaim
|
||||
fn handle_otk_exhaustion_failure(
|
||||
&self,
|
||||
request_id: &TransactionId,
|
||||
failed_servers: &BTreeSet<OwnedServerName>,
|
||||
one_time_keys: &BTreeMap<
|
||||
&OwnedUserId,
|
||||
BTreeMap<&OwnedDeviceId, BTreeSet<&OwnedDeviceKeyId>>,
|
||||
>,
|
||||
) {
|
||||
// First check that the response is for the request we were expecting.
|
||||
let request = {
|
||||
let mut guard = self.current_key_claim_request.write().unwrap();
|
||||
let expected_request_id = guard.as_ref().map(|e| e.0.as_ref());
|
||||
|
||||
if Some(request_id) == expected_request_id {
|
||||
// We have a confirmed match. Clear the expectation, but hang onto the details
|
||||
// of the request.
|
||||
guard.take().map(|(_, request)| request)
|
||||
} else {
|
||||
warn!(
|
||||
?request_id,
|
||||
?expected_request_id,
|
||||
"Received a `/keys/claim` response for the wrong request"
|
||||
);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// If we were able to pair this response with a request, look for devices that
|
||||
// were present in the request but did not elicit a successful response.
|
||||
if let Some(request) = request {
|
||||
let devices_in_response: BTreeSet<_> = one_time_keys
|
||||
.iter()
|
||||
.flat_map(|(user_id, device_key_map)| {
|
||||
device_key_map
|
||||
.keys()
|
||||
.map(|device_id| (*user_id, *device_id))
|
||||
.collect::<BTreeSet<_>>()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let devices_in_request: BTreeSet<(_, _)> = request
|
||||
.one_time_keys
|
||||
.iter()
|
||||
.flat_map(|(user_id, device_key_map)| {
|
||||
device_key_map
|
||||
.keys()
|
||||
.map(|device_id| (user_id, device_id))
|
||||
.collect::<BTreeSet<_>>()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let missing_devices: BTreeSet<_> = devices_in_request
|
||||
.difference(&devices_in_response)
|
||||
.filter(|(user_id, _)| {
|
||||
// Skip over users whose homeservers were in the "failed servers" list: we don't
|
||||
// want to mark individual devices as broken *as well as* the server.
|
||||
!failed_servers.contains(user_id.server_name())
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !missing_devices.is_empty() {
|
||||
let mut missing_devices_by_user: BTreeMap<_, BTreeSet<_>> = BTreeMap::new();
|
||||
|
||||
for &(user_id, device_id) in missing_devices {
|
||||
missing_devices_by_user.entry(user_id).or_default().insert(device_id.clone());
|
||||
}
|
||||
|
||||
warn!(
|
||||
?missing_devices_by_user,
|
||||
"Tried to create new Olm sessions, but the signed one-time key was missing for some devices",
|
||||
);
|
||||
|
||||
let mut failed_devices_lock = self.failed_devices.write().unwrap();
|
||||
|
||||
for (user_id, device_set) in missing_devices_by_user {
|
||||
failed_devices_lock.entry(user_id.clone()).or_default().extend(device_set);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Receive a successful key claim response and create new Olm sessions with
|
||||
/// the claimed keys.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request_id` - The unique id of the request that was sent out. This is
|
||||
/// needed to couple the response with the sent out request.
|
||||
///
|
||||
/// * `response` - The response containing the claimed one-time keys.
|
||||
pub async fn receive_keys_claim_response(&self, response: &KeysClaimResponse) -> OlmResult<()> {
|
||||
#[instrument(skip(self, response))]
|
||||
pub async fn receive_keys_claim_response(
|
||||
&self,
|
||||
request_id: &TransactionId,
|
||||
response: &KeysClaimResponse,
|
||||
) -> OlmResult<()> {
|
||||
// Collect the (user_id, device_id, device_key_id) triple for logging reasons.
|
||||
let one_time_keys: BTreeMap<_, BTreeMap<_, BTreeSet<_>>> = response
|
||||
.one_time_keys
|
||||
@@ -337,24 +463,43 @@ impl SessionManager {
|
||||
})
|
||||
.collect();
|
||||
|
||||
debug!(?one_time_keys, failures = ?response.failures, "Received a `/keys/claim` response");
|
||||
debug!(?request_id, ?one_time_keys, failures = ?response.failures, "Received a `/keys/claim` response");
|
||||
|
||||
let failed_servers = response
|
||||
// Collect all the servers in the `failures` field of the response.
|
||||
let failed_servers: BTreeSet<_> = response
|
||||
.failures
|
||||
.keys()
|
||||
.filter_map(|s| ServerName::parse(s).ok())
|
||||
.filter(|s| s != self.store.static_account().user_id.server_name());
|
||||
.filter(|s| s != self.store.static_account().user_id.server_name())
|
||||
.collect();
|
||||
let successful_servers = response.one_time_keys.keys().map(|u| u.server_name());
|
||||
|
||||
// Add the user/device pairs that don't have any one-time keys to the failures
|
||||
// cache.
|
||||
self.handle_otk_exhaustion_failure(request_id, &failed_servers, &one_time_keys);
|
||||
// Add the failed servers to the failures cache.
|
||||
self.failures.extend(failed_servers);
|
||||
// Remove the servers we successfully contacted from the failures cache.
|
||||
self.failures.remove(successful_servers);
|
||||
|
||||
// Finally, create some 1-to-1 sessions.
|
||||
self.create_sessions(response).await
|
||||
}
|
||||
|
||||
/// Create new Olm sessions for the requested devices.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `device_map` - a map from (user ID, device ID) pairs to key object,
|
||||
/// for each device we should create a session for.
|
||||
pub(crate) async fn create_sessions(&self, response: &KeysClaimResponse) -> OlmResult<()> {
|
||||
struct SessionInfo {
|
||||
session_id: String,
|
||||
algorithm: EventEncryptionAlgorithm,
|
||||
fallback_key_used: bool,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl std::fmt::Debug for SessionInfo {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
@@ -367,26 +512,23 @@ impl SessionManager {
|
||||
|
||||
let mut changes = Changes::default();
|
||||
let mut new_sessions: BTreeMap<&UserId, BTreeMap<&DeviceId, SessionInfo>> = BTreeMap::new();
|
||||
|
||||
let mut store_transaction = self.store.transaction().await;
|
||||
|
||||
for (user_id, user_devices) in &response.one_time_keys {
|
||||
for (device_id, key_map) in user_devices {
|
||||
let device = match self.store.get_readonly_device(user_id, device_id).await {
|
||||
Ok(Some(d)) => d,
|
||||
Ok(None) => {
|
||||
warn!(
|
||||
user_id = user_id.as_str(),
|
||||
device_id = device_id.as_str(),
|
||||
"Tried to create an Olm session but the device is \
|
||||
unknown",
|
||||
?user_id,
|
||||
?device_id,
|
||||
"Tried to create an Olm session but the device is unknown",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
user_id = user_id.as_str(),
|
||||
device_id = device_id.as_str(),
|
||||
error = ?e,
|
||||
?user_id, ?device_id, error = ?e,
|
||||
"Tried to create an Olm session, but we can't \
|
||||
fetch the device from the store",
|
||||
);
|
||||
@@ -399,10 +541,8 @@ impl SessionManager {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
user_id = user_id.as_str(),
|
||||
device_id = device_id.as_str(),
|
||||
error = ?e,
|
||||
"Error creating outbound session"
|
||||
?user_id, ?device_id, error = ?e,
|
||||
"Error creating Olm session"
|
||||
);
|
||||
|
||||
self.failed_devices
|
||||
@@ -467,6 +607,7 @@ mod tests {
|
||||
iter,
|
||||
ops::Deref,
|
||||
sync::{Arc, RwLock as StdRwLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use matrix_sdk_test::{async_test, response_from_file};
|
||||
@@ -478,7 +619,7 @@ mod tests {
|
||||
},
|
||||
IncomingResponse,
|
||||
},
|
||||
device_id, user_id, DeviceId, UserId,
|
||||
device_id, owned_server_name, user_id, DeviceId, OwnedUserId, UserId,
|
||||
};
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -572,7 +713,7 @@ mod tests {
|
||||
|
||||
manager.store.save_devices(&[bob_device]).await.unwrap();
|
||||
|
||||
let (_, request) =
|
||||
let (txn_id, request) =
|
||||
manager.get_missing_sessions(iter::once(bob.user_id())).await.unwrap().unwrap();
|
||||
|
||||
assert!(request.one_time_keys.contains_key(bob.user_id()));
|
||||
@@ -590,7 +731,7 @@ mod tests {
|
||||
|
||||
let response = KeyClaimResponse::new(one_time_keys);
|
||||
|
||||
manager.receive_keys_claim_response(&response).await.unwrap();
|
||||
manager.receive_keys_claim_response(&txn_id, &response).await.unwrap();
|
||||
|
||||
assert!(manager.get_missing_sessions(iter::once(bob.user_id())).await.unwrap().is_none());
|
||||
}
|
||||
@@ -599,8 +740,8 @@ mod tests {
|
||||
async fn test_session_creation_waits_for_keys_query() {
|
||||
let (manager, identity_manager) = session_manager_test_helper().await;
|
||||
|
||||
// start a keys query request. At this point, we are only interested in our own
|
||||
// devices.
|
||||
// start a `/keys/query` request. At this point, we are only interested in our
|
||||
// own devices.
|
||||
let (key_query_txn_id, key_query_request) =
|
||||
identity_manager.users_for_key_query().await.unwrap().pop_first().unwrap();
|
||||
info!("Initial key query: {:?}", key_query_request);
|
||||
@@ -632,7 +773,7 @@ mod tests {
|
||||
})
|
||||
};
|
||||
|
||||
// the initial keys query completes, and we start another
|
||||
// the initial `/keys/query` completes, and we start another
|
||||
let response_json =
|
||||
json!({ "device_keys": { manager.store.static_account().user_id.to_owned(): {}}});
|
||||
let response =
|
||||
@@ -659,6 +800,45 @@ mod tests {
|
||||
assert!(bob_key_claims.contains_key(bob_device.device_id()));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_session_creation_does_not_wait_for_keys_query_on_failed_server() {
|
||||
let (manager, identity_manager) = session_manager_test_helper().await;
|
||||
|
||||
// We start tracking Bob's devices.
|
||||
let other_user_id = OwnedUserId::try_from("@bob:example.com").unwrap();
|
||||
{
|
||||
let cache = manager.store.cache().await.unwrap();
|
||||
identity_manager
|
||||
.key_query_manager
|
||||
.synced(&cache)
|
||||
.await
|
||||
.unwrap()
|
||||
.update_tracked_users(iter::once(other_user_id.as_ref()))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Do a `/keys/query` request, in which Bob's server is a failure.
|
||||
let (key_query_txn_id, _key_query_request) =
|
||||
identity_manager.users_for_key_query().await.unwrap().pop_first().unwrap();
|
||||
let response = KeysQueryResponse::try_from_http_response(response_from_file(
|
||||
&json!({ "device_keys": {}, "failures": { other_user_id.server_name(): "unreachable" }})
|
||||
)).unwrap();
|
||||
identity_manager.receive_keys_query_response(&key_query_txn_id, &response).await.unwrap();
|
||||
|
||||
// Now, an attempt to get the missing sessions should now *not* block. We use a
|
||||
// timeout so that we can detect the call blocking.
|
||||
let result = tokio::time::timeout(
|
||||
Duration::from_millis(10),
|
||||
manager.get_missing_sessions(iter::once(other_user_id.as_ref())),
|
||||
)
|
||||
.await
|
||||
.expect("get_missing_sessions blocked rather than completing quickly")
|
||||
.expect("get_missing_sessions returned an error");
|
||||
|
||||
assert!(result.is_none(), "get_missing_sessions returned Some(...)");
|
||||
}
|
||||
|
||||
// This test doesn't run on macos because we're modifying the session
|
||||
// creation time so we can get around the UNWEDGING_INTERVAL.
|
||||
#[async_test]
|
||||
@@ -697,7 +877,7 @@ mod tests {
|
||||
assert!(manager.is_device_wedged(&bob_device));
|
||||
assert!(manager.users_for_key_claim.read().unwrap().contains_key(bob.user_id()));
|
||||
|
||||
let (_, request) =
|
||||
let (txn_id, request) =
|
||||
manager.get_missing_sessions(iter::once(bob.user_id())).await.unwrap().unwrap();
|
||||
|
||||
assert!(request.one_time_keys.contains_key(bob.user_id()));
|
||||
@@ -717,7 +897,7 @@ mod tests {
|
||||
|
||||
assert!(manager.outgoing_to_device_requests.read().unwrap().is_empty());
|
||||
|
||||
manager.receive_keys_claim_response(&response).await.unwrap();
|
||||
manager.receive_keys_claim_response(&txn_id, &response).await.unwrap();
|
||||
|
||||
assert!(!manager.is_device_wedged(&bob_device));
|
||||
assert!(manager.get_missing_sessions(iter::once(bob.user_id())).await.unwrap().is_none());
|
||||
@@ -734,20 +914,54 @@ mod tests {
|
||||
|
||||
manager.store.save_devices(&[alice_device]).await.unwrap();
|
||||
|
||||
let (_, users_for_key_claim) =
|
||||
let (txn_id, users_for_key_claim) =
|
||||
manager.get_missing_sessions(iter::once(alice)).await.unwrap().unwrap();
|
||||
assert!(users_for_key_claim.one_time_keys.contains_key(alice));
|
||||
|
||||
manager.receive_keys_claim_response(&keys_claim_with_failure()).await.unwrap();
|
||||
manager.receive_keys_claim_response(&txn_id, &keys_claim_with_failure()).await.unwrap();
|
||||
assert!(manager.get_missing_sessions(iter::once(alice)).await.unwrap().is_none());
|
||||
|
||||
manager.receive_keys_claim_response(&keys_claim_without_failure()).await.unwrap();
|
||||
// expire the failure
|
||||
manager.failures.expire(&owned_server_name!("example.org"));
|
||||
|
||||
let (txn_id, users_for_key_claim) =
|
||||
manager.get_missing_sessions(iter::once(alice)).await.unwrap().unwrap();
|
||||
assert!(users_for_key_claim.one_time_keys.contains_key(alice));
|
||||
|
||||
manager.receive_keys_claim_response(&txn_id, &keys_claim_without_failure()).await.unwrap();
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn failed_devices_handling() {
|
||||
let response_with_invalid_signature = json!({
|
||||
// Alice is missing altogether
|
||||
test_invalid_claim_response(json!({
|
||||
"one_time_keys": {},
|
||||
"failures": {},
|
||||
}))
|
||||
.await;
|
||||
|
||||
// Alice is present but with no devices
|
||||
test_invalid_claim_response(json!({
|
||||
"one_time_keys": {
|
||||
"@alice:example.org": {}
|
||||
},
|
||||
"failures": {},
|
||||
}))
|
||||
.await;
|
||||
|
||||
// Alice's device is present but with no keys
|
||||
test_invalid_claim_response(json!({
|
||||
"one_time_keys": {
|
||||
"@alice:example.org": {
|
||||
"DEVICEID": {}
|
||||
}
|
||||
},
|
||||
"failures": {},
|
||||
}))
|
||||
.await;
|
||||
|
||||
// Alice's device is present with a bad signature
|
||||
test_invalid_claim_response(json!({
|
||||
"one_time_keys": {
|
||||
"@alice:example.org": {
|
||||
"DEVICEID": {
|
||||
@@ -764,9 +978,16 @@ mod tests {
|
||||
}
|
||||
},
|
||||
"failures": {},
|
||||
});
|
||||
})).await;
|
||||
}
|
||||
|
||||
let response = response_from_file(&response_with_invalid_signature);
|
||||
/// Helper for failed_devices_handling.
|
||||
///
|
||||
/// Takes an invalid /keys/claim response for Alice's device DEVICEID and
|
||||
/// checks that it is handled correctly. (The device should be marked as
|
||||
/// 'failed'; and once that
|
||||
async fn test_invalid_claim_response(response_json: serde_json::Value) {
|
||||
let response = response_from_file(&response_json);
|
||||
let response = KeyClaimResponse::try_from_http_response(response).unwrap();
|
||||
|
||||
let alice = user_id!("@alice:example.org");
|
||||
@@ -778,13 +999,13 @@ mod tests {
|
||||
|
||||
// Since we don't have a session with Alice yet, the machine will try to claim
|
||||
// some keys for alice.
|
||||
let (_, users_for_key_claim) =
|
||||
let (txn_id, users_for_key_claim) =
|
||||
manager.get_missing_sessions(iter::once(alice)).await.unwrap().unwrap();
|
||||
assert!(users_for_key_claim.one_time_keys.contains_key(alice));
|
||||
|
||||
// We receive a response with an invalid one-time key, this will mark Alice as
|
||||
// timed out.
|
||||
manager.receive_keys_claim_response(&response).await.unwrap();
|
||||
manager.receive_keys_claim_response(&txn_id, &response).await.unwrap();
|
||||
// Since alice is timed out, we won't claim keys for her.
|
||||
assert!(manager.get_missing_sessions(iter::once(alice)).await.unwrap().is_none());
|
||||
|
||||
@@ -798,17 +1019,29 @@ mod tests {
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(alice_account.device_id().to_owned(), one_time);
|
||||
|
||||
// Now we receive a valid one-time key from Alice.
|
||||
let response = KeyClaimResponse::new(one_time_keys);
|
||||
manager.receive_keys_claim_response(&response).await.unwrap();
|
||||
|
||||
// Alice isn't timed out anymore.
|
||||
assert!(!manager
|
||||
// Now we expire Alice's timeout, and receive a valid one-time key for her.
|
||||
manager
|
||||
.failed_devices
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry(alice.to_owned())
|
||||
.or_default()
|
||||
.contains(alice_account.device_id()));
|
||||
.get(alice)
|
||||
.unwrap()
|
||||
.expire(&alice_account.device_id().to_owned());
|
||||
let (txn_id, users_for_key_claim) =
|
||||
manager.get_missing_sessions(iter::once(alice)).await.unwrap().unwrap();
|
||||
assert!(users_for_key_claim.one_time_keys.contains_key(alice));
|
||||
|
||||
let response = KeyClaimResponse::new(one_time_keys);
|
||||
manager.receive_keys_claim_response(&txn_id, &response).await.unwrap();
|
||||
|
||||
// Alice isn't timed out anymore.
|
||||
assert!(manager
|
||||
.failed_devices
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(alice)
|
||||
.unwrap()
|
||||
.failure_count(alice_account.device_id())
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,22 +359,19 @@ impl UsersForKeyQuery {
|
||||
&mut self,
|
||||
user: &UserId,
|
||||
) -> Option<Arc<KeysQueryWaiter>> {
|
||||
match self.user_map.get(user) {
|
||||
None => None,
|
||||
Some(&sequence_number) => {
|
||||
trace!(?user, %sequence_number, "Registering new waiting task");
|
||||
self.user_map.get(user).map(|&sequence_number| {
|
||||
trace!(?user, %sequence_number, "Registering new waiting task");
|
||||
|
||||
let waiter = Arc::new(KeysQueryWaiter {
|
||||
sequence_number,
|
||||
user: user.to_owned(),
|
||||
completed: AtomicBool::new(false),
|
||||
});
|
||||
let waiter = Arc::new(KeysQueryWaiter {
|
||||
sequence_number,
|
||||
user: user.to_owned(),
|
||||
completed: AtomicBool::new(false),
|
||||
});
|
||||
|
||||
self.tasks_awaiting_key_query.push(Arc::downgrade(&waiter));
|
||||
self.tasks_awaiting_key_query.push(Arc::downgrade(&waiter));
|
||||
|
||||
Some(waiter)
|
||||
}
|
||||
}
|
||||
waiter
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ macro_rules! cryptostore_integration_tests {
|
||||
room_id,
|
||||
serde::{Base64, Raw},
|
||||
to_device::DeviceIdOrAllDevices,
|
||||
user_id, DeviceId, JsOption, OwnedDeviceId, OwnedUserId, TransactionId, UserId,
|
||||
user_id, DeviceId, JsOption, OwnedDeviceId, OwnedUserId, RoomId, TransactionId, UserId
|
||||
};
|
||||
use serde_json::value::to_raw_value;
|
||||
use $crate::{
|
||||
@@ -292,6 +292,54 @@ macro_rules! cryptostore_integration_tests {
|
||||
assert_eq!(to_back_up, vec![session])
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn mark_inbound_group_sessions_as_backed_up() {
|
||||
// Given a store exists with multiple unbacked-up sessions
|
||||
let (account, store) =
|
||||
get_loaded_store("mark_inbound_group_sessions_as_backed_up").await;
|
||||
let room_id = &room_id!("!test:localhost");
|
||||
let mut sessions: Vec<InboundGroupSession> = Vec::with_capacity(10);
|
||||
for i in 0..10 {
|
||||
sessions.push(account.create_group_session_pair_with_defaults(room_id).await.1);
|
||||
}
|
||||
let changes = Changes { inbound_group_sessions: sessions.clone(), ..Default::default() };
|
||||
store.save_changes(changes).await.expect("Can't save group session");
|
||||
assert_eq!(store.inbound_group_sessions_for_backup(100).await.unwrap().len(), 10);
|
||||
|
||||
fn session_info(session: &InboundGroupSession) -> (&RoomId, &str) {
|
||||
(&session.room_id(), &session.session_id())
|
||||
}
|
||||
|
||||
// When I mark some as backed up
|
||||
let x = store.mark_inbound_group_sessions_as_backed_up(&[
|
||||
session_info(&sessions[1]),
|
||||
session_info(&sessions[3]),
|
||||
session_info(&sessions[5]),
|
||||
session_info(&sessions[7]),
|
||||
session_info(&sessions[9]),
|
||||
]).await.expect("Failed to mark sessions as backed up");
|
||||
|
||||
|
||||
// And ask which still need backing up
|
||||
let to_back_up = store.inbound_group_sessions_for_backup(10).await.unwrap();
|
||||
let needs_backing_up = |i: usize| to_back_up.iter().any(|s| s.session_id() == sessions[i].session_id());
|
||||
|
||||
// Then the sessions we said were backed up no longer need backing up
|
||||
assert!(!needs_backing_up(1));
|
||||
assert!(!needs_backing_up(3));
|
||||
assert!(!needs_backing_up(5));
|
||||
assert!(!needs_backing_up(7));
|
||||
assert!(!needs_backing_up(9));
|
||||
|
||||
// And the sessions we didn't mention still need backing up
|
||||
assert!(needs_backing_up(0));
|
||||
assert!(needs_backing_up(2));
|
||||
assert!(needs_backing_up(4));
|
||||
assert!(needs_backing_up(6));
|
||||
assert!(needs_backing_up(8));
|
||||
assert_eq!(to_back_up.len(), 5);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn reset_inbound_group_session_for_backup() {
|
||||
let (account, store) =
|
||||
|
||||
@@ -265,6 +265,20 @@ impl CryptoStore for MemoryStore {
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn mark_inbound_group_sessions_as_backed_up(
|
||||
&self,
|
||||
room_and_session_ids: &[(&RoomId, &str)],
|
||||
) -> Result<()> {
|
||||
for (room_id, session_id) in room_and_session_ids {
|
||||
let session = self.inbound_group_sessions.get(room_id, session_id);
|
||||
if let Some(session) = session {
|
||||
session.mark_as_backed_up();
|
||||
self.inbound_group_sessions.add(session);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn reset_backup_state(&self) -> Result<()> {
|
||||
for session in self.get_inbound_group_sessions().await? {
|
||||
session.reset_backup_state();
|
||||
|
||||
@@ -42,12 +42,12 @@ use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
|
||||
fmt::Debug,
|
||||
ops::Deref,
|
||||
pin::pin,
|
||||
sync::{atomic::Ordering, Arc, RwLock as StdRwLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use as_variant::as_variant;
|
||||
use async_std::sync::{Condvar, Mutex as AsyncStdMutex};
|
||||
use futures_core::Stream;
|
||||
use futures_util::StreamExt;
|
||||
use ruma::{
|
||||
@@ -55,7 +55,7 @@ use ruma::{
|
||||
};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use tokio::sync::{Mutex, MutexGuard, OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock};
|
||||
use tokio::sync::{Mutex, MutexGuard, Notify, OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock};
|
||||
use tracing::{info, warn};
|
||||
use vodozemac::{base64_encode, megolm::SessionOrdering, Curve25519PublicKey};
|
||||
use zeroize::Zeroize;
|
||||
@@ -66,12 +66,12 @@ use crate::{
|
||||
user::UserIdentities, Device, ReadOnlyDevice, ReadOnlyUserIdentities, UserDevices,
|
||||
},
|
||||
olm::{
|
||||
Account, InboundGroupSession, OlmMessageHash, OutboundGroupSession,
|
||||
Account, ExportedRoomKey, InboundGroupSession, OlmMessageHash, OutboundGroupSession,
|
||||
PrivateCrossSigningIdentity, Session, StaticAccountData,
|
||||
},
|
||||
types::{events::room_key_withheld::RoomKeyWithheldEvent, EventEncryptionAlgorithm},
|
||||
verification::VerificationMachine,
|
||||
CrossSigningStatus, ReadOnlyOwnUserIdentity,
|
||||
CrossSigningStatus, ReadOnlyOwnUserIdentity, RoomKeyImportResult,
|
||||
};
|
||||
|
||||
pub mod caches;
|
||||
@@ -108,14 +108,10 @@ pub struct Store {
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct KeyQueryManager {
|
||||
/// Record of the users that are waiting for a /keys/query.
|
||||
//
|
||||
// This uses an async_std::sync::Mutex rather than a
|
||||
// matrix_sdk_common::locks::Mutex because it has to match the Condvar (and tokio lacks a
|
||||
// working Condvar implementation)
|
||||
users_for_key_query: AsyncStdMutex<UsersForKeyQuery>,
|
||||
users_for_key_query: Mutex<UsersForKeyQuery>,
|
||||
|
||||
// condition variable that is notified each time an update is received for a user.
|
||||
users_for_key_query_condvar: Condvar,
|
||||
/// Notifier that is triggered each time an update is received for a user.
|
||||
users_for_key_query_notify: Notify,
|
||||
}
|
||||
|
||||
impl KeyQueryManager {
|
||||
@@ -164,6 +160,66 @@ impl KeyQueryManager {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Wait for a `/keys/query` response to be received if one is expected for
|
||||
/// the given user.
|
||||
///
|
||||
/// If the given timeout elapses, the method will stop waiting and return
|
||||
/// `UserKeyQueryResult::TimeoutExpired`.
|
||||
///
|
||||
/// Requires a [`StoreCacheGuard`] to make sure the users for which a key
|
||||
/// query is pending are up to date, but doesn't hold on to it
|
||||
/// thereafter: the lock is short-lived in this case.
|
||||
pub async fn wait_if_user_key_query_pending(
|
||||
&self,
|
||||
cache: StoreCacheGuard,
|
||||
timeout_duration: Duration,
|
||||
user: &UserId,
|
||||
) -> Result<UserKeyQueryResult> {
|
||||
{
|
||||
// Drop the cache early, so we don't keep it while waiting (since writing the
|
||||
// results requires to write in the cache, thus take another lock).
|
||||
self.ensure_sync_tracked_users(&cache).await?;
|
||||
drop(cache);
|
||||
}
|
||||
|
||||
let mut users_for_key_query = self.users_for_key_query.lock().await;
|
||||
let Some(waiter) = users_for_key_query.maybe_register_waiting_task(user) else {
|
||||
return Ok(UserKeyQueryResult::WasNotPending);
|
||||
};
|
||||
|
||||
let wait_for_completion = async {
|
||||
while !waiter.completed.load(Ordering::Relaxed) {
|
||||
// Register for being notified before releasing the mutex, so
|
||||
// it's impossible to miss a wakeup between the last check for
|
||||
// whether we should wait, and starting to wait.
|
||||
let mut notified = pin!(self.users_for_key_query_notify.notified());
|
||||
notified.as_mut().enable();
|
||||
drop(users_for_key_query);
|
||||
|
||||
// Wait for a notification
|
||||
notified.await;
|
||||
|
||||
// Reclaim the lock before checking the flag to avoid races
|
||||
// when two notifications happen right after each other and the
|
||||
// second one sets the flag we want to wait for.
|
||||
users_for_key_query = self.users_for_key_query.lock().await;
|
||||
}
|
||||
};
|
||||
|
||||
match timeout(Box::pin(wait_for_completion), timeout_duration).await {
|
||||
Err(_) => {
|
||||
warn!(
|
||||
user_id = ?user,
|
||||
"The user has a pending `/key/query` request which did \
|
||||
not finish yet, some devices might be missing."
|
||||
);
|
||||
|
||||
Ok(UserKeyQueryResult::TimeoutExpired)
|
||||
}
|
||||
_ => Ok(UserKeyQueryResult::WasPending),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SyncedKeyQueryManager<'a> {
|
||||
@@ -193,43 +249,6 @@ impl<'a> SyncedKeyQueryManager<'a> {
|
||||
self.cache.store.save_tracked_users(&store_updates).await
|
||||
}
|
||||
|
||||
/// Wait for a `/keys/query` response to be received if one is expected for
|
||||
/// the given user.
|
||||
///
|
||||
/// If the given timeout elapses, the method will stop waiting and return
|
||||
/// `UserKeyQueryResult::TimeoutExpired`
|
||||
pub async fn wait_if_user_key_query_pending(
|
||||
&self,
|
||||
timeout_duration: Duration,
|
||||
user: &UserId,
|
||||
) -> UserKeyQueryResult {
|
||||
let mut users_for_key_query = self.manager.users_for_key_query.lock().await;
|
||||
|
||||
let Some(waiter) = users_for_key_query.maybe_register_waiting_task(user) else {
|
||||
return UserKeyQueryResult::WasNotPending;
|
||||
};
|
||||
|
||||
let wait_for_completion = async {
|
||||
while !waiter.completed.load(Ordering::Relaxed) {
|
||||
users_for_key_query =
|
||||
self.manager.users_for_key_query_condvar.wait(users_for_key_query).await;
|
||||
}
|
||||
};
|
||||
|
||||
match timeout(Box::pin(wait_for_completion), timeout_duration).await {
|
||||
Err(_) => {
|
||||
warn!(
|
||||
user_id = ?user,
|
||||
"The user has a pending `/key/query` request which did \
|
||||
not finish yet, some devices might be missing."
|
||||
);
|
||||
|
||||
UserKeyQueryResult::TimeoutExpired
|
||||
}
|
||||
_ => UserKeyQueryResult::WasPending,
|
||||
}
|
||||
}
|
||||
|
||||
/// Process notifications that users have changed devices.
|
||||
///
|
||||
/// This is used to handle the list of device-list updates that is received
|
||||
@@ -281,7 +300,7 @@ impl<'a> SyncedKeyQueryManager<'a> {
|
||||
|
||||
self.cache.store.save_tracked_users(&store_updates).await?;
|
||||
// wake up any tasks that may have been waiting for updates
|
||||
self.manager.users_for_key_query_condvar.notify_all();
|
||||
self.manager.users_for_key_query_notify.notify_waiters();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1048,18 +1067,6 @@ impl Store {
|
||||
.map(|d| d.devices().find(|d| d.curve25519_key() == Some(curve_key)))
|
||||
}
|
||||
|
||||
/// Get all devices associated with the given `user_id`
|
||||
///
|
||||
/// *Note*: This doesn't return our own device.
|
||||
pub(crate) async fn get_user_devices_filtered(&self, user_id: &UserId) -> Result<UserDevices> {
|
||||
self.get_user_devices(user_id).await.map(|mut d| {
|
||||
if user_id == self.user_id() {
|
||||
d.inner.remove(self.device_id());
|
||||
}
|
||||
d
|
||||
})
|
||||
}
|
||||
|
||||
/// Get all devices associated with the given `user_id`
|
||||
///
|
||||
/// *Note*: This does also return our own device.
|
||||
@@ -1142,16 +1149,12 @@ impl Store {
|
||||
self.inner.identity.lock().await.export_secret(secret_name).await
|
||||
}
|
||||
SecretName::RecoveryKey => {
|
||||
#[cfg(feature = "backups_v1")]
|
||||
if let Some(key) = self.load_backup_keys().await?.decryption_key {
|
||||
let exported = key.to_base64();
|
||||
Some(exported)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "backups_v1"))]
|
||||
None
|
||||
}
|
||||
name => {
|
||||
warn!(secret = ?name, "Unknown secret was requested");
|
||||
@@ -1470,6 +1473,116 @@ impl Store {
|
||||
pub fn secrets_stream(&self) -> impl Stream<Item = GossippedSecret> {
|
||||
self.inner.store.secrets_stream()
|
||||
}
|
||||
|
||||
pub(crate) async fn import_room_keys(
|
||||
&self,
|
||||
exported_keys: Vec<ExportedRoomKey>,
|
||||
from_backup: bool,
|
||||
progress_listener: impl Fn(usize, usize),
|
||||
) -> Result<RoomKeyImportResult> {
|
||||
let mut sessions = Vec::new();
|
||||
|
||||
async fn new_session_better(
|
||||
session: &InboundGroupSession,
|
||||
old_session: Option<InboundGroupSession>,
|
||||
) -> bool {
|
||||
if let Some(old_session) = &old_session {
|
||||
session.compare(old_session).await == SessionOrdering::Better
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
let total_count = exported_keys.len();
|
||||
let mut keys = BTreeMap::new();
|
||||
|
||||
for (i, key) in exported_keys.into_iter().enumerate() {
|
||||
match InboundGroupSession::from_export(&key) {
|
||||
Ok(session) => {
|
||||
let old_session = self
|
||||
.inner
|
||||
.store
|
||||
.get_inbound_group_session(session.room_id(), session.session_id())
|
||||
.await?;
|
||||
|
||||
// Only import the session if we didn't have this session or
|
||||
// if it's a better version of the same session.
|
||||
if new_session_better(&session, old_session).await {
|
||||
if from_backup {
|
||||
session.mark_as_backed_up();
|
||||
}
|
||||
|
||||
keys.entry(session.room_id().to_owned())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.entry(session.sender_key().to_base64())
|
||||
.or_insert_with(BTreeSet::new)
|
||||
.insert(session.session_id().to_owned());
|
||||
|
||||
sessions.push(session);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
sender_key= key.sender_key.to_base64(),
|
||||
room_id = ?key.room_id,
|
||||
session_id = key.session_id,
|
||||
error = ?e,
|
||||
"Couldn't import a room key from a file export."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
progress_listener(i, total_count);
|
||||
}
|
||||
|
||||
let imported_count = sessions.len();
|
||||
|
||||
let changes = Changes { inbound_group_sessions: sessions, ..Default::default() };
|
||||
|
||||
self.save_changes(changes).await?;
|
||||
|
||||
info!(total_count, imported_count, room_keys = ?keys, "Successfully imported room keys");
|
||||
|
||||
Ok(RoomKeyImportResult::new(imported_count, total_count, keys))
|
||||
}
|
||||
|
||||
/// Import the given room keys into our store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `exported_keys` - A list of previously exported keys that should be
|
||||
/// imported into our store. If we already have a better version of a key
|
||||
/// the key will *not* be imported.
|
||||
///
|
||||
/// Returns a tuple of numbers that represent the number of sessions that
|
||||
/// were imported and the total number of sessions that were found in the
|
||||
/// key export.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use std::io::Cursor;
|
||||
/// # use matrix_sdk_crypto::{OlmMachine, decrypt_room_key_export};
|
||||
/// # use ruma::{device_id, user_id};
|
||||
/// # let alice = user_id!("@alice:example.org");
|
||||
/// # async {
|
||||
/// # let machine = OlmMachine::new(&alice, device_id!("DEVICEID")).await;
|
||||
/// # let export = Cursor::new("".to_owned());
|
||||
/// let exported_keys = decrypt_room_key_export(export, "1234").unwrap();
|
||||
/// machine.import_room_keys(exported_keys, false, |_, _| {}).await.unwrap();
|
||||
/// # };
|
||||
/// ```
|
||||
pub async fn import_exported_room_keys(
|
||||
&self,
|
||||
exported_keys: Vec<ExportedRoomKey>,
|
||||
progress_listener: impl Fn(usize, usize),
|
||||
) -> Result<RoomKeyImportResult> {
|
||||
self.import_room_keys(exported_keys, false, progress_listener).await
|
||||
}
|
||||
|
||||
pub(crate) fn crypto_store(&self) -> Arc<CryptoStoreWrapper> {
|
||||
self.inner.store.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Store {
|
||||
|
||||
@@ -113,6 +113,13 @@ pub trait CryptoStore: AsyncTraitDeps {
|
||||
limit: usize,
|
||||
) -> Result<Vec<InboundGroupSession>, Self::Error>;
|
||||
|
||||
/// Mark the inbound group sessions with the supplied room and session IDs
|
||||
/// as backed up
|
||||
async fn mark_inbound_group_sessions_as_backed_up(
|
||||
&self,
|
||||
room_and_session_ids: &[(&RoomId, &str)],
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Reset the backup state of all the stored inbound group sessions.
|
||||
async fn reset_backup_state(&self) -> Result<(), Self::Error>;
|
||||
|
||||
@@ -277,6 +284,7 @@ pub trait CryptoStore: AsyncTraitDeps {
|
||||
#[repr(transparent)]
|
||||
struct EraseCryptoStoreError<T>(T);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<T: fmt::Debug> fmt::Debug for EraseCryptoStoreError<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
@@ -331,6 +339,16 @@ impl<T: CryptoStore> CryptoStore for EraseCryptoStoreError<T> {
|
||||
self.0.inbound_group_sessions_for_backup(limit).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn mark_inbound_group_sessions_as_backed_up(
|
||||
&self,
|
||||
room_and_session_ids: &[(&RoomId, &str)],
|
||||
) -> Result<()> {
|
||||
self.0
|
||||
.mark_inbound_group_sessions_as_backed_up(room_and_session_ids)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn reset_backup_state(&self) -> Result<()> {
|
||||
self.0.reset_backup_state().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,14 @@ pub struct MegolmV1AuthData {
|
||||
extra: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
impl MegolmV1AuthData {
|
||||
// Create a new [`MegolmV1AuthData`] from a public Curve25519 key and a
|
||||
// [`Signatures`] map.
|
||||
pub(crate) fn new(public_key: Curve25519PublicKey, signatures: Signatures) -> Self {
|
||||
Self { public_key, signatures, extra: Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Information pertaining to a room key backup. Can be used to upload a new
|
||||
/// backup version as defined in the [spec].
|
||||
///
|
||||
|
||||
@@ -71,7 +71,7 @@ impl CrossSigningKey {
|
||||
|
||||
/// Serialize the cross signing key into a Raw version.
|
||||
pub fn to_raw<T>(&self) -> Raw<T> {
|
||||
Raw::from_json(to_raw_value(&self).expect("Coulnd't serialize cross signing keys"))
|
||||
Raw::from_json(to_raw_value(&self).expect("Couldn't serialize cross signing keys"))
|
||||
}
|
||||
|
||||
/// Get the Ed25519 cross-signing key (and its ID).
|
||||
|
||||
@@ -80,7 +80,6 @@ impl MasterPubkey {
|
||||
/// **Note**: Use this method with caution, the `canonical_json` needs to be
|
||||
/// correctly canonicalized and make sure that the object you are checking
|
||||
/// the signature for is allowed to be signed by a master key.
|
||||
#[cfg(any(feature = "backups_v1", test))]
|
||||
pub(crate) fn has_signed_raw(
|
||||
&self,
|
||||
signatures: &Signatures,
|
||||
|
||||
@@ -84,7 +84,7 @@ impl DeviceKeys {
|
||||
|
||||
/// Serialize the device keys key into a Raw version.
|
||||
pub fn to_raw<T>(&self) -> Raw<T> {
|
||||
Raw::from_json(to_raw_value(&self).expect("Coulnd't serialize device keys"))
|
||||
Raw::from_json(to_raw_value(&self).expect("Couldn't serialize device keys"))
|
||||
}
|
||||
|
||||
/// Get the key of the given key algorithm belonging to this device.
|
||||
|
||||
@@ -176,6 +176,7 @@ pub struct UnknownRoomKeyContent {
|
||||
other: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl std::fmt::Debug for ForwardedMegolmV1AesSha2Content {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ForwardedMegolmV1AesSha2Content")
|
||||
@@ -188,6 +189,7 @@ impl std::fmt::Debug for ForwardedMegolmV1AesSha2Content {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl std::fmt::Debug for ForwardedMegolmV2AesSha2Content {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ForwardedMegolmV2AesSha2Content")
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use ruma::{JsOption, OwnedDeviceId, RoomId};
|
||||
use ruma::{OwnedDeviceId, RoomId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use vodozemac::{megolm::MegolmMessage, olm::OlmMessage, Curve25519PublicKey};
|
||||
@@ -126,7 +126,7 @@ pub struct OlmV1Curve25519AesSha2Content {
|
||||
pub sender_key: Curve25519PublicKey,
|
||||
|
||||
/// The unique ID of this content.
|
||||
pub message_id: JsOption<String>,
|
||||
pub message_id: Option<String>,
|
||||
}
|
||||
|
||||
/// The event content for events encrypted with the m.olm.v2.curve25519-aes-sha2
|
||||
@@ -142,8 +142,8 @@ pub struct OlmV2Curve25519AesSha2Content {
|
||||
pub sender_key: Curve25519PublicKey,
|
||||
|
||||
/// The unique ID of this content.
|
||||
#[serde(default, skip_serializing_if = "JsOption::is_undefined", rename = "org.matrix.msgid")]
|
||||
pub message_id: JsOption<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none", rename = "org.matrix.msgid")]
|
||||
pub message_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
@@ -151,8 +151,8 @@ struct OlmHelper {
|
||||
#[serde(deserialize_with = "deserialize_curve_key", serialize_with = "serialize_curve_key")]
|
||||
sender_key: Curve25519PublicKey,
|
||||
ciphertext: BTreeMap<String, OlmMessage>,
|
||||
#[serde(default, skip_serializing_if = "JsOption::is_undefined", rename = "org.matrix.msgid")]
|
||||
message_id: JsOption<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none", rename = "org.matrix.msgid")]
|
||||
message_id: Option<String>,
|
||||
}
|
||||
|
||||
impl Serialize for OlmV1Curve25519AesSha2Content {
|
||||
@@ -503,7 +503,7 @@ pub(crate) mod tests {
|
||||
assert_let!(
|
||||
ToDeviceEncryptedEventContent::OlmV1Curve25519AesSha2(content) = &event.content
|
||||
);
|
||||
assert!(content.message_id.is_undefined());
|
||||
assert!(content.message_id.is_none());
|
||||
|
||||
let serialized = serde_json::to_value(event)?;
|
||||
assert_eq!(json, serialized);
|
||||
|
||||
@@ -125,6 +125,7 @@ impl MegolmV1AesSha2Content {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl std::fmt::Debug for MegolmV1AesSha2Content {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("MegolmV1AesSha2Content")
|
||||
|
||||
@@ -19,7 +19,7 @@ use std::collections::BTreeMap;
|
||||
use ruma::{
|
||||
exports::ruma_macros::AsStrAsRefStr,
|
||||
serde::{AsRefStr, DebugAsRefStr, DeserializeFromCowStr, FromString, SerializeAsRefStr},
|
||||
JsOption, OwnedDeviceId, OwnedRoomId,
|
||||
OwnedDeviceId, OwnedRoomId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
@@ -107,7 +107,7 @@ impl RoomKeyWithheldContent {
|
||||
sender_key: Curve25519PublicKey,
|
||||
from_device: OwnedDeviceId,
|
||||
) -> Self {
|
||||
let from_device = JsOption::Some(from_device);
|
||||
let from_device = Some(from_device);
|
||||
|
||||
match algorithm {
|
||||
EventEncryptionAlgorithm::MegolmV1AesSha2 => {
|
||||
@@ -222,7 +222,7 @@ impl std::fmt::Display for WithheldCode {
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct WithheldHelper {
|
||||
pub algorithm: EventEncryptionAlgorithm,
|
||||
pub reason: JsOption<String>,
|
||||
pub reason: Option<String>,
|
||||
pub code: WithheldCode,
|
||||
#[serde(flatten)]
|
||||
other: Value,
|
||||
@@ -259,8 +259,8 @@ pub struct CommonWithheldCodeContent {
|
||||
|
||||
/// The device ID of the device sending the m.room_key.withheld message
|
||||
/// MSC3735.
|
||||
#[serde(default, skip_serializing_if = "JsOption::is_undefined")]
|
||||
pub from_device: JsOption<OwnedDeviceId>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub from_device: Option<OwnedDeviceId>,
|
||||
|
||||
#[serde(flatten)]
|
||||
other: BTreeMap<String, Value>,
|
||||
@@ -278,7 +278,7 @@ impl CommonWithheldCodeContent {
|
||||
room_id,
|
||||
session_id,
|
||||
sender_key,
|
||||
from_device: JsOption::Some(device_id),
|
||||
from_device: Some(device_id),
|
||||
other: Default::default(),
|
||||
}
|
||||
}
|
||||
@@ -318,13 +318,14 @@ pub struct NoOlmWithheldContent {
|
||||
|
||||
/// The device ID of the device sending the m.room_key.withheld message
|
||||
/// MSC3735.
|
||||
#[serde(default, skip_serializing_if = "JsOption::is_undefined")]
|
||||
pub from_device: JsOption<OwnedDeviceId>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub from_device: Option<OwnedDeviceId>,
|
||||
|
||||
#[serde(flatten)]
|
||||
other: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl std::fmt::Debug for CommonWithheldCodeContent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("AnyWithheldContent")
|
||||
@@ -336,6 +337,7 @@ impl std::fmt::Debug for CommonWithheldCodeContent {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl std::fmt::Debug for NoOlmWithheldContent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("NoOlmWithheldContent")
|
||||
@@ -356,8 +358,8 @@ pub struct UnknownRoomKeyWithHeld {
|
||||
/// The withheld code
|
||||
pub code: WithheldCode,
|
||||
/// A human-readable reason for why the key was not sent.
|
||||
#[serde(default, skip_serializing_if = "JsOption::is_undefined")]
|
||||
pub reason: JsOption<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reason: Option<String>,
|
||||
/// The other data of the unknown room key.
|
||||
#[serde(flatten)]
|
||||
other: BTreeMap<String, Value>,
|
||||
@@ -427,7 +429,7 @@ impl Serialize for RoomKeyWithheldContent {
|
||||
let helper = match self {
|
||||
Self::MegolmV1AesSha2(r) => {
|
||||
let code = r.withheld_code();
|
||||
let reason = JsOption::Some(code.to_string());
|
||||
let reason = Some(code.to_string());
|
||||
|
||||
match r {
|
||||
MegolmV1AesSha2WithheldContent::BlackListed(content)
|
||||
@@ -450,7 +452,7 @@ impl Serialize for RoomKeyWithheldContent {
|
||||
#[cfg(feature = "experimental-algorithms")]
|
||||
Self::MegolmV2AesSha2(r) => {
|
||||
let code = r.withheld_code();
|
||||
let reason = JsOption::Some(code.to_string());
|
||||
let reason = Some(code.to_string());
|
||||
|
||||
match r {
|
||||
MegolmV1AesSha2WithheldContent::BlackListed(content)
|
||||
|
||||
@@ -66,6 +66,7 @@ impl Drop for SecretSendContent {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl std::fmt::Debug for SecretSendContent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SecretSendContent")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user