Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b16d488ad | |||
| d40aac89cb | |||
| e4ebeb8a42 | |||
| 22bbe0c32e | |||
| 05505a5a48 | |||
| 21bb85ac21 | |||
| f1a442bad0 | |||
| a8a83c3b45 | |||
| 47246483fa | |||
| 31006ab3bf | |||
| 3ed5d34f49 | |||
| 232391c6b2 | |||
| cefd5a27f5 | |||
| 97952902a3 | |||
| bf4a2ed297 | |||
| a499988621 | |||
| 0d01cabb8d | |||
| f3c0309fbc | |||
| 8070e3c165 | |||
| 02c7c2cdfc | |||
| 9b6de4e436 | |||
| b7d4be9b65 | |||
| bc86027853 | |||
| 50db563363 | |||
| 8fa07ec22d | |||
| 7aa930b81c | |||
| c02d8cee77 | |||
| 2872af234b | |||
| d614878436 | |||
| afaecdc457 | |||
| aca83fb4ed | |||
| c3e28f7e33 | |||
| 949cd78d94 | |||
| 99b9c50548 | |||
| 371e7bc052 | |||
| 0541ec7e3f | |||
| 0509236cf8 | |||
| 6cef7f20c5 | |||
| e798a51709 | |||
| 8f8aad6f4d | |||
| af84c79e69 | |||
| a920c3fdec | |||
| 9dd2d5ee3c | |||
| f341dc4131 | |||
| d446eb933e | |||
| 8f0f0fa4d4 | |||
| 36b96ccef2 | |||
| 5957232e54 | |||
| 6f60eea9ce | |||
| 982c6eab54 | |||
| cfd0c5ce0c | |||
| 8e2939bd91 | |||
| 66a79729ed | |||
| bd5f5f3fe0 | |||
| 403be3dea0 | |||
| 4d39d176d9 | |||
| d3a232607a | |||
| 3070154a57 | |||
| 563c3aae31 | |||
| 90b8ba3c2e | |||
| 031a96200b | |||
| 57e78dd22b | |||
| 53900294d0 | |||
| f483f35573 | |||
| 204e6e4ca0 | |||
| ca8c635f62 | |||
| b8a61cfc17 | |||
| ab61077a8b | |||
| 26bee1cc38 | |||
| 46232ee2c1 | |||
| 7c600fddf0 | |||
| 965a59d5b8 | |||
| f032d16d20 | |||
| 57137cdd5b | |||
| 5d83808143 | |||
| df465a0420 | |||
| 4ca69da93c | |||
| 4039359512 | |||
| 1304902cb4 | |||
| 219be9b731 |
@@ -10,6 +10,7 @@ exclude = [
|
||||
version = 2
|
||||
ignore = [
|
||||
{ id = "RUSTSEC-2023-0071", reason = "We are not using RSA directly, nor do we depend on the RSA crate directly" },
|
||||
{ id = "RUSTSEC-2024-0384", reason = "Unmaintained backoff crate, not critical. We'll migrate soon." },
|
||||
]
|
||||
|
||||
[licenses]
|
||||
|
||||
@@ -304,7 +304,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check the spelling of the files in our repo
|
||||
uses: crate-ci/typos@v1.27.0
|
||||
uses: crate-ci/typos@v1.27.3
|
||||
|
||||
clippy:
|
||||
name: Run clippy
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Copied with minimal adjustments, source:
|
||||
# https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/coverage-report.yml
|
||||
name: Codecov
|
||||
name: Upload code coverage
|
||||
|
||||
on:
|
||||
# This workflow is triggered after every successful execution
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
path: repo_root
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
# Architecture
|
||||
|
||||
The SDK is split into multiple layers:
|
||||
|
||||
```
|
||||
WASM (external crate matrix-rust-sdk-crypto-wasm)
|
||||
/
|
||||
/ uniffi
|
||||
/ /
|
||||
/ bindings (matrix-sdk-ffi)
|
||||
crypto |
|
||||
bindings |
|
||||
| |
|
||||
| UI (matrix-sdk-ui)
|
||||
| \
|
||||
| \
|
||||
| main (matrix-sdk)
|
||||
| / /
|
||||
crypto /
|
||||
\ /
|
||||
store (matrix-sdk-base, + all the store impls)
|
||||
|
|
||||
common (matrix-sdk-common)
|
||||
```
|
||||
|
||||
Where the store implementations are `matrix-sdk-sqlite` and `matrix-sdk-indexeddb` as well as
|
||||
`MemoryStore` which is defined in `matrix-sdk-base`.
|
||||
|
||||
## `crates/matrix-sdk`
|
||||
|
||||
This is the main crate, and one that is expected to be used by most consumers. Notable data types
|
||||
include:
|
||||
|
||||
- the `Client`, which can run room-independent requests: logging in/out, creating rooms, running
|
||||
sync, etc.
|
||||
- the `Room`, which represents a room and its state (notably via the observable `RoomInfo`), and
|
||||
allows running queries that are room-specific, notably sending events.
|
||||
|
||||
## `crates/matrix-sdk-base`
|
||||
|
||||
A *sans I/O* crate to represent the base data types persisted in the SDK. No network or storage I/O
|
||||
happens in this crate, although it defines traits (`StateStore` and `EventCacheStore`) representing
|
||||
storage backends, as well as dummy in-memory implementations of these traits.
|
||||
|
||||
## `crates/matrix-sdk-common`
|
||||
|
||||
Common helpers used by most of the other crates; almost a leaf in the dependency tree of our own
|
||||
crates (the only crate it's using is test helpers).
|
||||
|
||||
## `crates/matrix-sdk-crypto`
|
||||
|
||||
A *sans I/O* implementation of a state machine that handles end-to-end encryption for Matrix
|
||||
clients. It defines a `CryptoStore` trait representing storage backends that will perform the
|
||||
actual storage I/O later, as well as a dummy in-memory implementation of this trait.
|
||||
|
||||
## `crates/matrix-sdk-indexeddb`
|
||||
|
||||
Implementations of `EventCacheStore`, `StateStore` and `CryptoStore` for a
|
||||
indexeddb backend (for use in Web browsers, via WebAssembly).
|
||||
|
||||
## `crates/matrix-sdk-qrcode`
|
||||
|
||||
Implementation of QR codes for interactive verifications, used in the crypto crate.
|
||||
|
||||
## `crates/matrix-sdk-sqlite`
|
||||
|
||||
Implementations of `EventCacheStore`, `StateStore` and `CryptoStore` for a
|
||||
SQLite backend.
|
||||
|
||||
## `crates/matrix-sdk-store-encryption`
|
||||
|
||||
Low-level primitives for encrypting/decrypting/hashing values. Store implementations that
|
||||
implement encryption at rest can use those primitives.
|
||||
|
||||
## `crates/matrix-sdk-ui`
|
||||
|
||||
Very high-level primitives implementing the best practices and cutting-edge Matrix tech:
|
||||
|
||||
- `EncryptionSyncService`: a specialized service running simplified sliding sync (MSC4186) for
|
||||
everything related to crypto and E2EE for the current `Client`.
|
||||
- `RoomListService`: a specialized service running simplified sliding sync (MSC4186) for
|
||||
retrieving the list of current rooms, and exposing its entries.
|
||||
- `SyncService`: a wrapper for the two previous services, coordinating their running and shutting
|
||||
down.
|
||||
- `Timeline`: a high-level view for a `Room`'s timeline of events, grouping related events
|
||||
(aggregations) into single timeline items.
|
||||
|
||||
## `bindings/matrix-sdk-crypto-ffi/`
|
||||
|
||||
FFI bindings for the crypto crate, used in a Web browser context via WebAssembly. These use
|
||||
`wasm-bindgen` to generate the bindings. These bindings are used in Element Web and the legacy
|
||||
Element apps, as of 2024-11-07.
|
||||
|
||||
## `bindings/matrix-sdk-ffi/`
|
||||
|
||||
FFI bindings for important concepts in `matrix-sdk-ui` and `matrix-sdk`, generated with
|
||||
[UniFFI](https://github.com/mozilla/uniffi-rs) and to be used from other languages like
|
||||
Swift/Go/Kotlin. These bindings are used in the ElementX apps, as of 2024-11-07.
|
||||
|
||||
## `bindings/matrix-sdk-ffi-macros/`
|
||||
|
||||
Macros used in `bindings/matrix-sdk-ffi`.
|
||||
|
||||
## `testing/matrix-sdk-test/`
|
||||
|
||||
Common test helpers, used by all the other crates.
|
||||
|
||||
## `testing/matrix-sdk-test-macros/`
|
||||
|
||||
Implementation of the `#[async_test]` test macro.
|
||||
|
||||
## `testing/matrix-sdk-integration-testing/`
|
||||
|
||||
Fully-fledged integration tests that require spawning a Synapse instance to run. A docker-compose
|
||||
setup is provided to ease running the tests, and it is compatible for running with Podman too.
|
||||
|
||||
# Inspiration
|
||||
|
||||
This document has been inspired by the reading of this [blog post](https://matklad.github.io/2021/02/06/ARCHITECTURE.md.html).
|
||||
Generated
+46
-16
@@ -1246,6 +1246,17 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "decancer"
|
||||
version = "3.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a41401dd84c9335e2f5aec7f64057e243585d62622260d41c245919a601ccc9"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"paste",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "delegate-display"
|
||||
version = "2.1.1"
|
||||
@@ -1642,22 +1653,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "eyeball-im"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ae8c5165c9770f3ec7cccce12f4c5d70f01fa8bf84cf30cfbfd5a1c6f8901d5"
|
||||
checksum = "a1c02432230060cae0621e15803e073976d22974e0f013c9cb28a4ea1b484629"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"imbl",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "eyeball-im-util"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32b6b037e2cdce928a432ecc2880c944e5436d8a38c827974b882ad373f60037"
|
||||
checksum = "f63a70e454238b5f66a0a0544c3e6a38be765cb01f34da9b94a2f3ecd8777cf8"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"eyeball-im",
|
||||
@@ -2872,7 +2882,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matrix-sdk"
|
||||
version = "0.7.1"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"anymap2",
|
||||
@@ -2915,6 +2925,7 @@ dependencies = [
|
||||
"mime2ext",
|
||||
"once_cell",
|
||||
"openidconnect",
|
||||
"pin-project-lite",
|
||||
"proptest",
|
||||
"rand",
|
||||
"reqwest",
|
||||
@@ -2930,6 +2941,7 @@ dependencies = [
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-test",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tracing",
|
||||
@@ -2946,7 +2958,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matrix-sdk-base"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"assert_matches",
|
||||
@@ -2954,6 +2966,7 @@ dependencies = [
|
||||
"assign",
|
||||
"async-trait",
|
||||
"bitflags 2.6.0",
|
||||
"decancer",
|
||||
"eyeball",
|
||||
"eyeball-im",
|
||||
"futures-executor",
|
||||
@@ -2969,23 +2982,27 @@ dependencies = [
|
||||
"ruma",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"similar-asserts",
|
||||
"stream_assert",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"unicode-normalization",
|
||||
"uniffi",
|
||||
"wasm-bindgen-test",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matrix-sdk-common"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"assert_matches",
|
||||
"async-trait",
|
||||
"eyeball-im",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"gloo-timers",
|
||||
"imbl",
|
||||
"js-sys",
|
||||
"matrix-sdk-test",
|
||||
"proptest",
|
||||
@@ -3005,7 +3022,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matrix-sdk-crypto"
|
||||
version = "0.7.2"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"anyhow",
|
||||
@@ -3128,7 +3145,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matrix-sdk-indexeddb"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_matches",
|
||||
@@ -3196,7 +3213,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matrix-sdk-qrcode"
|
||||
version = "0.7.1"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"image",
|
||||
@@ -3208,7 +3225,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matrix-sdk-sqlite"
|
||||
version = "0.7.1"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"assert_matches",
|
||||
"async-trait",
|
||||
@@ -3235,7 +3252,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matrix-sdk-store-encryption"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
@@ -3281,7 +3298,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matrix-sdk-ui"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"as_variant",
|
||||
@@ -5621,6 +5638,19 @@ dependencies = [
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-test"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.11"
|
||||
@@ -5891,9 +5921,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.23"
|
||||
version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
|
||||
checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
+11
-11
@@ -32,8 +32,8 @@ as_variant = "1.2.0"
|
||||
base64 = "0.22.0"
|
||||
byteorder = "1.4.3"
|
||||
eyeball = { version = "0.8.8", features = ["tracing"] }
|
||||
eyeball-im = { version = "0.5.0", features = ["tracing"] }
|
||||
eyeball-im-util = "0.6.0"
|
||||
eyeball-im = { version = "0.5.1", features = ["tracing"] }
|
||||
eyeball-im-util = "0.7.0"
|
||||
futures-core = "0.3.28"
|
||||
futures-executor = "0.3.21"
|
||||
futures-util = "0.3.26"
|
||||
@@ -79,17 +79,17 @@ vodozemac = { version = "0.8.0", features = ["insecure-pk-encryption"] }
|
||||
wiremock = "0.6.0"
|
||||
zeroize = "1.6.0"
|
||||
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.7.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.7.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.7.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.7.0" }
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.8.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.8.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.8.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.8.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.7.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.7.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.7.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.7.0" }
|
||||
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.8.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.8.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.8.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.8.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.7.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.7.0", default-features = false }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.8.0", default-features = false }
|
||||
|
||||
# Default release profile, select with `--release`
|
||||
[profile.release]
|
||||
|
||||
@@ -74,7 +74,10 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
|
||||
.block_on(sqlite_store.save_changes(&changes))
|
||||
.expect("initial filling of sqlite failed");
|
||||
|
||||
let base_client = BaseClient::with_store_config(StoreConfig::new().state_store(sqlite_store));
|
||||
let base_client = BaseClient::with_store_config(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
|
||||
.state_store(sqlite_store),
|
||||
);
|
||||
|
||||
runtime
|
||||
.block_on(base_client.set_session_meta(
|
||||
|
||||
@@ -69,7 +69,10 @@ pub fn restore_session(c: &mut Criterion) {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let client = Client::builder()
|
||||
.homeserver_url("https://matrix.example.com")
|
||||
.store_config(StoreConfig::new().state_store(store.clone()))
|
||||
.store_config(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
|
||||
.state_store(store.clone()),
|
||||
)
|
||||
.build()
|
||||
.await
|
||||
.expect("Can't build client");
|
||||
@@ -96,7 +99,10 @@ pub fn restore_session(c: &mut Criterion) {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let client = Client::builder()
|
||||
.homeserver_url("https://matrix.example.com")
|
||||
.store_config(StoreConfig::new().state_store(store.clone()))
|
||||
.store_config(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
|
||||
.state_store(store.clone()),
|
||||
)
|
||||
.build()
|
||||
.await
|
||||
.expect("Can't build client");
|
||||
|
||||
@@ -194,7 +194,7 @@ pub struct Client {
|
||||
impl Client {
|
||||
pub async fn new(
|
||||
sdk_client: MatrixClient,
|
||||
cross_process_refresh_lock_id: Option<String>,
|
||||
enable_oidc_refresh_lock: bool,
|
||||
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
|
||||
) -> Result<Self, ClientError> {
|
||||
let session_verification_controller: Arc<
|
||||
@@ -210,19 +210,27 @@ impl Client {
|
||||
}
|
||||
});
|
||||
|
||||
let cross_process_store_locks_holder_name =
|
||||
sdk_client.cross_process_store_locks_holder_name().to_owned();
|
||||
|
||||
let client = Client {
|
||||
inner: AsyncRuntimeDropped::new(sdk_client),
|
||||
delegate: RwLock::new(None),
|
||||
session_verification_controller,
|
||||
};
|
||||
|
||||
if let Some(process_id) = cross_process_refresh_lock_id {
|
||||
if enable_oidc_refresh_lock {
|
||||
if session_delegate.is_none() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"missing session delegates when enabling the cross-process lock"
|
||||
))?;
|
||||
}
|
||||
client.inner.oidc().enable_cross_process_refresh_lock(process_id.clone()).await?;
|
||||
|
||||
client
|
||||
.inner
|
||||
.oidc()
|
||||
.enable_cross_process_refresh_lock(cross_process_store_locks_holder_name)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(session_delegate) = session_delegate {
|
||||
@@ -695,7 +703,7 @@ impl Client {
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Result<String, ClientError> {
|
||||
let mime_type: mime::Mime = mime_type.parse().context("Parsing mime type")?;
|
||||
let request = self.inner.media().upload(&mime_type, data);
|
||||
let request = self.inner.media().upload(&mime_type, data, None);
|
||||
|
||||
if let Some(progress_watcher) = progress_watcher {
|
||||
let mut subscriber = request.subscribe_to_send_progress();
|
||||
@@ -1130,21 +1138,27 @@ impl Client {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks if a room alias is available in the current homeserver.
|
||||
/// Checks if a room alias is not in use yet.
|
||||
///
|
||||
/// Returns:
|
||||
/// - `Ok(true)` if the room alias is available.
|
||||
/// - `Ok(false)` if it's not (the resolve alias request returned a `404`
|
||||
/// status code).
|
||||
/// - An `Err` otherwise.
|
||||
pub async fn is_room_alias_available(&self, alias: String) -> Result<bool, ClientError> {
|
||||
let alias = RoomAliasId::parse(alias)?;
|
||||
match self.inner.resolve_room_alias(&alias).await {
|
||||
// The room alias was resolved, so it's already in use.
|
||||
Ok(_) => Ok(false),
|
||||
Err(HttpError::Reqwest(error)) => {
|
||||
match error.status() {
|
||||
// The room alias wasn't found, so it's available.
|
||||
Some(StatusCode::NOT_FOUND) => Ok(true),
|
||||
_ => Err(HttpError::Reqwest(error).into()),
|
||||
}
|
||||
}
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -260,7 +260,8 @@ pub struct ClientBuilder {
|
||||
proxy: Option<String>,
|
||||
disable_ssl_verification: bool,
|
||||
disable_automatic_token_refresh: bool,
|
||||
cross_process_refresh_lock_id: Option<String>,
|
||||
cross_process_store_locks_holder_name: Option<String>,
|
||||
enable_oidc_refresh_lock: bool,
|
||||
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
|
||||
additional_root_certificates: Vec<Vec<u8>>,
|
||||
disable_built_in_root_certificates: bool,
|
||||
@@ -284,7 +285,8 @@ impl ClientBuilder {
|
||||
proxy: None,
|
||||
disable_ssl_verification: false,
|
||||
disable_automatic_token_refresh: false,
|
||||
cross_process_refresh_lock_id: None,
|
||||
cross_process_store_locks_holder_name: None,
|
||||
enable_oidc_refresh_lock: false,
|
||||
session_delegate: None,
|
||||
additional_root_certificates: Default::default(),
|
||||
disable_built_in_root_certificates: false,
|
||||
@@ -300,14 +302,18 @@ impl ClientBuilder {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn enable_cross_process_refresh_lock(
|
||||
pub fn cross_process_store_locks_holder_name(
|
||||
self: Arc<Self>,
|
||||
process_id: String,
|
||||
session_delegate: Box<dyn ClientSessionDelegate>,
|
||||
holder_name: String,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.cross_process_refresh_lock_id = Some(process_id);
|
||||
builder.session_delegate = Some(session_delegate.into());
|
||||
builder.cross_process_store_locks_holder_name = Some(holder_name);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn enable_oidc_refresh_lock(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.enable_oidc_refresh_lock = true;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
@@ -472,6 +478,11 @@ impl ClientBuilder {
|
||||
let builder = unwrap_or_clone_arc(self);
|
||||
let mut inner_builder = MatrixClient::builder();
|
||||
|
||||
if let Some(holder_name) = &builder.cross_process_store_locks_holder_name {
|
||||
inner_builder =
|
||||
inner_builder.cross_process_store_locks_holder_name(holder_name.clone());
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -614,12 +625,8 @@ impl ClientBuilder {
|
||||
let sdk_client = inner_builder.build().await?;
|
||||
|
||||
Ok(Arc::new(
|
||||
Client::new(
|
||||
sdk_client,
|
||||
builder.cross_process_refresh_lock_id,
|
||||
builder.session_delegate,
|
||||
)
|
||||
.await?,
|
||||
Client::new(sdk_client, builder.enable_oidc_refresh_lock, builder.session_delegate)
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,7 @@ use ruma::{
|
||||
},
|
||||
TimelineEventType,
|
||||
},
|
||||
EventId, Int, OwnedDeviceId, OwnedTransactionId, OwnedUserId, RoomAliasId, TransactionId,
|
||||
UserId,
|
||||
EventId, Int, OwnedDeviceId, OwnedUserId, RoomAliasId, UserId,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::error;
|
||||
@@ -40,7 +39,7 @@ use crate::{
|
||||
room_info::RoomInfo,
|
||||
room_member::RoomMember,
|
||||
ruma::{ImageInfo, Mentions, NotifyType},
|
||||
timeline::{FocusEventError, ReceiptType, Timeline},
|
||||
timeline::{FocusEventError, ReceiptType, SendHandle, Timeline},
|
||||
utils::u64_to_uint,
|
||||
TaskHandle,
|
||||
};
|
||||
@@ -790,10 +789,8 @@ impl Room {
|
||||
pub async fn withdraw_verification_and_resend(
|
||||
&self,
|
||||
user_ids: Vec<String>,
|
||||
transaction_id: String,
|
||||
send_handle: Arc<SendHandle>,
|
||||
) -> Result<(), ClientError> {
|
||||
let transaction_id: OwnedTransactionId = transaction_id.into();
|
||||
|
||||
let user_ids: Vec<OwnedUserId> =
|
||||
user_ids.iter().map(UserId::parse).collect::<Result<_, _>>()?;
|
||||
|
||||
@@ -805,7 +802,7 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
self.inner.send_queue().unwedge(&transaction_id).await?;
|
||||
send_handle.try_resend().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -823,10 +820,8 @@ impl Room {
|
||||
pub async fn ignore_device_trust_and_resend(
|
||||
&self,
|
||||
devices: HashMap<String, Vec<String>>,
|
||||
transaction_id: String,
|
||||
send_handle: Arc<SendHandle>,
|
||||
) -> Result<(), ClientError> {
|
||||
let transaction_id: OwnedTransactionId = transaction_id.into();
|
||||
|
||||
let encryption = self.inner.client().encryption();
|
||||
|
||||
for (user_id, device_ids) in devices.iter() {
|
||||
@@ -841,28 +836,10 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
self.inner.send_queue().unwedge(&transaction_id).await?;
|
||||
send_handle.try_resend().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to manually resend messages that failed to send due to issues
|
||||
/// that should now have been fixed.
|
||||
///
|
||||
/// This is useful for example, when there's a
|
||||
/// `SessionRecipientCollectionError::VerifiedUserChangedIdentity` error;
|
||||
/// the user may have re-verified on a different device and would now
|
||||
/// like to send the failed message that's waiting on this device.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `transaction_id` - The send queue transaction identifier of the local
|
||||
/// echo that should be unwedged.
|
||||
pub async fn try_resend(&self, transaction_id: String) -> Result<(), ClientError> {
|
||||
let transaction_id: &TransactionId = transaction_id.as_str().into();
|
||||
self.inner.send_queue().unwedge(transaction_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a `matrix.to` permalink to the given room alias.
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
use matrix_sdk::DisplayName;
|
||||
use ruma::RoomAliasId;
|
||||
use matrix_sdk::RoomDisplayName;
|
||||
|
||||
/// Verifies the passed `String` matches the expected room alias format.
|
||||
/// Verifies the passed `String` matches the expected room alias format:
|
||||
///
|
||||
/// This means it's lowercase, with no whitespace chars, has a single leading
|
||||
/// `#` char and a single `:` separator between the local and domain parts, and
|
||||
/// the local part only contains characters that can't be percent encoded.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
fn is_room_alias_format_valid(alias: String) -> bool {
|
||||
RoomAliasId::parse(alias).is_ok()
|
||||
matrix_sdk::utils::is_room_alias_format_valid(alias)
|
||||
}
|
||||
|
||||
/// Transforms a Room's display name into a valid room alias name.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
fn room_alias_name_from_room_display_name(room_name: String) -> String {
|
||||
DisplayName::Named(room_name).to_room_alias_name()
|
||||
RoomDisplayName::Named(room_name).to_room_alias_name()
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use anyhow::Context as _;
|
||||
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, Client};
|
||||
use ruma::space::SpaceRoomJoinRule;
|
||||
use ruma::{room::RoomType as RumaRoomType, space::SpaceRoomJoinRule};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{client::JoinRule, error::ClientError, room::Membership, utils::AsyncRuntimeDropped};
|
||||
use crate::{
|
||||
client::JoinRule, error::ClientError, room::Membership, room_member::RoomMember,
|
||||
utils::AsyncRuntimeDropped,
|
||||
};
|
||||
|
||||
/// A room preview for a room. It's intended to be used to represent rooms that
|
||||
/// aren't joined yet.
|
||||
@@ -25,7 +28,8 @@ impl RoomPreview {
|
||||
topic: info.topic.clone(),
|
||||
avatar_url: info.avatar_url.as_ref().map(|url| url.to_string()),
|
||||
num_joined_members: info.num_joined_members,
|
||||
room_type: info.room_type.as_ref().map(|room_type| room_type.to_string()),
|
||||
num_active_members: info.num_active_members,
|
||||
room_type: info.room_type.as_ref().into(),
|
||||
is_history_world_readable: info.is_world_readable,
|
||||
membership: info.state.map(|state| state.into()),
|
||||
join_rule: info
|
||||
@@ -33,6 +37,7 @@ impl RoomPreview {
|
||||
.clone()
|
||||
.try_into()
|
||||
.map_err(|_| anyhow::anyhow!("unhandled SpaceRoomJoinRule kind"))?,
|
||||
is_direct: info.is_direct,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -45,6 +50,13 @@ impl RoomPreview {
|
||||
self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?;
|
||||
room.leave().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Get the user who created the invite, if any.
|
||||
pub async fn inviter(&self) -> Option<RoomMember> {
|
||||
let room = self.client.get_room(&self.inner.room_id)?;
|
||||
let invite_details = room.invite_details().await.ok()?;
|
||||
invite_details.inviter.and_then(|m| m.try_into().ok())
|
||||
}
|
||||
}
|
||||
|
||||
impl RoomPreview {
|
||||
@@ -68,14 +80,18 @@ pub struct RoomPreviewInfo {
|
||||
pub avatar_url: Option<String>,
|
||||
/// The number of joined members.
|
||||
pub num_joined_members: u64,
|
||||
/// The number of active members, if known (joined + invited).
|
||||
pub num_active_members: Option<u64>,
|
||||
/// The room type (space, custom) or nothing, if it's a regular room.
|
||||
pub room_type: Option<String>,
|
||||
pub room_type: RoomType,
|
||||
/// Is the history world-readable for this room?
|
||||
pub is_history_world_readable: bool,
|
||||
/// The membership state for the current user, if known.
|
||||
pub membership: Option<Membership>,
|
||||
/// The join rule for this room (private, public, knock, etc.).
|
||||
pub join_rule: JoinRule,
|
||||
/// Whether the room is direct or not, if known.
|
||||
pub is_direct: Option<bool>,
|
||||
}
|
||||
|
||||
impl TryFrom<SpaceRoomJoinRule> for JoinRule {
|
||||
@@ -97,3 +113,27 @@ impl TryFrom<SpaceRoomJoinRule> for JoinRule {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of room for a [`RoomPreviewInfo`].
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum RoomType {
|
||||
/// It's a plain chat room.
|
||||
Room,
|
||||
/// It's a space that can group several rooms.
|
||||
Space,
|
||||
/// It's a custom implementation.
|
||||
Custom { value: String },
|
||||
}
|
||||
|
||||
impl From<Option<&RumaRoomType>> for RoomType {
|
||||
fn from(value: Option<&RumaRoomType>) -> Self {
|
||||
match value {
|
||||
Some(RumaRoomType::Space) => RoomType::Space,
|
||||
Some(RumaRoomType::_Custom(_)) => RoomType::Custom {
|
||||
// SAFETY: this was checked in the match branch above
|
||||
value: value.unwrap().to_string(),
|
||||
},
|
||||
_ => RoomType::Room,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,6 +262,23 @@ pub enum MessageType {
|
||||
Other { msgtype: String, body: String },
|
||||
}
|
||||
|
||||
/// From MSC2530: https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2530-body-as-caption.md
|
||||
/// If the filename field is present in a media message, clients should treat
|
||||
/// body as a caption instead of a file name. Otherwise, the body is the
|
||||
/// file name.
|
||||
///
|
||||
/// So:
|
||||
/// - if a media has a filename and a caption, the body is the caption, filename
|
||||
/// is its own field.
|
||||
/// - if a media only has a filename, then body is the filename.
|
||||
fn get_body_and_filename(filename: String, caption: Option<String>) -> (String, Option<String>) {
|
||||
if let Some(caption) = caption {
|
||||
(caption, Some(filename))
|
||||
} else {
|
||||
(filename, None)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<MessageType> for RumaMessageType {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
@@ -273,35 +290,39 @@ impl TryFrom<MessageType> for RumaMessageType {
|
||||
}))
|
||||
}
|
||||
MessageType::Image { content } => {
|
||||
let (body, filename) = get_body_and_filename(content.filename, content.caption);
|
||||
let mut event_content =
|
||||
RumaImageMessageEventContent::new(content.body, (*content.source).clone())
|
||||
RumaImageMessageEventContent::new(body, (*content.source).clone())
|
||||
.info(content.info.map(Into::into).map(Box::new));
|
||||
event_content.formatted = content.formatted.map(Into::into);
|
||||
event_content.filename = content.raw_filename;
|
||||
event_content.formatted = content.formatted_caption.map(Into::into);
|
||||
event_content.filename = filename;
|
||||
Self::Image(event_content)
|
||||
}
|
||||
MessageType::Audio { content } => {
|
||||
let (body, filename) = get_body_and_filename(content.filename, content.caption);
|
||||
let mut event_content =
|
||||
RumaAudioMessageEventContent::new(content.body, (*content.source).clone())
|
||||
RumaAudioMessageEventContent::new(body, (*content.source).clone())
|
||||
.info(content.info.map(Into::into).map(Box::new));
|
||||
event_content.formatted = content.formatted.map(Into::into);
|
||||
event_content.filename = content.raw_filename;
|
||||
event_content.formatted = content.formatted_caption.map(Into::into);
|
||||
event_content.filename = filename;
|
||||
Self::Audio(event_content)
|
||||
}
|
||||
MessageType::Video { content } => {
|
||||
let (body, filename) = get_body_and_filename(content.filename, content.caption);
|
||||
let mut event_content =
|
||||
RumaVideoMessageEventContent::new(content.body, (*content.source).clone())
|
||||
RumaVideoMessageEventContent::new(body, (*content.source).clone())
|
||||
.info(content.info.map(Into::into).map(Box::new));
|
||||
event_content.formatted = content.formatted.map(Into::into);
|
||||
event_content.filename = content.raw_filename;
|
||||
event_content.formatted = content.formatted_caption.map(Into::into);
|
||||
event_content.filename = filename;
|
||||
Self::Video(event_content)
|
||||
}
|
||||
MessageType::File { content } => {
|
||||
let (body, filename) = get_body_and_filename(content.filename, content.caption);
|
||||
let mut event_content =
|
||||
RumaFileMessageEventContent::new(content.body, (*content.source).clone())
|
||||
RumaFileMessageEventContent::new(body, (*content.source).clone())
|
||||
.info(content.info.map(Into::into).map(Box::new));
|
||||
event_content.formatted = content.formatted.map(Into::into);
|
||||
event_content.filename = content.raw_filename;
|
||||
event_content.formatted = content.formatted_caption.map(Into::into);
|
||||
event_content.filename = filename;
|
||||
Self::File(event_content)
|
||||
}
|
||||
MessageType::Notice { content } => {
|
||||
@@ -335,9 +356,6 @@ impl From<RumaMessageType> for MessageType {
|
||||
},
|
||||
RumaMessageType::Image(c) => MessageType::Image {
|
||||
content: ImageMessageContent {
|
||||
body: c.body.clone(),
|
||||
formatted: c.formatted.as_ref().map(Into::into),
|
||||
raw_filename: c.filename.clone(),
|
||||
filename: c.filename().to_owned(),
|
||||
caption: c.caption().map(ToString::to_string),
|
||||
formatted_caption: c.formatted_caption().map(Into::into),
|
||||
@@ -347,9 +365,6 @@ impl From<RumaMessageType> for MessageType {
|
||||
},
|
||||
RumaMessageType::Audio(c) => MessageType::Audio {
|
||||
content: AudioMessageContent {
|
||||
body: c.body.clone(),
|
||||
formatted: c.formatted.as_ref().map(Into::into),
|
||||
raw_filename: c.filename.clone(),
|
||||
filename: c.filename().to_owned(),
|
||||
caption: c.caption().map(ToString::to_string),
|
||||
formatted_caption: c.formatted_caption().map(Into::into),
|
||||
@@ -361,9 +376,6 @@ impl From<RumaMessageType> for MessageType {
|
||||
},
|
||||
RumaMessageType::Video(c) => MessageType::Video {
|
||||
content: VideoMessageContent {
|
||||
body: c.body.clone(),
|
||||
formatted: c.formatted.as_ref().map(Into::into),
|
||||
raw_filename: c.filename.clone(),
|
||||
filename: c.filename().to_owned(),
|
||||
caption: c.caption().map(ToString::to_string),
|
||||
formatted_caption: c.formatted_caption().map(Into::into),
|
||||
@@ -373,9 +385,6 @@ impl From<RumaMessageType> for MessageType {
|
||||
},
|
||||
RumaMessageType::File(c) => MessageType::File {
|
||||
content: FileMessageContent {
|
||||
body: c.body.clone(),
|
||||
formatted: c.formatted.as_ref().map(Into::into),
|
||||
raw_filename: c.filename.clone(),
|
||||
filename: c.filename().to_owned(),
|
||||
caption: c.caption().map(ToString::to_string),
|
||||
formatted_caption: c.formatted_caption().map(Into::into),
|
||||
@@ -452,15 +461,6 @@ pub struct EmoteMessageContent {
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct ImageMessageContent {
|
||||
/// The original body field, deserialized from the event. Prefer the use of
|
||||
/// `filename` and `caption` over this.
|
||||
pub body: String,
|
||||
/// The original formatted body field, deserialized from the event. Prefer
|
||||
/// the use of `filename` and `formatted_caption` over this.
|
||||
pub formatted: Option<FormattedBody>,
|
||||
/// The original filename field, deserialized from the event. Prefer the use
|
||||
/// of `filename` over this.
|
||||
pub raw_filename: Option<String>,
|
||||
/// The computed filename, for use in a client.
|
||||
pub filename: String,
|
||||
pub caption: Option<String>,
|
||||
@@ -471,15 +471,6 @@ pub struct ImageMessageContent {
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct AudioMessageContent {
|
||||
/// The original body field, deserialized from the event. Prefer the use of
|
||||
/// `filename` and `caption` over this.
|
||||
pub body: String,
|
||||
/// The original formatted body field, deserialized from the event. Prefer
|
||||
/// the use of `filename` and `formatted_caption` over this.
|
||||
pub formatted: Option<FormattedBody>,
|
||||
/// The original filename field, deserialized from the event. Prefer the use
|
||||
/// of `filename` over this.
|
||||
pub raw_filename: Option<String>,
|
||||
/// The computed filename, for use in a client.
|
||||
pub filename: String,
|
||||
pub caption: Option<String>,
|
||||
@@ -492,15 +483,6 @@ pub struct AudioMessageContent {
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct VideoMessageContent {
|
||||
/// The original body field, deserialized from the event. Prefer the use of
|
||||
/// `filename` and `caption` over this.
|
||||
pub body: String,
|
||||
/// The original formatted body field, deserialized from the event. Prefer
|
||||
/// the use of `filename` and `formatted_caption` over this.
|
||||
pub formatted: Option<FormattedBody>,
|
||||
/// The original filename field, deserialized from the event. Prefer the use
|
||||
/// of `filename` over this.
|
||||
pub raw_filename: Option<String>,
|
||||
/// The computed filename, for use in a client.
|
||||
pub filename: String,
|
||||
pub caption: Option<String>,
|
||||
@@ -511,15 +493,6 @@ pub struct VideoMessageContent {
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct FileMessageContent {
|
||||
/// The original body field, deserialized from the event. Prefer the use of
|
||||
/// `filename` and `caption` over this.
|
||||
pub body: String,
|
||||
/// The original formatted body field, deserialized from the event. Prefer
|
||||
/// the use of `filename` and `formatted_caption` over this.
|
||||
pub formatted: Option<FormattedBody>,
|
||||
/// The original filename field, deserialized from the event. Prefer the use
|
||||
/// of `filename` over this.
|
||||
pub raw_filename: Option<String>,
|
||||
/// The computed filename, for use in a client.
|
||||
pub filename: String,
|
||||
pub caption: Option<String>,
|
||||
|
||||
@@ -112,9 +112,9 @@ impl SyncServiceBuilder {
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SyncServiceBuilder {
|
||||
pub fn with_cross_process_lock(self: Arc<Self>, app_identifier: Option<String>) -> Arc<Self> {
|
||||
pub fn with_cross_process_lock(self: Arc<Self>) -> Arc<Self> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
let builder = this.builder.with_cross_process_lock(app_identifier);
|
||||
let builder = this.builder.with_cross_process_lock();
|
||||
Arc::new(Self { client: this.client, builder, utd_hook: this.utd_hook })
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use matrix_sdk::{crypto::types::events::UtdCause, room::power_levels::power_level_user_changes};
|
||||
use matrix_sdk_ui::timeline::{PollResult, RoomPinnedEventsChange, TimelineDetails};
|
||||
use ruma::events::room::MediaSource;
|
||||
use ruma::events::{room::MediaSource, FullStateEventContent};
|
||||
|
||||
use super::ProfileDetails;
|
||||
use crate::ruma::{ImageInfo, Mentions, MessageType, PollKind};
|
||||
@@ -49,11 +49,18 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
|
||||
TimelineItemContent::UnableToDecrypt { msg: EncryptedMessage::new(&msg) }
|
||||
}
|
||||
|
||||
Content::MembershipChange(membership) => TimelineItemContent::RoomMembership {
|
||||
user_id: membership.user_id().to_string(),
|
||||
user_display_name: membership.display_name(),
|
||||
change: membership.change().map(Into::into),
|
||||
},
|
||||
Content::MembershipChange(membership) => {
|
||||
let reason = match membership.content() {
|
||||
FullStateEventContent::Original { content, .. } => content.reason.clone(),
|
||||
_ => None,
|
||||
};
|
||||
TimelineItemContent::RoomMembership {
|
||||
user_id: membership.user_id().to_string(),
|
||||
user_display_name: membership.display_name(),
|
||||
change: membership.change().map(Into::into),
|
||||
reason,
|
||||
}
|
||||
}
|
||||
|
||||
Content::ProfileChange(profile) => {
|
||||
let (display_name, prev_display_name) = profile
|
||||
@@ -161,6 +168,7 @@ pub enum TimelineItemContent {
|
||||
user_id: String,
|
||||
user_display_name: Option<String>,
|
||||
change: Option<MembershipChange>,
|
||||
reason: Option<String>,
|
||||
},
|
||||
ProfileChange {
|
||||
display_name: Option<String>,
|
||||
|
||||
@@ -81,6 +81,7 @@ use crate::{
|
||||
mod content;
|
||||
|
||||
pub use content::MessageContent;
|
||||
use matrix_sdk::utils::formatted_body_from;
|
||||
|
||||
use crate::error::QueueWedgeError;
|
||||
|
||||
@@ -270,7 +271,7 @@ impl Timeline {
|
||||
msg: Arc<RoomMessageEventContentWithoutRelation>,
|
||||
) -> Result<Arc<SendHandle>, ClientError> {
|
||||
match self.inner.send((*msg).to_owned().with_relation(None).into()).await {
|
||||
Ok(handle) => Ok(Arc::new(SendHandle { inner: Mutex::new(Some(handle)) })),
|
||||
Ok(handle) => Ok(Arc::new(SendHandle::new(handle))),
|
||||
Err(err) => {
|
||||
error!("error when sending a message: {err}");
|
||||
Err(anyhow::anyhow!(err).into())
|
||||
@@ -289,6 +290,8 @@ impl Timeline {
|
||||
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)?;
|
||||
@@ -297,7 +300,7 @@ impl Timeline {
|
||||
let attachment_config = build_thumbnail_info(thumbnail_url, image_info.thumbnail_info)?
|
||||
.info(attachment_info)
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption.map(Into::into));
|
||||
.formatted_caption(formatted_caption);
|
||||
|
||||
self.send_attachment(
|
||||
url,
|
||||
@@ -321,6 +324,8 @@ impl Timeline {
|
||||
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)?;
|
||||
@@ -351,6 +356,8 @@ impl Timeline {
|
||||
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)?;
|
||||
@@ -383,6 +390,8 @@ impl Timeline {
|
||||
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)?;
|
||||
@@ -409,15 +418,22 @@ impl Timeline {
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
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);
|
||||
let attachment_config = AttachmentConfig::new()
|
||||
.info(attachment_info)
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption.map(Into::into));
|
||||
|
||||
self.send_attachment(
|
||||
url,
|
||||
@@ -710,11 +726,18 @@ impl Timeline {
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle to perform actions onto a local echo.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SendHandle {
|
||||
inner: Mutex<Option<matrix_sdk::send_queue::SendHandle>>,
|
||||
}
|
||||
|
||||
impl SendHandle {
|
||||
fn new(handle: matrix_sdk::send_queue::SendHandle) -> Self {
|
||||
Self { inner: Mutex::new(Some(handle)) }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SendHandle {
|
||||
/// Try to abort the sending of the current event.
|
||||
@@ -732,10 +755,32 @@ impl SendHandle {
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!("error when saving in store: {err}"))?)
|
||||
} else {
|
||||
warn!("trying to abort an send handle that's already been actioned");
|
||||
warn!("trying to abort a send handle that's already been actioned");
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to manually resend messages that failed to send due to issues
|
||||
/// that should now have been fixed.
|
||||
///
|
||||
/// This is useful for example, when there's a
|
||||
/// `SessionRecipientCollectionError::VerifiedUserChangedIdentity` error;
|
||||
/// the user may have re-verified on a different device and would now
|
||||
/// like to send the failed message that's waiting on this device.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `transaction_id` - The send queue transaction identifier of the local
|
||||
/// echo that should be unwedged.
|
||||
pub async fn try_resend(self: Arc<Self>) -> Result<(), ClientError> {
|
||||
let locked = self.inner.lock().await;
|
||||
if let Some(handle) = locked.as_ref() {
|
||||
handle.unwedge().await?;
|
||||
} else {
|
||||
warn!("trying to unwedge a send handle that's been aborted");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
@@ -1273,4 +1318,10 @@ impl LazyTimelineItemProvider {
|
||||
latest_edit_json: self.0.latest_edit_json().map(|raw| raw.json().get().to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
/// For local echoes, return the associated send handle; returns `None` for
|
||||
/// remote echoes.
|
||||
fn get_send_handle(&self) -> Option<Arc<SendHandle>> {
|
||||
self.0.local_echo_send_handle().map(|handle| Arc::new(SendHandle::new(handle)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,24 +2,80 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
# unreleased
|
||||
## [0.8.0] - 2024-11-19
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Add more invalid characters for room aliases.
|
||||
|
||||
- Use the `DisplayName` struct to protect against homoglyph attacks.
|
||||
|
||||
|
||||
### Features
|
||||
- Add `BaseClient::room_key_recipient_strategy` field
|
||||
- Replace the `Notification` type from Ruma in `SyncResponse` and `StateChanges` by a custom one
|
||||
- The ambiguity maps in `SyncResponse` are moved to `JoinedRoom` and `LeftRoom`
|
||||
- `AmbiguityCache` contains the room member's user ID
|
||||
|
||||
- `AmbiguityCache` contains the room member's user ID.
|
||||
|
||||
- [**breaking**] `Media::get_thumbnail` and `MediaFormat::Thumbnail` allow to
|
||||
request an animated thumbnail They both take a `MediaThumbnailSettings`
|
||||
instead of `MediaThumbnailSize`.
|
||||
|
||||
- Consider knocked members to be part of the room for display name
|
||||
disambiguation.
|
||||
|
||||
- `Client::cross_process_store_locks_holder_name` is used everywhere:
|
||||
- `StoreConfig::new()` now takes a
|
||||
`cross_process_store_locks_holder_name` argument.
|
||||
- `StoreConfig` no longer implements `Default`.
|
||||
- `BaseClient::new()` has been removed.
|
||||
- `BaseClient::clone_with_in_memory_state_store()` now takes a
|
||||
`cross_process_store_locks_holder_name` argument.
|
||||
- `BaseClient` no longer implements `Default`.
|
||||
- `EventCacheStoreLock::new()` no longer takes a `key` argument.
|
||||
- `BuilderStoreConfig` no longer has
|
||||
`cross_process_store_locks_holder_name` field for `Sqlite` and
|
||||
`IndexedDb`.
|
||||
|
||||
- Make `ObservableMap::stream` works on `wasm32-unknown-unknown`.
|
||||
|
||||
- Allow aborting media uploads.
|
||||
|
||||
- Replace the `Notification` type from Ruma in `SyncResponse` and `StateChanges`
|
||||
by a custom one.
|
||||
|
||||
- Introduce a `DisplayName` struct which normalizes and sanitizes
|
||||
display names.
|
||||
|
||||
|
||||
### Refactor
|
||||
|
||||
- [**breaking**] Rename `DisplayName` to `RoomDisplayName`.
|
||||
|
||||
- Rename `AmbiguityMap` to `DisplayNameUsers`.
|
||||
|
||||
- Move `event_cache_store/` to `event_cache/store/` in `matrix-sdk-base`.
|
||||
|
||||
- Move `linked_chunk` from `matrix-sdk` to `matrix-sdk-common`.
|
||||
|
||||
- Move `Event` and `Gap` into `matrix_sdk_base::event_cache`.
|
||||
|
||||
- The ambiguity maps in `SyncResponse` are moved to `JoinedRoom` and `LeftRoom`.
|
||||
|
||||
- `Store::get_rooms` and `Store::get_rooms_filtered` are way faster because they
|
||||
don't acquire the lock for every room they read.
|
||||
|
||||
- `Store::get_rooms`, `Store::get_rooms_filtered` and `Store::get_room` are
|
||||
renamed `Store::rooms`, `Store::rooms_filtered` and `Store::room`.
|
||||
- `Client::get_rooms` and `Client::get_rooms_filtered` are renamed
|
||||
|
||||
- [**breaking**] `Client::get_rooms` and `Client::get_rooms_filtered` are renamed
|
||||
`Client::rooms` and `Client::rooms_filtered`.
|
||||
- `Client::get_stripped_rooms` has finally been removed.
|
||||
- `Media::get_thumbnail` and `MediaFormat::Thumbnail` allow to request an animated thumbnail
|
||||
- They both take a `MediaThumbnailSettings` instead of `MediaThumbnailSize`.
|
||||
- The `StateStore` methods to access data in the media cache where moved to a separate
|
||||
`EventCacheStore` trait.
|
||||
- The `instant` module was removed, use the `ruma::time` module instead.
|
||||
|
||||
- [**breaking**] `Client::get_stripped_rooms` has finally been removed.
|
||||
|
||||
- [**breaking**] The `StateStore` methods to access data in the media cache
|
||||
where moved to a separate `EventCacheStore` trait.
|
||||
|
||||
- [**breaking**] The `instant` module was removed, use the `ruma::time` module instead.
|
||||
|
||||
# 0.7.0
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ name = "matrix-sdk-base"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = { workspace = true }
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
@@ -50,6 +50,7 @@ assert_matches = { workspace = true, optional = true }
|
||||
assert_matches2 = { workspace = true, optional = true }
|
||||
async-trait = { workspace = true }
|
||||
bitflags = { version = "2.4.0", features = ["serde"] }
|
||||
decancer = "3.2.4"
|
||||
eyeball = { workspace = true }
|
||||
eyeball-im = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
@@ -60,14 +61,15 @@ matrix-sdk-crypto = { workspace = true, optional = true }
|
||||
matrix-sdk-store-encryption = { workspace = true }
|
||||
matrix-sdk-test = { workspace = true, optional = true }
|
||||
once_cell = { workspace = true }
|
||||
regex = "1.11.0"
|
||||
ruma = { workspace = true, features = ["canonical-json", "unstable-msc3381", "unstable-msc2867", "rand"] }
|
||||
unicode-normalization = "0.1.24"
|
||||
serde = { workspace = true, features = ["rc"] }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
uniffi = { workspace = true, optional = true }
|
||||
regex = "1.11.1"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = { workspace = true }
|
||||
@@ -77,6 +79,7 @@ futures-executor = { workspace = true }
|
||||
http = { workspace = true }
|
||||
matrix-sdk-test = { workspace = true }
|
||||
stream_assert = { workspace = true }
|
||||
similar-asserts = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
@@ -16,15 +16,13 @@
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
fmt, iter,
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use eyeball::{SharedObservable, Subscriber};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use eyeball_im::{Vector, VectorDiff};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use futures_util::Stream;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_crypto::{
|
||||
@@ -70,9 +68,9 @@ use crate::latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLat
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use crate::RoomMemberships;
|
||||
use crate::{
|
||||
deserialized_responses::{RawAnySyncOrStrippedTimelineEvent, SyncTimelineEvent},
|
||||
deserialized_responses::{DisplayName, RawAnySyncOrStrippedTimelineEvent, SyncTimelineEvent},
|
||||
error::{Error, Result},
|
||||
event_cache_store::EventCacheStoreLock,
|
||||
event_cache::store::EventCacheStoreLock,
|
||||
response_processors::AccountDataProcessor,
|
||||
rooms::{
|
||||
normal::{RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons},
|
||||
@@ -139,11 +137,6 @@ impl fmt::Debug for BaseClient {
|
||||
}
|
||||
|
||||
impl BaseClient {
|
||||
/// Create a new default client.
|
||||
pub fn new() -> Self {
|
||||
BaseClient::with_store_config(StoreConfig::default())
|
||||
}
|
||||
|
||||
/// Create a new client.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -173,8 +166,12 @@ impl BaseClient {
|
||||
/// Clones the current base client to use the same crypto store but a
|
||||
/// different, in-memory store config, and resets transient state.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub async fn clone_with_in_memory_state_store(&self) -> Result<Self> {
|
||||
let config = StoreConfig::new().state_store(MemoryStore::new());
|
||||
pub async fn clone_with_in_memory_state_store(
|
||||
&self,
|
||||
cross_process_store_locks_holder_name: &str,
|
||||
) -> Result<Self> {
|
||||
let config = StoreConfig::new(cross_process_store_locks_holder_name.to_owned())
|
||||
.state_store(MemoryStore::new());
|
||||
let config = config.crypto_store(self.crypto_store.clone());
|
||||
|
||||
let copy = Self {
|
||||
@@ -207,8 +204,12 @@ impl BaseClient {
|
||||
/// different, in-memory store config, and resets transient state.
|
||||
#[cfg(not(feature = "e2e-encryption"))]
|
||||
#[allow(clippy::unused_async)]
|
||||
pub async fn clone_with_in_memory_state_store(&self) -> Result<Self> {
|
||||
let config = StoreConfig::new().state_store(MemoryStore::new());
|
||||
pub async fn clone_with_in_memory_state_store(
|
||||
&self,
|
||||
cross_process_store_locks_holder: &str,
|
||||
) -> Result<Self> {
|
||||
let config = StoreConfig::new(cross_process_store_locks_holder.to_owned())
|
||||
.state_store(MemoryStore::new());
|
||||
Ok(Self::with_store_config(config))
|
||||
}
|
||||
|
||||
@@ -233,7 +234,6 @@ impl BaseClient {
|
||||
|
||||
/// Get a stream of all the rooms changes, in addition to the existing
|
||||
/// rooms.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn rooms_stream(&self) -> (Vector<Room>, impl Stream<Item = Vec<VectorDiff<Room>>>) {
|
||||
self.store.rooms_stream()
|
||||
}
|
||||
@@ -1332,7 +1332,7 @@ impl BaseClient {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
let mut user_ids = BTreeSet::new();
|
||||
|
||||
let mut ambiguity_map: BTreeMap<String, BTreeSet<OwnedUserId>> = BTreeMap::new();
|
||||
let mut ambiguity_map: HashMap<DisplayName, BTreeSet<OwnedUserId>> = Default::default();
|
||||
|
||||
for raw_event in &response.chunk {
|
||||
let member = match raw_event.deserialize() {
|
||||
@@ -1363,7 +1363,11 @@ impl BaseClient {
|
||||
|
||||
if let StateEvent::Original(e) = &member {
|
||||
if let Some(d) = &e.content.displayname {
|
||||
ambiguity_map.entry(d.clone()).or_default().insert(member.state_key().clone());
|
||||
let display_name = DisplayName::new(d);
|
||||
ambiguity_map
|
||||
.entry(display_name)
|
||||
.or_default()
|
||||
.insert(member.state_key().clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1689,12 +1693,6 @@ impl BaseClient {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BaseClient {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_room_member_event_for_profiles(
|
||||
room_id: &RoomId,
|
||||
event: &SyncStateEvent<RoomMemberEventContent>,
|
||||
@@ -1737,8 +1735,9 @@ mod tests {
|
||||
|
||||
use super::BaseClient;
|
||||
use crate::{
|
||||
store::StateStoreExt, test_utils::logged_in_base_client, DisplayName, RoomState,
|
||||
SessionMeta,
|
||||
store::{StateStoreExt, StoreConfig},
|
||||
test_utils::logged_in_base_client,
|
||||
RoomDisplayName, RoomState, SessionMeta,
|
||||
};
|
||||
|
||||
#[async_test]
|
||||
@@ -1869,7 +1868,7 @@ mod tests {
|
||||
assert_eq!(room.state(), RoomState::Invited);
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.expect("fetching display name failed"),
|
||||
DisplayName::Calculated("Kyra".to_owned())
|
||||
RoomDisplayName::Calculated("Kyra".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1945,7 +1944,9 @@ mod tests {
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
|
||||
|
||||
let client = BaseClient::new();
|
||||
let client = BaseClient::with_store_config(StoreConfig::new(
|
||||
"cross-process-store-locks-holder-name".to_owned(),
|
||||
));
|
||||
client
|
||||
.set_session_meta(
|
||||
SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
|
||||
@@ -2003,7 +2004,9 @@ mod tests {
|
||||
let inviter_user_id = user_id!("@bob:example.org");
|
||||
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
|
||||
|
||||
let client = BaseClient::new();
|
||||
let client = BaseClient::with_store_config(StoreConfig::new(
|
||||
"cross-process-store-locks-holder-name".to_owned(),
|
||||
));
|
||||
client
|
||||
.set_session_meta(
|
||||
SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
|
||||
@@ -2063,7 +2066,9 @@ mod tests {
|
||||
let inviter_user_id = user_id!("@bob:example.org");
|
||||
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
|
||||
|
||||
let client = BaseClient::new();
|
||||
let client = BaseClient::with_store_config(StoreConfig::new(
|
||||
"cross-process-store-locks-holder-name".to_owned(),
|
||||
));
|
||||
client
|
||||
.set_session_meta(
|
||||
SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
|
||||
|
||||
@@ -14,9 +14,11 @@
|
||||
|
||||
//! SDK-specific variations of response types from Ruma.
|
||||
|
||||
use std::{collections::BTreeMap, fmt, iter};
|
||||
use std::{collections::BTreeMap, fmt, hash::Hash, iter};
|
||||
|
||||
pub use matrix_sdk_common::deserialized_responses::*;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use ruma::{
|
||||
events::{
|
||||
room::{
|
||||
@@ -31,6 +33,7 @@ use ruma::{
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UserId,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use unicode_normalization::UnicodeNormalization;
|
||||
|
||||
/// A change in ambiguity of room members that an `m.room.member` event
|
||||
/// triggers.
|
||||
@@ -67,6 +70,178 @@ pub struct AmbiguityChanges {
|
||||
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
|
||||
}
|
||||
|
||||
static MXID_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(DisplayName::MXID_PATTERN)
|
||||
.expect("We should be able to create a regex from our static MXID pattern")
|
||||
});
|
||||
static LEFT_TO_RIGHT_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(DisplayName::LEFT_TO_RIGHT_PATTERN)
|
||||
.expect("We should be able to create a regex from our static left-to-right pattern")
|
||||
});
|
||||
static HIDDEN_CHARACTERS_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(DisplayName::HIDDEN_CHARACTERS_PATTERN)
|
||||
.expect("We should be able to create a regex from our static hidden characters pattern")
|
||||
});
|
||||
|
||||
/// Regex to match `i` characters.
|
||||
///
|
||||
/// This is used to replace an `i` with a lowercase `l`, i.e. to mark "Hello"
|
||||
/// and "HeIlo" as ambiguous. Decancer will lowercase an `I` for us.
|
||||
static I_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new("[i]").expect("We should be able to create a regex from our uppercase I pattern")
|
||||
});
|
||||
|
||||
/// Regex to match `0` characters.
|
||||
///
|
||||
/// This is used to replace an `0` with a lowercase `o`, i.e. to mark "HellO"
|
||||
/// and "Hell0" as ambiguous. Decancer will lowercase an `O` for us.
|
||||
static ZERO_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new("[0]").expect("We should be able to create a regex from our zero pattern")
|
||||
});
|
||||
|
||||
/// Regex to match a couple of dot-like characters, also matches an actual dot.
|
||||
///
|
||||
/// This is used to replace a `.` with a `:`, i.e. to mark "@mxid.domain.tld" as
|
||||
/// ambiguous.
|
||||
static DOT_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new("[.\u{1d16d}]").expect("We should be able to create a regex from our dot pattern")
|
||||
});
|
||||
|
||||
/// A high-level wrapper for strings representing display names.
|
||||
///
|
||||
/// This wrapper provides attempts to determine whether a display name
|
||||
/// contains characters that could make it ambiguous or easily confused
|
||||
/// with similar names.
|
||||
///
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use matrix_sdk_base::deserialized_responses::DisplayName;
|
||||
///
|
||||
/// let display_name = DisplayName::new("𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶");
|
||||
///
|
||||
/// // The normalized and sanitized string will be returned by DisplayName.as_normalized_str().
|
||||
/// assert_eq!(display_name.as_normalized_str(), Some("sahasrahla"));
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
/// # use matrix_sdk_base::deserialized_responses::DisplayName;
|
||||
/// let display_name = DisplayName::new("@alice:localhost");
|
||||
///
|
||||
/// // The display name looks like an MXID, which makes it ambiguous.
|
||||
/// assert!(display_name.is_inherently_ambiguous());
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Eq)]
|
||||
pub struct DisplayName {
|
||||
raw: String,
|
||||
decancered: Option<String>,
|
||||
}
|
||||
|
||||
impl Hash for DisplayName {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
if let Some(decancered) = &self.decancered {
|
||||
decancered.hash(state);
|
||||
} else {
|
||||
self.raw.hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for DisplayName {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self.decancered.as_deref(), other.decancered.as_deref()) {
|
||||
(None, None) => self.raw == other.raw,
|
||||
(None, Some(_)) | (Some(_), None) => false,
|
||||
(Some(this), Some(other)) => this == other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DisplayName {
|
||||
/// Regex pattern matching an MXID.
|
||||
const MXID_PATTERN: &str = "@.+[:.].+";
|
||||
|
||||
/// Regex pattern matching some left-to-right formatting marks:
|
||||
/// * LTR and RTL marks U+200E and U+200F
|
||||
/// * LTR/RTL and other directional formatting marks U+202A - U+202F
|
||||
const LEFT_TO_RIGHT_PATTERN: &str = "[\u{202a}-\u{202f}\u{200e}\u{200f}]";
|
||||
|
||||
/// Regex pattern matching bunch of unicode control characters and otherwise
|
||||
/// misleading/invisible characters.
|
||||
///
|
||||
/// This includes:
|
||||
/// * various width spaces U+2000 - U+200D
|
||||
/// * Combining characters U+0300 - U+036F
|
||||
/// * Blank/invisible characters (U2800, U2062-U2063)
|
||||
/// * Arabic Letter RTL mark U+061C
|
||||
/// * Zero width no-break space (BOM) U+FEFF
|
||||
const HIDDEN_CHARACTERS_PATTERN: &str =
|
||||
"[\u{2000}-\u{200D}\u{300}-\u{036f}\u{2062}-\u{2063}\u{2800}\u{061c}\u{feff}]";
|
||||
|
||||
/// Creates a new [`DisplayName`] from the given raw string.
|
||||
///
|
||||
/// The raw display name is transformed into a Unicode-normalized form, with
|
||||
/// common confusable characters removed to reduce ambiguity.
|
||||
///
|
||||
/// **Note**: If removing confusable characters fails,
|
||||
/// [`DisplayName::is_inherently_ambiguous`] will return `true`, and
|
||||
/// [`DisplayName::as_normalized_str()`] will return `None.
|
||||
pub fn new(raw: &str) -> Self {
|
||||
let normalized = raw.nfd().collect::<String>();
|
||||
let replaced = DOT_REGEX.replace_all(&normalized, ":");
|
||||
let replaced = HIDDEN_CHARACTERS_REGEX.replace_all(&replaced, "");
|
||||
|
||||
let decancered = decancer::cure!(&replaced).ok().map(|cured| {
|
||||
let removed_left_to_right = LEFT_TO_RIGHT_REGEX.replace_all(cured.as_ref(), "");
|
||||
let replaced = I_REGEX.replace_all(&removed_left_to_right, "l");
|
||||
// We re-run the dot replacement because decancer normalized a lot of weird
|
||||
// characets into a `.`, it just doesn't do that for /u{1d16d}.
|
||||
let replaced = DOT_REGEX.replace_all(&replaced, ":");
|
||||
let replaced = ZERO_REGEX.replace_all(&replaced, "o");
|
||||
|
||||
replaced.to_string()
|
||||
});
|
||||
|
||||
Self { raw: raw.to_owned(), decancered }
|
||||
}
|
||||
|
||||
/// Is this display name considered to be ambiguous?
|
||||
///
|
||||
/// If the display name has cancer (i.e. fails normalisation or has a
|
||||
/// different normalised form) or looks like an MXID, then it's ambiguous.
|
||||
pub fn is_inherently_ambiguous(&self) -> bool {
|
||||
// If we look like an MXID or have hidden characters then we're ambiguous.
|
||||
self.looks_like_an_mxid() || self.has_hidden_characters() || self.decancered.is_none()
|
||||
}
|
||||
|
||||
/// Returns the underlying raw and and unsanitized string of this
|
||||
/// [`DisplayName`].
|
||||
pub fn as_raw_str(&self) -> &str {
|
||||
&self.raw
|
||||
}
|
||||
|
||||
/// Returns the underlying normalized and and sanitized string of this
|
||||
/// [`DisplayName`].
|
||||
///
|
||||
/// Returns `None` if normalization failed during construction of this
|
||||
/// [`DisplayName`].
|
||||
pub fn as_normalized_str(&self) -> Option<&str> {
|
||||
self.decancered.as_deref()
|
||||
}
|
||||
|
||||
fn has_hidden_characters(&self) -> bool {
|
||||
HIDDEN_CHARACTERS_REGEX.is_match(&self.raw)
|
||||
}
|
||||
|
||||
fn looks_like_an_mxid(&self) -> bool {
|
||||
self.decancered
|
||||
.as_deref()
|
||||
.map(|d| MXID_REGEX.is_match(d))
|
||||
.unwrap_or_else(|| MXID_REGEX.is_match(&self.raw))
|
||||
}
|
||||
}
|
||||
|
||||
/// A deserialized response for the rooms members API call.
|
||||
///
|
||||
/// [`GET /_matrix/client/r0/rooms/{roomId}/members`](https://spec.matrix.org/v1.5/client-server-api/#get_matrixclientv3roomsroomidmembers)
|
||||
@@ -294,10 +469,12 @@ impl MemberEvent {
|
||||
///
|
||||
/// It there is no `displayname` in the event's content, the localpart or
|
||||
/// the user ID is returned.
|
||||
pub fn display_name(&self) -> &str {
|
||||
self.original_content()
|
||||
.and_then(|c| c.displayname.as_deref())
|
||||
.unwrap_or_else(|| self.user_id().localpart())
|
||||
pub fn display_name(&self) -> DisplayName {
|
||||
DisplayName::new(
|
||||
self.original_content()
|
||||
.and_then(|c| c.displayname.as_deref())
|
||||
.unwrap_or_else(|| self.user_id().localpart()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,3 +487,240 @@ impl SyncOrStrippedState<RoomPowerLevelsEventContent> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
macro_rules! assert_display_name_eq {
|
||||
($left:expr, $right:expr $(, $desc:expr)?) => {{
|
||||
let left = crate::deserialized_responses::DisplayName::new($left);
|
||||
let right = crate::deserialized_responses::DisplayName::new($right);
|
||||
|
||||
similar_asserts::assert_eq!(
|
||||
left,
|
||||
right
|
||||
$(, $desc)?
|
||||
);
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! assert_display_name_ne {
|
||||
($left:expr, $right:expr $(, $desc:expr)?) => {{
|
||||
let left = crate::deserialized_responses::DisplayName::new($left);
|
||||
let right = crate::deserialized_responses::DisplayName::new($right);
|
||||
|
||||
assert_ne!(
|
||||
left,
|
||||
right
|
||||
$(, $desc)?
|
||||
);
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! assert_ambiguous {
|
||||
($name:expr) => {
|
||||
let name = crate::deserialized_responses::DisplayName::new($name);
|
||||
|
||||
assert!(
|
||||
name.is_inherently_ambiguous(),
|
||||
"The display {:?} should be considered amgibuous",
|
||||
name
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! assert_not_ambiguous {
|
||||
($name:expr) => {
|
||||
let name = crate::deserialized_responses::DisplayName::new($name);
|
||||
|
||||
assert!(
|
||||
!name.is_inherently_ambiguous(),
|
||||
"The display {:?} should not be considered amgibuous",
|
||||
name
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_inherently_ambiguous() {
|
||||
// These should not be inherently ambiguous, only if another similarly looking
|
||||
// display name appears should they be considered to be ambiguous.
|
||||
assert_not_ambiguous!("Alice");
|
||||
assert_not_ambiguous!("Carol");
|
||||
assert_not_ambiguous!("Car0l");
|
||||
assert_not_ambiguous!("Ivan");
|
||||
assert_not_ambiguous!("𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶");
|
||||
assert_not_ambiguous!("Ⓢⓐⓗⓐⓢⓡⓐⓗⓛⓐ");
|
||||
assert_not_ambiguous!("🅂🄰🄷🄰🅂🅁🄰🄷🄻🄰");
|
||||
assert_not_ambiguous!("Sahasrahla");
|
||||
// Left to right is fine, if it's the only one in the room.
|
||||
assert_not_ambiguous!("\u{202e}alharsahas");
|
||||
|
||||
// These on the other hand contain invisible chars.
|
||||
assert_ambiguous!("Sa̴hasrahla");
|
||||
assert_ambiguous!("Sahas\u{200D}rahla");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_capitalization() {
|
||||
// Display name with different capitalization
|
||||
assert_display_name_eq!("Alice", "alice");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_different_names() {
|
||||
// Different display names
|
||||
assert_display_name_ne!("Alice", "Carol");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_capital_l() {
|
||||
// Different display names
|
||||
assert_display_name_eq!("Hello", "HeIlo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_confusable_zero() {
|
||||
// Different display names
|
||||
assert_display_name_eq!("Carol", "Car0l");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_cyrilic() {
|
||||
// Display name with scritpure symbols
|
||||
assert_display_name_eq!("alice", "аlice");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_scriptures() {
|
||||
// Display name with scritpure symbols
|
||||
assert_display_name_eq!("Sahasrahla", "𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_frakturs() {
|
||||
// Display name with fraktur symbols
|
||||
assert_display_name_eq!("Sahasrahla", "𝔖𝔞𝔥𝔞𝔰𝔯𝔞𝔥𝔩𝔞");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_circled() {
|
||||
// Display name with circled symbols
|
||||
assert_display_name_eq!("Sahasrahla", "Ⓢⓐⓗⓐⓢⓡⓐⓗⓛⓐ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_squared() {
|
||||
// Display name with squared symbols
|
||||
assert_display_name_eq!("Sahasrahla", "🅂🄰🄷🄰🅂🅁🄰🄷🄻🄰");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_big_unicode() {
|
||||
// Display name with big unicode letters
|
||||
assert_display_name_eq!("Sahasrahla", "Sahasrahla");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_left_to_right() {
|
||||
// Display name with a left-to-right character
|
||||
assert_display_name_eq!("Sahasrahla", "\u{202e}alharsahas");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_diacritical() {
|
||||
// Display name with a diacritical mark.
|
||||
assert_display_name_eq!("Sahasrahla", "Sa̴hasrahla");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_zero_width_joiner() {
|
||||
// Display name with a zero-width joiner
|
||||
assert_display_name_eq!("Sahasrahla", "Sahas\u{200B}rahla");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_zero_width_space() {
|
||||
// Display name with zero-width space.
|
||||
assert_display_name_eq!("Sahasrahla", "Sahas\u{200D}rahla");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_ligatures() {
|
||||
// Display name with a ligature.
|
||||
assert_display_name_eq!("ff", "\u{FB00}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_confusable_mxid_colon() {
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0589}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{05c3}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0703}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0a83}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{16ec}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{205a}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{2236}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe13}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe52}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe30}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{ff1a}domain.tld");
|
||||
|
||||
// Additionally these should be considered to be ambiguous on their own.
|
||||
assert_ambiguous!("@mxid\u{0589}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{05c3}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{0703}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{0a83}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{16ec}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{205a}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{2236}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{fe13}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{fe52}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{fe30}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{ff1a}domain.tld");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_confusable_mxid_dot() {
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{0701}tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{0702}tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{2024}tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{fe52}tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{ff0e}tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{1d16d}tld");
|
||||
|
||||
// Additionally these should be considered to be ambiguous on their own.
|
||||
assert_ambiguous!("@mxid:domain\u{0701}tld");
|
||||
assert_ambiguous!("@mxid:domain\u{0702}tld");
|
||||
assert_ambiguous!("@mxid:domain\u{2024}tld");
|
||||
assert_ambiguous!("@mxid:domain\u{fe52}tld");
|
||||
assert_ambiguous!("@mxid:domain\u{ff0e}tld");
|
||||
assert_ambiguous!("@mxid:domain\u{1d16d}tld");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_confusable_mxid_replacing_a() {
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:dom\u{1d44e}in.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:dom\u{0430}in.tld");
|
||||
|
||||
// Additionally these should be considered to be ambiguous on their own.
|
||||
assert_ambiguous!("@mxid:dom\u{1d44e}in.tld");
|
||||
assert_ambiguous!("@mxid:dom\u{0430}in.tld");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_confusable_mxid_replacing_l() {
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain.tId");
|
||||
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{217c}d");
|
||||
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{ff4c}d");
|
||||
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{1d5f9}d");
|
||||
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{1d695}d");
|
||||
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{2223}d");
|
||||
|
||||
// Additionally these should be considered to be ambiguous on their own.
|
||||
assert_ambiguous!("@mxid:domain.tId");
|
||||
assert_ambiguous!("@mxid:domain.t\u{217c}d");
|
||||
assert_ambiguous!("@mxid:domain.t\u{ff4c}d");
|
||||
assert_ambiguous!("@mxid:domain.t\u{1d5f9}d");
|
||||
assert_ambiguous!("@mxid:domain.t\u{1d695}d");
|
||||
assert_ambiguous!("@mxid:domain.t\u{2223}d");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// 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.
|
||||
|
||||
//! Event cache store and common types shared with `matrix_sdk::event_cache`.
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
|
||||
pub mod store;
|
||||
|
||||
/// The kind of event the event storage holds.
|
||||
pub type Event = SyncTimelineEvent;
|
||||
|
||||
/// The kind of gap the event storage holds.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Gap {
|
||||
/// The token to use in the query, extracted from a previous "from" /
|
||||
/// "end" field of a `/messages` response.
|
||||
pub prev_token: String,
|
||||
}
|
||||
+5
-3
@@ -193,7 +193,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
///
|
||||
/// ## Usage Example:
|
||||
/// ```no_run
|
||||
/// # use matrix_sdk_base::event_cache_store::{
|
||||
/// # use matrix_sdk_base::event_cache::store::{
|
||||
/// # EventCacheStore,
|
||||
/// # MemoryStore as MyStore,
|
||||
/// # Result as EventCacheStoreResult,
|
||||
@@ -217,7 +217,9 @@ macro_rules! event_cache_store_integration_tests {
|
||||
() => {
|
||||
mod event_cache_store_integration_tests {
|
||||
use matrix_sdk_test::async_test;
|
||||
use $crate::event_cache_store::{EventCacheStoreIntegrationTests, IntoEventCacheStore};
|
||||
use $crate::event_cache::store::{
|
||||
EventCacheStoreIntegrationTests, IntoEventCacheStore,
|
||||
};
|
||||
|
||||
use super::get_event_cache_store;
|
||||
|
||||
@@ -249,7 +251,7 @@ macro_rules! event_cache_store_integration_tests_time {
|
||||
use std::time::Duration;
|
||||
|
||||
use matrix_sdk_test::async_test;
|
||||
use $crate::event_cache_store::IntoEventCacheStore;
|
||||
use $crate::event_cache::store::IntoEventCacheStore;
|
||||
|
||||
use super::get_event_cache_store;
|
||||
|
||||
+5
-2
@@ -60,7 +60,10 @@ impl fmt::Debug for EventCacheStoreLock {
|
||||
|
||||
impl EventCacheStoreLock {
|
||||
/// Create a new lock around the [`EventCacheStore`].
|
||||
pub fn new<S>(store: S, key: String, holder: String) -> Self
|
||||
///
|
||||
/// The `holder` argument represents the holder inside the
|
||||
/// [`CrossProcessStoreLock::new`].
|
||||
pub fn new<S>(store: S, holder: String) -> Self
|
||||
where
|
||||
S: IntoEventCacheStore,
|
||||
{
|
||||
@@ -69,7 +72,7 @@ impl EventCacheStoreLock {
|
||||
Self {
|
||||
cross_process_lock: CrossProcessStoreLock::new(
|
||||
LockableEventCacheStore(store.clone()),
|
||||
key,
|
||||
"default".to_owned(),
|
||||
holder,
|
||||
),
|
||||
store,
|
||||
+3
@@ -98,6 +98,9 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
/// Remove all the media files' content associated to an `MxcUri` from the
|
||||
/// media store.
|
||||
///
|
||||
/// This should not raise an error when the `uri` parameter points to an
|
||||
/// unknown media, and it should return an Ok result in this case.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `uri` - The `MxcUri` of the media files.
|
||||
@@ -28,7 +28,7 @@ mod client;
|
||||
pub mod debug;
|
||||
pub mod deserialized_responses;
|
||||
mod error;
|
||||
pub mod event_cache_store;
|
||||
pub mod event_cache;
|
||||
pub mod latest_event;
|
||||
pub mod media;
|
||||
pub mod notification_settings;
|
||||
@@ -56,7 +56,7 @@ pub use http;
|
||||
pub use matrix_sdk_crypto as crypto;
|
||||
pub use once_cell;
|
||||
pub use rooms::{
|
||||
DisplayName, Room, RoomCreateWithCreatorEventContent, RoomHero, RoomInfo,
|
||||
Room, RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo,
|
||||
RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember, RoomMemberships, RoomState,
|
||||
RoomStateFilter,
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
collections::{BTreeSet, HashMap},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
@@ -30,7 +30,8 @@ use ruma::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
deserialized_responses::{MemberEvent, SyncOrStrippedState},
|
||||
deserialized_responses::{DisplayName, MemberEvent, SyncOrStrippedState},
|
||||
store::ambiguity_map::is_display_name_ambiguous,
|
||||
MinimalRoomMemberEvent,
|
||||
};
|
||||
|
||||
@@ -67,8 +68,10 @@ impl RoomMember {
|
||||
} = room_info;
|
||||
|
||||
let is_room_creator = room_creator.as_deref() == Some(event.user_id());
|
||||
let display_name_ambiguous =
|
||||
users_display_names.get(event.display_name()).is_some_and(|s| s.len() > 1);
|
||||
let display_name = event.display_name();
|
||||
let display_name_ambiguous = users_display_names
|
||||
.get(&display_name)
|
||||
.is_some_and(|s| is_display_name_ambiguous(&display_name, s));
|
||||
let is_ignored = ignored_users.as_ref().is_some_and(|s| s.contains(event.user_id()));
|
||||
|
||||
Self {
|
||||
@@ -245,6 +248,6 @@ pub(crate) struct MemberRoomInfo<'a> {
|
||||
pub(crate) power_levels: Arc<Option<SyncOrStrippedState<RoomPowerLevelsEventContent>>>,
|
||||
pub(crate) max_power_level: i64,
|
||||
pub(crate) room_creator: Option<OwnedUserId>,
|
||||
pub(crate) users_display_names: BTreeMap<&'a str, BTreeSet<OwnedUserId>>,
|
||||
pub(crate) users_display_names: HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>,
|
||||
pub(crate) ignored_users: Option<BTreeSet<OwnedUserId>>,
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ use crate::MinimalStateEvent;
|
||||
/// The name of the room, either from the metadata or calculated
|
||||
/// according to [matrix specification](https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room)
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub enum DisplayName {
|
||||
pub enum RoomDisplayName {
|
||||
/// The room has been named explicitly as
|
||||
Named(String),
|
||||
/// The room has a canonical alias that should be used
|
||||
@@ -66,9 +66,9 @@ pub enum DisplayName {
|
||||
}
|
||||
|
||||
const WHITESPACE_REGEX: &str = r"\s+";
|
||||
const INVALID_SYMBOLS_REGEX: &str = r"[#,:]+";
|
||||
const INVALID_SYMBOLS_REGEX: &str = r"[#,:\{\}\\]+";
|
||||
|
||||
impl DisplayName {
|
||||
impl RoomDisplayName {
|
||||
/// Transforms the current display name into the name part of a
|
||||
/// `RoomAliasId`.
|
||||
pub fn to_room_alias_name(&self) -> String {
|
||||
@@ -97,14 +97,16 @@ impl DisplayName {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DisplayName {
|
||||
impl fmt::Display for RoomDisplayName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
DisplayName::Named(s) | DisplayName::Calculated(s) | DisplayName::Aliased(s) => {
|
||||
RoomDisplayName::Named(s)
|
||||
| RoomDisplayName::Calculated(s)
|
||||
| RoomDisplayName::Aliased(s) => {
|
||||
write!(f, "{s}")
|
||||
}
|
||||
DisplayName::EmptyWas(s) => write!(f, "Empty Room (was {s})"),
|
||||
DisplayName::Empty => write!(f, "Empty Room"),
|
||||
RoomDisplayName::EmptyWas(s) => write!(f, "Empty Room (was {s})"),
|
||||
RoomDisplayName::Empty => write!(f, "Empty Room"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -574,7 +576,7 @@ mod tests {
|
||||
use ruma::events::tag::{TagInfo, TagName, Tags};
|
||||
|
||||
use super::{BaseRoomInfo, RoomNotableTags};
|
||||
use crate::DisplayName;
|
||||
use crate::RoomDisplayName;
|
||||
|
||||
#[test]
|
||||
fn test_handle_notable_tags_favourite() {
|
||||
@@ -608,21 +610,33 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_room_alias_from_room_display_name_lowercases() {
|
||||
assert_eq!("roomalias", DisplayName::Named("RoomAlias".to_owned()).to_room_alias_name());
|
||||
assert_eq!(
|
||||
"roomalias",
|
||||
RoomDisplayName::Named("RoomAlias".to_owned()).to_room_alias_name()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_room_alias_from_room_display_name_removes_whitespace() {
|
||||
assert_eq!("room-alias", DisplayName::Named("Room Alias".to_owned()).to_room_alias_name());
|
||||
assert_eq!(
|
||||
"room-alias",
|
||||
RoomDisplayName::Named("Room Alias".to_owned()).to_room_alias_name()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_room_alias_from_room_display_name_removes_non_ascii_symbols() {
|
||||
assert_eq!("roomalias", DisplayName::Named("Room±Alias√".to_owned()).to_room_alias_name());
|
||||
assert_eq!(
|
||||
"roomalias",
|
||||
RoomDisplayName::Named("Room±Alias√".to_owned()).to_room_alias_name()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_room_alias_from_room_display_name_removes_invalid_ascii_symbols() {
|
||||
assert_eq!("roomalias", DisplayName::Named("#Room,Alias:".to_owned()).to_room_alias_name());
|
||||
assert_eq!(
|
||||
"roomalias",
|
||||
RoomDisplayName::Named("#Room,{Alias}:".to_owned()).to_room_alias_name()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,13 +61,13 @@ use tokio::sync::broadcast;
|
||||
use tracing::{debug, field::debug, info, instrument, warn};
|
||||
|
||||
use super::{
|
||||
members::MemberRoomInfo, BaseRoomInfo, DisplayName, RoomCreateWithCreatorEventContent,
|
||||
members::MemberRoomInfo, BaseRoomInfo, RoomCreateWithCreatorEventContent, RoomDisplayName,
|
||||
RoomMember, RoomNotableTags,
|
||||
};
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
use crate::latest_event::LatestEvent;
|
||||
use crate::{
|
||||
deserialized_responses::{MemberEvent, RawSyncOrStrippedState},
|
||||
deserialized_responses::{DisplayName, MemberEvent, RawSyncOrStrippedState},
|
||||
notification_settings::RoomNotificationMode,
|
||||
read_receipts::RoomReadReceipts,
|
||||
store::{DynStateStore, Result as StoreResult, StateStoreExt},
|
||||
@@ -572,8 +572,8 @@ impl Room {
|
||||
/// [`Self::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<DisplayName> {
|
||||
let update_cache = |new_val: DisplayName| {
|
||||
pub async fn compute_display_name(&self) -> StoreResult<RoomDisplayName> {
|
||||
let update_cache = |new_val: RoomDisplayName| {
|
||||
self.inner.update_if(|info| {
|
||||
if info.cached_display_name.as_ref() != Some(&new_val) {
|
||||
info.cached_display_name = Some(new_val.clone());
|
||||
@@ -591,13 +591,13 @@ impl Room {
|
||||
if let Some(name) = inner.name() {
|
||||
let name = name.trim().to_owned();
|
||||
drop(inner); // drop the lock on `self.inner` to avoid deadlocking in `update_cache`.
|
||||
return Ok(update_cache(DisplayName::Named(name)));
|
||||
return Ok(update_cache(RoomDisplayName::Named(name)));
|
||||
}
|
||||
|
||||
if let Some(alias) = inner.canonical_alias() {
|
||||
let alias = alias.alias().trim().to_owned();
|
||||
drop(inner); // See above comment.
|
||||
return Ok(update_cache(DisplayName::Aliased(alias)));
|
||||
return Ok(update_cache(RoomDisplayName::Aliased(alias)));
|
||||
}
|
||||
|
||||
inner.summary.clone()
|
||||
@@ -703,7 +703,7 @@ impl Room {
|
||||
///
|
||||
/// This cache is refilled every time we call
|
||||
/// [`Self::compute_display_name`].
|
||||
pub fn cached_display_name(&self) -> Option<DisplayName> {
|
||||
pub fn cached_display_name(&self) -> Option<RoomDisplayName> {
|
||||
self.inner.read().cached_display_name.clone()
|
||||
}
|
||||
|
||||
@@ -819,8 +819,7 @@ impl Room {
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
let display_names =
|
||||
member_events.iter().map(|e| e.display_name().to_owned()).collect::<Vec<_>>();
|
||||
let display_names = member_events.iter().map(|e| e.display_name()).collect::<Vec<_>>();
|
||||
let room_info = self.member_room_info(&display_names).await?;
|
||||
|
||||
let mut members = Vec::new();
|
||||
@@ -900,7 +899,7 @@ impl Room {
|
||||
|
||||
let profile = self.store.get_profile(self.room_id(), user_id).await?;
|
||||
|
||||
let display_names = [event.display_name().to_owned()];
|
||||
let display_names = [event.display_name()];
|
||||
let room_info = self.member_room_info(&display_names).await?;
|
||||
|
||||
Ok(Some(RoomMember::from_parts(event, profile, presence, &room_info)))
|
||||
@@ -911,7 +910,7 @@ impl Room {
|
||||
/// Async because it can read from storage.
|
||||
async fn member_room_info<'a>(
|
||||
&self,
|
||||
display_names: &'a [String],
|
||||
display_names: &'a [DisplayName],
|
||||
) -> StoreResult<MemberRoomInfo<'a>> {
|
||||
let max_power_level = self.max_power_level();
|
||||
let room_creator = self.inner.read().creator().map(ToOwned::to_owned);
|
||||
@@ -1103,7 +1102,7 @@ pub struct RoomInfo {
|
||||
/// Filled by calling [`Room::compute_display_name`]. It's automatically
|
||||
/// filled at start when creating a room, or on every successful sync.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) cached_display_name: Option<DisplayName>,
|
||||
pub(crate) cached_display_name: Option<RoomDisplayName>,
|
||||
|
||||
/// Cached user defined notification mode.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -1767,7 +1766,10 @@ impl RoomStateFilter {
|
||||
/// Calculate room name according to step 3 of the [naming algorithm].
|
||||
///
|
||||
/// [naming algorithm]: https://spec.matrix.org/latest/client-server-api/#calculating-the-display-name-for-a-room
|
||||
fn compute_display_name_from_heroes(num_joined_invited: u64, mut heroes: Vec<&str>) -> DisplayName {
|
||||
fn compute_display_name_from_heroes(
|
||||
num_joined_invited: u64,
|
||||
mut heroes: Vec<&str>,
|
||||
) -> RoomDisplayName {
|
||||
let num_heroes = heroes.len() as u64;
|
||||
let num_joined_invited_except_self = num_joined_invited.saturating_sub(1);
|
||||
|
||||
@@ -1789,12 +1791,12 @@ fn compute_display_name_from_heroes(num_joined_invited: u64, mut heroes: Vec<&st
|
||||
// User is alone.
|
||||
if num_joined_invited <= 1 {
|
||||
if names.is_empty() {
|
||||
DisplayName::Empty
|
||||
RoomDisplayName::Empty
|
||||
} else {
|
||||
DisplayName::EmptyWas(names)
|
||||
RoomDisplayName::EmptyWas(names)
|
||||
}
|
||||
} else {
|
||||
DisplayName::Calculated(names)
|
||||
RoomDisplayName::Calculated(names)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1850,8 +1852,8 @@ mod tests {
|
||||
use crate::latest_event::LatestEvent;
|
||||
use crate::{
|
||||
rooms::RoomNotableTags,
|
||||
store::{IntoStateStore, MemoryStore, StateChanges, StateStore},
|
||||
BaseClient, DisplayName, MinimalStateEvent, OriginalMinimalStateEvent,
|
||||
store::{IntoStateStore, MemoryStore, StateChanges, StateStore, StoreConfig},
|
||||
BaseClient, MinimalStateEvent, OriginalMinimalStateEvent, RoomDisplayName,
|
||||
RoomInfoNotableUpdateReasons, SessionMeta,
|
||||
};
|
||||
|
||||
@@ -2126,7 +2128,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
info.cached_display_name.as_ref(),
|
||||
Some(&DisplayName::Calculated("lol".to_owned())),
|
||||
Some(&RoomDisplayName::Calculated("lol".to_owned())),
|
||||
);
|
||||
assert_eq!(
|
||||
info.cached_user_defined_notification_mode.as_ref(),
|
||||
@@ -2138,7 +2140,9 @@ mod tests {
|
||||
#[async_test]
|
||||
async fn test_is_favourite() {
|
||||
// Given a room,
|
||||
let client = BaseClient::new();
|
||||
let client = BaseClient::with_store_config(StoreConfig::new(
|
||||
"cross-process-store-locks-holder-name".to_owned(),
|
||||
));
|
||||
|
||||
client
|
||||
.set_session_meta(
|
||||
@@ -2216,7 +2220,9 @@ mod tests {
|
||||
#[async_test]
|
||||
async fn test_is_low_priority() {
|
||||
// Given a room,
|
||||
let client = BaseClient::new();
|
||||
let client = BaseClient::with_store_config(StoreConfig::new(
|
||||
"cross-process-store-locks-holder-name".to_owned(),
|
||||
));
|
||||
|
||||
client
|
||||
.set_session_meta(
|
||||
@@ -2331,7 +2337,7 @@ mod tests {
|
||||
#[async_test]
|
||||
async fn test_display_name_for_joined_room_is_empty_if_no_info() {
|
||||
let (_, room) = make_room_test_helper(RoomState::Joined);
|
||||
assert_eq!(room.compute_display_name().await.unwrap(), DisplayName::Empty);
|
||||
assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
@@ -2341,7 +2347,7 @@ mod tests {
|
||||
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap(),
|
||||
DisplayName::Aliased("test".to_owned())
|
||||
RoomDisplayName::Aliased("test".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2352,20 +2358,20 @@ mod tests {
|
||||
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap(),
|
||||
DisplayName::Aliased("test".to_owned())
|
||||
RoomDisplayName::Aliased("test".to_owned())
|
||||
);
|
||||
room.inner.update(|info| info.base_info.name = Some(make_name_event()));
|
||||
// Display name wasn't cached when we asked for it above, and name overrides
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap(),
|
||||
DisplayName::Named("Test Room".to_owned())
|
||||
RoomDisplayName::Named("Test Room".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_display_name_for_invited_room_is_empty_if_no_info() {
|
||||
let (_, room) = make_room_test_helper(RoomState::Invited);
|
||||
assert_eq!(room.compute_display_name().await.unwrap(), DisplayName::Empty);
|
||||
assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
@@ -2378,7 +2384,7 @@ mod tests {
|
||||
});
|
||||
room.inner.update(|info| info.base_info.name = Some(room_name));
|
||||
|
||||
assert_eq!(room.compute_display_name().await.unwrap(), DisplayName::Empty);
|
||||
assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
@@ -2388,7 +2394,7 @@ mod tests {
|
||||
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap(),
|
||||
DisplayName::Aliased("test".to_owned())
|
||||
RoomDisplayName::Aliased("test".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2399,13 +2405,13 @@ mod tests {
|
||||
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap(),
|
||||
DisplayName::Aliased("test".to_owned())
|
||||
RoomDisplayName::Aliased("test".to_owned())
|
||||
);
|
||||
room.inner.update(|info| info.base_info.name = Some(make_name_event()));
|
||||
// Display name wasn't cached when we asked for it above, and name overrides
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap(),
|
||||
DisplayName::Named("Test Room".to_owned())
|
||||
RoomDisplayName::Named("Test Room".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2447,7 +2453,7 @@ mod tests {
|
||||
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap(),
|
||||
DisplayName::Calculated("Matthew".to_owned())
|
||||
RoomDisplayName::Calculated("Matthew".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2469,7 +2475,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap(),
|
||||
DisplayName::Calculated("Matthew".to_owned())
|
||||
RoomDisplayName::Calculated("Matthew".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2499,7 +2505,7 @@ mod tests {
|
||||
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap(),
|
||||
DisplayName::Calculated("Matthew".to_owned())
|
||||
RoomDisplayName::Calculated("Matthew".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2524,7 +2530,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap(),
|
||||
DisplayName::Calculated("Matthew".to_owned())
|
||||
RoomDisplayName::Calculated("Matthew".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2579,7 +2585,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap(),
|
||||
DisplayName::Calculated("Bob, Carol, Denis, Erica, and 3 others".to_owned())
|
||||
RoomDisplayName::Calculated("Bob, Carol, Denis, Erica, and 3 others".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2628,7 +2634,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap(),
|
||||
DisplayName::Calculated("Alice, Bob, Carol, Denis, Erica, and 2 others".to_owned())
|
||||
RoomDisplayName::Calculated("Alice, Bob, Carol, Denis, Erica, and 2 others".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2658,7 +2664,7 @@ mod tests {
|
||||
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap(),
|
||||
DisplayName::EmptyWas("Matthew".to_owned())
|
||||
RoomDisplayName::EmptyWas("Matthew".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2672,7 +2678,9 @@ mod tests {
|
||||
use crate::{RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons};
|
||||
|
||||
// Given a room,
|
||||
let client = BaseClient::new();
|
||||
let client = BaseClient::with_store_config(StoreConfig::new(
|
||||
"cross-process-store-locks-holder-name".to_owned(),
|
||||
));
|
||||
|
||||
client
|
||||
.set_session_meta(
|
||||
@@ -3054,34 +3062,34 @@ mod tests {
|
||||
#[test]
|
||||
fn test_calculate_room_name() {
|
||||
let mut actual = compute_display_name_from_heroes(2, vec!["a"]);
|
||||
assert_eq!(DisplayName::Calculated("a".to_owned()), actual);
|
||||
assert_eq!(RoomDisplayName::Calculated("a".to_owned()), actual);
|
||||
|
||||
actual = compute_display_name_from_heroes(3, vec!["a", "b"]);
|
||||
assert_eq!(DisplayName::Calculated("a, b".to_owned()), actual);
|
||||
assert_eq!(RoomDisplayName::Calculated("a, b".to_owned()), actual);
|
||||
|
||||
actual = compute_display_name_from_heroes(4, vec!["a", "b", "c"]);
|
||||
assert_eq!(DisplayName::Calculated("a, b, c".to_owned()), actual);
|
||||
assert_eq!(RoomDisplayName::Calculated("a, b, c".to_owned()), actual);
|
||||
|
||||
actual = compute_display_name_from_heroes(5, vec!["a", "b", "c"]);
|
||||
assert_eq!(DisplayName::Calculated("a, b, c, and 2 others".to_owned()), actual);
|
||||
assert_eq!(RoomDisplayName::Calculated("a, b, c, and 2 others".to_owned()), actual);
|
||||
|
||||
actual = compute_display_name_from_heroes(5, vec![]);
|
||||
assert_eq!(DisplayName::Calculated("5 people".to_owned()), actual);
|
||||
assert_eq!(RoomDisplayName::Calculated("5 people".to_owned()), actual);
|
||||
|
||||
actual = compute_display_name_from_heroes(0, vec![]);
|
||||
assert_eq!(DisplayName::Empty, actual);
|
||||
assert_eq!(RoomDisplayName::Empty, actual);
|
||||
|
||||
actual = compute_display_name_from_heroes(1, vec![]);
|
||||
assert_eq!(DisplayName::Empty, actual);
|
||||
assert_eq!(RoomDisplayName::Empty, actual);
|
||||
|
||||
actual = compute_display_name_from_heroes(1, vec!["a"]);
|
||||
assert_eq!(DisplayName::EmptyWas("a".to_owned()), actual);
|
||||
assert_eq!(RoomDisplayName::EmptyWas("a".to_owned()), actual);
|
||||
|
||||
actual = compute_display_name_from_heroes(1, vec!["a", "b"]);
|
||||
assert_eq!(DisplayName::EmptyWas("a, b".to_owned()), actual);
|
||||
assert_eq!(RoomDisplayName::EmptyWas("a, b".to_owned()), actual);
|
||||
|
||||
actual = compute_display_name_from_heroes(1, vec!["a", "b", "c"]);
|
||||
assert_eq!(DisplayName::EmptyWas("a, b, c".to_owned()), actual);
|
||||
assert_eq!(RoomDisplayName::EmptyWas("a, b, c".to_owned()), actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -695,6 +695,10 @@ async fn cache_latest_events(
|
||||
changes: Option<&StateChanges>,
|
||||
store: Option<&Store>,
|
||||
) {
|
||||
use crate::{
|
||||
deserialized_responses::DisplayName, store::ambiguity_map::is_display_name_ambiguous,
|
||||
};
|
||||
|
||||
let mut encrypted_events =
|
||||
Vec::with_capacity(room.latest_encrypted_events.read().unwrap().capacity());
|
||||
|
||||
@@ -752,11 +756,13 @@ async fn cache_latest_events(
|
||||
.as_original()
|
||||
.and_then(|profile| profile.content.displayname.as_ref())
|
||||
.and_then(|display_name| {
|
||||
let display_name = DisplayName::new(display_name);
|
||||
|
||||
changes.ambiguity_maps.get(room.room_id()).and_then(
|
||||
|map_for_room| {
|
||||
map_for_room
|
||||
.get(display_name)
|
||||
.map(|user_ids| user_ids.len() > 1)
|
||||
map_for_room.get(&display_name).map(|users| {
|
||||
is_display_name_ambiguous(&display_name, users)
|
||||
})
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
@@ -24,28 +24,24 @@ use ruma::{
|
||||
},
|
||||
OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
use tracing::trace;
|
||||
use tracing::{instrument, trace};
|
||||
|
||||
use super::{DynStateStore, Result, StateChanges};
|
||||
use crate::{
|
||||
deserialized_responses::{AmbiguityChange, RawMemberEvent},
|
||||
deserialized_responses::{AmbiguityChange, DisplayName, RawMemberEvent},
|
||||
store::StateStoreExt,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct AmbiguityCache {
|
||||
pub store: Arc<DynStateStore>,
|
||||
pub cache: BTreeMap<OwnedRoomId, BTreeMap<String, BTreeSet<OwnedUserId>>>,
|
||||
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AmbiguityMap {
|
||||
display_name: String,
|
||||
/// A map of users that use a certain display name.
|
||||
#[derive(Debug, Clone)]
|
||||
struct DisplayNameUsers {
|
||||
display_name: DisplayName,
|
||||
users: BTreeSet<OwnedUserId>,
|
||||
}
|
||||
|
||||
impl AmbiguityMap {
|
||||
impl DisplayNameUsers {
|
||||
/// Remove the given [`UserId`] from the map, marking that the [`UserId`]
|
||||
/// doesn't use the display name anymore.
|
||||
fn remove(&mut self, user_id: &UserId) -> Option<OwnedUserId> {
|
||||
self.users.remove(user_id);
|
||||
|
||||
@@ -56,6 +52,8 @@ impl AmbiguityMap {
|
||||
}
|
||||
}
|
||||
|
||||
/// Add the given [`UserId`] from the map, marking that the [`UserId`]
|
||||
/// is using the display name.
|
||||
fn add(&mut self, user_id: OwnedUserId) -> Option<OwnedUserId> {
|
||||
let ambiguous_user =
|
||||
if self.user_count() == 1 { self.users.iter().next().cloned() } else { None };
|
||||
@@ -65,46 +63,73 @@ impl AmbiguityMap {
|
||||
ambiguous_user
|
||||
}
|
||||
|
||||
/// How many users are using this display name.
|
||||
fn user_count(&self) -> usize {
|
||||
self.users.len()
|
||||
}
|
||||
|
||||
/// Is the display name considered to be ambiguous.
|
||||
fn is_ambiguous(&self) -> bool {
|
||||
self.user_count() > 1
|
||||
is_display_name_ambiguous(&self.display_name, &self.users)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_member_active(membership: &MembershipState) -> bool {
|
||||
use MembershipState::*;
|
||||
matches!(membership, Join | Invite | Knock)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct AmbiguityCache {
|
||||
pub store: Arc<DynStateStore>,
|
||||
pub cache: BTreeMap<OwnedRoomId, HashMap<DisplayName, BTreeSet<OwnedUserId>>>,
|
||||
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
|
||||
}
|
||||
|
||||
#[instrument(ret)]
|
||||
pub(crate) fn is_display_name_ambiguous(
|
||||
display_name: &DisplayName,
|
||||
users_with_display_name: &BTreeSet<OwnedUserId>,
|
||||
) -> bool {
|
||||
trace!("Checking if a display name is ambiguous");
|
||||
display_name.is_inherently_ambiguous() || users_with_display_name.len() > 1
|
||||
}
|
||||
|
||||
impl AmbiguityCache {
|
||||
/// Create a new [`AmbiguityCache`] backed by the given state store.
|
||||
pub fn new(store: Arc<DynStateStore>) -> Self {
|
||||
Self { store, cache: BTreeMap::new(), changes: BTreeMap::new() }
|
||||
}
|
||||
|
||||
/// Handle a newly received [`SyncRoomMemberEvent`] for the given room.
|
||||
pub async fn handle_event(
|
||||
&mut self,
|
||||
changes: &StateChanges,
|
||||
room_id: &RoomId,
|
||||
member_event: &SyncRoomMemberEvent,
|
||||
) -> Result<()> {
|
||||
// Synapse seems to have a bug where it puts the same event into the
|
||||
// state and the timeline sometimes.
|
||||
// Synapse seems to have a bug where it puts the same event into the state and
|
||||
// the timeline sometimes.
|
||||
//
|
||||
// Since our state, e.g. the old display name, already ended up inside
|
||||
// the state changes and we're pulling stuff out of the cache if it's
|
||||
// there calculating this twice for the same event will result in an
|
||||
// incorrect AmbiguityChange overwriting the correct one. In other
|
||||
// words, this method is not idempotent so we make it by ignoring
|
||||
// duplicate events.
|
||||
// Since our state, e.g. the old display name, already ended up inside the state
|
||||
// changes and we're pulling stuff out of the cache if it's there calculating
|
||||
// this twice for the same event will result in an incorrect AmbiguityChange
|
||||
// overwriting the correct one. In other words, this method is not idempotent so
|
||||
// we make it by ignoring duplicate events.
|
||||
if self.changes.get(room_id).is_some_and(|c| c.contains_key(member_event.event_id())) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (mut old_map, mut new_map) = self.get(changes, room_id, member_event).await?;
|
||||
let (mut old_map, mut new_map) =
|
||||
self.calculate_changes(changes, room_id, member_event).await?;
|
||||
|
||||
let display_names_same = match (&old_map, &new_map) {
|
||||
(Some(a), Some(b)) => a.display_name == b.display_name,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
// If the user's display name didn't change, then there's nothing more to
|
||||
// calculate here.
|
||||
if display_names_same {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -126,16 +151,21 @@ impl AmbiguityCache {
|
||||
|
||||
trace!(user_id = ?member_event.state_key(), "Handling display name ambiguity: {change:#?}");
|
||||
|
||||
self.add_change(room_id, member_event.event_id().to_owned(), change);
|
||||
self.changes
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.insert(member_event.event_id().to_owned(), change);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the [`AmbiguityCache`] state for the given room with a pair of
|
||||
/// [`DisplayNameUsers`] that got created by a new [`SyncRoomMemberEvent`].
|
||||
fn update(
|
||||
&mut self,
|
||||
room_id: &RoomId,
|
||||
old_map: Option<AmbiguityMap>,
|
||||
new_map: Option<AmbiguityMap>,
|
||||
old_map: Option<DisplayNameUsers>,
|
||||
new_map: Option<DisplayNameUsers>,
|
||||
) {
|
||||
let entry = self.cache.entry(room_id.to_owned()).or_default();
|
||||
|
||||
@@ -148,74 +178,102 @@ impl AmbiguityCache {
|
||||
}
|
||||
}
|
||||
|
||||
fn add_change(&mut self, room_id: &RoomId, event_id: OwnedEventId, change: AmbiguityChange) {
|
||||
self.changes.entry(room_id.to_owned()).or_default().insert(event_id, change);
|
||||
/// Get the previously used display name, if any, of the member described in
|
||||
/// the given new [`SyncRoomMemberEvent`].
|
||||
async fn get_old_display_name(
|
||||
&self,
|
||||
changes: &StateChanges,
|
||||
room_id: &RoomId,
|
||||
new_event: &SyncRoomMemberEvent,
|
||||
) -> Result<Option<String>> {
|
||||
let user_id = new_event.state_key();
|
||||
|
||||
let old_event = if let Some(m) = changes
|
||||
.state
|
||||
.get(room_id)
|
||||
.and_then(|events| events.get(&StateEventType::RoomMember)?.get(user_id.as_str()))
|
||||
{
|
||||
Some(RawMemberEvent::Sync(m.clone().cast()))
|
||||
} else {
|
||||
self.store.get_member_event(room_id, user_id).await?
|
||||
};
|
||||
|
||||
let Some(Ok(old_event)) = old_event.map(|r| r.deserialize()) else { return Ok(None) };
|
||||
|
||||
if is_member_active(old_event.membership()) {
|
||||
let display_name = if let Some(d) = changes
|
||||
.profiles
|
||||
.get(room_id)
|
||||
.and_then(|p| p.get(user_id)?.as_original()?.content.displayname.as_deref())
|
||||
{
|
||||
Some(d.to_owned())
|
||||
} else if let Some(d) = self
|
||||
.store
|
||||
.get_profile(room_id, user_id)
|
||||
.await?
|
||||
.and_then(|p| p.into_original()?.content.displayname)
|
||||
{
|
||||
Some(d)
|
||||
} else {
|
||||
old_event.original_content().and_then(|c| c.displayname.clone())
|
||||
};
|
||||
|
||||
Ok(Some(display_name.unwrap_or_else(|| user_id.localpart().to_owned())))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get(
|
||||
/// Get the [`DisplayNameUsers`] for the given display name in the given
|
||||
/// room.
|
||||
///
|
||||
/// This method will get the [`DisplayNameUsers`] from the cache, if the
|
||||
/// cache doesn't contain such an entry, it falls back to the state
|
||||
/// store.
|
||||
async fn get_users_with_display_name(
|
||||
&mut self,
|
||||
room_id: &RoomId,
|
||||
display_name: &DisplayName,
|
||||
) -> Result<DisplayNameUsers> {
|
||||
Ok(if let Some(u) = self.cache.entry(room_id.to_owned()).or_default().get(display_name) {
|
||||
DisplayNameUsers { display_name: display_name.clone(), users: u.clone() }
|
||||
} else {
|
||||
let users_with_display_name =
|
||||
self.store.get_users_with_display_name(room_id, display_name).await?;
|
||||
|
||||
DisplayNameUsers { display_name: display_name.clone(), users: users_with_display_name }
|
||||
})
|
||||
}
|
||||
|
||||
/// Calculate the change in the users that use a display name a
|
||||
/// [`SyncRoomMemberEvent`] will cause for a given room.
|
||||
///
|
||||
/// Returns the [`DisplayNameUsers`] before the member event is applied and
|
||||
/// the [`DisplayNameUsers`] after the member event is applied to the
|
||||
/// room state.
|
||||
async fn calculate_changes(
|
||||
&mut self,
|
||||
changes: &StateChanges,
|
||||
room_id: &RoomId,
|
||||
member_event: &SyncRoomMemberEvent,
|
||||
) -> Result<(Option<AmbiguityMap>, Option<AmbiguityMap>)> {
|
||||
use MembershipState::*;
|
||||
|
||||
let old_event = if let Some(m) = changes.state.get(room_id).and_then(|events| {
|
||||
events.get(&StateEventType::RoomMember)?.get(member_event.state_key().as_str())
|
||||
}) {
|
||||
Some(RawMemberEvent::Sync(m.clone().cast()))
|
||||
} else {
|
||||
self.store.get_member_event(room_id, member_event.state_key()).await?
|
||||
};
|
||||
|
||||
// FIXME: Use let chains once stable
|
||||
let old_display_name = if let Some(Ok(event)) = old_event.map(|r| r.deserialize()) {
|
||||
if matches!(event.membership(), Join | Invite) {
|
||||
let display_name = if let Some(d) = changes.profiles.get(room_id).and_then(|p| {
|
||||
p.get(member_event.state_key())?.as_original()?.content.displayname.as_deref()
|
||||
}) {
|
||||
Some(d.to_owned())
|
||||
} else if let Some(d) = self
|
||||
.store
|
||||
.get_profile(room_id, member_event.state_key())
|
||||
.await?
|
||||
.and_then(|p| p.into_original()?.content.displayname)
|
||||
{
|
||||
Some(d)
|
||||
} else {
|
||||
event.original_content().and_then(|c| c.displayname.clone())
|
||||
};
|
||||
|
||||
Some(display_name.unwrap_or_else(|| event.user_id().localpart().to_owned()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
) -> Result<(Option<DisplayNameUsers>, Option<DisplayNameUsers>)> {
|
||||
let old_display_name = self.get_old_display_name(changes, room_id, member_event).await?;
|
||||
|
||||
let old_map = if let Some(old_name) = old_display_name.as_deref() {
|
||||
let old_display_name_map =
|
||||
if let Some(u) = self.cache.entry(room_id.to_owned()).or_default().get(old_name) {
|
||||
u.clone()
|
||||
} else {
|
||||
self.store.get_users_with_display_name(room_id, old_name).await?
|
||||
};
|
||||
|
||||
Some(AmbiguityMap { display_name: old_name.to_owned(), users: old_display_name_map })
|
||||
let old_display_name = DisplayName::new(old_name);
|
||||
Some(self.get_users_with_display_name(room_id, &old_display_name).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let new_map = if matches!(member_event.membership(), Join | Invite) {
|
||||
let new_map = if is_member_active(member_event.membership()) {
|
||||
let new = member_event
|
||||
.as_original()
|
||||
.and_then(|ev| ev.content.displayname.as_deref())
|
||||
.unwrap_or_else(|| member_event.state_key().localpart());
|
||||
|
||||
// We don't allow other users to set the display name, so if we
|
||||
// have a more trusted version of the display
|
||||
// name use that.
|
||||
// We don't allow other users to set the display name, so if we have a more
|
||||
// trusted version of the display name use that.
|
||||
let new_display_name = if member_event.sender().as_str() == member_event.state_key() {
|
||||
new
|
||||
} else if let Some(old) = old_display_name.as_deref() {
|
||||
@@ -224,22 +282,221 @@ impl AmbiguityCache {
|
||||
new
|
||||
};
|
||||
|
||||
let new_display_name_map = if let Some(u) =
|
||||
self.cache.entry(room_id.to_owned()).or_default().get(new_display_name)
|
||||
{
|
||||
u.clone()
|
||||
} else {
|
||||
self.store.get_users_with_display_name(room_id, new_display_name).await?
|
||||
};
|
||||
let new_display_name = DisplayName::new(new_display_name);
|
||||
|
||||
Some(AmbiguityMap {
|
||||
display_name: new_display_name.to_owned(),
|
||||
users: new_display_name_map,
|
||||
})
|
||||
Some(self.get_users_with_display_name(room_id, &new_display_name).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok((old_map, new_map))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn check(&self, room_id: &RoomId, display_name: &DisplayName) -> bool {
|
||||
self.cache
|
||||
.get(room_id)
|
||||
.and_then(|display_names| {
|
||||
display_names
|
||||
.get(display_name)
|
||||
.map(|user_ids| is_display_name_ambiguous(display_name, user_ids))
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"The display name {:?} should be part of the cache {:?}",
|
||||
display_name, self.cache
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{room_id, server_name, user_id, EventId};
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
use crate::store::{IntoStateStore, MemoryStore};
|
||||
|
||||
fn generate_event(user_id: &UserId, display_name: &str) -> SyncRoomMemberEvent {
|
||||
let server_name = server_name!("localhost");
|
||||
serde_json::from_value(json!({
|
||||
"content": {
|
||||
"displayname": display_name,
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": EventId::new(server_name),
|
||||
"origin_server_ts": 152037280,
|
||||
"sender": user_id,
|
||||
"state_key": user_id,
|
||||
"type": "m.room.member",
|
||||
|
||||
}))
|
||||
.expect("We should be able to deserialize the static member event")
|
||||
}
|
||||
|
||||
macro_rules! assert_ambiguity {
|
||||
(
|
||||
[ $( ($user:literal, $display_name:literal) ),* ],
|
||||
[ $( ($check_display_name:literal, $ambiguous:expr) ),* ] $(,)?
|
||||
) => {
|
||||
assert_ambiguity!(
|
||||
[ $( ($user, $display_name) ),* ],
|
||||
[ $( ($check_display_name, $ambiguous) ),* ],
|
||||
"The test failed the ambiguity assertions"
|
||||
)
|
||||
};
|
||||
|
||||
(
|
||||
[ $( ($user:literal, $display_name:literal) ),* ],
|
||||
[ $( ($check_display_name:literal, $ambiguous:expr) ),* ],
|
||||
$description:literal $(,)?
|
||||
) => {
|
||||
let store = MemoryStore::new();
|
||||
let mut ambiguity_cache = AmbiguityCache::new(store.into_state_store());
|
||||
|
||||
let changes = Default::default();
|
||||
let room_id = room_id!("!foo:bar");
|
||||
|
||||
macro_rules! add_display_name {
|
||||
($u:literal, $n:literal) => {
|
||||
let event = generate_event(user_id!($u), $n);
|
||||
|
||||
ambiguity_cache
|
||||
.handle_event(&changes, room_id, &event)
|
||||
.await
|
||||
.expect("We should be able to handle a member event to calculate the ambiguity.");
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! assert_display_name_ambiguity {
|
||||
($n:literal, $a:expr) => {
|
||||
let display_name = DisplayName::new($n);
|
||||
|
||||
if ambiguity_cache.check(room_id, &display_name) != $a {
|
||||
let foo = if $a { "be" } else { "not be" };
|
||||
panic!("{}: the display name {} should {} ambiguous", $description, $n, foo);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
$(
|
||||
add_display_name!($user, $display_name);
|
||||
)*
|
||||
|
||||
$(
|
||||
assert_display_name_ambiguity!($check_display_name, $ambiguous);
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_disambiguation() {
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "alice")],
|
||||
[("alice", false)],
|
||||
"Alice is alone in the room"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "alice")],
|
||||
[("Alice", false)],
|
||||
"Alice is alone in the room and has a capitalized display name"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "alice"), ("@bob:localhost", "alice")],
|
||||
[("alice", true)],
|
||||
"Alice and bob share a display name"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[
|
||||
("@alice:localhost", "alice"),
|
||||
("@bob:localhost", "alice"),
|
||||
("@carol:localhost", "carol")
|
||||
],
|
||||
[("alice", true), ("carol", false)],
|
||||
"Alice and Bob share a display name, while Carol is unique"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "alice"), ("@bob:localhost", "ALICE")],
|
||||
[("alice", true)],
|
||||
"Alice and Bob share a display name that is differently capitalized"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "alice"), ("@bob:localhost", "аlice")],
|
||||
[("alice", true)],
|
||||
"Bob tries to impersonate Alice using a cyrilic а"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "@bob:localhost"), ("@bob:localhost", "аlice")],
|
||||
[("@bob:localhost", true)],
|
||||
"Alice tries to impersonate bob using an mxid"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶")],
|
||||
[("Sahasrahla", true)],
|
||||
"Bob tries to impersonate Alice using scripture symbols"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "𝔖𝔞𝔥𝔞𝔰𝔯𝔞𝔥𝔩𝔞")],
|
||||
[("Sahasrahla", true)],
|
||||
"Bob tries to impersonate Alice using fraktur symbols"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Ⓢⓐⓗⓐⓢⓡⓐⓗⓛⓐ")],
|
||||
[("Sahasrahla", true)],
|
||||
"Bob tries to impersonate Alice using circled symbols"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "🅂🄰🄷🄰🅂🅁🄰🄷🄻🄰")],
|
||||
[("Sahasrahla", true)],
|
||||
"Bob tries to impersonate Alice using squared symbols"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Sahasrahla")],
|
||||
[("Sahasrahla", true)],
|
||||
"Bob tries to impersonate Alice using big unicode letters"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "\u{202e}alharsahas")],
|
||||
[("Sahasrahla", true)],
|
||||
"Bob tries to impersonate Alice using left to right shenanigans"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Sa̴hasrahla")],
|
||||
[("Sahasrahla", true)],
|
||||
"Bob tries to impersonate Alice using a diacritical mark"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Sahas\u{200B}rahla")],
|
||||
[("Sahasrahla", true)],
|
||||
"Bob tries to impersonate Alice using a zero-width space"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Sahas\u{200D}rahla")],
|
||||
[("Sahasrahla", true)],
|
||||
"Bob tries to impersonate Alice using a zero-width space"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "ff"), ("@bob:localhost", "\u{FB00}")],
|
||||
[("ff", true)],
|
||||
"Bob tries to impersonate Alice using a ligature"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Trait and macro of integration tests for StateStore implementations.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use assert_matches2::assert_let;
|
||||
@@ -34,7 +34,8 @@ use ruma::{
|
||||
use serde_json::{json, value::Value as JsonValue};
|
||||
|
||||
use super::{
|
||||
send_queue::SentRequestKey, DependentQueuedRequestKind, DynStateStore, ServerCapabilities,
|
||||
send_queue::SentRequestKey, DependentQueuedRequestKind, DisplayName, DynStateStore,
|
||||
ServerCapabilities,
|
||||
};
|
||||
use crate::{
|
||||
deserialized_responses::MemberEvent,
|
||||
@@ -85,6 +86,8 @@ pub trait StateStoreIntegrationTests {
|
||||
async fn test_display_names_saving(&self);
|
||||
/// Test operations with the send queue.
|
||||
async fn test_send_queue(&self);
|
||||
/// Test priority of operations with the send queue.
|
||||
async fn test_send_queue_priority(&self);
|
||||
/// Test operations related to send queue dependents.
|
||||
async fn test_send_queue_dependents(&self);
|
||||
/// Test saving/restoring server capabilities.
|
||||
@@ -139,13 +142,15 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
room.handle_state_event(&topic_event);
|
||||
changes.add_state_event(room_id, topic_event, topic_raw);
|
||||
|
||||
let mut room_ambiguity_map = BTreeMap::new();
|
||||
let mut room_ambiguity_map = HashMap::new();
|
||||
let mut room_profiles = BTreeMap::new();
|
||||
|
||||
let member_json: &JsonValue = &test_json::MEMBER;
|
||||
let member_event: SyncRoomMemberEvent =
|
||||
serde_json::from_value(member_json.clone()).unwrap();
|
||||
let displayname = member_event.as_original().unwrap().content.displayname.clone().unwrap();
|
||||
let displayname = DisplayName::new(
|
||||
member_event.as_original().unwrap().content.displayname.as_ref().unwrap(),
|
||||
);
|
||||
room_ambiguity_map.insert(displayname.clone(), BTreeSet::from([user_id.to_owned()]));
|
||||
room_profiles.insert(user_id.to_owned(), (&member_event).into());
|
||||
|
||||
@@ -254,6 +259,8 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
async fn test_populate_store(&self) -> Result<()> {
|
||||
let room_id = room_id();
|
||||
let user_id = user_id();
|
||||
let display_name = DisplayName::new("example");
|
||||
|
||||
self.populate().await?;
|
||||
|
||||
assert!(self.get_kv_data(StateStoreDataKey::SyncToken).await?.is_some());
|
||||
@@ -288,7 +295,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
"Expected to find 1 joined user ids"
|
||||
);
|
||||
assert_eq!(
|
||||
self.get_users_with_display_name(room_id, "example").await?.len(),
|
||||
self.get_users_with_display_name(room_id, &display_name).await?.len(),
|
||||
2,
|
||||
"Expected to find 2 display names for room"
|
||||
);
|
||||
@@ -960,6 +967,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
async fn test_room_removal(&self) -> Result<()> {
|
||||
let room_id = room_id();
|
||||
let user_id = user_id();
|
||||
let display_name = DisplayName::new("example");
|
||||
let stripped_room_id = stripped_room_id();
|
||||
|
||||
self.populate().await?;
|
||||
@@ -988,7 +996,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
"still joined users found"
|
||||
);
|
||||
assert!(
|
||||
self.get_users_with_display_name(room_id, "example").await?.is_empty(),
|
||||
self.get_users_with_display_name(room_id, &display_name).await?.is_empty(),
|
||||
"still display names found"
|
||||
);
|
||||
assert!(self
|
||||
@@ -1143,15 +1151,15 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
async fn test_display_names_saving(&self) {
|
||||
let room_id = room_id!("!test_display_names_saving:localhost");
|
||||
let user_id = user_id();
|
||||
let user_display_name = "User";
|
||||
let user_display_name = DisplayName::new("User");
|
||||
let second_user_id = user_id!("@second:localhost");
|
||||
let third_user_id = user_id!("@third:localhost");
|
||||
let other_display_name = "Raoul";
|
||||
let unknown_display_name = "Unknown";
|
||||
let other_display_name = DisplayName::new("Raoul");
|
||||
let unknown_display_name = DisplayName::new("Unknown");
|
||||
|
||||
// No event in store.
|
||||
let mut display_names = vec![user_display_name.to_owned()];
|
||||
let users = self.get_users_with_display_name(room_id, user_display_name).await.unwrap();
|
||||
let users = self.get_users_with_display_name(room_id, &user_display_name).await.unwrap();
|
||||
assert!(users.is_empty());
|
||||
let names = self.get_users_with_display_names(room_id, &display_names).await.unwrap();
|
||||
assert!(names.is_empty());
|
||||
@@ -1165,7 +1173,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
.insert(user_display_name.to_owned(), [user_id.to_owned()].into());
|
||||
self.save_changes(&changes).await.unwrap();
|
||||
|
||||
let users = self.get_users_with_display_name(room_id, user_display_name).await.unwrap();
|
||||
let users = self.get_users_with_display_name(room_id, &user_display_name).await.unwrap();
|
||||
assert_eq!(users.len(), 1);
|
||||
let names = self.get_users_with_display_names(room_id, &display_names).await.unwrap();
|
||||
assert_eq!(names.len(), 1);
|
||||
@@ -1180,9 +1188,9 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
self.save_changes(&changes).await.unwrap();
|
||||
|
||||
display_names.push(other_display_name.to_owned());
|
||||
let users = self.get_users_with_display_name(room_id, user_display_name).await.unwrap();
|
||||
let users = self.get_users_with_display_name(room_id, &user_display_name).await.unwrap();
|
||||
assert_eq!(users.len(), 1);
|
||||
let users = self.get_users_with_display_name(room_id, other_display_name).await.unwrap();
|
||||
let users = self.get_users_with_display_name(room_id, &other_display_name).await.unwrap();
|
||||
assert_eq!(users.len(), 2);
|
||||
let names = self.get_users_with_display_names(room_id, &display_names).await.unwrap();
|
||||
assert_eq!(names.len(), 2);
|
||||
@@ -1212,7 +1220,7 @@ 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()).await.unwrap();
|
||||
self.save_send_queue_request(room_id, txn0.clone(), event0.into(), 0).await.unwrap();
|
||||
|
||||
// Reading it will work.
|
||||
let pending = self.load_send_queue_requests(room_id).await.unwrap();
|
||||
@@ -1236,7 +1244,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.save_send_queue_request(room_id, txn, event.into()).await.unwrap();
|
||||
self.save_send_queue_request(room_id, txn, event.into(), 0).await.unwrap();
|
||||
}
|
||||
|
||||
// Reading all the events should work.
|
||||
@@ -1334,7 +1342,7 @@ 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()).await.unwrap();
|
||||
self.save_send_queue_request(room_id2, txn.clone(), event.into(), 0).await.unwrap();
|
||||
}
|
||||
|
||||
// Add and remove one event for room3.
|
||||
@@ -1344,7 +1352,7 @@ 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()).await.unwrap();
|
||||
self.save_send_queue_request(room_id3, txn.clone(), event.into(), 0).await.unwrap();
|
||||
|
||||
self.remove_send_queue_request(room_id3, &txn).await.unwrap();
|
||||
}
|
||||
@@ -1357,6 +1365,64 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
assert!(outstanding_rooms.iter().any(|room| room == room_id2));
|
||||
}
|
||||
|
||||
async fn test_send_queue_priority(&self) {
|
||||
let room_id = room_id!("!test_send_queue:localhost");
|
||||
|
||||
// No queued event in store at first.
|
||||
let events = self.load_send_queue_requests(room_id).await.unwrap();
|
||||
assert!(events.is_empty());
|
||||
|
||||
// Saving one request should work.
|
||||
let low0_txn = TransactionId::new();
|
||||
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();
|
||||
|
||||
// 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();
|
||||
|
||||
// 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();
|
||||
|
||||
// The requests should be ordered from higher priority to lower, and when equal,
|
||||
// should use the insertion order instead.
|
||||
let pending = self.load_send_queue_requests(room_id).await.unwrap();
|
||||
|
||||
assert_eq!(pending.len(), 3);
|
||||
{
|
||||
assert_eq!(pending[0].transaction_id, high_txn);
|
||||
|
||||
let deserialized = pending[0].as_event().unwrap().deserialize().unwrap();
|
||||
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
|
||||
assert_eq!(content.body(), "high");
|
||||
}
|
||||
|
||||
{
|
||||
assert_eq!(pending[1].transaction_id, low0_txn);
|
||||
|
||||
let deserialized = pending[1].as_event().unwrap().deserialize().unwrap();
|
||||
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
|
||||
assert_eq!(content.body(), "low0");
|
||||
}
|
||||
|
||||
{
|
||||
assert_eq!(pending[2].transaction_id, low1_txn);
|
||||
|
||||
let deserialized = pending[2].as_event().unwrap().deserialize().unwrap();
|
||||
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
|
||||
assert_eq!(content.body(), "low1");
|
||||
}
|
||||
}
|
||||
|
||||
async fn test_send_queue_dependents(&self) {
|
||||
let room_id = room_id!("!test_send_queue_dependents:localhost");
|
||||
|
||||
@@ -1365,7 +1431,7 @@ 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()).await.unwrap();
|
||||
self.save_send_queue_request(room_id, txn0.clone(), event0.into(), 0).await.unwrap();
|
||||
|
||||
// No dependents, to start with.
|
||||
assert!(self.load_dependent_queued_requests(room_id).await.unwrap().is_empty());
|
||||
@@ -1427,7 +1493,7 @@ 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()).await.unwrap();
|
||||
self.save_send_queue_request(room_id, txn1.clone(), event1.into(), 0).await.unwrap();
|
||||
|
||||
self.save_dependent_queued_request(
|
||||
room_id,
|
||||
@@ -1609,6 +1675,12 @@ macro_rules! statestore_integration_tests {
|
||||
store.test_send_queue().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_send_queue_priority() {
|
||||
let store = get_store().await.expect("creating store failed").into_state_store();
|
||||
store.test_send_queue_priority().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_send_queue_dependents() {
|
||||
let store = get_store().await.expect("creating store failed").into_state_store();
|
||||
|
||||
@@ -42,7 +42,8 @@ use super::{
|
||||
StateChanges, StateStore, StoreError,
|
||||
};
|
||||
use crate::{
|
||||
deserialized_responses::RawAnySyncOrStrippedState, store::QueueWedgeError,
|
||||
deserialized_responses::{DisplayName, RawAnySyncOrStrippedState},
|
||||
store::QueueWedgeError,
|
||||
MinimalRoomMemberEvent, RoomMemberships, StateStoreDataKey, StateStoreDataValue,
|
||||
};
|
||||
|
||||
@@ -61,7 +62,7 @@ pub struct MemoryStore {
|
||||
utd_hook_manager_data: StdRwLock<Option<GrowableBloom>>,
|
||||
account_data: StdRwLock<HashMap<GlobalAccountDataEventType, Raw<AnyGlobalAccountDataEvent>>>,
|
||||
profiles: StdRwLock<HashMap<OwnedRoomId, HashMap<OwnedUserId, MinimalRoomMemberEvent>>>,
|
||||
display_names: StdRwLock<HashMap<OwnedRoomId, HashMap<String, BTreeSet<OwnedUserId>>>>,
|
||||
display_names: StdRwLock<HashMap<OwnedRoomId, HashMap<DisplayName, BTreeSet<OwnedUserId>>>>,
|
||||
members: StdRwLock<HashMap<OwnedRoomId, HashMap<OwnedUserId, MembershipState>>>,
|
||||
room_info: StdRwLock<HashMap<OwnedRoomId, RoomInfo>>,
|
||||
room_state: StdRwLock<
|
||||
@@ -701,7 +702,7 @@ impl StateStore for MemoryStore {
|
||||
async fn get_users_with_display_name(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
display_name: &str,
|
||||
display_name: &DisplayName,
|
||||
) -> Result<BTreeSet<OwnedUserId>> {
|
||||
Ok(self
|
||||
.display_names
|
||||
@@ -715,21 +716,18 @@ impl StateStore for MemoryStore {
|
||||
async fn get_users_with_display_names<'a>(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
display_names: &'a [String],
|
||||
) -> Result<BTreeMap<&'a str, BTreeSet<OwnedUserId>>> {
|
||||
display_names: &'a [DisplayName],
|
||||
) -> Result<HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>> {
|
||||
if display_names.is_empty() {
|
||||
return Ok(BTreeMap::new());
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
|
||||
let read_guard = &self.display_names.read().unwrap();
|
||||
let Some(room_names) = read_guard.get(room_id) else {
|
||||
return Ok(BTreeMap::new());
|
||||
return Ok(HashMap::new());
|
||||
};
|
||||
|
||||
Ok(display_names
|
||||
.iter()
|
||||
.filter_map(|n| room_names.get(n).map(|d| (n.as_str(), d.clone())))
|
||||
.collect())
|
||||
Ok(display_names.iter().filter_map(|n| room_names.get(n).map(|d| (n, d.clone()))).collect())
|
||||
}
|
||||
|
||||
async fn get_account_data_event(
|
||||
@@ -807,13 +805,14 @@ impl StateStore for MemoryStore {
|
||||
room_id: &RoomId,
|
||||
transaction_id: OwnedTransactionId,
|
||||
kind: QueuedRequestKind,
|
||||
priority: usize,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.send_queue_events
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.push(QueuedRequest { kind, transaction_id, error: None });
|
||||
.push(QueuedRequest { kind, transaction_id, error: None, priority });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -867,7 +866,11 @@ impl StateStore for MemoryStore {
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Vec<QueuedRequest>, Self::Error> {
|
||||
Ok(self.send_queue_events.write().unwrap().entry(room_id.to_owned()).or_default().clone())
|
||||
let mut ret =
|
||||
self.send_queue_events.write().unwrap().entry(room_id.to_owned()).or_default().clone();
|
||||
// Inverted order of priority, use stable sort to keep insertion order.
|
||||
ret.sort_by(|lhs, rhs| rhs.priority.cmp(&lhs.priority));
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
async fn update_send_queue_request_status(
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
//! store.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
fmt,
|
||||
ops::Deref,
|
||||
result::Result as StdResult,
|
||||
@@ -29,9 +29,7 @@ use std::{
|
||||
sync::{Arc, RwLock as StdRwLock},
|
||||
};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use eyeball_im::{Vector, VectorDiff};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use futures_util::Stream;
|
||||
use once_cell::sync::OnceCell;
|
||||
|
||||
@@ -60,7 +58,8 @@ use tokio::sync::{broadcast, Mutex, RwLock};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
event_cache_store,
|
||||
deserialized_responses::DisplayName,
|
||||
event_cache::store as event_cache_store,
|
||||
rooms::{normal::RoomInfoNotableUpdate, RoomInfo, RoomState},
|
||||
MinimalRoomMemberEvent, Room, RoomStateFilter, SessionMeta,
|
||||
};
|
||||
@@ -267,7 +266,6 @@ impl Store {
|
||||
|
||||
/// Get a stream of all the rooms changes, in addition to the existing
|
||||
/// rooms.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn rooms_stream(&self) -> (Vector<Room>, impl Stream<Item = Vec<VectorDiff<Room>>>) {
|
||||
self.rooms.read().unwrap().stream()
|
||||
}
|
||||
@@ -387,7 +385,7 @@ pub struct StateChanges {
|
||||
|
||||
/// A map from room id to a map of a display name and a set of user ids that
|
||||
/// share that display name in the given room.
|
||||
pub ambiguity_maps: BTreeMap<OwnedRoomId, BTreeMap<String, BTreeSet<OwnedUserId>>>,
|
||||
pub ambiguity_maps: BTreeMap<OwnedRoomId, HashMap<DisplayName, BTreeSet<OwnedUserId>>>,
|
||||
}
|
||||
|
||||
impl StateChanges {
|
||||
@@ -483,7 +481,8 @@ impl StateChanges {
|
||||
/// ```
|
||||
/// # use matrix_sdk_base::store::StoreConfig;
|
||||
///
|
||||
/// let store_config = StoreConfig::new();
|
||||
/// let store_config =
|
||||
/// StoreConfig::new("cross-process-store-locks-holder-name".to_owned());
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct StoreConfig {
|
||||
@@ -491,6 +490,7 @@ pub struct StoreConfig {
|
||||
pub(crate) crypto_store: Arc<DynCryptoStore>,
|
||||
pub(crate) state_store: Arc<DynStateStore>,
|
||||
pub(crate) event_cache_store: event_cache_store::EventCacheStoreLock,
|
||||
cross_process_store_locks_holder_name: String,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
@@ -502,17 +502,20 @@ impl fmt::Debug for StoreConfig {
|
||||
|
||||
impl StoreConfig {
|
||||
/// Create a new default `StoreConfig`.
|
||||
///
|
||||
/// To learn more about `cross_process_store_locks_holder_name`, please read
|
||||
/// [`CrossProcessStoreLock::new`](matrix_sdk_common::store_locks::CrossProcessStoreLock::new).
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
pub fn new(cross_process_store_locks_holder_name: String) -> Self {
|
||||
Self {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
crypto_store: matrix_sdk_crypto::store::MemoryStore::new().into_crypto_store(),
|
||||
state_store: Arc::new(MemoryStore::new()),
|
||||
event_cache_store: event_cache_store::EventCacheStoreLock::new(
|
||||
event_cache_store::MemoryStore::new(),
|
||||
"default-key".to_owned(),
|
||||
"matrix-sdk-base".to_owned(),
|
||||
cross_process_store_locks_holder_name.clone(),
|
||||
),
|
||||
cross_process_store_locks_holder_name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -532,21 +535,14 @@ impl StoreConfig {
|
||||
}
|
||||
|
||||
/// Set a custom implementation of an `EventCacheStore`.
|
||||
///
|
||||
/// The `key` and `holder` arguments represent the key and holder inside the
|
||||
/// [`CrossProcessStoreLock::new`][matrix_sdk_common::store_locks::CrossProcessStoreLock::new].
|
||||
pub fn event_cache_store<S>(mut self, event_cache_store: S, key: String, holder: String) -> Self
|
||||
pub fn event_cache_store<S>(mut self, event_cache_store: S) -> Self
|
||||
where
|
||||
S: event_cache_store::IntoEventCacheStore,
|
||||
{
|
||||
self.event_cache_store =
|
||||
event_cache_store::EventCacheStoreLock::new(event_cache_store, key, holder);
|
||||
self.event_cache_store = event_cache_store::EventCacheStoreLock::new(
|
||||
event_cache_store,
|
||||
self.cross_process_store_locks_holder_name.clone(),
|
||||
);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for StoreConfig {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,212 +14,137 @@
|
||||
|
||||
//! An [`ObservableMap`] implementation.
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod impl_non_wasm32 {
|
||||
use std::{borrow::Borrow, collections::HashMap, hash::Hash};
|
||||
use std::{borrow::Borrow, collections::HashMap, hash::Hash};
|
||||
|
||||
use eyeball_im::{ObservableVector, Vector, VectorDiff};
|
||||
use futures_util::Stream;
|
||||
use eyeball_im::{ObservableVector, Vector, VectorDiff};
|
||||
use futures_util::Stream;
|
||||
|
||||
/// An observable map.
|
||||
///
|
||||
/// This is an “observable map” naive implementation. Just like regular
|
||||
/// hashmap, we have a redirection from a key to a position, and from a
|
||||
/// position to a value. The (key, position) tuples are stored in an
|
||||
/// [`HashMap`]. The (position, value) tuples are stored in an
|
||||
/// [`ObservableVector`]. The (key, position) tuple is only provided for
|
||||
/// fast _reading_ implementations, like `Self::get` and
|
||||
/// `Self::get_or_create`. The (position, value) tuples are observable,
|
||||
/// this is what interests us the most here.
|
||||
///
|
||||
/// Why not implementing a new `ObservableMap` type in `eyeball-im` instead
|
||||
/// of this custom implementation? Because we want to continue providing
|
||||
/// `VectorDiff` when observing the changes, so that the rest of the API in
|
||||
/// the Matrix Rust SDK aren't broken. Indeed, an `ObservableMap` must
|
||||
/// produce `MapDiff`, which would be quite different.
|
||||
/// Plus, we would like to re-use all our existing code, test, stream
|
||||
/// adapters and so on.
|
||||
///
|
||||
/// This is a trade-off. This implementation is simple enough for the
|
||||
/// moment, and basically does the job.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ObservableMap<K, V>
|
||||
where
|
||||
V: Clone + Send + Sync + 'static,
|
||||
{
|
||||
/// The (key, position) tuples.
|
||||
mapping: HashMap<K, usize>,
|
||||
/// An observable map.
|
||||
///
|
||||
/// This is an “observable map” naive implementation. Just like regular
|
||||
/// hashmap, we have a redirection from a key to a position, and from a
|
||||
/// position to a value. The (key, position) tuples are stored in an
|
||||
/// [`HashMap`]. The (position, value) tuples are stored in an
|
||||
/// [`ObservableVector`]. The (key, position) tuple is only provided for
|
||||
/// fast _reading_ implementations, like `Self::get` and
|
||||
/// `Self::get_or_create`. The (position, value) tuples are observable,
|
||||
/// this is what interests us the most here.
|
||||
///
|
||||
/// Why not implementing a new `ObservableMap` type in `eyeball-im` instead
|
||||
/// of this custom implementation? Because we want to continue providing
|
||||
/// `VectorDiff` when observing the changes, so that the rest of the API in
|
||||
/// the Matrix Rust SDK aren't broken. Indeed, an `ObservableMap` must
|
||||
/// produce `MapDiff`, which would be quite different.
|
||||
/// Plus, we would like to re-use all our existing code, test, stream
|
||||
/// adapters and so on.
|
||||
///
|
||||
/// This is a trade-off. This implementation is simple enough for the
|
||||
/// moment, and basically does the job.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ObservableMap<K, V>
|
||||
where
|
||||
V: Clone + 'static,
|
||||
{
|
||||
/// The (key, position) tuples.
|
||||
mapping: HashMap<K, usize>,
|
||||
|
||||
/// The values where the indices are the `position` part of
|
||||
/// `Self::mapping`.
|
||||
values: ObservableVector<V>,
|
||||
/// The values where the indices are the `position` part of
|
||||
/// `Self::mapping`.
|
||||
values: ObservableVector<V>,
|
||||
}
|
||||
|
||||
impl<K, V> ObservableMap<K, V>
|
||||
where
|
||||
K: Hash + Eq,
|
||||
V: Clone + 'static,
|
||||
{
|
||||
/// Create a new `Self`.
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { mapping: HashMap::new(), values: ObservableVector::new() }
|
||||
}
|
||||
|
||||
impl<K, V> ObservableMap<K, V>
|
||||
where
|
||||
K: Hash + Eq,
|
||||
V: Clone + Send + Sync + 'static,
|
||||
{
|
||||
/// Create a new `Self`.
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { mapping: HashMap::new(), values: ObservableVector::new() }
|
||||
}
|
||||
/// Insert a new `V` in the collection.
|
||||
///
|
||||
/// If the `V` value already exists, it will be updated to the new one.
|
||||
pub(crate) fn insert(&mut self, key: K, value: V) -> usize {
|
||||
match self.mapping.get(&key) {
|
||||
Some(position) => {
|
||||
self.values.set(*position, value);
|
||||
|
||||
/// Insert a new `V` in the collection.
|
||||
///
|
||||
/// If the `V` value already exists, it will be updated to the new one.
|
||||
pub(crate) fn insert(&mut self, key: K, value: V) -> usize {
|
||||
match self.mapping.get(&key) {
|
||||
Some(position) => {
|
||||
self.values.set(*position, value);
|
||||
*position
|
||||
}
|
||||
None => {
|
||||
let position = self.values.len();
|
||||
|
||||
*position
|
||||
}
|
||||
None => {
|
||||
let position = self.values.len();
|
||||
self.values.push_back(value);
|
||||
self.mapping.insert(key, position);
|
||||
|
||||
self.values.push_back(value);
|
||||
self.mapping.insert(key, position);
|
||||
|
||||
position
|
||||
}
|
||||
position
|
||||
}
|
||||
}
|
||||
|
||||
/// Reading one `V` value based on their ID, if it exists.
|
||||
pub(crate) fn get<L>(&self, key: &L) -> Option<&V>
|
||||
where
|
||||
K: Borrow<L>,
|
||||
L: Hash + Eq + ?Sized,
|
||||
{
|
||||
self.mapping.get(key).and_then(|position| self.values.get(*position))
|
||||
}
|
||||
|
||||
/// Reading one `V` value based on their ID, or create a new one (by
|
||||
/// using `default`).
|
||||
pub(crate) fn get_or_create<L, F>(&mut self, key: &L, default: F) -> &V
|
||||
where
|
||||
K: Borrow<L>,
|
||||
L: Hash + Eq + ?Sized + ToOwned<Owned = K>,
|
||||
F: FnOnce() -> V,
|
||||
{
|
||||
let position = match self.mapping.get(key) {
|
||||
Some(position) => *position,
|
||||
None => {
|
||||
let value = default();
|
||||
let position = self.values.len();
|
||||
|
||||
self.values.push_back(value);
|
||||
self.mapping.insert(key.to_owned(), position);
|
||||
|
||||
position
|
||||
}
|
||||
};
|
||||
|
||||
self.values
|
||||
.get(position)
|
||||
.expect("Value should be present or has just been inserted, but it's missing")
|
||||
}
|
||||
|
||||
/// Return an iterator over the existing values.
|
||||
pub(crate) fn iter(&self) -> impl Iterator<Item = &V> {
|
||||
self.values.iter()
|
||||
}
|
||||
|
||||
/// Get a [`Stream`] of the values.
|
||||
pub(crate) fn stream(&self) -> (Vector<V>, impl Stream<Item = Vec<VectorDiff<V>>>) {
|
||||
self.values.subscribe().into_values_and_batched_stream()
|
||||
}
|
||||
|
||||
/// Remove a `V` value based on their ID, if it exists.
|
||||
///
|
||||
/// Returns the removed value.
|
||||
pub(crate) fn remove<L>(&mut self, key: &L) -> Option<V>
|
||||
where
|
||||
K: Borrow<L>,
|
||||
L: Hash + Eq + ?Sized,
|
||||
{
|
||||
let position = self.mapping.remove(key)?;
|
||||
Some(self.values.remove(position))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod impl_wasm32 {
|
||||
use std::{borrow::Borrow, collections::BTreeMap, hash::Hash};
|
||||
|
||||
/// An observable map for Wasm. It's a simple wrapper around `BTreeMap`.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ObservableMap<K, V>(BTreeMap<K, V>)
|
||||
/// Reading one `V` value based on their ID, if it exists.
|
||||
pub(crate) fn get<L>(&self, key: &L) -> Option<&V>
|
||||
where
|
||||
V: Clone + 'static;
|
||||
|
||||
impl<K, V> ObservableMap<K, V>
|
||||
where
|
||||
K: Hash + Eq + Ord,
|
||||
V: Clone + 'static,
|
||||
K: Borrow<L>,
|
||||
L: Hash + Eq + ?Sized,
|
||||
{
|
||||
/// Create a new `Self`.
|
||||
pub(crate) fn new() -> Self {
|
||||
Self(BTreeMap::new())
|
||||
}
|
||||
self.mapping.get(key).and_then(|position| self.values.get(*position))
|
||||
}
|
||||
|
||||
/// Insert a new `V` in the collection.
|
||||
///
|
||||
/// If the `V` value already exists, it will be updated to the new one.
|
||||
pub(crate) fn insert(&mut self, key: K, value: V) {
|
||||
self.0.insert(key, value);
|
||||
}
|
||||
/// Reading one `V` value based on their ID, or create a new one (by
|
||||
/// using `default`).
|
||||
pub(crate) fn get_or_create<L, F>(&mut self, key: &L, default: F) -> &V
|
||||
where
|
||||
K: Borrow<L>,
|
||||
L: Hash + Eq + ?Sized + ToOwned<Owned = K>,
|
||||
F: FnOnce() -> V,
|
||||
{
|
||||
let position = match self.mapping.get(key) {
|
||||
Some(position) => *position,
|
||||
None => {
|
||||
let value = default();
|
||||
let position = self.values.len();
|
||||
|
||||
/// Reading one `V` value based on their ID, if it exists.
|
||||
pub(crate) fn get<L>(&self, key: &L) -> Option<&V>
|
||||
where
|
||||
K: Borrow<L>,
|
||||
L: Hash + Eq + Ord + ?Sized,
|
||||
{
|
||||
self.0.get(key)
|
||||
}
|
||||
self.values.push_back(value);
|
||||
self.mapping.insert(key.to_owned(), position);
|
||||
|
||||
/// Reading one `V` value based on their ID, or create a new one (by
|
||||
/// using `default`).
|
||||
pub(crate) fn get_or_create<L, F>(&mut self, key: &L, default: F) -> &V
|
||||
where
|
||||
K: Borrow<L>,
|
||||
L: Hash + Eq + ?Sized + ToOwned<Owned = K>,
|
||||
F: FnOnce() -> V,
|
||||
{
|
||||
self.0.entry(key.to_owned()).or_insert_with(default)
|
||||
}
|
||||
position
|
||||
}
|
||||
};
|
||||
|
||||
/// Return an iterator over the existing values.
|
||||
pub(crate) fn iter(&self) -> impl Iterator<Item = &V> {
|
||||
self.0.values()
|
||||
}
|
||||
self.values
|
||||
.get(position)
|
||||
.expect("Value should be present or has just been inserted, but it's missing")
|
||||
}
|
||||
|
||||
/// Remove a `V` value based on their ID, if it exists.
|
||||
///
|
||||
/// Returns the removed value.
|
||||
pub(crate) fn remove<L>(&mut self, key: &L) -> Option<V>
|
||||
where
|
||||
K: Borrow<L>,
|
||||
L: Hash + Eq + Ord + ?Sized,
|
||||
{
|
||||
self.0.remove(key)
|
||||
}
|
||||
/// Return an iterator over the existing values.
|
||||
pub(crate) fn iter(&self) -> impl Iterator<Item = &V> {
|
||||
self.values.iter()
|
||||
}
|
||||
|
||||
/// Get a [`Stream`] of the values.
|
||||
pub(crate) fn stream(&self) -> (Vector<V>, impl Stream<Item = Vec<VectorDiff<V>>>) {
|
||||
self.values.subscribe().into_values_and_batched_stream()
|
||||
}
|
||||
|
||||
/// Remove a `V` value based on their ID, if it exists.
|
||||
///
|
||||
/// Returns the removed value.
|
||||
pub(crate) fn remove<L>(&mut self, key: &L) -> Option<V>
|
||||
where
|
||||
K: Borrow<L>,
|
||||
L: Hash + Eq + ?Sized,
|
||||
{
|
||||
let position = self.mapping.remove(key)?;
|
||||
Some(self.values.remove(position))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) use impl_non_wasm32::ObservableMap;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) use impl_wasm32::ObservableMap;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use eyeball_im::VectorDiff;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use stream_assert::{assert_closed, assert_next_eq, assert_pending};
|
||||
|
||||
use super::ObservableMap;
|
||||
@@ -314,7 +239,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn test_stream() {
|
||||
let mut map = ObservableMap::<char, char>::new();
|
||||
|
||||
@@ -125,6 +125,12 @@ pub struct QueuedRequest {
|
||||
///
|
||||
/// `None` if the request is in the queue, waiting to be sent.
|
||||
pub error: Option<QueueWedgeError>,
|
||||
|
||||
/// At which priority should this be handled?
|
||||
///
|
||||
/// The bigger the value, the higher the priority at which this request
|
||||
/// should be handled.
|
||||
pub priority: usize,
|
||||
}
|
||||
|
||||
impl QueuedRequest {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
fmt,
|
||||
sync::Arc,
|
||||
};
|
||||
@@ -46,7 +46,9 @@ use super::{
|
||||
StoreError,
|
||||
};
|
||||
use crate::{
|
||||
deserialized_responses::{RawAnySyncOrStrippedState, RawMemberEvent, RawSyncOrStrippedState},
|
||||
deserialized_responses::{
|
||||
DisplayName, RawAnySyncOrStrippedState, RawMemberEvent, RawSyncOrStrippedState,
|
||||
},
|
||||
MinimalRoomMemberEvent, RoomInfo, RoomMemberships,
|
||||
};
|
||||
|
||||
@@ -206,7 +208,7 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
async fn get_users_with_display_name(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
display_name: &str,
|
||||
display_name: &DisplayName,
|
||||
) -> Result<BTreeSet<OwnedUserId>, Self::Error>;
|
||||
|
||||
/// Get all the users that use the given display names in the given room.
|
||||
@@ -219,8 +221,8 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
async fn get_users_with_display_names<'a>(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
display_names: &'a [String],
|
||||
) -> Result<BTreeMap<&'a str, BTreeSet<OwnedUserId>>, Self::Error>;
|
||||
display_names: &'a [DisplayName],
|
||||
) -> Result<HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>, Self::Error>;
|
||||
|
||||
/// Get an event out of the account data store.
|
||||
///
|
||||
@@ -358,6 +360,7 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
room_id: &RoomId,
|
||||
transaction_id: OwnedTransactionId,
|
||||
request: QueuedRequestKind,
|
||||
priority: usize,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Updates a send queue request with the given content, and resets its
|
||||
@@ -390,6 +393,10 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
) -> Result<bool, Self::Error>;
|
||||
|
||||
/// Loads all the send queue requests for the given room.
|
||||
///
|
||||
/// The resulting vector of queued requests should be ordered from higher
|
||||
/// priority to lower priority, and respect the insertion order when
|
||||
/// priorities are equal.
|
||||
async fn load_send_queue_requests(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
@@ -562,7 +569,7 @@ impl<T: StateStore> StateStore for EraseStateStoreError<T> {
|
||||
async fn get_users_with_display_name(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
display_name: &str,
|
||||
display_name: &DisplayName,
|
||||
) -> Result<BTreeSet<OwnedUserId>, Self::Error> {
|
||||
self.0.get_users_with_display_name(room_id, display_name).await.map_err(Into::into)
|
||||
}
|
||||
@@ -570,8 +577,8 @@ impl<T: StateStore> StateStore for EraseStateStoreError<T> {
|
||||
async fn get_users_with_display_names<'a>(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
display_names: &'a [String],
|
||||
) -> Result<BTreeMap<&'a str, BTreeSet<OwnedUserId>>, Self::Error> {
|
||||
display_names: &'a [DisplayName],
|
||||
) -> Result<HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>, Self::Error> {
|
||||
self.0.get_users_with_display_names(room_id, display_names).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
@@ -641,8 +648,12 @@ impl<T: StateStore> StateStore for EraseStateStoreError<T> {
|
||||
room_id: &RoomId,
|
||||
transaction_id: OwnedTransactionId,
|
||||
content: QueuedRequestKind,
|
||||
priority: usize,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.save_send_queue_request(room_id, transaction_id, content).await.map_err(Into::into)
|
||||
self.0
|
||||
.save_send_queue_request(room_id, transaction_id, content, priority)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn update_send_queue_request(
|
||||
|
||||
@@ -18,12 +18,14 @@
|
||||
|
||||
use ruma::{owned_user_id, UserId};
|
||||
|
||||
use crate::{BaseClient, SessionMeta};
|
||||
use crate::{store::StoreConfig, BaseClient, SessionMeta};
|
||||
|
||||
/// Create a [`BaseClient`] with the given user id, if provided, or an hardcoded
|
||||
/// one otherwise.
|
||||
pub(crate) async fn logged_in_base_client(user_id: Option<&UserId>) -> BaseClient {
|
||||
let client = BaseClient::new();
|
||||
let client = BaseClient::with_store_config(StoreConfig::new(
|
||||
"cross-process-store-locks-holder-name".to_owned(),
|
||||
));
|
||||
let user_id =
|
||||
user_id.map(|user_id| user_id.to_owned()).unwrap_or_else(|| owned_user_id!("@u:e.uk"));
|
||||
client
|
||||
|
||||
@@ -2,3 +2,10 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.8.0] - 2024-11-19
|
||||
|
||||
### Refactor
|
||||
|
||||
- Move `linked_chunk` from `matrix-sdk` to `matrix-sdk-common`.
|
||||
|
||||
|
||||
|
||||
@@ -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.7.0"
|
||||
version = "0.8.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
default-target = "x86_64-unknown-linux-gnu"
|
||||
@@ -21,7 +21,10 @@ uniffi = ["dep:uniffi"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
eyeball-im = { workspace = true }
|
||||
futures-core = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
imbl = { workspace = true }
|
||||
ruma = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -343,7 +343,7 @@ impl SyncTimelineEvent {
|
||||
/// Get the event id of this `SyncTimelineEvent` if the event has any valid
|
||||
/// id.
|
||||
pub fn event_id(&self) -> Option<OwnedEventId> {
|
||||
self.kind.raw().get_field::<OwnedEventId>("event_id").ok().flatten()
|
||||
self.kind.event_id()
|
||||
}
|
||||
|
||||
/// Returns a reference to the (potentially decrypted) Matrix event inside
|
||||
@@ -529,6 +529,12 @@ impl TimelineEventKind {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the event id of this `TimelineEventKind` if the event has any valid
|
||||
/// id.
|
||||
pub fn event_id(&self) -> Option<OwnedEventId> {
|
||||
self.raw().get_field::<OwnedEventId>("event_id").ok().flatten()
|
||||
}
|
||||
|
||||
/// If the event was a decrypted event that was successfully decrypted, get
|
||||
/// its encryption info. Otherwise, `None`.
|
||||
pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
|
||||
|
||||
@@ -25,6 +25,7 @@ pub mod debug;
|
||||
pub mod deserialized_responses;
|
||||
pub mod executor;
|
||||
pub mod failures_cache;
|
||||
pub mod linked_chunk;
|
||||
pub mod ring_buffer;
|
||||
pub mod store_locks;
|
||||
pub mod timeout;
|
||||
|
||||
+5
-4
@@ -13,6 +13,7 @@
|
||||
// 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.
|
||||
|
||||
@@ -56,7 +57,7 @@ macro_rules! assert_items_eq {
|
||||
let chunk = $iterator .next().expect("next chunk (expect items)");
|
||||
assert!(chunk.is_items(), "chunk should contain items");
|
||||
|
||||
let $crate::event_cache::linked_chunk::ChunkContent::Items(items) = chunk.content() else {
|
||||
let $crate::linked_chunk::ChunkContent::Items(items) = chunk.content() else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
@@ -934,7 +935,6 @@ impl ChunkIdentifierGenerator {
|
||||
#[repr(transparent)]
|
||||
pub struct ChunkIdentifier(u64);
|
||||
|
||||
#[cfg(test)]
|
||||
impl PartialEq<u64> for ChunkIdentifier {
|
||||
fn eq(&self, other: &u64) -> bool {
|
||||
self.0 == *other
|
||||
@@ -963,7 +963,7 @@ impl Position {
|
||||
/// # Panic
|
||||
///
|
||||
/// This method will panic if it will underflow, i.e. if the index is 0.
|
||||
pub(super) fn decrement_index(&mut self) {
|
||||
pub fn decrement_index(&mut self) {
|
||||
self.1 = self.1.checked_sub(1).expect("Cannot decrement the index because it's already 0");
|
||||
}
|
||||
}
|
||||
@@ -1346,7 +1346,8 @@ where
|
||||
}
|
||||
|
||||
/// A type representing what to do when the system has to handle an empty chunk.
|
||||
pub(crate) enum EmptyChunk {
|
||||
#[derive(Debug)]
|
||||
pub enum EmptyChunk {
|
||||
/// Keep the empty chunk.
|
||||
Keep,
|
||||
|
||||
@@ -1,6 +1,70 @@
|
||||
# UNRELEASED
|
||||
# Changelog
|
||||
|
||||
Changes:
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.8.0] - 2024-11-19
|
||||
|
||||
### Features
|
||||
|
||||
- Pin identity when we withdraw verification.
|
||||
|
||||
- Expose new method `OlmMachine::room_keys_withheld_received_stream`, to allow
|
||||
applications to receive notifications about received `m.room_key.withheld`
|
||||
events.
|
||||
([#3660](https://github.com/matrix-org/matrix-rust-sdk/pull/3660)),
|
||||
([#3674](https://github.com/matrix-org/matrix-rust-sdk/pull/3674))
|
||||
|
||||
- Expose new method `OlmMachine::clear_crypto_cache()`, with FFI bindings.
|
||||
([#3462](https://github.com/matrix-org/matrix-rust-sdk/pull/3462))
|
||||
|
||||
- Expose new method `OlmMachine::upload_device_keys()`.
|
||||
([#3457](https://github.com/matrix-org/matrix-rust-sdk/pull/3457))
|
||||
|
||||
- Expose new method `CryptoStore::import_room_keys`.
|
||||
([#3448](https://github.com/matrix-org/matrix-rust-sdk/pull/3448))
|
||||
|
||||
- Expose new method `BackupMachine::backup_version`.
|
||||
([#3320](https://github.com/matrix-org/matrix-rust-sdk/pull/3320))
|
||||
|
||||
- Add data types to parse the QR code data for the QR code login defined in.
|
||||
[MSC4108](https://github.com/matrix-org/matrix-spec-proposals/pull/4108)
|
||||
|
||||
- Expose new method `CryptoStore::clear_caches`.
|
||||
([#3338](https://github.com/matrix-org/matrix-rust-sdk/pull/3338))
|
||||
|
||||
- Expose new method `OlmMachine::device_creation_time`.
|
||||
([#3275](https://github.com/matrix-org/matrix-rust-sdk/pull/3275))
|
||||
|
||||
- Log more details about the Olm session after encryption and decryption.
|
||||
([#3242](https://github.com/matrix-org/matrix-rust-sdk/pull/3242))
|
||||
|
||||
- When Olm message decryption fails, report the error code(s) from the failure.
|
||||
([#3212](https://github.com/matrix-org/matrix-rust-sdk/pull/3212))
|
||||
|
||||
- Expose new methods `OlmMachine::set_room_settings` and
|
||||
`OlmMachine::get_room_settings`.
|
||||
([#3042](https://github.com/matrix-org/matrix-rust-sdk/pull/3042))
|
||||
|
||||
- Add new properties `session_rotation_period` and
|
||||
`session_rotation_period_msgs` to `store::RoomSettings`.
|
||||
([#3042](https://github.com/matrix-org/matrix-rust-sdk/pull/3042))
|
||||
|
||||
- Fix bug which caused `SecretStorageKey` to incorrectly reject secret storage
|
||||
keys whose metadata lacked check fields.
|
||||
([#3046](https://github.com/matrix-org/matrix-rust-sdk/pull/3046))
|
||||
|
||||
- Add new API `Device::encrypt_event_raw` that allows
|
||||
to encrypt an event to a specific device.
|
||||
([#3091](https://github.com/matrix-org/matrix-rust-sdk/pull/3091))
|
||||
|
||||
- Add new API `store::Store::export_room_keys_stream` that provides room
|
||||
keys on demand.
|
||||
|
||||
- Include event timestamps on logs from event decryption.
|
||||
([#3194](https://github.com/matrix-org/matrix-rust-sdk/pull/3194))
|
||||
|
||||
|
||||
### Refactor
|
||||
|
||||
- Add new method `OlmMachine::try_decrypt_room_event`.
|
||||
([#4116](https://github.com/matrix-org/matrix-rust-sdk/pull/4116))
|
||||
@@ -8,14 +72,14 @@ Changes:
|
||||
- Add reason code to `matrix_sdk_common::deserialized_responses::UnableToDecryptInfo`.
|
||||
([#4116](https://github.com/matrix-org/matrix-rust-sdk/pull/4116))
|
||||
|
||||
- The `UserIdentity` struct has been renamed to `OtherUserIdentity`
|
||||
- [**breaking**] The `UserIdentity` struct has been renamed to `OtherUserIdentity`.
|
||||
([#4036](https://github.com/matrix-org/matrix-rust-sdk/pull/4036]))
|
||||
|
||||
- The `UserIdentities` enum has been renamed to `UserIdentity`
|
||||
- [**breaking**] The `UserIdentities` enum has been renamed to `UserIdentity`.
|
||||
([#4036](https://github.com/matrix-org/matrix-rust-sdk/pull/4036]))
|
||||
|
||||
- Change the withheld code for keys not shared due to the `IdentityBasedStrategy`, from `m.unauthorised`
|
||||
to `m.unverified`.
|
||||
- Change the withheld code for keys not shared due to the
|
||||
`IdentityBasedStrategy`, from `m.unauthorised` to `m.unverified`.
|
||||
([#3985](https://github.com/matrix-org/matrix-rust-sdk/pull/3985))
|
||||
|
||||
- Improve logging for undecryptable Megolm events.
|
||||
@@ -59,47 +123,47 @@ Changes:
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- `VerificationRequestState::Transitioned` now includes a new field
|
||||
- [**breaking**] `VerificationRequestState::Transitioned` now includes a new field
|
||||
`other_device_data` of type `DeviceData`.
|
||||
([#4153](https://github.com/matrix-org/matrix-rust-sdk/pull/4153))
|
||||
|
||||
- `OlmMachine::decrypt_room_event` now returns a `DecryptedRoomEvent` type,
|
||||
- [**breaking**] `OlmMachine::decrypt_room_event` now returns a `DecryptedRoomEvent` type,
|
||||
instead of the more generic `TimelineEvent` type.
|
||||
|
||||
- **NOTE**: this version causes changes to the format of the serialised data in
|
||||
- [**breaking**] **NOTE**: this version causes changes to the format of the serialised data in
|
||||
the CryptoStore, meaning that, once upgraded, it will not be possible to roll
|
||||
back applications to earlier versions without breaking user sessions.
|
||||
|
||||
- Renamed `VerificationLevel::PreviouslyVerified` to
|
||||
- [**breaking**] Renamed `VerificationLevel::PreviouslyVerified` to
|
||||
`VerificationLevel::VerificationViolation`.
|
||||
|
||||
- `OlmMachine::decrypt_room_event` now takes a `DecryptionSettings` argument,
|
||||
which includes a `TrustRequirement` indicating the required trust level for
|
||||
the sending device. When it is called with `TrustRequirement` other than
|
||||
`TrustRequirement::Unverified`, it may return the new
|
||||
`MegolmError::SenderIdentityNotTrusted` variant if the sending device does not
|
||||
satisfy the required trust level.
|
||||
- [**breaking**] `OlmMachine::decrypt_room_event` now takes a
|
||||
`DecryptionSettings` argument, which includes a `TrustRequirement` indicating
|
||||
the required trust level for the sending device. When it is called with
|
||||
`TrustRequirement` other than `TrustRequirement::Unverified`, it may return
|
||||
the new `MegolmError::SenderIdentityNotTrusted` variant if the sending device
|
||||
does not satisfy the required trust level.
|
||||
([#3899](https://github.com/matrix-org/matrix-rust-sdk/pull/3899))
|
||||
|
||||
- Change the structure of the `SenderData` enum to separate variants for
|
||||
previously-verified, unverified and verified.
|
||||
- [**breaking**] Change the structure of the `SenderData` enum to separate
|
||||
variants for previously-verified, unverified and verified.
|
||||
([#3877](https://github.com/matrix-org/matrix-rust-sdk/pull/3877))
|
||||
|
||||
- Where `EncryptionInfo` is returned it may include the new `PreviouslyVerified`
|
||||
variant of `VerificationLevel` to indicate that the user was previously
|
||||
verified and is no longer verified.
|
||||
- [**breaking**] Where `EncryptionInfo` is returned it may include the new
|
||||
`PreviouslyVerified` variant of `VerificationLevel` to indicate that the user
|
||||
was previously verified and is no longer verified.
|
||||
([#3877](https://github.com/matrix-org/matrix-rust-sdk/pull/3877))
|
||||
|
||||
- Expose new methods `OwnUserIdentity::was_previously_verified`,
|
||||
- [**breaking**] Expose new methods `OwnUserIdentity::was_previously_verified`,
|
||||
`OwnUserIdentity::withdraw_verification`, and
|
||||
`OwnUserIdentity::has_verification_violation`, which track whether our own
|
||||
identity was previously verified.
|
||||
([#3846](https://github.com/matrix-org/matrix-rust-sdk/pull/3846))
|
||||
|
||||
- Add a new `error_on_verified_user_problem` property to
|
||||
- [**breaking**] Add a new `error_on_verified_user_problem` property to
|
||||
`CollectStrategy::DeviceBasedStrategy`, which, when set, causes
|
||||
`OlmMachine::share_room_key` to fail with an error if any verified users on
|
||||
the recipient list have unsigned devices, or are no lonver verified.
|
||||
the recipient list have unsigned devices, or are no longer verified.
|
||||
|
||||
When `CallectStrategy::IdentityBasedStrategy` is used,
|
||||
`OlmMachine::share_room_key` will fail with an error if any verified users on
|
||||
@@ -109,103 +173,43 @@ Breaking changes:
|
||||
Also remove `CollectStrategy::new_device_based`: callers should construct a
|
||||
`CollectStrategy::DeviceBasedStrategy` directly.
|
||||
|
||||
`EncryptionSettings::new` now takes a `CollectStrategy` argument, instead of
|
||||
a list of booleans.
|
||||
`EncryptionSettings::new` now takes a `CollectStrategy` argument, instead of a
|
||||
list of booleans.
|
||||
([#3810](https://github.com/matrix-org/matrix-rust-sdk/pull/3810))
|
||||
([#3816](https://github.com/matrix-org/matrix-rust-sdk/pull/3816))
|
||||
([#3896](https://github.com/matrix-org/matrix-rust-sdk/pull/3896))
|
||||
|
||||
- Remove the method `OlmMachine::clear_crypto_cache()`, crypto stores are not
|
||||
supposed to have any caches anymore.
|
||||
- [**breaking**] Remove the method `OlmMachine::clear_crypto_cache()`, crypto
|
||||
stores are not supposed to have any caches anymore.
|
||||
|
||||
- Add a `custom_account` argument to the `OlmMachine::with_store()` method, this
|
||||
allows users to learn their identity keys before they get access to the user
|
||||
and device ID.
|
||||
- [**breaking**] Add a `custom_account` argument to the
|
||||
`OlmMachine::with_store()` method, this allows users to learn their identity
|
||||
keys before they get access to the user and device ID.
|
||||
([#3451](https://github.com/matrix-org/matrix-rust-sdk/pull/3451))
|
||||
|
||||
- Add a `backup_version` argument to `CryptoStore`'s
|
||||
- [**breaking**] Add a `backup_version` argument to `CryptoStore`'s
|
||||
`inbound_group_sessions_for_backup`,
|
||||
`mark_inbound_group_sessions_as_backed_up` and
|
||||
`inbound_group_session_counts` methods.
|
||||
([#3253](https://github.com/matrix-org/matrix-rust-sdk/pull/3253))
|
||||
`mark_inbound_group_sessions_as_backed_up` and `inbound_group_session_counts`
|
||||
methods. ([#3253](https://github.com/matrix-org/matrix-rust-sdk/pull/3253))
|
||||
|
||||
- Rename the `OlmMachine::invalidate_group_session` method to
|
||||
`OlmMachine::discard_room_key`
|
||||
- [**breaking**] Rename the `OlmMachine::invalidate_group_session` method to
|
||||
`OlmMachine::discard_room_key`.
|
||||
|
||||
- Move `OlmMachine::export_room_keys` to `matrix_sdk_crypto::store::Store`.
|
||||
- [**breaking**] Move `OlmMachine::export_room_keys` to `matrix_sdk_crypto::store::Store`.
|
||||
(Call it with `olm_machine.store().export_room_keys(...)`.)
|
||||
|
||||
- Add new `dehydrated` property to `olm::account::PickledAccount`.
|
||||
- [**breaking**] Add new `dehydrated` property to `olm::account::PickledAccount`.
|
||||
([#3164](https://github.com/matrix-org/matrix-rust-sdk/pull/3164))
|
||||
|
||||
- Remove deprecated `OlmMachine::import_room_keys`.
|
||||
- [**breaking**] Remove deprecated `OlmMachine::import_room_keys`.
|
||||
([#3448](https://github.com/matrix-org/matrix-rust-sdk/pull/3448))
|
||||
|
||||
- Add the `SasState::Created` variant to differentiate the state between the
|
||||
- [**breaking**] Add the `SasState::Created` variant to differentiate the state between the
|
||||
party that sent the verification start and the party that received it.
|
||||
|
||||
Deprecations:
|
||||
|
||||
- Deprecate `BackupMachine::import_backed_up_room_keys`.
|
||||
- [**breaking**] Deprecate `BackupMachine::import_backed_up_room_keys`.
|
||||
([#3448](https://github.com/matrix-org/matrix-rust-sdk/pull/3448))
|
||||
|
||||
Additions:
|
||||
|
||||
- Expose new method `OlmMachine::room_keys_withheld_received_stream`, to allow
|
||||
applications to receive notifications about received `m.room_key.withheld`
|
||||
events.
|
||||
([#3660](https://github.com/matrix-org/matrix-rust-sdk/pull/3660)),
|
||||
([#3674](https://github.com/matrix-org/matrix-rust-sdk/pull/3674))
|
||||
|
||||
- Expose new method `OlmMachine::clear_crypto_cache()`, with FFI bindings
|
||||
([#3462](https://github.com/matrix-org/matrix-rust-sdk/pull/3462))
|
||||
|
||||
- Expose new method `OlmMachine::upload_device_keys()`.
|
||||
([#3457](https://github.com/matrix-org/matrix-rust-sdk/pull/3457))
|
||||
|
||||
- Expose new method `CryptoStore::import_room_keys`.
|
||||
([#3448](https://github.com/matrix-org/matrix-rust-sdk/pull/3448))
|
||||
|
||||
- Expose new method `BackupMachine::backup_version`.
|
||||
([#3320](https://github.com/matrix-org/matrix-rust-sdk/pull/3320))
|
||||
|
||||
- Add data types to parse the QR code data for the QR code login defined in
|
||||
[MSC4108](https://github.com/matrix-org/matrix-spec-proposals/pull/4108)
|
||||
|
||||
- Expose new method `CryptoStore::clear_caches`.
|
||||
([#3338](https://github.com/matrix-org/matrix-rust-sdk/pull/3338))
|
||||
|
||||
- Expose new method `OlmMachine::device_creation_time`.
|
||||
([#3275](https://github.com/matrix-org/matrix-rust-sdk/pull/3275))
|
||||
|
||||
- Log more details about the Olm session after encryption and decryption.
|
||||
([#3242](https://github.com/matrix-org/matrix-rust-sdk/pull/3242))
|
||||
|
||||
- When Olm message decryption fails, report the error code(s) from the failure.
|
||||
([#3212](https://github.com/matrix-org/matrix-rust-sdk/pull/3212))
|
||||
|
||||
- Expose new methods `OlmMachine::set_room_settings` and
|
||||
`OlmMachine::get_room_settings`.
|
||||
([#3042](https://github.com/matrix-org/matrix-rust-sdk/pull/3042))
|
||||
|
||||
- Add new properties `session_rotation_period` and
|
||||
`session_rotation_period_msgs` to `store::RoomSettings`.
|
||||
([#3042](https://github.com/matrix-org/matrix-rust-sdk/pull/3042))
|
||||
|
||||
- Fix bug which caused `SecretStorageKey` to incorrectly reject secret storage
|
||||
keys whose metadata lacked check fields.
|
||||
([#3046](https://github.com/matrix-org/matrix-rust-sdk/pull/3046))
|
||||
|
||||
- Add new API `Device::encrypt_event_raw` that allows
|
||||
to encrypt an event to a specific device.
|
||||
([#3091](https://github.com/matrix-org/matrix-rust-sdk/pull/3091))
|
||||
|
||||
- Add new API `store::Store::export_room_keys_stream` that provides room
|
||||
keys on demand.
|
||||
|
||||
- Include event timestamps on logs from event decryption.
|
||||
([#3194](https://github.com/matrix-org/matrix-rust-sdk/pull/3194))
|
||||
|
||||
|
||||
# 0.7.2
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ name = "matrix-sdk-crypto"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = { workspace = true }
|
||||
version = "0.7.2"
|
||||
version = "0.8.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
@@ -803,6 +803,9 @@ impl OtherUserIdentityData {
|
||||
/// reported to the user. In order to remove this notice users have to
|
||||
/// verify again or to withdraw the verification requirement.
|
||||
pub fn withdraw_verification(&self) {
|
||||
// We also pin when we withdraw, since withdrawing implicitly acknowledges
|
||||
// the identity change
|
||||
self.pin();
|
||||
self.previously_verified.store(false, Ordering::SeqCst)
|
||||
}
|
||||
|
||||
@@ -1770,6 +1773,45 @@ pub(crate) mod tests {
|
||||
assert!(other_identity.inner.has_pin_violation());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_resolve_identity_pin_violation_with_withdraw_verification() {
|
||||
use test_json::keys_query_sets::IdentityChangeDataSet as DataSet;
|
||||
|
||||
let my_user_id = user_id!("@me:localhost");
|
||||
let machine = OlmMachine::new(my_user_id, device_id!("ABCDEFGH")).await;
|
||||
machine.bootstrap_cross_signing(false).await.unwrap();
|
||||
|
||||
let keys_query = DataSet::key_query_with_identity_a();
|
||||
let txn_id = TransactionId::new();
|
||||
machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap();
|
||||
|
||||
// Simulate an identity change
|
||||
let keys_query = DataSet::key_query_with_identity_b();
|
||||
let txn_id = TransactionId::new();
|
||||
machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap();
|
||||
|
||||
let other_user_id = DataSet::user_id();
|
||||
|
||||
let other_identity =
|
||||
machine.get_identity(other_user_id, None).await.unwrap().unwrap().other().unwrap();
|
||||
|
||||
// For testing purpose mark it as previously verified
|
||||
other_identity.mark_as_previously_verified().await.unwrap();
|
||||
|
||||
// The identity should need user approval now
|
||||
assert!(other_identity.identity_needs_user_approval());
|
||||
|
||||
// We withdraw verification
|
||||
other_identity.withdraw_verification().await.unwrap();
|
||||
|
||||
// The identity should not need any user approval now
|
||||
let other_identity =
|
||||
machine.get_identity(other_user_id, None).await.unwrap().unwrap().other().unwrap();
|
||||
assert!(!other_identity.identity_needs_user_approval());
|
||||
// And should not have a pin violation
|
||||
assert!(!other_identity.inner.has_pin_violation());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_resolve_identity_verification_violation_with_withdraw() {
|
||||
use test_json::keys_query_sets::VerificationViolationTestData as DataSet;
|
||||
|
||||
@@ -737,7 +737,7 @@ impl OlmMachine {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get the a key claiming request for the user/device pairs that we are
|
||||
/// Get a key claiming request for the user/device pairs that we are
|
||||
/// missing Olm sessions for.
|
||||
///
|
||||
/// Returns None if no key claiming request needs to be sent out.
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
# UNRELEASED
|
||||
## [0.8.0] - 2024-11-19
|
||||
|
||||
### Features
|
||||
|
||||
- Improve the efficiency of objects stored in the crypto store.
|
||||
([#3645](https://github.com/matrix-org/matrix-rust-sdk/pull/3645), [#3651](https://github.com/matrix-org/matrix-rust-sdk/pull/3651))
|
||||
@@ -11,3 +13,6 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
- `save_change` performance improvement, all encryption and serialization
|
||||
is done now outside of the db transaction.
|
||||
### Bug Fixes
|
||||
|
||||
- Use the `DisplayName` struct to protect against homoglyph attacks.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "matrix-sdk-indexeddb"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
description = "Web's IndexedDB Storage backend for matrix-sdk"
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashSet},
|
||||
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ use gloo_utils::format::JsValueSerdeExt;
|
||||
use growable_bloom_filter::GrowableBloom;
|
||||
use indexed_db_futures::prelude::*;
|
||||
use matrix_sdk_base::{
|
||||
deserialized_responses::RawAnySyncOrStrippedState,
|
||||
deserialized_responses::{DisplayName, RawAnySyncOrStrippedState},
|
||||
store::{
|
||||
ChildTransactionId, ComposerDraft, DependentQueuedRequest, DependentQueuedRequestKind,
|
||||
QueuedRequest, QueuedRequestKind, SentRequestKey, SerializableEventContent,
|
||||
@@ -437,6 +437,8 @@ struct PersistedQueuedRequest {
|
||||
|
||||
pub error: Option<QueueWedgeError>,
|
||||
|
||||
priority: Option<usize>,
|
||||
|
||||
// Migrated fields: keep these private, they're not used anymore elsewhere in the code base.
|
||||
/// Deprecated (from old format), now replaced with error field.
|
||||
is_wedged: Option<bool>,
|
||||
@@ -459,7 +461,10 @@ impl PersistedQueuedRequest {
|
||||
_ => self.error,
|
||||
};
|
||||
|
||||
Some(QueuedRequest { kind, transaction_id: self.transaction_id, error })
|
||||
// By default, events without a priority have a priority of 0.
|
||||
let priority = self.priority.unwrap_or(0);
|
||||
|
||||
Some(QueuedRequest { kind, transaction_id: self.transaction_id, error, priority })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -660,7 +665,15 @@ impl_state_store!({
|
||||
let store = tx.object_store(keys::DISPLAY_NAMES)?;
|
||||
for (room_id, ambiguity_maps) in &changes.ambiguity_maps {
|
||||
for (display_name, map) in ambiguity_maps {
|
||||
let key = self.encode_key(keys::DISPLAY_NAMES, (room_id, display_name));
|
||||
let key = self.encode_key(
|
||||
keys::DISPLAY_NAMES,
|
||||
(
|
||||
room_id,
|
||||
display_name
|
||||
.as_normalized_str()
|
||||
.unwrap_or_else(|| display_name.as_raw_str()),
|
||||
),
|
||||
);
|
||||
|
||||
store.put_key_val(&key, &self.serialize_value(&map)?)?;
|
||||
}
|
||||
@@ -1117,12 +1130,18 @@ impl_state_store!({
|
||||
async fn get_users_with_display_name(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
display_name: &str,
|
||||
display_name: &DisplayName,
|
||||
) -> Result<BTreeSet<OwnedUserId>> {
|
||||
self.inner
|
||||
.transaction_on_one_with_mode(keys::DISPLAY_NAMES, IdbTransactionMode::Readonly)?
|
||||
.object_store(keys::DISPLAY_NAMES)?
|
||||
.get(&self.encode_key(keys::DISPLAY_NAMES, (room_id, display_name)))?
|
||||
.get(&self.encode_key(
|
||||
keys::DISPLAY_NAMES,
|
||||
(
|
||||
room_id,
|
||||
display_name.as_normalized_str().unwrap_or_else(|| display_name.as_raw_str()),
|
||||
),
|
||||
))?
|
||||
.await?
|
||||
.map(|f| self.deserialize_value::<BTreeSet<OwnedUserId>>(&f))
|
||||
.unwrap_or_else(|| Ok(Default::default()))
|
||||
@@ -1131,10 +1150,12 @@ impl_state_store!({
|
||||
async fn get_users_with_display_names<'a>(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
display_names: &'a [String],
|
||||
) -> Result<BTreeMap<&'a str, BTreeSet<OwnedUserId>>> {
|
||||
display_names: &'a [DisplayName],
|
||||
) -> Result<HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>> {
|
||||
let mut map = HashMap::new();
|
||||
|
||||
if display_names.is_empty() {
|
||||
return Ok(BTreeMap::new());
|
||||
return Ok(map);
|
||||
}
|
||||
|
||||
let txn = self
|
||||
@@ -1142,15 +1163,24 @@ impl_state_store!({
|
||||
.transaction_on_one_with_mode(keys::DISPLAY_NAMES, IdbTransactionMode::Readonly)?;
|
||||
let store = txn.object_store(keys::DISPLAY_NAMES)?;
|
||||
|
||||
let mut map = BTreeMap::new();
|
||||
for display_name in display_names {
|
||||
if let Some(user_ids) = store
|
||||
.get(&self.encode_key(keys::DISPLAY_NAMES, (room_id, display_name)))?
|
||||
.get(
|
||||
&self.encode_key(
|
||||
keys::DISPLAY_NAMES,
|
||||
(
|
||||
room_id,
|
||||
display_name
|
||||
.as_normalized_str()
|
||||
.unwrap_or_else(|| display_name.as_raw_str()),
|
||||
),
|
||||
),
|
||||
)?
|
||||
.await?
|
||||
.map(|f| self.deserialize_value::<BTreeSet<OwnedUserId>>(&f))
|
||||
.transpose()?
|
||||
{
|
||||
map.insert(display_name.as_ref(), user_ids);
|
||||
map.insert(display_name, user_ids);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1329,6 +1359,7 @@ impl_state_store!({
|
||||
room_id: &RoomId,
|
||||
transaction_id: OwnedTransactionId,
|
||||
kind: QueuedRequestKind,
|
||||
priority: usize,
|
||||
) -> Result<()> {
|
||||
let encoded_key = self.encode_key(keys::ROOM_SEND_QUEUE, room_id);
|
||||
|
||||
@@ -1357,6 +1388,7 @@ impl_state_store!({
|
||||
error: None,
|
||||
is_wedged: None,
|
||||
event: None,
|
||||
priority: Some(priority),
|
||||
});
|
||||
|
||||
// Save the new vector into db.
|
||||
@@ -1460,11 +1492,14 @@ impl_state_store!({
|
||||
.get(&encoded_key)?
|
||||
.await?;
|
||||
|
||||
let prev = prev.map_or_else(
|
||||
let mut prev = prev.map_or_else(
|
||||
|| Ok(Vec::new()),
|
||||
|val| self.deserialize_value::<Vec<PersistedQueuedRequest>>(&val),
|
||||
)?;
|
||||
|
||||
// Inverted stable ordering on priority.
|
||||
prev.sort_by(|lhs, rhs| rhs.priority.unwrap_or(0).cmp(&lhs.priority.unwrap_or(0)));
|
||||
|
||||
Ok(prev.into_iter().filter_map(PersistedQueuedRequest::into_queued_request).collect())
|
||||
}
|
||||
|
||||
|
||||
@@ -2,3 +2,6 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.8.0] - 2024-11-19
|
||||
|
||||
No notable changes in this release.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "matrix-sdk-qrcode"
|
||||
description = "Library to encode and decode QR codes for interactive verifications in Matrix land"
|
||||
version = "0.7.1"
|
||||
version = "0.8.0"
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk>"]
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
|
||||
@@ -2,3 +2,15 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.8.0] - 2024-11-19
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Use the `DisplayName` struct to protect against homoglyph attacks.
|
||||
|
||||
|
||||
### Refactor
|
||||
|
||||
- Move `event_cache_store/` to `event_cache/store/` in `matrix-sdk-base`.
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "matrix-sdk-sqlite"
|
||||
version = "0.7.1"
|
||||
version = "0.8.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
description = "Sqlite storage backend for matrix-sdk"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Add a priority column, defaulting to 0 for all events in the send queue.
|
||||
ALTER TABLE "send_queue_events"
|
||||
ADD COLUMN "priority" INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
use deadpool_sqlite::{CreatePoolError, PoolError};
|
||||
#[cfg(feature = "event-cache")]
|
||||
use matrix_sdk_base::event_cache_store::EventCacheStoreError;
|
||||
use matrix_sdk_base::event_cache::store::EventCacheStoreError;
|
||||
#[cfg(feature = "state-store")]
|
||||
use matrix_sdk_base::store::StoreError as StateStoreError;
|
||||
#[cfg(feature = "crypto-store")]
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{borrow::Cow, fmt, path::Path, sync::Arc};
|
||||
use async_trait::async_trait;
|
||||
use deadpool_sqlite::{Object as SqliteAsyncConn, Pool as SqlitePool, Runtime};
|
||||
use matrix_sdk_base::{
|
||||
event_cache_store::EventCacheStore,
|
||||
event_cache::store::EventCacheStore,
|
||||
media::{MediaRequestParameters, UniqueKey},
|
||||
};
|
||||
use matrix_sdk_store_encryption::StoreCipher;
|
||||
@@ -279,7 +279,7 @@ mod tests {
|
||||
};
|
||||
|
||||
use matrix_sdk_base::{
|
||||
event_cache_store::{EventCacheStore, EventCacheStoreError},
|
||||
event_cache::store::{EventCacheStore, EventCacheStoreError},
|
||||
event_cache_store_integration_tests, event_cache_store_integration_tests_time,
|
||||
media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},
|
||||
};
|
||||
@@ -387,7 +387,7 @@ mod encrypted_tests {
|
||||
use std::sync::atomic::{AtomicU32, Ordering::SeqCst};
|
||||
|
||||
use matrix_sdk_base::{
|
||||
event_cache_store::EventCacheStoreError, event_cache_store_integration_tests,
|
||||
event_cache::store::EventCacheStoreError, event_cache_store_integration_tests,
|
||||
event_cache_store_integration_tests_time,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
fmt, iter,
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
@@ -9,7 +9,7 @@ use std::{
|
||||
use async_trait::async_trait;
|
||||
use deadpool_sqlite::{Object as SqliteAsyncConn, Pool as SqlitePool, Runtime};
|
||||
use matrix_sdk_base::{
|
||||
deserialized_responses::{RawAnySyncOrStrippedState, SyncOrStrippedState},
|
||||
deserialized_responses::{DisplayName, RawAnySyncOrStrippedState, SyncOrStrippedState},
|
||||
store::{
|
||||
migration_helpers::RoomInfoV1, ChildTransactionId, DependentQueuedRequest,
|
||||
DependentQueuedRequestKind, QueueWedgeError, QueuedRequest, QueuedRequestKind,
|
||||
@@ -69,7 +69,7 @@ mod keys {
|
||||
/// This is used to figure whether the sqlite database requires a migration.
|
||||
/// Every new SQL migration should imply a bump of this number, and changes in
|
||||
/// the [`SqliteStateStore::run_migrations`] function..
|
||||
const DATABASE_VERSION: u8 = 9;
|
||||
const DATABASE_VERSION: u8 = 10;
|
||||
|
||||
/// A sqlite based cryptostore.
|
||||
#[derive(Clone)]
|
||||
@@ -307,6 +307,17 @@ impl SqliteStateStore {
|
||||
.await?;
|
||||
}
|
||||
|
||||
if from < 10 && to >= 10 {
|
||||
conn.with_transaction(move |txn| {
|
||||
// Run the migration.
|
||||
txn.execute_batch(include_str!(
|
||||
"../migrations/state_store/009_send_queue_priority.sql"
|
||||
))?;
|
||||
txn.set_db_version(10)
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1294,13 +1305,34 @@ impl StateStore for SqliteStateStore {
|
||||
let room_id = this.encode_key(keys::DISPLAY_NAME, room_id);
|
||||
|
||||
for (name, user_ids) in display_names {
|
||||
let name = this.encode_key(keys::DISPLAY_NAME, name);
|
||||
let encoded_name = this.encode_key(
|
||||
keys::DISPLAY_NAME,
|
||||
name.as_normalized_str().unwrap_or_else(|| name.as_raw_str()),
|
||||
);
|
||||
let data = this.serialize_json(&user_ids)?;
|
||||
|
||||
if user_ids.is_empty() {
|
||||
txn.remove_display_name(&room_id, &name)?;
|
||||
txn.remove_display_name(&room_id, &encoded_name)?;
|
||||
|
||||
// We can't do a migration to merge the previously distinct buckets of
|
||||
// user IDs since the display names themselves are hashed before they
|
||||
// are persisted in the store. So the store will always retain two
|
||||
// buckets: one for raw display names and one for normalised ones.
|
||||
//
|
||||
// We therefore do the next best thing, which is a sort of a soft
|
||||
// migration: we fetch both the raw and normalised buckets, then merge
|
||||
// the user IDs contained in them into a separate, temporary merged
|
||||
// bucket. The SDK then operates on the merged buckets exclusively. See
|
||||
// the comment in `get_users_with_display_names` for details.
|
||||
//
|
||||
// If the merged bucket is empty, that must mean that both the raw and
|
||||
// normalised buckets were also empty, so we can remove both from the
|
||||
// store.
|
||||
let raw_name = this.encode_key(keys::DISPLAY_NAME, name.as_raw_str());
|
||||
txn.remove_display_name(&room_id, &raw_name)?;
|
||||
} else {
|
||||
txn.set_display_name(&room_id, &name, &data)?;
|
||||
// We only create new buckets with the normalized display name.
|
||||
txn.set_display_name(&room_id, &encoded_name, &data)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1489,10 +1521,13 @@ impl StateStore for SqliteStateStore {
|
||||
async fn get_users_with_display_name(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
display_name: &str,
|
||||
display_name: &DisplayName,
|
||||
) -> Result<BTreeSet<OwnedUserId>> {
|
||||
let room_id = self.encode_key(keys::DISPLAY_NAME, room_id);
|
||||
let names = vec![self.encode_key(keys::DISPLAY_NAME, display_name)];
|
||||
let names = vec![self.encode_key(
|
||||
keys::DISPLAY_NAME,
|
||||
display_name.as_normalized_str().unwrap_or_else(|| display_name.as_raw_str()),
|
||||
)];
|
||||
|
||||
Ok(self
|
||||
.acquire()
|
||||
@@ -1509,33 +1544,49 @@ impl StateStore for SqliteStateStore {
|
||||
async fn get_users_with_display_names<'a>(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
display_names: &'a [String],
|
||||
) -> Result<BTreeMap<&'a str, BTreeSet<OwnedUserId>>> {
|
||||
display_names: &'a [DisplayName],
|
||||
) -> Result<HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>> {
|
||||
let mut result = HashMap::new();
|
||||
|
||||
if display_names.is_empty() {
|
||||
return Ok(BTreeMap::new());
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
let room_id = self.encode_key(keys::DISPLAY_NAME, room_id);
|
||||
let mut names_map = display_names
|
||||
.iter()
|
||||
.map(|n| (self.encode_key(keys::DISPLAY_NAME, n), n.as_ref()))
|
||||
.flat_map(|display_name| {
|
||||
// We encode the display name as the `raw_str()` and the normalized string.
|
||||
//
|
||||
// This is for compatibility reasons since:
|
||||
// 1. Previously "Alice" and "alice" were considered to be distinct display
|
||||
// names, while we now consider them to be the same so we need to merge the
|
||||
// previously distinct buckets of user IDs.
|
||||
// 2. We can't do a migration to merge the previously distinct buckets of user
|
||||
// IDs since the display names itself are hashed before they are persisted
|
||||
// in the store.
|
||||
let raw =
|
||||
(self.encode_key(keys::DISPLAY_NAME, display_name.as_raw_str()), display_name);
|
||||
let normalized = display_name.as_normalized_str().map(|normalized| {
|
||||
(self.encode_key(keys::DISPLAY_NAME, normalized), display_name)
|
||||
});
|
||||
|
||||
iter::once(raw).chain(normalized.into_iter())
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let names = names_map.keys().cloned().collect();
|
||||
|
||||
self.acquire()
|
||||
.await?
|
||||
.get_display_names(room_id, names)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|(name, data)| {
|
||||
Ok((
|
||||
names_map
|
||||
.remove(name.as_slice())
|
||||
.expect("returned display names were requested"),
|
||||
self.deserialize_json(&data)?,
|
||||
))
|
||||
})
|
||||
.collect::<Result<BTreeMap<_, _>>>()
|
||||
for (name, data) in
|
||||
self.acquire().await?.get_display_names(room_id, names).await?.into_iter()
|
||||
{
|
||||
let display_name =
|
||||
names_map.remove(name.as_slice()).expect("returned display names were requested");
|
||||
let user_ids: BTreeSet<_> = self.deserialize_json(&data)?;
|
||||
|
||||
result.entry(display_name).or_insert_with(BTreeSet::new).extend(user_ids);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn get_account_data_event(
|
||||
@@ -1685,6 +1736,7 @@ impl StateStore for SqliteStateStore {
|
||||
room_id: &RoomId,
|
||||
transaction_id: OwnedTransactionId,
|
||||
content: QueuedRequestKind,
|
||||
priority: usize,
|
||||
) -> Result<(), Self::Error> {
|
||||
let room_id_key = self.encode_key(keys::SEND_QUEUE, room_id);
|
||||
let room_id_value = self.serialize_value(&room_id.to_owned())?;
|
||||
@@ -1699,7 +1751,7 @@ impl StateStore for SqliteStateStore {
|
||||
self.acquire()
|
||||
.await?
|
||||
.with_transaction(move |txn| {
|
||||
txn.prepare_cached("INSERT INTO send_queue_events (room_id, room_id_val, transaction_id, content) VALUES (?, ?, ?, ?)")?.execute((room_id_key, room_id_value, transaction_id.to_string(), content))?;
|
||||
txn.prepare_cached("INSERT INTO send_queue_events (room_id, room_id_val, transaction_id, content, priority) VALUES (?, ?, ?, ?, ?)")?.execute((room_id_key, room_id_value, transaction_id.to_string(), content, priority))?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
@@ -1761,14 +1813,14 @@ impl StateStore for SqliteStateStore {
|
||||
// Note: ROWID is always present and is an auto-incremented integer counter. We
|
||||
// want to maintain the insertion order, so we can sort using it.
|
||||
// Note 2: transaction_id is not encoded, see why in `save_send_queue_event`.
|
||||
let res: Vec<(String, Vec<u8>, Option<Vec<u8>>)> = self
|
||||
let res: Vec<(String, Vec<u8>, Option<Vec<u8>>, usize)> = self
|
||||
.acquire()
|
||||
.await?
|
||||
.prepare(
|
||||
"SELECT transaction_id, content, wedge_reason FROM send_queue_events WHERE room_id = ? ORDER BY ROWID",
|
||||
"SELECT transaction_id, content, wedge_reason, priority FROM send_queue_events WHERE room_id = ? ORDER BY priority DESC, ROWID",
|
||||
|mut stmt| {
|
||||
stmt.query((room_id,))?
|
||||
.mapped(|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))
|
||||
.mapped(|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)))
|
||||
.collect()
|
||||
},
|
||||
)
|
||||
@@ -1780,6 +1832,7 @@ impl StateStore for SqliteStateStore {
|
||||
transaction_id: entry.0.into(),
|
||||
kind: self.deserialize_json(&entry.1)?,
|
||||
error: entry.2.map(|v| self.deserialize_value(&v)).transpose()?,
|
||||
priority: entry.3,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,3 +2,6 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.8.0] - 2024-11-19
|
||||
|
||||
No notable changes in this release.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "matrix-sdk-store-encryption"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
edition = "2021"
|
||||
description = "Helpers for encrypted storage keys for the Matrix SDK"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
|
||||
@@ -2,29 +2,40 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
# unreleased
|
||||
## [0.8.0] - 2024-11-19
|
||||
|
||||
Breaking changes:
|
||||
### Bug Fixes
|
||||
|
||||
- `Timeline::edit` now takes a `RoomMessageEventContentWithoutRelation`.
|
||||
- `Timeline::send_attachment` now takes an `impl Into<PathBuf>` for the path of
|
||||
the file to send.
|
||||
- `Timeline::item_by_transaction_id` has been renamed to `Timeline::local_item_by_transaction_id`
|
||||
(always returns local echoes).
|
||||
|
||||
Bug fixes:
|
||||
- Disable `share_pos()` inside `RoomListService`.
|
||||
|
||||
- `UtdHookManager` no longer re-reports UTD events as late decryptions.
|
||||
([#3480](https://github.com/matrix-org/matrix-rust-sdk/pull/3480))
|
||||
|
||||
- Messages that we were unable to decrypt no longer display a red padlock.
|
||||
([#3956](https://github.com/matrix-org/matrix-rust-sdk/issues/3956))
|
||||
|
||||
Other changes:
|
||||
|
||||
- `UtdHookManager` no longer reports UTD events that were already reported in a
|
||||
previous session.
|
||||
([#3519](https://github.com/matrix-org/matrix-rust-sdk/pull/3519))
|
||||
|
||||
### Features
|
||||
|
||||
- Add `m.room.join_rules` to the required state.
|
||||
|
||||
- `EncryptionSyncService` and `Notification` are using
|
||||
`Client::cross_process_store_locks_holder_name`.
|
||||
|
||||
|
||||
### Refactor
|
||||
|
||||
- [**breaking**] `Timeline::edit` now takes a `RoomMessageEventContentWithoutRelation`.
|
||||
|
||||
- [**breaking**] `Timeline::send_attachment` now takes an `impl Into<PathBuf>`
|
||||
for the path of the file to send.
|
||||
|
||||
- [**breaking**] `Timeline::item_by_transaction_id` has been renamed to
|
||||
`Timeline::local_item_by_transaction_id` (always returns local echoes).
|
||||
|
||||
|
||||
# 0.7.0
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "matrix-sdk-ui"
|
||||
description = "GUI-centric utilities on top of matrix-rust-sdk (experimental)."
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -88,11 +88,7 @@ impl EncryptionSyncService {
|
||||
/// Creates a new instance of a `EncryptionSyncService`.
|
||||
///
|
||||
/// This will create and manage an instance of [`matrix_sdk::SlidingSync`].
|
||||
/// The `process_id` is used as the identifier of that instance, as such
|
||||
/// make sure to not reuse a name used by another process, at the risk
|
||||
/// of causing problems.
|
||||
pub async fn new(
|
||||
process_id: String,
|
||||
client: Client,
|
||||
poll_and_network_timeouts: Option<(Duration, Duration)>,
|
||||
with_locking: WithLocking,
|
||||
@@ -119,7 +115,13 @@ impl EncryptionSyncService {
|
||||
|
||||
if with_locking {
|
||||
// Gently try to enable the cross-process lock on behalf of the user.
|
||||
match client.encryption().enable_cross_process_store_lock(process_id).await {
|
||||
match client
|
||||
.encryption()
|
||||
.enable_cross_process_store_lock(
|
||||
client.cross_process_store_locks_holder_name().to_owned(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) | Err(matrix_sdk::Error::BadCryptoStoreState) => {
|
||||
// Ignore; we've already set the crypto store lock to
|
||||
// something, and that's sufficient as
|
||||
|
||||
@@ -111,7 +111,8 @@ impl NotificationClient {
|
||||
parent_client: Client,
|
||||
process_setup: NotificationProcessSetup,
|
||||
) -> Result<Self, Error> {
|
||||
let client = parent_client.notification_client().await?;
|
||||
let client = parent_client.notification_client(Self::LOCK_ID.to_owned()).await?;
|
||||
|
||||
Ok(NotificationClient {
|
||||
client,
|
||||
parent_client,
|
||||
@@ -242,7 +243,6 @@ impl NotificationClient {
|
||||
};
|
||||
|
||||
let encryption_sync = EncryptionSyncService::new(
|
||||
Self::LOCK_ID.to_owned(),
|
||||
self.client.clone(),
|
||||
Some((Duration::from_secs(3), Duration::from_secs(4))),
|
||||
with_locking,
|
||||
|
||||
@@ -88,6 +88,7 @@ const DEFAULT_REQUIRED_STATE: &[(StateEventType, &str)] = &[
|
||||
(StateEventType::RoomCanonicalAlias, ""),
|
||||
(StateEventType::RoomPowerLevels, ""),
|
||||
(StateEventType::CallMember, "*"),
|
||||
(StateEventType::RoomJoinRules, ""),
|
||||
];
|
||||
|
||||
/// The default `required_state` constant value for sliding sync room
|
||||
@@ -135,9 +136,10 @@ impl RoomListService {
|
||||
}))
|
||||
.with_typing_extension(assign!(http::request::Typing::default(), {
|
||||
enabled: Some(true),
|
||||
}))
|
||||
// We don't deal with encryption device messages here so this is safe
|
||||
.share_pos();
|
||||
}));
|
||||
// TODO: Re-enable once we know it creates slowness.
|
||||
// // We don't deal with encryption device messages here so this is safe
|
||||
// .share_pos();
|
||||
|
||||
let sliding_sync = builder
|
||||
.add_cached_list(
|
||||
|
||||
@@ -435,15 +435,11 @@ pub struct SyncServiceBuilder {
|
||||
|
||||
/// Is the cross-process lock for the crypto store enabled?
|
||||
with_cross_process_lock: bool,
|
||||
|
||||
/// Application identifier, used as the cross-process lock value, if
|
||||
/// applicable.
|
||||
identifier: String,
|
||||
}
|
||||
|
||||
impl SyncServiceBuilder {
|
||||
fn new(client: Client) -> Self {
|
||||
Self { client, with_cross_process_lock: false, identifier: "app".to_owned() }
|
||||
Self { client, with_cross_process_lock: false }
|
||||
}
|
||||
|
||||
/// Enables the cross-process lock, if the sync service is being built in a
|
||||
@@ -454,14 +450,10 @@ impl SyncServiceBuilder {
|
||||
/// external process attempting to decrypt notifications. In general,
|
||||
/// `with_cross_process_lock` should not be called.
|
||||
///
|
||||
/// An app identifier can be provided too, to identify the current process;
|
||||
/// if it's not provided, a default value of "app" is used as the
|
||||
/// application identifier.
|
||||
pub fn with_cross_process_lock(mut self, app_identifier: Option<String>) -> Self {
|
||||
/// Be sure to have configured
|
||||
/// [`Client::cross_process_store_locks_holder_name`] accordingly.
|
||||
pub fn with_cross_process_lock(mut self) -> Self {
|
||||
self.with_cross_process_lock = true;
|
||||
if let Some(app_identifier) = app_identifier {
|
||||
self.identifier = app_identifier;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
@@ -477,7 +469,6 @@ impl SyncServiceBuilder {
|
||||
|
||||
let encryption_sync = Arc::new(
|
||||
EncryptionSyncService::new(
|
||||
self.identifier,
|
||||
self.client,
|
||||
None,
|
||||
WithLocking::from(self.with_cross_process_lock),
|
||||
|
||||
@@ -570,6 +570,11 @@ impl EventTimelineItem {
|
||||
EventTimelineItemKind::Remote(remote) => TimelineItemHandle::Remote(&remote.event_id),
|
||||
}
|
||||
}
|
||||
|
||||
/// For local echoes, return the associated send handle.
|
||||
pub fn local_echo_send_handle(&self) -> Option<SendHandle> {
|
||||
as_variant!(self.handle(), TimelineItemHandle::Local(handle) => handle.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LocalEventTimelineItem> for EventTimelineItemKind {
|
||||
|
||||
@@ -40,8 +40,7 @@ async fn test_smoke_encryption_sync_works() -> anyhow::Result<()> {
|
||||
|
||||
let sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new_for_testing()));
|
||||
let sync_permit_guard = sync_permit.clone().lock_owned().await;
|
||||
let encryption_sync =
|
||||
EncryptionSyncService::new("tests".to_owned(), client, None, WithLocking::Yes).await?;
|
||||
let encryption_sync = EncryptionSyncService::new(client, None, WithLocking::Yes).await?;
|
||||
|
||||
let stream = encryption_sync.sync(sync_permit_guard);
|
||||
pin_mut!(stream);
|
||||
@@ -186,8 +185,7 @@ async fn test_encryption_sync_one_fixed_iteration() -> anyhow::Result<()> {
|
||||
|
||||
let sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new_for_testing()));
|
||||
let sync_permit_guard = sync_permit.lock_owned().await;
|
||||
let encryption_sync =
|
||||
EncryptionSyncService::new("tests".to_owned(), client, None, WithLocking::Yes).await?;
|
||||
let encryption_sync = EncryptionSyncService::new(client, None, WithLocking::Yes).await?;
|
||||
|
||||
// Run all the iterations.
|
||||
encryption_sync.run_fixed_iterations(1, sync_permit_guard).await?;
|
||||
@@ -218,8 +216,7 @@ async fn test_encryption_sync_two_fixed_iterations() -> anyhow::Result<()> {
|
||||
|
||||
let sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new_for_testing()));
|
||||
let sync_permit_guard = sync_permit.lock_owned().await;
|
||||
let encryption_sync =
|
||||
EncryptionSyncService::new("tests".to_owned(), client, None, WithLocking::Yes).await?;
|
||||
let encryption_sync = EncryptionSyncService::new(client, None, WithLocking::Yes).await?;
|
||||
|
||||
encryption_sync.run_fixed_iterations(2, sync_permit_guard).await?;
|
||||
|
||||
@@ -254,8 +251,7 @@ async fn test_encryption_sync_always_reloads_todevice_token() -> anyhow::Result<
|
||||
let sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new_for_testing()));
|
||||
let sync_permit_guard = sync_permit.lock_owned().await;
|
||||
let encryption_sync =
|
||||
EncryptionSyncService::new("tests".to_owned(), client.clone(), None, WithLocking::Yes)
|
||||
.await?;
|
||||
EncryptionSyncService::new(client.clone(), None, WithLocking::Yes).await?;
|
||||
|
||||
let stream = encryption_sync.sync(sync_permit_guard);
|
||||
pin_mut!(stream);
|
||||
@@ -363,15 +359,14 @@ async fn test_notification_client_does_not_upload_duplicate_one_time_keys() -> a
|
||||
|
||||
info!("Creating the notification client");
|
||||
let notification_client = client
|
||||
.notification_client()
|
||||
.notification_client("tests".to_owned())
|
||||
.await
|
||||
.expect("We should be able to build a notification client");
|
||||
|
||||
let sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new_for_testing()));
|
||||
let sync_permit_guard = sync_permit.lock_owned().await;
|
||||
let encryption_sync =
|
||||
EncryptionSyncService::new("tests".to_owned(), client.clone(), None, WithLocking::Yes)
|
||||
.await?;
|
||||
EncryptionSyncService::new(client.clone(), None, WithLocking::Yes).await?;
|
||||
|
||||
let stream = encryption_sync.sync(sync_permit_guard);
|
||||
pin_mut!(stream);
|
||||
|
||||
@@ -357,6 +357,7 @@ async fn test_sync_all_states() -> Result<(), Error> {
|
||||
["m.room.canonical_alias", ""],
|
||||
["m.room.power_levels", ""],
|
||||
["org.matrix.msc3401.call.member", "*"],
|
||||
["m.room.join_rules", ""],
|
||||
],
|
||||
"include_heroes": true,
|
||||
"filters": {
|
||||
@@ -590,6 +591,7 @@ async fn test_sync_resumes_from_previous_state() -> Result<(), Error> {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
#[ignore] // `share_pos()` has been disabled in the room list, see there to learn more.
|
||||
async fn test_sync_resumes_from_previous_state_after_restart() -> Result<(), Error> {
|
||||
let tmp_dir = TempDir::new().unwrap();
|
||||
let store_path = tmp_dir.path();
|
||||
@@ -2220,6 +2222,7 @@ async fn test_room_subscription() -> Result<(), Error> {
|
||||
["m.room.canonical_alias", ""],
|
||||
["m.room.power_levels", ""],
|
||||
["org.matrix.msc3401.call.member", "*"],
|
||||
["m.room.join_rules", ""],
|
||||
["m.room.create", ""],
|
||||
["m.room.pinned_events", ""],
|
||||
],
|
||||
@@ -2258,6 +2261,7 @@ async fn test_room_subscription() -> Result<(), Error> {
|
||||
["m.room.canonical_alias", ""],
|
||||
["m.room.power_levels", ""],
|
||||
["org.matrix.msc3401.call.member", "*"],
|
||||
["m.room.join_rules", ""],
|
||||
["m.room.create", ""],
|
||||
["m.room.pinned_events", ""],
|
||||
],
|
||||
|
||||
+165
-43
@@ -1,51 +1,14 @@
|
||||
# unreleased
|
||||
# Changelog
|
||||
|
||||
Breaking changes:
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
- Renamed `VerificationLevel::PreviouslyVerified` to `VerificationLevel::VerificationViolation`.
|
||||
- Add a `PreviouslyVerified` variant to `VerificationLevel` indicating that the identity is unverified and previously it was verified.
|
||||
- Replace the `Notification` type from Ruma in `SyncResponse` and `Client::register_notification_handler`
|
||||
by a custom one
|
||||
- `Room::can_user_redact` and `Member::can_redact` are split between `*_redact_own` and `*_redact_other`
|
||||
- The ambiguity maps in `SyncResponse` are moved to `JoinedRoom` and `LeftRoom`
|
||||
- `AmbiguityCache` contains the room member's user ID
|
||||
- Replace `impl MediaEventContent` with `&impl MediaEventContent` in
|
||||
`Media::get_file`/`Media::remove_file`/`Media::get_thumbnail`/`Media::remove_thumbnail`
|
||||
- A custom sliding sync proxy set with `ClientBuilder::sliding_sync_proxy` now takes precedence over a discovered proxy.
|
||||
- `Client::get_profile` was moved to `Account` and renamed to `Account::fetch_user_profile_of`. `Account::get_profile` was renamed to `Account::fetch_user_profile`.
|
||||
- The `HttpError::UnableToCloneRequest` error variant has been removed because it was never used or
|
||||
generated by the SDK.
|
||||
- The `Error::InconsistentState` error variant has been removed because it was never used or
|
||||
generated by the SDK.
|
||||
- The widget capabilities in the FFI now need two additional flags: `update_delayed_event`, `send_delayed_event`.
|
||||
- `Room::event` now takes an optional `RequestConfig` to allow for tweaking the network behavior.
|
||||
- The `instant` module was removed, use the `ruma::time` module instead.
|
||||
- Add `ClientBuilder::sqlite_store_with_cache_path` to build a client that stores caches in a different directory to state/crypto.
|
||||
- The `body` parameter in `get_media_file` has been replaced with a `filename` parameter now that Ruma has a `filename()` method.
|
||||
## [0.8.0] - 2024-11-19
|
||||
|
||||
Additions:
|
||||
### Bug Fixes
|
||||
|
||||
- new `UserIdentity::pin` method.
|
||||
- new `ClientBuilder::with_decryption_trust_requirement` method.
|
||||
- new `ClientBuilder::with_room_key_recipient_strategy` method
|
||||
- new `Room.set_account_data` and `Room.set_account_data_raw` RoomAccountData setters, analogous to the GlobalAccountData
|
||||
- new `RequestConfig.max_concurrent_requests` which allows to limit the maximum number of concurrent requests the internal HTTP client issues (all others have to wait until the number drops below that threshold again)
|
||||
- Expose new method `Client::Oidc::login_with_qr_code()`.
|
||||
([#3466](https://github.com/matrix-org/matrix-rust-sdk/pull/3466))
|
||||
- Add the `ClientBuilder::add_root_certificates()` method which re-exposes the
|
||||
`reqwest::ClientBuilder::add_root_certificate()` functionality.
|
||||
- Add `Room::get_user_power_level(user_id)` and `Room::get_suggested_user_role(user_id)` to be able to fetch power level info about an user without loading the room member list.
|
||||
- Add new method `discard_room_key` on `Room` that allows to discard the current
|
||||
outbound session for that room. Can be used by clients as a dev tool like the `/discardsession` command.
|
||||
- Add a new `LinkedChunk` data structure to represents all events per room ([#3166](https://github.com/matrix-org/matrix-rust-sdk/pull/3166)).
|
||||
- Add new methods for tracking (on device only) the user's recently visited rooms called `Account::track_recently_visited_room(roomId)` and `Account::get_recently_visited_rooms()`
|
||||
- Add `send_call_notification` and `send_call_notification_if_needed` methods. This allows to implement sending ring events on call start.
|
||||
- The `get_media_content`, `get_media_file` and `get_file` methods of the
|
||||
`Media` api now support the new authenticated media endpoints.
|
||||
- WidgetDriver: Support the `"delay"` field in the `send_event` widget actions.
|
||||
This allows to send delayed events, as defined in [MSC4157](https://github.com/matrix-org/matrix-spec-proposals/pull/4157)
|
||||
- Add more invalid characters for room aliases.
|
||||
|
||||
Bug fixes:
|
||||
- Match the right status code in `Client::is_room_alias_available`.
|
||||
|
||||
- Fix a bug where room keys were considered to be downloaded before backups were
|
||||
enabled. This bug only affects the
|
||||
@@ -53,6 +16,165 @@ Bug fixes:
|
||||
made to download a room key, if a decryption failure with a given room key
|
||||
would have been encountered before the backups were enabled.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Improve documentation of `Client::observe_events`.
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
|
||||
- Add `create_room_alias` function.
|
||||
|
||||
- `Client::cross_process_store_locks_holder_name` is used everywhere:
|
||||
- `StoreConfig::new()` now takes a
|
||||
`cross_process_store_locks_holder_name` argument.
|
||||
- `StoreConfig` no longer implements `Default`.
|
||||
- `BaseClient::new()` has been removed.
|
||||
- `BaseClient::clone_with_in_memory_state_store()` now takes a
|
||||
`cross_process_store_locks_holder_name` argument.
|
||||
- `BaseClient` no longer implements `Default`.
|
||||
- `EventCacheStoreLock::new()` no longer takes a `key` argument.
|
||||
- `BuilderStoreConfig` no longer has
|
||||
`cross_process_store_locks_holder_name` field for `Sqlite` and
|
||||
`IndexedDb`.
|
||||
|
||||
- `EncryptionSyncService` and `Notification` are using `Client::cross_process_store_locks_holder_name`.
|
||||
|
||||
- Allow passing a custom `RequestConfig` to an upload request.
|
||||
|
||||
- Retry uploads if they've failed with transient errors.
|
||||
|
||||
- Implement `EventHandlerContext` for tuples.
|
||||
|
||||
- Introduce a mechanism similar to `Client::add_event_handler` and
|
||||
`Client::add_room_event_handler` but with a reactive programming pattern. Add
|
||||
`Client::observe_events` and `Client::observe_room_events`.
|
||||
|
||||
```rust
|
||||
// Get an observer.
|
||||
let observer =
|
||||
client.observe_events::<SyncRoomMessageEvent, (Room, Vec<Action>)>();
|
||||
|
||||
// Subscribe to the observer.
|
||||
let mut subscriber = observer.subscribe();
|
||||
|
||||
// Use the subscriber as a `Stream`.
|
||||
let (message_event, (room, push_actions)) = subscriber.next().await.unwrap();
|
||||
```
|
||||
|
||||
When calling `observe_events`, one has to specify the type of event (in the
|
||||
example, `SyncRoomMessageEvent`) and a context (in the example, `(Room,
|
||||
Vec<Action>)`, respectively for the room and the push actions).
|
||||
|
||||
- Implement unwedging for media uploads.
|
||||
|
||||
- Send state from state sync and not from timeline to widget ([#4254](https://github.com/matrix-org/matrix-rust-sdk/pull/4254))
|
||||
|
||||
- Allow aborting media uploads.
|
||||
|
||||
- Add `RoomPreviewInfo::num_active_members`.
|
||||
|
||||
- Use room directory search as another data source.
|
||||
|
||||
- Check if the user is allowed to do a room mention before trying to send a call
|
||||
notify event.
|
||||
([#4271](https://github.com/matrix-org/matrix-rust-sdk/pull/4271))
|
||||
|
||||
- Add `Client::cross_process_store_locks_holder_name()`.
|
||||
|
||||
- Add a `PreviouslyVerified` variant to `VerificationLevel` indicating that the
|
||||
identity is unverified and previously it was verified.
|
||||
|
||||
- New `UserIdentity::pin` method.
|
||||
|
||||
- New `ClientBuilder::with_decryption_trust_requirement` method.
|
||||
|
||||
- New `ClientBuilder::with_room_key_recipient_strategy` method
|
||||
|
||||
- New `Room.set_account_data` and `Room.set_account_data_raw` RoomAccountData
|
||||
setters, analogous to the GlobalAccountData
|
||||
|
||||
- New `RequestConfig.max_concurrent_requests` which allows to limit the maximum
|
||||
number of concurrent requests the internal HTTP client issues (all others have
|
||||
to wait until the number drops below that threshold again)
|
||||
|
||||
- Implement proper redact handling in the widget driver. This allows the Rust
|
||||
SDK widget driver to support widgets that rely on redacting.
|
||||
|
||||
|
||||
### Refactor
|
||||
- [**breaking**] Rename `DisplayName` to `RoomDisplayName`.
|
||||
|
||||
- Improve `is_room_alias_format_valid` so it's more strict.
|
||||
|
||||
- Remove duplicated fields in media event contents.
|
||||
|
||||
- Use `SendHandle` for media uploads too.
|
||||
|
||||
- Move `event_cache_store/` to `event_cache/store/` in `matrix-sdk-base`.
|
||||
|
||||
- Move `linked_chunk` from `matrix-sdk` to `matrix-sdk-common`.
|
||||
|
||||
- Move `Event` and `Gap` into `matrix_sdk_base::event_cache`.
|
||||
|
||||
- Move `formatted_caption_from` to the SDK, rename it.
|
||||
|
||||
- Tidy up and start commenting the widget code.
|
||||
|
||||
- Get rid of `ProcessingContext` and inline it in its callers.
|
||||
|
||||
- Get rid of unused `limits` parameter when constructing a `WidgetMachine`.
|
||||
|
||||
- Use a specialized mutex for locking access to the state store and
|
||||
`being_sent`.
|
||||
|
||||
- Renamed `VerificationLevel::PreviouslyVerified` to
|
||||
`VerificationLevel::VerificationViolation`.
|
||||
|
||||
- [**breaking**] Replace the `Notification` type from Ruma in `SyncResponse` and
|
||||
`Client::register_notification_handler` by a custom one.
|
||||
|
||||
- [**breaking**] The ambiguity maps in `SyncResponse` are moved to `JoinedRoom`
|
||||
and `LeftRoom`.
|
||||
|
||||
- [**breaking**] `Room::can_user_redact` and `Member::can_redact` are split
|
||||
between `*_redact_own` and `*_redact_other`.
|
||||
|
||||
- [**breaking**] `AmbiguityCache` contains the room member's user ID.
|
||||
|
||||
- [**breaking**] Replace `impl MediaEventContent` with `&impl MediaEventContent` in
|
||||
`Media::get_file`/`Media::remove_file`/`Media::get_thumbnail`/`Media::remove_thumbnail`
|
||||
|
||||
- [**breaking**] A custom sliding sync proxy set with
|
||||
`ClientBuilder::sliding_sync_proxy` now takes precedence over a discovered
|
||||
proxy.
|
||||
|
||||
- [**breaking**] `Client::get_profile` was moved to `Account` and renamed to
|
||||
`Account::fetch_user_profile_of`. `Account::get_profile` was renamed to
|
||||
`Account::fetch_user_profile`.
|
||||
|
||||
- [**breaking**] The `HttpError::UnableToCloneRequest` error variant has been
|
||||
removed because it was never used or generated by the SDK.
|
||||
|
||||
- [**breaking**] The `Error::InconsistentState` error variant has been removed
|
||||
because it was never used or generated by the SDK.
|
||||
|
||||
- [**breaking**] The widget capabilities in the FFI now need two additional
|
||||
flags: `update_delayed_event`, `send_delayed_event`.
|
||||
|
||||
- [**breaking**] `Room::event` now takes an optional `RequestConfig` to allow
|
||||
for tweaking the network behavior.
|
||||
|
||||
- [**breaking**] The `instant` module was removed, use the `ruma::time` module
|
||||
instead.
|
||||
|
||||
- [**breaking**] Add `ClientBuilder::sqlite_store_with_cache_path` to build a
|
||||
client that stores caches in a different directory to state/crypto.
|
||||
|
||||
- [**breaking**] The `body` parameter in `get_media_file` has been replaced with
|
||||
a `filename` parameter now that Ruma has a `filename()` method.
|
||||
|
||||
# 0.7.0
|
||||
|
||||
Breaking changes:
|
||||
|
||||
@@ -9,7 +9,7 @@ name = "matrix-sdk"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = { workspace = true }
|
||||
version = "0.7.1"
|
||||
version = "0.8.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["docsrs"]
|
||||
@@ -97,6 +97,7 @@ matrix-sdk-sqlite = { workspace = true, optional = true }
|
||||
matrix-sdk-test = { workspace = true, optional = true }
|
||||
mime = "0.3.16"
|
||||
mime2ext = "0.1.52"
|
||||
pin-project-lite = { workspace = true }
|
||||
rand = { workspace = true , optional = true }
|
||||
ruma = { workspace = true, features = ["rand", "unstable-msc2448", "unstable-msc2965", "unstable-msc3930", "unstable-msc3245-v1-compat", "unstable-msc2867"] }
|
||||
serde = { workspace = true }
|
||||
@@ -144,6 +145,7 @@ serde_urlencoded = "0.7.1"
|
||||
similar-asserts = { workspace = true }
|
||||
stream_assert = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
tokio-test = "0.4.4"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.33"
|
||||
|
||||
@@ -252,7 +252,7 @@ impl Account {
|
||||
///
|
||||
/// [`Media::upload()`]: crate::Media::upload
|
||||
pub async fn upload_avatar(&self, content_type: &Mime, data: Vec<u8>) -> Result<OwnedMxcUri> {
|
||||
let upload_response = self.client.media().upload(content_type, data).await?;
|
||||
let upload_response = self.client.media().upload(content_type, data, None).await?;
|
||||
self.set_avatar_url(Some(&upload_response.content_uri)).await?;
|
||||
Ok(upload_response.content_uri)
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ impl From<AttachmentInfo> for FileInfo {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
/// Base metadata about a thumbnail.
|
||||
pub struct BaseThumbnailInfo {
|
||||
/// The height of the thumbnail in pixels.
|
||||
|
||||
@@ -101,16 +101,21 @@ pub struct ClientBuilder {
|
||||
room_key_recipient_strategy: CollectStrategy,
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
decryption_trust_requirement: TrustRequirement,
|
||||
cross_process_store_locks_holder_name: String,
|
||||
}
|
||||
|
||||
impl ClientBuilder {
|
||||
const DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME: &str = "main";
|
||||
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
homeserver_cfg: None,
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
sliding_sync_version_builder: SlidingSyncVersionBuilder::Native,
|
||||
http_cfg: None,
|
||||
store_config: BuilderStoreConfig::Custom(StoreConfig::default()),
|
||||
store_config: BuilderStoreConfig::Custom(StoreConfig::new(
|
||||
Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME.to_owned(),
|
||||
)),
|
||||
request_config: Default::default(),
|
||||
respect_login_well_known: true,
|
||||
server_versions: None,
|
||||
@@ -122,6 +127,8 @@ impl ClientBuilder {
|
||||
room_key_recipient_strategy: Default::default(),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
decryption_trust_requirement: TrustRequirement::Untrusted,
|
||||
cross_process_store_locks_holder_name:
|
||||
Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +215,6 @@ impl ClientBuilder {
|
||||
path: path.as_ref().to_owned(),
|
||||
cache_path: None,
|
||||
passphrase: passphrase.map(ToOwned::to_owned),
|
||||
event_cache_store_lock_holder: "matrix-sdk".to_owned(),
|
||||
};
|
||||
self
|
||||
}
|
||||
@@ -226,7 +232,6 @@ impl ClientBuilder {
|
||||
path: path.as_ref().to_owned(),
|
||||
cache_path: Some(cache_path.as_ref().to_owned()),
|
||||
passphrase: passphrase.map(ToOwned::to_owned),
|
||||
event_cache_store_lock_holder: "matrix-sdk".to_owned(),
|
||||
};
|
||||
self
|
||||
}
|
||||
@@ -237,7 +242,6 @@ impl ClientBuilder {
|
||||
self.store_config = BuilderStoreConfig::IndexedDb {
|
||||
name: name.to_owned(),
|
||||
passphrase: passphrase.map(ToOwned::to_owned),
|
||||
event_cache_store_lock_holder: "matrix-sdk".to_owned(),
|
||||
};
|
||||
self
|
||||
}
|
||||
@@ -258,7 +262,9 @@ impl ClientBuilder {
|
||||
/// # let custom_state_store = MemoryStore::new();
|
||||
/// use matrix_sdk::{config::StoreConfig, Client};
|
||||
///
|
||||
/// let store_config = StoreConfig::new().state_store(custom_state_store);
|
||||
/// let store_config =
|
||||
/// StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
|
||||
/// .state_store(custom_state_store);
|
||||
/// let client_builder = Client::builder().store_config(store_config);
|
||||
/// ```
|
||||
pub fn store_config(mut self, store_config: StoreConfig) -> Self {
|
||||
@@ -424,6 +430,20 @@ impl ClientBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the cross-process store locks holder name.
|
||||
///
|
||||
/// The SDK provides cross-process store locks (see
|
||||
/// [`matrix_sdk_common::store_locks::CrossProcessStoreLock`]). The
|
||||
/// `holder_name` will be the value used for all cross-process store locks
|
||||
/// used by the `Client` being built.
|
||||
///
|
||||
/// If 2 concurrent `Client`s are running in 2 different process, this
|
||||
/// method must be called with different `hold_name` values.
|
||||
pub fn cross_process_store_locks_holder_name(mut self, holder_name: String) -> Self {
|
||||
self.cross_process_store_locks_holder_name = holder_name;
|
||||
self
|
||||
}
|
||||
|
||||
/// Create a [`Client`] with the options set on this builder.
|
||||
///
|
||||
/// # Errors
|
||||
@@ -457,13 +477,17 @@ impl ClientBuilder {
|
||||
base_client
|
||||
} else {
|
||||
#[allow(unused_mut)]
|
||||
let mut client =
|
||||
BaseClient::with_store_config(build_store_config(self.store_config).await?);
|
||||
let mut client = BaseClient::with_store_config(
|
||||
build_store_config(self.store_config, &self.cross_process_store_locks_holder_name)
|
||||
.await?,
|
||||
);
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
{
|
||||
client.room_key_recipient_strategy = self.room_key_recipient_strategy;
|
||||
client.decryption_trust_requirement = self.decryption_trust_requirement;
|
||||
}
|
||||
|
||||
client
|
||||
};
|
||||
|
||||
@@ -529,6 +553,7 @@ impl ClientBuilder {
|
||||
send_queue,
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
self.encryption_settings,
|
||||
self.cross_process_store_locks_holder_name,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -547,20 +572,16 @@ pub fn sanitize_server_name(s: &str) -> crate::Result<OwnedServerName, IdParseEr
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)] // False positive when building with !sqlite & !indexeddb
|
||||
#[allow(clippy::unused_async, unused)] // False positive when building with !sqlite & !indexeddb
|
||||
async fn build_store_config(
|
||||
builder_config: BuilderStoreConfig,
|
||||
cross_process_store_locks_holder_name: &str,
|
||||
) -> Result<StoreConfig, ClientBuildError> {
|
||||
#[allow(clippy::infallible_destructuring_match)]
|
||||
let store_config = match builder_config {
|
||||
#[cfg(feature = "sqlite")]
|
||||
BuilderStoreConfig::Sqlite {
|
||||
path,
|
||||
cache_path,
|
||||
passphrase,
|
||||
event_cache_store_lock_holder,
|
||||
} => {
|
||||
let store_config = StoreConfig::new()
|
||||
BuilderStoreConfig::Sqlite { path, cache_path, passphrase } => {
|
||||
let store_config = StoreConfig::new(cross_process_store_locks_holder_name.to_owned())
|
||||
.state_store(
|
||||
matrix_sdk_sqlite::SqliteStateStore::open(&path, passphrase.as_deref()).await?,
|
||||
)
|
||||
@@ -570,8 +591,6 @@ async fn build_store_config(
|
||||
passphrase.as_deref(),
|
||||
)
|
||||
.await?,
|
||||
"default-key".to_owned(),
|
||||
event_cache_store_lock_holder,
|
||||
);
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
@@ -583,11 +602,11 @@ async fn build_store_config(
|
||||
}
|
||||
|
||||
#[cfg(feature = "indexeddb")]
|
||||
BuilderStoreConfig::IndexedDb { name, passphrase, event_cache_store_lock_holder } => {
|
||||
BuilderStoreConfig::IndexedDb { name, passphrase } => {
|
||||
build_indexeddb_store_config(
|
||||
&name,
|
||||
passphrase.as_deref(),
|
||||
event_cache_store_lock_holder,
|
||||
cross_process_store_locks_holder_name,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
@@ -603,28 +622,28 @@ async fn build_store_config(
|
||||
async fn build_indexeddb_store_config(
|
||||
name: &str,
|
||||
passphrase: Option<&str>,
|
||||
event_cache_store_lock_holder: String,
|
||||
cross_process_store_locks_holder_name: &str,
|
||||
) -> Result<StoreConfig, ClientBuildError> {
|
||||
let cross_process_store_locks_holder_name = cross_process_store_locks_holder_name.to_owned();
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
let store_config = {
|
||||
let (state_store, crypto_store) =
|
||||
matrix_sdk_indexeddb::open_stores_with_name(name, passphrase).await?;
|
||||
StoreConfig::new().state_store(state_store).crypto_store(crypto_store)
|
||||
StoreConfig::new(cross_process_store_locks_holder_name)
|
||||
.state_store(state_store)
|
||||
.crypto_store(crypto_store)
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "e2e-encryption"))]
|
||||
let store_config = {
|
||||
let state_store = matrix_sdk_indexeddb::open_state_store(name, passphrase).await?;
|
||||
StoreConfig::new().state_store(state_store)
|
||||
StoreConfig::new(cross_process_store_locks_holder_name).state_store(state_store)
|
||||
};
|
||||
|
||||
let store_config = {
|
||||
tracing::warn!("The IndexedDB backend does not implement an event cache store, falling back to the in-memory event cache store…");
|
||||
store_config.event_cache_store(
|
||||
matrix_sdk_base::event_cache_store::MemoryStore::new(),
|
||||
"default-key".to_owned(),
|
||||
event_cache_store_lock_holder,
|
||||
)
|
||||
store_config.event_cache_store(matrix_sdk_base::event_cache::store::MemoryStore::new())
|
||||
};
|
||||
|
||||
Ok(store_config)
|
||||
@@ -634,7 +653,7 @@ async fn build_indexeddb_store_config(
|
||||
async fn build_indexeddb_store_config(
|
||||
_name: &str,
|
||||
_passphrase: Option<&str>,
|
||||
_event_cache_store_lock_holder: String,
|
||||
_event_cache_store_lock_holder_name: &str,
|
||||
) -> Result<StoreConfig, ClientBuildError> {
|
||||
panic!("the IndexedDB is only available on the 'wasm32' arch")
|
||||
}
|
||||
@@ -679,13 +698,11 @@ enum BuilderStoreConfig {
|
||||
path: std::path::PathBuf,
|
||||
cache_path: Option<std::path::PathBuf>,
|
||||
passphrase: Option<String>,
|
||||
event_cache_store_lock_holder: String,
|
||||
},
|
||||
#[cfg(feature = "indexeddb")]
|
||||
IndexedDb {
|
||||
name: String,
|
||||
passphrase: Option<String>,
|
||||
event_cache_store_lock_holder: String,
|
||||
},
|
||||
Custom(StoreConfig),
|
||||
}
|
||||
@@ -753,6 +770,7 @@ pub(crate) mod tests {
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk_test::{async_test, test_json};
|
||||
use serde_json::{json_internal, Value as JsonValue};
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
use url::Url;
|
||||
use wiremock::{
|
||||
matchers::{method, path},
|
||||
@@ -1129,4 +1147,27 @@ pub(crate) mod tests {
|
||||
object
|
||||
})
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_cross_process_store_locks_holder_name() {
|
||||
{
|
||||
let homeserver = make_mock_homeserver().await;
|
||||
let client =
|
||||
ClientBuilder::new().homeserver_url(homeserver.uri()).build().await.unwrap();
|
||||
|
||||
assert_eq!(client.cross_process_store_locks_holder_name(), "main");
|
||||
}
|
||||
|
||||
{
|
||||
let homeserver = make_mock_homeserver().await;
|
||||
let client = ClientBuilder::new()
|
||||
.homeserver_url(homeserver.uri())
|
||||
.cross_process_store_locks_holder_name("foo".to_owned())
|
||||
.build()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(client.cross_process_store_locks_holder_name(), "foo");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
use std::{
|
||||
collections::{btree_map, BTreeMap},
|
||||
fmt::{self, Debug},
|
||||
future::Future,
|
||||
future::{ready, Future},
|
||||
pin::Pin,
|
||||
sync::{Arc, Mutex as StdMutex, RwLock as StdRwLock, Weak},
|
||||
};
|
||||
@@ -33,7 +33,7 @@ use imbl::Vector;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_base::crypto::store::LockableCryptoStore;
|
||||
use matrix_sdk_base::{
|
||||
event_cache_store::EventCacheStoreLock,
|
||||
event_cache::store::EventCacheStoreLock,
|
||||
store::{DynStateStore, ServerCapabilities},
|
||||
sync::{Notification, RoomUpdates},
|
||||
BaseClient, RoomInfoNotableUpdate, RoomState, RoomStateFilter, SendOutsideWasm, SessionMeta,
|
||||
@@ -45,13 +45,14 @@ use ruma::{
|
||||
api::{
|
||||
client::{
|
||||
account::whoami,
|
||||
alias::get_alias,
|
||||
alias::{create_alias, get_alias},
|
||||
device::{delete_devices, get_devices, update_device},
|
||||
directory::{get_public_rooms, get_public_rooms_filtered},
|
||||
discovery::{
|
||||
get_capabilities::{self, Capabilities},
|
||||
get_supported_versions,
|
||||
},
|
||||
error::ErrorKind,
|
||||
filter::{create_filter::v3::Request as FilterUploadRequest, FilterDefinition},
|
||||
knock::knock_room,
|
||||
membership::{join_room_by_id, join_room_by_id_or_alias},
|
||||
@@ -87,7 +88,8 @@ use crate::{
|
||||
error::{HttpError, HttpResult},
|
||||
event_cache::EventCache,
|
||||
event_handler::{
|
||||
EventHandler, EventHandlerDropGuard, EventHandlerHandle, EventHandlerStore, SyncEvent,
|
||||
EventHandler, EventHandlerContext, EventHandlerDropGuard, EventHandlerHandle,
|
||||
EventHandlerStore, ObservableEventHandler, SyncEvent,
|
||||
},
|
||||
http_client::HttpClient,
|
||||
matrix_auth::MatrixAuth,
|
||||
@@ -274,6 +276,17 @@ pub(crate) struct ClientInner {
|
||||
/// deduplicate multiple calls to a method.
|
||||
pub(crate) locks: ClientLocks,
|
||||
|
||||
/// The cross-process store locks holder name.
|
||||
///
|
||||
/// The SDK provides cross-process store locks (see
|
||||
/// [`matrix_sdk_common::store_locks::CrossProcessStoreLock`]). The
|
||||
/// `holder_name` is the value used for all cross-process store locks
|
||||
/// used by this `Client`.
|
||||
///
|
||||
/// If multiple `Client`s are running in different processes, this
|
||||
/// value MUST be different for each `Client`.
|
||||
cross_process_store_locks_holder_name: String,
|
||||
|
||||
/// A mapping of the times at which the current user sent typing notices,
|
||||
/// keyed by room.
|
||||
pub(crate) typing_notice_times: StdRwLock<BTreeMap<OwnedRoomId, Instant>>,
|
||||
@@ -340,6 +353,7 @@ impl ClientInner {
|
||||
event_cache: OnceCell<EventCache>,
|
||||
send_queue: Arc<SendQueueData>,
|
||||
#[cfg(feature = "e2e-encryption")] encryption_settings: EncryptionSettings,
|
||||
cross_process_store_locks_holder_name: String,
|
||||
) -> Arc<Self> {
|
||||
let client = Self {
|
||||
server,
|
||||
@@ -350,6 +364,7 @@ impl ClientInner {
|
||||
http_client,
|
||||
base_client,
|
||||
locks: Default::default(),
|
||||
cross_process_store_locks_holder_name,
|
||||
server_capabilities: RwLock::new(server_capabilities),
|
||||
typing_notice_times: Default::default(),
|
||||
event_handlers: Default::default(),
|
||||
@@ -424,6 +439,16 @@ impl Client {
|
||||
&self.inner.locks
|
||||
}
|
||||
|
||||
/// The cross-process store locks holder name.
|
||||
///
|
||||
/// The SDK provides cross-process store locks (see
|
||||
/// [`matrix_sdk_common::store_locks::CrossProcessStoreLock`]). The
|
||||
/// `holder_name` is the value used for all cross-process store locks
|
||||
/// used by this `Client`.
|
||||
pub fn cross_process_store_locks_holder_name(&self) -> &str {
|
||||
&self.inner.cross_process_store_locks_holder_name
|
||||
}
|
||||
|
||||
/// Change the homeserver URL used by this client.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -657,8 +682,6 @@ impl Client {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use url::Url;
|
||||
/// # let homeserver = Url::parse("http://localhost:8080").unwrap();
|
||||
/// use matrix_sdk::{
|
||||
/// deserialized_responses::EncryptionInfo,
|
||||
/// event_handler::Ctx,
|
||||
@@ -675,14 +698,7 @@ impl Client {
|
||||
/// };
|
||||
/// use serde::{Deserialize, Serialize};
|
||||
///
|
||||
/// # futures_executor::block_on(async {
|
||||
/// # let client = matrix_sdk::Client::builder()
|
||||
/// # .homeserver_url(homeserver)
|
||||
/// # .server_versions([ruma::api::MatrixVersion::V1_0])
|
||||
/// # .build()
|
||||
/// # .await
|
||||
/// # .unwrap();
|
||||
/// #
|
||||
/// # async fn example(client: Client) {
|
||||
/// client.add_event_handler(
|
||||
/// |ev: SyncRoomMessageEvent, room: Room, client: Client| async move {
|
||||
/// // Common usage: Room event plus room and client.
|
||||
@@ -748,11 +764,11 @@ impl Client {
|
||||
/// client.add_event_handler(move |ev: SyncRoomMessageEvent | async move {
|
||||
/// println!("Calling the handler with identifier {data}");
|
||||
/// });
|
||||
/// # });
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn add_event_handler<Ev, Ctx, H>(&self, handler: H) -> EventHandlerHandle
|
||||
where
|
||||
Ev: SyncEvent + DeserializeOwned + Send + 'static,
|
||||
Ev: SyncEvent + DeserializeOwned + SendOutsideWasm + 'static,
|
||||
H: EventHandler<Ev, Ctx>,
|
||||
{
|
||||
self.add_event_handler_impl(handler, None)
|
||||
@@ -774,12 +790,133 @@ impl Client {
|
||||
handler: H,
|
||||
) -> EventHandlerHandle
|
||||
where
|
||||
Ev: SyncEvent + DeserializeOwned + Send + 'static,
|
||||
Ev: SyncEvent + DeserializeOwned + SendOutsideWasm + 'static,
|
||||
H: EventHandler<Ev, Ctx>,
|
||||
{
|
||||
self.add_event_handler_impl(handler, Some(room_id.to_owned()))
|
||||
}
|
||||
|
||||
/// Observe a specific event type.
|
||||
///
|
||||
/// `Ev` represents the kind of event that will be observed. `Ctx`
|
||||
/// represents the context that will come with the event. It relies on the
|
||||
/// same mechanism as [`Client::add_event_handler`]. The main difference is
|
||||
/// that it returns an [`ObservableEventHandler`] and doesn't require a
|
||||
/// user-defined closure. It is possible to subscribe to the
|
||||
/// [`ObservableEventHandler`] to get an [`EventHandlerSubscriber`], which
|
||||
/// implements a [`Stream`]. The `Stream::Item` will be of type `(Ev,
|
||||
/// Ctx)`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Let's see a classical usage:
|
||||
///
|
||||
/// ```
|
||||
/// use futures_util::StreamExt as _;
|
||||
/// use matrix_sdk::{
|
||||
/// ruma::{events::room::message::SyncRoomMessageEvent, push::Action},
|
||||
/// Client, Room,
|
||||
/// };
|
||||
///
|
||||
/// # async fn example(client: Client) -> Option<()> {
|
||||
/// let observer =
|
||||
/// client.observe_events::<SyncRoomMessageEvent, (Room, Vec<Action>)>();
|
||||
///
|
||||
/// let mut subscriber = observer.subscribe();
|
||||
///
|
||||
/// let (event, (room, push_actions)) = subscriber.next().await?;
|
||||
/// # Some(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Now let's see how to get several contexts that can be useful for you:
|
||||
///
|
||||
/// ```
|
||||
/// use matrix_sdk::{
|
||||
/// deserialized_responses::EncryptionInfo,
|
||||
/// ruma::{
|
||||
/// events::room::{
|
||||
/// message::SyncRoomMessageEvent, topic::SyncRoomTopicEvent,
|
||||
/// },
|
||||
/// push::Action,
|
||||
/// },
|
||||
/// Client, Room,
|
||||
/// };
|
||||
///
|
||||
/// # async fn example(client: Client) {
|
||||
/// // Observe `SyncRoomMessageEvent` and fetch `Room` + `Client`.
|
||||
/// let _ = client.observe_events::<SyncRoomMessageEvent, (Room, Client)>();
|
||||
///
|
||||
/// // Observe `SyncRoomMessageEvent` and fetch `Room` + `EncryptionInfo`
|
||||
/// // to distinguish between unencrypted events and events that were decrypted
|
||||
/// // by the SDK.
|
||||
/// let _ = client
|
||||
/// .observe_events::<SyncRoomMessageEvent, (Room, Option<EncryptionInfo>)>(
|
||||
/// );
|
||||
///
|
||||
/// // Observe `SyncRoomMessageEvent` and fetch `Room` + push actions.
|
||||
/// // For example, an event with `Action::SetTweak(Tweak::Highlight(true))`
|
||||
/// // should be highlighted in the timeline.
|
||||
/// let _ =
|
||||
/// client.observe_events::<SyncRoomMessageEvent, (Room, Vec<Action>)>();
|
||||
///
|
||||
/// // Observe `SyncRoomTopicEvent` and fetch nothing else.
|
||||
/// let _ = client.observe_events::<SyncRoomTopicEvent, ()>();
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// [`EventHandlerSubscriber`]: crate::event_handler::EventHandlerSubscriber
|
||||
pub fn observe_events<Ev, Ctx>(&self) -> ObservableEventHandler<(Ev, Ctx)>
|
||||
where
|
||||
Ev: SyncEvent + DeserializeOwned + SendOutsideWasm + SyncOutsideWasm + 'static,
|
||||
Ctx: EventHandlerContext + SendOutsideWasm + SyncOutsideWasm + 'static,
|
||||
{
|
||||
self.observe_room_events_impl(None)
|
||||
}
|
||||
|
||||
/// Observe a specific room, and event type.
|
||||
///
|
||||
/// This method works the same way as [`Client::observe_events`], except
|
||||
/// that the observability will only be applied for events in the room with
|
||||
/// the specified ID. See that method for more details.
|
||||
pub fn observe_room_events<Ev, Ctx>(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> ObservableEventHandler<(Ev, Ctx)>
|
||||
where
|
||||
Ev: SyncEvent + DeserializeOwned + SendOutsideWasm + SyncOutsideWasm + 'static,
|
||||
Ctx: EventHandlerContext + SendOutsideWasm + SyncOutsideWasm + 'static,
|
||||
{
|
||||
self.observe_room_events_impl(Some(room_id.to_owned()))
|
||||
}
|
||||
|
||||
/// Shared implementation for `Client::observe_events` and
|
||||
/// `Client::observe_room_events`.
|
||||
fn observe_room_events_impl<Ev, Ctx>(
|
||||
&self,
|
||||
room_id: Option<OwnedRoomId>,
|
||||
) -> ObservableEventHandler<(Ev, Ctx)>
|
||||
where
|
||||
Ev: SyncEvent + DeserializeOwned + SendOutsideWasm + SyncOutsideWasm + 'static,
|
||||
Ctx: EventHandlerContext + SendOutsideWasm + SyncOutsideWasm + 'static,
|
||||
{
|
||||
// The default value is `None`. It becomes `Some((Ev, Ctx))` once it has a
|
||||
// new value.
|
||||
let shared_observable = SharedObservable::new(None);
|
||||
|
||||
ObservableEventHandler::new(
|
||||
shared_observable.clone(),
|
||||
self.event_handler_drop_guard(self.add_event_handler_impl(
|
||||
move |event: Ev, context: Ctx| {
|
||||
shared_observable.set(Some((event, context)));
|
||||
|
||||
ready(())
|
||||
},
|
||||
room_id,
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Remove the event handler associated with the handle.
|
||||
///
|
||||
/// Note that you **must not** call `remove_event_handler` from the
|
||||
@@ -1038,6 +1175,34 @@ impl Client {
|
||||
self.send(request, None).await
|
||||
}
|
||||
|
||||
/// Checks if a room alias is not in use yet.
|
||||
///
|
||||
/// Returns:
|
||||
/// - `Ok(true)` if the room alias is available.
|
||||
/// - `Ok(false)` if it's not (the resolve alias request returned a `404`
|
||||
/// status code).
|
||||
/// - An `Err` otherwise.
|
||||
pub async fn is_room_alias_available(&self, alias: &RoomAliasId) -> HttpResult<bool> {
|
||||
match self.resolve_room_alias(alias).await {
|
||||
// The room alias was resolved, so it's already in use.
|
||||
Ok(_) => Ok(false),
|
||||
Err(error) => {
|
||||
match error.client_api_error_kind() {
|
||||
// The room alias wasn't found, so it's available.
|
||||
Some(ErrorKind::NotFound) => Ok(true),
|
||||
_ => Err(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new room alias associated with a room.
|
||||
pub async fn create_room_alias(&self, alias: &RoomAliasId, room_id: &RoomId) -> HttpResult<()> {
|
||||
let request = create_alias::v3::Request::new(alias.to_owned(), room_id.to_owned());
|
||||
self.send(request, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the homeserver from the login response well-known if needed.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -2201,7 +2366,15 @@ impl Client {
|
||||
}
|
||||
|
||||
/// Create a new specialized `Client` that can process notifications.
|
||||
pub async fn notification_client(&self) -> Result<Client> {
|
||||
///
|
||||
/// See [`CrossProcessStoreLock::new`] to learn more about
|
||||
/// `cross_process_store_locks_holder_name`.
|
||||
///
|
||||
/// [`CrossProcessStoreLock::new`]: matrix_sdk_common::store_locks::CrossProcessStoreLock::new
|
||||
pub async fn notification_client(
|
||||
&self,
|
||||
cross_process_store_locks_holder_name: String,
|
||||
) -> Result<Client> {
|
||||
let client = Client {
|
||||
inner: ClientInner::new(
|
||||
self.inner.auth_ctx.clone(),
|
||||
@@ -2210,13 +2383,17 @@ impl Client {
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
self.sliding_sync_version(),
|
||||
self.inner.http_client.clone(),
|
||||
self.inner.base_client.clone_with_in_memory_state_store().await?,
|
||||
self.inner
|
||||
.base_client
|
||||
.clone_with_in_memory_state_store(&cross_process_store_locks_holder_name)
|
||||
.await?,
|
||||
self.inner.server_capabilities.read().await.clone(),
|
||||
self.inner.respect_login_well_known,
|
||||
self.inner.event_cache.clone(),
|
||||
self.inner.send_queue_data.clone(),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
self.inner.e2ee.encryption_settings,
|
||||
cross_process_store_locks_holder_name,
|
||||
)
|
||||
.await,
|
||||
};
|
||||
@@ -2328,7 +2505,7 @@ pub(crate) mod tests {
|
||||
api::{client::room::create_room::v3::Request as CreateRoomRequest, MatrixVersion},
|
||||
assign,
|
||||
events::ignored_user_list::IgnoredUserListEventContent,
|
||||
owned_room_id, room_id, RoomId, ServerName, UserId,
|
||||
owned_room_id, room_alias_id, room_id, RoomId, ServerName, UserId,
|
||||
};
|
||||
use serde_json::json;
|
||||
use tokio::{
|
||||
@@ -2346,8 +2523,8 @@ pub(crate) mod tests {
|
||||
client::WeakClient,
|
||||
config::{RequestConfig, SyncSettings},
|
||||
test_utils::{
|
||||
logged_in_client, no_retry_test_client, set_client_session, test_client_builder,
|
||||
test_client_builder_with_server,
|
||||
logged_in_client, mocks::MatrixMockServer, no_retry_test_client, set_client_session,
|
||||
test_client_builder, test_client_builder_with_server,
|
||||
},
|
||||
Error,
|
||||
};
|
||||
@@ -2739,7 +2916,10 @@ pub(crate) mod tests {
|
||||
let memory_store = Arc::new(MemoryStore::new());
|
||||
let client = Client::builder()
|
||||
.insecure_server_name_no_tls(server_name)
|
||||
.store_config(StoreConfig::new().state_store(memory_store.clone()))
|
||||
.store_config(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
|
||||
.state_store(memory_store.clone()),
|
||||
)
|
||||
.build()
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -2758,7 +2938,10 @@ pub(crate) mod tests {
|
||||
|
||||
let client = Client::builder()
|
||||
.insecure_server_name_no_tls(server_name)
|
||||
.store_config(StoreConfig::new().state_store(memory_store.clone()))
|
||||
.store_config(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
|
||||
.state_store(memory_store.clone()),
|
||||
)
|
||||
.build()
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -2915,4 +3098,58 @@ pub(crate) mod tests {
|
||||
.await
|
||||
.unwrap_err();
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_is_room_alias_available_if_alias_is_not_resolved() {
|
||||
let server = MatrixMockServer::new().await;
|
||||
let client = server.client_builder().build().await;
|
||||
|
||||
server.mock_room_directory_resolve_alias().not_found().expect(1).mount().await;
|
||||
|
||||
let ret = client.is_room_alias_available(room_alias_id!("#some_alias:matrix.org")).await;
|
||||
assert_matches!(ret, Ok(true));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_is_room_alias_available_if_alias_is_resolved() {
|
||||
let server = MatrixMockServer::new().await;
|
||||
let client = server.client_builder().build().await;
|
||||
|
||||
server
|
||||
.mock_room_directory_resolve_alias()
|
||||
.ok("!some_room_id:matrix.org", Vec::new())
|
||||
.expect(1)
|
||||
.mount()
|
||||
.await;
|
||||
|
||||
let ret = client.is_room_alias_available(room_alias_id!("#some_alias:matrix.org")).await;
|
||||
assert_matches!(ret, Ok(false));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_is_room_alias_available_if_error_found() {
|
||||
let server = MatrixMockServer::new().await;
|
||||
let client = server.client_builder().build().await;
|
||||
|
||||
server.mock_room_directory_resolve_alias().error500().expect(1).mount().await;
|
||||
|
||||
let ret = client.is_room_alias_available(room_alias_id!("#some_alias:matrix.org")).await;
|
||||
assert_matches!(ret, Err(_));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_create_room_alias() {
|
||||
let server = MatrixMockServer::new().await;
|
||||
let client = server.client_builder().build().await;
|
||||
|
||||
server.mock_create_room_alias().ok().expect(1).mount().await;
|
||||
|
||||
let ret = client
|
||||
.create_room_alias(
|
||||
room_alias_id!("#some_alias:matrix.org"),
|
||||
room_id!("!some_room:matrix.org"),
|
||||
)
|
||||
.await;
|
||||
assert_matches!(ret, Ok(()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ use eyeball::Subscriber;
|
||||
use matrix_sdk_common::boxed_into_future;
|
||||
use ruma::events::room::{EncryptedFile, EncryptedFileInit};
|
||||
|
||||
use crate::{Client, Result, TransmissionProgress};
|
||||
use crate::{config::RequestConfig, Client, Media, Result, TransmissionProgress};
|
||||
|
||||
/// Future returned by [`Client::upload_encrypted_file`].
|
||||
#[allow(missing_debug_implementations)]
|
||||
@@ -34,11 +34,18 @@ pub struct UploadEncryptedFile<'a, R: ?Sized> {
|
||||
content_type: &'a mime::Mime,
|
||||
reader: &'a mut R,
|
||||
send_progress: SharedObservable<TransmissionProgress>,
|
||||
request_config: Option<RequestConfig>,
|
||||
}
|
||||
|
||||
impl<'a, R: ?Sized> UploadEncryptedFile<'a, R> {
|
||||
pub(crate) fn new(client: &'a Client, content_type: &'a mime::Mime, reader: &'a mut R) -> Self {
|
||||
Self { client, content_type, reader, send_progress: Default::default() }
|
||||
Self {
|
||||
client,
|
||||
content_type,
|
||||
reader,
|
||||
send_progress: Default::default(),
|
||||
request_config: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace the default `SharedObservable` used for tracking upload
|
||||
@@ -55,6 +62,15 @@ impl<'a, R: ?Sized> UploadEncryptedFile<'a, R> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Replace the default request config used for the upload request.
|
||||
///
|
||||
/// The timeout value will be overridden with a reasonable default, based on
|
||||
/// the size of the encrypted payload.
|
||||
pub fn with_request_config(mut self, request_config: RequestConfig) -> Self {
|
||||
self.request_config = Some(request_config);
|
||||
self
|
||||
}
|
||||
|
||||
/// Get a subscriber to observe the progress of sending the request
|
||||
/// body.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -71,16 +87,21 @@ where
|
||||
boxed_into_future!(extra_bounds: 'a);
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
let Self { client, content_type, reader, send_progress } = self;
|
||||
let Self { client, content_type, reader, send_progress, request_config } = self;
|
||||
Box::pin(async move {
|
||||
let mut encryptor = matrix_sdk_base::crypto::AttachmentEncryptor::new(reader);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
encryptor.read_to_end(&mut buf)?;
|
||||
|
||||
// Override the reasonable upload timeout value, based on the size of the
|
||||
// encrypted payload.
|
||||
let request_config =
|
||||
request_config.map(|config| config.timeout(Media::reasonable_upload_timeout(&buf)));
|
||||
|
||||
let response = client
|
||||
.media()
|
||||
.upload(content_type, buf)
|
||||
.upload(content_type, buf, request_config)
|
||||
.with_send_progress_observable(send_progress)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -1458,6 +1458,8 @@ impl Encryption {
|
||||
/// caches.
|
||||
///
|
||||
/// The provided `lock_value` must be a unique identifier for this process.
|
||||
/// Check [`Client::cross_process_store_locks_holder_name`] to
|
||||
/// get the global value.
|
||||
pub async fn enable_cross_process_store_lock(&self, lock_value: String) -> Result<(), Error> {
|
||||
// If the lock has already been created, don't recreate it from scratch.
|
||||
if let Some(prev_lock) = self.client.locks().cross_process_crypto_store_lock.get() {
|
||||
|
||||
@@ -25,7 +25,7 @@ use matrix_sdk_base::crypto::{
|
||||
CryptoStoreError, DecryptorError, KeyExportError, MegolmError, OlmError,
|
||||
};
|
||||
use matrix_sdk_base::{
|
||||
event_cache_store::EventCacheStoreError, Error as SdkBaseError, QueueWedgeError, RoomState,
|
||||
event_cache::store::EventCacheStoreError, Error as SdkBaseError, QueueWedgeError, RoomState,
|
||||
StoreError,
|
||||
};
|
||||
use reqwest::Error as ReqwestError;
|
||||
|
||||
@@ -54,7 +54,6 @@ use self::paginator::PaginatorError;
|
||||
use crate::{client::WeakClient, Client};
|
||||
|
||||
mod deduplicator;
|
||||
mod linked_chunk;
|
||||
mod pagination;
|
||||
mod room;
|
||||
|
||||
|
||||
@@ -18,11 +18,11 @@ use std::{future::Future, ops::ControlFlow, sync::Arc, time::Duration};
|
||||
|
||||
use eyeball::Subscriber;
|
||||
use matrix_sdk_base::deserialized_responses::SyncTimelineEvent;
|
||||
use matrix_sdk_common::linked_chunk::ChunkContent;
|
||||
use tokio::time::timeout;
|
||||
use tracing::{debug, instrument, trace};
|
||||
|
||||
use super::{
|
||||
linked_chunk::ChunkContent,
|
||||
paginator::{PaginationResult, PaginatorState},
|
||||
room::{
|
||||
events::{Gap, RoomEvents},
|
||||
|
||||
@@ -14,24 +14,14 @@
|
||||
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
pub use matrix_sdk_base::event_cache::{Event, Gap};
|
||||
use matrix_sdk_common::linked_chunk::{
|
||||
Chunk, ChunkIdentifier, EmptyChunk, Error, Iter, LinkedChunk, Position,
|
||||
};
|
||||
use ruma::OwnedEventId;
|
||||
use tracing::{debug, error, warn};
|
||||
|
||||
use super::super::{
|
||||
deduplicator::{Decoration, Deduplicator},
|
||||
linked_chunk::{Chunk, ChunkIdentifier, EmptyChunk, Error, Iter, LinkedChunk, Position},
|
||||
};
|
||||
|
||||
/// An alias for the real event type.
|
||||
pub(crate) type Event = SyncTimelineEvent;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Gap {
|
||||
/// The token to use in the query, extracted from a previous "from" /
|
||||
/// "end" field of a `/messages` response.
|
||||
pub prev_token: String,
|
||||
}
|
||||
use super::super::deduplicator::{Decoration, Deduplicator};
|
||||
|
||||
const DEFAULT_CHUNK_CAPACITY: usize = 128;
|
||||
|
||||
|
||||
@@ -107,3 +107,38 @@ impl<T> Deref for Ctx<T> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
// `EventHandlerContext` for tuples.
|
||||
|
||||
impl EventHandlerContext for () {
|
||||
fn from_data(_data: &EventHandlerData<'_>) -> Option<Self> {
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_context_for_tuple {
|
||||
( $( $ty:ident ),* $(,)? ) => {
|
||||
#[allow(non_snake_case)]
|
||||
impl< $( $ty ),* > EventHandlerContext for ( $( $ty ),* , )
|
||||
where
|
||||
$( $ty : EventHandlerContext, )*
|
||||
{
|
||||
fn from_data(data: &EventHandlerData<'_>) -> Option<Self> {
|
||||
$(
|
||||
let $ty = $ty ::from_data(data)?;
|
||||
)*
|
||||
|
||||
Some(( $( $ty ),* , ))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_context_for_tuple!(A);
|
||||
impl_context_for_tuple!(A, B);
|
||||
impl_context_for_tuple!(A, B, C);
|
||||
impl_context_for_tuple!(A, B, C, D);
|
||||
impl_context_for_tuple!(A, B, C, D, E);
|
||||
impl_context_for_tuple!(A, B, C, D, E, F);
|
||||
impl_context_for_tuple!(A, B, C, D, E, F, G);
|
||||
impl_context_for_tuple!(A, B, C, D, E, F, G, H);
|
||||
|
||||
@@ -40,16 +40,20 @@ use std::{
|
||||
pin::Pin,
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering::SeqCst},
|
||||
RwLock,
|
||||
Arc, RwLock, Weak,
|
||||
},
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use anymap2::any::CloneAnySendSync;
|
||||
use eyeball::{SharedObservable, Subscriber};
|
||||
use futures_core::Stream;
|
||||
use futures_util::stream::{FuturesUnordered, StreamExt};
|
||||
use matrix_sdk_base::{
|
||||
deserialized_responses::{EncryptionInfo, SyncTimelineEvent},
|
||||
SendOutsideWasm, SyncOutsideWasm,
|
||||
};
|
||||
use pin_project_lite::pin_project;
|
||||
use ruma::{events::AnySyncStateEvent, push::Action, serde::Raw, OwnedRoomId};
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
use serde_json::value::RawValue as RawJsonValue;
|
||||
@@ -287,7 +291,7 @@ impl Client {
|
||||
room_id: Option<OwnedRoomId>,
|
||||
) -> EventHandlerHandle
|
||||
where
|
||||
Ev: SyncEvent + DeserializeOwned + Send + 'static,
|
||||
Ev: SyncEvent + DeserializeOwned + SendOutsideWasm + 'static,
|
||||
H: EventHandler<Ev, Ctx>,
|
||||
{
|
||||
let handler_fn: Box<EventHandlerFn> = Box::new(move |data| {
|
||||
@@ -535,11 +539,139 @@ impl_event_handler!(A, B, C, D, E, F);
|
||||
impl_event_handler!(A, B, C, D, E, F, G);
|
||||
impl_event_handler!(A, B, C, D, E, F, G, H);
|
||||
|
||||
/// An observer of events (may be tailored to a room).
|
||||
///
|
||||
/// To create such observer, use [`Client::observe_events`] or
|
||||
/// [`Client::observe_room_events`].
|
||||
#[derive(Debug)]
|
||||
pub struct ObservableEventHandler<T> {
|
||||
/// This type is actually nothing more than a thin glue layer between the
|
||||
/// [`EventHandler`] mechanism and the reactive programming types from
|
||||
/// [`eyeball`]. Here, we use a [`SharedObservable`] that is updated by the
|
||||
/// [`EventHandler`].
|
||||
shared_observable: SharedObservable<Option<T>>,
|
||||
|
||||
/// This type owns the [`EventHandlerDropGuard`]. As soon as this type goes
|
||||
/// out of scope, the event handler is unregistered/removed.
|
||||
///
|
||||
/// [`EventHandlerSubscriber`] holds a weak, non-owning reference, to this
|
||||
/// guard. It is useful to detect when to close the [`Stream`]: as soon as
|
||||
/// this type goes out of scope, the subscriber will close itself on poll.
|
||||
event_handler_guard: Arc<EventHandlerDropGuard>,
|
||||
}
|
||||
|
||||
impl<T> ObservableEventHandler<T> {
|
||||
pub(crate) fn new(
|
||||
shared_observable: SharedObservable<Option<T>>,
|
||||
event_handler_guard: EventHandlerDropGuard,
|
||||
) -> Self {
|
||||
Self { shared_observable, event_handler_guard: Arc::new(event_handler_guard) }
|
||||
}
|
||||
|
||||
/// Subscribe to this observer.
|
||||
///
|
||||
/// It returns an [`EventHandlerSubscriber`], which implements [`Stream`].
|
||||
/// See its documentation to learn more.
|
||||
pub fn subscribe(&self) -> EventHandlerSubscriber<T> {
|
||||
EventHandlerSubscriber::new(
|
||||
self.shared_observable.subscribe(),
|
||||
// The subscriber holds a weak non-owning reference to the event handler guard, so that
|
||||
// it can detect when this observer is dropped, and can close the subscriber's stream.
|
||||
Arc::downgrade(&self.event_handler_guard),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pin_project! {
|
||||
/// The subscriber of an [`ObservableEventHandler`].
|
||||
///
|
||||
/// To create such subscriber, use [`ObservableEventHandler::subscribe`].
|
||||
///
|
||||
/// This type implements [`Stream`], which means it is possible to poll the
|
||||
/// next value asynchronously. In other terms, polling this type will return
|
||||
/// the new event as soon as they are synced. See [`Client::observe_events`]
|
||||
/// to learn more.
|
||||
#[derive(Debug)]
|
||||
pub struct EventHandlerSubscriber<T> {
|
||||
// The `Subscriber` associated to the `SharedObservable` inside
|
||||
// `ObservableEventHandle`.
|
||||
//
|
||||
// Keep in mind all this API is just a thin glue layer between
|
||||
// `EventHandle` and `SharedObservable`, that's… maagiic!
|
||||
#[pin]
|
||||
subscriber: Subscriber<Option<T>>,
|
||||
|
||||
// A weak non-owning reference to the event handler guard from
|
||||
// `ObservableEventHandler`. When this type is polled (via its `Stream`
|
||||
// implementation), it is possible to detect whether the observable has
|
||||
// been dropped by upgrading this weak reference, and close the `Stream`
|
||||
// if it needs to.
|
||||
event_handler_guard: Weak<EventHandlerDropGuard>,
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> EventHandlerSubscriber<T> {
|
||||
fn new(
|
||||
subscriber: Subscriber<Option<T>>,
|
||||
event_handler_handle: Weak<EventHandlerDropGuard>,
|
||||
) -> Self {
|
||||
Self { subscriber, event_handler_guard: event_handler_handle }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Stream for EventHandlerSubscriber<T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
type Item = T;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let mut this = self.project();
|
||||
|
||||
let Some(_) = this.event_handler_guard.upgrade() else {
|
||||
// The `EventHandlerHandle` has been dropped via `EventHandlerDropGuard`. It
|
||||
// means the `ObservableEventHandler` has been dropped. It's time to
|
||||
// close this stream.
|
||||
return Poll::Ready(None);
|
||||
};
|
||||
|
||||
// First off, the subscriber is of type `Subscriber<Option<T>>` because the
|
||||
// `SharedObservable` starts with a `None` value to indicate it has no yet
|
||||
// received any update. We want the `Stream` to return `T`, not `Option<T>`. We
|
||||
// then filter out all `None` value.
|
||||
//
|
||||
// Second, when a `None` value is met, we want to poll again (hence the `loop`).
|
||||
// At best, there is a new value to return. At worst, the subscriber will return
|
||||
// `Poll::Pending` and will register the wakers accordingly.
|
||||
|
||||
loop {
|
||||
match this.subscriber.as_mut().poll_next(context) {
|
||||
// Stream has been closed somehow.
|
||||
Poll::Ready(None) => return Poll::Ready(None),
|
||||
|
||||
// The initial value (of the `SharedObservable` behind `self.subscriber`) has been
|
||||
// polled. We want to filter it out.
|
||||
Poll::Ready(Some(None)) => {
|
||||
// Loop over.
|
||||
continue;
|
||||
}
|
||||
|
||||
// We have a new value!
|
||||
Poll::Ready(Some(Some(value))) => return Poll::Ready(Some(value)),
|
||||
|
||||
// Classical pending.
|
||||
Poll::Pending => return Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use matrix_sdk_test::{
|
||||
async_test, InvitedRoomBuilder, JoinedRoomBuilder, DEFAULT_TEST_ROOM_ID,
|
||||
};
|
||||
use stream_assert::{assert_closed, assert_pending, assert_ready};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
||||
use std::{
|
||||
@@ -753,6 +885,20 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
#[allow(dependency_on_unit_never_type_fallback)]
|
||||
async fn test_add_event_handler_with_tuples() -> crate::Result<()> {
|
||||
let client = logged_in_client(None).await;
|
||||
|
||||
client.add_event_handler(
|
||||
|_ev: OriginalSyncRoomMemberEvent, (_room, _client): (Room, Client)| future::ready(()),
|
||||
);
|
||||
|
||||
// If it compiles, it works. No need to assert anything.
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
#[allow(dependency_on_unit_never_type_fallback)]
|
||||
async fn test_remove_event_handler() -> crate::Result<()> {
|
||||
@@ -870,4 +1016,152 @@ mod tests {
|
||||
assert_eq!(counter.load(SeqCst), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
#[allow(dependency_on_unit_never_type_fallback)]
|
||||
async fn test_observe_events() -> crate::Result<()> {
|
||||
let client = logged_in_client(None).await;
|
||||
|
||||
let room_id_0 = room_id!("!r0.matrix.org");
|
||||
let room_id_1 = room_id!("!r1.matrix.org");
|
||||
|
||||
let observable = client.observe_events::<OriginalSyncRoomNameEvent, Room>();
|
||||
|
||||
let mut subscriber = observable.subscribe();
|
||||
|
||||
assert_pending!(subscriber);
|
||||
|
||||
let mut response_builder = SyncResponseBuilder::new();
|
||||
let response = response_builder
|
||||
.add_joined_room(JoinedRoomBuilder::new(room_id_0).add_state_event(
|
||||
StateTestEvent::Custom(json!({
|
||||
"content": {
|
||||
"name": "Name 0"
|
||||
},
|
||||
"event_id": "$ev0",
|
||||
"origin_server_ts": 1,
|
||||
"sender": "@mnt_io:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.name",
|
||||
"unsigned": {
|
||||
"age": 1,
|
||||
}
|
||||
})),
|
||||
))
|
||||
.build_sync_response();
|
||||
client.process_sync(response).await?;
|
||||
|
||||
let (room_name, room) = assert_ready!(subscriber);
|
||||
|
||||
assert_eq!(room_name.event_id.as_str(), "$ev0");
|
||||
assert_eq!(room.room_id(), room_id_0);
|
||||
assert_eq!(room.name().unwrap(), "Name 0");
|
||||
|
||||
assert_pending!(subscriber);
|
||||
|
||||
let response = response_builder
|
||||
.add_joined_room(JoinedRoomBuilder::new(room_id_1).add_state_event(
|
||||
StateTestEvent::Custom(json!({
|
||||
"content": {
|
||||
"name": "Name 1"
|
||||
},
|
||||
"event_id": "$ev1",
|
||||
"origin_server_ts": 2,
|
||||
"sender": "@mnt_io:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.name",
|
||||
"unsigned": {
|
||||
"age": 2,
|
||||
}
|
||||
})),
|
||||
))
|
||||
.build_sync_response();
|
||||
client.process_sync(response).await?;
|
||||
|
||||
let (room_name, room) = assert_ready!(subscriber);
|
||||
|
||||
assert_eq!(room_name.event_id.as_str(), "$ev1");
|
||||
assert_eq!(room.room_id(), room_id_1);
|
||||
assert_eq!(room.name().unwrap(), "Name 1");
|
||||
|
||||
assert_pending!(subscriber);
|
||||
|
||||
drop(observable);
|
||||
assert_closed!(subscriber);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
#[allow(dependency_on_unit_never_type_fallback)]
|
||||
async fn test_observe_room_events() -> crate::Result<()> {
|
||||
let client = logged_in_client(None).await;
|
||||
|
||||
let room_id = room_id!("!r0.matrix.org");
|
||||
|
||||
let observable_for_room =
|
||||
client.observe_room_events::<OriginalSyncRoomNameEvent, (Room, Client)>(room_id);
|
||||
|
||||
let mut subscriber_for_room = observable_for_room.subscribe();
|
||||
|
||||
assert_pending!(subscriber_for_room);
|
||||
|
||||
let mut response_builder = SyncResponseBuilder::new();
|
||||
let response = response_builder
|
||||
.add_joined_room(JoinedRoomBuilder::new(room_id).add_state_event(
|
||||
StateTestEvent::Custom(json!({
|
||||
"content": {
|
||||
"name": "Name 0"
|
||||
},
|
||||
"event_id": "$ev0",
|
||||
"origin_server_ts": 1,
|
||||
"sender": "@mnt_io:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.name",
|
||||
"unsigned": {
|
||||
"age": 1,
|
||||
}
|
||||
})),
|
||||
))
|
||||
.build_sync_response();
|
||||
client.process_sync(response).await?;
|
||||
|
||||
let (room_name, (room, _client)) = assert_ready!(subscriber_for_room);
|
||||
|
||||
assert_eq!(room_name.event_id.as_str(), "$ev0");
|
||||
assert_eq!(room.name().unwrap(), "Name 0");
|
||||
|
||||
assert_pending!(subscriber_for_room);
|
||||
|
||||
let response = response_builder
|
||||
.add_joined_room(JoinedRoomBuilder::new(room_id).add_state_event(
|
||||
StateTestEvent::Custom(json!({
|
||||
"content": {
|
||||
"name": "Name 1"
|
||||
},
|
||||
"event_id": "$ev1",
|
||||
"origin_server_ts": 2,
|
||||
"sender": "@mnt_io:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.name",
|
||||
"unsigned": {
|
||||
"age": 2,
|
||||
}
|
||||
})),
|
||||
))
|
||||
.build_sync_response();
|
||||
client.process_sync(response).await?;
|
||||
|
||||
let (room_name, (room, _client)) = assert_ready!(subscriber_for_room);
|
||||
|
||||
assert_eq!(room_name.event_id.as_str(), "$ev1");
|
||||
assert_eq!(room.name().unwrap(), "Name 1");
|
||||
|
||||
assert_pending!(subscriber_for_room);
|
||||
|
||||
drop(observable_for_room);
|
||||
assert_closed!(subscriber_for_room);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,10 @@ pub use matrix_sdk_base::crypto;
|
||||
pub use matrix_sdk_base::{
|
||||
deserialized_responses,
|
||||
store::{DynStateStore, MemoryStore, StateStoreExt},
|
||||
ComposerDraft, ComposerDraftType, DisplayName, QueueWedgeError, Room as BaseRoom,
|
||||
RoomCreateWithCreatorEventContent, RoomHero, RoomInfo, RoomMember as BaseRoomMember,
|
||||
RoomMemberships, RoomState, SessionMeta, StateChanges, StateStore, StoreError,
|
||||
ComposerDraft, ComposerDraftType, QueueWedgeError, Room as BaseRoom,
|
||||
RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo,
|
||||
RoomMember as BaseRoomMember, RoomMemberships, RoomState, SessionMeta, StateChanges,
|
||||
StateStore, StoreError,
|
||||
};
|
||||
pub use matrix_sdk_common::*;
|
||||
pub use reqwest;
|
||||
|
||||
@@ -40,7 +40,8 @@ use tempfile::{Builder as TempFileBuilder, NamedTempFile, TempDir};
|
||||
use tokio::{fs::File as TokioFile, io::AsyncWriteExt};
|
||||
|
||||
use crate::{
|
||||
attachment::Thumbnail, futures::SendRequest, Client, Error, Result, TransmissionProgress,
|
||||
attachment::Thumbnail, config::RequestConfig, futures::SendRequest, Client, Error, Result,
|
||||
TransmissionProgress,
|
||||
};
|
||||
|
||||
/// A conservative upload speed of 1Mbps
|
||||
@@ -144,8 +145,11 @@ impl Media {
|
||||
/// * `content_type` - The type of the media, this will be used as the
|
||||
/// content-type header.
|
||||
///
|
||||
/// * `reader` - A `Reader` that will be used to fetch the raw bytes of the
|
||||
/// media.
|
||||
/// * `data` - Vector of bytes to be uploaded to the server.
|
||||
///
|
||||
/// * `request_config` - Optional request configuration for the HTTP client,
|
||||
/// overriding the default. If not provided, a reasonable timeout value is
|
||||
/// inferred.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -159,25 +163,38 @@ impl Media {
|
||||
/// # let mut client = Client::new(homeserver).await?;
|
||||
/// let image = fs::read("/home/example/my-cat.jpg")?;
|
||||
///
|
||||
/// let response = client.media().upload(&mime::IMAGE_JPEG, image).await?;
|
||||
/// let response =
|
||||
/// client.media().upload(&mime::IMAGE_JPEG, image, None).await?;
|
||||
///
|
||||
/// println!("Cat URI: {}", response.content_uri);
|
||||
/// # anyhow::Ok(()) };
|
||||
/// ```
|
||||
pub fn upload(&self, content_type: &Mime, data: Vec<u8>) -> SendUploadRequest {
|
||||
let timeout = std::cmp::max(
|
||||
Duration::from_secs(data.len() as u64 / DEFAULT_UPLOAD_SPEED),
|
||||
MIN_UPLOAD_REQUEST_TIMEOUT,
|
||||
);
|
||||
pub fn upload(
|
||||
&self,
|
||||
content_type: &Mime,
|
||||
data: Vec<u8>,
|
||||
request_config: Option<RequestConfig>,
|
||||
) -> SendUploadRequest {
|
||||
let request_config = request_config.unwrap_or_else(|| {
|
||||
self.client.request_config().timeout(Self::reasonable_upload_timeout(&data))
|
||||
});
|
||||
|
||||
let request = assign!(media::create_content::v3::Request::new(data), {
|
||||
content_type: Some(content_type.essence_str().to_owned()),
|
||||
});
|
||||
|
||||
let request_config = self.client.request_config().timeout(timeout);
|
||||
self.client.send(request, Some(request_config))
|
||||
}
|
||||
|
||||
/// Returns a reasonable upload timeout for an upload, based on the size of
|
||||
/// the data to be uploaded.
|
||||
pub(crate) fn reasonable_upload_timeout(data: &[u8]) -> Duration {
|
||||
std::cmp::max(
|
||||
Duration::from_secs(data.len() as u64 / DEFAULT_UPLOAD_SPEED),
|
||||
MIN_UPLOAD_REQUEST_TIMEOUT,
|
||||
)
|
||||
}
|
||||
|
||||
/// Preallocates an MXC URI for a media that will be uploaded soon.
|
||||
///
|
||||
/// This preallocates an URI *before* any content is uploaded to the server.
|
||||
@@ -630,7 +647,7 @@ impl Media {
|
||||
let upload_thumbnail = self.upload_thumbnail(thumbnail, send_progress.clone());
|
||||
|
||||
let upload_attachment = async move {
|
||||
self.upload(content_type, data)
|
||||
self.upload(content_type, data, None)
|
||||
.with_send_progress_observable(send_progress)
|
||||
.await
|
||||
.map_err(Error::from)
|
||||
@@ -653,7 +670,7 @@ impl Media {
|
||||
};
|
||||
|
||||
let response = self
|
||||
.upload(&thumbnail.content_type, thumbnail.data)
|
||||
.upload(&thumbnail.content_type, thumbnail.data, None)
|
||||
.with_send_progress_observable(send_progress)
|
||||
.await?;
|
||||
let url = response.content_uri;
|
||||
|
||||
@@ -2052,7 +2052,7 @@ impl Room {
|
||||
) -> MessageType {
|
||||
// If caption is set, use it as body, and filename as the file name; otherwise,
|
||||
// body is the filename, and the filename is not set.
|
||||
// https://github.com/tulir/matrix-spec-proposals/blob/body-as-caption/proposals/2530-body-as-caption.md
|
||||
// https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2530-body-as-caption.md
|
||||
let (body, filename) = match caption {
|
||||
Some(caption) => (caption, Some(filename.to_owned())),
|
||||
None => (filename.to_owned(), None),
|
||||
@@ -2263,7 +2263,7 @@ impl Room {
|
||||
) -> Result<send_state_event::v3::Response> {
|
||||
self.ensure_room_joined()?;
|
||||
|
||||
let upload_response = self.client.media().upload(mime, data).await?;
|
||||
let upload_response = self.client.media().upload(mime, data, None).await?;
|
||||
let mut info = info.unwrap_or_default();
|
||||
info.blurhash = upload_response.blurhash;
|
||||
info.mimetype = Some(mime.to_string());
|
||||
@@ -2975,6 +2975,10 @@ impl Room {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !self.can_user_trigger_room_notification(self.own_user_id()).await? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.send_call_notification(
|
||||
self.room_id().to_string().to_owned(),
|
||||
ApplicationType::Call,
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
//! This offers a few capabilities for previewing the content of the room as
|
||||
//! well.
|
||||
|
||||
use futures_util::future::join_all;
|
||||
use matrix_sdk_base::{RoomInfo, RoomState};
|
||||
use ruma::{
|
||||
api::client::{membership::joined_members, state::get_state_events},
|
||||
directory::PublicRoomJoinRule,
|
||||
events::room::{history_visibility::HistoryVisibility, join_rules::JoinRule},
|
||||
room::RoomType,
|
||||
space::SpaceRoomJoinRule,
|
||||
@@ -29,7 +31,7 @@ use ruma::{
|
||||
use tokio::try_join;
|
||||
use tracing::{instrument, warn};
|
||||
|
||||
use crate::{Client, Room};
|
||||
use crate::{room_directory_search::RoomDirectorySearch, Client, Room};
|
||||
|
||||
/// The preview of a room, be it invited/joined/left, or not.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -55,6 +57,9 @@ pub struct RoomPreview {
|
||||
/// The number of joined members.
|
||||
pub num_joined_members: u64,
|
||||
|
||||
/// The number of active members, if known (joined + invited).
|
||||
pub num_active_members: Option<u64>,
|
||||
|
||||
/// The room type (space, custom) or nothing, if it's a regular room.
|
||||
pub room_type: Option<RoomType>,
|
||||
|
||||
@@ -83,6 +88,7 @@ impl RoomPreview {
|
||||
room_info: RoomInfo,
|
||||
is_direct: Option<bool>,
|
||||
num_joined_members: u64,
|
||||
num_active_members: Option<u64>,
|
||||
state: Option<RoomState>,
|
||||
) -> Self {
|
||||
RoomPreview {
|
||||
@@ -107,6 +113,7 @@ impl RoomPreview {
|
||||
},
|
||||
is_world_readable: *room_info.history_visibility() == HistoryVisibility::WorldReadable,
|
||||
num_joined_members,
|
||||
num_active_members,
|
||||
state,
|
||||
is_direct,
|
||||
}
|
||||
@@ -121,6 +128,7 @@ impl RoomPreview {
|
||||
room.clone_info(),
|
||||
is_direct,
|
||||
room.joined_members_count(),
|
||||
Some(room.active_members_count()),
|
||||
Some(room.state()),
|
||||
)
|
||||
}
|
||||
@@ -134,21 +142,76 @@ impl RoomPreview {
|
||||
) -> crate::Result<Self> {
|
||||
// Use the room summary endpoint, if available, as described in
|
||||
// https://github.com/deepbluev7/matrix-doc/blob/room-summaries/proposals/3266-room-summary.md
|
||||
match Self::from_room_summary(client, room_id.clone(), room_or_alias_id, via).await {
|
||||
match Self::from_room_summary(client, room_id.clone(), room_or_alias_id, via.clone()).await
|
||||
{
|
||||
Ok(res) => return Ok(res),
|
||||
Err(err) => {
|
||||
warn!("error when previewing room from the room summary endpoint: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: (optimization) Use the room search directory, if available:
|
||||
// - if the room directory visibility is public,
|
||||
// - then use a public room filter set to this room id
|
||||
// Try room directory search next.
|
||||
match Self::from_room_directory_search(client, &room_id, room_or_alias_id, via).await {
|
||||
Ok(Some(res)) => return Ok(res),
|
||||
Ok(None) => warn!("Room '{room_or_alias_id}' not found in room directory search."),
|
||||
Err(err) => {
|
||||
warn!("Searching for '{room_or_alias_id}' in room directory search failed: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
// Resort to using the room state endpoint, as well as the joined members one.
|
||||
Self::from_state_events(client, &room_id).await
|
||||
}
|
||||
|
||||
/// Get a [`RoomPreview`] by searching in the room directory for the
|
||||
/// provided room alias or room id and transforming the [`RoomDescription`]
|
||||
/// into a preview.
|
||||
pub(crate) async fn from_room_directory_search(
|
||||
client: &Client,
|
||||
room_id: &RoomId,
|
||||
room_or_alias_id: &RoomOrAliasId,
|
||||
via: Vec<OwnedServerName>,
|
||||
) -> crate::Result<Option<Self>> {
|
||||
// Get either the room alias or the room id without the leading identifier char
|
||||
let search_term = if room_or_alias_id.is_room_alias_id() {
|
||||
Some(room_or_alias_id.as_str()[1..].to_owned())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// If we have no alias, filtering using a room id is impossible, so just take
|
||||
// the first 100 results and try to find the current room #YOLO
|
||||
let batch_size = if search_term.is_some() { 20 } else { 100 };
|
||||
|
||||
if via.is_empty() {
|
||||
// Just search in the current homeserver
|
||||
search_for_room_preview_in_room_directory(
|
||||
client.clone(),
|
||||
search_term,
|
||||
batch_size,
|
||||
None,
|
||||
room_id,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
let mut futures = Vec::new();
|
||||
// Search for all servers and retrieve the results
|
||||
for server in via {
|
||||
futures.push(search_for_room_preview_in_room_directory(
|
||||
client.clone(),
|
||||
search_term.clone(),
|
||||
batch_size,
|
||||
Some(server),
|
||||
room_id,
|
||||
));
|
||||
}
|
||||
|
||||
let joined_results = join_all(futures).await;
|
||||
|
||||
Ok(joined_results.into_iter().flatten().next().flatten())
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a [`RoomPreview`] using MSC3266, if available on the remote server.
|
||||
///
|
||||
/// Will fail with a 404 if the API is not available.
|
||||
@@ -178,6 +241,8 @@ impl RoomPreview {
|
||||
response.membership.map(|membership| RoomState::from(&membership))
|
||||
};
|
||||
|
||||
let num_active_members = cached_room.as_ref().map(|r| r.active_members_count());
|
||||
|
||||
let is_direct = if let Some(cached_room) = cached_room {
|
||||
cached_room.is_direct().await.ok()
|
||||
} else {
|
||||
@@ -191,6 +256,7 @@ impl RoomPreview {
|
||||
topic: response.topic,
|
||||
avatar_url: response.avatar_url,
|
||||
num_joined_members: response.num_joined_members.into(),
|
||||
num_active_members,
|
||||
room_type: response.room_type,
|
||||
join_rule: response.join_rule,
|
||||
is_world_readable: response.world_readable,
|
||||
@@ -238,8 +304,59 @@ impl RoomPreview {
|
||||
|
||||
let room = client.get_room(room_id);
|
||||
let state = room.as_ref().map(|room| room.state());
|
||||
let num_active_members = room.as_ref().map(|r| r.active_members_count());
|
||||
let is_direct = if let Some(room) = room { room.is_direct().await.ok() } else { None };
|
||||
|
||||
Ok(Self::from_room_info(room_info, is_direct, num_joined_members, state))
|
||||
Ok(Self::from_room_info(
|
||||
room_info,
|
||||
is_direct,
|
||||
num_joined_members,
|
||||
num_active_members,
|
||||
state,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn search_for_room_preview_in_room_directory(
|
||||
client: Client,
|
||||
filter: Option<String>,
|
||||
batch_size: u32,
|
||||
server: Option<OwnedServerName>,
|
||||
expected_room_id: &RoomId,
|
||||
) -> crate::Result<Option<RoomPreview>> {
|
||||
let mut directory_search = RoomDirectorySearch::new(client);
|
||||
directory_search.search(filter, batch_size, server).await?;
|
||||
|
||||
let (results, _) = directory_search.results();
|
||||
|
||||
for room_description in results {
|
||||
// Iterate until we find a room description with a matching room id
|
||||
if room_description.room_id != expected_room_id {
|
||||
continue;
|
||||
}
|
||||
return Ok(Some(RoomPreview {
|
||||
room_id: room_description.room_id,
|
||||
canonical_alias: room_description.alias,
|
||||
name: room_description.name,
|
||||
topic: room_description.topic,
|
||||
avatar_url: room_description.avatar_url,
|
||||
num_joined_members: room_description.joined_members,
|
||||
num_active_members: None,
|
||||
// Assume it's a room
|
||||
room_type: None,
|
||||
join_rule: match room_description.join_rule {
|
||||
PublicRoomJoinRule::Public => SpaceRoomJoinRule::Public,
|
||||
PublicRoomJoinRule::Knock => SpaceRoomJoinRule::Knock,
|
||||
PublicRoomJoinRule::_Custom(rule) => SpaceRoomJoinRule::_Custom(rule),
|
||||
_ => {
|
||||
panic!("Unexpected PublicRoomJoinRule {:?}", room_description.join_rule)
|
||||
}
|
||||
},
|
||||
is_world_readable: room_description.is_world_readable,
|
||||
state: None,
|
||||
is_direct: None,
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
+348
-156
@@ -77,7 +77,7 @@
|
||||
//! no thumbnails):
|
||||
//!
|
||||
//! - The file's content is immediately cached in the
|
||||
//! [`matrix_sdk_base::event_cache_store::EventCacheStore`], using an MXC ID
|
||||
//! [`matrix_sdk_base::event_cache::store::EventCacheStore`], using an MXC ID
|
||||
//! that is temporary and designates a local URI without any possible doubt.
|
||||
//! - An initial media event is created and uses this temporary MXC ID, and
|
||||
//! propagated as a local echo for an event.
|
||||
@@ -129,17 +129,17 @@
|
||||
//! remembered and fixed up into the media event, just before sending it.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
collections::{BTreeMap, HashMap},
|
||||
str::FromStr as _,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, RwLock as SyncRwLock,
|
||||
Arc, RwLock,
|
||||
},
|
||||
};
|
||||
|
||||
use as_variant::as_variant;
|
||||
use matrix_sdk_base::{
|
||||
event_cache_store::EventCacheStoreError,
|
||||
event_cache::store::EventCacheStoreError,
|
||||
media::MediaRequestParameters,
|
||||
store::{
|
||||
ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind,
|
||||
@@ -161,7 +161,7 @@ use ruma::{
|
||||
serde::Raw,
|
||||
OwnedEventId, OwnedRoomId, OwnedTransactionId, TransactionId,
|
||||
};
|
||||
use tokio::sync::{broadcast, Notify, RwLock};
|
||||
use tokio::sync::{broadcast, oneshot, Mutex, Notify, OwnedMutexGuard};
|
||||
use tracing::{debug, error, info, instrument, trace, warn};
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
@@ -171,7 +171,7 @@ use crate::{
|
||||
config::RequestConfig,
|
||||
error::RetryKind,
|
||||
room::{edit::EditedContent, WeakRoom},
|
||||
Client, Room,
|
||||
Client, Media, Room,
|
||||
};
|
||||
|
||||
mod upload;
|
||||
@@ -310,7 +310,7 @@ impl Client {
|
||||
|
||||
pub(super) struct SendQueueData {
|
||||
/// Mapping of room to their unique send queue.
|
||||
rooms: SyncRwLock<BTreeMap<OwnedRoomId, RoomSendQueue>>,
|
||||
rooms: RwLock<BTreeMap<OwnedRoomId, RoomSendQueue>>,
|
||||
|
||||
/// Is the whole mechanism enabled or disabled?
|
||||
///
|
||||
@@ -449,7 +449,7 @@ impl RoomSendQueue {
|
||||
let send_handle = SendHandle {
|
||||
room: self.clone(),
|
||||
transaction_id: transaction_id.clone(),
|
||||
is_upload: false,
|
||||
media_handles: None,
|
||||
};
|
||||
|
||||
let _ = self.inner.updates.send(RoomSendQueueUpdate::NewLocalEvent(LocalEcho {
|
||||
@@ -542,7 +542,7 @@ impl RoomSendQueue {
|
||||
continue;
|
||||
}
|
||||
|
||||
let queued_request = match queue.peek_next_to_send().await {
|
||||
let (queued_request, cancel_upload_rx) = match queue.peek_next_to_send().await {
|
||||
Ok(Some(request)) => request,
|
||||
|
||||
Ok(None) => {
|
||||
@@ -571,8 +571,9 @@ impl RoomSendQueue {
|
||||
continue;
|
||||
};
|
||||
|
||||
match Self::handle_request(&room, queued_request).await {
|
||||
Ok(parent_key) => match queue.mark_as_sent(&txn_id, parent_key.clone()).await {
|
||||
match Self::handle_request(&room, queued_request, cancel_upload_rx).await {
|
||||
Ok(Some(parent_key)) => match queue.mark_as_sent(&txn_id, parent_key.clone()).await
|
||||
{
|
||||
Ok(()) => match parent_key {
|
||||
SentRequestKey::Event(event_id) => {
|
||||
let _ = updates.send(RoomSendQueueUpdate::SentEvent {
|
||||
@@ -594,6 +595,10 @@ impl RoomSendQueue {
|
||||
}
|
||||
},
|
||||
|
||||
Ok(None) => {
|
||||
debug!("Request has been aborted while running, continuing.");
|
||||
}
|
||||
|
||||
Err(err) => {
|
||||
let is_recoverable = match err {
|
||||
crate::Error::Http(ref http_err) => {
|
||||
@@ -661,11 +666,14 @@ impl RoomSendQueue {
|
||||
info!("exited sending task");
|
||||
}
|
||||
|
||||
/// Handles a single request and returns the [`SentRequestKey`] on success.
|
||||
/// Handles a single request and returns the [`SentRequestKey`] on success
|
||||
/// (unless the request was cancelled, in which case it'll return
|
||||
/// `None`).
|
||||
async fn handle_request(
|
||||
room: &Room,
|
||||
request: QueuedRequest,
|
||||
) -> Result<SentRequestKey, crate::Error> {
|
||||
cancel_upload_rx: Option<oneshot::Receiver<()>>,
|
||||
) -> Result<Option<SentRequestKey>, crate::Error> {
|
||||
match request.kind {
|
||||
QueuedRequestKind::Event { content } => {
|
||||
let (event, event_type) = content.raw();
|
||||
@@ -677,7 +685,7 @@ impl RoomSendQueue {
|
||||
.await?;
|
||||
|
||||
trace!(txn_id = %request.transaction_id, event_id = %res.event_id, "event successfully sent");
|
||||
Ok(SentRequestKey::Event(res.event_id))
|
||||
Ok(Some(SentRequestKey::Event(res.event_id)))
|
||||
}
|
||||
|
||||
QueuedRequestKind::MediaUpload {
|
||||
@@ -688,52 +696,83 @@ impl RoomSendQueue {
|
||||
} => {
|
||||
trace!(%relates_to, "uploading media related to event");
|
||||
|
||||
let mime = Mime::from_str(&content_type).map_err(|_| {
|
||||
crate::Error::SendQueueWedgeError(QueueWedgeError::InvalidMimeType {
|
||||
mime_type: content_type.clone(),
|
||||
})
|
||||
})?;
|
||||
let fut = async move {
|
||||
let mime = Mime::from_str(&content_type).map_err(|_| {
|
||||
crate::Error::SendQueueWedgeError(QueueWedgeError::InvalidMimeType {
|
||||
mime_type: content_type.clone(),
|
||||
})
|
||||
})?;
|
||||
|
||||
let data = room
|
||||
.client()
|
||||
.event_cache_store()
|
||||
.lock()
|
||||
.await?
|
||||
.get_media_content(&cache_key)
|
||||
.await?
|
||||
.ok_or(crate::Error::SendQueueWedgeError(
|
||||
QueueWedgeError::MissingMediaContent,
|
||||
))?;
|
||||
let data = room
|
||||
.client()
|
||||
.event_cache_store()
|
||||
.lock()
|
||||
.await?
|
||||
.get_media_content(&cache_key)
|
||||
.await?
|
||||
.ok_or(crate::Error::SendQueueWedgeError(
|
||||
QueueWedgeError::MissingMediaContent,
|
||||
))?;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
let media_source = if room.is_encrypted().await? {
|
||||
trace!("upload will be encrypted (encrypted room)");
|
||||
let mut cursor = std::io::Cursor::new(data);
|
||||
let encrypted_file =
|
||||
room.client().upload_encrypted_file(&mime, &mut cursor).await?;
|
||||
MediaSource::Encrypted(Box::new(encrypted_file))
|
||||
} else {
|
||||
trace!("upload will be in clear text (room without encryption)");
|
||||
let res = room.client().media().upload(&mime, data).await?;
|
||||
MediaSource::Plain(res.content_uri)
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
let media_source = if room.is_encrypted().await? {
|
||||
trace!("upload will be encrypted (encrypted room)");
|
||||
let mut cursor = std::io::Cursor::new(data);
|
||||
let encrypted_file = room
|
||||
.client()
|
||||
.upload_encrypted_file(&mime, &mut cursor)
|
||||
.with_request_config(RequestConfig::short_retry())
|
||||
.await?;
|
||||
MediaSource::Encrypted(Box::new(encrypted_file))
|
||||
} else {
|
||||
trace!("upload will be in clear text (room without encryption)");
|
||||
let request_config = RequestConfig::short_retry()
|
||||
.timeout(Media::reasonable_upload_timeout(&data));
|
||||
let res =
|
||||
room.client().media().upload(&mime, data, Some(request_config)).await?;
|
||||
MediaSource::Plain(res.content_uri)
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "e2e-encryption"))]
|
||||
let media_source = {
|
||||
let request_config = RequestConfig::short_retry()
|
||||
.timeout(Media::reasonable_upload_timeout(&data));
|
||||
let res =
|
||||
room.client().media().upload(&mime, data, Some(request_config)).await?;
|
||||
MediaSource::Plain(res.content_uri)
|
||||
};
|
||||
|
||||
let uri = match &media_source {
|
||||
MediaSource::Plain(uri) => uri,
|
||||
MediaSource::Encrypted(encrypted_file) => &encrypted_file.url,
|
||||
};
|
||||
trace!(%relates_to, mxc_uri = %uri, "media successfully uploaded");
|
||||
|
||||
Ok(SentRequestKey::Media(SentMediaInfo {
|
||||
file: media_source,
|
||||
thumbnail: thumbnail_source,
|
||||
}))
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "e2e-encryption"))]
|
||||
let media_source = {
|
||||
let res = room.client().media().upload(&mime, data).await?;
|
||||
MediaSource::Plain(res.content_uri)
|
||||
let wait_for_cancel = async move {
|
||||
if let Some(rx) = cancel_upload_rx {
|
||||
rx.await
|
||||
} else {
|
||||
std::future::pending().await
|
||||
}
|
||||
};
|
||||
|
||||
let uri = match &media_source {
|
||||
MediaSource::Plain(uri) => uri,
|
||||
MediaSource::Encrypted(encrypted_file) => &encrypted_file.url,
|
||||
};
|
||||
trace!(%relates_to, mxc_uri = %uri, "media successfully uploaded");
|
||||
tokio::select! {
|
||||
biased;
|
||||
|
||||
Ok(SentRequestKey::Media(SentMediaInfo {
|
||||
file: media_source,
|
||||
thumbnail: thumbnail_source,
|
||||
}))
|
||||
_ = wait_for_cancel => {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
res = fut => {
|
||||
res.map(Some)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -753,26 +792,6 @@ impl RoomSendQueue {
|
||||
self.inner.notifier.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
/// Unwedge a local echo identified by its transaction identifier and try to
|
||||
/// resend it.
|
||||
pub async fn unwedge(&self, transaction_id: &TransactionId) -> Result<(), RoomSendQueueError> {
|
||||
self.inner
|
||||
.queue
|
||||
.mark_as_unwedged(transaction_id)
|
||||
.await
|
||||
.map_err(RoomSendQueueError::StorageError)?;
|
||||
|
||||
// Wake up the queue, in case the room was asleep before unwedging the request.
|
||||
self.inner.notifier.notify_one();
|
||||
|
||||
let _ = self
|
||||
.inner
|
||||
.updates
|
||||
.send(RoomSendQueueUpdate::RetryEvent { transaction_id: transaction_id.to_owned() });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&crate::Error> for QueueWedgeError {
|
||||
@@ -833,32 +852,96 @@ struct RoomSendQueueInner {
|
||||
_task: JoinHandle<()>,
|
||||
}
|
||||
|
||||
/// Information about a request being sent right this moment.
|
||||
struct BeingSentInfo {
|
||||
/// Transaction id of the thing being sent.
|
||||
transaction_id: OwnedTransactionId,
|
||||
|
||||
/// For an upload request, a trigger to cancel the upload before it
|
||||
/// completes.
|
||||
cancel_upload: Option<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
impl BeingSentInfo {
|
||||
/// Aborts the upload, if a trigger is available.
|
||||
///
|
||||
/// Consumes the object because the sender is a oneshot and will be consumed
|
||||
/// upon sending.
|
||||
fn cancel_upload(self) -> bool {
|
||||
if let Some(cancel_upload) = self.cancel_upload {
|
||||
let _ = cancel_upload.send(());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A specialized lock that guards both against the state store and the
|
||||
/// [`Self::being_sent`] data.
|
||||
#[derive(Clone)]
|
||||
struct QueueStorage {
|
||||
struct StoreLock {
|
||||
/// Reference to the client, to get access to the underlying store.
|
||||
client: WeakClient,
|
||||
|
||||
/// To which room is this storage related.
|
||||
room_id: OwnedRoomId,
|
||||
|
||||
/// All the queued requests that are being sent at the moment.
|
||||
/// The one queued request that is being sent at the moment, along with
|
||||
/// associated data that can be useful to act upon it.
|
||||
///
|
||||
/// It also serves as an internal lock on the storage backend.
|
||||
being_sent: Arc<RwLock<BTreeSet<OwnedTransactionId>>>,
|
||||
/// Also used as the lock to access the state store.
|
||||
being_sent: Arc<Mutex<Option<BeingSentInfo>>>,
|
||||
}
|
||||
|
||||
impl QueueStorage {
|
||||
/// Create a new queue for queuing requests to be sent later.
|
||||
fn new(client: WeakClient, room: OwnedRoomId) -> Self {
|
||||
Self { room_id: room, being_sent: Default::default(), client }
|
||||
impl StoreLock {
|
||||
/// Gets a hold of the locked store and [`Self::being_sent`] pair.
|
||||
async fn lock(&self) -> StoreLockGuard {
|
||||
StoreLockGuard {
|
||||
client: self.client.clone(),
|
||||
being_sent: self.being_sent.clone().lock_owned().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Small helper to get a strong Client from the weak one.
|
||||
/// A lock guard obtained through locking with [`StoreLock`].
|
||||
/// `being_sent` data.
|
||||
struct StoreLockGuard {
|
||||
/// Reference to the client, to get access to the underlying store.
|
||||
client: WeakClient,
|
||||
|
||||
/// The one queued request that is being sent at the moment, along with
|
||||
/// associated data that can be useful to act upon it.
|
||||
being_sent: OwnedMutexGuard<Option<BeingSentInfo>>,
|
||||
}
|
||||
|
||||
impl StoreLockGuard {
|
||||
/// Get a client from the locked state, useful to get a handle on a store.
|
||||
fn client(&self) -> Result<Client, RoomSendQueueStorageError> {
|
||||
self.client.get().ok_or(RoomSendQueueStorageError::ClientShuttingDown)
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a new event to be sent in the queue.
|
||||
#[derive(Clone)]
|
||||
struct QueueStorage {
|
||||
/// A lock to make sure the state store is only accessed once at a time, to
|
||||
/// make some store operations atomic.
|
||||
store: StoreLock,
|
||||
|
||||
/// To which room is this storage related.
|
||||
room_id: OwnedRoomId,
|
||||
}
|
||||
|
||||
impl QueueStorage {
|
||||
/// Default priority for a queued request.
|
||||
const LOW_PRIORITY: usize = 0;
|
||||
|
||||
/// High priority for a queued request that must be handled before others.
|
||||
const HIGH_PRIORITY: usize = 10;
|
||||
|
||||
/// Create a new queue for queuing requests to be sent later.
|
||||
fn new(client: WeakClient, room: OwnedRoomId) -> Self {
|
||||
Self { room_id: room, store: StoreLock { client, being_sent: Default::default() } }
|
||||
}
|
||||
|
||||
/// Push a new event to be sent in the queue, with a default priority of 0.
|
||||
///
|
||||
/// Returns the transaction id chosen to identify the request.
|
||||
async fn push(
|
||||
@@ -867,9 +950,17 @@ impl QueueStorage {
|
||||
) -> Result<OwnedTransactionId, RoomSendQueueStorageError> {
|
||||
let transaction_id = TransactionId::new();
|
||||
|
||||
self.client()?
|
||||
self.store
|
||||
.lock()
|
||||
.await
|
||||
.client()?
|
||||
.store()
|
||||
.save_send_queue_request(&self.room_id, transaction_id.clone(), request)
|
||||
.save_send_queue_request(
|
||||
&self.room_id,
|
||||
transaction_id.clone(),
|
||||
request,
|
||||
Self::LOW_PRIORITY,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(transaction_id)
|
||||
@@ -879,17 +970,36 @@ impl QueueStorage {
|
||||
///
|
||||
/// It is required to call [`Self::mark_as_sent`] after it's been
|
||||
/// effectively sent.
|
||||
async fn peek_next_to_send(&self) -> Result<Option<QueuedRequest>, RoomSendQueueStorageError> {
|
||||
// Keep the lock until we're done touching the storage.
|
||||
let mut being_sent = self.being_sent.write().await;
|
||||
|
||||
async fn peek_next_to_send(
|
||||
&self,
|
||||
) -> Result<Option<(QueuedRequest, Option<oneshot::Receiver<()>>)>, RoomSendQueueStorageError>
|
||||
{
|
||||
let mut guard = self.store.lock().await;
|
||||
let queued_requests =
|
||||
self.client()?.store().load_send_queue_requests(&self.room_id).await?;
|
||||
guard.client()?.store().load_send_queue_requests(&self.room_id).await?;
|
||||
|
||||
if let Some(request) = queued_requests.iter().find(|queued| !queued.is_wedged()) {
|
||||
being_sent.insert(request.transaction_id.clone());
|
||||
let (cancel_upload_tx, cancel_upload_rx) =
|
||||
if matches!(request.kind, QueuedRequestKind::MediaUpload { .. }) {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
(Some(tx), Some(rx))
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
Ok(Some(request.clone()))
|
||||
let prev = guard.being_sent.replace(BeingSentInfo {
|
||||
transaction_id: request.transaction_id.clone(),
|
||||
cancel_upload: cancel_upload_tx,
|
||||
});
|
||||
|
||||
if let Some(prev) = prev {
|
||||
error!(
|
||||
prev_txn = ?prev.transaction_id,
|
||||
"a previous request was still active while picking a new one"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Some((request.clone(), cancel_upload_rx)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
@@ -899,7 +1009,12 @@ impl QueueStorage {
|
||||
/// with the given transaction id as not being sent anymore, so it can
|
||||
/// be removed from the queue later.
|
||||
async fn mark_as_not_being_sent(&self, transaction_id: &TransactionId) {
|
||||
self.being_sent.write().await.remove(transaction_id);
|
||||
let was_being_sent = self.store.lock().await.being_sent.take();
|
||||
|
||||
let prev_txn = was_being_sent.as_ref().map(|info| info.transaction_id.as_ref());
|
||||
if prev_txn != Some(transaction_id) {
|
||||
error!(prev_txn = ?prev_txn, "previous active request didn't match that we expect (after transient error)");
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks a request popped with [`Self::peek_next_to_send`] and identified
|
||||
@@ -911,10 +1026,15 @@ impl QueueStorage {
|
||||
reason: QueueWedgeError,
|
||||
) -> Result<(), RoomSendQueueStorageError> {
|
||||
// Keep the lock until we're done touching the storage.
|
||||
let mut being_sent = self.being_sent.write().await;
|
||||
being_sent.remove(transaction_id);
|
||||
let mut guard = self.store.lock().await;
|
||||
let was_being_sent = guard.being_sent.take();
|
||||
|
||||
Ok(self
|
||||
let prev_txn = was_being_sent.as_ref().map(|info| info.transaction_id.as_ref());
|
||||
if prev_txn != Some(transaction_id) {
|
||||
error!(prev_txn = ?prev_txn, "previous active request didn't match that we expect (after permanent error)");
|
||||
}
|
||||
|
||||
Ok(guard
|
||||
.client()?
|
||||
.store()
|
||||
.update_send_queue_request_status(&self.room_id, transaction_id, Some(reason))
|
||||
@@ -928,6 +1048,9 @@ impl QueueStorage {
|
||||
transaction_id: &TransactionId,
|
||||
) -> Result<(), RoomSendQueueStorageError> {
|
||||
Ok(self
|
||||
.store
|
||||
.lock()
|
||||
.await
|
||||
.client()?
|
||||
.store()
|
||||
.update_send_queue_request_status(&self.room_id, transaction_id, None)
|
||||
@@ -942,10 +1065,15 @@ impl QueueStorage {
|
||||
parent_key: SentRequestKey,
|
||||
) -> Result<(), RoomSendQueueStorageError> {
|
||||
// Keep the lock until we're done touching the storage.
|
||||
let mut being_sent = self.being_sent.write().await;
|
||||
being_sent.remove(transaction_id);
|
||||
let mut guard = self.store.lock().await;
|
||||
let was_being_sent = guard.being_sent.take();
|
||||
|
||||
let client = self.client()?;
|
||||
let prev_txn = was_being_sent.as_ref().map(|info| info.transaction_id.as_ref());
|
||||
if prev_txn != Some(transaction_id) {
|
||||
error!(prev_txn = ?prev_txn, "previous active request didn't match that we expect (after successful send");
|
||||
}
|
||||
|
||||
let client = guard.client()?;
|
||||
let store = client.store();
|
||||
|
||||
// Update all dependent requests.
|
||||
@@ -970,12 +1098,14 @@ impl QueueStorage {
|
||||
&self,
|
||||
transaction_id: &TransactionId,
|
||||
) -> Result<bool, RoomSendQueueStorageError> {
|
||||
// Keep the lock until we're done touching the storage.
|
||||
let being_sent = self.being_sent.read().await;
|
||||
let guard = self.store.lock().await;
|
||||
|
||||
if being_sent.contains(transaction_id) {
|
||||
if guard.being_sent.as_ref().map(|info| info.transaction_id.as_ref())
|
||||
== Some(transaction_id)
|
||||
{
|
||||
// Save the intent to redact the event.
|
||||
self.client()?
|
||||
guard
|
||||
.client()?
|
||||
.store()
|
||||
.save_dependent_queued_request(
|
||||
&self.room_id,
|
||||
@@ -988,8 +1118,11 @@ impl QueueStorage {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let removed =
|
||||
self.client()?.store().remove_send_queue_request(&self.room_id, transaction_id).await?;
|
||||
let removed = guard
|
||||
.client()?
|
||||
.store()
|
||||
.remove_send_queue_request(&self.room_id, transaction_id)
|
||||
.await?;
|
||||
|
||||
Ok(removed)
|
||||
}
|
||||
@@ -1005,12 +1138,14 @@ impl QueueStorage {
|
||||
transaction_id: &TransactionId,
|
||||
serializable: SerializableEventContent,
|
||||
) -> Result<bool, RoomSendQueueStorageError> {
|
||||
// Keep the lock until we're done touching the storage.
|
||||
let being_sent = self.being_sent.read().await;
|
||||
let guard = self.store.lock().await;
|
||||
|
||||
if being_sent.contains(transaction_id) {
|
||||
if guard.being_sent.as_ref().map(|info| info.transaction_id.as_ref())
|
||||
== Some(transaction_id)
|
||||
{
|
||||
// Save the intent to edit the associated event.
|
||||
self.client()?
|
||||
guard
|
||||
.client()?
|
||||
.store()
|
||||
.save_dependent_queued_request(
|
||||
&self.room_id,
|
||||
@@ -1023,7 +1158,7 @@ impl QueueStorage {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let edited = self
|
||||
let edited = guard
|
||||
.client()?
|
||||
.store()
|
||||
.update_send_queue_request(&self.room_id, transaction_id, serializable.into())
|
||||
@@ -1044,12 +1179,8 @@ impl QueueStorage {
|
||||
file_media_request: MediaRequestParameters,
|
||||
thumbnail: Option<(FinishUploadThumbnailInfo, MediaRequestParameters, Mime)>,
|
||||
) -> Result<(), RoomSendQueueStorageError> {
|
||||
// Keep the lock until we're done touching the storage.
|
||||
// TODO refactor to make the relationship between being_sent and the store more
|
||||
// obvious.
|
||||
let _guard = self.being_sent.read().await;
|
||||
|
||||
let client = self.client()?;
|
||||
let guard = self.store.lock().await;
|
||||
let client = guard.client()?;
|
||||
let store = client.store();
|
||||
|
||||
let thumbnail_info =
|
||||
@@ -1069,6 +1200,7 @@ impl QueueStorage {
|
||||
thumbnail_source: None, // the thumbnail has no thumbnails :)
|
||||
related_to: send_event_txn.clone(),
|
||||
},
|
||||
Self::LOW_PRIORITY,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1099,6 +1231,7 @@ impl QueueStorage {
|
||||
thumbnail_source: None,
|
||||
related_to: send_event_txn.clone(),
|
||||
},
|
||||
Self::LOW_PRIORITY,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1129,7 +1262,8 @@ impl QueueStorage {
|
||||
transaction_id: &TransactionId,
|
||||
key: String,
|
||||
) -> Result<Option<ChildTransactionId>, RoomSendQueueStorageError> {
|
||||
let client = self.client()?;
|
||||
let guard = self.store.lock().await;
|
||||
let client = guard.client()?;
|
||||
let store = client.store();
|
||||
|
||||
let requests = store.load_send_queue_requests(&self.room_id).await?;
|
||||
@@ -1159,7 +1293,8 @@ impl QueueStorage {
|
||||
&self,
|
||||
room: &RoomSendQueue,
|
||||
) -> Result<Vec<LocalEcho>, RoomSendQueueStorageError> {
|
||||
let client = self.client()?;
|
||||
let guard = self.store.lock().await;
|
||||
let client = guard.client()?;
|
||||
let store = client.store();
|
||||
|
||||
let local_requests =
|
||||
@@ -1172,7 +1307,7 @@ impl QueueStorage {
|
||||
send_handle: SendHandle {
|
||||
room: room.clone(),
|
||||
transaction_id: queued.transaction_id,
|
||||
is_upload: false,
|
||||
media_handles: None,
|
||||
},
|
||||
send_error: queued.error,
|
||||
},
|
||||
@@ -1216,8 +1351,8 @@ impl QueueStorage {
|
||||
|
||||
DependentQueuedRequestKind::FinishUpload {
|
||||
local_echo,
|
||||
file_upload: _,
|
||||
thumbnail_info: _,
|
||||
file_upload,
|
||||
thumbnail_info,
|
||||
} => {
|
||||
// Materialize as an event local echo.
|
||||
Some(LocalEcho {
|
||||
@@ -1225,11 +1360,13 @@ impl QueueStorage {
|
||||
content: LocalEchoContent::Event {
|
||||
serialized_event: SerializableEventContent::new(&local_echo.into())
|
||||
.ok()?,
|
||||
// TODO this should be a `SendAttachmentHandle`!
|
||||
send_handle: SendHandle {
|
||||
room: room.clone(),
|
||||
transaction_id: dep.own_transaction_id.into(),
|
||||
is_upload: true,
|
||||
media_handles: Some(MediaHandles {
|
||||
upload_thumbnail_txn: thumbnail_info.map(|info| info.txn),
|
||||
upload_file_txn: file_upload,
|
||||
}),
|
||||
},
|
||||
send_error: None,
|
||||
},
|
||||
@@ -1320,6 +1457,7 @@ impl QueueStorage {
|
||||
&self.room_id,
|
||||
dependent_request.own_transaction_id.into(),
|
||||
serializable.into(),
|
||||
Self::HIGH_PRIORITY,
|
||||
)
|
||||
.await
|
||||
.map_err(RoomSendQueueStorageError::StateStoreError)?;
|
||||
@@ -1406,6 +1544,7 @@ impl QueueStorage {
|
||||
&self.room_id,
|
||||
dependent_request.own_transaction_id.into(),
|
||||
serializable.into(),
|
||||
Self::HIGH_PRIORITY,
|
||||
)
|
||||
.await
|
||||
.map_err(RoomSendQueueStorageError::StateStoreError)?;
|
||||
@@ -1465,10 +1604,9 @@ impl QueueStorage {
|
||||
&self,
|
||||
new_updates: &mut Vec<RoomSendQueueUpdate>,
|
||||
) -> Result<(), RoomSendQueueError> {
|
||||
// Keep the lock until we're done touching the storage.
|
||||
let _being_sent = self.being_sent.read().await;
|
||||
let guard = self.store.lock().await;
|
||||
|
||||
let client = self.client()?;
|
||||
let client = guard.client()?;
|
||||
let store = client.store();
|
||||
|
||||
let dependent_requests = store
|
||||
@@ -1539,10 +1677,10 @@ impl QueueStorage {
|
||||
&self,
|
||||
dependent_event_id: &ChildTransactionId,
|
||||
) -> Result<bool, RoomSendQueueStorageError> {
|
||||
// Keep the lock until we're done touching the storage.
|
||||
let _being_sent = self.being_sent.read().await;
|
||||
|
||||
Ok(self
|
||||
.store
|
||||
.lock()
|
||||
.await
|
||||
.client()?
|
||||
.store()
|
||||
.remove_dependent_queued_request(&self.room_id, dependent_event_id)
|
||||
@@ -1706,18 +1844,37 @@ pub enum RoomSendQueueStorageError {
|
||||
OperationNotImplementedYet,
|
||||
}
|
||||
|
||||
/// Extra transaction IDs useful during an upload.
|
||||
#[derive(Clone, Debug)]
|
||||
struct MediaHandles {
|
||||
/// Transaction id used when uploading the thumbnail.
|
||||
///
|
||||
/// Optional because a media can be uploaded without a thumbnail.
|
||||
upload_thumbnail_txn: Option<OwnedTransactionId>,
|
||||
|
||||
/// Transaction id used when uploading the media itself.
|
||||
upload_file_txn: OwnedTransactionId,
|
||||
}
|
||||
|
||||
/// A handle to manipulate an event that was scheduled to be sent to a room.
|
||||
// TODO (bnjbvr): consider renaming `SendEventHandle`, unless we can reuse it for medias too.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SendHandle {
|
||||
/// Link to the send queue used to send this request.
|
||||
room: RoomSendQueue,
|
||||
|
||||
/// Transaction id used for the sent request.
|
||||
///
|
||||
/// If this is a media upload, this is the "main" transaction id, i.e. the
|
||||
/// one used to send the event, and that will be seen by observers.
|
||||
transaction_id: OwnedTransactionId,
|
||||
is_upload: bool,
|
||||
|
||||
/// Additional handles for a media upload.
|
||||
media_handles: Option<MediaHandles>,
|
||||
}
|
||||
|
||||
impl SendHandle {
|
||||
fn nyi_for_uploads(&self) -> Result<(), RoomSendQueueStorageError> {
|
||||
if self.is_upload {
|
||||
if self.media_handles.is_some() {
|
||||
Err(RoomSendQueueStorageError::OperationNotImplementedYet)
|
||||
} else {
|
||||
Ok(())
|
||||
@@ -1731,9 +1888,25 @@ impl SendHandle {
|
||||
#[instrument(skip(self), fields(room_id = %self.room.inner.room.room_id(), txn_id = %self.transaction_id))]
|
||||
pub async fn abort(&self) -> Result<bool, RoomSendQueueStorageError> {
|
||||
trace!("received an abort request");
|
||||
self.nyi_for_uploads()?;
|
||||
|
||||
if self.room.inner.queue.cancel_event(&self.transaction_id).await? {
|
||||
let queue = &self.room.inner.queue;
|
||||
|
||||
if let Some(handles) = &self.media_handles {
|
||||
if queue.abort_upload(&self.transaction_id, handles).await? {
|
||||
// Propagate a cancelled update.
|
||||
let _ = self.room.inner.updates.send(RoomSendQueueUpdate::CancelledLocalEvent {
|
||||
transaction_id: self.transaction_id.clone(),
|
||||
});
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// If it failed, it means the sending of the event is not a
|
||||
// dependent request anymore. Fall back to the regular
|
||||
// code path below, that handles aborting sending of an event.
|
||||
}
|
||||
|
||||
if queue.cancel_event(&self.transaction_id).await? {
|
||||
trace!("successful abort");
|
||||
|
||||
// Propagate a cancelled update too.
|
||||
@@ -1797,6 +1970,43 @@ impl SendHandle {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Unwedge a local echo identified by its transaction identifier and try to
|
||||
/// resend it.
|
||||
pub async fn unwedge(&self) -> Result<(), RoomSendQueueError> {
|
||||
let room = &self.room.inner;
|
||||
room.queue
|
||||
.mark_as_unwedged(&self.transaction_id)
|
||||
.await
|
||||
.map_err(RoomSendQueueError::StorageError)?;
|
||||
|
||||
// If we have media handles, also try to unwedge them.
|
||||
//
|
||||
// It's fine to always do it to *all* the transaction IDs at once, because only
|
||||
// one of the three requests will be active at the same time, i.e. only
|
||||
// one entry will be updated in the store. The other two are either
|
||||
// done, or dependent requests.
|
||||
|
||||
if let Some(handles) = &self.media_handles {
|
||||
room.queue
|
||||
.mark_as_unwedged(&handles.upload_file_txn)
|
||||
.await
|
||||
.map_err(RoomSendQueueError::StorageError)?;
|
||||
|
||||
if let Some(txn) = &handles.upload_thumbnail_txn {
|
||||
room.queue.mark_as_unwedged(txn).await.map_err(RoomSendQueueError::StorageError)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Wake up the queue, in case the room was asleep before unwedging the request.
|
||||
room.notifier.notify_one();
|
||||
|
||||
let _ = room
|
||||
.updates
|
||||
.send(RoomSendQueueUpdate::RetryEvent { transaction_id: self.transaction_id.clone() });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a reaction to the event as soon as it's sent.
|
||||
///
|
||||
/// If returning `Ok(None)`; this means the reaction couldn't be sent
|
||||
@@ -1872,7 +2082,7 @@ impl SendReactionHandle {
|
||||
let handle = SendHandle {
|
||||
room: self.room.clone(),
|
||||
transaction_id: self.transaction_id.clone().into(),
|
||||
is_upload: false,
|
||||
media_handles: None,
|
||||
};
|
||||
|
||||
handle.abort().await
|
||||
@@ -1884,24 +2094,6 @@ impl SendReactionHandle {
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle to execute actions while sending an attachment.
|
||||
///
|
||||
/// In the future, this may support cancellation, subscribing to progress, etc.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SendAttachmentHandle {
|
||||
/// Reference to the send queue for the room where this attachment was sent.
|
||||
_room: RoomSendQueue,
|
||||
|
||||
/// Transaction id for the sending of the event itself.
|
||||
_transaction_id: OwnedTransactionId,
|
||||
|
||||
/// Transaction id for the file upload.
|
||||
_file_upload: OwnedTransactionId,
|
||||
|
||||
/// Transaction id for the thumbnail upload.
|
||||
_thumbnail_transaction_id: Option<OwnedTransactionId>,
|
||||
}
|
||||
|
||||
/// From a given source of [`DependentQueuedRequest`], return only the most
|
||||
/// meaningful, i.e. the ones that wouldn't be overridden after applying the
|
||||
/// others.
|
||||
|
||||
@@ -33,20 +33,21 @@ use ruma::{
|
||||
};
|
||||
use tracing::{debug, error, instrument, trace, warn, Span};
|
||||
|
||||
use super::{QueueStorage, RoomSendQueue, RoomSendQueueError, SendAttachmentHandle};
|
||||
use super::{QueueStorage, RoomSendQueue, RoomSendQueueError};
|
||||
use crate::{
|
||||
attachment::AttachmentConfig,
|
||||
send_queue::{
|
||||
LocalEcho, LocalEchoContent, RoomSendQueueStorageError, RoomSendQueueUpdate, SendHandle,
|
||||
LocalEcho, LocalEchoContent, MediaHandles, RoomSendQueueStorageError, RoomSendQueueUpdate,
|
||||
SendHandle,
|
||||
},
|
||||
Client, Room,
|
||||
};
|
||||
|
||||
/// Create a [`MediaRequest`] for a file we want to store locally before
|
||||
/// sending it.
|
||||
/// Create an [`OwnedMxcUri`] for a file or thumbnail we want to store locally
|
||||
/// before sending it.
|
||||
///
|
||||
/// This uses a MXC ID that is only locally valid.
|
||||
fn make_local_file_media_request(txn_id: &TransactionId) -> MediaRequestParameters {
|
||||
fn make_local_uri(txn_id: &TransactionId) -> OwnedMxcUri {
|
||||
// This mustn't represent a potentially valid media server, otherwise it'd be
|
||||
// possible for an attacker to return malicious content under some
|
||||
// preconditions (e.g. the cache store has been cleared before the upload
|
||||
@@ -54,10 +55,16 @@ fn make_local_file_media_request(txn_id: &TransactionId) -> MediaRequestParamete
|
||||
// which is guaranteed to be on the local machine. As a result, the only attack
|
||||
// possible would be coming from the user themselves, which we consider a
|
||||
// non-threat.
|
||||
OwnedMxcUri::from(format!("mxc://send-queue.localhost/{txn_id}"))
|
||||
}
|
||||
|
||||
/// Create a [`MediaRequest`] for a file we want to store locally before
|
||||
/// sending it.
|
||||
///
|
||||
/// This uses a MXC ID that is only locally valid.
|
||||
fn make_local_file_media_request(txn_id: &TransactionId) -> MediaRequestParameters {
|
||||
MediaRequestParameters {
|
||||
source: MediaSource::Plain(OwnedMxcUri::from(format!(
|
||||
"mxc://send-queue.localhost/{txn_id}"
|
||||
))),
|
||||
source: MediaSource::Plain(make_local_uri(txn_id)),
|
||||
format: MediaFormat::File,
|
||||
}
|
||||
}
|
||||
@@ -73,9 +80,7 @@ fn make_local_thumbnail_media_request(
|
||||
) -> MediaRequestParameters {
|
||||
// See comment in [`make_local_file_media_request`].
|
||||
MediaRequestParameters {
|
||||
source: MediaSource::Plain(OwnedMxcUri::from(format!(
|
||||
"mxc://send-queue.localhost/{txn_id}"
|
||||
))),
|
||||
source: MediaSource::Plain(make_local_uri(txn_id)),
|
||||
format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(width, height)),
|
||||
}
|
||||
}
|
||||
@@ -141,7 +146,7 @@ impl RoomSendQueue {
|
||||
content_type: Mime,
|
||||
data: Vec<u8>,
|
||||
mut config: AttachmentConfig,
|
||||
) -> Result<SendAttachmentHandle, RoomSendQueueError> {
|
||||
) -> Result<SendHandle, RoomSendQueueError> {
|
||||
let Some(room) = self.inner.room.get() else {
|
||||
return Err(RoomSendQueueError::RoomDisappeared);
|
||||
};
|
||||
@@ -249,27 +254,23 @@ impl RoomSendQueue {
|
||||
|
||||
self.inner.notifier.notify_one();
|
||||
|
||||
let send_handle = SendHandle {
|
||||
room: self.clone(),
|
||||
transaction_id: send_event_txn.clone().into(),
|
||||
media_handles: Some(MediaHandles { upload_thumbnail_txn, upload_file_txn }),
|
||||
};
|
||||
|
||||
let _ = self.inner.updates.send(RoomSendQueueUpdate::NewLocalEvent(LocalEcho {
|
||||
transaction_id: send_event_txn.clone().into(),
|
||||
content: LocalEchoContent::Event {
|
||||
serialized_event: SerializableEventContent::new(&event_content.into())
|
||||
.map_err(RoomSendQueueStorageError::JsonSerialization)?,
|
||||
// TODO: this should be a `SendAttachmentHandle`!
|
||||
send_handle: SendHandle {
|
||||
room: self.clone(),
|
||||
transaction_id: send_event_txn.clone().into(),
|
||||
is_upload: true,
|
||||
},
|
||||
send_handle: send_handle.clone(),
|
||||
send_error: None,
|
||||
},
|
||||
}));
|
||||
|
||||
Ok(SendAttachmentHandle {
|
||||
_room: self.clone(),
|
||||
_transaction_id: send_event_txn.into(),
|
||||
_file_upload: upload_file_txn,
|
||||
_thumbnail_transaction_id: upload_thumbnail_txn,
|
||||
})
|
||||
Ok(send_handle)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,18 +323,27 @@ impl QueueStorage {
|
||||
let from_req =
|
||||
make_local_thumbnail_media_request(&info.txn, info.height, info.width);
|
||||
|
||||
trace!( from = ?from_req.source, to = ?new_source, "renaming thumbnail file key in cache store");
|
||||
if info.height == uint!(0) || info.width == uint!(0) {
|
||||
trace!(from = ?from_req.source, "removing thumbnail with unknown dimension from cache store");
|
||||
|
||||
// Reuse the same format for the cached thumbnail with the final MXC ID.
|
||||
let new_format = from_req.format.clone();
|
||||
cache_store
|
||||
.remove_media_content(&from_req)
|
||||
.await
|
||||
.map_err(RoomSendQueueStorageError::EventCacheStoreError)?;
|
||||
} else {
|
||||
trace!(from = ?from_req.source, to = ?new_source, "renaming thumbnail file key in cache store");
|
||||
|
||||
cache_store
|
||||
.replace_media_key(
|
||||
&from_req,
|
||||
&MediaRequestParameters { source: new_source, format: new_format },
|
||||
)
|
||||
.await
|
||||
.map_err(RoomSendQueueStorageError::EventCacheStoreError)?;
|
||||
// Reuse the same format for the cached thumbnail with the final MXC ID.
|
||||
let new_format = from_req.format.clone();
|
||||
|
||||
cache_store
|
||||
.replace_media_key(
|
||||
&from_req,
|
||||
&MediaRequestParameters { source: new_source, format: new_format },
|
||||
)
|
||||
.await
|
||||
.map_err(RoomSendQueueStorageError::EventCacheStoreError)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,7 +363,12 @@ impl QueueStorage {
|
||||
|
||||
client
|
||||
.store()
|
||||
.save_send_queue_request(&self.room_id, event_txn, new_content.into())
|
||||
.save_send_queue_request(
|
||||
&self.room_id,
|
||||
event_txn,
|
||||
new_content.into(),
|
||||
Self::HIGH_PRIORITY,
|
||||
)
|
||||
.await
|
||||
.map_err(RoomSendQueueStorageError::StateStoreError)?;
|
||||
|
||||
@@ -395,10 +410,136 @@ impl QueueStorage {
|
||||
|
||||
client
|
||||
.store()
|
||||
.save_send_queue_request(&self.room_id, next_upload_txn, request)
|
||||
.save_send_queue_request(&self.room_id, next_upload_txn, request, Self::HIGH_PRIORITY)
|
||||
.await
|
||||
.map_err(RoomSendQueueStorageError::StateStoreError)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Try to abort an upload that would be ongoing.
|
||||
///
|
||||
/// Return true if any media (media itself or its thumbnail) was being
|
||||
/// uploaded. In this case, the media event has also been removed from
|
||||
/// the send queue. If it returns false, then the uploads already
|
||||
/// happened, and the event sending *may* have started.
|
||||
#[instrument(skip(self, handles))]
|
||||
pub(super) async fn abort_upload(
|
||||
&self,
|
||||
event_txn: &TransactionId,
|
||||
handles: &MediaHandles,
|
||||
) -> Result<bool, RoomSendQueueStorageError> {
|
||||
let mut guard = self.store.lock().await;
|
||||
let client = guard.client()?;
|
||||
|
||||
// Keep the lock until we're done touching the storage.
|
||||
debug!("trying to abort an upload");
|
||||
|
||||
let store = client.store();
|
||||
|
||||
let upload_file_as_dependent = ChildTransactionId::from(handles.upload_file_txn.clone());
|
||||
let event_as_dependent = ChildTransactionId::from(event_txn.to_owned());
|
||||
|
||||
let mut removed_dependent_upload = false;
|
||||
let mut removed_dependent_event = false;
|
||||
|
||||
if let Some(thumbnail_txn) = &handles.upload_thumbnail_txn {
|
||||
if store.remove_send_queue_request(&self.room_id, thumbnail_txn).await? {
|
||||
// The thumbnail upload existed as a request: either it was pending (something
|
||||
// else was being sent), or it was actively being sent.
|
||||
trace!("could remove thumbnail request, removing 2 dependent requests now");
|
||||
|
||||
// 1. Try to abort sending using the being_sent info, in case it was active.
|
||||
if let Some(info) = guard.being_sent.as_ref() {
|
||||
if info.transaction_id == *thumbnail_txn {
|
||||
// SAFETY: we knew it was Some(), two lines above.
|
||||
let info = guard.being_sent.take().unwrap();
|
||||
if info.cancel_upload() {
|
||||
trace!("aborted ongoing thumbnail upload");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Remove the dependent requests.
|
||||
removed_dependent_upload = store
|
||||
.remove_dependent_queued_request(&self.room_id, &upload_file_as_dependent)
|
||||
.await?;
|
||||
|
||||
if !removed_dependent_upload {
|
||||
warn!("unable to find the dependent file upload request");
|
||||
}
|
||||
|
||||
removed_dependent_event = store
|
||||
.remove_dependent_queued_request(&self.room_id, &event_as_dependent)
|
||||
.await?;
|
||||
|
||||
if !removed_dependent_event {
|
||||
warn!("unable to find the dependent media event upload request");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we're here:
|
||||
// - either there was no thumbnail to upload,
|
||||
// - or the thumbnail request has terminated already.
|
||||
//
|
||||
// So the next target is the upload request itself, in both cases.
|
||||
|
||||
if !removed_dependent_upload {
|
||||
if store.remove_send_queue_request(&self.room_id, &handles.upload_file_txn).await? {
|
||||
// The upload existed as a request: either it was pending (something else was
|
||||
// being sent), or it was actively being sent.
|
||||
trace!("could remove file upload request, removing 1 dependent request");
|
||||
|
||||
// 1. Try to abort sending using the being_sent info, in case it was active.
|
||||
if let Some(info) = guard.being_sent.as_ref() {
|
||||
if info.transaction_id == handles.upload_file_txn {
|
||||
// SAFETY: we knew it was Some(), two lines above.
|
||||
let info = guard.being_sent.take().unwrap();
|
||||
if info.cancel_upload() {
|
||||
trace!("aborted ongoing file upload");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Remove the dependent request.
|
||||
if !store
|
||||
.remove_dependent_queued_request(&self.room_id, &event_as_dependent)
|
||||
.await?
|
||||
{
|
||||
warn!("unable to find the dependent media event upload request");
|
||||
}
|
||||
} else {
|
||||
// The upload was not in the send queue, so it's completed.
|
||||
//
|
||||
// It means the event sending is either still queued as a dependent request, or
|
||||
// it's graduated into a request.
|
||||
if !removed_dependent_event
|
||||
&& !store
|
||||
.remove_dependent_queued_request(&self.room_id, &event_as_dependent)
|
||||
.await?
|
||||
{
|
||||
// The media event has been promoted into a request, or the promoted request
|
||||
// has been sent already: we couldn't abort, let the caller decide what to do.
|
||||
debug!("uploads already happened => deferring to aborting an event sending");
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, all the requests and dependent requests have been cleaned up.
|
||||
// Perform the final step: empty the cache from the local items.
|
||||
{
|
||||
let event_cache = client.event_cache_store().lock().await?;
|
||||
event_cache
|
||||
.remove_media_content_for_uri(&make_local_uri(&handles.upload_file_txn))
|
||||
.await?;
|
||||
if let Some(txn) = &handles.upload_thumbnail_txn {
|
||||
event_cache.remove_media_content_for_uri(&make_local_uri(txn)).await?;
|
||||
}
|
||||
}
|
||||
|
||||
debug!("successfully aborted!");
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -703,7 +703,7 @@ impl SlidingSync {
|
||||
|| !self.inner.lists.read().await.is_empty()
|
||||
}
|
||||
|
||||
#[instrument(skip_all, fields(pos))]
|
||||
#[instrument(skip_all, fields(pos, conn_id = self.inner.id))]
|
||||
async fn sync_once(&self) -> Result<UpdateSummary> {
|
||||
let (request, request_config, position_guard) =
|
||||
self.generate_sync_request(&mut LazyTransactionId::new()).await?;
|
||||
|
||||
@@ -14,6 +14,9 @@ use url::Url;
|
||||
|
||||
pub mod events;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod mocks;
|
||||
|
||||
use crate::{
|
||||
config::RequestConfig,
|
||||
matrix_auth::{MatrixSession, MatrixSessionTokens},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,9 +21,12 @@ use std::sync::{Arc, RwLock};
|
||||
use futures_core::Stream;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use futures_util::StreamExt;
|
||||
#[cfg(feature = "markdown")]
|
||||
use ruma::events::room::message::FormattedBody;
|
||||
use ruma::{
|
||||
events::{AnyMessageLikeEventContent, AnyStateEventContent},
|
||||
serde::Raw,
|
||||
RoomAliasId,
|
||||
};
|
||||
use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
@@ -190,8 +193,61 @@ impl IntoRawStateEventContent for &Box<RawJsonValue> {
|
||||
}
|
||||
}
|
||||
|
||||
const INVALID_ROOM_ALIAS_NAME_CHARS: &str = "#,:{}\\";
|
||||
|
||||
/// Verifies the passed `String` matches the expected room alias format:
|
||||
///
|
||||
/// This means it's lowercase, with no whitespace chars, has a single leading
|
||||
/// `#` char and a single `:` separator between the local and domain parts, and
|
||||
/// the local part only contains characters that can't be percent encoded.
|
||||
pub fn is_room_alias_format_valid(alias: String) -> bool {
|
||||
let alias_parts: Vec<&str> = alias.split(':').collect();
|
||||
if alias_parts.len() != 2 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let local_part = alias_parts[0];
|
||||
let has_valid_format = local_part.chars().skip(1).all(|c| {
|
||||
c.is_ascii()
|
||||
&& !c.is_whitespace()
|
||||
&& !c.is_control()
|
||||
&& !INVALID_ROOM_ALIAS_NAME_CHARS.contains(c)
|
||||
});
|
||||
|
||||
let is_lowercase = alias.to_lowercase() == alias;
|
||||
|
||||
// Checks both the local part and the domain part
|
||||
has_valid_format && is_lowercase && RoomAliasId::parse(alias).is_ok()
|
||||
}
|
||||
|
||||
/// Given a pair of optional `body` and `formatted_body` parameters,
|
||||
/// returns a formatted body.
|
||||
///
|
||||
/// Return the formatted body if available, or interpret the `body` parameter as
|
||||
/// markdown, if provided.
|
||||
#[cfg(feature = "markdown")]
|
||||
pub fn formatted_body_from(
|
||||
body: Option<&str>,
|
||||
formatted_body: Option<FormattedBody>,
|
||||
) -> Option<FormattedBody> {
|
||||
if formatted_body.is_some() {
|
||||
formatted_body
|
||||
} else {
|
||||
body.and_then(FormattedBody::markdown)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#[cfg(feature = "markdown")]
|
||||
use assert_matches2::{assert_let, assert_matches};
|
||||
#[cfg(feature = "markdown")]
|
||||
use ruma::events::room::message::FormattedBody;
|
||||
|
||||
#[cfg(feature = "markdown")]
|
||||
use crate::utils::formatted_body_from;
|
||||
use crate::utils::is_room_alias_format_valid;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_channel_observable_get_set() {
|
||||
@@ -202,4 +258,100 @@ mod test {
|
||||
assert_eq!(observable.set(10), 1);
|
||||
assert_eq!(observable.get(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_room_alias_format_valid_when_it_has_no_leading_hash_char_is_not_valid() {
|
||||
assert!(!is_room_alias_format_valid("alias:domain.org".to_owned()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_room_alias_format_valid_when_it_has_several_colon_chars_is_not_valid() {
|
||||
assert!(!is_room_alias_format_valid("#alias:something:domain.org".to_owned()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_room_alias_format_valid_when_it_has_no_colon_chars_is_not_valid() {
|
||||
assert!(!is_room_alias_format_valid("#alias.domain.org".to_owned()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_room_alias_format_valid_when_server_part_is_not_valid() {
|
||||
assert!(!is_room_alias_format_valid("#alias:".to_owned()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_room_alias_format_valid_when_name_part_has_whitespace_is_not_valid() {
|
||||
assert!(!is_room_alias_format_valid("#alias with whitespace:domain.org".to_owned()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_room_alias_format_valid_when_name_part_has_control_char_is_not_valid() {
|
||||
assert!(!is_room_alias_format_valid("#alias\u{0009}:domain.org".to_owned()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_room_alias_format_valid_when_name_part_has_invalid_char_is_not_valid() {
|
||||
assert!(!is_room_alias_format_valid("#a#lias,{t\\est}:domain.org".to_owned()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_room_alias_format_valid_when_name_part_is_not_lowercase_is_not_valid() {
|
||||
assert!(!is_room_alias_format_valid("#Alias:domain.org".to_owned()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_room_alias_format_valid_when_server_part_is_not_lowercase_is_not_valid() {
|
||||
assert!(!is_room_alias_format_valid("#alias:Domain.org".to_owned()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_room_alias_format_valid_when_has_valid_format() {
|
||||
assert!(is_room_alias_format_valid("#alias.test:domain.org".to_owned()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "markdown")]
|
||||
fn test_formatted_body_from_nothing_returns_none() {
|
||||
assert_matches!(formatted_body_from(None, None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "markdown")]
|
||||
fn test_formatted_body_from_only_formatted_body_returns_the_formatted_body() {
|
||||
let formatted_body = FormattedBody::html(r"<h1>Hello!</h1>");
|
||||
|
||||
assert_let!(
|
||||
Some(result_formatted_body) = formatted_body_from(None, Some(formatted_body.clone()))
|
||||
);
|
||||
|
||||
assert_eq!(formatted_body.body, result_formatted_body.body);
|
||||
assert_eq!(result_formatted_body.format, result_formatted_body.format);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "markdown")]
|
||||
fn test_formatted_body_from_markdown_body_returns_a_processed_formatted_body() {
|
||||
let markdown_body = Some(r"# Parsed");
|
||||
|
||||
assert_let!(Some(result_formatted_body) = formatted_body_from(markdown_body, None));
|
||||
|
||||
let expected_formatted_body = FormattedBody::html("<h1>Parsed</h1>\n".to_owned());
|
||||
assert_eq!(expected_formatted_body.body, result_formatted_body.body);
|
||||
assert_eq!(expected_formatted_body.format, result_formatted_body.format);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "markdown")]
|
||||
fn test_formatted_body_from_body_and_formatted_body_returns_the_formatted_body() {
|
||||
let markdown_body = Some(r"# Markdown");
|
||||
let formatted_body = FormattedBody::html(r"<h1>HTML</h1>");
|
||||
|
||||
assert_let!(
|
||||
Some(result_formatted_body) =
|
||||
formatted_body_from(markdown_body, Some(formatted_body.clone()))
|
||||
);
|
||||
|
||||
assert_eq!(formatted_body.body, result_formatted_body.body);
|
||||
assert_eq!(formatted_body.format, result_formatted_body.format);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#![allow(dead_code)] // temporary
|
||||
|
||||
use ruma::events::{MessageLikeEventType, StateEventType, TimelineEventType};
|
||||
use serde::Deserialize;
|
||||
|
||||
|
||||
@@ -124,7 +124,6 @@ pub(super) enum ReadEventRequest {
|
||||
event_type: StateEventType,
|
||||
state_key: StateKeySelector,
|
||||
},
|
||||
#[allow(dead_code)]
|
||||
ReadMessageLikeEvent {
|
||||
#[serde(rename = "type")]
|
||||
event_type: MessageLikeEventType,
|
||||
|
||||
@@ -44,7 +44,7 @@ pub(crate) enum IncomingMessage {
|
||||
/// The `MatrixDriver` notified the `WidgetMachine` of a new matrix event.
|
||||
///
|
||||
/// This means that the machine previously subscribed to some events
|
||||
/// (`Action::Subscribe` request).
|
||||
/// ([`crate::widget::Action::Subscribe`] request).
|
||||
MatrixEventReceived(Raw<AnyTimelineEvent>),
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
|
||||
//! No I/O logic of the [`WidgetDriver`].
|
||||
|
||||
#![warn(unreachable_pub)]
|
||||
|
||||
use std::{fmt, iter, time::Duration};
|
||||
|
||||
use driver_req::UpdateDelayedEventRequest;
|
||||
@@ -54,7 +52,6 @@ use super::{
|
||||
filter::{MatrixEventContent, MatrixEventFilterInput},
|
||||
Capabilities, StateKeySelector,
|
||||
};
|
||||
use crate::widget::EventFilter;
|
||||
|
||||
mod driver_req;
|
||||
mod from_widget;
|
||||
@@ -71,8 +68,11 @@ pub(crate) use self::{
|
||||
incoming::{IncomingMessage, MatrixDriverResponse},
|
||||
};
|
||||
|
||||
/// Action (a command) that client (driver) must perform.
|
||||
#[derive(Clone, Debug)]
|
||||
/// A command to perform in reaction to an [`IncomingMessage`].
|
||||
///
|
||||
/// There are also initial actions that may be performed at the creation of a
|
||||
/// [`WidgetMachine`].
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Action {
|
||||
/// Send a raw message to the widget.
|
||||
SendToWidget(String),
|
||||
@@ -94,23 +94,33 @@ pub(crate) enum Action {
|
||||
|
||||
/// Subscribe to the events in the *current* room, i.e. a room which this
|
||||
/// widget is instantiated with. The client is aware of the room.
|
||||
#[allow(dead_code)]
|
||||
Subscribe,
|
||||
|
||||
/// Unsuscribe from the events in the *current* room. Symmetrical to
|
||||
/// `Subscribe`.
|
||||
#[allow(dead_code)]
|
||||
Unsubscribe,
|
||||
}
|
||||
|
||||
/// No I/O state machine.
|
||||
///
|
||||
/// Handles interactions with the widget as well as the `MatrixDriver`.
|
||||
/// Handles interactions with the widget as well as the
|
||||
/// [`crate::widget::MatrixDriver`].
|
||||
pub(crate) struct WidgetMachine {
|
||||
/// Unique identifier for the widget.
|
||||
///
|
||||
/// Allows distinguishing different widgets.
|
||||
widget_id: String,
|
||||
|
||||
/// The room to which this widget machine is attached.
|
||||
room_id: OwnedRoomId,
|
||||
|
||||
/// Outstanding requests sent to the widget (mapped by uuid).
|
||||
pending_to_widget_requests: PendingRequests<ToWidgetRequestMeta>,
|
||||
|
||||
/// Outstanding requests sent to the matrix driver (mapped by uuid).
|
||||
pending_matrix_driver_requests: PendingRequests<MatrixDriverRequestMeta>,
|
||||
|
||||
/// Current negotiation state for capabilities.
|
||||
capabilities: CapabilitiesState,
|
||||
}
|
||||
|
||||
@@ -122,12 +132,9 @@ impl WidgetMachine {
|
||||
widget_id: String,
|
||||
room_id: OwnedRoomId,
|
||||
init_on_content_load: bool,
|
||||
limits: Option<RequestLimits>,
|
||||
) -> (Self, Vec<Action>) {
|
||||
let limits = limits.unwrap_or_else(|| RequestLimits {
|
||||
max_pending_requests: 15,
|
||||
response_timeout: Duration::from_secs(10),
|
||||
});
|
||||
let limits =
|
||||
RequestLimits { max_pending_requests: 15, response_timeout: Duration::from_secs(10) };
|
||||
|
||||
let mut machine = Self {
|
||||
widget_id,
|
||||
@@ -137,8 +144,10 @@ impl WidgetMachine {
|
||||
capabilities: CapabilitiesState::Unset,
|
||||
};
|
||||
|
||||
let actions = (!init_on_content_load).then(|| machine.negotiate_capabilities());
|
||||
(machine, actions.unwrap_or_default())
|
||||
let initial_actions =
|
||||
if init_on_content_load { Vec::new() } else { machine.negotiate_capabilities() };
|
||||
|
||||
(machine, initial_actions)
|
||||
}
|
||||
|
||||
/// Main entry point to drive the state machine.
|
||||
@@ -149,9 +158,11 @@ impl WidgetMachine {
|
||||
|
||||
match event {
|
||||
IncomingMessage::WidgetMessage(raw) => self.process_widget_message(&raw),
|
||||
|
||||
IncomingMessage::MatrixDriverResponse { request_id, response } => {
|
||||
self.process_matrix_driver_response(request_id, response)
|
||||
}
|
||||
|
||||
IncomingMessage::MatrixEventReceived(event) => {
|
||||
let CapabilitiesState::Negotiated(capabilities) = &self.capabilities else {
|
||||
error!("Received matrix event before capabilities negotiation");
|
||||
@@ -172,10 +183,8 @@ impl WidgetMachine {
|
||||
fn process_widget_message(&mut self, raw: &str) -> Vec<Action> {
|
||||
let message = match serde_json::from_str::<IncomingWidgetMessage>(raw) {
|
||||
Ok(msg) => msg,
|
||||
Err(e) => {
|
||||
// TODO: There is a special error handling required for the invalid
|
||||
// messages. Refer to the `widget-api-poc` for implementation notes.
|
||||
error!("Failed to parse incoming message: {e}");
|
||||
Err(error) => {
|
||||
error!("couldn't deserialize incoming widget message: {error}");
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
@@ -203,21 +212,22 @@ impl WidgetMachine {
|
||||
) -> Vec<Action> {
|
||||
let request = match raw_request.deserialize() {
|
||||
Ok(r) => r,
|
||||
Err(e) => return vec![self.send_from_widget_error_response(raw_request, e)],
|
||||
Err(e) => return vec![Self::send_from_widget_error_response(raw_request, e)],
|
||||
};
|
||||
|
||||
match request {
|
||||
FromWidgetRequest::SupportedApiVersions {} => {
|
||||
let response = SupportedApiVersionsResponse::new();
|
||||
vec![self.send_from_widget_response(raw_request, response)]
|
||||
vec![Self::send_from_widget_response(raw_request, response)]
|
||||
}
|
||||
|
||||
FromWidgetRequest::ContentLoaded {} => {
|
||||
let response = vec![self.send_from_widget_response(raw_request, JsonObject::new())];
|
||||
self.capabilities
|
||||
.is_unset()
|
||||
.then(|| [&response, self.negotiate_capabilities().as_slice()].concat())
|
||||
.unwrap_or(response)
|
||||
let mut response =
|
||||
vec![Self::send_from_widget_response(raw_request, JsonObject::new())];
|
||||
if self.capabilities.is_unset() {
|
||||
response.append(&mut self.negotiate_capabilities());
|
||||
}
|
||||
response
|
||||
}
|
||||
|
||||
FromWidgetRequest::ReadEvent(req) => self
|
||||
@@ -245,17 +255,20 @@ impl WidgetMachine {
|
||||
action.map(|a| vec![a]).unwrap_or_default()
|
||||
});
|
||||
|
||||
let response = self.send_from_widget_response(raw_request, OpenIdResponse::Pending);
|
||||
let response =
|
||||
Self::send_from_widget_response(raw_request, OpenIdResponse::Pending);
|
||||
iter::once(response).chain(request_action).collect()
|
||||
}
|
||||
|
||||
FromWidgetRequest::DelayedEventUpdate(req) => {
|
||||
let CapabilitiesState::Negotiated(capabilities) = &self.capabilities else {
|
||||
let text =
|
||||
"Received send update delayed event request before capabilities were negotiated";
|
||||
return vec![self.send_from_widget_error_response(raw_request, text)];
|
||||
return vec![Self::send_from_widget_error_response(raw_request, text)];
|
||||
};
|
||||
|
||||
if !capabilities.update_delayed_event {
|
||||
return vec![self.send_from_widget_error_response(
|
||||
return vec![Self::send_from_widget_error_response(
|
||||
raw_request,
|
||||
format!(
|
||||
"Not allowed: missing the {} capability.",
|
||||
@@ -263,19 +276,22 @@ impl WidgetMachine {
|
||||
),
|
||||
)];
|
||||
}
|
||||
|
||||
let (request, request_action) =
|
||||
self.send_matrix_driver_request(UpdateDelayedEventRequest {
|
||||
action: req.action,
|
||||
delay_id: req.delay_id,
|
||||
});
|
||||
request.then(|res, machine| {
|
||||
vec![machine.send_from_widget_result_response(
|
||||
|
||||
request.then(|res, _machine| {
|
||||
vec![Self::send_from_widget_result_response(
|
||||
raw_request,
|
||||
// This is mapped to another type because the update_delay_event::Response
|
||||
// does not impl Serialize
|
||||
res.map(Into::<UpdateDelayedEventResponse>::into),
|
||||
)]
|
||||
});
|
||||
|
||||
request_action.map(|a| vec![a]).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
@@ -288,20 +304,25 @@ impl WidgetMachine {
|
||||
) -> Option<Action> {
|
||||
let CapabilitiesState::Negotiated(capabilities) = &self.capabilities else {
|
||||
let text = "Received read event request before capabilities were negotiated";
|
||||
return Some(self.send_from_widget_error_response(raw_request, text));
|
||||
return Some(Self::send_from_widget_error_response(raw_request, text));
|
||||
};
|
||||
|
||||
match request {
|
||||
ReadEventRequest::ReadMessageLikeEvent { event_type, limit } => {
|
||||
let filter_fn = |f: &EventFilter| f.matches_message_like_event_type(&event_type);
|
||||
if !capabilities.read.iter().any(filter_fn) {
|
||||
return Some(self.send_from_widget_error_response(raw_request, "Not allowed"));
|
||||
if !capabilities.read.iter().any(|f| f.matches_message_like_event_type(&event_type))
|
||||
{
|
||||
return Some(Self::send_from_widget_error_response(
|
||||
raw_request,
|
||||
"Not allowed to read message like event",
|
||||
));
|
||||
}
|
||||
|
||||
const DEFAULT_EVENT_LIMIT: u32 = 50;
|
||||
let limit = limit.unwrap_or(DEFAULT_EVENT_LIMIT);
|
||||
let request = ReadMessageLikeEventRequest { event_type, limit };
|
||||
|
||||
let (request, action) = self.send_matrix_driver_request(request);
|
||||
|
||||
request.then(|result, machine| {
|
||||
let response = result.and_then(|mut events| {
|
||||
let CapabilitiesState::Negotiated(capabilities) = &machine.capabilities
|
||||
@@ -313,10 +334,13 @@ impl WidgetMachine {
|
||||
events.retain(|e| capabilities.raw_event_matches_read_filter(e));
|
||||
Ok(ReadEventResponse { events })
|
||||
});
|
||||
vec![machine.send_from_widget_result_response(raw_request, response)]
|
||||
|
||||
vec![Self::send_from_widget_result_response(raw_request, response)]
|
||||
});
|
||||
|
||||
action
|
||||
}
|
||||
|
||||
ReadEventRequest::ReadStateEvent { event_type, state_key } => {
|
||||
let allowed = match &state_key {
|
||||
StateKeySelector::Any => capabilities
|
||||
@@ -339,13 +363,16 @@ impl WidgetMachine {
|
||||
if allowed {
|
||||
let request = ReadStateEventRequest { event_type, state_key };
|
||||
let (request, action) = self.send_matrix_driver_request(request);
|
||||
request.then(|result, machine| {
|
||||
request.then(|result, _machine| {
|
||||
let response = result.map(|events| ReadEventResponse { events });
|
||||
vec![machine.send_from_widget_result_response(raw_request, response)]
|
||||
vec![Self::send_from_widget_result_response(raw_request, response)]
|
||||
});
|
||||
action
|
||||
} else {
|
||||
Some(self.send_from_widget_error_response(raw_request, "Not allowed"))
|
||||
Some(Self::send_from_widget_error_response(
|
||||
raw_request,
|
||||
"Not allowed to read state event",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -371,8 +398,9 @@ impl WidgetMachine {
|
||||
Default::default()
|
||||
}),
|
||||
};
|
||||
|
||||
if !capabilities.send_delayed_event && request.delay.is_some() {
|
||||
return Some(self.send_from_widget_error_response(
|
||||
return Some(Self::send_from_widget_error_response(
|
||||
raw_request,
|
||||
format!(
|
||||
"Not allowed: missing the {} capability.",
|
||||
@@ -380,17 +408,23 @@ impl WidgetMachine {
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
if !capabilities.send.iter().any(|filter| filter.matches(&filter_in)) {
|
||||
return Some(self.send_from_widget_error_response(raw_request, "Not allowed"));
|
||||
return Some(Self::send_from_widget_error_response(
|
||||
raw_request,
|
||||
"Not allowed to send event",
|
||||
));
|
||||
}
|
||||
|
||||
let (request, action) = self.send_matrix_driver_request(request);
|
||||
|
||||
request.then(|mut result, machine| {
|
||||
if let Ok(r) = result.as_mut() {
|
||||
r.set_room_id(machine.room_id.clone());
|
||||
}
|
||||
vec![machine.send_from_widget_result_response(raw_request, result)]
|
||||
vec![Self::send_from_widget_result_response(raw_request, result)]
|
||||
});
|
||||
|
||||
action
|
||||
}
|
||||
|
||||
@@ -445,38 +479,39 @@ impl WidgetMachine {
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip_all, fields(request_id))]
|
||||
#[instrument(skip_all)]
|
||||
fn send_from_widget_response(
|
||||
&self,
|
||||
raw_request: Raw<FromWidgetRequest>,
|
||||
response_data: impl Serialize,
|
||||
) -> Action {
|
||||
let mut object = raw_request
|
||||
.deserialize_as::<IndexMap<String, Box<RawJsonValue>>>()
|
||||
.expect("Failed to converted FromWidgetRequest to object representation");
|
||||
let response_data = serde_json::value::to_raw_value(&response_data)
|
||||
.expect("Failed to serialize response data");
|
||||
object.insert("response".to_owned(), response_data);
|
||||
let serialized = serde_json::to_string(&object).expect("Failed to serialize response");
|
||||
let f = || {
|
||||
let mut object = raw_request.deserialize_as::<IndexMap<String, Box<RawJsonValue>>>()?;
|
||||
let response_data = serde_json::value::to_raw_value(&response_data)?;
|
||||
object.insert("response".to_owned(), response_data);
|
||||
serde_json::to_string(&object)
|
||||
};
|
||||
|
||||
// SAFETY: we expect the raw request to be a valid JSON map, to which we add a
|
||||
// new field.
|
||||
let serialized = f().expect("error when attaching response to incoming request");
|
||||
|
||||
Action::SendToWidget(serialized)
|
||||
}
|
||||
|
||||
fn send_from_widget_error_response(
|
||||
&self,
|
||||
raw_request: Raw<FromWidgetRequest>,
|
||||
error: impl fmt::Display,
|
||||
) -> Action {
|
||||
self.send_from_widget_response(raw_request, FromWidgetErrorResponse::new(error))
|
||||
Self::send_from_widget_response(raw_request, FromWidgetErrorResponse::new(error))
|
||||
}
|
||||
|
||||
fn send_from_widget_result_response(
|
||||
&self,
|
||||
raw_request: Raw<FromWidgetRequest>,
|
||||
result: Result<impl Serialize, impl fmt::Display>,
|
||||
) -> Action {
|
||||
match result {
|
||||
Ok(res) => self.send_from_widget_response(raw_request, res),
|
||||
Err(msg) => self.send_from_widget_error_response(raw_request, msg),
|
||||
Ok(res) => Self::send_from_widget_response(raw_request, res),
|
||||
Err(msg) => Self::send_from_widget_error_response(raw_request, msg),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,7 +522,7 @@ impl WidgetMachine {
|
||||
) -> (ToWidgetRequestHandle<'_, T::ResponseData>, Option<Action>) {
|
||||
#[derive(Serialize)]
|
||||
#[serde(tag = "api", rename = "toWidget", rename_all = "camelCase")]
|
||||
struct ToWidgetRequestSerHelper<'a, T> {
|
||||
struct ToWidgetRequestSerdeHelper<'a, T> {
|
||||
widget_id: &'a str,
|
||||
request_id: Uuid,
|
||||
action: &'static str,
|
||||
@@ -495,7 +530,7 @@ impl WidgetMachine {
|
||||
}
|
||||
|
||||
let request_id = Uuid::new_v4();
|
||||
let full_request = ToWidgetRequestSerHelper {
|
||||
let full_request = ToWidgetRequestSerdeHelper {
|
||||
widget_id: &self.widget_id,
|
||||
request_id,
|
||||
action: T::ACTION,
|
||||
@@ -553,7 +588,7 @@ impl WidgetMachine {
|
||||
let update = NotifyCapabilitiesChanged { approved, requested };
|
||||
let (_request, action) = machine.send_to_widget_request(update);
|
||||
|
||||
(subscribe_required).then(|| Action::Subscribe).into_iter().chain(action).collect()
|
||||
subscribe_required.then(|| Action::Subscribe).into_iter().chain(action).collect()
|
||||
});
|
||||
|
||||
action.map(|a| vec![a]).unwrap_or_default()
|
||||
@@ -590,9 +625,13 @@ impl MatrixDriverRequestMeta {
|
||||
}
|
||||
}
|
||||
|
||||
/// Current negotiation state for capabilities.
|
||||
enum CapabilitiesState {
|
||||
/// Capabilities have never been defined.
|
||||
Unset,
|
||||
/// We're currently negotiating capabilities.
|
||||
Negotiating,
|
||||
/// The capabilities have already been negotiated.
|
||||
Negotiated(Capabilities),
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user