Compare commits
327 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c46e42201 | |||
| 0d4bc65e28 | |||
| 5e1bae02fe | |||
| 77a67de7df | |||
| f27eb4d1c8 | |||
| 05814c5559 | |||
| d5d9898fb4 | |||
| 3f71d9a379 | |||
| b077f45e78 | |||
| 648d527f2f | |||
| 8513547e92 | |||
| d18669e8d9 | |||
| a0426251a3 | |||
| 2739c5bf27 | |||
| 381f4d419f | |||
| 9ab5547065 | |||
| df3cb002a5 | |||
| 6ebd4295b9 | |||
| c5104d68fd | |||
| 0064839283 | |||
| 2727d72916 | |||
| 33a2cc3031 | |||
| 38097f90b2 | |||
| 47d08683a2 | |||
| 57919f5480 | |||
| b8949cfe26 | |||
| 8d27b0c811 | |||
| 3707d2fb81 | |||
| eaaa5e17a0 | |||
| e3958b754c | |||
| 78d9e1292f | |||
| d594b4dad7 | |||
| 3f40ad83a5 | |||
| 5049d1a3b6 | |||
| 29862fc9bd | |||
| 585224b2fa | |||
| 0dc5e69ace | |||
| b323802ab0 | |||
| 252786d2ef | |||
| 97cbe57d3f | |||
| 0d8ad159c3 | |||
| 9d732395ce | |||
| 6a772d1c56 | |||
| b71499ffe6 | |||
| 4dadf8581a | |||
| 4d4cd61363 | |||
| 6f42b0a67b | |||
| e3b348761e | |||
| d755a8a3aa | |||
| 66e3ddec47 | |||
| 720d443452 | |||
| 33d691a58e | |||
| c2f39c1086 | |||
| df9c355aed | |||
| 0334ff3f64 | |||
| 8262726369 | |||
| a4a6bf540d | |||
| 9b38f38aea | |||
| 2e05cc74bf | |||
| eb9b86971a | |||
| 31e7ec182c | |||
| 5e8f3b2bc8 | |||
| c0b91c4b0e | |||
| 46d90afa9c | |||
| 29e19b729b | |||
| 20c1eff391 | |||
| 3e40db3d7f | |||
| 8ca5983093 | |||
| 144f568a5c | |||
| 34c7dd48ae | |||
| 6c7d8c16bb | |||
| 2c930df8aa | |||
| 834bed2b1a | |||
| 8d530ef220 | |||
| 542d68dcda | |||
| 50696a0d74 | |||
| 182fc6fd8f | |||
| fe85cddf88 | |||
| 9ff3761cac | |||
| a311dcbd3e | |||
| 447bd67fe1 | |||
| d8ba2b521c | |||
| 8de15429fb | |||
| 3f398d8934 | |||
| f8ec957193 | |||
| 7cc121ab38 | |||
| 8a4918309a | |||
| 30d7fac927 | |||
| be71c6df56 | |||
| 06ad67f99c | |||
| 28fb6f7c27 | |||
| 842d32d41b | |||
| b52cf8327a | |||
| d14526f161 | |||
| 1b8a6b705c | |||
| 7c2b15fe86 | |||
| 3085f05d51 | |||
| 4344e06707 | |||
| 173ec75bb3 | |||
| 1d3f8bf898 | |||
| 5b3b87d3e2 | |||
| 6dc5b33d87 | |||
| 408b843156 | |||
| 0820170261 | |||
| 254ac6f2ce | |||
| 468a7ac883 | |||
| 3e610c80e1 | |||
| f43edbd31f | |||
| 7c57f2cee4 | |||
| 8d612eca46 | |||
| 98f4d55aa0 | |||
| 709b09c4ec | |||
| 818876a22e | |||
| 2657eb7866 | |||
| aaecbf07f2 | |||
| f336638a17 | |||
| 839fbe477c | |||
| 35ad5441d3 | |||
| 756dec264d | |||
| 87983ab610 | |||
| 66ffc3448e | |||
| c6e308717d | |||
| 4c4dd03411 | |||
| 2d0f873342 | |||
| 041627ec4a | |||
| da4b8004f2 | |||
| 3428494468 | |||
| 0c2046f93b | |||
| eb31f035e6 | |||
| df51404a14 | |||
| 3e78e441d4 | |||
| 02c2e55855 | |||
| a528624274 | |||
| 1d83d42e9f | |||
| 4684cfb780 | |||
| 991c9ad610 | |||
| e826c54a42 | |||
| 9ae658c1b9 | |||
| 4341aaf65c | |||
| ad847a82c8 | |||
| dad3e6839f | |||
| d078ef6155 | |||
| 210c5749f1 | |||
| 0c74abbc50 | |||
| dbadfe19b0 | |||
| f3e43dbfa4 | |||
| b846a6dd81 | |||
| c82e469fc3 | |||
| f2c9a8f723 | |||
| 47fc073b70 | |||
| 160600e8c0 | |||
| 993c103270 | |||
| e077980ba2 | |||
| 63d14b798b | |||
| 077d63a9fc | |||
| 453c4e12db | |||
| f7db52e069 | |||
| 2bd8c56e64 | |||
| f231c74314 | |||
| 2cb6ee8e6d | |||
| c24770a774 | |||
| 7fa06cb028 | |||
| 50383098ff | |||
| 6f780a499c | |||
| 425e48a46d | |||
| 3dd81fbe2c | |||
| 6a0333e812 | |||
| b3a789af90 | |||
| 560e582e41 | |||
| de7397a20e | |||
| 8bd94318c0 | |||
| fe3cc09ae0 | |||
| 3a3cc54067 | |||
| 47f8b32ea1 | |||
| 49748dbd4b | |||
| 25ea5fdd73 | |||
| 5fadde5a6d | |||
| b6be4d5170 | |||
| c9bac4ff2b | |||
| fedf7d214f | |||
| c969f903b7 | |||
| bd5d7aafee | |||
| e015a531da | |||
| b9014a5e2a | |||
| e9487b0851 | |||
| c60bfb877a | |||
| ee32b1f600 | |||
| 9641aa9082 | |||
| a8ca77f4fc | |||
| e647ff935e | |||
| 7f04a9a18b | |||
| 67d2cb790d | |||
| 279c78b3e2 | |||
| 9514388108 | |||
| e6dc10933c | |||
| c456356424 | |||
| 5af326b36e | |||
| 5548f38393 | |||
| d9c1188f87 | |||
| 588702756b | |||
| d6a74d389d | |||
| d807d71e22 | |||
| 587545ae82 | |||
| 49985e5476 | |||
| 4fbe79a27d | |||
| f61ad19ae6 | |||
| c9a49006f6 | |||
| ca9eb70db5 | |||
| f173aea6e4 | |||
| e37ad11b47 | |||
| d6c2a63f5c | |||
| 4ebf5056be | |||
| a79d409f9d | |||
| 5941495e68 | |||
| b3491582d0 | |||
| def4bbbed2 | |||
| 1dd2b2c9e8 | |||
| e4b269e0de | |||
| cb72d4375f | |||
| 7ec384c61a | |||
| 7466f77eae | |||
| 526b5c4630 | |||
| 4043f9bf5d | |||
| ff5dcbf631 | |||
| 6c053a86bf | |||
| 0cae54cc3f | |||
| 692aceba50 | |||
| c4a86a3d0a | |||
| 5f5aa81174 | |||
| 6e0f258a39 | |||
| c4bfbd0f44 | |||
| 8e0ee47637 | |||
| 0915eeed51 | |||
| fb54e869e9 | |||
| 9e97ed3134 | |||
| b926c4287a | |||
| ddf4d575b7 | |||
| 2a954e3ce3 | |||
| b837865226 | |||
| eac5a5eb35 | |||
| d6c64027f6 | |||
| df4b69666c | |||
| 5675ac7f46 | |||
| 6b2233f8c4 | |||
| 61dd560499 | |||
| 62567ca6eb | |||
| 46dc2a9c5e | |||
| 891583b70e | |||
| e19bdbfd59 | |||
| 14d0cc1935 | |||
| b8d0384da7 | |||
| 4e0a6d15ca | |||
| 251433382f | |||
| 34e993435d | |||
| dc2775e194 | |||
| 45c3752cae | |||
| ed178602d7 | |||
| 35a03278c3 | |||
| 9e69b631ee | |||
| 5ff556f6c3 | |||
| d64960679f | |||
| 7ff1170681 | |||
| 55e25a3717 | |||
| 3f977b79fa | |||
| aca8c8b8ee | |||
| 47c24b9a17 | |||
| 47445b10f1 | |||
| c5a9a1e215 | |||
| 2ef14ded41 | |||
| 8205da898e | |||
| 618e47250d | |||
| 5110aa64aa | |||
| bcad0a3059 | |||
| b7b88f58d2 | |||
| 412fcab4dc | |||
| 8e75a940f7 | |||
| 70fb7899e6 | |||
| 1480fada6e | |||
| c50358366f | |||
| adb4428a69 | |||
| 667a8e684c | |||
| f4b50db972 | |||
| 1abb2efc51 | |||
| c4132252d3 | |||
| 51c76a15ad | |||
| f1842ba5d0 | |||
| d8dd72fd9c | |||
| 054f5e28f6 | |||
| 38e35b99d0 | |||
| 39afb531ef | |||
| c1ff5ff49f | |||
| 2358e4c32f | |||
| 409fccb709 | |||
| b25fd830ec | |||
| 02ab57870a | |||
| eca3749b28 | |||
| 3f17325bac | |||
| 23c09b2c9d | |||
| c1f8232450 | |||
| e28073361d | |||
| 1c2fb1ab72 | |||
| be89e3aacb | |||
| 36427b0e12 | |||
| f8a9d12c88 | |||
| 5f5e979e16 | |||
| 519f281844 | |||
| 3b31bbec0c | |||
| f2942db316 | |||
| e4712be946 | |||
| bc8c4f5e58 | |||
| fe9354a886 | |||
| d00ff8fa1f | |||
| 60f521cc23 | |||
| bcb9a86a00 | |||
| 3f0712010f | |||
| d89194f071 | |||
| a20ad728b5 | |||
| 0d546dce5f | |||
| 38cc9fb7c8 | |||
| 616c193a30 | |||
| 4a88e7cfee | |||
| 5d0fed5e53 | |||
| 9975365a1e | |||
| f18e0b18a1 | |||
| b18100228e | |||
| de568837fb | |||
| bc582ae101 |
@@ -61,4 +61,7 @@ allow-git = [
|
||||
"https://github.com/jplatte/const_panic",
|
||||
# A patch override for the bindings: https://github.com/smol-rs/async-compat/pull/22
|
||||
"https://github.com/jplatte/async-compat",
|
||||
# We can release vodozemac whenever we need but let's not block development
|
||||
# on releases.
|
||||
"https://github.com/matrix-org/vodozemac",
|
||||
]
|
||||
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
java-version: '17'
|
||||
|
||||
- name: Install android sdk
|
||||
uses: malinskiy/action-android/install-sdk@release/0.1.4
|
||||
uses: malinskiy/action-android/install-sdk@release/0.1.7
|
||||
|
||||
- name: Install android ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
|
||||
+20
-25
@@ -18,6 +18,9 @@ concurrency:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
# Insta.rs is run directly via cargo test. We don't want insta.rs to create new snapshots files.
|
||||
# Just want it to run the tests (option `no` instead of `auto`).
|
||||
INSTA_UPDATE: no
|
||||
|
||||
jobs:
|
||||
xtask:
|
||||
@@ -221,12 +224,16 @@ jobs:
|
||||
- name: '[m]-common'
|
||||
cmd: matrix-sdk-common
|
||||
|
||||
- name: '[m], no-default'
|
||||
cmd: matrix-sdk-no-default
|
||||
|
||||
- name: '[m]-ui'
|
||||
cmd: matrix-sdk-ui
|
||||
check_only: true
|
||||
|
||||
- name: '[m]-indexeddb'
|
||||
cmd: indexeddb
|
||||
|
||||
- name: '[m], no-default, wasm-flags'
|
||||
cmd: matrix-sdk-no-default
|
||||
|
||||
- name: '[m], indexeddb stores'
|
||||
cmd: matrix-sdk-indexeddb-stores
|
||||
|
||||
@@ -245,6 +252,7 @@ jobs:
|
||||
|
||||
- name: Install wasm-pack
|
||||
uses: qmaru/wasm-pack-action@v0.5.0
|
||||
if: '!matrix.check_only'
|
||||
with:
|
||||
version: v0.10.3
|
||||
|
||||
@@ -274,27 +282,10 @@ jobs:
|
||||
target/debug/xtask ci wasm ${{ matrix.cmd }}
|
||||
|
||||
- name: Wasm-Pack test
|
||||
if: '!matrix.check_only'
|
||||
run: |
|
||||
target/debug/xtask ci wasm-pack ${{ matrix.cmd }}
|
||||
|
||||
formatting:
|
||||
name: Check Formatting
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-11-26
|
||||
components: rustfmt
|
||||
|
||||
- name: Cargo fmt
|
||||
run: |
|
||||
cargo fmt -- --check
|
||||
|
||||
typos:
|
||||
name: Spell Check with Typos
|
||||
runs-on: ubuntu-latest
|
||||
@@ -304,10 +295,10 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check the spelling of the files in our repo
|
||||
uses: crate-ci/typos@v1.28.3
|
||||
uses: crate-ci/typos@v1.29.5
|
||||
|
||||
clippy:
|
||||
name: Run clippy
|
||||
lint:
|
||||
name: Lint
|
||||
needs: xtask
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -324,7 +315,7 @@ jobs:
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-11-26
|
||||
components: clippy
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
@@ -338,6 +329,10 @@ jobs:
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Check Formatting
|
||||
run: |
|
||||
target/debug/xtask ci style
|
||||
|
||||
- name: Clippy
|
||||
run: |
|
||||
target/debug/xtask ci clippy
|
||||
|
||||
@@ -30,6 +30,33 @@ integration tests that need a running synapse instance. These tests reside in
|
||||
synapse for testing purposes.
|
||||
|
||||
|
||||
### Snapshot Testing
|
||||
|
||||
You can add/review snapshot tests using [insta.rs](https://insta.rs)
|
||||
|
||||
Every new struct/enum that derives `Serialize` `Deserialise` should have a snapshot test for it.
|
||||
Any code change that breaks serialisation will then break a test, the author will then have to decide
|
||||
how to handle migration and test it if needed.
|
||||
|
||||
|
||||
And for an improved review experience it's recommended (but not necessary) to install the cargo-insta tool:
|
||||
|
||||
Unix:
|
||||
```
|
||||
curl -LsSf https://insta.rs/install.sh | sh
|
||||
```
|
||||
|
||||
Windows:
|
||||
```
|
||||
powershell -c "irm https://insta.rs/install.ps1 | iex"
|
||||
```
|
||||
|
||||
Usual flow is to first run the test, then review them.
|
||||
```
|
||||
cargo insta test
|
||||
cargo insta review
|
||||
```
|
||||
|
||||
## Pull requests
|
||||
|
||||
Ideally, a PR should have a *proper title*, with *atomic logical commits*, and
|
||||
|
||||
Generated
+404
-210
File diff suppressed because it is too large
Load Diff
+43
-36
@@ -18,45 +18,49 @@ default-members = ["benchmarks", "crates/*", "labs/*"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
rust-version = "1.82"
|
||||
rust-version = "1.83"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.93"
|
||||
anyhow = "1.0.95"
|
||||
aquamarine = "0.6.0"
|
||||
assert-json-diff = "2.0.2"
|
||||
assert_matches = "1.5.0"
|
||||
assert_matches2 = "0.1.2"
|
||||
async-rx = "0.1.3"
|
||||
async-stream = "0.3.5"
|
||||
async-trait = "0.1.83"
|
||||
async-trait = "0.1.85"
|
||||
as_variant = "1.2.0"
|
||||
base64 = "0.22.1"
|
||||
byteorder = "1.5.0"
|
||||
chrono = "0.4.38"
|
||||
chrono = "0.4.39"
|
||||
eyeball = { version = "0.8.8", features = ["tracing"] }
|
||||
eyeball-im = { version = "0.5.1", features = ["tracing"] }
|
||||
eyeball-im-util = "0.7.0"
|
||||
eyeball-im = { version = "0.6.0", features = ["tracing"] }
|
||||
eyeball-im-util = "0.8.0"
|
||||
futures-core = "0.3.31"
|
||||
futures-executor = "0.3.21"
|
||||
futures-executor = "0.3.31"
|
||||
futures-util = "0.3.31"
|
||||
getrandom = { version = "0.2.15", default-features = false }
|
||||
gloo-timers = "0.3.0"
|
||||
growable-bloom-filter = "2.1.1"
|
||||
hkdf = "0.12.4"
|
||||
hmac = "0.12.1"
|
||||
http = "1.1.0"
|
||||
imbl = "3.0.0"
|
||||
indexmap = "2.6.0"
|
||||
itertools = "0.13.0"
|
||||
http = "1.2.0"
|
||||
imbl = "4.0.1"
|
||||
indexmap = "2.7.1"
|
||||
insta = { version = "1.42.1", features = ["json"] }
|
||||
itertools = "0.14.0"
|
||||
js-sys = "0.3.69"
|
||||
mime = "0.3.17"
|
||||
once_cell = "1.20.2"
|
||||
pbkdf2 = { version = "0.12.2" }
|
||||
pin-project-lite = "0.2.15"
|
||||
proptest = { version = "1.5.0", default-features = false, features = ["std"] }
|
||||
pin-project-lite = "0.2.16"
|
||||
proptest = { version = "1.6.0", default-features = false, features = ["std"] }
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.12.4", default-features = false }
|
||||
reqwest = { version = "0.12.12", default-features = false }
|
||||
rmp-serde = "1.3.0"
|
||||
ruma = { version = "0.12.0", features = [
|
||||
# Be careful to use commits from the https://github.com/ruma/ruma/tree/ruma-0.12
|
||||
# branch until a proper release with breaking changes happens.
|
||||
ruma = { version = "0.12.1", features = [
|
||||
"client-api-c",
|
||||
"compat-upload-signatures",
|
||||
"compat-user-id",
|
||||
@@ -71,17 +75,17 @@ ruma = { version = "0.12.0", features = [
|
||||
"unstable-msc4140",
|
||||
"unstable-msc4171",
|
||||
] }
|
||||
ruma-common = "0.15.0"
|
||||
serde = "1.0.151"
|
||||
serde_html_form = "0.2.0"
|
||||
serde_json = "1.0.91"
|
||||
ruma-common = { version = "0.15.1" }
|
||||
serde = "1.0.217"
|
||||
serde_html_form = "0.2.7"
|
||||
serde_json = "1.0.138"
|
||||
sha2 = "0.10.8"
|
||||
similar-asserts = "1.6.0"
|
||||
similar-asserts = "1.6.1"
|
||||
stream_assert = "0.1.1"
|
||||
tempfile = "3.9.0"
|
||||
thiserror = "2.0.3"
|
||||
tokio = { version = "1.41.1", default-features = false, features = ["sync"] }
|
||||
tokio-stream = "0.1.14"
|
||||
tempfile = "3.16.0"
|
||||
thiserror = "2.0.11"
|
||||
tokio = { version = "1.43.0", default-features = false, features = ["sync"] }
|
||||
tokio-stream = "0.1.17"
|
||||
tracing = { version = "0.1.40", default-features = false, features = ["std"] }
|
||||
tracing-core = "0.1.32"
|
||||
tracing-subscriber = "0.3.18"
|
||||
@@ -89,25 +93,25 @@ unicode-normalization = "0.1.24"
|
||||
uniffi = { version = "0.28.0" }
|
||||
uniffi_bindgen = { version = "0.28.0" }
|
||||
url = "2.5.4"
|
||||
uuid = "1.11.0"
|
||||
vodozemac = { version = "0.8.1", features = ["insecure-pk-encryption"] }
|
||||
uuid = "1.12.1"
|
||||
vodozemac = { version = "0.9.0", features = ["insecure-pk-encryption"] }
|
||||
wasm-bindgen = "0.2.84"
|
||||
wasm-bindgen-test = "0.3.33"
|
||||
web-sys = "0.3.69"
|
||||
wiremock = "0.6.2"
|
||||
zeroize = "1.8.1"
|
||||
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.9.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.9.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.9.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.9.0" }
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.10.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.10.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.10.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.10.0" }
|
||||
matrix-sdk-ffi-macros = { path = "bindings/matrix-sdk-ffi-macros", version = "0.7.0" }
|
||||
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.9.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.9.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.9.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.9.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.7.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.9.0", default-features = false }
|
||||
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.10.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.10.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.10.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.10.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.10.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.10.0", default-features = false }
|
||||
|
||||
# Default release profile, select with `--release`
|
||||
[profile.release]
|
||||
@@ -124,6 +128,9 @@ debug = 0
|
||||
# for the extra time of optimizing it for a clean build of matrix-sdk-ffi.
|
||||
quote = { opt-level = 2 }
|
||||
sha2 = { opt-level = 2 }
|
||||
# faster runs for insta.rs snapshot testing
|
||||
insta.opt-level = 3
|
||||
similar.opt-level = 3
|
||||
|
||||
# Custom profile with full debugging info, use `--profile dbg` to select
|
||||
[profile.dbg]
|
||||
|
||||
@@ -26,11 +26,9 @@ The rust-sdk consists of multiple crates that can be picked at your convenience:
|
||||
|
||||
## Status
|
||||
|
||||
The library is in an alpha state, things that are implemented generally work but
|
||||
the API will change in breaking ways.
|
||||
The library is considered production ready and backs multiple client implementations such as Element X [[1]](https://github.com/element-hq/element-x-ios) [[2]](https://github.com/element-hq/element-x-android) and [Fractal](https://gitlab.gnome.org/World/fractal). Client developers should feel confident to build upon it.
|
||||
|
||||
If you are interested in using the matrix-sdk now is the time to try it out and
|
||||
provide feedback.
|
||||
Development of the SDK has been primarily sponsored by Element though accepts contributions from all.
|
||||
|
||||
## Bindings
|
||||
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
# Upgrades 0.5 ➜ 0.6
|
||||
|
||||
This is a rough migration guide to help you upgrade your code using matrix-sdk 0.5 to the newly released matrix-sdk 0.6 . While it won't cover all edge cases and problems, we are trying to get the most common issues covered. If you experience any other difficulties in upgrade or need support with using the matrix-sdk in general, please approach us in our [matrix-sdk channel on matrix.org][matrix-channel].
|
||||
|
||||
## Minimum Supported Rust Version Update: `1.60`
|
||||
|
||||
We have updated the minimal rust version you need in order to build `matrix-sdk`, as we require some new dependency resolving features from it:
|
||||
|
||||
> These crates are built with the Rust language version 2021 and require a minimum compiler version of 1.60
|
||||
|
||||
## Dependencies
|
||||
|
||||
Many dependencies have been upgraded. Most notably, we are using `ruma` at version `0.7.0` now. It has seen some renamings and restructurings since our last release, so you might find that some Types have new names now.
|
||||
|
||||
## Repo Structure Updates
|
||||
|
||||
If you are looking at the repository itself, you will find we've rearranged the code quite a bit: we have split out any bindings-specific and testing related crates (and other things) into respective folders, and we've moved all `examples` into its own top-level-folder with each example as their own crate (rendering them easier to find and copy as starting points), all in all slimming down the `crates` folder to the core aspects.
|
||||
|
||||
|
||||
## Architecture Changes / API overall
|
||||
|
||||
### Builder Pattern
|
||||
|
||||
We are moving to the [builder pattern][] (familiar from e.g. `std::io:process:Command`) as the main configurable path for many aspects of the API, including to construct Matrix-Requests and workflows. This has been and is an on-going effort, and this release sees a lot of APIs transitioning to this pattern, you should already be familiar with from the `matrix_sdk::Client::builder()` in `0.5`. This pattern been extended onto:
|
||||
- the [login configuration][login builder] and [login with sso][ssologin builder],
|
||||
- [`SledStore` configuratiion][sled-store builder]
|
||||
- [`Indexeddb` configuration][indexeddb builder]
|
||||
|
||||
Most have fallback (though maybe with deprecation warning) support for an existing code path, but these are likely to be removed in upcoming releases.
|
||||
|
||||
### Splitting of concerns: Media
|
||||
|
||||
In an effort to declutter the `Client` API dedicated types have been created dealing with specific concerns in one place. In `0.5` we introduced `client.account()`, and `client.encryption()`, we are doing the same with `client.media()` to manage media and attachments in one place with the [`media::Media` type][media typ] now.
|
||||
|
||||
The signatures of media uploads, have also changed slightly: rather than expecting a reader `R: Read + Seek`, it now is a simple `&[u8]`. Which also means no more unnecessary `seek(0)` to reset the cursor, as we are just taking an immutable reference now.
|
||||
|
||||
### Event Handling & sync updaes
|
||||
|
||||
If you are using the `client.register_event_handler` function to receive updates on incoming sync events, you'll find yourself with a deprecation warning now. That is because we've refactored and redesigned the event handler logic to allowing `removing` of event handlers on the fly, too. For that the new `add_event_handler()` (and `add_room_event_handler`) will hand you an `EventHandlerHandle` (pardon the pun), which you can pass to `remove_event_handler`, or by using the convenient `client.event_handler_drop_guard` to create a `DropGuard` that will remove the handler when the guard is dropped. While the code still works, we recommend you switch to the new one, as we will be removing the `register_event_handler` and `register_event_handler_context` in a coming release.
|
||||
|
||||
Secondly, you will find a new [`sync_with_result_callback` sync function][sync with result]. Other than the previous sync functions, this will pass the entire `Result` to your callback, allowing you to handle errors or even raise some yourself to stop the loop. Further more, it will propagate any unhandled errors (it still handles retries as before) to the outer caller, allowing the higher level to decide how to handle that (e.g. in case of a network failure). This result-returning-behavior also punshes through the existing `sync` and `sync_with_callback`-API, allowing you to handle them on a higher level now (rather than the futures just resolving). If you find that warning, just adding a `?` to the `.await` of the call is probably the quickest way to move forward.
|
||||
|
||||
### Refresh Tokens
|
||||
|
||||
This release now [supports `refresh_token`s][refresh tokens PR] as part of the [`Session`][session]. It is implemented with a default-flag in serde so deserializing a previously serialized Session (e.g. in a store) will work as before. As part of `refresh_token` support, you can now configure the client via `ClientBuilder.request_refresh_token()` to refresh the access token automagically on certain failures or do it manually by calling `client.refresh_access_token()` yourself. Auto-refresh is _off_ by default.
|
||||
|
||||
You can stay informed about updates on the access token by listening to `client.session_tokens_signal()`.
|
||||
|
||||
### Further changes
|
||||
|
||||
- [`MessageOptions`][message options] has been updated to Matrix 1.3 by making the `from` parameter optional (and function signatures have been updated, too). You can now request the server sends you messages from the first one you are allowed to have received.
|
||||
- `client.user_id()` is not a `future` anymore. Remove any `.await` you had behind it.
|
||||
- `verified()`, `blacklisted()` and `deleted()` on `matrix_sdk::encryption::identities::Device` have been renamed with a `is_` prefix.
|
||||
- `verified()` on `matrix_sdk::encryption::identities::UserIdentity`, too has been prefixed with `is_` and thus is now called `is_verified()`.
|
||||
- The top-level crypto and state-store types of Indexeddb and Sled have been renamed to unique types>
|
||||
- `state_store` and `crypto_store` do not need to be boxed anymore when passed to the [`StoreConfig`][store config]
|
||||
- Indexeddb's `SerializationError` is now `IndexedDBStoreError`
|
||||
- Javascript specific features are now behind the `js` feature-gate
|
||||
- The new experimental next generation of sync ("sliding sync"), with a totally revamped api, can be found behind the optional `sliding-sync`-feature-gate
|
||||
|
||||
|
||||
## Quick Troubleshooting
|
||||
|
||||
You find yourself focused with any of these, here are the steps to follow to upgrade your code accordingly:
|
||||
|
||||
### warning: use of deprecated associated function `matrix_sdk::Client::register_event_handler`: Use [`Client::add_event_handler`](#method.add_event_handler) instead
|
||||
|
||||
As it says on the tin: we have deprecated this function in favor of the newer removable handler approach (see above). You can still continue to use this `fn` for now, but it will be removed in a future release.
|
||||
|
||||
### warning: use of deprecated associated function `matrix_sdk::Client::login`: Replaced by [`Client::login_username`](#method.login_username)
|
||||
|
||||
We have replaced the login facilities with a `LoginBuilder` and recommend you use that from now on. This isn't an error yet, but the function will be removed in a future release.
|
||||
|
||||
### expected slice `[u8]`, found struct ...
|
||||
|
||||
We've updated the `send_attachment` and `Media` signatures to use `&[u8]` rather than `reader: Read + Seek` as it is more convenient and common place for most architectures anyways. If you are using `File::open(path)?` to get that handler, you can just replace that with `std::fs::read(path)?`
|
||||
|
||||
### no method named `verified` found for struct `matrix_sdk::encryption::identities::Device` in the current scope
|
||||
|
||||
Boolean flags like `verified`, `deleted`, `blacklisted`, etc have been renamed with a `is_` prefix. So, just follow the cargo suggestion:
|
||||
```
|
||||
|
|
||||
69 | device.verified()
|
||||
| ^^^^^^^^ help: there is an associated function with a similar name: `is_verified`
|
||||
```
|
||||
|
||||
### unresolved import `matrix_sdk::ruma::events::AnySyncRoomEvent`
|
||||
|
||||
Ruma has been updated to `0.7.0`, you will find some ruma Events names have changed, most notably, the `AnySyncRoomEvent` is now named `AnySyncTimelineEvent` (and not `AnySyncStateEvent`, which cargo wrongly suggests). Just rename the import and usage of it.
|
||||
|
||||
### `std::option::Option<&matrix_sdk::ruma::UserId>` is not a future
|
||||
|
||||
You are seeing something along the lines of:
|
||||
```
|
||||
19 | if room_member.state_key != client.user_id().await.unwrap() {
|
||||
| ^^^^^^ `std::option::Option<&matrix_sdk::ruma::UserId>` is not a future
|
||||
|
|
||||
= help: the trait `Future` is not implemented for `std::option::Option<&matrix_sdk::ruma::UserId>`
|
||||
= note: std::option::Option<&matrix_sdk::ruma::UserId> must be a future or must implement `IntoFuture` to be awaited
|
||||
= note: required because of the requirements on the impl of `IntoFuture` for `std::option::Option<&matrix_sdk::ruma::UserId>`
|
||||
help: remove the `.await`
|
||||
|
|
||||
19 - if room_member.state_key != client.user_id().await.unwrap() {
|
||||
19 + if room_member.state_key != client.user_id().unwrap() {
|
||||
```
|
||||
|
||||
You are using `client.user_id().await` but `user_id()` is no longer `async`. Just follow the cargo suggestion and remove the `.await`, it is not necessary any longer.
|
||||
|
||||
|
||||
[matrix-channel]: https://matrix.to/#/#matrix-rust-sdk:matrix.org
|
||||
[builder pattern]: https://doc.rust-lang.org/1.0.0/style/ownership/builders.html
|
||||
[login builder]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.LoginBuilder.html
|
||||
[ssologin builder]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.SsoLoginBuilder.html
|
||||
[sled-store builder]: https://docs.rs/matrix-sdk-sled/latest/matrix_sdk_sled/struct.SledStateStoreBuilder.html
|
||||
[indexeddb builder]: https://docs.rs/matrix-sdk-indexeddb/latest/matrix_sdk_indexeddb/struct.IndexeddbStateStoreBuilder.html
|
||||
[media type]: https://docs.rs/matrix-sdk/latest/matrix_sdk//media/struct.Media.html
|
||||
[sync with result]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.Client.html#method.sync_with_result_callback
|
||||
[session]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.Session.html
|
||||
[refresh tokens PR]: https://github.com/matrix-org/matrix-rust-sdk/pull/892
|
||||
[store config]: https://docs.rs/matrix-sdk-base/latest/matrix_sdk_base/store/struct.StoreConfig.html
|
||||
[message options]: https://docs.rs/matrix-sdk/latest/matrix_sdk/room/struct.MessagesOptions.html
|
||||
@@ -1,23 +1,20 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::{
|
||||
config::SyncSettings, test_utils::logged_in_client_with_server, utils::IntoRawStateEventContent,
|
||||
};
|
||||
use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server};
|
||||
use matrix_sdk_base::{
|
||||
store::StoreConfig, BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore,
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteStateStore;
|
||||
use matrix_sdk_test::{
|
||||
event_factory::EventFactory, EventBuilder, JoinedRoomBuilder, StateTestEvent,
|
||||
SyncResponseBuilder,
|
||||
event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder,
|
||||
};
|
||||
use matrix_sdk_ui::{timeline::TimelineFocus, Timeline};
|
||||
use ruma::{
|
||||
api::client::membership::get_member_events,
|
||||
device_id,
|
||||
events::room::member::{RoomMemberEvent, RoomMemberEventContent},
|
||||
owned_room_id, owned_user_id,
|
||||
events::room::member::{MembershipState, RoomMemberEvent},
|
||||
mxc_uri, owned_room_id, owned_user_id,
|
||||
serde::Raw,
|
||||
user_id, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId,
|
||||
};
|
||||
@@ -35,28 +32,17 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
|
||||
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
|
||||
let room_id = owned_room_id!("!room:example.com");
|
||||
|
||||
let ev_builder = EventBuilder::new();
|
||||
let f = EventFactory::new().room(&room_id);
|
||||
let mut member_events: Vec<Raw<RoomMemberEvent>> = Vec::with_capacity(MEMBERS_IN_ROOM);
|
||||
let member_content_json = json!({
|
||||
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"displayname": "Alice Margatroid",
|
||||
"membership": "join",
|
||||
"reason": "Looking for support",
|
||||
});
|
||||
let member_content: Raw<RoomMemberEventContent> =
|
||||
member_content_json.into_raw_state_event_content().cast();
|
||||
for i in 0..MEMBERS_IN_ROOM {
|
||||
let user_id = OwnedUserId::try_from(format!("@user_{}:matrix.org", i)).unwrap();
|
||||
let state_key = user_id.to_string();
|
||||
let event: Raw<RoomMemberEvent> = ev_builder
|
||||
.make_state_event(
|
||||
&user_id,
|
||||
&room_id,
|
||||
&state_key,
|
||||
member_content.deserialize().unwrap(),
|
||||
None,
|
||||
)
|
||||
.cast();
|
||||
let event = f
|
||||
.member(&user_id)
|
||||
.membership(MembershipState::Join)
|
||||
.avatar_url(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
|
||||
.display_name("Alice Margatroid")
|
||||
.reason("Looking for support")
|
||||
.into_raw();
|
||||
member_events.push(event);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ use std::sync::Arc;
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::{
|
||||
authentication::matrix::{MatrixSession, MatrixSessionTokens},
|
||||
config::StoreConfig,
|
||||
matrix_auth::{MatrixSession, MatrixSessionTokens},
|
||||
Client, RoomInfo, RoomState, StateChanges,
|
||||
};
|
||||
use matrix_sdk_base::{store::MemoryStore, SessionMeta, StateStore as _};
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
use std::{env, error::Error, path::PathBuf, process::Command};
|
||||
use std::{
|
||||
env,
|
||||
error::Error,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use vergen::EmitBuilder;
|
||||
|
||||
@@ -39,7 +44,7 @@ fn setup_x86_64_android_workaround() {
|
||||
}
|
||||
|
||||
/// Run the clang binary at `clang_path`, and return its major version number
|
||||
fn get_clang_major_version(clang_path: &PathBuf) -> String {
|
||||
fn get_clang_major_version(clang_path: &Path) -> String {
|
||||
let clang_output =
|
||||
Command::new(clang_path).arg("-dumpversion").output().expect("failed to start clang");
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@ use crate::{CryptoStoreError, DehydratedDeviceKey};
|
||||
#[uniffi(flat_error)]
|
||||
pub enum DehydrationError {
|
||||
#[error(transparent)]
|
||||
Pickle(#[from] matrix_sdk_crypto::vodozemac::LibolmPickleError),
|
||||
Pickle(#[from] matrix_sdk_crypto::vodozemac::DehydratedDeviceError),
|
||||
#[error(transparent)]
|
||||
LegacyPickle(#[from] matrix_sdk_crypto::vodozemac::LibolmPickleError),
|
||||
#[error(transparent)]
|
||||
MissingSigningKey(#[from] matrix_sdk_crypto::SignatureError),
|
||||
#[error(transparent)]
|
||||
@@ -35,6 +37,9 @@ impl From<matrix_sdk_crypto::dehydrated_devices::DehydrationError> for Dehydrati
|
||||
match value {
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::Json(e) => Self::Json(e),
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::Pickle(e) => Self::Pickle(e),
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::LegacyPickle(e) => {
|
||||
Self::LegacyPickle(e)
|
||||
}
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::MissingSigningKey(e) => {
|
||||
Self::MissingSigningKey(e)
|
||||
}
|
||||
|
||||
@@ -680,15 +680,20 @@ pub struct EncryptionSettings {
|
||||
|
||||
impl From<EncryptionSettings> for RustEncryptionSettings {
|
||||
fn from(v: EncryptionSettings) -> Self {
|
||||
let sharing_strategy = if v.only_allow_trusted_devices {
|
||||
CollectStrategy::OnlyTrustedDevices
|
||||
} else if v.error_on_verified_user_problem {
|
||||
CollectStrategy::ErrorOnVerifiedUserProblem
|
||||
} else {
|
||||
CollectStrategy::AllDevices
|
||||
};
|
||||
|
||||
RustEncryptionSettings {
|
||||
algorithm: v.algorithm.into(),
|
||||
rotation_period: Duration::from_secs(v.rotation_period),
|
||||
rotation_period_msgs: v.rotation_period_msgs,
|
||||
history_visibility: v.history_visibility.into(),
|
||||
sharing_strategy: CollectStrategy::DeviceBasedStrategy {
|
||||
only_allow_trusted_devices: v.only_allow_trusted_devices,
|
||||
error_on_verified_user_problem: v.error_on_verified_user_problem,
|
||||
},
|
||||
sharing_strategy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -791,8 +791,7 @@ impl VerificationRequest {
|
||||
// task.
|
||||
let should_break = matches!(
|
||||
state,
|
||||
RustVerificationRequestState::Done { .. }
|
||||
| RustVerificationRequestState::Cancelled { .. }
|
||||
RustVerificationRequestState::Done | RustVerificationRequestState::Cancelled { .. }
|
||||
);
|
||||
|
||||
let state = Self::convert_verification_request(&request, state);
|
||||
|
||||
@@ -9,6 +9,7 @@ readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = { workspace = true }
|
||||
version = "0.7.0"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
@@ -22,3 +23,6 @@ syn = { version = "2.0.43", features = ["full", "extra-traits"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- Matrix client API errors coming from API responses will now be mapped to `ClientError::MatrixApi`, containing both the
|
||||
original message and the associated error code and kind.
|
||||
|
||||
- `EventSendState` now has two additional variants: `CrossSigningNotSetup` and
|
||||
`SendingFromUnverifiedDevice`. These indicate that your own device is not
|
||||
properly cross-signed, which is a requirement when using the identity-based
|
||||
@@ -34,3 +37,4 @@ Additions:
|
||||
- Add `Encryption::get_user_identity` which returns `UserIdentity`
|
||||
- Add `ClientBuilder::room_key_recipient_strategy`
|
||||
- Add `Room::send_raw`
|
||||
- Expose `withdraw_verification` to `UserIdentity`
|
||||
|
||||
@@ -56,7 +56,6 @@ features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
"experimental-oidc",
|
||||
"experimental-sliding-sync",
|
||||
"experimental-widgets",
|
||||
"markdown",
|
||||
"rustls-tls", # note: differ from block below
|
||||
@@ -71,7 +70,6 @@ features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
"experimental-oidc",
|
||||
"experimental-sliding-sync",
|
||||
"experimental-widgets",
|
||||
"markdown",
|
||||
"native-tls", # note: differ from block above
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
use std::{env, error::Error, path::PathBuf, process::Command};
|
||||
use std::{
|
||||
env,
|
||||
error::Error,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use vergen::EmitBuilder;
|
||||
|
||||
@@ -39,7 +44,7 @@ fn setup_x86_64_android_workaround() {
|
||||
}
|
||||
|
||||
/// Run the clang binary at `clang_path`, and return its major version number
|
||||
fn get_clang_major_version(clang_path: &PathBuf) -> String {
|
||||
fn get_clang_major_version(clang_path: &Path) -> String {
|
||||
let clang_output =
|
||||
Command::new(clang_path).arg("-dumpversion").output().expect("failed to start clang");
|
||||
|
||||
|
||||
@@ -8,8 +8,3 @@ dictionary Mentions {
|
||||
interface RoomMessageEventContentWithoutRelation {
|
||||
RoomMessageEventContentWithoutRelation with_mentions(Mentions mentions);
|
||||
};
|
||||
|
||||
[Error]
|
||||
interface ClientError {
|
||||
Generic(string msg);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
};
|
||||
|
||||
use matrix_sdk::{
|
||||
oidc::{
|
||||
authentication::oidc::{
|
||||
registrations::OidcRegistrationsError,
|
||||
types::{
|
||||
iana::oauth::OAuthClientAuthenticationMethod,
|
||||
|
||||
@@ -7,11 +7,7 @@ use std::{
|
||||
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use matrix_sdk::{
|
||||
media::{
|
||||
MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequestParameters,
|
||||
MediaThumbnailSettings,
|
||||
},
|
||||
oidc::{
|
||||
authentication::oidc::{
|
||||
registrations::{ClientId, OidcRegistrations},
|
||||
requests::account_management::AccountManagementActionFull,
|
||||
types::{
|
||||
@@ -23,6 +19,10 @@ use matrix_sdk::{
|
||||
},
|
||||
OidcAuthorizationData, OidcSession,
|
||||
},
|
||||
media::{
|
||||
MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequestParameters,
|
||||
MediaThumbnailSettings,
|
||||
},
|
||||
reqwest::StatusCode,
|
||||
ruma::{
|
||||
api::client::{
|
||||
@@ -1152,17 +1152,6 @@ impl Client {
|
||||
let alias = RoomAliasId::parse(alias)?;
|
||||
self.inner.is_room_alias_available(&alias).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Creates a new room alias associated with the provided room id.
|
||||
pub async fn create_room_alias(
|
||||
&self,
|
||||
room_alias: String,
|
||||
room_id: String,
|
||||
) -> Result<(), ClientError> {
|
||||
let room_alias = RoomAliasId::parse(room_alias)?;
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
self.inner.create_room_alias(&room_alias, &room_id).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
@@ -1462,6 +1451,9 @@ pub enum RoomVisibility {
|
||||
|
||||
/// Indicates that the room will not be shown in the published room list.
|
||||
Private,
|
||||
|
||||
/// A custom value that's not present in the spec.
|
||||
Custom { value: String },
|
||||
}
|
||||
|
||||
impl From<RoomVisibility> for Visibility {
|
||||
@@ -1469,6 +1461,17 @@ impl From<RoomVisibility> for Visibility {
|
||||
match value {
|
||||
RoomVisibility::Public => Self::Public,
|
||||
RoomVisibility::Private => Self::Private,
|
||||
RoomVisibility::Custom { value } => value.as_str().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Visibility> for RoomVisibility {
|
||||
fn from(value: Visibility) -> Self {
|
||||
match value {
|
||||
Visibility::Public => Self::Public,
|
||||
Visibility::Private => Self::Private,
|
||||
_ => Self::Custom { value: value.as_str().to_owned() },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1532,10 +1535,13 @@ impl Session {
|
||||
match auth_api {
|
||||
// Build the session from the regular Matrix Auth Session.
|
||||
AuthApi::Matrix(a) => {
|
||||
let matrix_sdk::matrix_auth::MatrixSession {
|
||||
let matrix_sdk::authentication::matrix::MatrixSession {
|
||||
meta: matrix_sdk::SessionMeta { user_id, device_id },
|
||||
tokens:
|
||||
matrix_sdk::matrix_auth::MatrixSessionTokens { access_token, refresh_token },
|
||||
matrix_sdk::authentication::matrix::MatrixSessionTokens {
|
||||
access_token,
|
||||
refresh_token,
|
||||
},
|
||||
} = a.session().context("Missing session")?;
|
||||
|
||||
Ok(Session {
|
||||
@@ -1550,10 +1556,10 @@ impl Session {
|
||||
}
|
||||
// Build the session from the OIDC UserSession.
|
||||
AuthApi::Oidc(api) => {
|
||||
let matrix_sdk::oidc::UserSession {
|
||||
let matrix_sdk::authentication::oidc::UserSession {
|
||||
meta: matrix_sdk::SessionMeta { user_id, device_id },
|
||||
tokens:
|
||||
matrix_sdk::oidc::OidcSessionTokens {
|
||||
matrix_sdk::authentication::oidc::OidcSessionTokens {
|
||||
access_token,
|
||||
refresh_token,
|
||||
latest_id_token,
|
||||
@@ -1614,12 +1620,12 @@ impl TryFrom<Session> for AuthSession {
|
||||
.transpose()
|
||||
.context("OIDC latest_id_token is invalid.")?;
|
||||
|
||||
let user_session = matrix_sdk::oidc::UserSession {
|
||||
let user_session = matrix_sdk::authentication::oidc::UserSession {
|
||||
meta: matrix_sdk::SessionMeta {
|
||||
user_id: user_id.try_into()?,
|
||||
device_id: device_id.into(),
|
||||
},
|
||||
tokens: matrix_sdk::oidc::OidcSessionTokens {
|
||||
tokens: matrix_sdk::authentication::oidc::OidcSessionTokens {
|
||||
access_token,
|
||||
refresh_token,
|
||||
latest_id_token,
|
||||
@@ -1636,12 +1642,12 @@ impl TryFrom<Session> for AuthSession {
|
||||
Ok(AuthSession::Oidc(session.into()))
|
||||
} else {
|
||||
// Create a regular Matrix Session.
|
||||
let session = matrix_sdk::matrix_auth::MatrixSession {
|
||||
let session = matrix_sdk::authentication::matrix::MatrixSession {
|
||||
meta: matrix_sdk::SessionMeta {
|
||||
user_id: user_id.try_into()?,
|
||||
device_id: device_id.into(),
|
||||
},
|
||||
tokens: matrix_sdk::matrix_auth::MatrixSessionTokens {
|
||||
tokens: matrix_sdk::authentication::matrix::MatrixSessionTokens {
|
||||
access_token,
|
||||
refresh_token,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{fs, num::NonZeroUsize, path::PathBuf, sync::Arc, time::Duration};
|
||||
use std::{fs, num::NonZeroUsize, path::Path, sync::Arc, time::Duration};
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
@@ -509,8 +509,8 @@ impl ClientBuilder {
|
||||
}
|
||||
|
||||
if let Some(session_paths) = &builder.session_paths {
|
||||
let data_path = PathBuf::from(&session_paths.data_path);
|
||||
let cache_path = PathBuf::from(&session_paths.cache_path);
|
||||
let data_path = Path::new(&session_paths.data_path);
|
||||
let cache_path = Path::new(&session_paths.cache_path);
|
||||
|
||||
debug!(
|
||||
data_path = %data_path.to_string_lossy(),
|
||||
@@ -518,12 +518,12 @@ impl ClientBuilder {
|
||||
"Creating directories for data and cache stores.",
|
||||
);
|
||||
|
||||
fs::create_dir_all(&data_path)?;
|
||||
fs::create_dir_all(&cache_path)?;
|
||||
fs::create_dir_all(data_path)?;
|
||||
fs::create_dir_all(cache_path)?;
|
||||
|
||||
inner_builder = inner_builder.sqlite_store_with_cache_path(
|
||||
&data_path,
|
||||
&cache_path,
|
||||
data_path,
|
||||
cache_path,
|
||||
builder.passphrase.as_deref(),
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -281,7 +281,7 @@ impl Encryption {
|
||||
}
|
||||
|
||||
pub async fn is_last_device(&self) -> Result<bool> {
|
||||
Ok(self.inner.recovery().are_we_the_last_man_standing().await?)
|
||||
Ok(self.inner.recovery().is_last_device().await?)
|
||||
}
|
||||
|
||||
pub async fn wait_for_backup_upload_steady_state(
|
||||
@@ -478,6 +478,15 @@ impl UserIdentity {
|
||||
Ok(self.inner.pin().await?)
|
||||
}
|
||||
|
||||
/// Remove the requirement for this identity to be verified.
|
||||
///
|
||||
/// If an identity was previously verified and is not anymore it will be
|
||||
/// reported to the user. In order to remove this notice users have to
|
||||
/// verify again or to withdraw the verification requirement.
|
||||
pub(crate) async fn withdraw_verification(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.withdraw_verification().await?)
|
||||
}
|
||||
|
||||
/// Get the public part of the Master key of this user identity.
|
||||
///
|
||||
/// The public part of the Master key is usually used to uniquely identify
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
use std::{collections::HashMap, fmt, fmt::Display};
|
||||
use std::{collections::HashMap, fmt, fmt::Display, time::SystemTime};
|
||||
|
||||
use matrix_sdk::{
|
||||
encryption::CryptoStoreError, event_cache::EventCacheError, oidc::OidcError, reqwest,
|
||||
room::edit::EditError, send_queue::RoomSendQueueError, HttpError, IdParseError,
|
||||
authentication::oidc::OidcError, encryption::CryptoStoreError, event_cache::EventCacheError,
|
||||
reqwest, room::edit::EditError, send_queue::RoomSendQueueError, HttpError, IdParseError,
|
||||
NotificationSettingsError as SdkNotificationSettingsError,
|
||||
QueueWedgeError as SdkQueueWedgeError, StoreError,
|
||||
};
|
||||
use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline};
|
||||
use ruma::api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter};
|
||||
use uniffi::UnexpectedUniFFICallbackError;
|
||||
|
||||
use crate::room_list::RoomListError;
|
||||
use crate::{room_list::RoomListError, timeline::FocusEventError};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
pub enum ClientError {
|
||||
#[error("client error: {msg}")]
|
||||
Generic { msg: String },
|
||||
#[error("api error {code}: {msg}")]
|
||||
MatrixApi { kind: ErrorKind, code: String, msg: String },
|
||||
}
|
||||
|
||||
impl ClientError {
|
||||
@@ -43,7 +46,22 @@ impl From<UnexpectedUniFFICallbackError> for ClientError {
|
||||
|
||||
impl From<matrix_sdk::Error> for ClientError {
|
||||
fn from(e: matrix_sdk::Error) -> Self {
|
||||
Self::new(e)
|
||||
match e {
|
||||
matrix_sdk::Error::Http(http_error) => {
|
||||
if let Some(api_error) = http_error.as_client_api_error() {
|
||||
if let ErrorBody::Standard { kind, message } = &api_error.body {
|
||||
let code = kind.errcode().to_string();
|
||||
let Ok(kind) = kind.to_owned().try_into() else {
|
||||
// We couldn't parse the API error, so we return a generic one instead
|
||||
return Self::Generic { msg: message.to_string() };
|
||||
};
|
||||
return Self::MatrixApi { kind, code, msg: message.to_owned() };
|
||||
}
|
||||
}
|
||||
Self::Generic { msg: http_error.to_string() }
|
||||
}
|
||||
_ => Self::Generic { msg: e.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +173,18 @@ impl From<RoomSendQueueError> for ClientError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NotYetImplemented> for ClientError {
|
||||
fn from(_: NotYetImplemented) -> Self {
|
||||
Self::new("This functionality is not implemented yet.")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FocusEventError> for ClientError {
|
||||
fn from(e: FocusEventError) -> Self {
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// Bindings version of the sdk type replacing OwnedUserId/DeviceIds with simple
|
||||
/// String.
|
||||
///
|
||||
@@ -321,3 +351,439 @@ impl From<matrix_sdk::Error> for NotificationSettingsError {
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("not implemented yet")]
|
||||
pub struct NotYetImplemented;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, uniffi::Enum)]
|
||||
// Please keep the variants sorted alphabetically.
|
||||
pub enum ErrorKind {
|
||||
/// `M_BAD_ALIAS`
|
||||
///
|
||||
/// One or more [room aliases] within the `m.room.canonical_alias` event do
|
||||
/// not point to the room ID for which the state event is to be sent to.
|
||||
///
|
||||
/// [room aliases]: https://spec.matrix.org/latest/client-server-api/#room-aliases
|
||||
BadAlias,
|
||||
|
||||
/// `M_BAD_JSON`
|
||||
///
|
||||
/// The request contained valid JSON, but it was malformed in some way, e.g.
|
||||
/// missing required keys, invalid values for keys.
|
||||
BadJson,
|
||||
|
||||
/// `M_BAD_STATE`
|
||||
///
|
||||
/// The state change requested cannot be performed, such as attempting to
|
||||
/// unban a user who is not banned.
|
||||
BadState,
|
||||
|
||||
/// `M_BAD_STATUS`
|
||||
///
|
||||
/// The application service returned a bad status.
|
||||
BadStatus {
|
||||
/// The HTTP status code of the response.
|
||||
status: Option<u16>,
|
||||
|
||||
/// The body of the response.
|
||||
body: Option<String>,
|
||||
},
|
||||
|
||||
/// `M_CANNOT_LEAVE_SERVER_NOTICE_ROOM`
|
||||
///
|
||||
/// The user is unable to reject an invite to join the [server notices]
|
||||
/// room.
|
||||
///
|
||||
/// [server notices]: https://spec.matrix.org/latest/client-server-api/#server-notices
|
||||
CannotLeaveServerNoticeRoom,
|
||||
|
||||
/// `M_CANNOT_OVERWRITE_MEDIA`
|
||||
///
|
||||
/// The [`create_content_async`] endpoint was called with a media ID that
|
||||
/// already has content.
|
||||
///
|
||||
/// [`create_content_async`]: crate::media::create_content_async
|
||||
CannotOverwriteMedia,
|
||||
|
||||
/// `M_CAPTCHA_INVALID`
|
||||
///
|
||||
/// The Captcha provided did not match what was expected.
|
||||
CaptchaInvalid,
|
||||
|
||||
/// `M_CAPTCHA_NEEDED`
|
||||
///
|
||||
/// A Captcha is required to complete the request.
|
||||
CaptchaNeeded,
|
||||
|
||||
/// `M_CONNECTION_FAILED`
|
||||
///
|
||||
/// The connection to the application service failed.
|
||||
ConnectionFailed,
|
||||
|
||||
/// `M_CONNECTION_TIMEOUT`
|
||||
///
|
||||
/// The connection to the application service timed out.
|
||||
ConnectionTimeout,
|
||||
|
||||
/// `M_DUPLICATE_ANNOTATION`
|
||||
///
|
||||
/// The request is an attempt to send a [duplicate annotation].
|
||||
///
|
||||
/// [duplicate annotation]: https://spec.matrix.org/latest/client-server-api/#avoiding-duplicate-annotations
|
||||
DuplicateAnnotation,
|
||||
|
||||
/// `M_EXCLUSIVE`
|
||||
///
|
||||
/// The resource being requested is reserved by an application service, or
|
||||
/// the application service making the request has not created the
|
||||
/// resource.
|
||||
Exclusive,
|
||||
|
||||
/// `M_FORBIDDEN`
|
||||
///
|
||||
/// Forbidden access, e.g. joining a room without permission, failed login.
|
||||
Forbidden,
|
||||
|
||||
/// `M_GUEST_ACCESS_FORBIDDEN`
|
||||
///
|
||||
/// The room or resource does not permit [guests] to access it.
|
||||
///
|
||||
/// [guests]: https://spec.matrix.org/latest/client-server-api/#guest-access
|
||||
GuestAccessForbidden,
|
||||
|
||||
/// `M_INCOMPATIBLE_ROOM_VERSION`
|
||||
///
|
||||
/// The client attempted to join a room that has a version the server does
|
||||
/// not support.
|
||||
IncompatibleRoomVersion {
|
||||
/// The room's version.
|
||||
room_version: String,
|
||||
},
|
||||
|
||||
/// `M_INVALID_PARAM`
|
||||
///
|
||||
/// A parameter that was specified has the wrong value. For example, the
|
||||
/// server expected an integer and instead received a string.
|
||||
InvalidParam,
|
||||
|
||||
/// `M_INVALID_ROOM_STATE`
|
||||
///
|
||||
/// The initial state implied by the parameters to the [`create_room`]
|
||||
/// request is invalid, e.g. the user's `power_level` is set below that
|
||||
/// necessary to set the room name.
|
||||
///
|
||||
/// [`create_room`]: crate::room::create_room
|
||||
InvalidRoomState,
|
||||
|
||||
/// `M_INVALID_USERNAME`
|
||||
///
|
||||
/// The desired user name is not valid.
|
||||
InvalidUsername,
|
||||
|
||||
/// `M_LIMIT_EXCEEDED`
|
||||
///
|
||||
/// The request has been refused due to [rate limiting]: too many requests
|
||||
/// have been sent in a short period of time.
|
||||
///
|
||||
/// [rate limiting]: https://spec.matrix.org/latest/client-server-api/#rate-limiting
|
||||
LimitExceeded {
|
||||
/// How long a client should wait before they can try again.
|
||||
retry_after_ms: Option<u64>,
|
||||
},
|
||||
|
||||
/// `M_MISSING_PARAM`
|
||||
///
|
||||
/// A required parameter was missing from the request.
|
||||
MissingParam,
|
||||
|
||||
/// `M_MISSING_TOKEN`
|
||||
///
|
||||
/// No [access token] was specified for the request, but one is required.
|
||||
///
|
||||
/// [access token]: https://spec.matrix.org/latest/client-server-api/#client-authentication
|
||||
MissingToken,
|
||||
|
||||
/// `M_NOT_FOUND`
|
||||
///
|
||||
/// No resource was found for this request.
|
||||
NotFound,
|
||||
|
||||
/// `M_NOT_JSON`
|
||||
///
|
||||
/// The request did not contain valid JSON.
|
||||
NotJson,
|
||||
|
||||
/// `M_NOT_YET_UPLOADED`
|
||||
///
|
||||
/// An `mxc:` URI generated with the [`create_mxc_uri`] endpoint was used
|
||||
/// and the content is not yet available.
|
||||
///
|
||||
/// [`create_mxc_uri`]: crate::media::create_mxc_uri
|
||||
NotYetUploaded,
|
||||
|
||||
/// `M_RESOURCE_LIMIT_EXCEEDED`
|
||||
///
|
||||
/// The request cannot be completed because the homeserver has reached a
|
||||
/// resource limit imposed on it. For example, a homeserver held in a
|
||||
/// shared hosting environment may reach a resource limit if it starts
|
||||
/// using too much memory or disk space.
|
||||
ResourceLimitExceeded {
|
||||
/// A URI giving a contact method for the server administrator.
|
||||
admin_contact: String,
|
||||
},
|
||||
|
||||
/// `M_ROOM_IN_USE`
|
||||
///
|
||||
/// The [room alias] specified in the [`create_room`] request is already
|
||||
/// taken.
|
||||
///
|
||||
/// [`create_room`]: crate::room::create_room
|
||||
/// [room alias]: https://spec.matrix.org/latest/client-server-api/#room-aliases
|
||||
RoomInUse,
|
||||
|
||||
/// `M_SERVER_NOT_TRUSTED`
|
||||
///
|
||||
/// The client's request used a third-party server, e.g. identity server,
|
||||
/// that this server does not trust.
|
||||
ServerNotTrusted,
|
||||
|
||||
/// `M_THREEPID_AUTH_FAILED`
|
||||
///
|
||||
/// Authentication could not be performed on the [third-party identifier].
|
||||
///
|
||||
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
|
||||
ThreepidAuthFailed,
|
||||
|
||||
/// `M_THREEPID_DENIED`
|
||||
///
|
||||
/// The server does not permit this [third-party identifier]. This may
|
||||
/// happen if the server only permits, for example, email addresses from
|
||||
/// a particular domain.
|
||||
///
|
||||
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
|
||||
ThreepidDenied,
|
||||
|
||||
/// `M_THREEPID_IN_USE`
|
||||
///
|
||||
/// The [third-party identifier] is already in use by another user.
|
||||
///
|
||||
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
|
||||
ThreepidInUse,
|
||||
|
||||
/// `M_THREEPID_MEDIUM_NOT_SUPPORTED`
|
||||
///
|
||||
/// The homeserver does not support adding a [third-party identifier] of the
|
||||
/// given medium.
|
||||
///
|
||||
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
|
||||
ThreepidMediumNotSupported,
|
||||
|
||||
/// `M_THREEPID_NOT_FOUND`
|
||||
///
|
||||
/// No account matching the given [third-party identifier] could be found.
|
||||
///
|
||||
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
|
||||
ThreepidNotFound,
|
||||
|
||||
/// `M_TOO_LARGE`
|
||||
///
|
||||
/// The request or entity was too large.
|
||||
TooLarge,
|
||||
|
||||
/// `M_UNABLE_TO_AUTHORISE_JOIN`
|
||||
///
|
||||
/// The room is [restricted] and none of the conditions can be validated by
|
||||
/// the homeserver. This can happen if the homeserver does not know
|
||||
/// about any of the rooms listed as conditions, for example.
|
||||
///
|
||||
/// [restricted]: https://spec.matrix.org/latest/client-server-api/#restricted-rooms
|
||||
UnableToAuthorizeJoin,
|
||||
|
||||
/// `M_UNABLE_TO_GRANT_JOIN`
|
||||
///
|
||||
/// A different server should be attempted for the join. This is typically
|
||||
/// because the resident server can see that the joining user satisfies
|
||||
/// one or more conditions, such as in the case of [restricted rooms],
|
||||
/// but the resident server would be unable to meet the authorization
|
||||
/// rules.
|
||||
///
|
||||
/// [restricted rooms]: https://spec.matrix.org/latest/client-server-api/#restricted-rooms
|
||||
UnableToGrantJoin,
|
||||
|
||||
/// `M_UNAUTHORIZED`
|
||||
///
|
||||
/// The request was not correctly authorized. Usually due to login failures.
|
||||
Unauthorized,
|
||||
|
||||
/// `M_UNKNOWN`
|
||||
///
|
||||
/// An unknown error has occurred.
|
||||
Unknown,
|
||||
|
||||
/// `M_UNKNOWN_TOKEN`
|
||||
///
|
||||
/// The [access or refresh token] specified was not recognized.
|
||||
///
|
||||
/// [access or refresh token]: https://spec.matrix.org/latest/client-server-api/#client-authentication
|
||||
UnknownToken {
|
||||
/// If this is `true`, the client is in a "[soft logout]" state, i.e.
|
||||
/// the server requires re-authentication but the session is not
|
||||
/// invalidated. The client can acquire a new access token by
|
||||
/// specifying the device ID it is already using to the login API.
|
||||
///
|
||||
/// [soft logout]: https://spec.matrix.org/latest/client-server-api/#soft-logout
|
||||
soft_logout: bool,
|
||||
},
|
||||
|
||||
/// `M_UNRECOGNIZED`
|
||||
///
|
||||
/// The server did not understand the request.
|
||||
///
|
||||
/// This is expected to be returned with a 404 HTTP status code if the
|
||||
/// endpoint is not implemented or a 405 HTTP status code if the
|
||||
/// endpoint is implemented, but the incorrect HTTP method is used.
|
||||
Unrecognized,
|
||||
|
||||
/// `M_UNSUPPORTED_ROOM_VERSION`
|
||||
///
|
||||
/// The request to [`create_room`] used a room version that the server does
|
||||
/// not support.
|
||||
///
|
||||
/// [`create_room`]: crate::room::create_room
|
||||
UnsupportedRoomVersion,
|
||||
|
||||
/// `M_URL_NOT_SET`
|
||||
///
|
||||
/// The application service doesn't have a URL configured.
|
||||
UrlNotSet,
|
||||
|
||||
/// `M_USER_DEACTIVATED`
|
||||
///
|
||||
/// The user ID associated with the request has been deactivated.
|
||||
UserDeactivated,
|
||||
|
||||
/// `M_USER_IN_USE`
|
||||
///
|
||||
/// The desired user ID is already taken.
|
||||
UserInUse,
|
||||
|
||||
/// `M_USER_LOCKED`
|
||||
///
|
||||
/// The account has been [locked] and cannot be used at this time.
|
||||
///
|
||||
/// [locked]: https://spec.matrix.org/latest/client-server-api/#account-locking
|
||||
UserLocked,
|
||||
|
||||
/// `M_USER_SUSPENDED`
|
||||
///
|
||||
/// The account has been [suspended] and can only be used for limited
|
||||
/// actions at this time.
|
||||
///
|
||||
/// [suspended]: https://spec.matrix.org/latest/client-server-api/#account-suspension
|
||||
UserSuspended,
|
||||
|
||||
/// `M_WEAK_PASSWORD`
|
||||
///
|
||||
/// The password was [rejected] by the server for being too weak.
|
||||
///
|
||||
/// [rejected]: https://spec.matrix.org/latest/client-server-api/#notes-on-password-management
|
||||
WeakPassword,
|
||||
|
||||
/// `M_WRONG_ROOM_KEYS_VERSION`
|
||||
///
|
||||
/// The version of the [room keys backup] provided in the request does not
|
||||
/// match the current backup version.
|
||||
///
|
||||
/// [room keys backup]: https://spec.matrix.org/latest/client-server-api/#server-side-key-backups
|
||||
WrongRoomKeysVersion {
|
||||
/// The currently active backup version.
|
||||
current_version: Option<String>,
|
||||
},
|
||||
|
||||
/// A custom API error.
|
||||
Custom { errcode: String },
|
||||
}
|
||||
|
||||
impl TryFrom<RumaApiErrorKind> for ErrorKind {
|
||||
type Error = NotYetImplemented;
|
||||
fn try_from(value: RumaApiErrorKind) -> Result<Self, Self::Error> {
|
||||
match &value {
|
||||
RumaApiErrorKind::BadAlias => Ok(ErrorKind::BadAlias),
|
||||
RumaApiErrorKind::BadJson => Ok(ErrorKind::BadJson),
|
||||
RumaApiErrorKind::BadState => Ok(ErrorKind::BadState),
|
||||
RumaApiErrorKind::BadStatus { status, body } => Ok(ErrorKind::BadStatus {
|
||||
status: status.map(|code| code.clone().as_u16()),
|
||||
body: body.clone(),
|
||||
}),
|
||||
RumaApiErrorKind::CannotLeaveServerNoticeRoom => {
|
||||
Ok(ErrorKind::CannotLeaveServerNoticeRoom)
|
||||
}
|
||||
RumaApiErrorKind::CannotOverwriteMedia => Ok(ErrorKind::CannotOverwriteMedia),
|
||||
RumaApiErrorKind::CaptchaInvalid => Ok(ErrorKind::CaptchaInvalid),
|
||||
RumaApiErrorKind::CaptchaNeeded => Ok(ErrorKind::CaptchaNeeded),
|
||||
RumaApiErrorKind::ConnectionFailed => Ok(ErrorKind::ConnectionFailed),
|
||||
RumaApiErrorKind::ConnectionTimeout => Ok(ErrorKind::ConnectionTimeout),
|
||||
RumaApiErrorKind::DuplicateAnnotation => Ok(ErrorKind::DuplicateAnnotation),
|
||||
RumaApiErrorKind::Exclusive => Ok(ErrorKind::Exclusive),
|
||||
RumaApiErrorKind::Forbidden { .. } => Ok(ErrorKind::Forbidden),
|
||||
RumaApiErrorKind::GuestAccessForbidden => Ok(ErrorKind::GuestAccessForbidden),
|
||||
RumaApiErrorKind::IncompatibleRoomVersion { room_version } => {
|
||||
Ok(ErrorKind::IncompatibleRoomVersion { room_version: room_version.to_string() })
|
||||
}
|
||||
RumaApiErrorKind::InvalidParam => Ok(ErrorKind::InvalidParam),
|
||||
RumaApiErrorKind::InvalidRoomState => Ok(ErrorKind::InvalidRoomState),
|
||||
RumaApiErrorKind::InvalidUsername => Ok(ErrorKind::InvalidUsername),
|
||||
RumaApiErrorKind::LimitExceeded { retry_after } => {
|
||||
let retry_after_ms = match retry_after {
|
||||
Some(RetryAfter::Delay(duration)) => Some(duration.as_millis() as u64),
|
||||
Some(RetryAfter::DateTime(system_time)) => {
|
||||
let duration = system_time.duration_since(SystemTime::now()).ok();
|
||||
duration.map(|duration| duration.as_millis() as u64)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
Ok(ErrorKind::LimitExceeded { retry_after_ms })
|
||||
}
|
||||
RumaApiErrorKind::MissingParam => Ok(ErrorKind::MissingParam),
|
||||
RumaApiErrorKind::MissingToken => Ok(ErrorKind::MissingToken),
|
||||
RumaApiErrorKind::NotFound => Ok(ErrorKind::NotFound),
|
||||
RumaApiErrorKind::NotJson => Ok(ErrorKind::NotJson),
|
||||
RumaApiErrorKind::NotYetUploaded => Ok(ErrorKind::NotYetUploaded),
|
||||
RumaApiErrorKind::ResourceLimitExceeded { admin_contact } => {
|
||||
Ok(ErrorKind::ResourceLimitExceeded { admin_contact: admin_contact.to_owned() })
|
||||
}
|
||||
RumaApiErrorKind::RoomInUse => Ok(ErrorKind::RoomInUse),
|
||||
RumaApiErrorKind::ServerNotTrusted => Ok(ErrorKind::ServerNotTrusted),
|
||||
RumaApiErrorKind::ThreepidAuthFailed => Ok(ErrorKind::ThreepidAuthFailed),
|
||||
RumaApiErrorKind::ThreepidDenied => Ok(ErrorKind::ThreepidDenied),
|
||||
RumaApiErrorKind::ThreepidInUse => Ok(ErrorKind::ThreepidInUse),
|
||||
RumaApiErrorKind::ThreepidMediumNotSupported => {
|
||||
Ok(ErrorKind::ThreepidMediumNotSupported)
|
||||
}
|
||||
RumaApiErrorKind::ThreepidNotFound => Ok(ErrorKind::ThreepidNotFound),
|
||||
RumaApiErrorKind::TooLarge => Ok(ErrorKind::TooLarge),
|
||||
RumaApiErrorKind::UnableToAuthorizeJoin => Ok(ErrorKind::UnableToAuthorizeJoin),
|
||||
RumaApiErrorKind::UnableToGrantJoin => Ok(ErrorKind::UnableToGrantJoin),
|
||||
RumaApiErrorKind::Unauthorized => Ok(ErrorKind::Unauthorized),
|
||||
RumaApiErrorKind::Unknown => Ok(ErrorKind::Unknown),
|
||||
RumaApiErrorKind::UnknownToken { soft_logout } => {
|
||||
Ok(ErrorKind::UnknownToken { soft_logout: soft_logout.to_owned() })
|
||||
}
|
||||
RumaApiErrorKind::Unrecognized => Ok(ErrorKind::Unrecognized),
|
||||
RumaApiErrorKind::UnsupportedRoomVersion => Ok(ErrorKind::UnsupportedRoomVersion),
|
||||
RumaApiErrorKind::UrlNotSet => Ok(ErrorKind::UrlNotSet),
|
||||
RumaApiErrorKind::UserDeactivated => Ok(ErrorKind::UserDeactivated),
|
||||
RumaApiErrorKind::UserInUse => Ok(ErrorKind::UserInUse),
|
||||
RumaApiErrorKind::UserLocked => Ok(ErrorKind::UserLocked),
|
||||
RumaApiErrorKind::UserSuspended => Ok(ErrorKind::UserSuspended),
|
||||
RumaApiErrorKind::WeakPassword => Ok(ErrorKind::WeakPassword),
|
||||
RumaApiErrorKind::WrongRoomKeysVersion { current_version } => {
|
||||
Ok(ErrorKind::WrongRoomKeysVersion { current_version: current_version.to_owned() })
|
||||
}
|
||||
RumaApiErrorKind::_Custom { .. } => {
|
||||
// There is no way to map the extra values since they're private, so we omit
|
||||
// them
|
||||
Ok(ErrorKind::Custom { errcode: value.errcode().to_string() })
|
||||
}
|
||||
// In any other case, return it as the mapping not being yet implemented
|
||||
_ => Err(NotYetImplemented),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use ruma::{
|
||||
use crate::{
|
||||
room_member::MembershipState,
|
||||
ruma::{MessageType, NotifyType},
|
||||
utils::Timestamp,
|
||||
ClientError,
|
||||
};
|
||||
|
||||
@@ -33,8 +34,8 @@ impl TimelineEvent {
|
||||
self.0.sender().to_string()
|
||||
}
|
||||
|
||||
pub fn timestamp(&self) -> u64 {
|
||||
self.0.origin_server_ts().0.into()
|
||||
pub fn timestamp(&self) -> Timestamp {
|
||||
self.0.origin_server_ts().into()
|
||||
}
|
||||
|
||||
pub fn event_type(&self) -> Result<TimelineEventType, ClientError> {
|
||||
|
||||
@@ -14,6 +14,7 @@ mod error;
|
||||
mod event;
|
||||
mod helpers;
|
||||
mod identity_status_change;
|
||||
mod live_location_share;
|
||||
mod notification;
|
||||
mod notification_settings;
|
||||
mod platform;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2024 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 crate::ruma::LocationContent;
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct LastLocation {
|
||||
/// The most recent location content of the user.
|
||||
pub location: LocationContent,
|
||||
/// A timestamp in milliseconds since Unix Epoch on that day in local
|
||||
/// time.
|
||||
pub ts: u64,
|
||||
}
|
||||
/// Details of a users live location share.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct LiveLocationShare {
|
||||
/// The user's last known location.
|
||||
pub last_location: LastLocation,
|
||||
/// The live status of the live location share.
|
||||
pub(crate) is_live: bool,
|
||||
/// The user ID of the person sharing their live location.
|
||||
pub user_id: String,
|
||||
}
|
||||
@@ -14,8 +14,11 @@ use tracing_subscriber::{
|
||||
EnvFilter, Layer,
|
||||
};
|
||||
|
||||
use crate::tracing::LogLevel;
|
||||
|
||||
pub fn log_panics() {
|
||||
std::env::set_var("RUST_BACKTRACE", "1");
|
||||
|
||||
log_panics::init();
|
||||
}
|
||||
|
||||
@@ -228,12 +231,76 @@ pub struct TracingFileConfiguration {
|
||||
max_files: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, PartialOrd)]
|
||||
enum LogTarget {
|
||||
Hyper,
|
||||
MatrixSdkFfi,
|
||||
MatrixSdk,
|
||||
MatrixSdkClient,
|
||||
MatrixSdkCrypto,
|
||||
MatrixSdkCryptoAccount,
|
||||
MatrixSdkOidc,
|
||||
MatrixSdkHttpClient,
|
||||
MatrixSdkSlidingSync,
|
||||
MatrixSdkBaseSlidingSync,
|
||||
MatrixSdkUiTimeline,
|
||||
MatrixSdkEventCache,
|
||||
MatrixSdkBaseEventCache,
|
||||
MatrixSdkEventCacheStore,
|
||||
}
|
||||
|
||||
impl LogTarget {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
LogTarget::Hyper => "hyper",
|
||||
LogTarget::MatrixSdkFfi => "matrix_sdk_ffi",
|
||||
LogTarget::MatrixSdk => "matrix_sdk",
|
||||
LogTarget::MatrixSdkClient => "matrix_sdk::client",
|
||||
LogTarget::MatrixSdkCrypto => "matrix_sdk_crypto",
|
||||
LogTarget::MatrixSdkCryptoAccount => "matrix_sdk_crypto::olm::account",
|
||||
LogTarget::MatrixSdkOidc => "matrix_sdk::oidc",
|
||||
LogTarget::MatrixSdkHttpClient => "matrix_sdk::http_client",
|
||||
LogTarget::MatrixSdkSlidingSync => "matrix_sdk::sliding_sync",
|
||||
LogTarget::MatrixSdkBaseSlidingSync => "matrix_sdk_base::sliding_sync",
|
||||
LogTarget::MatrixSdkUiTimeline => "matrix_sdk_ui::timeline",
|
||||
LogTarget::MatrixSdkEventCache => "matrix_sdk::event_cache",
|
||||
LogTarget::MatrixSdkBaseEventCache => "matrix_sdk_base::event_cache",
|
||||
LogTarget::MatrixSdkEventCacheStore => "matrix_sdk_sqlite::event_cache_store",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_TARGET_LOG_LEVELS: &[(LogTarget, LogLevel)] = &[
|
||||
(LogTarget::Hyper, LogLevel::Warn),
|
||||
(LogTarget::MatrixSdkFfi, LogLevel::Info),
|
||||
(LogTarget::MatrixSdk, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkClient, LogLevel::Trace),
|
||||
(LogTarget::MatrixSdkCrypto, LogLevel::Debug),
|
||||
(LogTarget::MatrixSdkCryptoAccount, LogLevel::Trace),
|
||||
(LogTarget::MatrixSdkOidc, LogLevel::Trace),
|
||||
(LogTarget::MatrixSdkHttpClient, LogLevel::Debug),
|
||||
(LogTarget::MatrixSdkSlidingSync, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkBaseSlidingSync, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkUiTimeline, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkEventCache, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkBaseEventCache, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkEventCacheStore, LogLevel::Info),
|
||||
];
|
||||
|
||||
const IMMUTABLE_TARGET_LOG_LEVELS: &[LogTarget] = &[
|
||||
LogTarget::Hyper, // Too verbose
|
||||
LogTarget::MatrixSdk, // Too generic
|
||||
LogTarget::MatrixSdkFfi, // Too verbose
|
||||
];
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct TracingConfiguration {
|
||||
/// A filter line following the [RUST_LOG format].
|
||||
///
|
||||
/// [RUST_LOG format]: https://rust-lang-nursery.github.io/rust-cookbook/development_tools/debugging/config_log.html
|
||||
filter: String,
|
||||
/// The desired log level
|
||||
log_level: LogLevel,
|
||||
|
||||
/// Additional targets that the FFI client would like to use e.g.
|
||||
/// the target names for created [`crate::tracing::Span`]
|
||||
extra_targets: Option<Vec<String>>,
|
||||
|
||||
/// Whether to log to stdout, or in the logcat on Android.
|
||||
write_to_stdout_or_system: bool,
|
||||
@@ -242,12 +309,111 @@ pub struct TracingConfiguration {
|
||||
write_to_files: Option<TracingFileConfiguration>,
|
||||
}
|
||||
|
||||
fn build_tracing_filter(config: &TracingConfiguration) -> String {
|
||||
// We are intentionally not setting a global log level because we don't want to
|
||||
// risk third party crates logging sensitive information.
|
||||
// As such we need to make sure that panics will be properly logged.
|
||||
// On 2025-01-08, `log_panics` uses the `panic` target, at the error log level.
|
||||
let mut filters = vec!["panic=error".to_owned()];
|
||||
|
||||
DEFAULT_TARGET_LOG_LEVELS.iter().for_each(|(target, level)| {
|
||||
// Use the default if the log level shouldn't be changed for this target or
|
||||
// if it's already logging more than requested
|
||||
let level = if IMMUTABLE_TARGET_LOG_LEVELS.contains(target) || level > &config.log_level {
|
||||
level.as_str()
|
||||
} else {
|
||||
config.log_level.as_str()
|
||||
};
|
||||
|
||||
filters.push(format!("{}={}", target.as_str(), level));
|
||||
});
|
||||
|
||||
// Finally append the extra targets requested by the client
|
||||
if let Some(extra_targets) = &config.extra_targets {
|
||||
for target in extra_targets {
|
||||
filters.push(format!("{}={}", target, config.log_level.as_str()));
|
||||
}
|
||||
}
|
||||
|
||||
filters.join(",")
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn setup_tracing(config: TracingConfiguration) {
|
||||
log_panics();
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(EnvFilter::new(&config.filter))
|
||||
.with(EnvFilter::new(build_tracing_filter(&config)))
|
||||
.with(text_layers(config))
|
||||
.init();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::build_tracing_filter;
|
||||
|
||||
#[test]
|
||||
fn test_default_tracing_filter() {
|
||||
let config = super::TracingConfiguration {
|
||||
log_level: super::LogLevel::Error,
|
||||
extra_targets: Some(vec!["super_duper_app".to_owned()]),
|
||||
write_to_stdout_or_system: true,
|
||||
write_to_files: None,
|
||||
};
|
||||
|
||||
let filter = build_tracing_filter(&config);
|
||||
|
||||
assert_eq!(
|
||||
filter,
|
||||
"panic=error,\
|
||||
hyper=warn,\
|
||||
matrix_sdk_ffi=info,\
|
||||
matrix_sdk=info,\
|
||||
matrix_sdk::client=trace,\
|
||||
matrix_sdk_crypto=debug,\
|
||||
matrix_sdk_crypto::olm::account=trace,\
|
||||
matrix_sdk::oidc=trace,\
|
||||
matrix_sdk::http_client=debug,\
|
||||
matrix_sdk::sliding_sync=info,\
|
||||
matrix_sdk_base::sliding_sync=info,\
|
||||
matrix_sdk_ui::timeline=info,\
|
||||
matrix_sdk::event_cache=info,\
|
||||
matrix_sdk_base::event_cache=info,\
|
||||
matrix_sdk_sqlite::event_cache_store=info,\
|
||||
super_duper_app=error"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trace_tracing_filter() {
|
||||
let config = super::TracingConfiguration {
|
||||
log_level: super::LogLevel::Trace,
|
||||
extra_targets: Some(vec!["super_duper_app".to_owned(), "some_other_span".to_owned()]),
|
||||
write_to_stdout_or_system: true,
|
||||
write_to_files: None,
|
||||
};
|
||||
|
||||
let filter = build_tracing_filter(&config);
|
||||
|
||||
assert_eq!(
|
||||
filter,
|
||||
"panic=error,\
|
||||
hyper=warn,\
|
||||
matrix_sdk_ffi=info,\
|
||||
matrix_sdk=info,\
|
||||
matrix_sdk::client=trace,\
|
||||
matrix_sdk_crypto=trace,\
|
||||
matrix_sdk_crypto::olm::account=trace,\
|
||||
matrix_sdk::oidc=trace,\
|
||||
matrix_sdk::http_client=trace,\
|
||||
matrix_sdk::sliding_sync=trace,\
|
||||
matrix_sdk_base::sliding_sync=trace,\
|
||||
matrix_sdk_ui::timeline=trace,\
|
||||
matrix_sdk::event_cache=trace,\
|
||||
matrix_sdk_base::event_cache=trace,\
|
||||
matrix_sdk_sqlite::event_cache_store=trace,\
|
||||
super_duper_app=trace,\
|
||||
some_other_span=trace"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+286
-120
@@ -4,14 +4,13 @@ use anyhow::{Context, Result};
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use matrix_sdk::{
|
||||
crypto::LocalTrust,
|
||||
event_cache::paginator::PaginatorError,
|
||||
room::{
|
||||
edit::EditedContent, power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole,
|
||||
},
|
||||
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType,
|
||||
RoomHero as SdkRoomHero, RoomMemberships, RoomState,
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{default_event_filter, PaginationError, RoomExt, TimelineFocus};
|
||||
use matrix_sdk_ui::timeline::{default_event_filter, RoomExt};
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
api::client::room::report_content,
|
||||
@@ -20,7 +19,8 @@ use ruma::{
|
||||
call::notify,
|
||||
room::{
|
||||
avatar::ImageInfo as RumaAvatarImageInfo,
|
||||
message::RoomMessageEventContentWithoutRelation,
|
||||
history_visibility::HistoryVisibility as RumaHistoryVisibility,
|
||||
join_rules::JoinRule as RumaJoinRule, message::RoomMessageEventContentWithoutRelation,
|
||||
power_levels::RoomPowerLevels as RumaPowerLevels, MediaSource,
|
||||
},
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent, TimelineEventType,
|
||||
@@ -28,18 +28,23 @@ use ruma::{
|
||||
EventId, Int, OwnedDeviceId, OwnedUserId, RoomAliasId, UserId,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::error;
|
||||
use tracing::{error, warn};
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::{
|
||||
chunk_iterator::ChunkIterator,
|
||||
error::{ClientError, MediaInfoError, RoomError},
|
||||
event::{MessageLikeEventType, RoomMessageEventMessageType, StateEventType},
|
||||
client::{JoinRule, RoomVisibility},
|
||||
error::{ClientError, MediaInfoError, NotYetImplemented, RoomError},
|
||||
event::{MessageLikeEventType, StateEventType},
|
||||
identity_status_change::IdentityStatusChange,
|
||||
live_location_share::{LastLocation, LiveLocationShare},
|
||||
room_info::RoomInfo,
|
||||
room_member::RoomMember,
|
||||
ruma::{ImageInfo, Mentions, NotifyType},
|
||||
timeline::{DateDividerMode, FocusEventError, ReceiptType, SendHandle, Timeline},
|
||||
ruma::{ImageInfo, LocationContent, Mentions, NotifyType},
|
||||
timeline::{
|
||||
configuration::{AllowedMessageTypes, TimelineConfiguration},
|
||||
ReceiptType, SendHandle, Timeline,
|
||||
},
|
||||
utils::u64_to_uint,
|
||||
TaskHandle,
|
||||
};
|
||||
@@ -85,10 +90,6 @@ impl Room {
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Room {
|
||||
pub fn id(&self) -> String {
|
||||
self.inner.room_id().to_string()
|
||||
}
|
||||
|
||||
/// Returns the room's name from the state event if available, otherwise
|
||||
/// compute a room name based on the room's nature (DM or not) and number of
|
||||
/// members.
|
||||
@@ -198,115 +199,44 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a timeline focused on the given event.
|
||||
///
|
||||
/// Note: this timeline is independent from that returned with
|
||||
/// [`Self::timeline`], and as such it is not cached.
|
||||
pub async fn timeline_focused_on_event(
|
||||
/// Build a new timeline instance with the given configuration.
|
||||
pub async fn timeline_with_configuration(
|
||||
&self,
|
||||
event_id: String,
|
||||
num_context_events: u16,
|
||||
internal_id_prefix: Option<String>,
|
||||
) -> Result<Arc<Timeline>, FocusEventError> {
|
||||
let parsed_event_id = EventId::parse(&event_id).map_err(|err| {
|
||||
FocusEventError::InvalidEventId { event_id: event_id.clone(), err: err.to_string() }
|
||||
})?;
|
||||
|
||||
let room = &self.inner;
|
||||
|
||||
let mut builder = matrix_sdk_ui::timeline::Timeline::builder(room);
|
||||
|
||||
if let Some(internal_id_prefix) = internal_id_prefix {
|
||||
builder = builder.with_internal_id_prefix(internal_id_prefix);
|
||||
}
|
||||
|
||||
let timeline = match builder
|
||||
.with_focus(TimelineFocus::Event { target: parsed_event_id, num_context_events })
|
||||
.build()
|
||||
.await
|
||||
{
|
||||
Ok(t) => t,
|
||||
Err(err) => {
|
||||
if let matrix_sdk_ui::timeline::Error::PaginationError(
|
||||
PaginationError::Paginator(PaginatorError::EventNotFound(..)),
|
||||
) = err
|
||||
{
|
||||
return Err(FocusEventError::EventNotFound { event_id: event_id.to_string() });
|
||||
}
|
||||
return Err(FocusEventError::Other { msg: err.to_string() });
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Timeline::new(timeline))
|
||||
}
|
||||
|
||||
pub async fn pinned_events_timeline(
|
||||
&self,
|
||||
internal_id_prefix: Option<String>,
|
||||
max_events_to_load: u16,
|
||||
max_concurrent_requests: u16,
|
||||
) -> Result<Arc<Timeline>, ClientError> {
|
||||
let room = &self.inner;
|
||||
|
||||
let mut builder = matrix_sdk_ui::timeline::Timeline::builder(room);
|
||||
|
||||
if let Some(internal_id_prefix) = internal_id_prefix {
|
||||
builder = builder.with_internal_id_prefix(internal_id_prefix);
|
||||
}
|
||||
|
||||
let timeline = builder
|
||||
.with_focus(TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests })
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
Ok(Timeline::new(timeline))
|
||||
}
|
||||
|
||||
/// A timeline instance that can be configured to only include RoomMessage
|
||||
/// type events and filter those further based on their message type.
|
||||
///
|
||||
/// Virtual timeline items will still be provided and the
|
||||
/// `default_event_filter` will be applied before everything else.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `internal_id_prefix` - An optional String that will be prepended to
|
||||
/// all the timeline item's internal IDs, making it possible to
|
||||
/// distinguish different timeline instances from each other.
|
||||
///
|
||||
/// * `allowed_message_types` - A list of `RoomMessageEventMessageType` that
|
||||
/// will be allowed to appear in the timeline
|
||||
pub async fn message_filtered_timeline(
|
||||
&self,
|
||||
internal_id_prefix: Option<String>,
|
||||
allowed_message_types: Vec<RoomMessageEventMessageType>,
|
||||
date_divider_mode: DateDividerMode,
|
||||
configuration: TimelineConfiguration,
|
||||
) -> Result<Arc<Timeline>, ClientError> {
|
||||
let mut builder = matrix_sdk_ui::timeline::Timeline::builder(&self.inner);
|
||||
|
||||
if let Some(internal_id_prefix) = internal_id_prefix {
|
||||
builder = builder.with_focus(configuration.focus.try_into()?);
|
||||
|
||||
if let AllowedMessageTypes::Only { types } = configuration.allowed_message_types {
|
||||
builder = builder.event_filter(move |event, room_version_id| {
|
||||
default_event_filter(event, room_version_id)
|
||||
&& match event {
|
||||
AnySyncTimelineEvent::MessageLike(msg) => match msg.original_content() {
|
||||
Some(AnyMessageLikeEventContent::RoomMessage(content)) => {
|
||||
types.contains(&content.msgtype.into())
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(internal_id_prefix) = configuration.internal_id_prefix {
|
||||
builder = builder.with_internal_id_prefix(internal_id_prefix);
|
||||
}
|
||||
|
||||
builder = builder.with_date_divider_mode(date_divider_mode.into());
|
||||
|
||||
builder = builder.event_filter(move |event, room_version_id| {
|
||||
default_event_filter(event, room_version_id)
|
||||
&& match event {
|
||||
AnySyncTimelineEvent::MessageLike(msg) => match msg.original_content() {
|
||||
Some(AnyMessageLikeEventContent::RoomMessage(content)) => {
|
||||
allowed_message_types.contains(&content.msgtype.into())
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
builder = builder.with_date_divider_mode(configuration.date_divider_mode.into());
|
||||
|
||||
let timeline = builder.build().await?;
|
||||
Ok(Timeline::new(timeline))
|
||||
}
|
||||
|
||||
pub fn id(&self) -> String {
|
||||
self.inner.room_id().to_string()
|
||||
}
|
||||
|
||||
pub fn is_encrypted(&self) -> Result<bool, ClientError> {
|
||||
Ok(RUNTIME.block_on(self.inner.is_encrypted())?)
|
||||
}
|
||||
@@ -345,7 +275,7 @@ impl Room {
|
||||
}
|
||||
|
||||
pub async fn room_info(&self) -> Result<RoomInfo, ClientError> {
|
||||
Ok(RoomInfo::new(&self.inner).await?)
|
||||
RoomInfo::new(&self.inner).await
|
||||
}
|
||||
|
||||
pub fn subscribe_to_room_info_updates(
|
||||
@@ -449,15 +379,12 @@ impl Room {
|
||||
let int_score = score.map(|value| value.into());
|
||||
self.inner
|
||||
.client()
|
||||
.send(
|
||||
report_content::v3::Request::new(
|
||||
self.inner.room_id().into(),
|
||||
event_id,
|
||||
int_score,
|
||||
reason,
|
||||
),
|
||||
None,
|
||||
)
|
||||
.send(report_content::v3::Request::new(
|
||||
self.inner.room_id().into(),
|
||||
event_id,
|
||||
int_score,
|
||||
reason,
|
||||
))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -924,13 +851,15 @@ impl Room {
|
||||
self: Arc<Self>,
|
||||
listener: Box<dyn KnockRequestsListener>,
|
||||
) -> Result<Arc<TaskHandle>, ClientError> {
|
||||
let stream = self.inner.subscribe_to_knock_requests().await?;
|
||||
let (stream, seen_ids_cleanup_handle) = self.inner.subscribe_to_knock_requests().await?;
|
||||
|
||||
let handle = Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
pin_mut!(stream);
|
||||
while let Some(requests) = stream.next().await {
|
||||
listener.call(requests.into_iter().map(Into::into).collect());
|
||||
}
|
||||
// Cancel the seen ids cleanup task
|
||||
seen_ids_cleanup_handle.abort();
|
||||
})));
|
||||
|
||||
Ok(handle)
|
||||
@@ -942,6 +871,184 @@ impl Room {
|
||||
let (cache, _drop_guards) = self.inner.event_cache().await?;
|
||||
Ok(cache.debug_string().await)
|
||||
}
|
||||
|
||||
/// Update the canonical alias of the room.
|
||||
///
|
||||
/// Note that publishing the alias in the room directory is done separately.
|
||||
pub async fn update_canonical_alias(
|
||||
&self,
|
||||
alias: Option<String>,
|
||||
alt_aliases: Vec<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let new_alias = alias.map(TryInto::try_into).transpose()?;
|
||||
let new_alt_aliases =
|
||||
alt_aliases.into_iter().map(RoomAliasId::parse).collect::<Result<_, _>>()?;
|
||||
self.inner
|
||||
.privacy_settings()
|
||||
.update_canonical_alias(new_alias, new_alt_aliases)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Publish a new room alias for this room in the room directory.
|
||||
///
|
||||
/// Returns:
|
||||
/// - `true` if the room alias didn't exist and it's now published.
|
||||
/// - `false` if the room alias was already present so it couldn't be
|
||||
/// published.
|
||||
pub async fn publish_room_alias_in_room_directory(
|
||||
&self,
|
||||
alias: String,
|
||||
) -> Result<bool, ClientError> {
|
||||
let new_alias = RoomAliasId::parse(alias)?;
|
||||
self.inner
|
||||
.privacy_settings()
|
||||
.publish_room_alias_in_room_directory(&new_alias)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Remove an existing room alias for this room in the room directory.
|
||||
///
|
||||
/// Returns:
|
||||
/// - `true` if the room alias was present and it's now removed from the
|
||||
/// room directory.
|
||||
/// - `false` if the room alias didn't exist so it couldn't be removed.
|
||||
pub async fn remove_room_alias_from_room_directory(
|
||||
&self,
|
||||
alias: String,
|
||||
) -> Result<bool, ClientError> {
|
||||
let alias = RoomAliasId::parse(alias)?;
|
||||
self.inner
|
||||
.privacy_settings()
|
||||
.remove_room_alias_from_room_directory(&alias)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Enable End-to-end encryption in this room.
|
||||
pub async fn enable_encryption(&self) -> Result<(), ClientError> {
|
||||
self.inner.enable_encryption().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Update room history visibility for this room.
|
||||
pub async fn update_history_visibility(
|
||||
&self,
|
||||
visibility: RoomHistoryVisibility,
|
||||
) -> Result<(), ClientError> {
|
||||
let visibility: RumaHistoryVisibility = visibility.try_into()?;
|
||||
self.inner
|
||||
.privacy_settings()
|
||||
.update_room_history_visibility(visibility)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Update the join rule for this room.
|
||||
pub async fn update_join_rules(&self, new_rule: JoinRule) -> Result<(), ClientError> {
|
||||
let new_rule: RumaJoinRule = new_rule.try_into()?;
|
||||
self.inner.privacy_settings().update_join_rule(new_rule).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Update the room's visibility in the room directory.
|
||||
pub async fn update_room_visibility(
|
||||
&self,
|
||||
visibility: RoomVisibility,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner
|
||||
.privacy_settings()
|
||||
.update_room_visibility(visibility.into())
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns the visibility for this room in the room directory.
|
||||
///
|
||||
/// [Public](`RoomVisibility::Public`) rooms are listed in the room
|
||||
/// directory and can be found using it.
|
||||
pub async fn get_room_visibility(&self) -> Result<RoomVisibility, ClientError> {
|
||||
let visibility = self.inner.privacy_settings().get_room_visibility().await?;
|
||||
Ok(visibility.into())
|
||||
}
|
||||
|
||||
/// Start the current users live location share in the room.
|
||||
pub async fn start_live_location_share(&self, duration_millis: u64) -> Result<(), ClientError> {
|
||||
self.inner.start_live_location_share(duration_millis, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop the current users live location share in the room.
|
||||
pub async fn stop_live_location_share(&self) -> Result<(), ClientError> {
|
||||
self.inner.stop_live_location_share().await.expect("Unable to stop live location share");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send the current users live location beacon in the room.
|
||||
pub async fn send_live_location(&self, geo_uri: String) -> Result<(), ClientError> {
|
||||
self.inner
|
||||
.send_location_beacon(geo_uri)
|
||||
.await
|
||||
.expect("Unable to send live location beacon");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Subscribes to live location shares in this room, using a `listener` to
|
||||
/// be notified of the changes.
|
||||
///
|
||||
/// The current live location shares will be emitted immediately when
|
||||
/// subscribing, along with a [`TaskHandle`] to cancel the subscription.
|
||||
pub fn subscribe_to_live_location_shares(
|
||||
self: Arc<Self>,
|
||||
listener: Box<dyn LiveLocationShareListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let room = self.inner.clone();
|
||||
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
let subscription = room.observe_live_location_shares();
|
||||
let mut stream = subscription.subscribe();
|
||||
let mut pinned_stream = pin!(stream);
|
||||
|
||||
while let Some(event) = pinned_stream.next().await {
|
||||
let last_location = LocationContent {
|
||||
body: "".to_owned(),
|
||||
geo_uri: event.last_location.location.uri.clone().to_string(),
|
||||
description: None,
|
||||
zoom_level: None,
|
||||
asset: None,
|
||||
};
|
||||
|
||||
let Some(beacon_info) = event.beacon_info else {
|
||||
warn!("Live location share is missing the associated beacon_info state, skipping event.");
|
||||
continue;
|
||||
};
|
||||
|
||||
listener.call(vec![LiveLocationShare {
|
||||
last_location: LastLocation {
|
||||
location: last_location,
|
||||
ts: event.last_location.ts.0.into(),
|
||||
},
|
||||
is_live: beacon_info.is_live(),
|
||||
user_id: event.user_id.to_string(),
|
||||
}])
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Forget this room.
|
||||
///
|
||||
/// This communicates to the homeserver that it should forget the room.
|
||||
///
|
||||
/// Only left or banned-from rooms can be forgotten.
|
||||
pub async fn forget(&self) -> Result<(), ClientError> {
|
||||
self.inner.forget().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A listener for receiving new live location shares in a room.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait LiveLocationShareListener: Sync + Send {
|
||||
fn call(&self, live_location_shares: Vec<LiveLocationShare>);
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::room::knock_requests::KnockRequest> for KnockRequest {
|
||||
@@ -1255,3 +1362,62 @@ impl TryFrom<ComposerDraftType> for SdkComposerDraftType {
|
||||
Ok(draft_type)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum RoomHistoryVisibility {
|
||||
/// Previous events are accessible to newly joined members from the point
|
||||
/// they were invited onwards.
|
||||
///
|
||||
/// Events stop being accessible when the member's state changes to
|
||||
/// something other than *invite* or *join*.
|
||||
Invited,
|
||||
|
||||
/// Previous events are accessible to newly joined members from the point
|
||||
/// they joined the room onwards.
|
||||
/// Events stop being accessible when the member's state changes to
|
||||
/// something other than *join*.
|
||||
Joined,
|
||||
|
||||
/// Previous events are always accessible to newly joined members.
|
||||
///
|
||||
/// All events in the room are accessible, even those sent when the member
|
||||
/// was not a part of the room.
|
||||
Shared,
|
||||
|
||||
/// All events while this is the `HistoryVisibility` value may be shared by
|
||||
/// any participating homeserver with anyone, regardless of whether they
|
||||
/// have ever joined the room.
|
||||
WorldReadable,
|
||||
|
||||
/// A custom visibility value.
|
||||
Custom { value: String },
|
||||
}
|
||||
|
||||
impl TryFrom<RumaHistoryVisibility> for RoomHistoryVisibility {
|
||||
type Error = NotYetImplemented;
|
||||
fn try_from(value: RumaHistoryVisibility) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
RumaHistoryVisibility::Invited => Ok(RoomHistoryVisibility::Invited),
|
||||
RumaHistoryVisibility::Shared => Ok(RoomHistoryVisibility::Shared),
|
||||
RumaHistoryVisibility::WorldReadable => Ok(RoomHistoryVisibility::WorldReadable),
|
||||
RumaHistoryVisibility::Joined => Ok(RoomHistoryVisibility::Joined),
|
||||
RumaHistoryVisibility::_Custom(_) => {
|
||||
Ok(RoomHistoryVisibility::Custom { value: value.to_string() })
|
||||
}
|
||||
_ => Err(NotYetImplemented),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<RoomHistoryVisibility> for RumaHistoryVisibility {
|
||||
type Error = NotYetImplemented;
|
||||
fn try_from(value: RoomHistoryVisibility) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
RoomHistoryVisibility::Invited => Ok(RumaHistoryVisibility::Invited),
|
||||
RoomHistoryVisibility::Shared => Ok(RumaHistoryVisibility::Shared),
|
||||
RoomHistoryVisibility::Joined => Ok(RumaHistoryVisibility::Joined),
|
||||
RoomHistoryVisibility::WorldReadable => Ok(RumaHistoryVisibility::WorldReadable),
|
||||
RoomHistoryVisibility::Custom { .. } => Err(NotYetImplemented),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@ use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
client::JoinRule,
|
||||
error::ClientError,
|
||||
notification_settings::RoomNotificationMode,
|
||||
room::{Membership, RoomHero},
|
||||
room::{Membership, RoomHero, RoomHistoryVisibility},
|
||||
room_member::RoomMember,
|
||||
};
|
||||
|
||||
@@ -60,10 +61,12 @@ pub struct RoomInfo {
|
||||
pinned_event_ids: Vec<String>,
|
||||
/// The join rule for this room, if known.
|
||||
join_rule: Option<JoinRule>,
|
||||
/// The history visibility for this room, if known.
|
||||
history_visibility: RoomHistoryVisibility,
|
||||
}
|
||||
|
||||
impl RoomInfo {
|
||||
pub(crate) async fn new(room: &matrix_sdk::Room) -> matrix_sdk::Result<Self> {
|
||||
pub(crate) async fn new(room: &matrix_sdk::Room) -> Result<Self, ClientError> {
|
||||
let unread_notification_counts = room.unread_notification_counts();
|
||||
|
||||
let power_levels_map = room.users_with_power_levels().await;
|
||||
@@ -128,6 +131,7 @@ impl RoomInfo {
|
||||
num_unread_mentions: room.num_unread_mentions(),
|
||||
pinned_event_ids,
|
||||
join_rule: join_rule.ok(),
|
||||
history_visibility: room.history_visibility_or_default().try_into()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,7 +566,7 @@ impl RoomListItem {
|
||||
}
|
||||
|
||||
async fn room_info(&self) -> Result<RoomInfo, ClientError> {
|
||||
Ok(RoomInfo::new(self.inner.inner_room()).await?)
|
||||
RoomInfo::new(self.inner.inner_room()).await
|
||||
}
|
||||
|
||||
/// The room's current membership state.
|
||||
|
||||
@@ -76,7 +76,7 @@ pub fn matrix_to_user_permalink(user_id: String) -> Result<String, ClientError>
|
||||
Ok(user_id.matrix_to_uri().to_string())
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct RoomMember {
|
||||
pub user_id: String,
|
||||
pub display_name: Option<String>,
|
||||
@@ -87,6 +87,7 @@ pub struct RoomMember {
|
||||
pub normalized_power_level: i64,
|
||||
pub is_ignored: bool,
|
||||
pub suggested_role_for_power_level: RoomMemberRole,
|
||||
pub membership_change_reason: Option<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<SdkRoomMember> for RoomMember {
|
||||
@@ -103,6 +104,7 @@ impl TryFrom<SdkRoomMember> for RoomMember {
|
||||
normalized_power_level: m.normalized_power_level(),
|
||||
is_ignored: m.is_ignored(),
|
||||
suggested_role_for_power_level: m.suggested_role_for_power_level(),
|
||||
membership_change_reason: m.event().reason().map(|s| s.to_owned()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,40 @@ impl RoomPreview {
|
||||
let invite_details = room.invite_details().await.ok()?;
|
||||
invite_details.inviter.and_then(|m| m.try_into().ok())
|
||||
}
|
||||
|
||||
/// Forget the room if we had access to it, and it was left or banned.
|
||||
pub async fn forget(&self) -> Result<(), ClientError> {
|
||||
let room =
|
||||
self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?;
|
||||
room.forget().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the membership details for the current user.
|
||||
pub async fn own_membership_details(&self) -> Option<RoomMembershipDetails> {
|
||||
let room = self.client.get_room(&self.inner.room_id)?;
|
||||
|
||||
let (own_member, sender_member) = match room.own_membership_details().await {
|
||||
Ok(memberships) => memberships,
|
||||
Err(error) => {
|
||||
warn!("Couldn't get membership info: {error}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
Some(RoomMembershipDetails {
|
||||
own_room_member: own_member.try_into().ok()?,
|
||||
sender_room_member: sender_member.and_then(|member| member.try_into().ok()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains the current user's room member info and the optional room member
|
||||
/// info of the sender of the `m.room.member` event that this info represents.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomMembershipDetails {
|
||||
pub own_room_member: RoomMember,
|
||||
pub sender_room_member: Option<RoomMember>,
|
||||
}
|
||||
|
||||
impl RoomPreview {
|
||||
|
||||
@@ -570,6 +570,7 @@ pub struct ImageInfo {
|
||||
pub thumbnail_info: Option<ThumbnailInfo>,
|
||||
pub thumbnail_source: Option<Arc<MediaSource>>,
|
||||
pub blurhash: Option<String>,
|
||||
pub is_animated: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<ImageInfo> for RumaImageInfo {
|
||||
@@ -582,6 +583,7 @@ impl From<ImageInfo> for RumaImageInfo {
|
||||
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
|
||||
blurhash: value.blurhash,
|
||||
is_animated: value.is_animated,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -603,6 +605,7 @@ impl TryFrom<&ImageInfo> for BaseImageInfo {
|
||||
width: Some(width),
|
||||
size: Some(size),
|
||||
blurhash: Some(blurhash),
|
||||
is_animated: value.is_animated,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -859,6 +862,7 @@ impl TryFrom<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
|
||||
.transpose()?
|
||||
.map(Arc::new),
|
||||
blurhash: info.blurhash.clone(),
|
||||
is_animated: info.is_animated,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use ruma::UserId;
|
||||
use tracing::{error, info};
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::error::ClientError;
|
||||
use crate::{error::ClientError, utils::Timestamp};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SessionVerificationEmoji {
|
||||
@@ -46,7 +46,7 @@ pub struct SessionVerificationRequestDetails {
|
||||
device_id: String,
|
||||
display_name: Option<String>,
|
||||
/// First time this device was seen in milliseconds since epoch.
|
||||
first_seen_timestamp: u64,
|
||||
first_seen_timestamp: Timestamp,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
@@ -242,7 +242,7 @@ impl SessionVerificationController {
|
||||
flow_id: request.flow_id().into(),
|
||||
device_id: other_device_data.device_id().into(),
|
||||
display_name: other_device_data.display_name().map(str::to_string),
|
||||
first_seen_timestamp: other_device_data.first_time_seen_ts().get().into(),
|
||||
first_seen_timestamp: other_device_data.first_time_seen_ts().into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ pub enum SyncServiceState {
|
||||
Running,
|
||||
Terminated,
|
||||
Error,
|
||||
Offline,
|
||||
}
|
||||
|
||||
impl From<MatrixSyncServiceState> for SyncServiceState {
|
||||
@@ -47,6 +48,7 @@ impl From<MatrixSyncServiceState> for SyncServiceState {
|
||||
MatrixSyncServiceState::Running => Self::Running,
|
||||
MatrixSyncServiceState::Terminated => Self::Terminated,
|
||||
MatrixSyncServiceState::Error => Self::Error,
|
||||
MatrixSyncServiceState::Offline => Self::Offline,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,11 +74,11 @@ impl SyncService {
|
||||
}
|
||||
|
||||
pub async fn start(&self) {
|
||||
self.inner.start().await;
|
||||
self.inner.start().await
|
||||
}
|
||||
|
||||
pub async fn stop(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.stop().await?)
|
||||
pub async fn stop(&self) {
|
||||
self.inner.stop().await
|
||||
}
|
||||
|
||||
pub fn state(&self, listener: Box<dyn SyncServiceStateObserver>) -> Arc<TaskHandle> {
|
||||
@@ -118,6 +120,13 @@ impl SyncServiceBuilder {
|
||||
Arc::new(Self { client: this.client, builder, utd_hook: this.utd_hook })
|
||||
}
|
||||
|
||||
/// Enable the "offline" mode for the [`SyncService`].
|
||||
pub fn with_offline_mode(self: Arc<Self>) -> Arc<Self> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
let builder = this.builder.with_offline_mode();
|
||||
Arc::new(Self { client: this.client, builder, utd_hook: this.utd_hook })
|
||||
}
|
||||
|
||||
pub async fn with_utd_hook(
|
||||
self: Arc<Self>,
|
||||
delegate: Box<dyn UnableToDecryptDelegate>,
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
use ruma::EventId;
|
||||
|
||||
use super::FocusEventError;
|
||||
use crate::{error::ClientError, event::RoomMessageEventMessageType};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum TimelineFocus {
|
||||
Live,
|
||||
Event { event_id: String, num_context_events: u16 },
|
||||
PinnedEvents { max_events_to_load: u16, max_concurrent_requests: u16 },
|
||||
}
|
||||
|
||||
impl TryFrom<TimelineFocus> for matrix_sdk_ui::timeline::TimelineFocus {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(
|
||||
value: TimelineFocus,
|
||||
) -> Result<matrix_sdk_ui::timeline::TimelineFocus, Self::Error> {
|
||||
match value {
|
||||
TimelineFocus::Live => Ok(Self::Live),
|
||||
TimelineFocus::Event { event_id, num_context_events } => {
|
||||
let parsed_event_id =
|
||||
EventId::parse(&event_id).map_err(|err| FocusEventError::InvalidEventId {
|
||||
event_id: event_id.clone(),
|
||||
err: err.to_string(),
|
||||
})?;
|
||||
|
||||
Ok(Self::Event { target: parsed_event_id, num_context_events })
|
||||
}
|
||||
TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests } => {
|
||||
Ok(Self::PinnedEvents { max_events_to_load, max_concurrent_requests })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes how date dividers get inserted, either in between each day or in
|
||||
/// between each month
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum DateDividerMode {
|
||||
Daily,
|
||||
Monthly,
|
||||
}
|
||||
|
||||
impl From<DateDividerMode> for matrix_sdk_ui::timeline::DateDividerMode {
|
||||
fn from(value: DateDividerMode) -> Self {
|
||||
match value {
|
||||
DateDividerMode::Daily => Self::Daily,
|
||||
DateDividerMode::Monthly => Self::Monthly,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum AllowedMessageTypes {
|
||||
All,
|
||||
Only { types: Vec<RoomMessageEventMessageType> },
|
||||
}
|
||||
|
||||
/// Various options used to configure the timeline's behavior.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `internal_id_prefix` -
|
||||
///
|
||||
/// * `allowed_message_types` -
|
||||
///
|
||||
/// * `date_divider_mode` -
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct TimelineConfiguration {
|
||||
/// What should the timeline focus on?
|
||||
pub focus: TimelineFocus,
|
||||
|
||||
/// A list of [`RoomMessageEventMessageType`] that will be allowed to appear
|
||||
/// in the timeline
|
||||
pub allowed_message_types: AllowedMessageTypes,
|
||||
|
||||
/// An optional String that will be prepended to
|
||||
/// all the timeline item's internal IDs, making it possible to
|
||||
/// distinguish different timeline instances from each other.
|
||||
pub internal_id_prefix: Option<String>,
|
||||
|
||||
/// How often to insert date dividers
|
||||
pub date_divider_mode: DateDividerMode,
|
||||
}
|
||||
@@ -22,6 +22,7 @@ use super::ProfileDetails;
|
||||
use crate::{
|
||||
error::ClientError,
|
||||
ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind},
|
||||
utils::Timestamp,
|
||||
};
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent {
|
||||
@@ -187,7 +188,7 @@ pub enum TimelineItemContent {
|
||||
max_selections: u64,
|
||||
answers: Vec<PollAnswer>,
|
||||
votes: HashMap<String, Vec<String>>,
|
||||
end_time: Option<u64>,
|
||||
end_time: Option<Timestamp>,
|
||||
has_been_edited: bool,
|
||||
},
|
||||
CallInvite,
|
||||
@@ -319,7 +320,7 @@ pub struct Reaction {
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct ReactionSenderData {
|
||||
pub sender_id: String,
|
||||
pub timestamp: u64,
|
||||
pub timestamp: Timestamp,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
@@ -481,7 +482,7 @@ impl From<PollResult> for TimelineItemContent {
|
||||
.map(|i| PollAnswer { id: i.id, text: i.text })
|
||||
.collect(),
|
||||
votes: value.votes,
|
||||
end_time: value.end_time,
|
||||
end_time: value.end_time.map(|t| t.into()),
|
||||
has_been_edited: value.has_been_edited,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@ use as_variant::as_variant;
|
||||
use content::{InReplyToDetails, RepliedToEventDetails};
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{pin_mut, StreamExt as _};
|
||||
#[cfg(doc)]
|
||||
use matrix_sdk::crypto::CollectStrategy;
|
||||
use matrix_sdk::{
|
||||
attachment::{
|
||||
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
|
||||
@@ -63,21 +61,21 @@ use tracing::{error, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use self::content::{Reaction, ReactionSenderData, TimelineItemContent};
|
||||
#[cfg(doc)]
|
||||
use crate::client_builder::ClientBuilder;
|
||||
use crate::{
|
||||
client::ProgressWatcher,
|
||||
error::{ClientError, RoomError},
|
||||
event::EventOrTransactionId,
|
||||
helpers::unwrap_or_clone_arc,
|
||||
ruma::{
|
||||
AssetType, AudioInfo, FileInfo, FormattedBody, ImageInfo, PollKind, ThumbnailInfo,
|
||||
VideoInfo,
|
||||
AssetType, AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, PollKind,
|
||||
ThumbnailInfo, VideoInfo,
|
||||
},
|
||||
task_handle::TaskHandle,
|
||||
utils::Timestamp,
|
||||
RUNTIME,
|
||||
};
|
||||
|
||||
pub mod configuration;
|
||||
mod content;
|
||||
|
||||
pub use content::MessageContent;
|
||||
@@ -101,48 +99,65 @@ impl Timeline {
|
||||
unsafe { Arc::from_raw(Arc::into_raw(inner) as _) }
|
||||
}
|
||||
|
||||
async fn send_attachment(
|
||||
&self,
|
||||
filename: String,
|
||||
fn send_attachment(
|
||||
self: Arc<Self>,
|
||||
params: UploadParameters,
|
||||
attachment_info: AttachmentInfo,
|
||||
mime_type: Option<String>,
|
||||
attachment_config: AttachmentConfig,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
use_send_queue: bool,
|
||||
) -> Result<(), RoomError> {
|
||||
thumbnail: Option<Thumbnail>,
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let mime_str = mime_type.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let mut request = self.inner.send_attachment(filename, mime_type, attachment_config);
|
||||
let formatted_caption = formatted_body_from(
|
||||
params.caption.as_deref(),
|
||||
params.formatted_caption.map(Into::into),
|
||||
);
|
||||
|
||||
if use_send_queue {
|
||||
request = request.use_send_queue();
|
||||
}
|
||||
let attachment_config = AttachmentConfig::new()
|
||||
.thumbnail(thumbnail)
|
||||
.info(attachment_info)
|
||||
.caption(params.caption)
|
||||
.formatted_caption(formatted_caption)
|
||||
.mentions(params.mentions.map(Into::into));
|
||||
|
||||
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 handle = SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
let mut request =
|
||||
self.inner.send_attachment(params.filename, mime_type, attachment_config);
|
||||
|
||||
request.await.map_err(|_| RoomError::FailedSendingAttachment)?;
|
||||
Ok(())
|
||||
if params.use_send_queue {
|
||||
request = request.use_send_queue();
|
||||
}
|
||||
|
||||
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(())
|
||||
}));
|
||||
|
||||
Ok(handle)
|
||||
}
|
||||
}
|
||||
|
||||
fn build_thumbnail_info(
|
||||
thumbnail_url: Option<String>,
|
||||
thumbnail_path: Option<String>,
|
||||
thumbnail_info: Option<ThumbnailInfo>,
|
||||
) -> Result<AttachmentConfig, RoomError> {
|
||||
match (thumbnail_url, thumbnail_info) {
|
||||
(None, None) => Ok(AttachmentConfig::new()),
|
||||
) -> Result<Option<Thumbnail>, RoomError> {
|
||||
match (thumbnail_path, thumbnail_info) {
|
||||
(None, None) => Ok(None),
|
||||
|
||||
(Some(thumbnail_url), Some(thumbnail_info)) => {
|
||||
(Some(thumbnail_path), Some(thumbnail_info)) => {
|
||||
let thumbnail_data =
|
||||
fs::read(thumbnail_url).map_err(|_| RoomError::InvalidThumbnailData)?;
|
||||
fs::read(thumbnail_path).map_err(|_| RoomError::InvalidThumbnailData)?;
|
||||
|
||||
let height = thumbnail_info
|
||||
.height
|
||||
@@ -162,23 +177,42 @@ fn build_thumbnail_info(
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let thumbnail =
|
||||
Thumbnail { data: thumbnail_data, content_type: mime_type, height, width, size };
|
||||
|
||||
Ok(AttachmentConfig::with_thumbnail(thumbnail))
|
||||
Ok(Some(Thumbnail {
|
||||
data: thumbnail_data,
|
||||
content_type: mime_type,
|
||||
height,
|
||||
width,
|
||||
size,
|
||||
}))
|
||||
}
|
||||
|
||||
_ => {
|
||||
warn!("Ignoring thumbnail because either the thumbnail URL or info isn't defined");
|
||||
Ok(AttachmentConfig::new())
|
||||
warn!("Ignoring thumbnail because either the thumbnail path or info isn't defined");
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct UploadParameters {
|
||||
/// Filename (previously called "url") for the media to be sent.
|
||||
filename: String,
|
||||
/// Optional non-formatted caption, for clients that support it.
|
||||
caption: Option<String>,
|
||||
/// Optional HTML-formatted caption, for clients that support it.
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
// Optional intentional mentions to be sent with the media.
|
||||
mentions: Option<Mentions>,
|
||||
/// Should the media be sent with the send queue, or synchronously?
|
||||
///
|
||||
/// Watching progress only works with the synchronous method, at the moment.
|
||||
use_send_queue: bool,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Timeline {
|
||||
pub async fn add_listener(&self, listener: Box<dyn TimelineListener>) -> Arc<TaskHandle> {
|
||||
let (timeline_items, timeline_stream) = self.inner.subscribe_batched().await;
|
||||
let (timeline_items, timeline_stream) = self.inner.subscribe().await;
|
||||
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
pin_mut!(timeline_stream);
|
||||
@@ -233,16 +267,16 @@ impl Timeline {
|
||||
|
||||
/// Paginate backwards, whether we are in focused mode or in live mode.
|
||||
///
|
||||
/// Returns whether we hit the end of the timeline or not.
|
||||
/// Returns whether we hit the start of the timeline or not.
|
||||
pub async fn paginate_backwards(&self, num_events: u16) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.paginate_backwards(num_events).await?)
|
||||
}
|
||||
|
||||
/// Paginate forwards, when in focused mode.
|
||||
/// Paginate forwards, whether we are in focused mode or in live mode.
|
||||
///
|
||||
/// Returns whether we hit the end of the timeline or not.
|
||||
pub async fn focused_paginate_forwards(&self, num_events: u16) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.focused_paginate_forwards(num_events).await?)
|
||||
pub async fn paginate_forwards(&self, num_events: u16) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.paginate_forwards(num_events).await?)
|
||||
}
|
||||
|
||||
pub async fn send_read_receipt(
|
||||
@@ -286,171 +320,83 @@ impl Timeline {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn send_image(
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
thumbnail_url: Option<String>,
|
||||
params: UploadParameters,
|
||||
thumbnail_path: Option<String>,
|
||||
image_info: ImageInfo,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
use_send_queue: bool,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
let formatted_caption =
|
||||
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
let base_image_info = BaseImageInfo::try_from(&image_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
let attachment_info = AttachmentInfo::Image(base_image_info);
|
||||
|
||||
let attachment_config = build_thumbnail_info(thumbnail_url, image_info.thumbnail_info)?
|
||||
.info(attachment_info)
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption);
|
||||
|
||||
self.send_attachment(
|
||||
url,
|
||||
image_info.mimetype,
|
||||
attachment_config,
|
||||
progress_watcher,
|
||||
use_send_queue,
|
||||
)
|
||||
.await
|
||||
}))
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let attachment_info = AttachmentInfo::Image(
|
||||
BaseImageInfo::try_from(&image_info).map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
);
|
||||
let thumbnail = build_thumbnail_info(thumbnail_path, image_info.thumbnail_info)?;
|
||||
self.send_attachment(
|
||||
params,
|
||||
attachment_info,
|
||||
image_info.mimetype,
|
||||
progress_watcher,
|
||||
thumbnail,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn send_video(
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
thumbnail_url: Option<String>,
|
||||
params: UploadParameters,
|
||||
thumbnail_path: Option<String>,
|
||||
video_info: VideoInfo,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
use_send_queue: bool,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
let formatted_caption =
|
||||
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
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 = build_thumbnail_info(thumbnail_url, video_info.thumbnail_info)?
|
||||
.info(attachment_info)
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption.map(Into::into));
|
||||
|
||||
self.send_attachment(
|
||||
url,
|
||||
video_info.mimetype,
|
||||
attachment_config,
|
||||
progress_watcher,
|
||||
use_send_queue,
|
||||
)
|
||||
.await
|
||||
}))
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let attachment_info = AttachmentInfo::Video(
|
||||
BaseVideoInfo::try_from(&video_info).map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
);
|
||||
let thumbnail = build_thumbnail_info(thumbnail_path, video_info.thumbnail_info)?;
|
||||
self.send_attachment(
|
||||
params,
|
||||
attachment_info,
|
||||
video_info.mimetype,
|
||||
progress_watcher,
|
||||
thumbnail,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn send_audio(
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
params: UploadParameters,
|
||||
audio_info: AudioInfo,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
use_send_queue: bool,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
let formatted_caption =
|
||||
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
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)
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption.map(Into::into));
|
||||
|
||||
self.send_attachment(
|
||||
url,
|
||||
audio_info.mimetype,
|
||||
attachment_config,
|
||||
progress_watcher,
|
||||
use_send_queue,
|
||||
)
|
||||
.await
|
||||
}))
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let attachment_info = AttachmentInfo::Audio(
|
||||
BaseAudioInfo::try_from(&audio_info).map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
);
|
||||
self.send_attachment(params, attachment_info, audio_info.mimetype, progress_watcher, None)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn send_voice_message(
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
params: UploadParameters,
|
||||
audio_info: AudioInfo,
|
||||
waveform: Vec<u16>,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
use_send_queue: bool,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
let formatted_caption =
|
||||
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
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)
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption.map(Into::into));
|
||||
|
||||
self.send_attachment(
|
||||
url,
|
||||
audio_info.mimetype,
|
||||
attachment_config,
|
||||
progress_watcher,
|
||||
use_send_queue,
|
||||
)
|
||||
.await
|
||||
}))
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let attachment_info = AttachmentInfo::Voice {
|
||||
audio_info: BaseAudioInfo::try_from(&audio_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
waveform: Some(waveform),
|
||||
};
|
||||
self.send_attachment(params, attachment_info, audio_info.mimetype, progress_watcher, None)
|
||||
}
|
||||
|
||||
pub fn send_file(
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
params: UploadParameters,
|
||||
file_info: FileInfo,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
use_send_queue: bool,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
let formatted_caption =
|
||||
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
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)
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption.map(Into::into));
|
||||
|
||||
self.send_attachment(
|
||||
url,
|
||||
file_info.mimetype,
|
||||
attachment_config,
|
||||
progress_watcher,
|
||||
use_send_queue,
|
||||
)
|
||||
.await
|
||||
}))
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let attachment_info = AttachmentInfo::File(
|
||||
BaseFileInfo::try_from(&file_info).map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
);
|
||||
self.send_attachment(params, attachment_info, file_info.mimetype, progress_watcher, None)
|
||||
}
|
||||
|
||||
pub async fn create_poll(
|
||||
@@ -986,7 +932,7 @@ impl TimelineItem {
|
||||
pub fn as_virtual(self: Arc<Self>) -> Option<VirtualTimelineItem> {
|
||||
use matrix_sdk_ui::timeline::VirtualTimelineItem as VItem;
|
||||
match self.0.as_virtual()? {
|
||||
VItem::DateDivider(ts) => Some(VirtualTimelineItem::DateDivider { ts: ts.0.into() }),
|
||||
VItem::DateDivider(ts) => Some(VirtualTimelineItem::DateDivider { ts: (*ts).into() }),
|
||||
VItem::ReadMarker => Some(VirtualTimelineItem::ReadMarker),
|
||||
}
|
||||
}
|
||||
@@ -1081,9 +1027,10 @@ pub struct EventTimelineItem {
|
||||
is_own: bool,
|
||||
is_editable: bool,
|
||||
content: TimelineItemContent,
|
||||
timestamp: u64,
|
||||
timestamp: Timestamp,
|
||||
reactions: Vec<Reaction>,
|
||||
local_send_state: Option<EventSendState>,
|
||||
local_created_at: Option<u64>,
|
||||
read_receipts: HashMap<String, Receipt>,
|
||||
origin: Option<EventItemOrigin>,
|
||||
can_be_replied_to: bool,
|
||||
@@ -1101,7 +1048,7 @@ impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
|
||||
.into_iter()
|
||||
.map(|(sender_id, info)| ReactionSenderData {
|
||||
sender_id: sender_id.to_string(),
|
||||
timestamp: info.timestamp.0.into(),
|
||||
timestamp: info.timestamp.into(),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
@@ -1118,9 +1065,10 @@ impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
|
||||
is_own: item.is_own(),
|
||||
is_editable: item.is_editable(),
|
||||
content: item.content().clone().into(),
|
||||
timestamp: item.timestamp().0.into(),
|
||||
timestamp: item.timestamp().into(),
|
||||
reactions,
|
||||
local_send_state: item.send_state().map(|s| s.into()),
|
||||
local_created_at: item.local_created_at().map(|t| t.0.into()),
|
||||
read_receipts,
|
||||
origin: item.origin(),
|
||||
can_be_replied_to: item.can_be_replied_to(),
|
||||
@@ -1131,12 +1079,12 @@ impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct Receipt {
|
||||
pub timestamp: Option<u64>,
|
||||
pub timestamp: Option<Timestamp>,
|
||||
}
|
||||
|
||||
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()) }
|
||||
Receipt { timestamp: value.ts.map(|ts| ts.into()) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1260,7 +1208,7 @@ pub enum VirtualTimelineItem {
|
||||
DateDivider {
|
||||
/// A timestamp in milliseconds since Unix Epoch on that day in local
|
||||
/// time.
|
||||
ts: u64,
|
||||
ts: Timestamp,
|
||||
},
|
||||
|
||||
/// The user's own read marker.
|
||||
@@ -1287,22 +1235,32 @@ impl From<ReceiptType> for ruma::api::client::receipt::create_receipt::v3::Recei
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum EditedContent {
|
||||
RoomMessage { content: Arc<RoomMessageEventContentWithoutRelation> },
|
||||
MediaCaption { caption: Option<String>, formatted_caption: Option<FormattedBody> },
|
||||
PollStart { poll_data: PollData },
|
||||
RoomMessage {
|
||||
content: Arc<RoomMessageEventContentWithoutRelation>,
|
||||
},
|
||||
MediaCaption {
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
mentions: Option<Mentions>,
|
||||
},
|
||||
PollStart {
|
||||
poll_data: PollData,
|
||||
},
|
||||
}
|
||||
|
||||
impl TryFrom<EditedContent> for SdkEditedContent {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: EditedContent) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
EditedContent::RoomMessage { content } => {
|
||||
Ok(SdkEditedContent::RoomMessage((*content).clone()))
|
||||
}
|
||||
EditedContent::MediaCaption { caption, formatted_caption } => {
|
||||
EditedContent::MediaCaption { caption, formatted_caption, mentions } => {
|
||||
Ok(SdkEditedContent::MediaCaption {
|
||||
caption,
|
||||
formatted_caption: formatted_caption.map(Into::into),
|
||||
mentions: mentions.map(Into::into),
|
||||
})
|
||||
}
|
||||
EditedContent::PollStart { poll_data } => {
|
||||
@@ -1324,12 +1282,14 @@ impl TryFrom<EditedContent> for SdkEditedContent {
|
||||
fn create_caption_edit(
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
mentions: Option<Mentions>,
|
||||
) -> EditedContent {
|
||||
let formatted_caption =
|
||||
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
|
||||
EditedContent::MediaCaption {
|
||||
caption,
|
||||
formatted_caption: formatted_caption.as_ref().map(Into::into),
|
||||
mentions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1358,21 +1318,8 @@ impl LazyTimelineItemProvider {
|
||||
fn get_send_handle(&self) -> Option<Arc<SendHandle>> {
|
||||
self.0.local_echo_send_handle().map(|handle| Arc::new(SendHandle::new(handle)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes how date dividers get inserted, either in between each day or in
|
||||
/// between each month
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum DateDividerMode {
|
||||
Daily,
|
||||
Monthly,
|
||||
}
|
||||
|
||||
impl From<DateDividerMode> for matrix_sdk_ui::timeline::DateDividerMode {
|
||||
fn from(value: DateDividerMode) -> Self {
|
||||
match value {
|
||||
DateDividerMode::Daily => Self::Daily,
|
||||
DateDividerMode::Monthly => Self::Monthly,
|
||||
}
|
||||
fn contains_only_emojis(&self) -> bool {
|
||||
self.0.contains_only_emojis()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,6 +185,16 @@ impl LogLevel {
|
||||
LogLevel::Trace => tracing::Level::TRACE,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
LogLevel::Error => "error",
|
||||
LogLevel::Warn => "warn",
|
||||
LogLevel::Info => "info",
|
||||
LogLevel::Debug => "debug",
|
||||
LogLevel::Trace => "trace",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
||||
|
||||
@@ -15,9 +15,20 @@
|
||||
use std::{mem::ManuallyDrop, ops::Deref};
|
||||
|
||||
use async_compat::TOKIO1 as RUNTIME;
|
||||
use ruma::UInt;
|
||||
use ruma::{MilliSecondsSinceUnixEpoch, UInt};
|
||||
use tracing::warn;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Timestamp(u64);
|
||||
|
||||
impl From<MilliSecondsSinceUnixEpoch> for Timestamp {
|
||||
fn from(date: MilliSecondsSinceUnixEpoch) -> Self {
|
||||
Self(date.0.into())
|
||||
}
|
||||
}
|
||||
|
||||
uniffi::custom_newtype!(Timestamp, u64);
|
||||
|
||||
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");
|
||||
|
||||
@@ -6,6 +6,38 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
## [0.10.0] - 2025-02-04
|
||||
|
||||
### Features
|
||||
|
||||
- [**breaking**] `EventCacheStore` allows to control which media content is
|
||||
allowed in the media cache, and how long it should be kept, with a
|
||||
`MediaRetentionPolicy`:
|
||||
- `EventCacheStore::add_media_content()` has an extra argument,
|
||||
`ignore_policy`, which decides whether a media content should ignore the
|
||||
`MediaRetentionPolicy`. It should be stored alongside the media content.
|
||||
- `EventCacheStore` has four new methods: `media_retention_policy()`,
|
||||
`set_media_retention_policy()`, `set_ignore_media_retention_policy()` and
|
||||
`clean_up_media_cache()`.
|
||||
- `EventCacheStore` implementations should delegate media cache methods to the
|
||||
methods of the same name of `MediaService` to use the `MediaRetentionPolicy`.
|
||||
They need to implement the `EventCacheStoreMedia` trait that can be tested
|
||||
with the `event_cache_store_media_integration_tests!` macro.
|
||||
([#4571](https://github.com/matrix-org/matrix-rust-sdk/pull/4571))
|
||||
|
||||
### Refactor
|
||||
|
||||
- [**breaking**] Replaced `Room::compute_display_name` with the reintroduced
|
||||
`Room::display_name()`. The new method computes a display name, or return a
|
||||
cached value from the previous successful computation. If you need a sync
|
||||
variant, consider using `Room::cached_display_name()`.
|
||||
([#4470](https://github.com/matrix-org/matrix-rust-sdk/pull/4470))
|
||||
- [**breaking**]: The reexported types `SyncTimelineEvent` and `TimelineEvent`
|
||||
have been fused into a single type `TimelineEvent`, and its field
|
||||
`push_actions` has been made `Option`al (it is set to `None` when we couldn't
|
||||
compute the push actions, because we lacked some information).
|
||||
([#4568](https://github.com/matrix-org/matrix-rust-sdk/pull/4568))
|
||||
|
||||
## [0.9.0] - 2024-12-18
|
||||
|
||||
### Features
|
||||
|
||||
@@ -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.9.0"
|
||||
version = "0.10.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
@@ -21,16 +21,15 @@ e2e-encryption = ["dep:matrix-sdk-crypto"]
|
||||
js = ["matrix-sdk-common/js", "matrix-sdk-crypto?/js", "ruma/js", "matrix-sdk-store-encryption/js"]
|
||||
qrcode = ["matrix-sdk-crypto?/qrcode"]
|
||||
automatic-room-key-forwarding = ["matrix-sdk-crypto?/automatic-room-key-forwarding"]
|
||||
experimental-sliding-sync = [
|
||||
"ruma/unstable-msc3575",
|
||||
"ruma/unstable-msc4186",
|
||||
]
|
||||
uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"]
|
||||
|
||||
# Private feature, see
|
||||
# https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823 for the gory
|
||||
# details.
|
||||
test-send-sync = ["matrix-sdk-crypto?/test-send-sync"]
|
||||
test-send-sync = [
|
||||
"matrix-sdk-common/test-send-sync",
|
||||
"matrix-sdk-crypto?/test-send-sync",
|
||||
]
|
||||
|
||||
# "message-ids" feature doesn't do anything and is deprecated.
|
||||
message-ids = []
|
||||
@@ -49,7 +48,7 @@ as_variant = { workspace = true }
|
||||
assert_matches = { workspace = true, optional = true }
|
||||
assert_matches2 = { workspace = true, optional = true }
|
||||
async-trait = { workspace = true }
|
||||
bitflags = { version = "2.6.0", features = ["serde"] }
|
||||
bitflags = { version = "2.8.0", features = ["serde"] }
|
||||
decancer = "3.2.8"
|
||||
eyeball = { workspace = true, features = ["async-lock"] }
|
||||
eyeball-im = { workspace = true }
|
||||
@@ -62,7 +61,14 @@ matrix-sdk-store-encryption = { workspace = true }
|
||||
matrix-sdk-test = { workspace = true, optional = true }
|
||||
once_cell = { workspace = true }
|
||||
regex = "1.11.1"
|
||||
ruma = { workspace = true, features = ["canonical-json", "unstable-msc3381", "unstable-msc2867", "rand"] }
|
||||
ruma = { workspace = true, features = [
|
||||
"canonical-json",
|
||||
"unstable-msc2867",
|
||||
"unstable-msc3381",
|
||||
"unstable-msc3575",
|
||||
"unstable-msc4186",
|
||||
"rand",
|
||||
] }
|
||||
unicode-normalization = { workspace = true }
|
||||
serde = { workspace = true, features = ["rc"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -63,17 +63,17 @@ use tokio::sync::{broadcast, Mutex};
|
||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
use tracing::{debug, error, info, instrument, trace, warn};
|
||||
|
||||
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use crate::latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use crate::RoomMemberships;
|
||||
use crate::{
|
||||
deserialized_responses::{DisplayName, RawAnySyncOrStrippedTimelineEvent, SyncTimelineEvent},
|
||||
deserialized_responses::{DisplayName, RawAnySyncOrStrippedTimelineEvent, TimelineEvent},
|
||||
error::{Error, Result},
|
||||
event_cache::store::EventCacheStoreLock,
|
||||
response_processors::AccountDataProcessor,
|
||||
rooms::{
|
||||
normal::{RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons},
|
||||
normal::{RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMembersUpdate},
|
||||
Room, RoomInfo, RoomState,
|
||||
},
|
||||
store::{
|
||||
@@ -129,7 +129,7 @@ pub struct BaseClient {
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for BaseClient {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Client")
|
||||
f.debug_struct("BaseClient")
|
||||
.field("session_meta", &self.store.session_meta())
|
||||
.field("sync_token", &self.store.sync_token)
|
||||
.finish_non_exhaustive()
|
||||
@@ -347,9 +347,9 @@ impl BaseClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to decrypt the given raw event into a `SyncTimelineEvent`.
|
||||
/// Attempt to decrypt the given raw event into a [`TimelineEvent`].
|
||||
///
|
||||
/// In the case of a decryption error, returns a `SyncTimelineEvent`
|
||||
/// In the case of a decryption error, returns a [`TimelineEvent`]
|
||||
/// representing the decryption error; in the case of problems with our
|
||||
/// application, returns `Err`.
|
||||
///
|
||||
@@ -359,7 +359,7 @@ impl BaseClient {
|
||||
&self,
|
||||
event: &Raw<AnySyncTimelineEvent>,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Option<SyncTimelineEvent>> {
|
||||
) -> Result<Option<TimelineEvent>> {
|
||||
let olm = self.olm_machine().await;
|
||||
let Some(olm) = olm.as_ref() else { return Ok(None) };
|
||||
|
||||
@@ -372,7 +372,7 @@ impl BaseClient {
|
||||
.await?
|
||||
{
|
||||
RoomEventDecryptionResult::Decrypted(decrypted) => {
|
||||
let event: SyncTimelineEvent = decrypted.into();
|
||||
let event: TimelineEvent = decrypted.into();
|
||||
|
||||
if let Ok(AnySyncTimelineEvent::MessageLike(e)) = event.raw().deserialize() {
|
||||
match &e {
|
||||
@@ -394,7 +394,7 @@ impl BaseClient {
|
||||
event
|
||||
}
|
||||
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
|
||||
SyncTimelineEvent::new_utd_event(event.clone(), utd_info)
|
||||
TimelineEvent::new_utd_event(event.clone(), utd_info)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -423,7 +423,7 @@ impl BaseClient {
|
||||
for raw_event in events {
|
||||
// Start by assuming we have a plaintext event. We'll replace it with a
|
||||
// decrypted or UTD event below if necessary.
|
||||
let mut event = SyncTimelineEvent::new(raw_event);
|
||||
let mut event = TimelineEvent::new(raw_event);
|
||||
|
||||
match event.raw().deserialize() {
|
||||
Ok(e) => {
|
||||
@@ -535,7 +535,7 @@ impl BaseClient {
|
||||
},
|
||||
);
|
||||
}
|
||||
event.push_actions = actions.to_owned();
|
||||
event.push_actions = Some(actions.to_owned());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -766,16 +766,12 @@ impl BaseClient {
|
||||
let (events, room_key_updates) =
|
||||
o.receive_sync_changes(encryption_sync_changes).await?;
|
||||
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
for room_key_update in room_key_updates {
|
||||
if let Some(room) = self.get_room(&room_key_update.room_id) {
|
||||
self.decrypt_latest_events(&room, changes, room_info_notable_updates).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "experimental-sliding-sync"))] // Silence unused variable warnings.
|
||||
let _ = (room_key_updates, changes, room_info_notable_updates);
|
||||
|
||||
Ok(events)
|
||||
} else {
|
||||
// If we have no OlmMachine, just return the events that were passed in.
|
||||
@@ -789,7 +785,7 @@ impl BaseClient {
|
||||
/// that we can and if we can, change latest_event to reflect what we
|
||||
/// found, and remove any older encrypted events from
|
||||
/// latest_encrypted_events.
|
||||
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
async fn decrypt_latest_events(
|
||||
&self,
|
||||
room: &Room,
|
||||
@@ -810,7 +806,7 @@ impl BaseClient {
|
||||
/// (i.e. we can usefully display it as a message preview). Returns the
|
||||
/// 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"))]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
async fn decrypt_latest_suitable_event(
|
||||
&self,
|
||||
room: &Room,
|
||||
@@ -983,6 +979,9 @@ impl BaseClient {
|
||||
let mut new_rooms = RoomUpdates::default();
|
||||
let mut notifications = Default::default();
|
||||
|
||||
let mut updated_members_in_room: BTreeMap<OwnedRoomId, BTreeSet<OwnedUserId>> =
|
||||
BTreeMap::new();
|
||||
|
||||
for (room_id, new_info) in response.rooms.join {
|
||||
let room = self.store.get_or_create_room(
|
||||
&room_id,
|
||||
@@ -1011,6 +1010,8 @@ impl BaseClient {
|
||||
)
|
||||
.await?;
|
||||
|
||||
updated_members_in_room.insert(room_id.to_owned(), user_ids.clone());
|
||||
|
||||
for raw in &new_info.ephemeral.events {
|
||||
match raw.deserialize() {
|
||||
Ok(AnySyncEphemeralRoomEvent::Receipt(event)) => {
|
||||
@@ -1252,6 +1253,13 @@ impl BaseClient {
|
||||
// above. Oh well.
|
||||
new_rooms.update_in_memory_caches(&self.store).await;
|
||||
|
||||
for (room_id, member_ids) in updated_members_in_room {
|
||||
if let Some(room) = self.get_room(&room_id) {
|
||||
let _ =
|
||||
room.room_member_updates_sender.send(RoomMembersUpdate::Partial(member_ids));
|
||||
}
|
||||
}
|
||||
|
||||
info!("Processed a sync response in {:?}", now.elapsed());
|
||||
|
||||
let response = SyncResponse {
|
||||
@@ -1401,6 +1409,8 @@ impl BaseClient {
|
||||
self.store.save_changes(&changes).await?;
|
||||
self.apply_changes(&changes, Default::default());
|
||||
|
||||
let _ = room.room_member_updates_sender.send(RoomMembersUpdate::FullReload);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1506,8 +1516,14 @@ impl BaseClient {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The id of the room that should be forgotten.
|
||||
pub async fn forget_room(&self, room_id: &RoomId) -> StoreResult<()> {
|
||||
self.store.forget_room(room_id).await
|
||||
pub async fn forget_room(&self, room_id: &RoomId) -> Result<()> {
|
||||
// Forget the room in the state store.
|
||||
self.store.forget_room(room_id).await?;
|
||||
|
||||
// Remove the room in the event cache store too.
|
||||
self.event_cache_store().lock().await?.remove_room(room_id).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the olm machine.
|
||||
@@ -1730,10 +1746,13 @@ fn handle_room_member_event_for_profiles(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use matrix_sdk_test::{
|
||||
async_test, ruma_response_from_json, sync_timeline_event, InvitedRoomBuilder,
|
||||
async_test, event_factory::EventFactory, ruma_response_from_json, InvitedRoomBuilder,
|
||||
LeftRoomBuilder, StateTestEvent, StrippedStateTestEvent, SyncResponseBuilder,
|
||||
};
|
||||
use ruma::{api::client as api, room_id, serde::Raw, user_id, UserId};
|
||||
use ruma::{
|
||||
api::client as api, event_id, events::room::member::MembershipState, room_id, serde::Raw,
|
||||
user_id,
|
||||
};
|
||||
use serde_json::{json, value::to_raw_value};
|
||||
|
||||
use super::BaseClient;
|
||||
@@ -1753,17 +1772,15 @@ mod tests {
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
|
||||
let response = sync_builder
|
||||
.add_left_room(LeftRoomBuilder::new(room_id).add_timeline_event(sync_timeline_event!({
|
||||
"content": {
|
||||
"displayname": "Alice",
|
||||
"membership": "left",
|
||||
},
|
||||
"event_id": "$994173582443PhrSn:example.org",
|
||||
"origin_server_ts": 1432135524678u64,
|
||||
"sender": user_id,
|
||||
"state_key": user_id,
|
||||
"type": "m.room.member",
|
||||
})))
|
||||
.add_left_room(
|
||||
LeftRoomBuilder::new(room_id).add_timeline_event(
|
||||
EventFactory::new()
|
||||
.member(user_id)
|
||||
.membership(MembershipState::Leave)
|
||||
.display_name("Alice")
|
||||
.event_id(event_id!("$994173582443PhrSn:example.org")),
|
||||
),
|
||||
)
|
||||
.build_sync_response();
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
|
||||
@@ -1875,18 +1892,37 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_when_there_are_no_latest_encrypted_events_decrypting_them_does_nothing() {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use matrix_sdk_test::event_factory::EventFactory;
|
||||
use ruma::{event_id, events::room::member::MembershipState};
|
||||
|
||||
use crate::{rooms::normal::RoomInfoNotableUpdateReasons, StateChanges};
|
||||
|
||||
// Given a room
|
||||
let user_id = user_id!("@u:u.to");
|
||||
let room_id = room_id!("!r:u.to");
|
||||
let client = logged_in_base_client(Some(user_id)).await;
|
||||
let room = process_room_join_test_helper(&client, room_id, "$1", user_id).await;
|
||||
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
|
||||
let response = sync_builder
|
||||
.add_joined_room(
|
||||
matrix_sdk_test::JoinedRoomBuilder::new(room_id).add_timeline_event(
|
||||
EventFactory::new()
|
||||
.member(user_id)
|
||||
.display_name("Alice")
|
||||
.membership(MembershipState::Join)
|
||||
.event_id(event_id!("$1")),
|
||||
),
|
||||
)
|
||||
.build_sync_response();
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
let room = client.get_room(room_id).expect("Just-created room not found!");
|
||||
|
||||
// Sanity: it has no latest_encrypted_events or latest_event
|
||||
assert!(room.latest_encrypted_events().is_empty());
|
||||
@@ -1908,40 +1944,6 @@ mod tests {
|
||||
.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT));
|
||||
}
|
||||
|
||||
// TODO: I wanted to write more tests here for decrypt_latest_events but I got
|
||||
// lost trying to set up my OlmMachine to be able to encrypt and decrypt
|
||||
// events. In the meantime, there are tests for the most difficult logic
|
||||
// inside Room. --andyb
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
async fn process_room_join_test_helper(
|
||||
client: &BaseClient,
|
||||
room_id: &ruma::RoomId,
|
||||
event_id: &str,
|
||||
user_id: &UserId,
|
||||
) -> crate::Room {
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
let response = sync_builder
|
||||
.add_joined_room(matrix_sdk_test::JoinedRoomBuilder::new(room_id).add_timeline_event(
|
||||
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",
|
||||
}),
|
||||
))
|
||||
.build_sync_response();
|
||||
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
client.get_room(room_id).expect("Just-created room not found!")
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_deserialization_failure() {
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
|
||||
@@ -602,7 +602,7 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_cyrilic() {
|
||||
fn test_display_name_equality_cyrillic() {
|
||||
// Display name with scritpure symbols
|
||||
assert_display_name_eq!("alice", "аlice");
|
||||
}
|
||||
|
||||
@@ -15,10 +15,13 @@
|
||||
|
||||
//! Error conditions.
|
||||
|
||||
use matrix_sdk_common::store_locks::LockStoreError;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_crypto::{CryptoStoreError, MegolmError, OlmError};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::event_cache::store::EventCacheStoreError;
|
||||
|
||||
/// Result type of the rust-sdk.
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
@@ -42,6 +45,14 @@ pub enum Error {
|
||||
#[error(transparent)]
|
||||
StateStore(#[from] crate::store::StoreError),
|
||||
|
||||
/// An error happened while manipulating the event cache store.
|
||||
#[error(transparent)]
|
||||
EventCacheStore(#[from] EventCacheStoreError),
|
||||
|
||||
/// An error happened while attempting to lock the event cache store.
|
||||
#[error(transparent)]
|
||||
EventCacheLock(#[from] LockStoreError),
|
||||
|
||||
/// An error occurred in the crypto store.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[error(transparent)]
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
|
||||
//! Event cache store and common types shared with `matrix_sdk::event_cache`.
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
|
||||
pub mod store;
|
||||
|
||||
/// The kind of event the event storage holds.
|
||||
pub type Event = SyncTimelineEvent;
|
||||
pub type Event = TimelineEvent;
|
||||
|
||||
/// The kind of gap the event storage holds.
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -18,10 +18,13 @@ use assert_matches::assert_matches;
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
deserialized_responses::{
|
||||
AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, TimelineEventKind,
|
||||
AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, TimelineEvent, TimelineEventKind,
|
||||
VerificationState,
|
||||
},
|
||||
linked_chunk::{ChunkContent, LinkedChunk, LinkedChunkBuilder, Position, RawChunk, Update},
|
||||
linked_chunk::{
|
||||
ChunkContent, ChunkIdentifier as CId, LinkedChunk, LinkedChunkBuilder, Position, RawChunk,
|
||||
Update,
|
||||
},
|
||||
};
|
||||
use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID};
|
||||
use ruma::{
|
||||
@@ -29,7 +32,7 @@ use ruma::{
|
||||
push::Action, room_id, uint, RoomId,
|
||||
};
|
||||
|
||||
use super::DynEventCacheStore;
|
||||
use super::{media::IgnoreMediaRetentionPolicy, DynEventCacheStore};
|
||||
use crate::{
|
||||
event_cache::{Event, Gap},
|
||||
media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},
|
||||
@@ -39,7 +42,7 @@ use crate::{
|
||||
/// correctly stores event data.
|
||||
///
|
||||
/// Keep in sync with [`check_test_event`].
|
||||
pub fn make_test_event(room_id: &RoomId, content: &str) -> SyncTimelineEvent {
|
||||
pub fn make_test_event(room_id: &RoomId, content: &str) -> TimelineEvent {
|
||||
let encryption_info = EncryptionInfo {
|
||||
sender: (*ALICE).into(),
|
||||
sender_device: None,
|
||||
@@ -57,13 +60,13 @@ pub fn make_test_event(room_id: &RoomId, content: &str) -> SyncTimelineEvent {
|
||||
.into_raw_timeline()
|
||||
.cast();
|
||||
|
||||
SyncTimelineEvent {
|
||||
TimelineEvent {
|
||||
kind: TimelineEventKind::Decrypted(DecryptedRoomEvent {
|
||||
event,
|
||||
encryption_info,
|
||||
unsigned_encryption_info: None,
|
||||
}),
|
||||
push_actions: vec![Action::Notify],
|
||||
push_actions: Some(vec![Action::Notify]),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,9 +75,9 @@ pub fn make_test_event(room_id: &RoomId, content: &str) -> SyncTimelineEvent {
|
||||
///
|
||||
/// Keep in sync with [`make_test_event`].
|
||||
#[track_caller]
|
||||
pub fn check_test_event(event: &SyncTimelineEvent, text: &str) {
|
||||
pub fn check_test_event(event: &TimelineEvent, text: &str) {
|
||||
// Check push actions.
|
||||
let actions = &event.push_actions;
|
||||
let actions = event.push_actions.as_ref().unwrap();
|
||||
assert_eq!(actions.len(), 1);
|
||||
assert_matches!(&actions[0], Action::Notify);
|
||||
|
||||
@@ -117,6 +120,9 @@ pub trait EventCacheStoreIntegrationTests {
|
||||
|
||||
/// Test that clear all the rooms' linked chunks works.
|
||||
async fn test_clear_all_rooms_chunks(&self);
|
||||
|
||||
/// Test that removing a room from storage empties all associated data.
|
||||
async fn test_remove_room(&self);
|
||||
}
|
||||
|
||||
fn rebuild_linked_chunk(raws: Vec<RawChunk<Event, Gap>>) -> Option<LinkedChunk<3, Event, Gap>> {
|
||||
@@ -162,7 +168,9 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
);
|
||||
|
||||
// Let's add the media.
|
||||
self.add_media_content(&request_file, content.clone()).await.expect("adding media failed");
|
||||
self.add_media_content(&request_file, content.clone(), IgnoreMediaRetentionPolicy::No)
|
||||
.await
|
||||
.expect("adding media failed");
|
||||
|
||||
// Media is present in the cache.
|
||||
assert_eq!(
|
||||
@@ -190,7 +198,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
);
|
||||
|
||||
// Let's add the media again.
|
||||
self.add_media_content(&request_file, content.clone())
|
||||
self.add_media_content(&request_file, content.clone(), IgnoreMediaRetentionPolicy::No)
|
||||
.await
|
||||
.expect("adding media again failed");
|
||||
|
||||
@@ -201,9 +209,13 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
);
|
||||
|
||||
// Let's add the thumbnail media.
|
||||
self.add_media_content(&request_thumbnail, thumbnail_content.clone())
|
||||
.await
|
||||
.expect("adding thumbnail failed");
|
||||
self.add_media_content(
|
||||
&request_thumbnail,
|
||||
thumbnail_content.clone(),
|
||||
IgnoreMediaRetentionPolicy::No,
|
||||
)
|
||||
.await
|
||||
.expect("adding thumbnail failed");
|
||||
|
||||
// Media's thumbnail is present.
|
||||
assert_eq!(
|
||||
@@ -219,9 +231,13 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
);
|
||||
|
||||
// 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");
|
||||
self.add_media_content(
|
||||
&request_other_file,
|
||||
other_content.clone(),
|
||||
IgnoreMediaRetentionPolicy::No,
|
||||
)
|
||||
.await
|
||||
.expect("adding other media failed");
|
||||
|
||||
// Other file is present.
|
||||
assert_eq!(
|
||||
@@ -273,7 +289,9 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
assert!(self.get_media_content(&req).await.unwrap().is_none(), "unexpected media found");
|
||||
|
||||
// Add the media.
|
||||
self.add_media_content(&req, content.clone()).await.expect("adding media failed");
|
||||
self.add_media_content(&req, content.clone(), IgnoreMediaRetentionPolicy::No)
|
||||
.await
|
||||
.expect("adding media failed");
|
||||
|
||||
// Sanity-check: media is found after adding it.
|
||||
assert_eq!(self.get_media_content(&req).await.unwrap().unwrap(), b"hello");
|
||||
@@ -299,8 +317,6 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
}
|
||||
|
||||
async fn test_handle_updates_and_rebuild_linked_chunk(&self) {
|
||||
use matrix_sdk_common::linked_chunk::ChunkIdentifier as CId;
|
||||
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
@@ -383,8 +399,6 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
}
|
||||
|
||||
async fn test_clear_all_rooms_chunks(&self) {
|
||||
use matrix_sdk_common::linked_chunk::ChunkIdentifier as CId;
|
||||
|
||||
let r0 = room_id!("!r0:matrix.org");
|
||||
let r1 = room_id!("!r1:matrix.org");
|
||||
|
||||
@@ -440,6 +454,54 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r0).await.unwrap()).is_none());
|
||||
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r1).await.unwrap()).is_none());
|
||||
}
|
||||
|
||||
async fn test_remove_room(&self) {
|
||||
let r0 = room_id!("!r0:matrix.org");
|
||||
let r1 = room_id!("!r1:matrix.org");
|
||||
|
||||
// Add updates to the first room.
|
||||
self.handle_linked_chunk_updates(
|
||||
r0,
|
||||
vec![
|
||||
// new chunk
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![make_test_event(r0, "hello"), make_test_event(r0, "world")],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Add updates to the second room.
|
||||
self.handle_linked_chunk_updates(
|
||||
r1,
|
||||
vec![
|
||||
// new chunk
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![make_test_event(r0, "yummy")],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Try to remove content from r0.
|
||||
self.remove_room(r0).await.unwrap();
|
||||
|
||||
// Check that r0 doesn't have a linked chunk anymore.
|
||||
let r0_linked_chunk = self.reload_linked_chunk(r0).await.unwrap();
|
||||
assert!(r0_linked_chunk.is_empty());
|
||||
|
||||
// Check that r1 is unaffected.
|
||||
let r1_linked_chunk = self.reload_linked_chunk(r1).await.unwrap();
|
||||
assert!(!r1_linked_chunk.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
/// Macro building to allow your `EventCacheStore` implementation to run the
|
||||
@@ -515,6 +577,13 @@ macro_rules! event_cache_store_integration_tests {
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_clear_all_rooms_chunks().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_remove_room() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_remove_room().await;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,330 @@
|
||||
// Copyright 2025 Kévin Commaille
|
||||
//
|
||||
// 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.
|
||||
|
||||
//! Configuration to decide whether or not to keep media in the cache, allowing
|
||||
//! to do periodic cleanups to avoid to have the size of the media cache grow
|
||||
//! indefinitely.
|
||||
//!
|
||||
//! To proceed to a cleanup, first set the [`MediaRetentionPolicy`] to use with
|
||||
//! [`EventCacheStore::set_media_retention_policy()`]. Then call
|
||||
//! [`EventCacheStore::clean_up_media_cache()`].
|
||||
//!
|
||||
//! In the future, other settings will allow to run automatic periodic cleanup
|
||||
//! jobs.
|
||||
//!
|
||||
//! [`EventCacheStore::set_media_retention_policy()`]: crate::event_cache::store::EventCacheStore::set_media_retention_policy
|
||||
//! [`EventCacheStore::clean_up_media_cache()`]: crate::event_cache::store::EventCacheStore::clean_up_media_cache
|
||||
|
||||
use ruma::time::{Duration, SystemTime};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The retention policy for media content used by the [`EventCacheStore`].
|
||||
///
|
||||
/// [`EventCacheStore`]: crate::event_cache::store::EventCacheStore
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[non_exhaustive]
|
||||
pub struct MediaRetentionPolicy {
|
||||
/// The maximum authorized size of the overall media cache, in bytes.
|
||||
///
|
||||
/// The cache size is defined as the sum of the sizes of all the (possibly
|
||||
/// encrypted) media contents in the cache, excluding any metadata
|
||||
/// associated with them.
|
||||
///
|
||||
/// If this is set and the cache size is bigger than this value, the oldest
|
||||
/// media contents in the cache will be removed during a cleanup until the
|
||||
/// cache size is below this threshold.
|
||||
///
|
||||
/// Note that it is possible for the cache size to temporarily exceed this
|
||||
/// value between two cleanups.
|
||||
///
|
||||
/// Defaults to 400 MiB.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_cache_size: Option<usize>,
|
||||
|
||||
/// The maximum authorized size of a single media content, in bytes.
|
||||
///
|
||||
/// The size of a media content is the size taken by the content in the
|
||||
/// database, after it was possibly encrypted, so it might differ from the
|
||||
/// initial size of the content.
|
||||
///
|
||||
/// The maximum authorized size of a single media content is actually the
|
||||
/// lowest value between `max_cache_size` and `max_file_size`.
|
||||
///
|
||||
/// If it is set, media content bigger than the maximum size will not be
|
||||
/// cached. If the maximum size changed after media content that exceeds the
|
||||
/// new value was cached, the corresponding content will be removed
|
||||
/// during a cleanup.
|
||||
///
|
||||
/// Defaults to 20 MiB.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_file_size: Option<usize>,
|
||||
|
||||
/// The duration after which unaccessed media content is considered
|
||||
/// expired.
|
||||
///
|
||||
/// If this is set, media content whose last access is older than this
|
||||
/// duration will be removed from the media cache during a cleanup.
|
||||
///
|
||||
/// Defaults to 60 days.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub last_access_expiry: Option<Duration>,
|
||||
}
|
||||
|
||||
impl MediaRetentionPolicy {
|
||||
/// Create a [`MediaRetentionPolicy`] with the default values.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Create an empty [`MediaRetentionPolicy`].
|
||||
///
|
||||
/// This means that all media will be cached and cleanups have no effect.
|
||||
pub fn empty() -> Self {
|
||||
Self { max_cache_size: None, max_file_size: None, last_access_expiry: None }
|
||||
}
|
||||
|
||||
/// Set the maximum authorized size of the overall media cache, in bytes.
|
||||
pub fn with_max_cache_size(mut self, size: Option<usize>) -> Self {
|
||||
self.max_cache_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the maximum authorized size of a single media content, in bytes.
|
||||
pub fn with_max_file_size(mut self, size: Option<usize>) -> Self {
|
||||
self.max_file_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the duration before which unaccessed media content is considered
|
||||
/// expired.
|
||||
pub fn with_last_access_expiry(mut self, duration: Option<Duration>) -> Self {
|
||||
self.last_access_expiry = duration;
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether this policy has limitations.
|
||||
///
|
||||
/// If this policy has no limitations, a cleanup job would have no effect.
|
||||
///
|
||||
/// Returns `true` if at least one limitation is set.
|
||||
pub fn has_limitations(&self) -> bool {
|
||||
self.max_cache_size.is_some()
|
||||
|| self.max_file_size.is_some()
|
||||
|| self.last_access_expiry.is_some()
|
||||
}
|
||||
|
||||
/// Whether the given size exceeds the maximum authorized size of the media
|
||||
/// cache.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `size` - The overall size of the media cache to check, in bytes.
|
||||
pub fn exceeds_max_cache_size(&self, size: usize) -> bool {
|
||||
self.max_cache_size.is_some_and(|max_size| size > max_size)
|
||||
}
|
||||
|
||||
/// The computed maximum authorized size of a single media content, in
|
||||
/// bytes.
|
||||
///
|
||||
/// This is the lowest value between `max_cache_size` and `max_file_size`.
|
||||
pub fn computed_max_file_size(&self) -> Option<usize> {
|
||||
match (self.max_cache_size, self.max_file_size) {
|
||||
(None, None) => None,
|
||||
(None, Some(size)) => Some(size),
|
||||
(Some(size), None) => Some(size),
|
||||
(Some(max_cache_size), Some(max_file_size)) => Some(max_cache_size.min(max_file_size)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the given size, in bytes, exceeds the computed maximum
|
||||
/// authorized size of a single media content.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `size` - The size of the media content to check, in bytes.
|
||||
pub fn exceeds_max_file_size(&self, size: usize) -> bool {
|
||||
self.computed_max_file_size().is_some_and(|max_size| size > max_size)
|
||||
}
|
||||
|
||||
/// Whether a content whose last access was at the given time has expired.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `current_time` - The current time.
|
||||
///
|
||||
/// * `last_access_time` - The time when the media content to check was last
|
||||
/// accessed.
|
||||
pub fn has_content_expired(
|
||||
&self,
|
||||
current_time: SystemTime,
|
||||
last_access_time: SystemTime,
|
||||
) -> bool {
|
||||
self.last_access_expiry.is_some_and(|max_duration| {
|
||||
current_time
|
||||
.duration_since(last_access_time)
|
||||
// If this returns an error, the last access time is newer than the current time.
|
||||
// This shouldn't happen but in this case the content cannot be expired.
|
||||
.is_ok_and(|elapsed| elapsed >= max_duration)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MediaRetentionPolicy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
// 400 MiB.
|
||||
max_cache_size: Some(400 * 1024 * 1024),
|
||||
// 20 MiB.
|
||||
max_file_size: Some(20 * 1024 * 1024),
|
||||
// 60 days.
|
||||
last_access_expiry: Some(Duration::from_secs(60 * 24 * 60 * 60)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruma::time::{Duration, SystemTime};
|
||||
|
||||
use super::MediaRetentionPolicy;
|
||||
|
||||
#[test]
|
||||
fn test_media_retention_policy_has_limitations() {
|
||||
let mut policy = MediaRetentionPolicy::empty();
|
||||
assert!(!policy.has_limitations());
|
||||
|
||||
policy = policy.with_last_access_expiry(Some(Duration::from_secs(60)));
|
||||
assert!(policy.has_limitations());
|
||||
|
||||
policy = policy.with_last_access_expiry(None);
|
||||
assert!(!policy.has_limitations());
|
||||
|
||||
policy = policy.with_max_cache_size(Some(1_024));
|
||||
assert!(policy.has_limitations());
|
||||
|
||||
policy = policy.with_max_cache_size(None);
|
||||
assert!(!policy.has_limitations());
|
||||
|
||||
policy = policy.with_max_file_size(Some(1_024));
|
||||
assert!(policy.has_limitations());
|
||||
|
||||
policy = policy.with_max_file_size(None);
|
||||
assert!(!policy.has_limitations());
|
||||
|
||||
// With default values.
|
||||
assert!(MediaRetentionPolicy::new().has_limitations());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_media_retention_policy_max_cache_size() {
|
||||
let file_size = 2_048;
|
||||
|
||||
let mut policy = MediaRetentionPolicy::empty();
|
||||
assert!(!policy.exceeds_max_cache_size(file_size));
|
||||
assert_eq!(policy.computed_max_file_size(), None);
|
||||
assert!(!policy.exceeds_max_file_size(file_size));
|
||||
|
||||
policy = policy.with_max_cache_size(Some(4_096));
|
||||
assert!(!policy.exceeds_max_cache_size(file_size));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(4_096));
|
||||
assert!(!policy.exceeds_max_file_size(file_size));
|
||||
|
||||
policy = policy.with_max_cache_size(Some(2_048));
|
||||
assert!(!policy.exceeds_max_cache_size(file_size));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(2_048));
|
||||
assert!(!policy.exceeds_max_file_size(file_size));
|
||||
|
||||
policy = policy.with_max_cache_size(Some(1_024));
|
||||
assert!(policy.exceeds_max_cache_size(file_size));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(1_024));
|
||||
assert!(policy.exceeds_max_file_size(file_size));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_media_retention_policy_max_file_size() {
|
||||
let file_size = 2_048;
|
||||
|
||||
let mut policy = MediaRetentionPolicy::empty();
|
||||
assert_eq!(policy.computed_max_file_size(), None);
|
||||
assert!(!policy.exceeds_max_file_size(file_size));
|
||||
|
||||
// With max_file_size only.
|
||||
policy = policy.with_max_file_size(Some(4_096));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(4_096));
|
||||
assert!(!policy.exceeds_max_file_size(file_size));
|
||||
|
||||
policy = policy.with_max_file_size(Some(2_048));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(2_048));
|
||||
assert!(!policy.exceeds_max_file_size(file_size));
|
||||
|
||||
policy = policy.with_max_file_size(Some(1_024));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(1_024));
|
||||
assert!(policy.exceeds_max_file_size(file_size));
|
||||
|
||||
// With max_cache_size as well.
|
||||
policy = policy.with_max_cache_size(Some(2_048));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(1_024));
|
||||
assert!(policy.exceeds_max_file_size(file_size));
|
||||
|
||||
policy = policy.with_max_file_size(Some(2_048));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(2_048));
|
||||
assert!(!policy.exceeds_max_file_size(file_size));
|
||||
|
||||
policy = policy.with_max_file_size(Some(4_096));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(2_048));
|
||||
assert!(!policy.exceeds_max_file_size(file_size));
|
||||
|
||||
policy = policy.with_max_cache_size(Some(1_024));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(1_024));
|
||||
assert!(policy.exceeds_max_file_size(file_size));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_media_retention_policy_has_content_expired() {
|
||||
let epoch = SystemTime::UNIX_EPOCH;
|
||||
let last_access_time = epoch + Duration::from_secs(30);
|
||||
let epoch_plus_60 = epoch + Duration::from_secs(60);
|
||||
let epoch_plus_120 = epoch + Duration::from_secs(120);
|
||||
|
||||
let mut policy = MediaRetentionPolicy::empty();
|
||||
assert!(!policy.has_content_expired(epoch, last_access_time));
|
||||
assert!(!policy.has_content_expired(last_access_time, last_access_time));
|
||||
assert!(!policy.has_content_expired(epoch_plus_60, last_access_time));
|
||||
assert!(!policy.has_content_expired(epoch_plus_120, last_access_time));
|
||||
|
||||
policy = policy.with_last_access_expiry(Some(Duration::from_secs(120)));
|
||||
assert!(!policy.has_content_expired(epoch, last_access_time));
|
||||
assert!(!policy.has_content_expired(last_access_time, last_access_time));
|
||||
assert!(!policy.has_content_expired(epoch_plus_60, last_access_time));
|
||||
assert!(!policy.has_content_expired(epoch_plus_120, last_access_time));
|
||||
|
||||
policy = policy.with_last_access_expiry(Some(Duration::from_secs(60)));
|
||||
assert!(!policy.has_content_expired(epoch, last_access_time));
|
||||
assert!(!policy.has_content_expired(last_access_time, last_access_time));
|
||||
assert!(!policy.has_content_expired(epoch_plus_60, last_access_time));
|
||||
assert!(policy.has_content_expired(epoch_plus_120, last_access_time));
|
||||
|
||||
policy = policy.with_last_access_expiry(Some(Duration::from_secs(30)));
|
||||
assert!(!policy.has_content_expired(epoch, last_access_time));
|
||||
assert!(!policy.has_content_expired(last_access_time, last_access_time));
|
||||
assert!(policy.has_content_expired(epoch_plus_60, last_access_time));
|
||||
assert!(policy.has_content_expired(epoch_plus_120, last_access_time));
|
||||
|
||||
policy = policy.with_last_access_expiry(Some(Duration::from_secs(0)));
|
||||
assert!(!policy.has_content_expired(epoch, last_access_time));
|
||||
assert!(policy.has_content_expired(last_access_time, last_access_time));
|
||||
assert!(policy.has_content_expired(epoch_plus_60, last_access_time));
|
||||
assert!(policy.has_content_expired(epoch_plus_120, last_access_time));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,884 @@
|
||||
// Copyright 2025 Kévin Commaille
|
||||
//
|
||||
// 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::fmt;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{locks::Mutex, AsyncTraitDeps};
|
||||
use ruma::{time::SystemTime, MxcUri};
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
|
||||
use super::MediaRetentionPolicy;
|
||||
use crate::{event_cache::store::EventCacheStoreError, media::MediaRequestParameters};
|
||||
|
||||
/// API for implementors of [`EventCacheStore`] to manage their media through
|
||||
/// their implementation of [`EventCacheStoreMedia`].
|
||||
///
|
||||
/// [`EventCacheStore`]: crate::event_cache::store::EventCacheStore
|
||||
#[derive(Debug)]
|
||||
pub struct MediaService<Time: TimeProvider = DefaultTimeProvider> {
|
||||
/// The time provider.
|
||||
time_provider: Time,
|
||||
|
||||
/// The current [`MediaRetentionPolicy`].
|
||||
policy: Mutex<MediaRetentionPolicy>,
|
||||
|
||||
/// A mutex to ensure a single cleanup is running at a time.
|
||||
cleanup_guard: AsyncMutex<()>,
|
||||
}
|
||||
|
||||
impl MediaService {
|
||||
/// Construct a new default `MediaService`.
|
||||
///
|
||||
/// [`MediaService::restore()`] should be called after constructing the
|
||||
/// `MediaService` to restore its previous state.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MediaService {
|
||||
fn default() -> Self {
|
||||
Self::with_time_provider(DefaultTimeProvider)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Time> MediaService<Time>
|
||||
where
|
||||
Time: TimeProvider,
|
||||
{
|
||||
/// Construct a new `MediaService` with the given `TimeProvider` and an
|
||||
/// empty `MediaRetentionPolicy`.
|
||||
fn with_time_provider(time_provider: Time) -> Self {
|
||||
Self {
|
||||
time_provider,
|
||||
policy: Mutex::new(MediaRetentionPolicy::empty()),
|
||||
cleanup_guard: AsyncMutex::new(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore the previous state of the [`MediaRetentionPolicy`] from data
|
||||
/// that was persisted in the store.
|
||||
///
|
||||
/// This should be called immediately after constructing the `MediaService`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `policy` - The `MediaRetentionPolicy` that was persisted in the store.
|
||||
pub fn restore(&self, policy: Option<MediaRetentionPolicy>) {
|
||||
if let Some(policy) = policy {
|
||||
*self.policy.lock() = policy;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the `MediaRetentionPolicy` of this service.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
///
|
||||
/// * `policy` - The `MediaRetentionPolicy` to use.
|
||||
pub async fn set_media_retention_policy<Store: EventCacheStoreMedia>(
|
||||
&self,
|
||||
store: &Store,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Store::Error> {
|
||||
store.set_media_retention_policy_inner(policy).await?;
|
||||
|
||||
*self.policy.lock() = policy;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the `MediaRetentionPolicy` of this service.
|
||||
pub fn media_retention_policy(&self) -> MediaRetentionPolicy {
|
||||
*self.policy.lock()
|
||||
}
|
||||
|
||||
/// Add a media file's content in the media store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
///
|
||||
/// * `content` - The content of the file.
|
||||
///
|
||||
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
|
||||
/// ignored.
|
||||
pub async fn add_media_content<Store: EventCacheStoreMedia>(
|
||||
&self,
|
||||
store: &Store,
|
||||
request: &MediaRequestParameters,
|
||||
content: Vec<u8>,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Store::Error> {
|
||||
let policy = self.media_retention_policy();
|
||||
|
||||
if ignore_policy == IgnoreMediaRetentionPolicy::No
|
||||
&& policy.exceeds_max_file_size(content.len())
|
||||
{
|
||||
// We do not cache the content.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
store
|
||||
.add_media_content_inner(
|
||||
request,
|
||||
content,
|
||||
self.time_provider.now(),
|
||||
policy,
|
||||
ignore_policy,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Set whether the current [`MediaRetentionPolicy`] should be ignored for
|
||||
/// the media.
|
||||
///
|
||||
/// The change will be taken into account in the next cleanup.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
///
|
||||
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
|
||||
/// ignored.
|
||||
pub async fn set_ignore_media_retention_policy<Store: EventCacheStoreMedia>(
|
||||
&self,
|
||||
store: &Store,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Store::Error> {
|
||||
store.set_ignore_media_retention_policy_inner(request, ignore_policy).await
|
||||
}
|
||||
|
||||
/// Get a media file's content out of the media store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
pub async fn get_media_content<Store: EventCacheStoreMedia>(
|
||||
&self,
|
||||
store: &Store,
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<Option<Vec<u8>>, Store::Error> {
|
||||
store.get_media_content_inner(request, self.time_provider.now()).await
|
||||
}
|
||||
|
||||
/// Get a media file's content associated to an `MxcUri` from the
|
||||
/// media store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
///
|
||||
/// * `uri` - The `MxcUri` of the media file.
|
||||
pub async fn get_media_content_for_uri<Store: EventCacheStoreMedia>(
|
||||
&self,
|
||||
store: &Store,
|
||||
uri: &MxcUri,
|
||||
) -> Result<Option<Vec<u8>>, Store::Error> {
|
||||
store.get_media_content_for_uri_inner(uri, self.time_provider.now()).await
|
||||
}
|
||||
|
||||
/// Clean up the media cache with the current `MediaRetentionPolicy`.
|
||||
///
|
||||
/// If there is already an ongoing cleanup, this is a noop.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
pub async fn clean_up_media_cache<Store: EventCacheStoreMedia>(
|
||||
&self,
|
||||
store: &Store,
|
||||
) -> Result<(), Store::Error> {
|
||||
let Ok(_guard) = self.cleanup_guard.try_lock() else {
|
||||
// There is another ongoing cleanup.
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let policy = self.media_retention_policy();
|
||||
|
||||
if !policy.has_limitations() {
|
||||
// No need to call the backend.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
store.clean_up_media_cache_inner(policy, self.time_provider.now()).await
|
||||
}
|
||||
}
|
||||
|
||||
/// An abstract trait that can be used to implement different store backends
|
||||
/// for the media cache of the SDK.
|
||||
///
|
||||
/// The main purposes of this trait are to be able to centralize where we handle
|
||||
/// [`MediaRetentionPolicy`] by wrapping this in a [`MediaService`], and to
|
||||
/// simplify the implementation of tests by being able to have complete control
|
||||
/// over the `SystemTime`s provided to the store.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait EventCacheStoreMedia: AsyncTraitDeps {
|
||||
/// The error type used by this media cache store.
|
||||
type Error: fmt::Debug + Into<EventCacheStoreError>;
|
||||
|
||||
/// The persisted media retention policy in the media cache.
|
||||
async fn media_retention_policy_inner(
|
||||
&self,
|
||||
) -> Result<Option<MediaRetentionPolicy>, Self::Error>;
|
||||
|
||||
/// Persist the media retention policy in the media cache.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `policy` - The `MediaRetentionPolicy` to persist.
|
||||
async fn set_media_retention_policy_inner(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Add a media file's content in the media cache.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
///
|
||||
/// * `content` - The content of the file.
|
||||
///
|
||||
/// * `current_time` - The current time, to set the last access time of the
|
||||
/// media.
|
||||
///
|
||||
/// * `policy` - The media retention policy, to check whether the media is
|
||||
/// too big to be cached.
|
||||
///
|
||||
/// * `ignore_policy` - Whether the `MediaRetentionPolicy` should be ignored
|
||||
/// for this media. This setting should be persisted alongside the media
|
||||
/// and taken into account whenever the policy is used.
|
||||
async fn add_media_content_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
content: Vec<u8>,
|
||||
current_time: SystemTime,
|
||||
policy: MediaRetentionPolicy,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Set whether the current [`MediaRetentionPolicy`] should be ignored for
|
||||
/// the media.
|
||||
///
|
||||
/// If the media of the given request is not found, this should be a noop.
|
||||
///
|
||||
/// The change will be taken into account in the next cleanup.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
///
|
||||
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
|
||||
/// ignored.
|
||||
async fn set_ignore_media_retention_policy_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Get a media file's content out of the media cache.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
///
|
||||
/// * `current_time` - The current time, to update the last access time of
|
||||
/// the media.
|
||||
async fn get_media_content_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
current_time: SystemTime,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error>;
|
||||
|
||||
/// Get a media file's content associated to an `MxcUri` from the
|
||||
/// media store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `uri` - The `MxcUri` of the media file.
|
||||
///
|
||||
/// * `current_time` - The current time, to update the last access time of
|
||||
/// the media.
|
||||
async fn get_media_content_for_uri_inner(
|
||||
&self,
|
||||
uri: &MxcUri,
|
||||
current_time: SystemTime,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error>;
|
||||
|
||||
/// Clean up the media cache with the given policy.
|
||||
///
|
||||
/// For the integration tests, it is expected that content that does not
|
||||
/// pass the last access expiry and max file size criteria will be
|
||||
/// removed first. After that, the remaining cache size should be
|
||||
/// computed to compare against the max cache size criteria.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `policy` - The media retention policy to use for the cleanup. The
|
||||
/// `cleanup_frequency` will be ignored.
|
||||
///
|
||||
/// * `current_time` - The current time, to be used to check for expired
|
||||
/// content.
|
||||
async fn clean_up_media_cache_inner(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
current_time: SystemTime,
|
||||
) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
/// Whether the [`MediaRetentionPolicy`] should be ignored for the current
|
||||
/// content.
|
||||
///
|
||||
/// Some media cache actions are noops when the media content that is processed
|
||||
/// is filtered out by the policy. This can break some features of the SDK, like
|
||||
/// the send queue, that expects to be able to persist all media files in the
|
||||
/// store to restore them when the client is restored.
|
||||
///
|
||||
/// This can be converted to a boolean with
|
||||
/// [`IgnoreMediaRetentionPolicy::is_yes()`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum IgnoreMediaRetentionPolicy {
|
||||
/// The media retention policy will be ignored and the current action will
|
||||
/// not be a noop.
|
||||
///
|
||||
/// Any media content in this state must NOT be used when applying a
|
||||
/// `MediaRetentionPolicy`. This applies to ANY criteria, like the maximum
|
||||
/// file size, the maximum cache size or the last access expiry.
|
||||
///
|
||||
/// This state is supposed to be transient, and to only be used internally
|
||||
/// by the SDK.
|
||||
Yes,
|
||||
|
||||
/// The media retention policy will be respected and the current action
|
||||
/// might be a noop.
|
||||
No,
|
||||
}
|
||||
|
||||
impl IgnoreMediaRetentionPolicy {
|
||||
/// Whether this is an [`IgnoreMediaRetentionPolicy::Yes`] variant.
|
||||
pub fn is_yes(self) -> bool {
|
||||
matches!(self, Self::Yes)
|
||||
}
|
||||
}
|
||||
|
||||
/// An abstract trait to provide the current `SystemTime` for the
|
||||
/// [`MediaService`].
|
||||
pub trait TimeProvider {
|
||||
/// The current time.
|
||||
fn now(&self) -> SystemTime;
|
||||
}
|
||||
|
||||
/// The default time provider, that calls `ruma::time::SystemTime::now()`.
|
||||
#[derive(Debug)]
|
||||
pub struct DefaultTimeProvider;
|
||||
|
||||
impl TimeProvider for DefaultTimeProvider {
|
||||
fn now(&self) -> SystemTime {
|
||||
SystemTime::now()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fmt, sync::MutexGuard};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::locks::Mutex;
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{
|
||||
events::room::MediaSource,
|
||||
mxc_uri,
|
||||
time::{Duration, SystemTime},
|
||||
MxcUri, OwnedMxcUri,
|
||||
};
|
||||
|
||||
use super::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaService, TimeProvider};
|
||||
use crate::{
|
||||
event_cache::store::{media::MediaRetentionPolicy, EventCacheStoreError},
|
||||
media::{MediaFormat, MediaRequestParameters, UniqueKey},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct MockEventCacheStoreMedia {
|
||||
inner: Mutex<MockEventCacheStoreMediaInner>,
|
||||
}
|
||||
|
||||
impl MockEventCacheStoreMedia {
|
||||
/// Whether the store was accessed.
|
||||
fn accessed(&self) -> bool {
|
||||
self.inner.lock().accessed
|
||||
}
|
||||
|
||||
/// Reset the `accessed` boolean.
|
||||
fn reset_accessed(&self) {
|
||||
self.inner.lock().accessed = false;
|
||||
}
|
||||
|
||||
/// Access the inner store.
|
||||
///
|
||||
/// Should be called for every access to the inner store as it also sets
|
||||
/// the `accessed` boolean.
|
||||
fn inner(&self) -> MutexGuard<'_, MockEventCacheStoreMediaInner> {
|
||||
let mut inner = self.inner.lock();
|
||||
inner.accessed = true;
|
||||
inner
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct MockEventCacheStoreMediaInner {
|
||||
/// Whether this store was accessed.
|
||||
///
|
||||
/// Must be set to `true` for any operation that unlocks the store.
|
||||
accessed: bool,
|
||||
|
||||
/// The persisted media retention policy.
|
||||
media_retention_policy: Option<MediaRetentionPolicy>,
|
||||
|
||||
/// The list of media content.
|
||||
media_list: Vec<MediaContent>,
|
||||
|
||||
/// The time of the last cleanup.
|
||||
cleanup_time: Option<SystemTime>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct MediaContent {
|
||||
/// The unique key for the media content.
|
||||
key: String,
|
||||
|
||||
/// The original URI of the media content.
|
||||
uri: OwnedMxcUri,
|
||||
|
||||
/// The media content.
|
||||
content: Vec<u8>,
|
||||
|
||||
/// Whether the `MediaRetentionPolicy` should be ignored for this media
|
||||
/// content;
|
||||
ignore_policy: bool,
|
||||
|
||||
/// The time of the last access of the media content.
|
||||
last_access: SystemTime,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MockEventCacheStoreMediaError;
|
||||
|
||||
impl fmt::Display for MockEventCacheStoreMediaError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "MockEventCacheStoreMediaError")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for MockEventCacheStoreMediaError {}
|
||||
|
||||
impl From<MockEventCacheStoreMediaError> for EventCacheStoreError {
|
||||
fn from(value: MockEventCacheStoreMediaError) -> Self {
|
||||
Self::backend(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl EventCacheStoreMedia for MockEventCacheStoreMedia {
|
||||
type Error = MockEventCacheStoreMediaError;
|
||||
|
||||
async fn media_retention_policy_inner(
|
||||
&self,
|
||||
) -> Result<Option<MediaRetentionPolicy>, Self::Error> {
|
||||
Ok(self.inner().media_retention_policy)
|
||||
}
|
||||
|
||||
async fn set_media_retention_policy_inner(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.inner().media_retention_policy = Some(policy);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_media_content_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
content: Vec<u8>,
|
||||
current_time: SystemTime,
|
||||
policy: MediaRetentionPolicy,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
let ignore_policy = ignore_policy.is_yes();
|
||||
|
||||
if !ignore_policy && policy.exceeds_max_file_size(content.len()) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut inner = self.inner();
|
||||
let key = request.unique_key();
|
||||
|
||||
if let Some(pos) = inner.media_list.iter().position(|content| content.key == key) {
|
||||
let media_content = &mut inner.media_list[pos];
|
||||
media_content.content = content;
|
||||
media_content.last_access = current_time;
|
||||
media_content.ignore_policy = ignore_policy;
|
||||
} else {
|
||||
inner.media_list.push(MediaContent {
|
||||
key,
|
||||
uri: request.uri().to_owned(),
|
||||
content,
|
||||
ignore_policy,
|
||||
last_access: current_time,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_ignore_media_retention_policy_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
let key = request.unique_key();
|
||||
let mut inner = self.inner();
|
||||
|
||||
if let Some(pos) = inner.media_list.iter().position(|content| content.key == key) {
|
||||
inner.media_list[pos].ignore_policy = ignore_policy.is_yes();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_media_content_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
current_time: SystemTime,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
let key = request.unique_key();
|
||||
let mut inner = self.inner();
|
||||
|
||||
let Some(media_content) =
|
||||
inner.media_list.iter_mut().find(|content| content.key == key)
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
media_content.last_access = current_time;
|
||||
|
||||
Ok(Some(media_content.content.clone()))
|
||||
}
|
||||
|
||||
async fn get_media_content_for_uri_inner(
|
||||
&self,
|
||||
uri: &MxcUri,
|
||||
current_time: SystemTime,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
let mut inner = self.inner();
|
||||
|
||||
let Some(media_content) =
|
||||
inner.media_list.iter_mut().find(|content| content.uri == uri)
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
media_content.last_access = current_time;
|
||||
|
||||
Ok(Some(media_content.content.clone()))
|
||||
}
|
||||
|
||||
async fn clean_up_media_cache_inner(
|
||||
&self,
|
||||
_policy: MediaRetentionPolicy,
|
||||
current_time: SystemTime,
|
||||
) -> Result<(), Self::Error> {
|
||||
// This is mostly a noop. We don't care about this test implementation, only
|
||||
// whether this method was called with the right time.
|
||||
self.inner().cleanup_time = Some(current_time);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MockTimeProvider {
|
||||
now: Mutex<SystemTime>,
|
||||
}
|
||||
|
||||
impl MockTimeProvider {
|
||||
/// Construct a `MockTimeProvider` with the given current time.
|
||||
fn new(now: SystemTime) -> Self {
|
||||
Self { now: Mutex::new(now) }
|
||||
}
|
||||
|
||||
/// Set the current time.
|
||||
fn set_now(&self, now: SystemTime) {
|
||||
*self.now.lock() = now;
|
||||
}
|
||||
}
|
||||
|
||||
impl TimeProvider for MockTimeProvider {
|
||||
fn now(&self) -> SystemTime {
|
||||
*self.now.lock()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_media_service_empty_policy() {
|
||||
let content = b"some text content";
|
||||
let uri = mxc_uri!("mxc://server.local/AbcDe1234");
|
||||
let request = MediaRequestParameters {
|
||||
source: MediaSource::Plain(uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
|
||||
let now = SystemTime::UNIX_EPOCH;
|
||||
|
||||
let store = MockEventCacheStoreMedia::default();
|
||||
let service = MediaService::with_time_provider(MockTimeProvider::new(now));
|
||||
|
||||
// By default an empty policy is used.
|
||||
assert!(!service.media_retention_policy().has_limitations());
|
||||
service.restore(None);
|
||||
assert!(!service.media_retention_policy().has_limitations());
|
||||
assert!(!store.accessed());
|
||||
|
||||
// Add media.
|
||||
service
|
||||
.add_media_content(&store, &request, content.to_vec(), IgnoreMediaRetentionPolicy::No)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(store.accessed());
|
||||
|
||||
let media_content = store.inner().media_list[0].clone();
|
||||
assert_eq!(media_content.uri, uri);
|
||||
assert_eq!(media_content.content, content);
|
||||
assert!(!media_content.ignore_policy);
|
||||
assert_eq!(media_content.last_access, now);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
// Get media from request.
|
||||
let loaded_content = service.get_media_content(&store, &request).await.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(loaded_content.as_deref(), Some(content.as_slice()));
|
||||
|
||||
// The last access time was updated.
|
||||
let media = store.inner().media_list[0].clone();
|
||||
assert_eq!(media.last_access, now);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
// Get media from URI.
|
||||
let loaded_content = service.get_media_content_for_uri(&store, uri).await.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(loaded_content.as_deref(), Some(content.as_slice()));
|
||||
|
||||
// The last access time was updated.
|
||||
let media = store.inner().media_list[0].clone();
|
||||
assert_eq!(media.last_access, now);
|
||||
|
||||
// Update ignore_policy.
|
||||
service
|
||||
.set_ignore_media_retention_policy(&store, &request, IgnoreMediaRetentionPolicy::Yes)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(store.accessed());
|
||||
|
||||
let media_content = store.inner().media_list[0].clone();
|
||||
assert!(media_content.ignore_policy);
|
||||
|
||||
// Try a cleanup. With the empty policy the store should not be accessed.
|
||||
assert_eq!(store.inner().cleanup_time, None);
|
||||
store.reset_accessed();
|
||||
|
||||
service.clean_up_media_cache(&store).await.unwrap();
|
||||
assert!(!store.accessed());
|
||||
assert_eq!(store.inner().cleanup_time, None);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_media_service_non_empty_policy() {
|
||||
// Content of less than 32 bytes.
|
||||
let small_content = b"some text content";
|
||||
let small_uri = mxc_uri!("mxc://server.local/small");
|
||||
let small_request = MediaRequestParameters {
|
||||
source: MediaSource::Plain(small_uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
|
||||
// Content of more than 32 bytes.
|
||||
let big_content = b"some much much larger text content";
|
||||
let big_uri = mxc_uri!("mxc://server.local/big");
|
||||
let big_request = MediaRequestParameters {
|
||||
source: MediaSource::Plain(big_uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
|
||||
// Limit the file size to 32 bytes in the retention policy.
|
||||
let policy = MediaRetentionPolicy { max_file_size: Some(32), ..Default::default() };
|
||||
|
||||
let now = SystemTime::UNIX_EPOCH;
|
||||
|
||||
let store = MockEventCacheStoreMedia::default();
|
||||
let service = MediaService::with_time_provider(MockTimeProvider::new(now));
|
||||
|
||||
// Check that restoring the policy works.
|
||||
service.restore(Some(MediaRetentionPolicy::default()));
|
||||
assert_eq!(service.media_retention_policy(), MediaRetentionPolicy::default());
|
||||
assert!(!store.accessed());
|
||||
|
||||
// Set the media retention policy.
|
||||
service.set_media_retention_policy(&store, policy).await.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(service.media_retention_policy(), policy);
|
||||
assert_eq!(store.inner().media_retention_policy, Some(policy));
|
||||
|
||||
store.reset_accessed();
|
||||
|
||||
// Add small media, it should work because its size is lower than the max file
|
||||
// size.
|
||||
service
|
||||
.add_media_content(
|
||||
&store,
|
||||
&small_request,
|
||||
small_content.to_vec(),
|
||||
IgnoreMediaRetentionPolicy::No,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(store.accessed());
|
||||
|
||||
let media_content = store.inner().media_list[0].clone();
|
||||
assert_eq!(media_content.uri, small_uri);
|
||||
assert_eq!(media_content.content, small_content);
|
||||
assert!(!media_content.ignore_policy);
|
||||
assert_eq!(media_content.last_access, now);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
// Get media from request.
|
||||
let loaded_content = service.get_media_content(&store, &small_request).await.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(loaded_content.as_deref(), Some(small_content.as_slice()));
|
||||
|
||||
// The last access time was updated.
|
||||
let media = store.inner().media_list[0].clone();
|
||||
assert_eq!(media.last_access, now);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
// Get media from URI.
|
||||
let loaded_content = service.get_media_content_for_uri(&store, small_uri).await.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(loaded_content.as_deref(), Some(small_content.as_slice()));
|
||||
|
||||
// The last access time was updated.
|
||||
let media = store.inner().media_list[0].clone();
|
||||
assert_eq!(media.last_access, now);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
// Add big media, it will not work because it is bigger than the max file size.
|
||||
service
|
||||
.add_media_content(
|
||||
&store,
|
||||
&big_request,
|
||||
big_content.to_vec(),
|
||||
IgnoreMediaRetentionPolicy::No,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!store.accessed());
|
||||
assert_eq!(store.inner().media_list.len(), 1);
|
||||
|
||||
store.reset_accessed();
|
||||
|
||||
let loaded_content = service.get_media_content(&store, &big_request).await.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(loaded_content, None);
|
||||
|
||||
store.reset_accessed();
|
||||
|
||||
let loaded_content = service.get_media_content_for_uri(&store, big_uri).await.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(loaded_content, None);
|
||||
|
||||
// Add big media, but this time ignore the policy.
|
||||
service
|
||||
.add_media_content(
|
||||
&store,
|
||||
&big_request,
|
||||
big_content.to_vec(),
|
||||
IgnoreMediaRetentionPolicy::Yes,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(store.inner().media_list.len(), 2);
|
||||
|
||||
store.reset_accessed();
|
||||
|
||||
// Get media from request.
|
||||
let loaded_content = service.get_media_content(&store, &big_request).await.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(loaded_content.as_deref(), Some(big_content.as_slice()));
|
||||
|
||||
// The last access time was updated.
|
||||
let media = store.inner().media_list[1].clone();
|
||||
assert_eq!(media.last_access, now);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
// Get media from URI.
|
||||
let loaded_content = service.get_media_content_for_uri(&store, big_uri).await.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(loaded_content.as_deref(), Some(big_content.as_slice()));
|
||||
|
||||
// The last access time was updated.
|
||||
let media = store.inner().media_list[1].clone();
|
||||
assert_eq!(media.last_access, now);
|
||||
|
||||
// Try a cleanup, the store should be accessed.
|
||||
assert_eq!(store.inner().cleanup_time, None);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
service.clean_up_media_cache(&store).await.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(store.inner().cleanup_time, Some(now));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2025 Kévin Commaille
|
||||
//
|
||||
// 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.
|
||||
|
||||
//! Types and traits regarding media caching of the event cache store.
|
||||
|
||||
mod media_retention_policy;
|
||||
mod media_service;
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
#[macro_use]
|
||||
pub mod integration_tests;
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
pub use self::integration_tests::EventCacheStoreMediaIntegrationTests;
|
||||
pub use self::{
|
||||
media_retention_policy::MediaRetentionPolicy,
|
||||
media_service::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaService},
|
||||
};
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock, time::Instant};
|
||||
use std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
@@ -20,9 +20,15 @@ use matrix_sdk_common::{
|
||||
ring_buffer::RingBuffer,
|
||||
store_locks::memory_store_helper::try_take_leased_lock,
|
||||
};
|
||||
use ruma::{MxcUri, OwnedMxcUri, RoomId};
|
||||
use ruma::{
|
||||
time::{Instant, SystemTime},
|
||||
MxcUri, OwnedMxcUri, RoomId,
|
||||
};
|
||||
|
||||
use super::{EventCacheStore, EventCacheStoreError, Result};
|
||||
use super::{
|
||||
media::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService},
|
||||
EventCacheStore, EventCacheStoreError, Result,
|
||||
};
|
||||
use crate::{
|
||||
event_cache::{Event, Gap},
|
||||
media::{MediaRequestParameters, UniqueKey as _},
|
||||
@@ -35,13 +41,34 @@ use crate::{
|
||||
#[derive(Debug)]
|
||||
pub struct MemoryStore {
|
||||
inner: StdRwLock<MemoryStoreInner>,
|
||||
media_service: MediaService,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MemoryStoreInner {
|
||||
media: RingBuffer<(OwnedMxcUri, String /* unique key */, Vec<u8>)>,
|
||||
media: RingBuffer<MediaContent>,
|
||||
leases: HashMap<String, (String, Instant)>,
|
||||
events: RelationalLinkedChunk<Event, Gap>,
|
||||
media_retention_policy: Option<MediaRetentionPolicy>,
|
||||
}
|
||||
|
||||
/// A media content in the `MemoryStore`.
|
||||
#[derive(Debug)]
|
||||
struct MediaContent {
|
||||
/// The URI of the content.
|
||||
uri: OwnedMxcUri,
|
||||
|
||||
/// The unique key of the content.
|
||||
key: String,
|
||||
|
||||
/// The bytes of the content.
|
||||
data: Vec<u8>,
|
||||
|
||||
/// Whether we should ignore the [`MediaRetentionPolicy`] for this content.
|
||||
ignore_policy: bool,
|
||||
|
||||
/// The time of the last access of the content.
|
||||
last_access: SystemTime,
|
||||
}
|
||||
|
||||
// SAFETY: `new_unchecked` is safe because 20 is not zero.
|
||||
@@ -54,7 +81,10 @@ impl Default for MemoryStore {
|
||||
media: RingBuffer::new(NUMBER_OF_MEDIAS),
|
||||
leases: Default::default(),
|
||||
events: RelationalLinkedChunk::new(),
|
||||
media_retention_policy: None,
|
||||
}),
|
||||
// No need to call `restore()` since nothing is persisted.
|
||||
media_service: MediaService::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,15 +143,9 @@ impl EventCacheStore for MemoryStore {
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
data: Vec<u8>,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<()> {
|
||||
// Avoid duplication. Let's try to remove it first.
|
||||
self.remove_media_content(request).await?;
|
||||
|
||||
// Now, let's add it.
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
inner.media.push((request.uri().to_owned(), request.unique_key(), data));
|
||||
|
||||
Ok(())
|
||||
self.media_service.add_media_content(self, request, data, ignore_policy).await
|
||||
}
|
||||
|
||||
async fn replace_media_key(
|
||||
@@ -133,23 +157,18 @@ impl EventCacheStore for MemoryStore {
|
||||
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
if let Some((mxc, key, _)) = inner.media.iter_mut().find(|(_, key, _)| *key == expected_key)
|
||||
if let Some(media_content) =
|
||||
inner.media.iter_mut().find(|media_content| media_content.key == expected_key)
|
||||
{
|
||||
*mxc = to.uri().to_owned();
|
||||
*key = to.unique_key();
|
||||
media_content.uri = to.uri().to_owned();
|
||||
media_content.key = to.unique_key();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_media_content(&self, request: &MediaRequestParameters) -> Result<Option<Vec<u8>>> {
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
Ok(inner.media.iter().find_map(|(_media_uri, media_key, media_content)| {
|
||||
(media_key == &expected_key).then(|| media_content.to_owned())
|
||||
}))
|
||||
self.media_service.get_media_content(self, request).await
|
||||
}
|
||||
|
||||
async fn remove_media_content(&self, request: &MediaRequestParameters) -> Result<()> {
|
||||
@@ -157,10 +176,8 @@ impl EventCacheStore for MemoryStore {
|
||||
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
let Some(index) = inner
|
||||
.media
|
||||
.iter()
|
||||
.position(|(_media_uri, media_key, _media_content)| media_key == &expected_key)
|
||||
let Some(index) =
|
||||
inner.media.iter().position(|media_content| media_content.key == expected_key)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
@@ -174,24 +191,17 @@ impl EventCacheStore for MemoryStore {
|
||||
&self,
|
||||
uri: &MxcUri,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
Ok(inner.media.iter().find_map(|(media_uri, _media_key, media_content)| {
|
||||
(media_uri == uri).then(|| media_content.to_owned())
|
||||
}))
|
||||
self.media_service.get_media_content_for_uri(self, uri).await
|
||||
}
|
||||
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
let expected_key = uri.to_owned();
|
||||
let positions = inner
|
||||
.media
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(position, (media_uri, _media_key, _media_content))| {
|
||||
(media_uri == &expected_key).then_some(position)
|
||||
})
|
||||
.filter_map(|(position, media_content)| (media_content.uri == uri).then_some(position))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Iterate in reverse-order so that positions stay valid after first removals.
|
||||
@@ -201,16 +211,240 @@ impl EventCacheStore for MemoryStore {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_media_retention_policy(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.media_service.set_media_retention_policy(self, policy).await
|
||||
}
|
||||
|
||||
fn media_retention_policy(&self) -> MediaRetentionPolicy {
|
||||
self.media_service.media_retention_policy()
|
||||
}
|
||||
|
||||
async fn set_ignore_media_retention_policy(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.media_service.set_ignore_media_retention_policy(self, request, ignore_policy).await
|
||||
}
|
||||
|
||||
async fn clean_up_media_cache(&self) -> Result<(), Self::Error> {
|
||||
self.media_service.clean_up_media_cache(self).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl EventCacheStoreMedia for MemoryStore {
|
||||
type Error = EventCacheStoreError;
|
||||
|
||||
async fn media_retention_policy_inner(
|
||||
&self,
|
||||
) -> Result<Option<MediaRetentionPolicy>, Self::Error> {
|
||||
Ok(self.inner.read().unwrap().media_retention_policy)
|
||||
}
|
||||
|
||||
async fn set_media_retention_policy_inner(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.inner.write().unwrap().media_retention_policy = Some(policy);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_media_content_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
data: Vec<u8>,
|
||||
last_access: SystemTime,
|
||||
policy: MediaRetentionPolicy,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
// Avoid duplication. Let's try to remove it first.
|
||||
self.remove_media_content(request).await?;
|
||||
|
||||
let ignore_policy = ignore_policy.is_yes();
|
||||
|
||||
if !ignore_policy && policy.exceeds_max_file_size(data.len()) {
|
||||
// Do not store it.
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// Now, let's add it.
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
inner.media.push(MediaContent {
|
||||
uri: request.uri().to_owned(),
|
||||
key: request.unique_key(),
|
||||
data,
|
||||
ignore_policy,
|
||||
last_access,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_ignore_media_retention_policy_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
if let Some(media_content) = inner.media.iter_mut().find(|media| media.key == expected_key)
|
||||
{
|
||||
media_content.ignore_policy = ignore_policy.is_yes();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_media_content_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
current_time: SystemTime,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
// First get the content out of the buffer, we are going to put it back at the
|
||||
// end.
|
||||
let Some(index) = inner.media.iter().position(|media| media.key == expected_key) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(mut content) = inner.media.remove(index) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Clone the data.
|
||||
let data = content.data.clone();
|
||||
|
||||
// Update the last access time.
|
||||
content.last_access = current_time;
|
||||
|
||||
// Put it back in the buffer.
|
||||
inner.media.push(content);
|
||||
|
||||
Ok(Some(data))
|
||||
}
|
||||
|
||||
async fn get_media_content_for_uri_inner(
|
||||
&self,
|
||||
expected_uri: &MxcUri,
|
||||
current_time: SystemTime,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
// First get the content out of the buffer, we are going to put it back at the
|
||||
// end.
|
||||
let Some(index) = inner.media.iter().position(|media| media.uri == expected_uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(mut content) = inner.media.remove(index) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Clone the data.
|
||||
let data = content.data.clone();
|
||||
|
||||
// Update the last access time.
|
||||
content.last_access = current_time;
|
||||
|
||||
// Put it back in the buffer.
|
||||
inner.media.push(content);
|
||||
|
||||
Ok(Some(data))
|
||||
}
|
||||
|
||||
async fn clean_up_media_cache_inner(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
current_time: SystemTime,
|
||||
) -> Result<(), Self::Error> {
|
||||
if !policy.has_limitations() {
|
||||
// We can safely skip all the checks.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
// First, check media content that exceed the max filesize.
|
||||
if policy.computed_max_file_size().is_some() {
|
||||
inner.media.retain(|content| {
|
||||
content.ignore_policy || !policy.exceeds_max_file_size(content.data.len())
|
||||
});
|
||||
}
|
||||
|
||||
// Then, clean up expired media content.
|
||||
if policy.last_access_expiry.is_some() {
|
||||
inner.media.retain(|content| {
|
||||
content.ignore_policy
|
||||
|| !policy.has_content_expired(current_time, content.last_access)
|
||||
});
|
||||
}
|
||||
|
||||
// Finally, if the cache size is too big, remove old items until it fits.
|
||||
if let Some(max_cache_size) = policy.max_cache_size {
|
||||
// Reverse the iterator because in case the cache size is overflowing, we want
|
||||
// to count the number of old items to remove. Items are sorted by last access
|
||||
// and old items are at the start.
|
||||
let (_, items_to_remove) = inner.media.iter().enumerate().rev().fold(
|
||||
(0usize, Vec::with_capacity(NUMBER_OF_MEDIAS.into())),
|
||||
|(mut cache_size, mut items_to_remove), (index, content)| {
|
||||
if content.ignore_policy {
|
||||
// Do not count it.
|
||||
return (cache_size, items_to_remove);
|
||||
}
|
||||
|
||||
let remove_item = if items_to_remove.is_empty() {
|
||||
// We have not reached the max cache size yet.
|
||||
if let Some(sum) = cache_size.checked_add(content.data.len()) {
|
||||
cache_size = sum;
|
||||
// Start removing items if we have exceeded the max cache size.
|
||||
cache_size > max_cache_size
|
||||
} else {
|
||||
// The cache size is overflowing, remove the remaining items, since the
|
||||
// max cache size cannot be bigger than
|
||||
// usize::MAX.
|
||||
true
|
||||
}
|
||||
} else {
|
||||
// We have reached the max cache size already, just remove it.
|
||||
true
|
||||
};
|
||||
|
||||
if remove_item {
|
||||
items_to_remove.push(index);
|
||||
}
|
||||
|
||||
(cache_size, items_to_remove)
|
||||
},
|
||||
);
|
||||
|
||||
// The indexes are already in reverse order so we can just iterate in that order
|
||||
// to remove them starting by the end.
|
||||
for index in items_to_remove {
|
||||
inner.media.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{EventCacheStore, MemoryStore, Result};
|
||||
use super::{MemoryStore, Result};
|
||||
use crate::event_cache_store_media_integration_tests;
|
||||
|
||||
async fn get_event_cache_store() -> Result<impl EventCacheStore> {
|
||||
async fn get_event_cache_store() -> Result<MemoryStore> {
|
||||
Ok(MemoryStore::new())
|
||||
}
|
||||
|
||||
event_cache_store_integration_tests!();
|
||||
event_cache_store_integration_tests_time!();
|
||||
event_cache_store_media_integration_tests!(with_media_size_tests);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ use std::{fmt, ops::Deref, str::Utf8Error, sync::Arc};
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
#[macro_use]
|
||||
pub mod integration_tests;
|
||||
pub mod media;
|
||||
mod memory_store;
|
||||
mod traits;
|
||||
|
||||
|
||||
@@ -21,7 +21,10 @@ use matrix_sdk_common::{
|
||||
};
|
||||
use ruma::{MxcUri, RoomId};
|
||||
|
||||
use super::EventCacheStoreError;
|
||||
use super::{
|
||||
media::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy},
|
||||
EventCacheStoreError,
|
||||
};
|
||||
use crate::{
|
||||
event_cache::{Event, Gap},
|
||||
media::MediaRequestParameters,
|
||||
@@ -57,6 +60,13 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
updates: Vec<Update<Event, Gap>>,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Remove all data tied to a given room from the cache.
|
||||
async fn remove_room(&self, room_id: &RoomId) -> Result<(), Self::Error> {
|
||||
// Right now, this means removing all the linked chunk. If implementations
|
||||
// override this behavior, they should *also* include this code.
|
||||
self.handle_linked_chunk_updates(room_id, vec![Update::Clear]).await
|
||||
}
|
||||
|
||||
/// Return all the raw components of a linked chunk, so the caller may
|
||||
/// reconstruct the linked chunk later.
|
||||
async fn reload_linked_chunk(
|
||||
@@ -81,6 +91,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
content: Vec<u8>,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Replaces the given media's content key with another one.
|
||||
@@ -155,6 +166,42 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
///
|
||||
/// * `uri` - The `MxcUri` of the media files.
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error>;
|
||||
|
||||
/// Set the `MediaRetentionPolicy` to use for deciding whether to store or
|
||||
/// keep media content.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `policy` - The `MediaRetentionPolicy` to use.
|
||||
async fn set_media_retention_policy(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Get the current `MediaRetentionPolicy`.
|
||||
fn media_retention_policy(&self) -> MediaRetentionPolicy;
|
||||
|
||||
/// Set whether the current [`MediaRetentionPolicy`] should be ignored for
|
||||
/// the media.
|
||||
///
|
||||
/// The change will be taken into account in the next cleanup.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
///
|
||||
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
|
||||
/// ignored.
|
||||
async fn set_ignore_media_retention_policy(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Clean up the media cache with the current `MediaRetentionPolicy`.
|
||||
///
|
||||
/// If there is already an ongoing cleanup, this is a noop.
|
||||
async fn clean_up_media_cache(&self) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
@@ -204,8 +251,9 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
content: Vec<u8>,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.add_media_content(request, content).await.map_err(Into::into)
|
||||
self.0.add_media_content(request, content, ignore_policy).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn replace_media_key(
|
||||
@@ -240,6 +288,29 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error> {
|
||||
self.0.remove_media_content_for_uri(uri).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn set_media_retention_policy(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.set_media_retention_policy(policy).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
fn media_retention_policy(&self) -> MediaRetentionPolicy {
|
||||
self.0.media_retention_policy()
|
||||
}
|
||||
|
||||
async fn set_ignore_media_retention_policy(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.set_ignore_media_retention_policy(request, ignore_policy).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn clean_up_media_cache(&self) -> Result<(), Self::Error> {
|
||||
self.0.clean_up_media_cache().await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// A type-erased [`EventCacheStore`].
|
||||
|
||||
@@ -1,28 +1,24 @@
|
||||
//! Utilities for working with events to decide whether they are suitable for
|
||||
//! use as a [crate::Room::latest_event].
|
||||
|
||||
#![cfg(any(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::events::{
|
||||
call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent},
|
||||
poll::unstable_start::SyncUnstablePollStartEvent,
|
||||
relation::RelationType,
|
||||
room::message::SyncRoomMessageEvent,
|
||||
AnySyncMessageLikeEvent, AnySyncTimelineEvent,
|
||||
};
|
||||
use ruma::{
|
||||
events::{
|
||||
call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent},
|
||||
poll::unstable_start::SyncUnstablePollStartEvent,
|
||||
relation::RelationType,
|
||||
room::{
|
||||
member::{MembershipState, SyncRoomMemberEvent},
|
||||
message::SyncRoomMessageEvent,
|
||||
power_levels::RoomPowerLevels,
|
||||
},
|
||||
sticker::SyncStickerEvent,
|
||||
AnySyncStateEvent,
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
|
||||
},
|
||||
MxcUri, OwnedEventId, UserId,
|
||||
UserId,
|
||||
};
|
||||
use ruma::{MxcUri, OwnedEventId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::MinimalRoomMemberEvent;
|
||||
@@ -168,7 +164,7 @@ pub fn is_suitable_for_latest_event<'a>(
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct LatestEvent {
|
||||
/// The actual event.
|
||||
event: SyncTimelineEvent,
|
||||
event: TimelineEvent,
|
||||
|
||||
/// The member profile of the event' sender.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -182,7 +178,7 @@ pub struct LatestEvent {
|
||||
#[derive(Deserialize)]
|
||||
struct SerializedLatestEvent {
|
||||
/// The actual event.
|
||||
event: SyncTimelineEvent,
|
||||
event: TimelineEvent,
|
||||
|
||||
/// The member profile of the event' sender.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -215,7 +211,7 @@ impl<'de> Deserialize<'de> for LatestEvent {
|
||||
Err(err) => variant_errors.push(err),
|
||||
}
|
||||
|
||||
match serde_json::from_str::<SyncTimelineEvent>(raw.get()) {
|
||||
match serde_json::from_str::<TimelineEvent>(raw.get()) {
|
||||
Ok(value) => {
|
||||
return Ok(LatestEvent {
|
||||
event: value,
|
||||
@@ -234,13 +230,13 @@ impl<'de> Deserialize<'de> for LatestEvent {
|
||||
|
||||
impl LatestEvent {
|
||||
/// Create a new [`LatestEvent`] without the sender's profile.
|
||||
pub fn new(event: SyncTimelineEvent) -> Self {
|
||||
pub fn new(event: TimelineEvent) -> Self {
|
||||
Self { event, sender_profile: None, sender_name_is_ambiguous: None }
|
||||
}
|
||||
|
||||
/// Create a new [`LatestEvent`] with maybe the sender's profile.
|
||||
pub fn new_with_sender_details(
|
||||
event: SyncTimelineEvent,
|
||||
event: TimelineEvent,
|
||||
sender_profile: Option<MinimalRoomMemberEvent>,
|
||||
sender_name_is_ambiguous: Option<bool>,
|
||||
) -> Self {
|
||||
@@ -248,17 +244,17 @@ impl LatestEvent {
|
||||
}
|
||||
|
||||
/// Transform [`Self`] into an event.
|
||||
pub fn into_event(self) -> SyncTimelineEvent {
|
||||
pub fn into_event(self) -> TimelineEvent {
|
||||
self.event
|
||||
}
|
||||
|
||||
/// Get a reference to the event.
|
||||
pub fn event(&self) -> &SyncTimelineEvent {
|
||||
pub fn event(&self) -> &TimelineEvent {
|
||||
&self.event
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the event.
|
||||
pub fn event_mut(&mut self) -> &mut SyncTimelineEvent {
|
||||
pub fn event_mut(&mut self) -> &mut TimelineEvent {
|
||||
&mut self.event
|
||||
}
|
||||
|
||||
@@ -298,11 +294,16 @@ impl LatestEvent {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use assert_matches::assert_matches;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use assert_matches2::assert_let;
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use ruma::serde::Raw;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::{
|
||||
events::{
|
||||
call::{
|
||||
@@ -340,14 +341,16 @@ mod tests {
|
||||
RedactedSyncMessageLikeEvent, RedactedUnsigned, StateUnsigned, SyncMessageLikeEvent,
|
||||
UnsignedRoomRedactionEvent,
|
||||
},
|
||||
owned_event_id, owned_mxc_uri, owned_user_id,
|
||||
serde::Raw,
|
||||
MilliSecondsSinceUnixEpoch, UInt, VoipVersionId,
|
||||
owned_event_id, owned_mxc_uri, owned_user_id, MilliSecondsSinceUnixEpoch, UInt,
|
||||
VoipVersionId,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent};
|
||||
use super::LatestEvent;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use super::{is_suitable_for_latest_event, PossibleLatestEvent};
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_room_messages_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
|
||||
@@ -372,6 +375,7 @@ mod tests {
|
||||
assert_eq!(m.content.msgtype.msgtype(), "m.image");
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_polls_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(
|
||||
@@ -395,6 +399,7 @@ mod tests {
|
||||
assert_eq!(m.content.poll_start().question.text, "do you like rust?");
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_call_invites_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(
|
||||
@@ -417,6 +422,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_call_notifications_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallNotify(
|
||||
@@ -439,6 +445,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_stickers_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(
|
||||
@@ -461,6 +468,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_different_types_of_messagelike_are_unsuitable() {
|
||||
let event =
|
||||
@@ -483,6 +491,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_redacted_messages_are_suitable() {
|
||||
// Ruma does not allow constructing UnsignedRoomRedactionEvent instances.
|
||||
@@ -511,6 +520,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_encrypted_messages_are_unsuitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(
|
||||
@@ -534,6 +544,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_state_events_are_unsuitable() {
|
||||
let event = AnySyncTimelineEvent::State(AnySyncStateEvent::RoomTopic(
|
||||
@@ -553,6 +564,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_replacement_events_are_unsuitable() {
|
||||
let mut event_content = RoomMessageEventContent::text_plain("Bye bye, world!");
|
||||
@@ -584,7 +596,7 @@ mod tests {
|
||||
latest_event: LatestEvent,
|
||||
}
|
||||
|
||||
let event = SyncTimelineEvent::new(
|
||||
let event = TimelineEvent::new(
|
||||
Raw::from_json_string(json!({ "event_id": "$1" }).to_string()).unwrap(),
|
||||
);
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ mod rooms;
|
||||
|
||||
pub mod read_receipts;
|
||||
pub use read_receipts::PreviousEventsProvider;
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
pub mod sliding_sync;
|
||||
|
||||
pub mod store;
|
||||
@@ -56,9 +55,9 @@ pub use http;
|
||||
pub use matrix_sdk_crypto as crypto;
|
||||
pub use once_cell;
|
||||
pub use rooms::{
|
||||
Room, RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo,
|
||||
RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember, RoomMemberships, RoomState,
|
||||
RoomStateFilter,
|
||||
apply_redaction, Room, RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo,
|
||||
RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember, RoomMembersUpdate,
|
||||
RoomMemberships, RoomState, RoomStateFilter,
|
||||
};
|
||||
pub use store::{
|
||||
ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey,
|
||||
|
||||
@@ -123,7 +123,7 @@ use std::{
|
||||
};
|
||||
|
||||
use eyeball_im::Vector;
|
||||
use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, ring_buffer::RingBuffer};
|
||||
use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer};
|
||||
use ruma::{
|
||||
events::{
|
||||
poll::{start::PollStartEventContent, unstable_start::UnstablePollStartEventContent},
|
||||
@@ -202,7 +202,7 @@ impl RoomReadReceipts {
|
||||
///
|
||||
/// Returns whether a new event triggered a new unread/notification/mention.
|
||||
#[inline(always)]
|
||||
fn process_event(&mut self, event: &SyncTimelineEvent, user_id: &UserId) {
|
||||
fn process_event(&mut self, event: &TimelineEvent, user_id: &UserId) {
|
||||
if marks_as_unread(event.raw(), user_id) {
|
||||
self.num_unread += 1;
|
||||
}
|
||||
@@ -210,7 +210,11 @@ impl RoomReadReceipts {
|
||||
let mut has_notify = false;
|
||||
let mut has_mention = false;
|
||||
|
||||
for action in &event.push_actions {
|
||||
let Some(actions) = event.push_actions.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
for action in actions.iter() {
|
||||
if !has_notify && action.should_notify() {
|
||||
self.num_notifications += 1;
|
||||
has_notify = true;
|
||||
@@ -236,7 +240,7 @@ impl RoomReadReceipts {
|
||||
&mut self,
|
||||
receipt_event_id: &EventId,
|
||||
user_id: &UserId,
|
||||
events: impl IntoIterator<Item = &'a SyncTimelineEvent>,
|
||||
events: impl IntoIterator<Item = &'a TimelineEvent>,
|
||||
) -> bool {
|
||||
let mut counting_receipts = false;
|
||||
|
||||
@@ -269,11 +273,11 @@ impl RoomReadReceipts {
|
||||
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>;
|
||||
fn for_room(&self, room_id: &RoomId) -> Vector<TimelineEvent>;
|
||||
}
|
||||
|
||||
impl PreviousEventsProvider for () {
|
||||
fn for_room(&self, _: &RoomId) -> Vector<SyncTimelineEvent> {
|
||||
fn for_room(&self, _: &RoomId) -> Vector<TimelineEvent> {
|
||||
Vector::new()
|
||||
}
|
||||
}
|
||||
@@ -292,7 +296,7 @@ struct ReceiptSelector {
|
||||
|
||||
impl ReceiptSelector {
|
||||
fn new(
|
||||
all_events: &Vector<SyncTimelineEvent>,
|
||||
all_events: &Vector<TimelineEvent>,
|
||||
latest_active_receipt_event: Option<&EventId>,
|
||||
) -> Self {
|
||||
let event_id_to_pos = Self::create_sync_index(all_events.iter());
|
||||
@@ -310,7 +314,7 @@ impl ReceiptSelector {
|
||||
/// Create a mapping of `event_id` -> sync order for all events that have an
|
||||
/// `event_id`.
|
||||
fn create_sync_index<'a>(
|
||||
events: impl Iterator<Item = &'a SyncTimelineEvent> + 'a,
|
||||
events: impl Iterator<Item = &'a TimelineEvent> + 'a,
|
||||
) -> BTreeMap<OwnedEventId, usize> {
|
||||
// TODO: this should be cached and incrementally updated.
|
||||
BTreeMap::from_iter(
|
||||
@@ -405,7 +409,7 @@ impl ReceiptSelector {
|
||||
/// Try to match an implicit receipt, that is, the one we get for events we
|
||||
/// sent ourselves.
|
||||
#[instrument(skip_all)]
|
||||
fn try_match_implicit(&mut self, user_id: &UserId, new_events: &[SyncTimelineEvent]) {
|
||||
fn try_match_implicit(&mut self, user_id: &UserId, new_events: &[TimelineEvent]) {
|
||||
for ev in new_events {
|
||||
// Get the `sender` field, if any, or skip this event.
|
||||
let Ok(Some(sender)) = ev.raw().get_field::<OwnedUserId>("sender") else { continue };
|
||||
@@ -432,8 +436,8 @@ impl ReceiptSelector {
|
||||
/// Returns true if there's an event common to both groups of events, based on
|
||||
/// their event id.
|
||||
fn events_intersects<'a>(
|
||||
previous_events: impl Iterator<Item = &'a SyncTimelineEvent>,
|
||||
new_events: &[SyncTimelineEvent],
|
||||
previous_events: impl Iterator<Item = &'a TimelineEvent>,
|
||||
new_events: &[TimelineEvent],
|
||||
) -> bool {
|
||||
let previous_events_ids = BTreeSet::from_iter(previous_events.filter_map(|ev| ev.event_id()));
|
||||
new_events
|
||||
@@ -454,8 +458,8 @@ pub(crate) fn compute_unread_counts(
|
||||
user_id: &UserId,
|
||||
room_id: &RoomId,
|
||||
receipt_event: Option<&ReceiptEventContent>,
|
||||
previous_events: Vector<SyncTimelineEvent>,
|
||||
new_events: &[SyncTimelineEvent],
|
||||
previous_events: Vector<TimelineEvent>,
|
||||
new_events: &[TimelineEvent],
|
||||
read_receipts: &mut RoomReadReceipts,
|
||||
) {
|
||||
debug!(?read_receipts, "Starting.");
|
||||
@@ -620,11 +624,14 @@ mod tests {
|
||||
use std::{num::NonZeroUsize, ops::Not as _};
|
||||
|
||||
use eyeball_im::Vector;
|
||||
use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, ring_buffer::RingBuffer};
|
||||
use matrix_sdk_test::{sync_timeline_event, EventBuilder};
|
||||
use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer};
|
||||
use matrix_sdk_test::event_factory::EventFactory;
|
||||
use ruma::{
|
||||
event_id,
|
||||
events::receipt::{ReceiptThread, ReceiptType},
|
||||
events::{
|
||||
receipt::{ReceiptThread, ReceiptType},
|
||||
room::{member::MembershipState, message::MessageType},
|
||||
},
|
||||
owned_event_id, owned_user_id,
|
||||
push::Action,
|
||||
room_id, user_id, EventId, UserId,
|
||||
@@ -638,24 +645,14 @@ mod tests {
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
let other_user_id = user_id!("@bob:example.org");
|
||||
|
||||
let f = EventFactory::new();
|
||||
|
||||
// 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" },
|
||||
});
|
||||
let ev = f.text_msg("A").event_id(event_id!("$ida")).sender(other_user_id).into_raw_sync();
|
||||
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" },
|
||||
});
|
||||
let ev = f.text_msg("A").event_id(event_id!("$ida")).sender(user_id).into_raw_sync();
|
||||
assert!(marks_as_unread(&ev, user_id).not());
|
||||
}
|
||||
|
||||
@@ -665,24 +662,16 @@ mod tests {
|
||||
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"
|
||||
},
|
||||
});
|
||||
let ev = EventFactory::new()
|
||||
.text_msg("* edited message")
|
||||
.edit(
|
||||
event_id!("$someeventid:localhost"),
|
||||
MessageType::text_plain("edited message").into(),
|
||||
)
|
||||
.event_id(event_id!("$ida"))
|
||||
.sender(other_user_id)
|
||||
.into_raw_sync();
|
||||
|
||||
assert!(marks_as_unread(&ev, user_id).not());
|
||||
}
|
||||
|
||||
@@ -692,19 +681,11 @@ mod tests {
|
||||
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
|
||||
}
|
||||
});
|
||||
let ev = EventFactory::new()
|
||||
.redaction(event_id!("$151957878228ssqrj:localhost"))
|
||||
.sender(other_user_id)
|
||||
.event_id(event_id!("$151957878228ssqrJ:localhost"))
|
||||
.into_raw_sync();
|
||||
|
||||
assert!(marks_as_unread(&ev, user_id).not());
|
||||
}
|
||||
@@ -715,22 +696,11 @@ mod tests {
|
||||
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
|
||||
}
|
||||
});
|
||||
let ev = EventFactory::new()
|
||||
.reaction(event_id!("$15275047031IXQRj:localhost"), "👍")
|
||||
.sender(other_user_id)
|
||||
.event_id(event_id!("$15275047031IXQRi:localhost"))
|
||||
.into_raw_sync();
|
||||
|
||||
assert!(marks_as_unread(&ev, user_id).not());
|
||||
}
|
||||
@@ -739,18 +709,13 @@ mod tests {
|
||||
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",
|
||||
});
|
||||
|
||||
let ev = EventFactory::new()
|
||||
.member(user_id)
|
||||
.membership(MembershipState::Join)
|
||||
.display_name("Alice")
|
||||
.event_id(event_id)
|
||||
.into_raw_sync();
|
||||
assert!(marks_as_unread(&ev, user_id).not());
|
||||
|
||||
let other_user_id = user_id!("@bob:example.org");
|
||||
@@ -759,17 +724,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_count_unread_and_mentions() {
|
||||
fn make_event(user_id: &UserId, push_actions: Vec<Action>) -> SyncTimelineEvent {
|
||||
SyncTimelineEvent::new_with_push_actions(
|
||||
sync_timeline_event!({
|
||||
"sender": user_id,
|
||||
"type": "m.room.message",
|
||||
"event_id": "$ida",
|
||||
"origin_server_ts": 12344446,
|
||||
"content": { "body":"A", "msgtype": "m.text" },
|
||||
}),
|
||||
push_actions,
|
||||
)
|
||||
fn make_event(user_id: &UserId, push_actions: Vec<Action>) -> TimelineEvent {
|
||||
let mut ev = EventFactory::new()
|
||||
.text_msg("A")
|
||||
.sender(user_id)
|
||||
.event_id(event_id!("$ida"))
|
||||
.into_event();
|
||||
ev.push_actions = Some(push_actions);
|
||||
ev
|
||||
}
|
||||
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
@@ -843,14 +805,12 @@ mod tests {
|
||||
|
||||
// When provided with one event, that's not the receipt event, we don't count
|
||||
// it.
|
||||
fn make_event(event_id: &EventId) -> SyncTimelineEvent {
|
||||
SyncTimelineEvent::new(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" },
|
||||
}))
|
||||
fn make_event(event_id: &EventId) -> TimelineEvent {
|
||||
EventFactory::new()
|
||||
.text_msg("A")
|
||||
.sender(user_id!("@bob:example.org"))
|
||||
.event_id(event_id)
|
||||
.into()
|
||||
}
|
||||
|
||||
let mut receipts = RoomReadReceipts {
|
||||
@@ -948,20 +908,6 @@ mod tests {
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
}
|
||||
|
||||
fn sync_timeline_message(
|
||||
sender: &UserId,
|
||||
event_id: impl serde::Serialize,
|
||||
body: impl serde::Serialize,
|
||||
) -> SyncTimelineEvent {
|
||||
SyncTimelineEvent::new(sync_timeline_event!({
|
||||
"sender": sender,
|
||||
"type": "m.room.message",
|
||||
"event_id": event_id,
|
||||
"origin_server_ts": 42,
|
||||
"content": { "body": body, "msgtype": "m.text" },
|
||||
}))
|
||||
}
|
||||
|
||||
/// Smoke test for `compute_unread_counts`.
|
||||
#[test]
|
||||
fn test_basic_compute_unread_counts() {
|
||||
@@ -972,15 +918,14 @@ mod tests {
|
||||
|
||||
let mut previous_events = Vector::new();
|
||||
|
||||
let ev1 = sync_timeline_message(other_user_id, receipt_event_id, "A");
|
||||
let ev2 = sync_timeline_message(other_user_id, "$2", "A");
|
||||
let f = EventFactory::new();
|
||||
let ev1 = f.text_msg("A").sender(other_user_id).event_id(receipt_event_id).into_event();
|
||||
let ev2 = f.text_msg("A").sender(other_user_id).event_id(event_id!("$2")).into_event();
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
receipt_event_id.to_owned(),
|
||||
ReceiptType::Read,
|
||||
user_id.to_owned(),
|
||||
ReceiptThread::Unthreaded,
|
||||
)]);
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(receipt_event_id, user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
|
||||
.build();
|
||||
|
||||
let mut read_receipts = Default::default();
|
||||
compute_unread_counts(
|
||||
@@ -999,7 +944,8 @@ mod tests {
|
||||
previous_events.push_back(ev1);
|
||||
previous_events.push_back(ev2);
|
||||
|
||||
let new_event = sync_timeline_message(other_user_id, "$3", "A");
|
||||
let new_event =
|
||||
f.text_msg("A").sender(other_user_id).event_id(event_id!("$3")).into_event();
|
||||
compute_unread_counts(
|
||||
user_id,
|
||||
room_id,
|
||||
@@ -1013,13 +959,14 @@ mod tests {
|
||||
assert_eq!(read_receipts.num_unread, 2);
|
||||
}
|
||||
|
||||
fn make_test_events(user_id: &UserId) -> Vector<SyncTimelineEvent> {
|
||||
let ev1 = sync_timeline_message(user_id, "$1", "With the lights out, it's less dangerous");
|
||||
let ev2 = sync_timeline_message(user_id, "$2", "Here we are now, entertain us");
|
||||
let ev3 = sync_timeline_message(user_id, "$3", "I feel stupid and contagious");
|
||||
let ev4 = sync_timeline_message(user_id, "$4", "Here we are now, entertain us");
|
||||
let ev5 = sync_timeline_message(user_id, "$5", "Hello, hello, hello, how low?");
|
||||
vec![ev1, ev2, ev3, ev4, ev5].into()
|
||||
fn make_test_events(user_id: &UserId) -> Vector<TimelineEvent> {
|
||||
let f = EventFactory::new().sender(user_id);
|
||||
let ev1 = f.text_msg("With the lights out, it's less dangerous").event_id(event_id!("$1"));
|
||||
let ev2 = f.text_msg("Here we are now, entertain us").event_id(event_id!("$2"));
|
||||
let ev3 = f.text_msg("I feel stupid and contagious").event_id(event_id!("$3"));
|
||||
let ev4 = f.text_msg("Here we are now, entertain us").event_id(event_id!("$4"));
|
||||
let ev5 = f.text_msg("Hello, hello, hello, how low?").event_id(event_id!("$5"));
|
||||
[ev1, ev2, ev3, ev4, ev5].into_iter().map(Into::into).collect()
|
||||
}
|
||||
|
||||
/// Test that when multiple receipts come in a single event, we can still
|
||||
@@ -1035,30 +982,32 @@ mod tests {
|
||||
|
||||
// Given a receipt event marking events 1-3 as read using a combination of
|
||||
// different thread and privacy types,
|
||||
let f = EventFactory::new();
|
||||
for receipt_type_1 in &[ReceiptType::Read, ReceiptType::ReadPrivate] {
|
||||
for receipt_thread_1 in &[ReceiptThread::Unthreaded, ReceiptThread::Main] {
|
||||
for receipt_type_2 in &[ReceiptType::Read, ReceiptType::ReadPrivate] {
|
||||
for receipt_thread_2 in &[ReceiptThread::Unthreaded, ReceiptThread::Main] {
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([
|
||||
(
|
||||
owned_event_id!("$2"),
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(
|
||||
event_id!("$2"),
|
||||
user_id,
|
||||
receipt_type_1.clone(),
|
||||
user_id.to_owned(),
|
||||
receipt_thread_1.clone(),
|
||||
),
|
||||
(
|
||||
owned_event_id!("$3"),
|
||||
)
|
||||
.add(
|
||||
event_id!("$3"),
|
||||
user_id,
|
||||
receipt_type_2.clone(),
|
||||
user_id.to_owned(),
|
||||
receipt_thread_2.clone(),
|
||||
),
|
||||
(
|
||||
owned_event_id!("$1"),
|
||||
)
|
||||
.add(
|
||||
event_id!("$1"),
|
||||
user_id,
|
||||
receipt_type_1.clone(),
|
||||
user_id.to_owned(),
|
||||
receipt_thread_2.clone(),
|
||||
),
|
||||
]);
|
||||
)
|
||||
.build();
|
||||
|
||||
// When I compute the notifications for this room (with no new events),
|
||||
let mut read_receipts = RoomReadReceipts::default();
|
||||
@@ -1118,12 +1067,10 @@ mod tests {
|
||||
|
||||
let events = make_test_events(user_id!("@bob:example.org"));
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
owned_event_id!("$6"),
|
||||
ReceiptType::Read,
|
||||
user_id.clone(),
|
||||
ReceiptThread::Unthreaded,
|
||||
)]);
|
||||
let receipt_event = EventFactory::new()
|
||||
.read_receipts()
|
||||
.add(event_id!("$6"), &user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
|
||||
.build();
|
||||
|
||||
let mut read_receipts = RoomReadReceipts::default();
|
||||
assert!(read_receipts.pending.is_empty());
|
||||
@@ -1154,12 +1101,10 @@ mod tests {
|
||||
|
||||
let events = make_test_events(user_id!("@bob:example.org"));
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
owned_event_id!("$1"),
|
||||
ReceiptType::Read,
|
||||
user_id.clone(),
|
||||
ReceiptThread::Unthreaded,
|
||||
)]);
|
||||
let receipt_event = EventFactory::new()
|
||||
.read_receipts()
|
||||
.add(event_id!("$1"), &user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
|
||||
.build();
|
||||
|
||||
// Sync with a read receipt *and* a single event that was already known: in that
|
||||
// case, only consider the new events in isolation, and compute the
|
||||
@@ -1190,12 +1135,7 @@ mod tests {
|
||||
let events = make_test_events(uid);
|
||||
|
||||
// An event with no id.
|
||||
let ev6 = SyncTimelineEvent::new(sync_timeline_event!({
|
||||
"sender": uid,
|
||||
"type": "m.room.message",
|
||||
"origin_server_ts": 42,
|
||||
"content": { "body": "yolo", "msgtype": "m.text" },
|
||||
}));
|
||||
let ev6 = EventFactory::new().text_msg("yolo").sender(uid).no_event_id().into_event();
|
||||
|
||||
let index = ReceiptSelector::create_sync_index(events.iter().chain(&[ev6]));
|
||||
|
||||
@@ -1261,8 +1201,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_receipt_selector_handle_pending_receipts_noop() {
|
||||
let sender = user_id!("@bob:example.org");
|
||||
let ev1 = sync_timeline_message(sender, event_id!("$1"), "yo");
|
||||
let ev2 = sync_timeline_message(sender, event_id!("$2"), "well?");
|
||||
let f = EventFactory::new().sender(sender);
|
||||
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
|
||||
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
|
||||
let events: Vector<_> = vec![ev1, ev2].into();
|
||||
|
||||
{
|
||||
@@ -1296,8 +1237,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_receipt_selector_handle_pending_receipts_doesnt_match_known_events() {
|
||||
let sender = user_id!("@bob:example.org");
|
||||
let ev1 = sync_timeline_message(sender, event_id!("$1"), "yo");
|
||||
let ev2 = sync_timeline_message(sender, event_id!("$2"), "well?");
|
||||
let f = EventFactory::new().sender(sender);
|
||||
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
|
||||
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
|
||||
let events: Vector<_> = vec![ev1, ev2].into();
|
||||
|
||||
{
|
||||
@@ -1332,8 +1274,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_receipt_selector_handle_pending_receipts_matches_known_events_no_initial() {
|
||||
let sender = user_id!("@bob:example.org");
|
||||
let ev1 = sync_timeline_message(sender, event_id!("$1"), "yo");
|
||||
let ev2 = sync_timeline_message(sender, event_id!("$2"), "well?");
|
||||
let f = EventFactory::new().sender(sender);
|
||||
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
|
||||
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
|
||||
let events: Vector<_> = vec![ev1, ev2].into();
|
||||
|
||||
{
|
||||
@@ -1373,8 +1316,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_receipt_selector_handle_pending_receipts_matches_known_events_with_initial() {
|
||||
let sender = user_id!("@bob:example.org");
|
||||
let ev1 = sync_timeline_message(sender, event_id!("$1"), "yo");
|
||||
let ev2 = sync_timeline_message(sender, event_id!("$2"), "well?");
|
||||
let f = EventFactory::new().sender(sender);
|
||||
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
|
||||
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
|
||||
let events: Vector<_> = vec![ev1, ev2].into();
|
||||
|
||||
{
|
||||
@@ -1412,21 +1356,25 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_receipt_selector_handle_new_receipt() {
|
||||
let myself = owned_user_id!("@alice:example.org");
|
||||
let myself = user_id!("@alice:example.org");
|
||||
let events = make_test_events(user_id!("@bob:example.org"));
|
||||
|
||||
let f = EventFactory::new();
|
||||
{
|
||||
// Thread receipts are ignored.
|
||||
let mut selector = ReceiptSelector::new(&events, None);
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
owned_event_id!("$5"),
|
||||
ReceiptType::Read,
|
||||
myself.clone(),
|
||||
ReceiptThread::Thread(owned_event_id!("$2")),
|
||||
)]);
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(
|
||||
event_id!("$5"),
|
||||
myself,
|
||||
ReceiptType::Read,
|
||||
ReceiptThread::Thread(owned_event_id!("$2")),
|
||||
)
|
||||
.build();
|
||||
|
||||
let pending = selector.handle_new_receipt(&myself, &receipt_event);
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert!(pending.is_empty());
|
||||
|
||||
let best_receipt = selector.select();
|
||||
@@ -1440,14 +1388,12 @@ mod tests {
|
||||
// receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, None);
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
owned_event_id!("$6"),
|
||||
receipt_type.clone(),
|
||||
myself.clone(),
|
||||
receipt_thread.clone(),
|
||||
)]);
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$6"), myself, receipt_type.clone(), receipt_thread.clone())
|
||||
.build();
|
||||
|
||||
let pending = selector.handle_new_receipt(&myself, &receipt_event);
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert_eq!(pending[0], event_id!("$6"));
|
||||
assert_eq!(pending.len(), 1);
|
||||
|
||||
@@ -1460,14 +1406,12 @@ mod tests {
|
||||
// receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, None);
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
owned_event_id!("$3"),
|
||||
receipt_type.clone(),
|
||||
myself.clone(),
|
||||
receipt_thread.clone(),
|
||||
)]);
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$3"), myself, receipt_type.clone(), receipt_thread.clone())
|
||||
.build();
|
||||
|
||||
let pending = selector.handle_new_receipt(&myself, &receipt_event);
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert!(pending.is_empty());
|
||||
|
||||
let best_receipt = selector.select();
|
||||
@@ -1479,14 +1423,12 @@ mod tests {
|
||||
// better receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$4")));
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
owned_event_id!("$3"),
|
||||
receipt_type.clone(),
|
||||
myself.clone(),
|
||||
receipt_thread.clone(),
|
||||
)]);
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$3"), myself, receipt_type.clone(), receipt_thread.clone())
|
||||
.build();
|
||||
|
||||
let pending = selector.handle_new_receipt(&myself, &receipt_event);
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert!(pending.is_empty());
|
||||
|
||||
let best_receipt = selector.select();
|
||||
@@ -1498,14 +1440,12 @@ mod tests {
|
||||
// new better receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$2")));
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
owned_event_id!("$3"),
|
||||
receipt_type.clone(),
|
||||
myself.clone(),
|
||||
receipt_thread.clone(),
|
||||
)]);
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$3"), myself, receipt_type.clone(), receipt_thread.clone())
|
||||
.build();
|
||||
|
||||
let pending = selector.handle_new_receipt(&myself, &receipt_event);
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert!(pending.is_empty());
|
||||
|
||||
let best_receipt = selector.select();
|
||||
@@ -1519,23 +1459,14 @@ mod tests {
|
||||
// new better receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$2")));
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([
|
||||
(
|
||||
owned_event_id!("$4"),
|
||||
ReceiptType::ReadPrivate,
|
||||
myself.clone(),
|
||||
ReceiptThread::Unthreaded,
|
||||
),
|
||||
(
|
||||
owned_event_id!("$6"),
|
||||
ReceiptType::ReadPrivate,
|
||||
myself.clone(),
|
||||
ReceiptThread::Main,
|
||||
),
|
||||
(owned_event_id!("$3"), ReceiptType::Read, myself.clone(), ReceiptThread::Main),
|
||||
]);
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$4"), myself, ReceiptType::ReadPrivate, ReceiptThread::Unthreaded)
|
||||
.add(event_id!("$6"), myself, ReceiptType::ReadPrivate, ReceiptThread::Main)
|
||||
.add(event_id!("$3"), myself, ReceiptType::Read, ReceiptThread::Main)
|
||||
.build();
|
||||
|
||||
let pending = selector.handle_new_receipt(&myself, &receipt_event);
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert_eq!(pending.len(), 1);
|
||||
assert_eq!(pending[0], event_id!("$6"));
|
||||
|
||||
@@ -1560,8 +1491,16 @@ mod tests {
|
||||
assert!(best_receipt.is_none());
|
||||
|
||||
// Now, if there are events I've written too...
|
||||
events.push_back(sync_timeline_message(&myself, "$6", "A mulatto, an albino"));
|
||||
events.push_back(sync_timeline_message(bob, "$7", "A mosquito, my libido"));
|
||||
let f = EventFactory::new();
|
||||
events.push_back(
|
||||
f.text_msg("A mulatto, an albino")
|
||||
.sender(&myself)
|
||||
.event_id(event_id!("$6"))
|
||||
.into_event(),
|
||||
);
|
||||
events.push_back(
|
||||
f.text_msg("A mosquito, my libido").sender(bob).event_id(event_id!("$7")).into_event(),
|
||||
);
|
||||
|
||||
let mut selector = ReceiptSelector::new(&events, None);
|
||||
// And I search for my implicit read receipt,
|
||||
@@ -1573,7 +1512,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_compute_unread_counts_with_implicit_receipt() {
|
||||
let user_id = owned_user_id!("@alice:example.org");
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
let bob = user_id!("@bob:example.org");
|
||||
let room_id = room_id!("!room:example.org");
|
||||
|
||||
@@ -1581,28 +1520,36 @@ mod tests {
|
||||
let mut events = make_test_events(bob);
|
||||
|
||||
// One by me,
|
||||
events.push_back(sync_timeline_message(&user_id, "$6", "A mulatto, an albino"));
|
||||
let f = EventFactory::new();
|
||||
events.push_back(
|
||||
f.text_msg("A mulatto, an albino")
|
||||
.sender(user_id)
|
||||
.event_id(event_id!("$6"))
|
||||
.into_event(),
|
||||
);
|
||||
|
||||
// And others by Bob,
|
||||
events.push_back(sync_timeline_message(bob, "$7", "A mosquito, my libido"));
|
||||
events.push_back(sync_timeline_message(bob, "$8", "A denial, a denial"));
|
||||
events.push_back(
|
||||
f.text_msg("A mosquito, my libido").sender(bob).event_id(event_id!("$7")).into_event(),
|
||||
);
|
||||
events.push_back(
|
||||
f.text_msg("A denial, a denial").sender(bob).event_id(event_id!("$8")).into_event(),
|
||||
);
|
||||
|
||||
let events: Vec<_> = events.into_iter().collect();
|
||||
|
||||
// I have a read receipt attached to one of Bob's event sent before my message,
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
owned_event_id!("$3"),
|
||||
ReceiptType::Read,
|
||||
user_id.clone(),
|
||||
ReceiptThread::Unthreaded,
|
||||
)]);
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$3"), user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
|
||||
.build();
|
||||
|
||||
let mut read_receipts = RoomReadReceipts::default();
|
||||
|
||||
// And I compute the unread counts for all those new events (no previous events
|
||||
// in that room),
|
||||
compute_unread_counts(
|
||||
&user_id,
|
||||
user_id,
|
||||
room_id,
|
||||
Some(&receipt_event),
|
||||
Vector::new(),
|
||||
|
||||
@@ -12,8 +12,8 @@ use std::{
|
||||
use bitflags::bitflags;
|
||||
pub use members::RoomMember;
|
||||
pub use normal::{
|
||||
Room, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomState,
|
||||
RoomStateFilter,
|
||||
apply_redaction, Room, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons,
|
||||
RoomMembersUpdate, RoomState, RoomStateFilter,
|
||||
};
|
||||
use regex::Regex;
|
||||
use ruma::{
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use std::sync::RwLock as SyncRwLock;
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
collections::{BTreeMap, BTreeSet, HashSet},
|
||||
mem,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
@@ -24,12 +24,9 @@ use as_variant::as_variant;
|
||||
use bitflags::bitflags;
|
||||
use eyeball::{AsyncLock, ObservableWriteGuard, SharedObservable, Subscriber};
|
||||
use futures_util::{Stream, StreamExt};
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEventKind;
|
||||
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_common::ring_buffer::RingBuffer;
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
use ruma::events::AnySyncTimelineEvent;
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::v3::RoomSummary as RumaSummary,
|
||||
events::{
|
||||
@@ -51,7 +48,7 @@ use ruma::{
|
||||
tombstone::RoomTombstoneEventContent,
|
||||
},
|
||||
tag::{TagEventContent, Tags},
|
||||
AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent,
|
||||
AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent,
|
||||
RoomAccountDataEventType, StateEventType, SyncStateEvent,
|
||||
},
|
||||
room::RoomType,
|
||||
@@ -67,12 +64,11 @@ use super::{
|
||||
members::MemberRoomInfo, BaseRoomInfo, RoomCreateWithCreatorEventContent, RoomDisplayName,
|
||||
RoomMember, RoomNotableTags,
|
||||
};
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
use crate::latest_event::LatestEvent;
|
||||
use crate::{
|
||||
deserialized_responses::{
|
||||
DisplayName, MemberEvent, RawSyncOrStrippedState, SyncOrStrippedState,
|
||||
DisplayName, MemberEvent, RawMemberEvent, RawSyncOrStrippedState, SyncOrStrippedState,
|
||||
},
|
||||
latest_event::LatestEvent,
|
||||
notification_settings::RoomNotificationMode,
|
||||
read_receipts::RoomReadReceipts,
|
||||
store::{DynStateStore, Result as StoreResult, StateStoreExt},
|
||||
@@ -166,7 +162,7 @@ pub struct Room {
|
||||
/// not sure whether holding too many of them might make the cache too
|
||||
/// slow to load on startup. Keeping them here means they are not cached
|
||||
/// to disk but held in memory.
|
||||
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub latest_encrypted_events: Arc<SyncRwLock<RingBuffer<Raw<AnySyncTimelineEvent>>>>,
|
||||
|
||||
/// A map for ids of room membership events in the knocking state linked to
|
||||
@@ -174,6 +170,9 @@ pub struct Room {
|
||||
/// user has marked as seen so they can be ignored.
|
||||
pub seen_knock_request_ids_map:
|
||||
SharedObservable<Option<BTreeMap<OwnedEventId, OwnedUserId>>, AsyncLock>,
|
||||
|
||||
/// A sender that will notify receivers when room member updates happen.
|
||||
pub room_member_updates_sender: broadcast::Sender<RoomMembersUpdate>,
|
||||
}
|
||||
|
||||
/// The room summary containing member counts and members that should be used to
|
||||
@@ -262,10 +261,19 @@ fn heroes_filter<'a>(
|
||||
move |user_id| user_id != own_user_id && !member_hints.service_members.contains(user_id)
|
||||
}
|
||||
|
||||
/// The kind of room member updates that just happened.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RoomMembersUpdate {
|
||||
/// The whole list room members was reloaded.
|
||||
FullReload,
|
||||
/// A few members were updated, their user ids are included.
|
||||
Partial(BTreeSet<OwnedUserId>),
|
||||
}
|
||||
|
||||
impl Room {
|
||||
/// The size of the latest_encrypted_events RingBuffer
|
||||
// SAFETY: `new_unchecked` is safe because 10 is not zero.
|
||||
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
const MAX_ENCRYPTED_EVENTS: std::num::NonZeroUsize =
|
||||
unsafe { std::num::NonZeroUsize::new_unchecked(10) };
|
||||
|
||||
@@ -286,17 +294,19 @@ impl Room {
|
||||
room_info: RoomInfo,
|
||||
room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
|
||||
) -> Self {
|
||||
let (room_member_updates_sender, _) = broadcast::channel(10);
|
||||
Self {
|
||||
own_user_id: own_user_id.into(),
|
||||
room_id: room_info.room_id.clone(),
|
||||
store,
|
||||
inner: SharedObservable::new(room_info),
|
||||
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
latest_encrypted_events: Arc::new(SyncRwLock::new(RingBuffer::new(
|
||||
Self::MAX_ENCRYPTED_EVENTS,
|
||||
))),
|
||||
room_info_notable_update_sender,
|
||||
seen_knock_request_ids_map: SharedObservable::new_async(None),
|
||||
room_member_updates_sender,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,6 +391,17 @@ impl Room {
|
||||
self.inner.read().members_synced
|
||||
}
|
||||
|
||||
/// Mark this Room as holding all member information.
|
||||
///
|
||||
/// Useful in tests if we want to persuade the Room not to sync when asked
|
||||
/// about its members.
|
||||
#[cfg(feature = "testing")]
|
||||
pub fn mark_members_synced(&self) {
|
||||
self.inner.update(|info| {
|
||||
info.members_synced = true;
|
||||
});
|
||||
}
|
||||
|
||||
/// Mark this Room as still missing member information.
|
||||
pub fn mark_members_missing(&self) {
|
||||
self.inner.update_if(|info| {
|
||||
@@ -609,18 +630,40 @@ impl Room {
|
||||
self.inner.read().active_room_call_participants()
|
||||
}
|
||||
|
||||
/// Return the cached display name of the room if it was provided via sync,
|
||||
/// or otherwise calculate it, taking into account its name, aliases and
|
||||
/// members.
|
||||
/// Calculate a room's display name, or return the cached value, taking into
|
||||
/// account its name, aliases and members.
|
||||
///
|
||||
/// The display name is calculated according to [this algorithm][spec].
|
||||
///
|
||||
/// This is automatically recomputed on every successful sync, and the
|
||||
/// cached result can be retrieved in
|
||||
/// [`Self::cached_display_name`].
|
||||
/// While the underlying computation can be slow, the result is cached and
|
||||
/// returned on the following calls. The cache is also filled on every
|
||||
/// successful sync, since a sync may cause a change in the display
|
||||
/// name.
|
||||
///
|
||||
/// If you need a variant that's sync (but with the drawback that it returns
|
||||
/// an `Option`), consider using [`Room::cached_display_name`].
|
||||
///
|
||||
/// [spec]: <https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room>
|
||||
pub async fn compute_display_name(&self) -> StoreResult<RoomDisplayName> {
|
||||
pub async fn display_name(&self) -> StoreResult<RoomDisplayName> {
|
||||
if let Some(name) = self.cached_display_name() {
|
||||
Ok(name)
|
||||
} else {
|
||||
self.compute_display_name().await
|
||||
}
|
||||
}
|
||||
|
||||
/// Force recalculating a room's display name, taking into account its name,
|
||||
/// aliases and members.
|
||||
///
|
||||
/// The display name is calculated according to [this algorithm][spec].
|
||||
///
|
||||
/// ⚠ This may be slowish to compute. As such, the result is cached and can
|
||||
/// be retrieved via [`Room::cached_display_name`] (sync, returns an option)
|
||||
/// or [`Room::display_name`] (async, always returns a value), which should
|
||||
/// be preferred in general.
|
||||
///
|
||||
/// [spec]: <https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room>
|
||||
pub(crate) async fn compute_display_name(&self) -> StoreResult<RoomDisplayName> {
|
||||
enum DisplayNameOrSummary {
|
||||
Summary(RoomSummary),
|
||||
DisplayName(RoomDisplayName),
|
||||
@@ -857,8 +900,7 @@ impl Room {
|
||||
|
||||
/// Returns the cached computed display name, if available.
|
||||
///
|
||||
/// This cache is refilled every time we call
|
||||
/// [`Self::compute_display_name`].
|
||||
/// This cache is refilled every time we call [`Self::display_name`].
|
||||
pub fn cached_display_name(&self) -> Option<RoomDisplayName> {
|
||||
self.inner.read().cached_display_name.clone()
|
||||
}
|
||||
@@ -890,7 +932,6 @@ impl Room {
|
||||
|
||||
/// Return the last event in this room, if one has been cached during
|
||||
/// sliding sync.
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
pub fn latest_event(&self) -> Option<LatestEvent> {
|
||||
self.inner.read().latest_event.as_deref().cloned()
|
||||
}
|
||||
@@ -899,7 +940,7 @@ impl Room {
|
||||
/// to decrypt these, the most recent relevant one will replace
|
||||
/// latest_event. (We can't tell which one is relevant until
|
||||
/// they are decrypted.)
|
||||
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub(crate) fn latest_encrypted_events(&self) -> Vec<Raw<AnySyncTimelineEvent>> {
|
||||
self.latest_encrypted_events.read().unwrap().iter().cloned().collect()
|
||||
}
|
||||
@@ -914,7 +955,7 @@ impl Room {
|
||||
///
|
||||
/// 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"))]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub(crate) fn on_latest_event_decrypted(
|
||||
&self,
|
||||
latest_event: Box<LatestEvent>,
|
||||
@@ -1160,7 +1201,6 @@ impl Room {
|
||||
/// Returns the recency stamp of the room.
|
||||
///
|
||||
/// Please read `RoomInfo::recency_stamp` to learn more.
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
pub fn recency_stamp(&self) -> Option<u64> {
|
||||
self.inner.read().recency_stamp
|
||||
}
|
||||
@@ -1207,28 +1247,61 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
let mut current_seen_events_guard = self.seen_knock_request_ids_map.write().await;
|
||||
// We're not calling `get_seen_join_request_ids` here because we need to keep
|
||||
// the Mutex's guard until we've updated the data
|
||||
let mut current_seen_events = if current_seen_events_guard.is_none() {
|
||||
self.load_cached_knock_request_ids().await?
|
||||
} else {
|
||||
current_seen_events_guard.clone().unwrap()
|
||||
};
|
||||
let current_seen_events_guard = self.get_write_guarded_current_knock_request_ids().await?;
|
||||
let mut current_seen_events = current_seen_events_guard.clone().unwrap_or_default();
|
||||
|
||||
current_seen_events.extend(event_to_user_ids);
|
||||
|
||||
ObservableWriteGuard::set(
|
||||
&mut current_seen_events_guard,
|
||||
Some(current_seen_events.clone()),
|
||||
);
|
||||
self.update_seen_knock_request_ids(current_seen_events_guard, current_seen_events).await?;
|
||||
|
||||
self.store
|
||||
.set_kv_data(
|
||||
StateStoreDataKey::SeenKnockRequests(self.room_id()),
|
||||
StateStoreDataValue::SeenKnockRequests(current_seen_events),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes the seen knock request ids that are no longer valid given the
|
||||
/// current room members.
|
||||
pub async fn remove_outdated_seen_knock_requests_ids(&self) -> StoreResult<()> {
|
||||
let current_seen_events_guard = self.get_write_guarded_current_knock_request_ids().await?;
|
||||
let mut current_seen_events = current_seen_events_guard.clone().unwrap_or_default();
|
||||
|
||||
// Get and deserialize the member events for the seen knock requests
|
||||
let keys: Vec<OwnedUserId> = current_seen_events.values().map(|id| id.to_owned()).collect();
|
||||
let raw_member_events: Vec<RawMemberEvent> =
|
||||
self.store.get_state_events_for_keys_static(self.room_id(), &keys).await?;
|
||||
let member_events = raw_member_events
|
||||
.into_iter()
|
||||
.map(|raw| raw.deserialize())
|
||||
.collect::<Result<Vec<MemberEvent>, _>>()?;
|
||||
|
||||
let mut ids_to_remove = Vec::new();
|
||||
|
||||
for (event_id, user_id) in current_seen_events.iter() {
|
||||
// Check the seen knock request ids against the current room member events for
|
||||
// the room members associated to them
|
||||
let matching_member = member_events.iter().find(|event| event.user_id() == user_id);
|
||||
|
||||
if let Some(member) = matching_member {
|
||||
let member_event_id = member.event_id();
|
||||
// If the member event is not a knock or it's different knock, it's outdated
|
||||
if *member.membership() != MembershipState::Knock
|
||||
|| member_event_id.is_some_and(|id| id != event_id)
|
||||
{
|
||||
ids_to_remove.push(event_id.to_owned());
|
||||
}
|
||||
} else {
|
||||
ids_to_remove.push(event_id.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no ids to remove, do nothing
|
||||
if ids_to_remove.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for event_id in ids_to_remove {
|
||||
current_seen_events.remove(&event_id);
|
||||
}
|
||||
|
||||
self.update_seen_knock_request_ids(current_seen_events_guard, current_seen_events).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1237,27 +1310,46 @@ impl Room {
|
||||
pub async fn get_seen_knock_request_ids(
|
||||
&self,
|
||||
) -> Result<BTreeMap<OwnedEventId, OwnedUserId>, StoreError> {
|
||||
let mut guard = self.seen_knock_request_ids_map.write().await;
|
||||
if guard.is_none() {
|
||||
ObservableWriteGuard::set(
|
||||
&mut guard,
|
||||
Some(self.load_cached_knock_request_ids().await?),
|
||||
);
|
||||
}
|
||||
Ok(guard.clone().unwrap_or_default())
|
||||
Ok(self.get_write_guarded_current_knock_request_ids().await?.clone().unwrap_or_default())
|
||||
}
|
||||
|
||||
/// This loads the current list of seen knock request ids from the state
|
||||
/// store.
|
||||
async fn load_cached_knock_request_ids(
|
||||
async fn get_write_guarded_current_knock_request_ids(
|
||||
&self,
|
||||
) -> StoreResult<BTreeMap<OwnedEventId, OwnedUserId>> {
|
||||
Ok(self
|
||||
.store
|
||||
.get_kv_data(StateStoreDataKey::SeenKnockRequests(self.room_id()))
|
||||
.await?
|
||||
.and_then(|v| v.into_seen_knock_requests())
|
||||
.unwrap_or_default())
|
||||
) -> StoreResult<ObservableWriteGuard<'_, Option<BTreeMap<OwnedEventId, OwnedUserId>>, AsyncLock>>
|
||||
{
|
||||
let mut guard = self.seen_knock_request_ids_map.write().await;
|
||||
// If there are no loaded request ids yet
|
||||
if guard.is_none() {
|
||||
// Load the values from the store and update the shared observable contents
|
||||
let updated_seen_ids = self
|
||||
.store
|
||||
.get_kv_data(StateStoreDataKey::SeenKnockRequests(self.room_id()))
|
||||
.await?
|
||||
.and_then(|v| v.into_seen_knock_requests())
|
||||
.unwrap_or_default();
|
||||
|
||||
ObservableWriteGuard::set(&mut guard, Some(updated_seen_ids));
|
||||
}
|
||||
Ok(guard)
|
||||
}
|
||||
|
||||
async fn update_seen_knock_request_ids(
|
||||
&self,
|
||||
mut guard: ObservableWriteGuard<'_, Option<BTreeMap<OwnedEventId, OwnedUserId>>, AsyncLock>,
|
||||
new_value: BTreeMap<OwnedEventId, OwnedUserId>,
|
||||
) -> StoreResult<()> {
|
||||
// Save the new values to the shared observable
|
||||
ObservableWriteGuard::set(&mut guard, Some(new_value.clone()));
|
||||
|
||||
// Save them into the store too
|
||||
self.store
|
||||
.set_kv_data(
|
||||
StateStoreDataKey::SeenKnockRequests(self.room_id()),
|
||||
StateStoreDataValue::SeenKnockRequests(new_value),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1318,7 +1410,6 @@ pub struct RoomInfo {
|
||||
pub(crate) encryption_state_synced: bool,
|
||||
|
||||
/// The last event send by sliding sync
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
pub(crate) latest_event: Option<Box<LatestEvent>>,
|
||||
|
||||
/// Information about read receipts for this room.
|
||||
@@ -1352,7 +1443,6 @@ pub struct RoomInfo {
|
||||
/// Sliding Sync might "ignore” some events when computing the recency
|
||||
/// stamp of the room. Thus, using this `recency_stamp` value is
|
||||
/// more accurate than relying on the latest event.
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
#[serde(default)]
|
||||
pub(crate) recency_stamp: Option<u64>,
|
||||
}
|
||||
@@ -1388,14 +1478,12 @@ impl RoomInfo {
|
||||
last_prev_batch: None,
|
||||
sync_info: SyncInfo::NoState,
|
||||
encryption_state_synced: false,
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
latest_event: None,
|
||||
read_receipts: Default::default(),
|
||||
base_info: Box::new(BaseRoomInfo::new()),
|
||||
warned_about_unknown_room_version: Arc::new(false.into()),
|
||||
cached_display_name: None,
|
||||
cached_user_defined_notification_mode: None,
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
recency_stamp: None,
|
||||
}
|
||||
}
|
||||
@@ -1540,7 +1628,6 @@ impl RoomInfo {
|
||||
};
|
||||
tracing::Span::current().record("redacts", debug(redacts));
|
||||
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
if let Some(latest_event) = &mut self.latest_event {
|
||||
tracing::trace!("Checking if redaction applies to latest event");
|
||||
if latest_event.event_id().as_deref() == Some(redacts) {
|
||||
@@ -1630,19 +1717,16 @@ impl RoomInfo {
|
||||
}
|
||||
|
||||
/// Updates the joined member count.
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
pub(crate) fn update_joined_member_count(&mut self, count: u64) {
|
||||
self.summary.joined_member_count = count;
|
||||
}
|
||||
|
||||
/// Updates the invited member count.
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
pub(crate) fn update_invited_member_count(&mut self, count: u64) {
|
||||
self.summary.invited_member_count = count;
|
||||
}
|
||||
|
||||
/// Updates the room heroes.
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
pub(crate) fn update_heroes(&mut self, heroes: Vec<RoomHero>) {
|
||||
self.summary.room_heroes = heroes;
|
||||
}
|
||||
@@ -1842,7 +1926,6 @@ impl RoomInfo {
|
||||
}
|
||||
|
||||
/// Returns the latest (decrypted) event recorded for this room.
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
pub fn latest_event(&self) -> Option<&LatestEvent> {
|
||||
self.latest_event.as_deref()
|
||||
}
|
||||
@@ -1850,7 +1933,6 @@ impl RoomInfo {
|
||||
/// Updates the recency stamp of this room.
|
||||
///
|
||||
/// Please read [`Self::recency_stamp`] to learn more.
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
pub(crate) fn update_recency_stamp(&mut self, stamp: u64) {
|
||||
self.recency_stamp = Some(stamp);
|
||||
}
|
||||
@@ -1936,8 +2018,9 @@ impl RoomInfo {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
fn apply_redaction(
|
||||
/// Apply a redaction to the given target `event`, given the raw redaction event
|
||||
/// and the room version.
|
||||
pub fn apply_redaction(
|
||||
event: &Raw<AnySyncTimelineEvent>,
|
||||
raw_redaction: &Raw<SyncRoomRedactionEvent>,
|
||||
room_version: &RoomVersionId,
|
||||
@@ -1963,7 +2046,7 @@ fn apply_redaction(
|
||||
let redact_result = redact_in_place(&mut event_json, room_version, Some(redacted_because));
|
||||
|
||||
if let Err(e) = redact_result {
|
||||
warn!("Failed to redact latest event: {e}");
|
||||
warn!("Failed to redact event: {e}");
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -2081,8 +2164,7 @@ mod tests {
|
||||
};
|
||||
|
||||
use assign::assign;
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use matrix_sdk_test::{
|
||||
async_test,
|
||||
event_factory::EventFactory,
|
||||
@@ -2118,9 +2200,8 @@ mod tests {
|
||||
use stream_assert::{assert_pending, assert_ready};
|
||||
|
||||
use super::{compute_display_name_from_heroes, Room, RoomHero, RoomInfo, RoomState, SyncInfo};
|
||||
#[cfg(any(feature = "experimental-sliding-sync", feature = "e2e-encryption"))]
|
||||
use crate::latest_event::LatestEvent;
|
||||
use crate::{
|
||||
latest_event::LatestEvent,
|
||||
rooms::RoomNotableTags,
|
||||
store::{IntoStateStore, MemoryStore, StateChanges, StateStore, StoreConfig},
|
||||
test_utils::logged_in_base_client,
|
||||
@@ -2129,7 +2210,6 @@ mod tests {
|
||||
};
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
fn test_room_info_serialization() {
|
||||
// This test exists to make sure we don't accidentally change the
|
||||
// serialized format for `RoomInfo`.
|
||||
@@ -2161,7 +2241,7 @@ mod tests {
|
||||
last_prev_batch: Some("pb".to_owned()),
|
||||
sync_info: SyncInfo::FullySynced,
|
||||
encryption_state_synced: true,
|
||||
latest_event: Some(Box::new(LatestEvent::new(SyncTimelineEvent::new(
|
||||
latest_event: Some(Box::new(LatestEvent::new(TimelineEvent::new(
|
||||
Raw::from_json_string(json!({"sender": "@u:i.uk"}).to_string()).unwrap(),
|
||||
)))),
|
||||
base_info: Box::new(
|
||||
@@ -2316,7 +2396,6 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
fn test_room_info_deserialization() {
|
||||
use ruma::{owned_mxc_uri, owned_user_id};
|
||||
|
||||
@@ -3066,8 +3145,8 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
async fn test_setting_the_latest_event_doesnt_cause_a_room_info_notable_update() {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -3086,7 +3165,6 @@ mod tests {
|
||||
user_id: user_id!("@alice:example.org").into(),
|
||||
device_id: ruma::device_id!("AYEAYEAYE").into(),
|
||||
},
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
None,
|
||||
)
|
||||
.await
|
||||
@@ -3134,8 +3212,8 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
async fn test_when_we_provide_a_newly_decrypted_event_it_replaces_latest_event() {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -3164,8 +3242,8 @@ mod tests {
|
||||
assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
async fn test_when_a_newly_decrypted_event_appears_we_delete_all_older_encrypted_events() {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -3203,8 +3281,8 @@ mod tests {
|
||||
assert_eq!(room.latest_event().unwrap().event_id(), new_event.event_id());
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
async fn test_replacing_the_newest_event_leaves_none_left() {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -3236,7 +3314,7 @@ mod tests {
|
||||
assert_eq!(enc_evs.len(), 0);
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
fn add_encrypted_event(room: &Room, event_id: &str) {
|
||||
room.latest_encrypted_events
|
||||
.write()
|
||||
@@ -3244,9 +3322,9 @@ mod tests {
|
||||
.push(Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap());
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
fn make_latest_event(event_id: &str) -> Box<LatestEvent> {
|
||||
Box::new(LatestEvent::new(SyncTimelineEvent::new(
|
||||
Box::new(LatestEvent::new(TimelineEvent::new(
|
||||
Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap(),
|
||||
)))
|
||||
}
|
||||
|
||||
@@ -21,27 +21,21 @@ use std::ops::Deref;
|
||||
use std::{borrow::Cow, collections::BTreeMap};
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::api::client::sync::sync_events::v5;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::events::AnyToDeviceEvent;
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::v3::{self, InvitedRoom, KnockedRoom},
|
||||
events::{
|
||||
room::member::MembershipState, AnyRoomAccountDataEvent, AnyStrippedStateEvent,
|
||||
AnySyncStateEvent, StateEventType,
|
||||
AnySyncStateEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
JsOption, OwnedRoomId, RoomId, UInt, UserId,
|
||||
};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::{api::client::sync::sync_events::v5, events::AnyToDeviceEvent, events::StateEventType};
|
||||
use tracing::{debug, error, instrument, trace, warn};
|
||||
|
||||
use super::BaseClient;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use crate::latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use crate::RoomMemberships;
|
||||
use crate::{
|
||||
error::Result,
|
||||
read_receipts::{compute_unread_counts, PreviousEventsProvider},
|
||||
@@ -55,6 +49,11 @@ use crate::{
|
||||
sync::{JoinedRoomUpdate, LeftRoomUpdate, Notification, RoomUpdates, SyncResponse},
|
||||
Room, RoomInfo,
|
||||
};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use crate::{
|
||||
latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent},
|
||||
RoomMemberships,
|
||||
};
|
||||
|
||||
impl BaseClient {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
@@ -691,7 +690,7 @@ impl BaseClient {
|
||||
async fn cache_latest_events(
|
||||
room: &Room,
|
||||
room_info: &mut RoomInfo,
|
||||
events: &[SyncTimelineEvent],
|
||||
events: &[TimelineEvent],
|
||||
changes: Option<&StateChanges>,
|
||||
store: Option<&Store>,
|
||||
) {
|
||||
@@ -894,16 +893,17 @@ fn process_room_properties(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(test, not(target_family = "wasm")))]
|
||||
mod tests {
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
sync::{Arc, RwLock as SyncRwLock},
|
||||
};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use std::sync::{Arc, RwLock as SyncRwLock};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_common::{
|
||||
deserialized_responses::{SyncTimelineEvent, UnableToDecryptInfo, UnableToDecryptReason},
|
||||
deserialized_responses::{UnableToDecryptInfo, UnableToDecryptReason},
|
||||
ring_buffer::RingBuffer,
|
||||
};
|
||||
use matrix_sdk_test::async_test;
|
||||
@@ -929,13 +929,16 @@ mod tests {
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use super::{cache_latest_events, http};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use super::cache_latest_events;
|
||||
use super::http;
|
||||
use crate::{
|
||||
rooms::normal::{RoomHero, RoomInfoNotableUpdateReasons},
|
||||
store::MemoryStore,
|
||||
test_utils::logged_in_base_client,
|
||||
BaseClient, Room, RoomInfoNotableUpdate, RoomState,
|
||||
BaseClient, RoomInfoNotableUpdate, RoomState,
|
||||
};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use crate::{store::MemoryStore, Room};
|
||||
|
||||
#[async_test]
|
||||
async fn test_notification_count_set() {
|
||||
@@ -1939,6 +1942,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_when_no_events_we_dont_cache_any() {
|
||||
let events = &[];
|
||||
@@ -1946,6 +1950,7 @@ mod tests {
|
||||
assert!(chosen.is_none());
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_when_only_one_event_we_cache_it() {
|
||||
let event1 = make_event("m.room.message", "$1");
|
||||
@@ -1954,6 +1959,7 @@ mod tests {
|
||||
assert_eq!(ev_id(chosen), rawev_id(event1));
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_with_multiple_events_we_cache_the_last_one() {
|
||||
let event1 = make_event("m.room.message", "$1");
|
||||
@@ -1963,6 +1969,7 @@ mod tests {
|
||||
assert_eq!(ev_id(chosen), rawev_id(event2));
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_cache_the_latest_relevant_event_and_ignore_irrelevant_ones_even_if_later() {
|
||||
let event1 = make_event("m.room.message", "$1");
|
||||
@@ -1974,6 +1981,7 @@ mod tests {
|
||||
assert_eq!(ev_id(chosen), rawev_id(event2));
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_prefer_to_cache_nothing_rather_than_irrelevant_events() {
|
||||
let event1 = make_event("m.room.power_levels", "$1");
|
||||
@@ -1982,6 +1990,7 @@ mod tests {
|
||||
assert!(chosen.is_none());
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_cache_encrypted_events_that_are_after_latest_message() {
|
||||
// Given two message events followed by two encrypted
|
||||
@@ -2012,6 +2021,7 @@ mod tests {
|
||||
assert_eq!(rawevs_ids(&room.latest_encrypted_events), evs_ids(&[event3, event4]));
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_dont_cache_encrypted_events_that_are_before_latest_message() {
|
||||
// Given an encrypted event before and after the message
|
||||
@@ -2036,6 +2046,7 @@ mod tests {
|
||||
assert_eq!(rawevs_ids(&room.latest_encrypted_events), evs_ids(&[event3]));
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_skip_irrelevant_events_eg_receipts_even_if_after_message() {
|
||||
// Given two message events followed by two encrypted, with a receipt in the
|
||||
@@ -2063,6 +2074,7 @@ mod tests {
|
||||
assert_eq!(rawevs_ids(&room.latest_encrypted_events), evs_ids(&[event3, event5]));
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_only_store_the_max_number_of_encrypted_events() {
|
||||
// Given two message events followed by lots of encrypted and other irrelevant
|
||||
@@ -2121,6 +2133,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_dont_overflow_capacity_if_previous_encrypted_events_exist() {
|
||||
// Given a RoomInfo with lots of encrypted events already inside it
|
||||
@@ -2163,6 +2176,7 @@ mod tests {
|
||||
assert_eq!(rawevs_ids(&room.latest_encrypted_events)[9], "$a");
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_existing_encrypted_events_are_deleted_if_we_receive_unencrypted() {
|
||||
// Given a RoomInfo with some encrypted events already inside it
|
||||
@@ -2591,7 +2605,8 @@ mod tests {
|
||||
assert!(room_2.is_direct().await.unwrap());
|
||||
}
|
||||
|
||||
async fn choose_event_to_cache(events: &[SyncTimelineEvent]) -> Option<SyncTimelineEvent> {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
async fn choose_event_to_cache(events: &[TimelineEvent]) -> Option<TimelineEvent> {
|
||||
let room = make_room();
|
||||
let mut room_info = room.clone_info();
|
||||
cache_latest_events(&room, &mut room_info, events, None, None).await;
|
||||
@@ -2599,22 +2614,26 @@ mod tests {
|
||||
room.latest_event().map(|latest_event| latest_event.event().clone())
|
||||
}
|
||||
|
||||
fn rawev_id(event: SyncTimelineEvent) -> String {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
fn rawev_id(event: TimelineEvent) -> String {
|
||||
event.event_id().unwrap().to_string()
|
||||
}
|
||||
|
||||
fn ev_id(event: Option<SyncTimelineEvent>) -> String {
|
||||
fn ev_id(event: Option<TimelineEvent>) -> String {
|
||||
event.unwrap().event_id().unwrap().to_string()
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
fn rawevs_ids(events: &Arc<SyncRwLock<RingBuffer<Raw<AnySyncTimelineEvent>>>>) -> Vec<String> {
|
||||
events.read().unwrap().iter().map(|e| e.get_field("event_id").unwrap().unwrap()).collect()
|
||||
}
|
||||
|
||||
fn evs_ids(events: &[SyncTimelineEvent]) -> Vec<String> {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
fn evs_ids(events: &[TimelineEvent]) -> Vec<String> {
|
||||
events.iter().map(|e| e.event_id().unwrap().to_string()).collect()
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
fn make_room() -> Room {
|
||||
let (sender, _receiver) = tokio::sync::broadcast::channel(1);
|
||||
|
||||
@@ -2641,12 +2660,14 @@ mod tests {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn make_event(typ: &str, id: &str) -> SyncTimelineEvent {
|
||||
SyncTimelineEvent::new(make_raw_event(typ, id))
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
fn make_event(typ: &str, id: &str) -> TimelineEvent {
|
||||
TimelineEvent::new(make_raw_event(typ, id))
|
||||
}
|
||||
|
||||
fn make_encrypted_event(id: &str) -> SyncTimelineEvent {
|
||||
SyncTimelineEvent::new_utd_event(
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
fn make_encrypted_event(id: &str) -> TimelineEvent {
|
||||
TimelineEvent::new_utd_event(
|
||||
Raw::from_json_string(
|
||||
json!({
|
||||
"type": "m.room.encrypted",
|
||||
|
||||
@@ -430,7 +430,7 @@ mod test {
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "alice"), ("@bob:localhost", "аlice")],
|
||||
[("alice", true)],
|
||||
"Bob tries to impersonate Alice using a cyrilic а"
|
||||
"Bob tries to impersonate Alice using a cyrillic а"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
|
||||
@@ -29,7 +29,8 @@ use ruma::{
|
||||
},
|
||||
owned_event_id, owned_mxc_uri, room_id,
|
||||
serde::Raw,
|
||||
uint, user_id, EventId, OwnedEventId, OwnedUserId, RoomId, TransactionId, UserId,
|
||||
uint, user_id, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, RoomId,
|
||||
TransactionId, UserId,
|
||||
};
|
||||
use serde_json::{json, value::Value as JsonValue};
|
||||
|
||||
@@ -980,13 +981,21 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let ev =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("sup").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id, txn.clone(), ev.into(), 0).await?;
|
||||
self.save_send_queue_request(
|
||||
room_id,
|
||||
txn.clone(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
ev.into(),
|
||||
0,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Add a single dependent queue request.
|
||||
self.save_dependent_queued_request(
|
||||
room_id,
|
||||
&txn,
|
||||
ChildTransactionId::new(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
DependentQueuedRequestKind::RedactEvent,
|
||||
)
|
||||
.await?;
|
||||
@@ -1242,7 +1251,15 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let event0 =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("msg0").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id, txn0.clone(), event0.into(), 0).await.unwrap();
|
||||
self.save_send_queue_request(
|
||||
room_id,
|
||||
txn0.clone(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
event0.into(),
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Reading it will work.
|
||||
let pending = self.load_send_queue_requests(room_id).await.unwrap();
|
||||
@@ -1266,7 +1283,15 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.save_send_queue_request(room_id, txn, event.into(), 0).await.unwrap();
|
||||
self.save_send_queue_request(
|
||||
room_id,
|
||||
txn,
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
event.into(),
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Reading all the events should work.
|
||||
@@ -1364,7 +1389,15 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let event =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("room2").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id2, txn.clone(), event.into(), 0).await.unwrap();
|
||||
self.save_send_queue_request(
|
||||
room_id2,
|
||||
txn.clone(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
event.into(),
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Add and remove one event for room3.
|
||||
@@ -1374,7 +1407,15 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let event =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("room3").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id3, txn.clone(), event.into(), 0).await.unwrap();
|
||||
self.save_send_queue_request(
|
||||
room_id3,
|
||||
txn.clone(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
event.into(),
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
self.remove_send_queue_request(room_id3, &txn).await.unwrap();
|
||||
}
|
||||
@@ -1399,21 +1440,45 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let ev0 =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("low0").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id, low0_txn.clone(), ev0.into(), 2).await.unwrap();
|
||||
self.save_send_queue_request(
|
||||
room_id,
|
||||
low0_txn.clone(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
ev0.into(),
|
||||
2,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Saving one request with higher priority should work.
|
||||
let high_txn = TransactionId::new();
|
||||
let ev1 =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("high").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id, high_txn.clone(), ev1.into(), 10).await.unwrap();
|
||||
self.save_send_queue_request(
|
||||
room_id,
|
||||
high_txn.clone(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
ev1.into(),
|
||||
10,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Saving another request with the low priority should work.
|
||||
let low1_txn = TransactionId::new();
|
||||
let ev2 =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("low1").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id, low1_txn.clone(), ev2.into(), 2).await.unwrap();
|
||||
self.save_send_queue_request(
|
||||
room_id,
|
||||
low1_txn.clone(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
ev2.into(),
|
||||
2,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// The requests should be ordered from higher priority to lower, and when equal,
|
||||
// should use the insertion order instead.
|
||||
@@ -1453,7 +1518,15 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let event0 =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("hey").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id, txn0.clone(), event0.into(), 0).await.unwrap();
|
||||
self.save_send_queue_request(
|
||||
room_id,
|
||||
txn0.clone(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
event0.into(),
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// No dependents, to start with.
|
||||
assert!(self.load_dependent_queued_requests(room_id).await.unwrap().is_empty());
|
||||
@@ -1464,6 +1537,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
room_id,
|
||||
&txn0,
|
||||
child_txn.clone(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
DependentQueuedRequestKind::RedactEvent,
|
||||
)
|
||||
.await
|
||||
@@ -1515,12 +1589,21 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let event1 =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("hey2").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id, txn1.clone(), event1.into(), 0).await.unwrap();
|
||||
self.save_send_queue_request(
|
||||
room_id,
|
||||
txn1.clone(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
event1.into(),
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
self.save_dependent_queued_request(
|
||||
room_id,
|
||||
&txn0,
|
||||
ChildTransactionId::new(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
DependentQueuedRequestKind::RedactEvent,
|
||||
)
|
||||
.await
|
||||
@@ -1531,6 +1614,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
room_id,
|
||||
&txn1,
|
||||
ChildTransactionId::new(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
DependentQueuedRequestKind::EditEvent {
|
||||
new_content: SerializableEventContent::new(
|
||||
&RoomMessageEventContent::text_plain("edit").into(),
|
||||
@@ -1563,6 +1647,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
room_id,
|
||||
&txn,
|
||||
child_txn.clone(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
DependentQueuedRequestKind::RedactEvent,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -30,8 +30,8 @@ use ruma::{
|
||||
},
|
||||
serde::Raw,
|
||||
time::Instant,
|
||||
CanonicalJsonObject, EventId, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedTransactionId,
|
||||
OwnedUserId, RoomId, RoomVersionId, TransactionId, UserId,
|
||||
CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri,
|
||||
OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, RoomVersionId, TransactionId, UserId,
|
||||
};
|
||||
use tracing::{debug, instrument, warn};
|
||||
|
||||
@@ -750,6 +750,7 @@ impl StateStore for MemoryStore {
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
transaction_id: OwnedTransactionId,
|
||||
created_at: MilliSecondsSinceUnixEpoch,
|
||||
kind: QueuedRequestKind,
|
||||
priority: usize,
|
||||
) -> Result<(), Self::Error> {
|
||||
@@ -759,7 +760,7 @@ impl StateStore for MemoryStore {
|
||||
.send_queue_events
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.push(QueuedRequest { kind, transaction_id, error: None, priority });
|
||||
.push(QueuedRequest { kind, transaction_id, error: None, priority, created_at });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -858,6 +859,7 @@ impl StateStore for MemoryStore {
|
||||
room: &RoomId,
|
||||
parent_transaction_id: &TransactionId,
|
||||
own_transaction_id: ChildTransactionId,
|
||||
created_at: MilliSecondsSinceUnixEpoch,
|
||||
content: DependentQueuedRequestKind,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.inner
|
||||
@@ -871,6 +873,7 @@ impl StateStore for MemoryStore {
|
||||
parent_transaction_id: parent_transaction_id.to_owned(),
|
||||
own_transaction_id,
|
||||
parent_key: None,
|
||||
created_at,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -19,8 +19,7 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use ruma::{
|
||||
events::{
|
||||
direct::OwnedDirectUserIdentifier,
|
||||
@@ -42,10 +41,9 @@ use ruma::{
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
use crate::latest_event::LatestEvent;
|
||||
use crate::{
|
||||
deserialized_responses::SyncOrStrippedState,
|
||||
latest_event::LatestEvent,
|
||||
rooms::{
|
||||
normal::{RoomSummary, SyncInfo},
|
||||
BaseRoomInfo, RoomNotableTags,
|
||||
@@ -78,8 +76,7 @@ pub struct RoomInfoV1 {
|
||||
sync_info: SyncInfo,
|
||||
#[serde(default = "encryption_state_default")] // see fn docs for why we use this default
|
||||
encryption_state_synced: bool,
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
latest_event: Option<SyncTimelineEvent>,
|
||||
latest_event: Option<TimelineEvent>,
|
||||
base_info: BaseRoomInfoV1,
|
||||
}
|
||||
|
||||
@@ -106,7 +103,6 @@ impl RoomInfoV1 {
|
||||
last_prev_batch,
|
||||
sync_info,
|
||||
encryption_state_synced,
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
latest_event,
|
||||
base_info,
|
||||
} = self;
|
||||
@@ -122,14 +118,12 @@ impl RoomInfoV1 {
|
||||
last_prev_batch,
|
||||
sync_info,
|
||||
encryption_state_synced,
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
latest_event: latest_event.map(|ev| Box::new(LatestEvent::new(ev))),
|
||||
read_receipts: Default::default(),
|
||||
base_info: base_info.migrate(create),
|
||||
warned_about_unknown_room_version: Arc::new(false.into()),
|
||||
cached_display_name: None,
|
||||
cached_user_defined_notification_mode: None,
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
recency_stamp: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,7 +276,6 @@ impl Store {
|
||||
}
|
||||
|
||||
/// Check if a room exists.
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
pub(crate) fn room_exists(&self, room_id: &RoomId) -> bool {
|
||||
self.rooms.read().unwrap().get(room_id).is_some()
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ use ruma::{
|
||||
AnyMessageLikeEventContent, EventContent as _, RawExt as _,
|
||||
},
|
||||
serde::Raw,
|
||||
OwnedDeviceId, OwnedEventId, OwnedTransactionId, OwnedUserId, TransactionId, UInt,
|
||||
MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedEventId, OwnedTransactionId, OwnedUserId,
|
||||
TransactionId, UInt,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -131,6 +132,9 @@ pub struct QueuedRequest {
|
||||
/// The bigger the value, the higher the priority at which this request
|
||||
/// should be handled.
|
||||
pub priority: usize,
|
||||
|
||||
/// The time that the request was originally attempted.
|
||||
pub created_at: MilliSecondsSinceUnixEpoch,
|
||||
}
|
||||
|
||||
impl QueuedRequest {
|
||||
@@ -371,6 +375,9 @@ pub struct DependentQueuedRequest {
|
||||
/// If the parent request has been sent, the parent's request identifier
|
||||
/// returned by the server once the local echo has been sent out.
|
||||
pub parent_key: Option<SentRequestKey>,
|
||||
|
||||
/// The time that the request was originally attempted.
|
||||
pub created_at: MilliSecondsSinceUnixEpoch,
|
||||
}
|
||||
|
||||
impl DependentQueuedRequest {
|
||||
|
||||
@@ -35,8 +35,8 @@ use ruma::{
|
||||
},
|
||||
serde::Raw,
|
||||
time::SystemTime,
|
||||
EventId, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId,
|
||||
TransactionId, UserId,
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedRoomId,
|
||||
OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -359,6 +359,7 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
transaction_id: OwnedTransactionId,
|
||||
created_at: MilliSecondsSinceUnixEpoch,
|
||||
request: QueuedRequestKind,
|
||||
priority: usize,
|
||||
) -> Result<(), Self::Error>;
|
||||
@@ -421,6 +422,7 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
room_id: &RoomId,
|
||||
parent_txn_id: &TransactionId,
|
||||
own_txn_id: ChildTransactionId,
|
||||
created_at: MilliSecondsSinceUnixEpoch,
|
||||
content: DependentQueuedRequestKind,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
@@ -657,11 +659,12 @@ impl<T: StateStore> StateStore for EraseStateStoreError<T> {
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
transaction_id: OwnedTransactionId,
|
||||
created_at: MilliSecondsSinceUnixEpoch,
|
||||
content: QueuedRequestKind,
|
||||
priority: usize,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0
|
||||
.save_send_queue_request(room_id, transaction_id, content, priority)
|
||||
.save_send_queue_request(room_id, transaction_id, created_at, content, priority)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
@@ -711,10 +714,11 @@ impl<T: StateStore> StateStore for EraseStateStoreError<T> {
|
||||
room_id: &RoomId,
|
||||
parent_txn_id: &TransactionId,
|
||||
own_txn_id: ChildTransactionId,
|
||||
created_at: MilliSecondsSinceUnixEpoch,
|
||||
content: DependentQueuedRequestKind,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0
|
||||
.save_dependent_queued_request(room_id, parent_txn_id, own_txn_id, content)
|
||||
.save_dependent_queued_request(room_id, parent_txn_id, own_txn_id, created_at, content)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
use std::{collections::BTreeMap, fmt};
|
||||
|
||||
use matrix_sdk_common::{debug::DebugRawEvent, deserialized_responses::SyncTimelineEvent};
|
||||
use matrix_sdk_common::{debug::DebugRawEvent, deserialized_responses::TimelineEvent};
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::{
|
||||
v3::{InvitedRoom as InvitedRoomUpdate, KnockedRoom as KnockedRoomUpdate},
|
||||
@@ -102,7 +102,7 @@ impl RoomUpdates {
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for RoomUpdates {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Rooms")
|
||||
f.debug_struct("RoomUpdates")
|
||||
.field("leave", &self.leave)
|
||||
.field("join", &self.join)
|
||||
.field("invite", &DebugInvitedRoomUpdates(&self.invite))
|
||||
@@ -138,7 +138,7 @@ pub struct JoinedRoomUpdate {
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for JoinedRoomUpdate {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("JoinedRoom")
|
||||
f.debug_struct("JoinedRoomUpdate")
|
||||
.field("unread_notifications", &self.unread_notifications)
|
||||
.field("timeline", &self.timeline)
|
||||
.field("state", &DebugListOfRawEvents(&self.state))
|
||||
@@ -215,7 +215,7 @@ impl LeftRoomUpdate {
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for LeftRoomUpdate {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("JoinedRoom")
|
||||
f.debug_struct("LeftRoomUpdate")
|
||||
.field("timeline", &self.timeline)
|
||||
.field("state", &DebugListOfRawEvents(&self.state))
|
||||
.field("account_data", &DebugListOfRawEventsNoId(&self.account_data))
|
||||
@@ -236,7 +236,7 @@ pub struct Timeline {
|
||||
pub prev_batch: Option<String>,
|
||||
|
||||
/// A list of events.
|
||||
pub events: Vec<SyncTimelineEvent>,
|
||||
pub events: Vec<TimelineEvent>,
|
||||
}
|
||||
|
||||
impl Timeline {
|
||||
|
||||
@@ -6,6 +6,13 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
## [0.10.0] - 2025-02-04
|
||||
|
||||
- [**breaking**]: `SyncTimelineEvent` and `TimelineEvent` have been fused into a single type
|
||||
`TimelineEvent`, and its field `push_actions` has been made `Option`al (it is set to `None` when
|
||||
we couldn't compute the push actions, because we lacked some information).
|
||||
([#4568](https://github.com/matrix-org/matrix-rust-sdk/pull/4568))
|
||||
|
||||
## [0.9.0] - 2024-12-18
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -9,7 +9,7 @@ name = "matrix-sdk-common"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = { workspace = true }
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
default-target = "x86_64-unknown-linux-gnu"
|
||||
@@ -18,6 +18,10 @@ targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
|
||||
[features]
|
||||
js = ["wasm-bindgen-futures"]
|
||||
uniffi = ["dep:uniffi"]
|
||||
# Private feature, see
|
||||
# https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823 for the gory
|
||||
# details.
|
||||
test-send-sync = []
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
@@ -46,6 +50,7 @@ assert_matches = { workspace = true }
|
||||
proptest = { workspace = true }
|
||||
matrix-sdk-test-macros = { path = "../../testing/matrix-sdk-test-macros" }
|
||||
wasm-bindgen-test = { workspace = true }
|
||||
insta = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
# Enable the test macro.
|
||||
@@ -53,7 +58,7 @@ tokio = { workspace = true, features = ["rt", "macros"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
# Enable the JS feature for getrandom.
|
||||
getrandom = { version = "0.2.6", default-features = false, features = ["js"] }
|
||||
getrandom = { workspace = true, default-features = false, features = ["js"] }
|
||||
js-sys = { workspace = true }
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
|
||||
use std::{collections::BTreeMap, fmt};
|
||||
|
||||
#[cfg(doc)]
|
||||
use ruma::events::AnyTimelineEvent;
|
||||
use ruma::{
|
||||
events::{AnyMessageLikeEvent, AnySyncTimelineEvent, AnyTimelineEvent},
|
||||
events::{AnyMessageLikeEvent, AnySyncTimelineEvent},
|
||||
push::Action,
|
||||
serde::{
|
||||
AsRefStr, AsStrAsRefStr, DebugAsRefStr, DeserializeFromCowStr, FromString, JsonObject, Raw,
|
||||
@@ -308,26 +310,61 @@ pub struct EncryptionInfo {
|
||||
/// Previously, this differed from [`TimelineEvent`] by wrapping an
|
||||
/// [`AnySyncTimelineEvent`] instead of an [`AnyTimelineEvent`], but nowadays
|
||||
/// they are essentially identical, and one of them should probably be removed.
|
||||
//
|
||||
// 🚨 Note about this type, please read! 🚨
|
||||
//
|
||||
// `TimelineEvent` is heavily used across the SDK crates. In some cases, we
|
||||
// are reaching a [`recursion_limit`] when the compiler is trying to figure out
|
||||
// if `TimelineEvent` implements `Sync` when it's embedded in other types.
|
||||
//
|
||||
// We want to help the compiler so that one doesn't need to increase the
|
||||
// `recursion_limit`. We stop the recursive check by (un)safely implement `Sync`
|
||||
// and `Send` on `TimelineEvent` directly.
|
||||
//
|
||||
// See
|
||||
// https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823
|
||||
// which has addressed this issue first
|
||||
//
|
||||
// [`recursion_limit`]: https://doc.rust-lang.org/reference/attributes/limits.html#the-recursion_limit-attribute
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct SyncTimelineEvent {
|
||||
pub struct TimelineEvent {
|
||||
/// The event itself, together with any information on decryption.
|
||||
pub kind: TimelineEventKind,
|
||||
|
||||
/// The push actions associated with this event.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub push_actions: Vec<Action>,
|
||||
///
|
||||
/// If it's set to `None`, then it means we couldn't compute those actions.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub push_actions: Option<Vec<Action>>,
|
||||
}
|
||||
|
||||
impl SyncTimelineEvent {
|
||||
/// Create a new `SyncTimelineEvent` from the given raw event.
|
||||
// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
|
||||
#[cfg(not(feature = "test-send-sync"))]
|
||||
unsafe impl Send for TimelineEvent {}
|
||||
|
||||
// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
|
||||
#[cfg(not(feature = "test-send-sync"))]
|
||||
unsafe impl Sync for TimelineEvent {}
|
||||
|
||||
#[cfg(feature = "test-send-sync")]
|
||||
#[test]
|
||||
// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
|
||||
fn test_send_sync_for_sync_timeline_event() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
|
||||
assert_send_sync::<TimelineEvent>();
|
||||
}
|
||||
|
||||
impl TimelineEvent {
|
||||
/// Create a new [`TimelineEvent`] from the given raw event.
|
||||
///
|
||||
/// This is a convenience constructor for a plaintext event when you don't
|
||||
/// need to set `push_action`, for example inside a test.
|
||||
pub fn new(event: Raw<AnySyncTimelineEvent>) -> Self {
|
||||
Self { kind: TimelineEventKind::PlainText { event }, push_actions: vec![] }
|
||||
Self { kind: TimelineEventKind::PlainText { event }, push_actions: None }
|
||||
}
|
||||
|
||||
/// Create a new `SyncTimelineEvent` from the given raw event and push
|
||||
/// Create a new [`TimelineEvent`] from the given raw event and push
|
||||
/// actions.
|
||||
///
|
||||
/// This is a convenience constructor for a plaintext event, for example
|
||||
@@ -336,140 +373,38 @@ impl SyncTimelineEvent {
|
||||
event: Raw<AnySyncTimelineEvent>,
|
||||
push_actions: Vec<Action>,
|
||||
) -> Self {
|
||||
Self { kind: TimelineEventKind::PlainText { event }, push_actions }
|
||||
Self { kind: TimelineEventKind::PlainText { event }, push_actions: Some(push_actions) }
|
||||
}
|
||||
|
||||
/// Create a new `SyncTimelineEvent` to represent the given decryption
|
||||
/// Create a new [`TimelineEvent`] to represent the given decryption
|
||||
/// failure.
|
||||
pub fn new_utd_event(event: Raw<AnySyncTimelineEvent>, utd_info: UnableToDecryptInfo) -> Self {
|
||||
Self { kind: TimelineEventKind::UnableToDecrypt { event, utd_info }, push_actions: vec![] }
|
||||
Self { kind: TimelineEventKind::UnableToDecrypt { event, utd_info }, push_actions: None }
|
||||
}
|
||||
|
||||
/// Get the event id of this `SyncTimelineEvent` if the event has any valid
|
||||
/// Get the event id of this [`TimelineEvent`] if the event has any valid
|
||||
/// id.
|
||||
pub fn event_id(&self) -> Option<OwnedEventId> {
|
||||
self.kind.event_id()
|
||||
}
|
||||
|
||||
/// Returns a reference to the (potentially decrypted) Matrix event inside
|
||||
/// this `TimelineEvent`.
|
||||
/// this [`TimelineEvent`].
|
||||
pub fn raw(&self) -> &Raw<AnySyncTimelineEvent> {
|
||||
self.kind.raw()
|
||||
}
|
||||
|
||||
/// If the event was a decrypted event that was successfully decrypted, get
|
||||
/// its encryption info. Otherwise, `None`.
|
||||
pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
|
||||
self.kind.encryption_info()
|
||||
}
|
||||
|
||||
/// Takes ownership of this `TimelineEvent`, returning the (potentially
|
||||
/// decrypted) Matrix event within.
|
||||
pub fn into_raw(self) -> Raw<AnySyncTimelineEvent> {
|
||||
self.kind.into_raw()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TimelineEvent> for SyncTimelineEvent {
|
||||
fn from(o: TimelineEvent) -> Self {
|
||||
Self { kind: o.kind, push_actions: o.push_actions.unwrap_or_default() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DecryptedRoomEvent> for SyncTimelineEvent {
|
||||
fn from(decrypted: DecryptedRoomEvent) -> Self {
|
||||
let timeline_event: TimelineEvent = decrypted.into();
|
||||
timeline_event.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SyncTimelineEvent {
|
||||
/// Custom deserializer for [`SyncTimelineEvent`], to support older formats.
|
||||
///
|
||||
/// Ideally we might use an untagged enum and then convert from that;
|
||||
/// however, that doesn't work due to a [serde bug](https://github.com/serde-rs/json/issues/497).
|
||||
///
|
||||
/// Instead, we first deserialize into an unstructured JSON map, and then
|
||||
/// inspect the json to figure out which format we have.
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
// First, deserialize to an unstructured JSON map
|
||||
let value = Map::<String, Value>::deserialize(deserializer)?;
|
||||
|
||||
// If we have a top-level `event`, it's V0
|
||||
if value.contains_key("event") {
|
||||
let v0: SyncTimelineEventDeserializationHelperV0 =
|
||||
serde_json::from_value(Value::Object(value)).map_err(|e| {
|
||||
serde::de::Error::custom(format!(
|
||||
"Unable to deserialize V0-format SyncTimelineEvent: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
Ok(v0.into())
|
||||
/// Replace the raw event included in this item by another one.
|
||||
pub fn replace_raw(&mut self, replacement: Raw<AnyMessageLikeEvent>) {
|
||||
match &mut self.kind {
|
||||
TimelineEventKind::Decrypted(decrypted) => decrypted.event = replacement,
|
||||
TimelineEventKind::UnableToDecrypt { event, .. }
|
||||
| TimelineEventKind::PlainText { event } => {
|
||||
// It's safe to cast `AnyMessageLikeEvent` into `AnySyncMessageLikeEvent`,
|
||||
// because the former contains a superset of the fields included in the latter.
|
||||
*event = replacement.cast();
|
||||
}
|
||||
}
|
||||
// Otherwise, it's V1
|
||||
else {
|
||||
let v1: SyncTimelineEventDeserializationHelperV1 =
|
||||
serde_json::from_value(Value::Object(value)).map_err(|e| {
|
||||
serde::de::Error::custom(format!(
|
||||
"Unable to deserialize V1-format SyncTimelineEvent: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
Ok(v1.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a matrix room event that has been returned from a Matrix
|
||||
/// client-server API endpoint such as `/messages`, after initial processing.
|
||||
///
|
||||
/// The "initial processing" includes an attempt to decrypt encrypted events, so
|
||||
/// the main thing this adds over [`AnyTimelineEvent`] is information on
|
||||
/// encryption.
|
||||
///
|
||||
/// Previously, this differed from [`SyncTimelineEvent`] by wrapping an
|
||||
/// [`AnyTimelineEvent`] instead of an [`AnySyncTimelineEvent`], but nowadays
|
||||
/// they are essentially identical, and one of them should probably be removed.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TimelineEvent {
|
||||
/// The event itself, together with any information on decryption.
|
||||
pub kind: TimelineEventKind,
|
||||
|
||||
/// The push actions associated with this event, if we had sufficient
|
||||
/// context to compute them.
|
||||
pub push_actions: Option<Vec<Action>>,
|
||||
}
|
||||
|
||||
impl TimelineEvent {
|
||||
/// Create a new `TimelineEvent` from the given raw event.
|
||||
///
|
||||
/// This is a convenience constructor for a plaintext event when you don't
|
||||
/// need to set `push_action`, for example inside a test.
|
||||
pub fn new(event: Raw<AnyTimelineEvent>) -> Self {
|
||||
Self {
|
||||
// This conversion is unproblematic since a `SyncTimelineEvent` is just a
|
||||
// `TimelineEvent` without the `room_id`. By converting the raw value in
|
||||
// this way, we simply cause the `room_id` field in the json to be
|
||||
// ignored by a subsequent deserialization.
|
||||
kind: TimelineEventKind::PlainText { event: event.cast() },
|
||||
push_actions: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `TimelineEvent` to represent the given decryption failure.
|
||||
pub fn new_utd_event(event: Raw<AnySyncTimelineEvent>, utd_info: UnableToDecryptInfo) -> Self {
|
||||
Self { kind: TimelineEventKind::UnableToDecrypt { event, utd_info }, push_actions: None }
|
||||
}
|
||||
|
||||
/// Returns a reference to the (potentially decrypted) Matrix event inside
|
||||
/// this `TimelineEvent`.
|
||||
pub fn raw(&self) -> &Raw<AnySyncTimelineEvent> {
|
||||
self.kind.raw()
|
||||
}
|
||||
|
||||
/// If the event was a decrypted event that was successfully decrypted, get
|
||||
@@ -491,8 +426,49 @@ impl From<DecryptedRoomEvent> for TimelineEvent {
|
||||
}
|
||||
}
|
||||
|
||||
/// The event within a [`TimelineEvent`] or [`SyncTimelineEvent`], together with
|
||||
/// encryption data.
|
||||
impl<'de> Deserialize<'de> for TimelineEvent {
|
||||
/// Custom deserializer for [`TimelineEvent`], to support older formats.
|
||||
///
|
||||
/// Ideally we might use an untagged enum and then convert from that;
|
||||
/// however, that doesn't work due to a [serde bug](https://github.com/serde-rs/json/issues/497).
|
||||
///
|
||||
/// Instead, we first deserialize into an unstructured JSON map, and then
|
||||
/// inspect the json to figure out which format we have.
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
// First, deserialize to an unstructured JSON map
|
||||
let value = Map::<String, Value>::deserialize(deserializer)?;
|
||||
|
||||
// If we have a top-level `event`, it's V0
|
||||
if value.contains_key("event") {
|
||||
let v0: SyncTimelineEventDeserializationHelperV0 =
|
||||
serde_json::from_value(Value::Object(value)).map_err(|e| {
|
||||
serde::de::Error::custom(format!(
|
||||
"Unable to deserialize V0-format TimelineEvent: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
Ok(v0.into())
|
||||
}
|
||||
// Otherwise, it's V1
|
||||
else {
|
||||
let v1: SyncTimelineEventDeserializationHelperV1 =
|
||||
serde_json::from_value(Value::Object(value)).map_err(|e| {
|
||||
serde::de::Error::custom(format!(
|
||||
"Unable to deserialize V1-format TimelineEvent: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
Ok(v1.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The event within a [`TimelineEvent`], together with encryption data.
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub enum TimelineEventKind {
|
||||
/// A successfully-decrypted encrypted event.
|
||||
@@ -831,9 +807,9 @@ impl fmt::Debug for PrivOwnedStr {
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserialization helper for [`SyncTimelineEvent`], for the modern format.
|
||||
/// Deserialization helper for [`TimelineEvent`], for the modern format.
|
||||
///
|
||||
/// This has the exact same fields as [`SyncTimelineEvent`] itself, but has a
|
||||
/// This has the exact same fields as [`TimelineEvent`] itself, but has a
|
||||
/// regular `Deserialize` implementation.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SyncTimelineEventDeserializationHelperV1 {
|
||||
@@ -845,14 +821,14 @@ struct SyncTimelineEventDeserializationHelperV1 {
|
||||
push_actions: Vec<Action>,
|
||||
}
|
||||
|
||||
impl From<SyncTimelineEventDeserializationHelperV1> for SyncTimelineEvent {
|
||||
impl From<SyncTimelineEventDeserializationHelperV1> for TimelineEvent {
|
||||
fn from(value: SyncTimelineEventDeserializationHelperV1) -> Self {
|
||||
let SyncTimelineEventDeserializationHelperV1 { kind, push_actions } = value;
|
||||
SyncTimelineEvent { kind, push_actions }
|
||||
TimelineEvent { kind, push_actions: Some(push_actions) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserialization helper for [`SyncTimelineEvent`], for an older format.
|
||||
/// Deserialization helper for [`TimelineEvent`], for an older format.
|
||||
#[derive(Deserialize)]
|
||||
struct SyncTimelineEventDeserializationHelperV0 {
|
||||
/// The actual event.
|
||||
@@ -873,7 +849,7 @@ struct SyncTimelineEventDeserializationHelperV0 {
|
||||
unsigned_encryption_info: Option<BTreeMap<UnsignedEventLocation, UnsignedDecryptionResult>>,
|
||||
}
|
||||
|
||||
impl From<SyncTimelineEventDeserializationHelperV0> for SyncTimelineEvent {
|
||||
impl From<SyncTimelineEventDeserializationHelperV0> for TimelineEvent {
|
||||
fn from(value: SyncTimelineEventDeserializationHelperV0) -> Self {
|
||||
let SyncTimelineEventDeserializationHelperV0 {
|
||||
event,
|
||||
@@ -900,7 +876,7 @@ impl From<SyncTimelineEventDeserializationHelperV0> for SyncTimelineEvent {
|
||||
None => TimelineEventKind::PlainText { event },
|
||||
};
|
||||
|
||||
SyncTimelineEvent { kind, push_actions }
|
||||
TimelineEvent { kind, push_actions: Some(push_actions) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -909,21 +885,20 @@ mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use insta::{assert_json_snapshot, with_settings};
|
||||
use ruma::{
|
||||
event_id,
|
||||
events::{room::message::RoomMessageEventContent, AnySyncTimelineEvent},
|
||||
serde::Raw,
|
||||
user_id,
|
||||
device_id, event_id, events::room::message::RoomMessageEventContent, serde::Raw, user_id,
|
||||
DeviceKeyAlgorithm,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
use super::{
|
||||
AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, TimelineEvent,
|
||||
TimelineEventKind, UnableToDecryptInfo, UnableToDecryptReason, UnsignedDecryptionResult,
|
||||
UnsignedEventLocation, VerificationState, WithheldCode,
|
||||
AlgorithmInfo, DecryptedRoomEvent, DeviceLinkProblem, EncryptionInfo, ShieldState,
|
||||
ShieldStateCode, TimelineEvent, TimelineEventKind, UnableToDecryptInfo,
|
||||
UnableToDecryptReason, UnsignedDecryptionResult, UnsignedEventLocation, VerificationLevel,
|
||||
VerificationState, WithheldCode,
|
||||
};
|
||||
use crate::deserialized_responses::{DeviceLinkProblem, ShieldStateCode, VerificationLevel};
|
||||
|
||||
fn example_event() -> serde_json::Value {
|
||||
json!({
|
||||
@@ -938,7 +913,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn sync_timeline_debug_content() {
|
||||
let room_event = SyncTimelineEvent::new(Raw::new(&example_event()).unwrap().cast());
|
||||
let room_event = TimelineEvent::new(Raw::new(&example_event()).unwrap().cast());
|
||||
let debug_s = format!("{room_event:?}");
|
||||
assert!(
|
||||
!debug_s.contains("secret"),
|
||||
@@ -946,18 +921,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn room_event_to_sync_room_event() {
|
||||
let room_event = TimelineEvent::new(Raw::new(&example_event()).unwrap().cast());
|
||||
let converted_room_event: SyncTimelineEvent = room_event.into();
|
||||
|
||||
let converted_event: AnySyncTimelineEvent =
|
||||
converted_room_event.raw().deserialize().unwrap();
|
||||
|
||||
assert_eq!(converted_event.event_id(), "$xxxxx:example.org");
|
||||
assert_eq!(converted_event.sender(), "@carl:example.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_verification_state_to_new_migration() {
|
||||
#[derive(Deserialize)]
|
||||
@@ -1068,7 +1031,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn sync_timeline_event_serialisation() {
|
||||
let room_event = SyncTimelineEvent {
|
||||
let room_event = TimelineEvent {
|
||||
kind: TimelineEventKind::Decrypted(DecryptedRoomEvent {
|
||||
event: Raw::new(&example_event()).unwrap().cast(),
|
||||
encryption_info: EncryptionInfo {
|
||||
@@ -1130,7 +1093,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// And it can be properly deserialized from the new format.
|
||||
let event: SyncTimelineEvent = serde_json::from_value(serialized).unwrap();
|
||||
let event: TimelineEvent = serde_json::from_value(serialized).unwrap();
|
||||
assert_eq!(event.event_id(), Some(event_id!("$xxxxx:example.org").to_owned()));
|
||||
assert_matches!(
|
||||
event.encryption_info().unwrap().algorithm_info,
|
||||
@@ -1159,7 +1122,7 @@ mod tests {
|
||||
"verification_state": "Verified",
|
||||
},
|
||||
});
|
||||
let event: SyncTimelineEvent = serde_json::from_value(serialized).unwrap();
|
||||
let event: TimelineEvent = serde_json::from_value(serialized).unwrap();
|
||||
assert_eq!(event.event_id(), Some(event_id!("$xxxxx:example.org").to_owned()));
|
||||
assert_matches!(
|
||||
event.encryption_info().unwrap().algorithm_info,
|
||||
@@ -1192,7 +1155,7 @@ mod tests {
|
||||
"RelationsReplace": {"UnableToDecrypt": {"session_id": "xyz"}}
|
||||
}
|
||||
});
|
||||
let event: SyncTimelineEvent = serde_json::from_value(serialized).unwrap();
|
||||
let event: TimelineEvent = serde_json::from_value(serialized).unwrap();
|
||||
assert_eq!(event.event_id(), Some(event_id!("$xxxxx:example.org").to_owned()));
|
||||
assert_matches!(
|
||||
event.encryption_info().unwrap().algorithm_info,
|
||||
@@ -1258,7 +1221,7 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
|
||||
// should have migrated to the new format
|
||||
let event: SyncTimelineEvent = result.unwrap();
|
||||
let event: TimelineEvent = result.unwrap();
|
||||
assert_matches!(
|
||||
event.kind,
|
||||
TimelineEventKind::UnableToDecrypt { utd_info, .. }=> {
|
||||
@@ -1317,4 +1280,129 @@ mod tests {
|
||||
let reason = UnableToDecryptReason::UnknownMegolmMessageIndex;
|
||||
assert!(reason.is_missing_room_key());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_test_verification_level() {
|
||||
assert_json_snapshot!(VerificationLevel::VerificationViolation);
|
||||
assert_json_snapshot!(VerificationLevel::UnsignedDevice);
|
||||
assert_json_snapshot!(VerificationLevel::None(DeviceLinkProblem::InsecureSource));
|
||||
assert_json_snapshot!(VerificationLevel::None(DeviceLinkProblem::MissingDevice));
|
||||
assert_json_snapshot!(VerificationLevel::UnverifiedIdentity);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_test_verification_states() {
|
||||
assert_json_snapshot!(VerificationState::Unverified(VerificationLevel::UnsignedDevice));
|
||||
assert_json_snapshot!(VerificationState::Unverified(
|
||||
VerificationLevel::VerificationViolation
|
||||
));
|
||||
assert_json_snapshot!(VerificationState::Unverified(VerificationLevel::None(
|
||||
DeviceLinkProblem::InsecureSource,
|
||||
)));
|
||||
assert_json_snapshot!(VerificationState::Unverified(VerificationLevel::None(
|
||||
DeviceLinkProblem::MissingDevice,
|
||||
)));
|
||||
assert_json_snapshot!(VerificationState::Verified);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_test_shield_states() {
|
||||
assert_json_snapshot!(ShieldState::None);
|
||||
assert_json_snapshot!(ShieldState::Red {
|
||||
code: ShieldStateCode::UnverifiedIdentity,
|
||||
message: "a message"
|
||||
});
|
||||
assert_json_snapshot!(ShieldState::Grey {
|
||||
code: ShieldStateCode::AuthenticityNotGuaranteed,
|
||||
message: "authenticity of this message cannot be guaranteed",
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_test_shield_codes() {
|
||||
assert_json_snapshot!(ShieldStateCode::AuthenticityNotGuaranteed);
|
||||
assert_json_snapshot!(ShieldStateCode::UnknownDevice);
|
||||
assert_json_snapshot!(ShieldStateCode::UnsignedDevice);
|
||||
assert_json_snapshot!(ShieldStateCode::UnverifiedIdentity);
|
||||
assert_json_snapshot!(ShieldStateCode::SentInClear);
|
||||
assert_json_snapshot!(ShieldStateCode::VerificationViolation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_test_algorithm_info() {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert(DeviceKeyAlgorithm::Curve25519, "claimedclaimedcurve25519".to_owned());
|
||||
map.insert(DeviceKeyAlgorithm::Ed25519, "claimedclaimeded25519".to_owned());
|
||||
let info = AlgorithmInfo::MegolmV1AesSha2 {
|
||||
curve25519_key: "curvecurvecurve".into(),
|
||||
sender_claimed_keys: BTreeMap::from([
|
||||
(DeviceKeyAlgorithm::Curve25519, "claimedclaimedcurve25519".to_owned()),
|
||||
(DeviceKeyAlgorithm::Ed25519, "claimedclaimeded25519".to_owned()),
|
||||
]),
|
||||
};
|
||||
|
||||
assert_json_snapshot!(info)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_test_encryption_info() {
|
||||
let info = EncryptionInfo {
|
||||
sender: user_id!("@alice:localhost").to_owned(),
|
||||
sender_device: Some(device_id!("ABCDEFGH").to_owned()),
|
||||
algorithm_info: AlgorithmInfo::MegolmV1AesSha2 {
|
||||
curve25519_key: "curvecurvecurve".into(),
|
||||
sender_claimed_keys: Default::default(),
|
||||
},
|
||||
verification_state: VerificationState::Verified,
|
||||
};
|
||||
|
||||
with_settings!({sort_maps =>true}, {
|
||||
assert_json_snapshot!(info)
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_test_sync_timeline_event() {
|
||||
let room_event = TimelineEvent {
|
||||
kind: TimelineEventKind::Decrypted(DecryptedRoomEvent {
|
||||
event: Raw::new(&example_event()).unwrap().cast(),
|
||||
encryption_info: EncryptionInfo {
|
||||
sender: user_id!("@sender:example.com").to_owned(),
|
||||
sender_device: Some(device_id!("ABCDEFGHIJ").to_owned()),
|
||||
algorithm_info: AlgorithmInfo::MegolmV1AesSha2 {
|
||||
curve25519_key: "xxx".to_owned(),
|
||||
sender_claimed_keys: BTreeMap::from([
|
||||
(
|
||||
DeviceKeyAlgorithm::Ed25519,
|
||||
"I3YsPwqMZQXHkSQbjFNEs7b529uac2xBpI83eN3LUXo".to_owned(),
|
||||
),
|
||||
(
|
||||
DeviceKeyAlgorithm::Curve25519,
|
||||
"qzdW3F5IMPFl0HQgz5w/L5Oi/npKUFn8Um84acIHfPY".to_owned(),
|
||||
),
|
||||
]),
|
||||
},
|
||||
verification_state: VerificationState::Verified,
|
||||
},
|
||||
unsigned_encryption_info: Some(BTreeMap::from([(
|
||||
UnsignedEventLocation::RelationsThreadLatestEvent,
|
||||
UnsignedDecryptionResult::UnableToDecrypt(UnableToDecryptInfo {
|
||||
session_id: Some("xyz".to_owned()),
|
||||
reason: UnableToDecryptReason::MissingMegolmSession {
|
||||
withheld_code: Some(WithheldCode::Unverified),
|
||||
},
|
||||
}),
|
||||
)])),
|
||||
}),
|
||||
push_actions: Default::default(),
|
||||
};
|
||||
|
||||
with_settings!({sort_maps =>true}, {
|
||||
// We use directly the serde_json formatter here, because of a bug in insta
|
||||
// not serializing custom BTreeMap key enum https://github.com/mitsuhiko/insta/issues/689
|
||||
assert_json_snapshot! {
|
||||
serde_json::to_value(&room_event).unwrap(),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,16 +15,12 @@
|
||||
//! 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 std::{borrow::Borrow, collections::HashMap, hash::Hash, sync::Arc, time::Duration};
|
||||
|
||||
use ruma::time::Instant;
|
||||
|
||||
use super::locks::RwLock;
|
||||
|
||||
const MAX_DELAY: u64 = 15 * 60;
|
||||
const MULTIPLIER: u64 = 15;
|
||||
|
||||
@@ -105,7 +101,7 @@ where
|
||||
T: Borrow<Q>,
|
||||
Q: Hash + Eq + ?Sized,
|
||||
{
|
||||
let lock = self.inner.items.read().unwrap();
|
||||
let lock = self.inner.items.read();
|
||||
|
||||
let contains = if let Some(item) = lock.get(key) { !item.expired() } else { false };
|
||||
|
||||
@@ -127,7 +123,7 @@ where
|
||||
T: Borrow<Q>,
|
||||
Q: Hash + Eq + ?Sized,
|
||||
{
|
||||
let lock = self.inner.items.read().unwrap();
|
||||
let lock = self.inner.items.read();
|
||||
lock.get(key).map(|i| i.failure_count)
|
||||
}
|
||||
|
||||
@@ -155,7 +151,7 @@ where
|
||||
/// 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 mut lock = self.inner.items.write();
|
||||
|
||||
let now = Instant::now();
|
||||
|
||||
@@ -181,7 +177,7 @@ where
|
||||
T: Borrow<Q>,
|
||||
Q: Hash + Eq + 'a + ?Sized,
|
||||
{
|
||||
let mut lock = self.inner.items.write().unwrap();
|
||||
let mut lock = self.inner.items.write();
|
||||
|
||||
for item in iterator {
|
||||
lock.remove(item);
|
||||
@@ -194,7 +190,7 @@ where
|
||||
/// for immediate retry.
|
||||
#[doc(hidden)]
|
||||
pub fn expire(&self, item: &T) {
|
||||
let mut lock = self.inner.items.write().unwrap();
|
||||
let mut lock = self.inner.items.write();
|
||||
lock.get_mut(item).map(FailuresItem::expire);
|
||||
}
|
||||
}
|
||||
@@ -221,11 +217,11 @@ mod tests {
|
||||
cache.extend([1u8].iter());
|
||||
assert!(cache.contains(&1));
|
||||
|
||||
cache.inner.items.write().unwrap().get_mut(&1).unwrap().duration = Duration::from_secs(0);
|
||||
cache.inner.items.write().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())
|
||||
assert!(cache.inner.items.read().get(&1).is_none())
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -26,7 +26,9 @@ pub mod deserialized_responses;
|
||||
pub mod executor;
|
||||
pub mod failures_cache;
|
||||
pub mod linked_chunk;
|
||||
pub mod locks;
|
||||
pub mod ring_buffer;
|
||||
pub mod sleep;
|
||||
pub mod store_locks;
|
||||
pub mod timeout;
|
||||
pub mod tracing_timer;
|
||||
|
||||
@@ -99,7 +99,7 @@ impl UpdateToVectorDiff {
|
||||
let mut initial_chunk_lengths = VecDeque::new();
|
||||
|
||||
for chunk in chunk_iterator {
|
||||
initial_chunk_lengths.push_front((
|
||||
initial_chunk_lengths.push_back((
|
||||
chunk.identifier(),
|
||||
match chunk.content() {
|
||||
ChunkContent::Gap(_) => 0,
|
||||
@@ -142,17 +142,19 @@ impl UpdateToVectorDiff {
|
||||
/// [`VectorDiff`] is emitted,
|
||||
/// * [`Update::StartReattachItems`] and [`Update::EndReattachItems`] are
|
||||
/// respectively muting or unmuting the emission of [`VectorDiff`] by
|
||||
/// [`Update::PushItems`].
|
||||
/// [`Update::PushItems`],
|
||||
/// * [`Update::Clear`] reinitialises the state.
|
||||
///
|
||||
/// The only `VectorDiff` that are emitted are [`VectorDiff::Insert`] or
|
||||
/// [`VectorDiff::Append`] because a [`LinkedChunk`] is append-only.
|
||||
/// The only `VectorDiff` that are emitted are [`VectorDiff::Insert`],
|
||||
/// [`VectorDiff::Append`], [`VectorDiff::Remove`] and
|
||||
/// [`VectorDiff::Clear`].
|
||||
///
|
||||
/// `VectorDiff::Append` is an optimisation when numerous
|
||||
/// `VectorDiff::Insert`s have to be emitted at the last position.
|
||||
///
|
||||
/// `VectorDiff::Insert` need an index. To compute this index, the algorithm
|
||||
/// will iterate over all pairs to accumulate each chunk length until it
|
||||
/// finds the appropriate pair (given by
|
||||
/// `VectorDiff::Insert` needs an index. To compute this index, the
|
||||
/// algorithm will iterate over all pairs to accumulate each chunk length
|
||||
/// until it finds the appropriate pair (given by
|
||||
/// [`Update::PushItems::at`]). This is _the offset_. To this offset, the
|
||||
/// algorithm adds the position's index of the new items (still given by
|
||||
/// [`Update::PushItems::at`]). This is _the index_. This logic works
|
||||
@@ -362,6 +364,14 @@ impl UpdateToVectorDiff {
|
||||
}
|
||||
}
|
||||
|
||||
Update::ReplaceItem { at: position, item } => {
|
||||
let (offset, (_chunk_index, _chunk_length)) = self.map_to_offset(position);
|
||||
|
||||
// The chunk length doesn't change.
|
||||
|
||||
diffs.push(VectorDiff::Set { index: offset, value: item.clone() });
|
||||
}
|
||||
|
||||
Update::RemoveItem { at: position } => {
|
||||
let (offset, (_chunk_index, chunk_length)) = self.map_to_offset(position);
|
||||
|
||||
@@ -482,15 +492,7 @@ mod tests {
|
||||
assert_eq!(diffs, expected_diffs);
|
||||
|
||||
for diff in diffs {
|
||||
match diff {
|
||||
VectorDiff::Insert { index, value } => accumulator.insert(index, value),
|
||||
VectorDiff::Append { values } => accumulator.append(values),
|
||||
VectorDiff::Remove { index } => {
|
||||
accumulator.remove(index);
|
||||
}
|
||||
VectorDiff::Clear => accumulator.clear(),
|
||||
diff => unimplemented!("{diff:?}"),
|
||||
}
|
||||
diff.apply(accumulator);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,6 +710,31 @@ mod tests {
|
||||
vector!['m', 'a', 'w', 'x', 'y', 'b', 'd', 'i', 'j', 'k', 'l', 'e', 'f', 'g', 'z', 'h']
|
||||
);
|
||||
|
||||
// Replace element 8 by an uppercase J.
|
||||
linked_chunk
|
||||
.replace_item_at(linked_chunk.item_position(|item| *item == 'j').unwrap(), 'J')
|
||||
.unwrap();
|
||||
|
||||
assert_items_eq!(
|
||||
linked_chunk,
|
||||
['m', 'a', 'w'] ['x'] ['y', 'b'] ['d'] ['i', 'J', 'k'] ['l'] ['e', 'f', 'g'] ['z', 'h']
|
||||
);
|
||||
|
||||
// From an `ObservableVector` point of view, it would look like:
|
||||
//
|
||||
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
||||
// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|
||||
// | m | a | w | x | y | b | d | i | J | k | l | e | f | g | z | h |
|
||||
// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|
||||
// ^^^^
|
||||
// |
|
||||
// new!
|
||||
apply_and_assert_eq(
|
||||
&mut accumulator,
|
||||
as_vector.take(),
|
||||
&[VectorDiff::Set { index: 8, value: 'J' }],
|
||||
);
|
||||
|
||||
// Let's try to clear the linked chunk now.
|
||||
linked_chunk.clear();
|
||||
|
||||
@@ -771,7 +798,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn updates_are_drained_when_constructing_as_vector() {
|
||||
fn test_updates_are_drained_when_constructing_as_vector() {
|
||||
let mut linked_chunk = LinkedChunk::<10, char, ()>::new_with_update_history();
|
||||
|
||||
linked_chunk.push_items_back(['a']);
|
||||
@@ -791,6 +818,40 @@ mod tests {
|
||||
assert_eq!(diffs.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_as_vector_with_initial_content() {
|
||||
// Fill the linked chunk with some initial items.
|
||||
let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
|
||||
linked_chunk.push_items_back(['a', 'b', 'c', 'd']);
|
||||
|
||||
#[rustfmt::skip]
|
||||
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d']);
|
||||
|
||||
// Empty updates first.
|
||||
let _ = linked_chunk.updates().unwrap().take();
|
||||
|
||||
// Start observing future updates.
|
||||
let mut as_vector = linked_chunk.as_vector().unwrap();
|
||||
|
||||
assert!(as_vector.take().is_empty());
|
||||
|
||||
// It's important to cause a change that will create new chunks, like pushing
|
||||
// enough items.
|
||||
linked_chunk.push_items_back(['e', 'f', 'g']);
|
||||
#[rustfmt::skip]
|
||||
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e', 'f'] ['g']);
|
||||
|
||||
// And the vector diffs can be computed without crashing.
|
||||
let diffs = as_vector.take();
|
||||
assert_eq!(diffs.len(), 2);
|
||||
assert_matches!(&diffs[0], VectorDiff::Append { values } => {
|
||||
assert_eq!(*values, ['e', 'f'].into());
|
||||
});
|
||||
assert_matches!(&diffs[1], VectorDiff::Append { values } => {
|
||||
assert_eq!(*values, ['g'].into());
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod proptests {
|
||||
use proptest::prelude::*;
|
||||
@@ -822,7 +883,7 @@ mod tests {
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn as_vector_is_correct(
|
||||
fn test_as_vector_is_correct(
|
||||
operations in prop::collection::vec(as_vector_operation_strategy(), 50..=200)
|
||||
) {
|
||||
let mut linked_chunk = LinkedChunk::<10, char, ()>::new_with_update_history();
|
||||
|
||||
@@ -31,7 +31,6 @@ use super::{
|
||||
/// which will get resolved later when re-building the full data structure. This
|
||||
/// allows using chunks that references other chunks that aren't known yet.
|
||||
struct TemporaryChunk<Item, Gap> {
|
||||
id: ChunkIdentifier,
|
||||
previous: Option<ChunkIdentifier>,
|
||||
next: Option<ChunkIdentifier>,
|
||||
content: ChunkContent<Item, Gap>,
|
||||
@@ -79,7 +78,7 @@ impl<const CAP: usize, Item, Gap> LinkedChunkBuilder<CAP, Item, Gap> {
|
||||
next: Option<ChunkIdentifier>,
|
||||
content: Gap,
|
||||
) {
|
||||
let chunk = TemporaryChunk { id, previous, next, content: ChunkContent::Gap(content) };
|
||||
let chunk = TemporaryChunk { previous, next, content: ChunkContent::Gap(content) };
|
||||
self.chunks.insert(id, chunk);
|
||||
}
|
||||
|
||||
@@ -96,7 +95,6 @@ impl<const CAP: usize, Item, Gap> LinkedChunkBuilder<CAP, Item, Gap> {
|
||||
items: impl IntoIterator<Item = Item>,
|
||||
) {
|
||||
let chunk = TemporaryChunk {
|
||||
id,
|
||||
previous,
|
||||
next,
|
||||
content: ChunkContent::Items(items.into_iter().collect()),
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#![allow(dead_code)]
|
||||
#![allow(rustdoc::private_intra_doc_links)]
|
||||
|
||||
//! A linked chunk is the underlying data structure that holds all events.
|
||||
@@ -529,6 +528,47 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
|
||||
Ok(removed_item)
|
||||
}
|
||||
|
||||
/// Replace item at a specified position in the [`LinkedChunk`].
|
||||
///
|
||||
/// `position` must point to a valid item, otherwise the method returns
|
||||
/// `Err`.
|
||||
pub fn replace_item_at(&mut self, position: Position, item: Item) -> Result<(), Error>
|
||||
where
|
||||
Item: Clone,
|
||||
{
|
||||
let chunk_identifier = position.chunk_identifier();
|
||||
let item_index = position.index();
|
||||
|
||||
let chunk = self
|
||||
.links
|
||||
.chunk_mut(chunk_identifier)
|
||||
.ok_or(Error::InvalidChunkIdentifier { identifier: chunk_identifier })?;
|
||||
|
||||
match &mut chunk.content {
|
||||
ChunkContent::Gap(..) => {
|
||||
return Err(Error::ChunkIsAGap { identifier: chunk_identifier })
|
||||
}
|
||||
|
||||
ChunkContent::Items(current_items) => {
|
||||
if item_index >= current_items.len() {
|
||||
return Err(Error::InvalidItemIndex { index: item_index });
|
||||
}
|
||||
|
||||
// Avoid one spurious clone by notifying about the update *before* applying it.
|
||||
if let Some(updates) = self.updates.as_mut() {
|
||||
updates.push(Update::ReplaceItem {
|
||||
at: Position(chunk_identifier, item_index),
|
||||
item: item.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
current_items[item_index] = item;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Insert a gap at a specified position in the [`LinkedChunk`].
|
||||
///
|
||||
/// Because the `position` can be invalid, this method returns a
|
||||
@@ -898,6 +938,7 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
|
||||
/// It returns `None` if updates are disabled, i.e. if this linked chunk has
|
||||
/// been constructed with [`Self::new`], otherwise, if it's been constructed
|
||||
/// with [`Self::new_with_update_history`], it returns `Some(…)`.
|
||||
#[must_use]
|
||||
pub fn updates(&mut self) -> Option<&mut ObservableUpdates<Item, Gap>> {
|
||||
self.updates.as_mut()
|
||||
}
|
||||
@@ -2919,4 +2960,30 @@ mod tests {
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replace_item() {
|
||||
let mut linked_chunk = LinkedChunk::<3, char, ()>::new();
|
||||
|
||||
linked_chunk.push_items_back(['a', 'b', 'c']);
|
||||
linked_chunk.push_gap_back(());
|
||||
// Sanity check.
|
||||
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] [-]);
|
||||
|
||||
// Replace item in bounds.
|
||||
linked_chunk.replace_item_at(Position(ChunkIdentifier::new(0), 1), 'B').unwrap();
|
||||
assert_items_eq!(linked_chunk, ['a', 'B', 'c'] [-]);
|
||||
|
||||
// Attempt to replace out-of-bounds.
|
||||
assert_matches!(
|
||||
linked_chunk.replace_item_at(Position(ChunkIdentifier::new(0), 3), 'Z'),
|
||||
Err(Error::InvalidItemIndex { index: 3 })
|
||||
);
|
||||
|
||||
// Attempt to replace gap.
|
||||
assert_matches!(
|
||||
linked_chunk.replace_item_at(Position(ChunkIdentifier::new(1), 0), 'Z'),
|
||||
Err(Error::ChunkIsAGap { .. })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +136,19 @@ impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
|
||||
}
|
||||
}
|
||||
|
||||
Update::ReplaceItem { at, item } => {
|
||||
let existing = self
|
||||
.items
|
||||
.iter_mut()
|
||||
.find(|item| item.position == at)
|
||||
.expect("trying to replace at an unknown position");
|
||||
assert!(
|
||||
matches!(existing.item, Either::Item(..)),
|
||||
"trying to replace a gap with an item"
|
||||
);
|
||||
existing.item = Either::Item(item);
|
||||
}
|
||||
|
||||
Update::RemoveItem { at } => {
|
||||
let mut entry_to_remove = None;
|
||||
|
||||
@@ -188,8 +201,8 @@ impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
|
||||
Update::StartReattachItems | Update::EndReattachItems => { /* nothing */ }
|
||||
|
||||
Update::Clear => {
|
||||
self.chunks.clear();
|
||||
self.items.clear();
|
||||
self.chunks.retain(|chunk| chunk.room_id != room_id);
|
||||
self.items.retain(|chunk| chunk.room_id != room_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -777,11 +790,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_clear() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let r0 = room_id!("!r0:matrix.org");
|
||||
let r1 = room_id!("!r1:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
r0,
|
||||
vec![
|
||||
// new chunk (this is not mandatory for this test, but let's try to be realistic)
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
@@ -790,42 +804,84 @@ mod tests {
|
||||
],
|
||||
);
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
r1,
|
||||
vec![
|
||||
// new chunk (this is not mandatory for this test, but let's try to be realistic)
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['x'] },
|
||||
],
|
||||
);
|
||||
|
||||
// Chunks are correctly linked.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.chunks,
|
||||
&[ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: None,
|
||||
chunk: CId::new(0),
|
||||
next_chunk: None,
|
||||
}],
|
||||
&[
|
||||
ChunkRow {
|
||||
room_id: r0.to_owned(),
|
||||
previous_chunk: None,
|
||||
chunk: CId::new(0),
|
||||
next_chunk: None,
|
||||
},
|
||||
ChunkRow {
|
||||
room_id: r1.to_owned(),
|
||||
previous_chunk: None,
|
||||
chunk: CId::new(0),
|
||||
next_chunk: None,
|
||||
}
|
||||
],
|
||||
);
|
||||
|
||||
// Items contains the pushed items.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.items,
|
||||
&[
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
room_id: r0.to_owned(),
|
||||
position: Position::new(CId::new(0), 0),
|
||||
item: Either::Item('a')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
room_id: r0.to_owned(),
|
||||
position: Position::new(CId::new(0), 1),
|
||||
item: Either::Item('b')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
room_id: r0.to_owned(),
|
||||
position: Position::new(CId::new(0), 2),
|
||||
item: Either::Item('c')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: r1.to_owned(),
|
||||
position: Position::new(CId::new(0), 0),
|
||||
item: Either::Item('x')
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
// Now, time for a clean up.
|
||||
relational_linked_chunk.apply_updates(room_id, vec![Update::Clear]);
|
||||
assert!(relational_linked_chunk.chunks.is_empty());
|
||||
assert!(relational_linked_chunk.items.is_empty());
|
||||
relational_linked_chunk.apply_updates(r0, vec![Update::Clear]);
|
||||
|
||||
// Only items from r1 remain.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.chunks,
|
||||
&[ChunkRow {
|
||||
room_id: r1.to_owned(),
|
||||
previous_chunk: None,
|
||||
chunk: CId::new(0),
|
||||
next_chunk: None,
|
||||
}],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
relational_linked_chunk.items,
|
||||
&[ItemRow {
|
||||
room_id: r1.to_owned(),
|
||||
position: Position::new(CId::new(0), 0),
|
||||
item: Either::Item('x')
|
||||
},],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -895,4 +951,55 @@ mod tests {
|
||||
// The linked chunk is correctly reloaded.
|
||||
assert_items_eq!(lc, ['a', 'b', 'c'] [-] ['d', 'e', 'f']);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replace_item() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
vec![
|
||||
// new chunk (this is not mandatory for this test, but let's try to be realistic)
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] },
|
||||
// update item at (0; 1).
|
||||
Update::ReplaceItem { at: Position::new(CId::new(0), 1), item: 'B' },
|
||||
],
|
||||
);
|
||||
|
||||
// Chunks are correctly linked.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.chunks,
|
||||
&[ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: None,
|
||||
chunk: CId::new(0),
|
||||
next_chunk: None,
|
||||
},],
|
||||
);
|
||||
|
||||
// Items contains the pushed *and* replaced items.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.items,
|
||||
&[
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 0),
|
||||
item: Either::Item('a')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 1),
|
||||
item: Either::Item('B')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 2),
|
||||
item: Either::Item('c')
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,18 @@ pub enum Update<Item, Gap> {
|
||||
items: Vec<Item>,
|
||||
},
|
||||
|
||||
/// An item has been replaced in the linked chunk.
|
||||
///
|
||||
/// The `at` position MUST resolve to the actual position an existing *item*
|
||||
/// (not a gap).
|
||||
ReplaceItem {
|
||||
/// The position of the item that's being replaced.
|
||||
at: Position,
|
||||
|
||||
/// The new value for the item.
|
||||
item: Item,
|
||||
},
|
||||
|
||||
/// An item has been removed inside a chunk of kind Items.
|
||||
RemoveItem {
|
||||
/// The [`Position`] of the item.
|
||||
@@ -138,6 +150,7 @@ impl<Item, Gap> ObservableUpdates<Item, Gap> {
|
||||
}
|
||||
|
||||
/// Subscribe to updates by using a [`Stream`].
|
||||
#[cfg(test)]
|
||||
pub(super) fn subscribe(&mut self) -> UpdatesSubscriber<Item, Gap> {
|
||||
// A subscriber is a new update reader, it needs its own token.
|
||||
let token = self.new_reader_token();
|
||||
@@ -264,6 +277,7 @@ impl<Item, Gap> UpdatesInner<Item, Gap> {
|
||||
}
|
||||
|
||||
/// Return the number of updates in the buffer.
|
||||
#[cfg(test)]
|
||||
fn len(&self) -> usize {
|
||||
self.updates.len()
|
||||
}
|
||||
@@ -302,6 +316,7 @@ pub(super) struct UpdatesSubscriber<Item, Gap> {
|
||||
|
||||
impl<Item, Gap> UpdatesSubscriber<Item, Gap> {
|
||||
/// Create a new [`Self`].
|
||||
#[cfg(test)]
|
||||
fn new(updates: Weak<RwLock<UpdatesInner<Item, Gap>>>, token: ReaderToken) -> Self {
|
||||
Self { updates, token }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright 2025 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.
|
||||
|
||||
//! Simplified locks hat panic instead of returning a `Result` when the lock is
|
||||
//! poisoned.
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
sync::{Mutex as StdMutex, MutexGuard, RwLock as StdRwLock, RwLockReadGuard, RwLockWriteGuard},
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A wrapper around `std::sync::Mutex` that panics on poison.
|
||||
///
|
||||
/// This `Mutex` works similarly to the standard library's `Mutex`, except its
|
||||
/// `lock` method does not return a `Result`. Instead, if the mutex is poisoned,
|
||||
/// it will panic.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use matrix_sdk_common::locks::Mutex;
|
||||
///
|
||||
/// let mutex = Mutex::new(42);
|
||||
///
|
||||
/// {
|
||||
/// let mut guard = mutex.lock();
|
||||
/// *guard = 100;
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(*mutex.lock(), 100);
|
||||
/// ```
|
||||
#[derive(Default)]
|
||||
pub struct Mutex<T: ?Sized>(StdMutex<T>);
|
||||
|
||||
impl<T> Mutex<T> {
|
||||
/// Creates a new `Mutex` wrapping the given value.
|
||||
pub const fn new(t: T) -> Self {
|
||||
Self(StdMutex::new(t))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Mutex<T> {
|
||||
/// Acquires the lock, panicking if the lock is poisoned.
|
||||
///
|
||||
/// This method blocks the current thread until the lock is acquired.
|
||||
pub fn lock(&self) -> MutexGuard<'_, T> {
|
||||
self.0.lock().expect("The Mutex should never be poisoned")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized + fmt::Debug> fmt::Debug for Mutex<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around [`std::sync::RwLock`] that panics on poison.
|
||||
///
|
||||
/// This `RwLock` works similarly to the standard library's `RwLock`, except its
|
||||
/// `read` and `write` methods do not return a `Result`. Instead, if the lock is
|
||||
/// poisoned, it will panic.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use matrix_sdk_common::locks::RwLock;
|
||||
///
|
||||
/// let lock = RwLock::new(42);
|
||||
///
|
||||
/// {
|
||||
/// let read_guard = lock.read();
|
||||
/// assert_eq!(*read_guard, 42);
|
||||
/// }
|
||||
/// {
|
||||
/// let mut write_guard = lock.write();
|
||||
/// *write_guard = 100;
|
||||
/// }
|
||||
/// assert_eq!(*lock.read(), 100);
|
||||
/// ```
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct RwLock<T: ?Sized>(StdRwLock<T>);
|
||||
|
||||
impl<T> RwLock<T> {
|
||||
/// Creates a new `RwLock` wrapping the given value.
|
||||
pub const fn new(t: T) -> Self {
|
||||
Self(StdRwLock::new(t))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> RwLock<T> {
|
||||
/// Acquires a mutable write lock, panicking if the lock is poisoned.
|
||||
///
|
||||
/// This method blocks the current thread until the lock is acquired.
|
||||
pub fn write(&self) -> RwLockWriteGuard<'_, T> {
|
||||
self.0.write().expect("The RwLock should never be poisoned")
|
||||
}
|
||||
|
||||
/// Acquires a shared read lock, panicking if the lock is poisoned.
|
||||
///
|
||||
/// This method blocks the current thread until the lock is acquired.
|
||||
pub fn read(&self) -> RwLockReadGuard<'_, T> {
|
||||
self.0.read().expect("The RwLock should never be poisoned")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized + fmt::Debug> fmt::Debug for RwLock<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for RwLock<T> {
|
||||
fn from(value: T) -> Self {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright 2024 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::time::Duration;
|
||||
|
||||
/// Sleep for the specified duration.
|
||||
///
|
||||
/// This is a cross-platform sleep implementation that works on both wasm32 and
|
||||
/// non-wasm32 targets.
|
||||
pub async fn sleep(duration: Duration) {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
tokio::time::sleep(duration).await;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
gloo_timers::future::TimeoutFuture::new(u32::try_from(duration.as_millis()).unwrap_or_else(
|
||||
|_| {
|
||||
tracing::error!("Sleep duration too long, sleeping for u32::MAX ms");
|
||||
u32::MAX
|
||||
},
|
||||
))
|
||||
.await;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use matrix_sdk_test_macros::async_test;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[async_test]
|
||||
async fn test_sleep() {
|
||||
// Just test that it doesn't panic
|
||||
sleep(Duration::from_millis(1)).await;
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
---
|
||||
source: crates/matrix-sdk-common/src/deserialized_responses.rs
|
||||
expression: info
|
||||
---
|
||||
{
|
||||
"MegolmV1AesSha2": {
|
||||
"curve25519_key": "curvecurvecurve",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "claimedclaimeded25519",
|
||||
"curve25519": "claimedclaimedcurve25519"
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
---
|
||||
source: crates/matrix-sdk-common/src/deserialized_responses.rs
|
||||
expression: info
|
||||
---
|
||||
{
|
||||
"sender": "@alice:localhost",
|
||||
"sender_device": "ABCDEFGH",
|
||||
"algorithm_info": {
|
||||
"MegolmV1AesSha2": {
|
||||
"curve25519_key": "curvecurvecurve",
|
||||
"sender_claimed_keys": {}
|
||||
}
|
||||
},
|
||||
"verification_state": "Verified"
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: crates/matrix-sdk-common/src/deserialized_responses.rs
|
||||
expression: "serde_json::to_value(&code).unwrap()"
|
||||
---
|
||||
"UnknownDevice"
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: crates/matrix-sdk-common/src/deserialized_responses.rs
|
||||
expression: "serde_json::to_value(&code).unwrap()"
|
||||
---
|
||||
"UnsignedDevice"
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: crates/matrix-sdk-common/src/deserialized_responses.rs
|
||||
expression: "serde_json::to_value(&code).unwrap()"
|
||||
---
|
||||
"UnverifiedIdentity"
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: crates/matrix-sdk-common/src/deserialized_responses.rs
|
||||
expression: "serde_json::to_value(&code).unwrap()"
|
||||
---
|
||||
"SentInClear"
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: crates/matrix-sdk-common/src/deserialized_responses.rs
|
||||
expression: "serde_json::to_value(&code).unwrap()"
|
||||
---
|
||||
"VerificationViolation"
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: crates/matrix-sdk-common/src/deserialized_responses.rs
|
||||
expression: "serde_json::to_value(&code).unwrap()"
|
||||
---
|
||||
"AuthenticityNotGuaranteed"
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
---
|
||||
source: crates/matrix-sdk-common/src/deserialized_responses.rs
|
||||
expression: "serde_json::to_value(&state).unwrap()"
|
||||
---
|
||||
{
|
||||
"Red": {
|
||||
"code": "UnverifiedIdentity",
|
||||
"message": "a message"
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
---
|
||||
source: crates/matrix-sdk-common/src/deserialized_responses.rs
|
||||
expression: "serde_json::to_value(&state).unwrap()"
|
||||
---
|
||||
{
|
||||
"Grey": {
|
||||
"code": "AuthenticityNotGuaranteed",
|
||||
"message": "authenticity of this message cannot be guaranteed"
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: crates/matrix-sdk-common/src/deserialized_responses.rs
|
||||
expression: "serde_json::to_value(&state).unwrap()"
|
||||
---
|
||||
"None"
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
---
|
||||
source: crates/matrix-sdk-common/src/deserialized_responses.rs
|
||||
expression: "serde_json::to_value(&room_event).unwrap()"
|
||||
---
|
||||
{
|
||||
"kind": {
|
||||
"Decrypted": {
|
||||
"encryption_info": {
|
||||
"algorithm_info": {
|
||||
"MegolmV1AesSha2": {
|
||||
"curve25519_key": "xxx",
|
||||
"sender_claimed_keys": {
|
||||
"curve25519": "qzdW3F5IMPFl0HQgz5w/L5Oi/npKUFn8Um84acIHfPY",
|
||||
"ed25519": "I3YsPwqMZQXHkSQbjFNEs7b529uac2xBpI83eN3LUXo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sender": "@sender:example.com",
|
||||
"sender_device": "ABCDEFGHIJ",
|
||||
"verification_state": "Verified"
|
||||
},
|
||||
"event": {
|
||||
"content": {
|
||||
"body": "secret",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$xxxxx:example.org",
|
||||
"origin_server_ts": 2189,
|
||||
"room_id": "!someroom:example.com",
|
||||
"sender": "@carl:example.com",
|
||||
"type": "m.room.message"
|
||||
},
|
||||
"unsigned_encryption_info": {
|
||||
"RelationsThreadLatestEvent": {
|
||||
"UnableToDecrypt": {
|
||||
"reason": {
|
||||
"MissingMegolmSession": {
|
||||
"withheld_code": "m.unverified"
|
||||
}
|
||||
},
|
||||
"session_id": "xyz"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: crates/matrix-sdk-common/src/deserialized_responses.rs
|
||||
expression: "serde_json::to_value(&level).unwrap()"
|
||||
---
|
||||
"UnsignedDevice"
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
---
|
||||
source: crates/matrix-sdk-common/src/deserialized_responses.rs
|
||||
expression: "serde_json::to_value(&level).unwrap()"
|
||||
---
|
||||
{
|
||||
"None": "InsecureSource"
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
---
|
||||
source: crates/matrix-sdk-common/src/deserialized_responses.rs
|
||||
expression: "serde_json::to_value(&level).unwrap()"
|
||||
---
|
||||
{
|
||||
"None": "MissingDevice"
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: crates/matrix-sdk-common/src/deserialized_responses.rs
|
||||
expression: "serde_json::to_value(&level).unwrap()"
|
||||
---
|
||||
"UnverifiedIdentity"
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: crates/matrix-sdk-common/src/deserialized_responses.rs
|
||||
expression: "serde_json::to_value(&level).unwrap()"
|
||||
---
|
||||
"VerificationViolation"
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
---
|
||||
source: crates/matrix-sdk-common/src/deserialized_responses.rs
|
||||
expression: "serde_json::to_value(&state).unwrap()"
|
||||
---
|
||||
{
|
||||
"Unverified": "VerificationViolation"
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
---
|
||||
source: crates/matrix-sdk-common/src/deserialized_responses.rs
|
||||
expression: "serde_json::to_value(&state).unwrap()"
|
||||
---
|
||||
{
|
||||
"Unverified": {
|
||||
"None": "InsecureSource"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user