Compare commits
316 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 | |||
| 237419c740 | |||
| 1658397139 | |||
| bab6761388 | |||
| 5193c2033f | |||
| 0f9bc20bb8 | |||
| d54f2a8b04 | |||
| 00c4071fe1 | |||
| 65287178d1 | |||
| 90b8015d71 | |||
| f256fe4b24 | |||
| 8d07f36247 | |||
| 77ee02f529 | |||
| 566a13b16e | |||
| 4bbe620d0f | |||
| 9178e4ce33 | |||
| c04a73c28d | |||
| 13244d808b | |||
| e9d5aa1221 | |||
| 57ad256fe1 | |||
| c196a9754b | |||
| a8992f37d7 | |||
| 9483703e35 | |||
| 0942dab2fd | |||
| 94bd421a8d | |||
| 7b3eb0b6f1 | |||
| 8b85ff2434 | |||
| 94c507dd38 | |||
| 37304c8cdc | |||
| 16a86587ea | |||
| e24d9b3ce3 | |||
| 9f11bced10 | |||
| e5d4ea5964 | |||
| fbc914f586 | |||
| bb2d19a1d8 | |||
| 933033cc25 | |||
| b233aa64d2 | |||
| ace96e372f | |||
| 8865e2ff74 | |||
| 2fa54e5cfa | |||
| 04275d7c27 | |||
| 90d6a37b31 | |||
| 590c2dd9fd | |||
| 478dc0ea90 | |||
| 7a422fe126 | |||
| a739ddfc84 | |||
| 1f2e8c5007 | |||
| c2a921cb58 | |||
| 06e6cba156 | |||
| 6828f93720 | |||
| 7f7b996d24 | |||
| ee252437d1 | |||
| 494532d579 | |||
| 4002136cfb | |||
| 5717eb1722 | |||
| c08194aa44 | |||
| 5d141fce13 | |||
| 70bcddfba5 | |||
| 3c48459768 | |||
| 5107f5f23a | |||
| d4b9145bc2 | |||
| 49f7fe90a9 | |||
| 75683d268f | |||
| 71abbeb1f1 | |||
| fe79826c7a | |||
| 7d64ea1bbc | |||
| 5158b39277 | |||
| be88e0ad69 | |||
| 50473ba1a8 | |||
| 5d828d234e | |||
| 9c858c1208 | |||
| 58d46f015b | |||
| 4cbd18cb37 | |||
| 888f992df0 | |||
| ee80291c41 | |||
| de3a667eb9 | |||
| ce9dc73376 | |||
| 03535832ec | |||
| c143f981bd | |||
| f4a18989fb | |||
| 6752cf73df | |||
| e87bed8ef4 | |||
| 2f19e2b762 | |||
| b66024c386 | |||
| c48bb13159 | |||
| 5f0ba1e7df | |||
| 91fa1669be | |||
| a1a4ce0a95 | |||
| 131921c045 | |||
| b62661bc70 | |||
| e0be1e8e32 | |||
| c23c3b9558 | |||
| ca3d5693b4 | |||
| 135c448f2d | |||
| 01cbce907c | |||
| df4a5c36fc | |||
| 8492968792 | |||
| d31f5b2a72 | |||
| 8469cb1146 | |||
| bb8b0cf6b9 | |||
| 35dabf7346 | |||
| f771eec3c5 | |||
| 6455585f1e | |||
| 3a34b03726 | |||
| 8cf0716db2 | |||
| 660a305cfa | |||
| b2af1eeb20 | |||
| 77b3aa8124 | |||
| cf7cb5c350 | |||
| ac7bc6461f | |||
| 7c39fd6ae5 | |||
| 40f4fc138b | |||
| d3d7c03892 | |||
| 3558886b98 | |||
| 47361b93e9 | |||
| f5cdbd8e41 | |||
| f8c23d8aa0 | |||
| 1a3c5045dd | |||
| ca1d829788 | |||
| 31e9600078 | |||
| 3291a426d8 | |||
| 1368a8534c | |||
| 7cfcc8ecc1 | |||
| c4f9c20115 | |||
| 0c81206653 | |||
| a61bc3cbbd | |||
| 74de617d76 | |||
| 3f5d54c494 | |||
| f2f99fb207 | |||
| 6196ebaba6 | |||
| 65bb373379 | |||
| e62c47132e | |||
| 996b391506 | |||
| 74722f48aa | |||
| e3180cdbc5 | |||
| 9c03c5dd7e | |||
| b46ebbf34e | |||
| d3bfdb9563 | |||
| 3887c10444 | |||
| b69575d5ff | |||
| 543152d914 | |||
| c8b38257f1 | |||
| 2df359d316 | |||
| 951a4354c6 | |||
| befcd069c3 | |||
| 0c26988cf5 | |||
| a7f69973c2 | |||
| 1750bf597f | |||
| ad677cb6f2 | |||
| 350a26cee9 | |||
| 08152bd9fc | |||
| 89183a3d4b | |||
| 65ed4f3f22 | |||
| 41d392f899 | |||
| 1ce5160846 | |||
| dc4c6b4d73 | |||
| 3b33f3779f | |||
| 7089ff51c4 | |||
| d8de12561b | |||
| 56edc9d00f | |||
| 2ea114d988 | |||
| c7708d6154 | |||
| bdfe64179b | |||
| 59c47fb22d | |||
| 821fa8fa99 | |||
| a5f1769e28 | |||
| 59fce90943 | |||
| 81bebcf692 | |||
| c4dd2d192e | |||
| a901506a53 | |||
| 87f89ec561 | |||
| 2820f5f3b4 | |||
| 427c59e266 | |||
| 4a7f924161 | |||
| 2829b07305 | |||
| 4f49b23751 | |||
| 962a78ab13 | |||
| 664f6d5f5a | |||
| 30f3a3c2e4 | |||
| 8df5d655c0 | |||
| 1552426961 | |||
| 8b7494d17b | |||
| 77e5281781 | |||
| efc2e2c4c8 | |||
| 79798a9de9 | |||
| 3d0423447c | |||
| 9999d3ba96 | |||
| ee4ef2eb53 | |||
| 0b57ef4bf6 | |||
| 6dd2e3becf | |||
| 1018d71bb7 | |||
| 87472e7679 | |||
| cdbfae2aee | |||
| 2eca7271ea | |||
| 92a02a51c4 | |||
| 019d198af8 | |||
| e6db85b7d4 | |||
| a4bda1ac66 | |||
| e46e63771b | |||
| 41a2ad09cf | |||
| 32919405d6 | |||
| b002a8da52 | |||
| 85682ac37f | |||
| 22c765b9ab | |||
| 3ad8f1d607 | |||
| a4782939b3 | |||
| 72dc307400 | |||
| 248cf55272 | |||
| 1260e740ba | |||
| 9a4a67d488 | |||
| ab0871f299 | |||
| 86d9fe59d2 | |||
| 1945b508c3 | |||
| 4c7461357c | |||
| ca7f2ad3d0 | |||
| 711f4cb868 | |||
| cb51a3155a | |||
| 81119a66d8 | |||
| 9c6413551c | |||
| 42f0d83b53 | |||
| d9167f208a | |||
| 7f0a3f0e47 | |||
| 8fe61e1fb3 | |||
| 07cfe3da94 | |||
| 4d472f6aed | |||
| ce231e6c2b | |||
| b36a9ad781 | |||
| 95ae5d1938 | |||
| 9d976d0bcf | |||
| 17370a5702 | |||
| b793acd2b1 | |||
| 752706c51d | |||
| 736aa0351c | |||
| 4bcb9b7d9f | |||
| 867d9c71fd | |||
| 2dcf06fad2 | |||
| 1fc3450eac |
@@ -1,13 +1,3 @@
|
||||
# Pass the rustflags specified to host dependencies (build scripts, proc-macros)
|
||||
# when a `--target` is passed to Cargo. Historically this was not the case, and
|
||||
# because of that, cross-compilation would not set the rustflags configured
|
||||
# below in `target.'cfg(...)'` for them, resulting in cache invalidation.
|
||||
#
|
||||
# Since this is an unstable feature (enabled at the bottom of the file), this
|
||||
# setting is unfortunately ignored on stable toolchains, but it's still better
|
||||
# to have it apply on nightly than using the old behavior for all toolchains.
|
||||
target-applies-to-host = false
|
||||
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
uniffi-bindgen = "run --package uniffi-bindgen --"
|
||||
@@ -15,43 +5,5 @@ uniffi-bindgen = "run --package uniffi-bindgen --"
|
||||
[doc.extern-map.registries]
|
||||
crates-io = "https://docs.rs/"
|
||||
|
||||
# Exclude tarpaulin, android and ios from extra lints since on stable, without
|
||||
# the nightly-only target-applies-to-host setting at the top, cross compilation
|
||||
# and otherwise changing cfg's can be very bad for caching. These should never
|
||||
# be the default either and don't have much target-specific code that would
|
||||
# benefit from the extra lints.
|
||||
[target.'cfg(not(any(tarpaulin, target_os = "android", target_os = "ios")))']
|
||||
rustflags = [
|
||||
"-Wrust_2018_idioms",
|
||||
"-Wsemicolon_in_expressions_from_macros",
|
||||
"-Wunused_extern_crates",
|
||||
"-Wunused_import_braces",
|
||||
"-Wunused_qualifications",
|
||||
"-Wtrivial_casts",
|
||||
"-Wtrivial_numeric_casts",
|
||||
"-Wclippy::cloned_instead_of_copied",
|
||||
"-Wclippy::dbg_macro",
|
||||
"-Wclippy::inefficient_to_string",
|
||||
"-Wclippy::macro_use_imports",
|
||||
"-Wclippy::mut_mut",
|
||||
"-Wclippy::needless_borrow",
|
||||
"-Wclippy::nonstandard_macro_braces",
|
||||
"-Wclippy::str_to_string",
|
||||
"-Wclippy::todo",
|
||||
"-Wclippy::unused_async",
|
||||
"-Wclippy::redundant_clone",
|
||||
]
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")']
|
||||
rustflags = [
|
||||
# We have some types that are !Send and/or !Sync only on wasm, it would be
|
||||
# slightly more efficient, but also pretty annoying, to wrap them in Rc
|
||||
# where we would use Arc on other platforms.
|
||||
"-Aclippy::arc_with_non_send_sync",
|
||||
]
|
||||
|
||||
# activate the target-applies-to-host feature.
|
||||
# Required for `target-applies-to-host` at the top to take effect.
|
||||
[unstable]
|
||||
rustdoc-map = true
|
||||
target-applies-to-host = true
|
||||
|
||||
+1
-3
@@ -10,7 +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-0370", reason = "Waiting for a Aquamarine release" },
|
||||
{ id = "RUSTSEC-2024-0384", reason = "Unmaintained backoff crate, not critical. We'll migrate soon." },
|
||||
]
|
||||
|
||||
[licenses]
|
||||
@@ -54,8 +54,6 @@ allow-git = [
|
||||
"https://github.com/element-hq/tracing.git",
|
||||
# Sam as for the tracing dependency.
|
||||
"https://github.com/element-hq/paranoid-android.git",
|
||||
# Well, it's Ruma.
|
||||
"https://github.com/ruma/ruma",
|
||||
# A patch override for the bindings: https://github.com/rodrimati1992/const_panic/pull/10
|
||||
"https://github.com/jplatte/const_panic",
|
||||
# A patch override for the bindings: https://github.com/smol-rs/async-compat/pull/22
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
@@ -69,10 +69,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Rust SDK
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout Kotlin Rust Components project
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: matrix-org/matrix-rust-components-kotlin
|
||||
path: rust-components-kotlin
|
||||
@@ -136,7 +136,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# install protoc in case we end up rebuilding opentelemetry-proto
|
||||
- name: Install protoc
|
||||
@@ -191,7 +191,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# install protoc in case we end up rebuilding opentelemetry-proto
|
||||
- name: Install protoc
|
||||
|
||||
+32
-12
@@ -40,15 +40,19 @@ jobs:
|
||||
- markdown
|
||||
- socks
|
||||
- sso-login
|
||||
- image-proc
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
@@ -81,7 +85,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -112,7 +116,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -158,13 +167,19 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install libsqlite
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
@@ -220,7 +235,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -268,7 +283,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
@@ -286,10 +301,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Actions Repository
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check the spelling of the files in our repo
|
||||
uses: crate-ci/typos@v1.25.0
|
||||
uses: crate-ci/typos@v1.27.3
|
||||
|
||||
clippy:
|
||||
name: Run clippy
|
||||
@@ -298,7 +313,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
@@ -356,7 +371,7 @@ jobs:
|
||||
# tests need a synapse: this is a service and not michaelkaye/setup-matrix-synapse@main as the
|
||||
# latter does not provide networking for services to communicate with it.
|
||||
synapse:
|
||||
image: ghcr.io/matrix-org/synapse-service:v1.115.0 # keep in sync with ./coverage.yml
|
||||
image: ghcr.io/matrix-org/synapse-service:v1.117.0 # keep in sync with ./coverage.yml
|
||||
env:
|
||||
SYNAPSE_COMPLEMENT_DATABASE: sqlite
|
||||
SERVER_NAME: synapse
|
||||
@@ -365,7 +380,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
# tests need a synapse: this is a service and not michaelkaye/setup-matrix-synapse@main as the
|
||||
# latter does not provide networking for services to communicate with it.
|
||||
synapse:
|
||||
image: ghcr.io/matrix-org/synapse-service:v1.115.0 # keep in sync with ./ci.yml
|
||||
image: ghcr.io/matrix-org/synapse-service:v1.117.0 # keep in sync with ./ci.yml
|
||||
env:
|
||||
SYNAPSE_COMPLEMENT_DATABASE: sqlite
|
||||
SERVER_NAME: synapse
|
||||
@@ -58,10 +58,15 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
@@ -89,7 +94,7 @@ jobs:
|
||||
run: |
|
||||
rustup run stable cargo tarpaulin \
|
||||
--skip-clean --profile cov --out xml \
|
||||
--features experimental-widgets,testing,image-proc
|
||||
--features experimental-widgets,testing
|
||||
env:
|
||||
CARGO_PROFILE_COV_INHERITS: 'dev'
|
||||
CARGO_PROFILE_COV_DEBUG: 1
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
name: Detects unused dependencies
|
||||
on:
|
||||
pull_request: { branches: "*" }
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Machete
|
||||
uses: bnjbvr/cargo-machete@main
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
@@ -51,8 +51,6 @@ jobs:
|
||||
# Keep in sync with xtask docs
|
||||
- name: Build documentation
|
||||
env:
|
||||
# Work around https://github.com/rust-lang/cargo/issues/10744
|
||||
CARGO_TARGET_APPLIES_TO_HOST: "true"
|
||||
RUSTDOCFLAGS: "--enable-index-page -Zunstable-options --cfg docsrs -Dwarnings"
|
||||
run:
|
||||
cargo doc --no-deps --workspace --features docsrs
|
||||
|
||||
@@ -7,6 +7,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.0
|
||||
- uses: actions/checkout@v4
|
||||
- name: Block Fixup Commit Merge
|
||||
uses: 13rac1/block-fixup-merge-action@v2.0.0
|
||||
|
||||
@@ -16,6 +16,6 @@ jobs:
|
||||
msrv:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.0
|
||||
- uses: actions/checkout@v4
|
||||
- uses: taiki-e/install-action@cargo-hack
|
||||
- run: cargo hack check --rust-version --workspace --all-targets --ignore-private
|
||||
|
||||
@@ -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
|
||||
@@ -58,13 +58,13 @@ jobs:
|
||||
echo "override_commit=$(<commit_sha.txt)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
|
||||
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
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Calculate cache key
|
||||
id: cachekey
|
||||
|
||||
+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).
|
||||
+96
-27
@@ -29,43 +29,112 @@ integration tests that need a running synapse instance. These tests reside in
|
||||
[README](./testing/matrix-sdk-integration-testing/README.md) to easily set up a
|
||||
synapse for testing purposes.
|
||||
|
||||
## Commit messages and PR title guidelines
|
||||
|
||||
Ideally, a PR should have a *proper title*, with *atomic logical commits*, and each commit
|
||||
should have a *good commit message*.
|
||||
## Pull requests
|
||||
|
||||
An *atomic logical commit* is one that is ideally small, can be compiled in isolation, and passes
|
||||
tests. This is useful to make the review process easier (help your reviewer), but also when running
|
||||
bisections, helping identifying which commit introduced a regression.
|
||||
Ideally, a PR should have a *proper title*, with *atomic logical commits*, and
|
||||
each commit should have a *good commit message*.
|
||||
|
||||
A *good commit message* should be composed of:
|
||||
A *proper PR title* would be a one-liner summary of the changes in the PR,
|
||||
following the same guidelines of a good commit message, including the
|
||||
area/feature prefix. Something like `FFI: Allow logs files to be pruned.` would
|
||||
be a good PR title.
|
||||
|
||||
- a prefix to indicate which area/feature is related by the commit
|
||||
- a short description that would give sufficient context for a reviewer to guess what the commit is
|
||||
about.
|
||||
(An additional bad example of a bad PR title would be `mynickname/branch name`,
|
||||
that is, just the branch name.)
|
||||
|
||||
Examples of commit messages that aren't so useful:
|
||||
# Writing changelog entries
|
||||
|
||||
- “add new method“
|
||||
- “enhance performance“
|
||||
- “fix receipts“
|
||||
We aim to maintain clear and informative changelogs that accurately reflect the
|
||||
changes in our project. This guide will help you write useful changelog entries
|
||||
using git-cliff, which fetches changelog entries from commit messages.
|
||||
|
||||
Examples of good commit messages:
|
||||
## Commit message format
|
||||
|
||||
- “ffi: Add new method `frobnicate_the_foos`”
|
||||
- “indexeddb: Break up the request inside `get_inbound_group_sessions`”
|
||||
- “read_receipts: Store receipts locally, fixing #12345”
|
||||
Commit messages should be formatted as Conventional Commits. In addition, some
|
||||
git trailers are supported and have special meaning (see below).
|
||||
|
||||
A *proper PR title* would be a one-liner summary of the changes in the PR, following the
|
||||
same guidelines of a good commit message, including the area/feature prefix. Something like
|
||||
`FFI: Allow logs files to be pruned.` would be a good PR title.
|
||||
### Conventional commits
|
||||
|
||||
(An additional bad example of a bad PR title would be `mynickname/branch name`, that is, just the
|
||||
branch name.)
|
||||
Conventional Commits are structured as follows:
|
||||
|
||||
Having good commit messages and PR titles also helps with reviews, scanning the `git log` of
|
||||
the project, and writing the [*This week in
|
||||
Matrix*](https://matrix.org/category/this-week-in-matrix/) updates for the SDK.
|
||||
```
|
||||
<type>(<scope>): <short summary>
|
||||
```
|
||||
|
||||
The type of changes which will be included in changelogs is one of the following:
|
||||
|
||||
* `feat`: A new feature
|
||||
* `fix`: A bug fix
|
||||
* `doc`: Documentation changes
|
||||
* `refactor`: Code refactoring
|
||||
* `perf`: Performance improvements
|
||||
* `ci`: Changes to CI configuration files and scripts
|
||||
|
||||
The scope is optional and can specify the area of the codebase affected (e.g.,
|
||||
olm, cipher).
|
||||
|
||||
### Changelog trailer
|
||||
|
||||
In addition to the Conventional Commit format, you can use the `Changelog` git
|
||||
trailer to specify the changelog message explicitly. When that trailer is
|
||||
present, its value will be used as the changelog entry instead of the commit's
|
||||
leading line. The `Breaking-Change` git trailer can be used in a similar manner
|
||||
if the changelog entry should be marked as a breaking change.
|
||||
|
||||
|
||||
#### Example commit message
|
||||
|
||||
```
|
||||
feat: Add a method to encode Ed25519 public keys to Base64
|
||||
|
||||
This patch adds the `Ed25519PublicKey::to_base64()` method, which allows us to
|
||||
stringify Ed25519 and thus present them to users. It's also commonly used when
|
||||
Ed25519 keys need to be inserted into JSON.
|
||||
|
||||
Changelog: Add the `Ed25519PublicKey::to_base64()` method which can be used to
|
||||
stringify the Ed25519 public key.
|
||||
```
|
||||
|
||||
In this commit message, the content specified in the `Changelog` trailer will be
|
||||
used for the changelog entry.
|
||||
|
||||
Be careful to add at least one whitespace after new lines to create a paragraph.
|
||||
|
||||
### Security fixes
|
||||
|
||||
Commits addressing security vulnerabilities must include specific trailers for
|
||||
vulnerability metadata. These commits are required to include at least the
|
||||
`Security-Impact` trailer to indicate that the commit is a security fix.
|
||||
|
||||
Security issues have some additional git-trailers:
|
||||
|
||||
* `Security-Impact`: The magnitude of harm that can be expected, i.e. low/moderate/high/critical.
|
||||
* `CVE`: The CVE that was assigned to this issue.
|
||||
* `GitHub-Advisory`: The GitHub advisory identifier.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
fix(crypto): Use a constant-time Base64 encoder for secret key material
|
||||
|
||||
This patch fixes a security issue around a side-channel vulnerability[1]
|
||||
when decoding secret key material using Base64.
|
||||
|
||||
In some circumstances an attacker can obtain information about secret
|
||||
secret key material via a controlled-channel and side-channel attack.
|
||||
|
||||
This patch avoids the side-channel by switching to the base64ct crate
|
||||
for the encoding, and more importantly, the decoding of secret key
|
||||
material.
|
||||
|
||||
Security-Impact: Low
|
||||
CVE: CVE-2024-40640
|
||||
GitHub-Advisory: GHSA-j8cm-g7r6-hfpq
|
||||
|
||||
Changelog: Use a constant-time Base64 encoder for secret key material
|
||||
to mitigate side-channel attacks leaking secret key material.
|
||||
```
|
||||
|
||||
## Review process
|
||||
|
||||
@@ -126,7 +195,7 @@ requested.
|
||||
commits, the [autosquash] option can help with this.
|
||||
|
||||
```bash
|
||||
git rebase main --autosquash
|
||||
git rebase main --interactive --autosquash
|
||||
```
|
||||
|
||||
[fixup]: https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---fixupamendrewordltcommitgt
|
||||
|
||||
Generated
+158
-627
File diff suppressed because it is too large
Load Diff
+33
-16
@@ -32,13 +32,11 @@ 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 = { version = "0.3.26", default-features = false, features = [
|
||||
"alloc",
|
||||
] }
|
||||
futures-util = "0.3.26"
|
||||
growable-bloom-filter = "2.1.0"
|
||||
http = "1.1.0"
|
||||
imbl = "3.0.0"
|
||||
@@ -47,7 +45,7 @@ once_cell = "1.16.0"
|
||||
pin-project-lite = "0.2.9"
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.12.4", default-features = false }
|
||||
ruma = { git = "https://github.com/ruma/ruma", rev = "26165b23fc2ae9928c5497a21db3d31f4b44cc2a", features = [
|
||||
ruma = { version = "0.11.1", features = [
|
||||
"client-api-c",
|
||||
"compat-upload-signatures",
|
||||
"compat-user-id",
|
||||
@@ -61,7 +59,7 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "26165b23fc2ae9928c5497a21d
|
||||
"unstable-msc4075",
|
||||
"unstable-msc4140",
|
||||
] }
|
||||
ruma-common = { git = "https://github.com/ruma/ruma", rev = "26165b23fc2ae9928c5497a21db3d31f4b44cc2a" }
|
||||
ruma-common = "0.14.1"
|
||||
serde = "1.0.151"
|
||||
serde_html_form = "0.2.0"
|
||||
serde_json = "1.0.91"
|
||||
@@ -81,16 +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-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 = { 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.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]
|
||||
@@ -130,8 +129,26 @@ tracing-appender = { git = "https://github.com/element-hq/tracing.git", rev = "c
|
||||
paranoid-android = { git = "https://github.com/element-hq/paranoid-android.git", rev = "69388ac5b4afeed7be4401c70ce17f6d9a2cf19b" }
|
||||
|
||||
[workspace.lints.rust]
|
||||
rust_2018_idioms = "warn"
|
||||
semicolon_in_expressions_from_macros = "warn"
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }
|
||||
unused_extern_crates = "warn"
|
||||
unused_import_braces = "warn"
|
||||
unused_qualifications = "warn"
|
||||
trivial_casts = "warn"
|
||||
trivial_numeric_casts = "warn"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
assigning_clones = "allow"
|
||||
box_default = "allow"
|
||||
cloned_instead_of_copied = "warn"
|
||||
dbg_macro = "warn"
|
||||
inefficient_to_string = "warn"
|
||||
macro_use_imports = "warn"
|
||||
mut_mut = "warn"
|
||||
needless_borrow = "warn"
|
||||
nonstandard_macro_braces = "warn"
|
||||
str_to_string = "warn"
|
||||
todo = "warn"
|
||||
unused_async = "warn"
|
||||
redundant_clone = "warn"
|
||||
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
# Releasing `matrix-rust-sdk`
|
||||
|
||||
- Make sure to bump all the crates to *the same version number*, and commit that (along with the
|
||||
changes to the `Cargo.lock` file).
|
||||
- Create a `git tag` for the current version, following the format `major.minor.patch`, e.g. `0.7.0`.
|
||||
- Push the tag: `git push origin 0.7.0`
|
||||
- Publish all the crates, in topological order of the dependency tree:
|
||||
|
||||
```
|
||||
cargo publish -p matrix-sdk-test-macros
|
||||
cargo publish -p matrix-sdk-test
|
||||
cargo publish -p matrix-sdk-common
|
||||
cargo publish -p matrix-sdk-qrcode
|
||||
cargo publish -p matrix-sdk-store-encryption
|
||||
cargo publish -p matrix-sdk-crypto
|
||||
cargo publish -p matrix-sdk-base
|
||||
cargo publish -p matrix-sdk-sqlite
|
||||
cargo publish -p matrix-sdk-indexeddb
|
||||
cargo publish -p matrix-sdk
|
||||
cargo publish -p matrix-sdk-ui
|
||||
```
|
||||
@@ -0,0 +1,47 @@
|
||||
# Releasing and publishing the SDK
|
||||
|
||||
While the release process can be handled manually, `cargo-release` has been
|
||||
configured to make it more convenient.
|
||||
|
||||
By default, [`cargo-release`](https://github.com/crate-ci/cargo-release) assumes
|
||||
that no pull request is required to cut a release. However, since the SDK
|
||||
repo is set up so that each push requires a pull request, we need to slightly
|
||||
deviate from the default workflow. A `cargo-xtask` has been created to make the
|
||||
process as smooth as possible.
|
||||
|
||||
The procedure is as follows:
|
||||
|
||||
1. Switch to a release branch:
|
||||
|
||||
```bash
|
||||
git switch -c release-x.y.z
|
||||
```
|
||||
|
||||
2. Prepare the release. This will update the `README.md`, prepend the `CHANGELOG.md`
|
||||
file using `git cliff`, and bump the version in the `Cargo.toml` file.
|
||||
|
||||
```bash
|
||||
cargo xtask release prepare --execute minor|patch|rc
|
||||
```
|
||||
|
||||
3. Double-check and edit the `CHANGELOG.md` and `README.md` if necessary. Once you are
|
||||
satisfied, push the branch and open a PR.
|
||||
|
||||
```bash
|
||||
git push --set-upstream origin/release-x.y.z
|
||||
```
|
||||
|
||||
4. Pass the review and merge the branch as you would with any other branch.
|
||||
|
||||
5. Create tags for your new release, publish the release on crates.io and push
|
||||
the tags:
|
||||
|
||||
```bash
|
||||
# Switch to main first.
|
||||
git switch main
|
||||
# Pull in the now-merged release commit(s).
|
||||
git pull
|
||||
# Create tags, publish the release on crates.io, and push the tags.
|
||||
cargo xtask release publish --execute
|
||||
```
|
||||
For more information on cargo-release: https://github.com/crate-ci/cargo-release
|
||||
@@ -36,3 +36,6 @@ harness = false
|
||||
[[bench]]
|
||||
name = "room_bench"
|
||||
harness = false
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
@@ -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(
|
||||
@@ -171,8 +174,9 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
);
|
||||
|
||||
let room = client.get_room(&room_id).expect("Room not found");
|
||||
assert!(!room.pinned_event_ids().is_empty());
|
||||
assert_eq!(room.pinned_event_ids().len(), PINNED_EVENTS_COUNT);
|
||||
let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
|
||||
assert!(!pinned_event_ids.is_empty());
|
||||
assert_eq!(pinned_event_ids.len(), PINNED_EVENTS_COUNT);
|
||||
|
||||
let count = PINNED_EVENTS_COUNT;
|
||||
let name = format!("{count} pinned events");
|
||||
@@ -191,8 +195,9 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
|
||||
group.bench_function(BenchmarkId::new("load_pinned_events", name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
assert!(!room.pinned_event_ids().is_empty());
|
||||
assert_eq!(room.pinned_event_ids().len(), PINNED_EVENTS_COUNT);
|
||||
let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
|
||||
assert!(!pinned_event_ids.is_empty());
|
||||
assert_eq!(pinned_event_ids.len(), PINNED_EVENTS_COUNT);
|
||||
|
||||
// Reset cache so it always loads the events from the mocked endpoint
|
||||
client.event_cache().empty_immutable_cache().await;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -26,6 +26,7 @@ futures-util = { workspace = true }
|
||||
hmac = "0.12.1"
|
||||
http = { workspace = true }
|
||||
matrix-sdk-common = { workspace = true, features = ["uniffi"] }
|
||||
matrix-sdk-ffi-macros = { workspace = true }
|
||||
pbkdf2 = "0.12.2"
|
||||
rand = { workspace = true }
|
||||
ruma = { workspace = true }
|
||||
@@ -66,3 +67,6 @@ assert_matches2 = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
@@ -69,7 +69,7 @@ impl BackupRecoveryKey {
|
||||
const PBKDF_ROUNDS: i32 = 500_000;
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl BackupRecoveryKey {
|
||||
/// Create a new random [`BackupRecoveryKey`].
|
||||
#[allow(clippy::new_without_default)]
|
||||
|
||||
@@ -53,7 +53,7 @@ impl Drop for DehydratedDevices {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl DehydratedDevices {
|
||||
pub fn create(&self) -> Result<Arc<DehydratedDevice>, DehydrationError> {
|
||||
let inner = self.runtime.block_on(self.inner.create())?;
|
||||
@@ -107,7 +107,7 @@ impl Drop for RehydratedDevice {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RehydratedDevice {
|
||||
pub fn receive_events(&self, events: String) -> Result<(), crate::CryptoStoreError> {
|
||||
let events: Vec<Raw<AnyToDeviceEvent>> = serde_json::from_str(&events)?;
|
||||
@@ -133,7 +133,7 @@ impl Drop for DehydratedDevice {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl DehydratedDevice {
|
||||
pub fn keys_for_upload(
|
||||
&self,
|
||||
|
||||
@@ -196,7 +196,7 @@ impl From<anyhow::Error> for MigrationError {
|
||||
///
|
||||
/// * `progress_listener` - A callback that can be used to introspect the
|
||||
/// progress of the migration.
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn migrate(
|
||||
data: MigrationData,
|
||||
path: String,
|
||||
@@ -359,7 +359,7 @@ async fn save_changes(
|
||||
///
|
||||
/// * `progress_listener` - A callback that can be used to introspect the
|
||||
/// progress of the migration.
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn migrate_sessions(
|
||||
data: SessionMigrationData,
|
||||
path: String,
|
||||
@@ -532,7 +532,7 @@ fn collect_sessions(
|
||||
/// * `passphrase` - The passphrase that should be used to encrypt the data at
|
||||
/// rest in the Sqlite store. **Warning**, if no passphrase is given, the
|
||||
/// store and all its data will remain unencrypted.
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn migrate_room_settings(
|
||||
room_settings: HashMap<String, RoomSettings>,
|
||||
path: String,
|
||||
@@ -558,7 +558,7 @@ pub fn migrate_room_settings(
|
||||
}
|
||||
|
||||
/// Callback that will be passed over the FFI to report progress
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait ProgressListener {
|
||||
/// The callback that should be called on the Rust side
|
||||
///
|
||||
@@ -794,7 +794,7 @@ pub struct BackupKeys {
|
||||
backup_version: String,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl BackupKeys {
|
||||
/// Get the recovery key that we're holding on to.
|
||||
pub fn recovery_key(&self) -> Arc<BackupRecoveryKey> {
|
||||
@@ -891,7 +891,7 @@ fn parse_user_id(user_id: &str) -> Result<OwnedUserId, CryptoStoreError> {
|
||||
ruma::UserId::parse(user_id).map_err(|e| CryptoStoreError::InvalidUserId(user_id.to_owned(), e))
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
fn version_info() -> VersionInfo {
|
||||
VersionInfo {
|
||||
version: matrix_sdk_crypto::VERSION.to_owned(),
|
||||
@@ -915,12 +915,12 @@ pub struct VersionInfo {
|
||||
pub git_description: String,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
fn version() -> String {
|
||||
matrix_sdk_crypto::VERSION.to_owned()
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
fn vodozemac_version() -> String {
|
||||
vodozemac::VERSION.to_owned()
|
||||
}
|
||||
@@ -935,7 +935,7 @@ pub struct PkEncryption {
|
||||
inner: matrix_sdk_crypto::vodozemac::pk_encryption::PkEncryption,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl PkEncryption {
|
||||
/// Create a new [`PkEncryption`] object from a `Curve25519PublicKey`
|
||||
/// encoded as Base64.
|
||||
|
||||
@@ -7,7 +7,7 @@ use tracing_subscriber::{fmt::MakeWriter, EnvFilter};
|
||||
|
||||
/// Trait that can be used to forward Rust logs over FFI to a language specific
|
||||
/// logger.
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait Logger: Send {
|
||||
/// Called every time the Rust side wants to post a log line.
|
||||
fn log(&self, log_line: String);
|
||||
@@ -42,7 +42,7 @@ pub struct LoggerWrapper {
|
||||
}
|
||||
|
||||
/// Set the logger that should be used to forward Rust logs over FFI.
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn set_logger(logger: Box<dyn Logger>) {
|
||||
let logger = LoggerWrapper { inner: Arc::new(Mutex::new(logger)) };
|
||||
|
||||
|
||||
@@ -42,7 +42,8 @@ use ruma::{
|
||||
},
|
||||
serde::Raw,
|
||||
to_device::DeviceIdOrAllDevices,
|
||||
DeviceKeyAlgorithm, EventId, OwnedTransactionId, OwnedUserId, RoomId, UserId,
|
||||
DeviceKeyAlgorithm, EventId, OneTimeKeyAlgorithm, OwnedTransactionId, OwnedUserId, RoomId,
|
||||
UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{value::RawValue, Value};
|
||||
@@ -178,7 +179,7 @@ impl From<RustSignatureCheckResult> for SignatureVerification {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl OlmMachine {
|
||||
/// Create a new `OlmMachine`
|
||||
///
|
||||
@@ -528,11 +529,11 @@ impl OlmMachine {
|
||||
) -> Result<SyncChangesResult, CryptoStoreError> {
|
||||
let to_device: ToDevice = serde_json::from_str(&events)?;
|
||||
let device_changes: RumaDeviceLists = device_changes.into();
|
||||
let key_counts: BTreeMap<DeviceKeyAlgorithm, UInt> = key_counts
|
||||
let key_counts: BTreeMap<OneTimeKeyAlgorithm, UInt> = key_counts
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
DeviceKeyAlgorithm::from(k),
|
||||
OneTimeKeyAlgorithm::from(k),
|
||||
v.clamp(0, i32::MAX)
|
||||
.try_into()
|
||||
.expect("Couldn't convert key counts into an UInt"),
|
||||
@@ -540,8 +541,8 @@ impl OlmMachine {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let unused_fallback_keys: Option<Vec<DeviceKeyAlgorithm>> =
|
||||
unused_fallback_keys.map(|u| u.into_iter().map(DeviceKeyAlgorithm::from).collect());
|
||||
let unused_fallback_keys: Option<Vec<OneTimeKeyAlgorithm>> =
|
||||
unused_fallback_keys.map(|u| u.into_iter().map(OneTimeKeyAlgorithm::from).collect());
|
||||
|
||||
let (to_device_events, room_key_infos) = self.runtime.block_on(
|
||||
self.inner.receive_sync_changes(matrix_sdk_crypto::EncryptionSyncChanges {
|
||||
|
||||
@@ -18,6 +18,8 @@ pub enum UserIdentity {
|
||||
user_signing_key: String,
|
||||
/// The public self-signing key of our identity.
|
||||
self_signing_key: String,
|
||||
/// True if this identity was verified at some point but is not anymore.
|
||||
has_verification_violation: bool,
|
||||
},
|
||||
/// The user identity of other users.
|
||||
Other {
|
||||
@@ -27,6 +29,8 @@ pub enum UserIdentity {
|
||||
master_key: String,
|
||||
/// The public self-signing key of our identity.
|
||||
self_signing_key: String,
|
||||
/// True if this identity was verified at some point but is not anymore.
|
||||
has_verification_violation: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -44,6 +48,7 @@ impl UserIdentity {
|
||||
master_key: serde_json::to_string(&master)?,
|
||||
user_signing_key: serde_json::to_string(&user_signing)?,
|
||||
self_signing_key: serde_json::to_string(&self_signing)?,
|
||||
has_verification_violation: i.has_verification_violation(),
|
||||
}
|
||||
}
|
||||
SdkUserIdentity::Other(i) => {
|
||||
@@ -54,6 +59,7 @@ impl UserIdentity {
|
||||
user_id: i.user_id().to_string(),
|
||||
master_key: serde_json::to_string(&master)?,
|
||||
self_signing_key: serde_json::to_string(&self_signing)?,
|
||||
has_verification_violation: i.has_verification_violation(),
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::{CryptoStoreError, OutgoingVerificationRequest, SignatureUploadReques
|
||||
|
||||
/// Listener that will be passed over the FFI to report changes to a SAS
|
||||
/// verification.
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SasListener: Send {
|
||||
/// The callback that should be called on the Rust side
|
||||
///
|
||||
@@ -82,7 +82,7 @@ pub struct Verification {
|
||||
pub(crate) runtime: Handle,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Verification {
|
||||
/// Try to represent the `Verification` as an `Sas` verification object,
|
||||
/// returns `None` if the verification is not a `Sas` verification.
|
||||
@@ -112,7 +112,7 @@ pub struct Sas {
|
||||
pub(crate) runtime: Handle,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Sas {
|
||||
/// Get the user id of the other side.
|
||||
pub fn other_user_id(&self) -> String {
|
||||
@@ -276,7 +276,7 @@ impl Sas {
|
||||
|
||||
/// Listener that will be passed over the FFI to report changes to a QrCode
|
||||
/// verification.
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait QrCodeListener: Send {
|
||||
/// The callback that should be called on the Rust side
|
||||
///
|
||||
@@ -328,7 +328,7 @@ pub struct QrCode {
|
||||
pub(crate) runtime: Handle,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl QrCode {
|
||||
/// Get the user id of the other side.
|
||||
pub fn other_user_id(&self) -> String {
|
||||
@@ -522,7 +522,7 @@ pub struct ConfirmVerificationResult {
|
||||
|
||||
/// Listener that will be passed over the FFI to report changes to a
|
||||
/// verification request.
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait VerificationRequestListener: Send {
|
||||
/// The callback that should be called on the Rust side
|
||||
///
|
||||
@@ -562,7 +562,7 @@ pub struct VerificationRequest {
|
||||
pub(crate) runtime: Handle,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl VerificationRequest {
|
||||
/// The id of the other user that is participating in this verification
|
||||
/// request.
|
||||
@@ -752,7 +752,7 @@ impl VerificationRequest {
|
||||
RustVerificationRequestState::Ready {
|
||||
their_methods,
|
||||
our_methods,
|
||||
other_device_id: _,
|
||||
other_device_data: _,
|
||||
} => VerificationRequestState::Ready {
|
||||
their_methods: their_methods.iter().map(|m| m.to_string()).collect(),
|
||||
our_methods: our_methods.iter().map(|m| m.to_string()).collect(),
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
description = "Helper macros to write FFI bindings"
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ruma"]
|
||||
license = "Apache-2.0"
|
||||
name = "matrix-sdk-ffi-macros"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = { workspace = true }
|
||||
version = "0.7.0"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
test = false
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0.86"
|
||||
quote = "1.0.18"
|
||||
syn = { version = "2.0.43", features = ["full", "extra-traits"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,14 @@
|
||||
[](https://travis-ci.org/matrix-org/matrix-rust-sdk)
|
||||
[](https://codecov.io/gh/matrix-org/matrix-rust-sdk)
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://matrix.to/#/#matrix-rust-sdk:matrix.org)
|
||||
|
||||
# matrix-sdk-ffi-macros
|
||||
|
||||
Internal macros used for the FFI layer (bindings) of the Rust Matrix SDK.
|
||||
|
||||
**NOTE:** These are just macros that help build the matrix-rust-sdk bindings, you're probably
|
||||
interested in the main [rust-sdk](https://github.com/matrix-org/matrix-rust-sdk/) crate.
|
||||
|
||||
[Matrix]: https://matrix.org/
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{ImplItem, Item, TraitItem};
|
||||
|
||||
/// Attribute to specify the async runtime parameter for the `uniffi`
|
||||
/// export macros if there any `async fn`s in the input.
|
||||
#[proc_macro_attribute]
|
||||
pub fn export(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let has_async_fn = |item| {
|
||||
if let Item::Fn(fun) = &item {
|
||||
if fun.sig.asyncness.is_some() {
|
||||
return true;
|
||||
}
|
||||
} else if let Item::Impl(blk) = &item {
|
||||
for item in &blk.items {
|
||||
if let ImplItem::Fn(fun) = item {
|
||||
if fun.sig.asyncness.is_some() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Item::Trait(blk) = &item {
|
||||
for item in &blk.items {
|
||||
if let TraitItem::Fn(fun) = item {
|
||||
if fun.sig.asyncness.is_some() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
};
|
||||
|
||||
let attr2 = proc_macro2::TokenStream::from(attr);
|
||||
let item2 = proc_macro2::TokenStream::from(item.clone());
|
||||
|
||||
let res = match syn::parse(item) {
|
||||
Ok(item) => match has_async_fn(item) {
|
||||
true => quote! { #[uniffi::export(async_runtime = "tokio", #attr2)] },
|
||||
false => quote! { #[uniffi::export(#attr2)] },
|
||||
},
|
||||
Err(e) => e.into_compile_error(),
|
||||
};
|
||||
|
||||
quote! {
|
||||
#res
|
||||
#item2
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@@ -28,11 +28,11 @@ eyeball-im = { workspace = true }
|
||||
extension-trait = "1.0.1"
|
||||
futures-util = { workspace = true }
|
||||
log-panics = { version = "2", features = ["with-backtrace"] }
|
||||
matrix-sdk-ffi-macros = { workspace = true }
|
||||
matrix-sdk-ui = { workspace = true, features = ["uniffi"] }
|
||||
mime = "0.3.16"
|
||||
once_cell = { workspace = true }
|
||||
ruma = { workspace = true, features = ["html", "unstable-unspecified", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat"] }
|
||||
sanitize-filename-reader-friendly = "2.2.1"
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
@@ -82,3 +82,6 @@ features = [
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
@@ -19,17 +19,18 @@ use matrix_sdk::{
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use crate::client::{Client, SlidingSyncVersion};
|
||||
use crate::client::{Client, OidcPrompt, SlidingSyncVersion};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct HomeserverLoginDetails {
|
||||
pub(crate) url: String,
|
||||
pub(crate) sliding_sync_version: SlidingSyncVersion,
|
||||
pub(crate) supports_oidc_login: bool,
|
||||
pub(crate) supported_oidc_prompts: Vec<OidcPrompt>,
|
||||
pub(crate) supports_password_login: bool,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl HomeserverLoginDetails {
|
||||
/// The URL of the currently configured homeserver.
|
||||
pub fn url(&self) -> String {
|
||||
@@ -46,6 +47,12 @@ impl HomeserverLoginDetails {
|
||||
self.supports_oidc_login
|
||||
}
|
||||
|
||||
/// The prompts advertised by the authentication issuer for use in the login
|
||||
/// URL.
|
||||
pub fn supported_oidc_prompts(&self) -> Vec<OidcPrompt> {
|
||||
self.supported_oidc_prompts.clone()
|
||||
}
|
||||
|
||||
/// Whether the current homeserver supports the password login flow.
|
||||
pub fn supports_password_login(&self) -> bool {
|
||||
self.supports_password_login
|
||||
@@ -62,7 +69,7 @@ pub struct SsoHandler {
|
||||
pub(crate) url: String,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SsoHandler {
|
||||
/// Returns the URL for starting SSO authentication. The URL should be
|
||||
/// opened in a web view. Once the web view succeeds, call `finish` with
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::Debug,
|
||||
mem::ManuallyDrop,
|
||||
path::Path,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
@@ -9,7 +8,8 @@ use std::{
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use matrix_sdk::{
|
||||
media::{
|
||||
MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequest, MediaThumbnailSettings,
|
||||
MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequestParameters,
|
||||
MediaThumbnailSettings,
|
||||
},
|
||||
oidc::{
|
||||
registrations::{ClientId, OidcRegistrations},
|
||||
@@ -19,12 +19,13 @@ use matrix_sdk::{
|
||||
registration::{
|
||||
ClientMetadata, ClientMetadataVerificationError, VerifiedClientMetadata,
|
||||
},
|
||||
requests::Prompt as SdkOidcPrompt,
|
||||
},
|
||||
OidcAuthorizationData, OidcSession,
|
||||
},
|
||||
reqwest::StatusCode,
|
||||
ruma::{
|
||||
api::client::{
|
||||
media::get_content_thumbnail::v3::Method,
|
||||
push::{EmailPusherData, PusherIds, PusherInit, PusherKind as RumaPusherKind},
|
||||
room::{create_room, Visibility},
|
||||
session::get_login_types,
|
||||
@@ -40,7 +41,7 @@ use matrix_sdk::{
|
||||
EventEncryptionAlgorithm, RoomId, TransactionId, UInt, UserId,
|
||||
},
|
||||
sliding_sync::Version as SdkSlidingSyncVersion,
|
||||
AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens,
|
||||
AuthApi, AuthSession, Client as MatrixClient, HttpError, SessionChange, SessionTokens,
|
||||
};
|
||||
use matrix_sdk_ui::notification_client::{
|
||||
NotificationClient as MatrixNotificationClient,
|
||||
@@ -54,7 +55,8 @@ use ruma::{
|
||||
},
|
||||
events::{
|
||||
ignored_user_list::IgnoredUserListEventContent,
|
||||
room::power_levels::RoomPowerLevelsEventContent, GlobalAccountDataEventType,
|
||||
room::{join_rules::RoomJoinRulesEventContent, power_levels::RoomPowerLevelsEventContent},
|
||||
GlobalAccountDataEventType,
|
||||
},
|
||||
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
|
||||
OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
|
||||
@@ -77,6 +79,7 @@ use crate::{
|
||||
ruma::AuthData,
|
||||
sync_service::{SyncService, SyncServiceBuilder},
|
||||
task_handle::TaskHandle,
|
||||
utils::AsyncRuntimeDropped,
|
||||
ClientError,
|
||||
};
|
||||
|
||||
@@ -140,25 +143,25 @@ impl From<PushFormat> for RumaPushFormat {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait ClientDelegate: Sync + Send {
|
||||
fn did_receive_auth_error(&self, is_soft_logout: bool);
|
||||
fn did_refresh_tokens(&self);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait ClientSessionDelegate: Sync + Send {
|
||||
fn retrieve_session_from_keychain(&self, user_id: String) -> Result<Session, ClientError>;
|
||||
fn save_session_in_keychain(&self, session: Session);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait ProgressWatcher: Send + Sync {
|
||||
fn transmission_progress(&self, progress: TransmissionProgress);
|
||||
}
|
||||
|
||||
/// A listener to the global (client-wide) error reporter of the send queue.
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SendQueueRoomErrorListener: Sync + Send {
|
||||
/// Called every time the send queue has ran into an error for a given room,
|
||||
/// which will disable the send queue for that particular room.
|
||||
@@ -182,58 +185,52 @@ impl From<matrix_sdk::TransmissionProgress> for TransmissionProgress {
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Client {
|
||||
pub(crate) inner: ManuallyDrop<MatrixClient>,
|
||||
pub(crate) inner: AsyncRuntimeDropped<MatrixClient>,
|
||||
delegate: RwLock<Option<Arc<dyn ClientDelegate>>>,
|
||||
session_verification_controller:
|
||||
Arc<tokio::sync::RwLock<Option<SessionVerificationController>>>,
|
||||
}
|
||||
|
||||
impl Drop for Client {
|
||||
fn drop(&mut self) {
|
||||
// Dropping the inner OlmMachine must happen within a tokio context
|
||||
// because deadpool drops sqlite connections in the DB pool on tokio's
|
||||
// blocking threadpool to avoid blocking async worker threads.
|
||||
let _guard = RUNTIME.enter();
|
||||
// SAFETY: self.inner is never used again, which is the only requirement
|
||||
// for ManuallyDrop::drop to be used safely.
|
||||
unsafe {
|
||||
ManuallyDrop::drop(&mut self.inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub 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<
|
||||
tokio::sync::RwLock<Option<SessionVerificationController>>,
|
||||
> = Default::default();
|
||||
let ctrl = session_verification_controller.clone();
|
||||
let controller = session_verification_controller.clone();
|
||||
|
||||
sdk_client.add_event_handler(move |ev: AnyToDeviceEvent| async move {
|
||||
if let Some(session_verification_controller) = &*ctrl.clone().read().await {
|
||||
if let Some(session_verification_controller) = &*controller.clone().read().await {
|
||||
session_verification_controller.process_to_device_message(ev).await;
|
||||
} else {
|
||||
debug!("received to-device message, but verification controller isn't ready");
|
||||
}
|
||||
});
|
||||
|
||||
let cross_process_store_locks_holder_name =
|
||||
sdk_client.cross_process_store_locks_holder_name().to_owned();
|
||||
|
||||
let client = Client {
|
||||
inner: ManuallyDrop::new(sdk_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 {
|
||||
@@ -260,11 +257,35 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Client {
|
||||
/// Information about login options for the client's homeserver.
|
||||
pub async fn homeserver_login_details(&self) -> Arc<HomeserverLoginDetails> {
|
||||
let supports_oidc_login = self.inner.oidc().fetch_authentication_issuer().await.is_ok();
|
||||
let oidc = self.inner.oidc();
|
||||
let (supports_oidc_login, supported_oidc_prompts) = match oidc
|
||||
.fetch_authentication_issuer()
|
||||
.await
|
||||
{
|
||||
Ok(issuer) => match &oidc.given_provider_metadata(&issuer).await {
|
||||
Ok(metadata) => {
|
||||
let prompts = metadata
|
||||
.prompt_values_supported
|
||||
.as_ref()
|
||||
.map_or_else(Vec::new, |prompts| prompts.iter().map(Into::into).collect());
|
||||
|
||||
(true, prompts)
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Failed to fetch OIDC provider metadata: {error}");
|
||||
(true, Default::default())
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
error!("Failed to fetch authentication issuer: {error}");
|
||||
(false, Default::default())
|
||||
}
|
||||
};
|
||||
|
||||
let supports_password_login = self.supports_password_login().await.ok().unwrap_or(false);
|
||||
let sliding_sync_version = self.sliding_sync_version();
|
||||
|
||||
@@ -272,6 +293,7 @@ impl Client {
|
||||
url: self.homeserver(),
|
||||
sliding_sync_version,
|
||||
supports_oidc_login,
|
||||
supported_oidc_prompts,
|
||||
supports_password_login,
|
||||
})
|
||||
}
|
||||
@@ -360,13 +382,14 @@ impl Client {
|
||||
Ok(Arc::new(SsoHandler { client: Arc::clone(self), url }))
|
||||
}
|
||||
|
||||
/// Requests the URL needed for login in a web view using OIDC. Once the web
|
||||
/// Requests the URL needed for opening a web view using OIDC. Once the web
|
||||
/// view has succeeded, call `login_with_oidc_callback` with the callback it
|
||||
/// returns. If a failure occurs and a callback isn't available, make sure
|
||||
/// to call `abort_oidc_login` to inform the client of this.
|
||||
pub async fn url_for_oidc_login(
|
||||
/// to call `abort_oidc_auth` to inform the client of this.
|
||||
pub async fn url_for_oidc(
|
||||
&self,
|
||||
oidc_configuration: &OidcConfiguration,
|
||||
prompt: OidcPrompt,
|
||||
) -> Result<Arc<OidcAuthorizationData>, OidcError> {
|
||||
let oidc_metadata: VerifiedClientMetadata = oidc_configuration.try_into()?;
|
||||
let registrations_file = Path::new(&oidc_configuration.dynamic_registrations_file);
|
||||
@@ -387,14 +410,15 @@ impl Client {
|
||||
static_registrations,
|
||||
)?;
|
||||
|
||||
let data = self.inner.oidc().url_for_oidc_login(oidc_metadata, registrations).await?;
|
||||
let data =
|
||||
self.inner.oidc().url_for_oidc(oidc_metadata, registrations, prompt.into()).await?;
|
||||
|
||||
Ok(Arc::new(data))
|
||||
}
|
||||
|
||||
/// Aborts an existing OIDC login operation that might have been cancelled,
|
||||
/// failed etc.
|
||||
pub async fn abort_oidc_login(&self, authorization_data: Arc<OidcAuthorizationData>) {
|
||||
pub async fn abort_oidc_auth(&self, authorization_data: Arc<OidcAuthorizationData>) {
|
||||
self.inner.oidc().abort_authorization(&authorization_data.state).await;
|
||||
}
|
||||
|
||||
@@ -414,7 +438,7 @@ impl Client {
|
||||
pub async fn get_media_file(
|
||||
&self,
|
||||
media_source: Arc<MediaSource>,
|
||||
body: Option<String>,
|
||||
filename: Option<String>,
|
||||
mime_type: String,
|
||||
use_cache: bool,
|
||||
temp_dir: Option<String>,
|
||||
@@ -426,8 +450,8 @@ impl Client {
|
||||
.inner
|
||||
.media()
|
||||
.get_media_file(
|
||||
&MediaRequest { source, format: MediaFormat::File },
|
||||
body,
|
||||
&MediaRequestParameters { source, format: MediaFormat::File },
|
||||
filename,
|
||||
&mime_type,
|
||||
use_cache,
|
||||
temp_dir,
|
||||
@@ -474,7 +498,7 @@ impl Client {
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
// Respawn tasks for rooms that had unsent events. At this point we've just
|
||||
// created the subscriber, so it'll be notified about errors.
|
||||
q.respawn_tasks_for_rooms_with_unsent_events().await;
|
||||
q.respawn_tasks_for_rooms_with_unsent_requests().await;
|
||||
|
||||
loop {
|
||||
match subscriber.recv().await {
|
||||
@@ -526,7 +550,7 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Client {
|
||||
/// The sliding sync version.
|
||||
pub fn sliding_sync_version(&self) -> SlidingSyncVersion {
|
||||
@@ -577,6 +601,10 @@ impl Client {
|
||||
&self,
|
||||
action: Option<AccountManagementAction>,
|
||||
) -> Result<Option<String>, ClientError> {
|
||||
if !matches!(self.inner.auth_api(), Some(AuthApi::Oidc(..))) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
match self.inner.oidc().account_management_url(action.map(Into::into)).await {
|
||||
Ok(url) => Ok(url.map(|u| u.to_string())),
|
||||
Err(e) => {
|
||||
@@ -642,7 +670,7 @@ impl Client {
|
||||
}
|
||||
|
||||
pub async fn create_room(&self, request: CreateRoomParameters) -> Result<String, ClientError> {
|
||||
let response = self.inner.create_room(request.into()).await?;
|
||||
let response = self.inner.create_room(request.try_into()?).await?;
|
||||
Ok(String::from(response.room_id()))
|
||||
}
|
||||
|
||||
@@ -675,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();
|
||||
@@ -697,10 +725,11 @@ impl Client {
|
||||
) -> Result<Vec<u8>, ClientError> {
|
||||
let source = (*media_source).clone();
|
||||
|
||||
debug!(?source, "requesting media file");
|
||||
Ok(self
|
||||
.inner
|
||||
.media()
|
||||
.get_media_content(&MediaRequest { source, format: MediaFormat::File }, true)
|
||||
.get_media_content(&MediaRequestParameters { source, format: MediaFormat::File }, true)
|
||||
.await?)
|
||||
}
|
||||
|
||||
@@ -712,14 +741,14 @@ impl Client {
|
||||
) -> Result<Vec<u8>, ClientError> {
|
||||
let source = (*media_source).clone();
|
||||
|
||||
debug!(source = ?media_source, width, height, "requesting media thumbnail");
|
||||
Ok(self
|
||||
.inner
|
||||
.media()
|
||||
.get_media_content(
|
||||
&MediaRequest {
|
||||
&MediaRequestParameters {
|
||||
source,
|
||||
format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(
|
||||
Method::Scale,
|
||||
UInt::new(width).unwrap(),
|
||||
UInt::new(height).unwrap(),
|
||||
)),
|
||||
@@ -972,6 +1001,20 @@ impl Client {
|
||||
Ok(Arc::new(Room::new(room)))
|
||||
}
|
||||
|
||||
/// Knock on a room to join it using its ID or alias.
|
||||
pub async fn knock(
|
||||
&self,
|
||||
room_id_or_alias: String,
|
||||
reason: Option<String>,
|
||||
server_names: Vec<String>,
|
||||
) -> Result<Arc<Room>, ClientError> {
|
||||
let room_id = RoomOrAliasId::parse(&room_id_or_alias)?;
|
||||
let server_names =
|
||||
server_names.iter().map(ServerName::parse).collect::<Result<Vec<_>, _>>()?;
|
||||
let room = self.inner.knock(room_id, reason, server_names).await?;
|
||||
Ok(Arc::new(Room::new(room)))
|
||||
}
|
||||
|
||||
pub async fn get_recently_visited_rooms(&self) -> Result<Vec<String>, ClientError> {
|
||||
Ok(self
|
||||
.inner
|
||||
@@ -994,10 +1037,21 @@ impl Client {
|
||||
pub async fn resolve_room_alias(
|
||||
&self,
|
||||
room_alias: String,
|
||||
) -> Result<ResolvedRoomAlias, ClientError> {
|
||||
) -> Result<Option<ResolvedRoomAlias>, ClientError> {
|
||||
let room_alias = RoomAliasId::parse(&room_alias)?;
|
||||
let response = self.inner.resolve_room_alias(&room_alias).await?;
|
||||
Ok(response.into())
|
||||
match self.inner.resolve_room_alias(&room_alias).await {
|
||||
Ok(response) => Ok(Some(response.into())),
|
||||
Err(HttpError::Reqwest(http_error)) => match http_error.status() {
|
||||
Some(StatusCode::NOT_FOUND) => Ok(None),
|
||||
_ => Err(http_error.into()),
|
||||
},
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if a room alias exists in the current homeserver.
|
||||
pub async fn room_alias_exists(&self, room_alias: String) -> Result<bool, ClientError> {
|
||||
self.resolve_room_alias(room_alias).await.map(|ret| ret.is_some())
|
||||
}
|
||||
|
||||
/// Given a room id, get the preview of a room, to interact with it.
|
||||
@@ -1009,7 +1063,7 @@ impl Client {
|
||||
&self,
|
||||
room_id: String,
|
||||
via_servers: Vec<String>,
|
||||
) -> Result<RoomPreview, ClientError> {
|
||||
) -> Result<Arc<RoomPreview>, ClientError> {
|
||||
let room_id = RoomId::parse(&room_id).context("room_id is not a valid room id")?;
|
||||
|
||||
let via_servers = via_servers
|
||||
@@ -1022,16 +1076,16 @@ impl Client {
|
||||
// rustc win that one fight.
|
||||
let room_id: &RoomId = &room_id;
|
||||
|
||||
let sdk_room_preview = self.inner.get_room_preview(room_id.into(), via_servers).await?;
|
||||
let room_preview = self.inner.get_room_preview(room_id.into(), via_servers).await?;
|
||||
|
||||
Ok(RoomPreview::from_sdk(sdk_room_preview))
|
||||
Ok(Arc::new(RoomPreview::new(self.inner.clone(), room_preview)))
|
||||
}
|
||||
|
||||
/// Given a room alias, get the preview of a room, to interact with it.
|
||||
pub async fn get_room_preview_from_room_alias(
|
||||
&self,
|
||||
room_alias: String,
|
||||
) -> Result<RoomPreview, ClientError> {
|
||||
) -> Result<Arc<RoomPreview>, ClientError> {
|
||||
let room_alias =
|
||||
RoomAliasId::parse(&room_alias).context("room_alias is not a valid room alias")?;
|
||||
|
||||
@@ -1039,9 +1093,9 @@ impl Client {
|
||||
// rustc win that one fight.
|
||||
let room_alias: &RoomAliasId = &room_alias;
|
||||
|
||||
let sdk_room_preview = self.inner.get_room_preview(room_alias.into(), Vec::new()).await?;
|
||||
let room_preview = self.inner.get_room_preview(room_alias.into(), Vec::new()).await?;
|
||||
|
||||
Ok(RoomPreview::from_sdk(sdk_room_preview))
|
||||
Ok(Arc::new(RoomPreview::new(self.inner.clone(), room_preview)))
|
||||
}
|
||||
|
||||
/// Waits until an at least partially synced room is received, and returns
|
||||
@@ -1083,9 +1137,32 @@ impl Client {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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)?;
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait IgnoredUsersListener: Sync + Send {
|
||||
fn call(&self, ignored_user_ids: Vec<String>);
|
||||
}
|
||||
@@ -1306,16 +1383,23 @@ pub struct CreateRoomParameters {
|
||||
pub avatar: Option<String>,
|
||||
#[uniffi(default = None)]
|
||||
pub power_level_content_override: Option<PowerLevels>,
|
||||
#[uniffi(default = None)]
|
||||
pub join_rule_override: Option<JoinRule>,
|
||||
#[uniffi(default = None)]
|
||||
pub canonical_alias: Option<String>,
|
||||
}
|
||||
|
||||
impl From<CreateRoomParameters> for create_room::v3::Request {
|
||||
fn from(value: CreateRoomParameters) -> create_room::v3::Request {
|
||||
impl TryFrom<CreateRoomParameters> for create_room::v3::Request {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: CreateRoomParameters) -> Result<create_room::v3::Request, Self::Error> {
|
||||
let mut request = create_room::v3::Request::new();
|
||||
request.name = value.name;
|
||||
request.topic = value.topic;
|
||||
request.is_direct = value.is_direct;
|
||||
request.visibility = value.visibility.into();
|
||||
request.preset = Some(value.preset.into());
|
||||
request.room_alias_name = value.canonical_alias;
|
||||
request.invite = match value.invite {
|
||||
Some(invite) => invite
|
||||
.iter()
|
||||
@@ -1343,6 +1427,12 @@ impl From<CreateRoomParameters> for create_room::v3::Request {
|
||||
content.url = Some(url.into());
|
||||
initial_state.push(InitialStateEvent::new(content).to_raw_any());
|
||||
}
|
||||
|
||||
if let Some(join_rule_override) = value.join_rule_override {
|
||||
let content = RoomJoinRulesEventContent::new(join_rule_override.try_into()?);
|
||||
initial_state.push(InitialStateEvent::new(content).to_raw_any());
|
||||
}
|
||||
|
||||
request.initial_state = initial_state;
|
||||
|
||||
if let Some(power_levels) = value.power_level_content_override {
|
||||
@@ -1351,12 +1441,14 @@ impl From<CreateRoomParameters> for create_room::v3::Request {
|
||||
request.power_level_content_override = Some(power_levels);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to serialize power levels, error: {e}");
|
||||
return Err(ClientError::Generic {
|
||||
msg: format!("Failed to serialize power levels, error: {e}"),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
request
|
||||
Ok(request)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1642,7 +1734,7 @@ impl From<AccountManagementAction> for AccountManagementActionFull {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
fn gen_transaction_id() -> String {
|
||||
TransactionId::new().to_string()
|
||||
}
|
||||
@@ -1660,7 +1752,7 @@ impl MediaFileHandle {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl MediaFileHandle {
|
||||
/// Get the media file's path.
|
||||
pub fn path(&self) -> Result<String, ClientError> {
|
||||
@@ -1724,3 +1816,156 @@ impl TryFrom<SlidingSyncVersion> for SdkSlidingSyncVersion {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum OidcPrompt {
|
||||
/// The Authorization Server must not display any authentication or consent
|
||||
/// user interface pages.
|
||||
None,
|
||||
|
||||
/// The Authorization Server should prompt the End-User for
|
||||
/// reauthentication.
|
||||
Login,
|
||||
|
||||
/// The Authorization Server should prompt the End-User for consent before
|
||||
/// returning information to the Client.
|
||||
Consent,
|
||||
|
||||
/// The Authorization Server should prompt the End-User to select a user
|
||||
/// account.
|
||||
///
|
||||
/// This enables an End-User who has multiple accounts at the Authorization
|
||||
/// Server to select amongst the multiple accounts that they might have
|
||||
/// current sessions for.
|
||||
SelectAccount,
|
||||
|
||||
/// The Authorization Server should prompt the End-User to create a user
|
||||
/// account.
|
||||
///
|
||||
/// Defined in [Initiating User Registration via OpenID Connect](https://openid.net/specs/openid-connect-prompt-create-1_0.html).
|
||||
Create,
|
||||
|
||||
/// An unknown value.
|
||||
Unknown { value: String },
|
||||
}
|
||||
|
||||
impl From<&SdkOidcPrompt> for OidcPrompt {
|
||||
fn from(value: &SdkOidcPrompt) -> Self {
|
||||
match value {
|
||||
SdkOidcPrompt::None => Self::None,
|
||||
SdkOidcPrompt::Login => Self::Login,
|
||||
SdkOidcPrompt::Consent => Self::Consent,
|
||||
SdkOidcPrompt::SelectAccount => Self::SelectAccount,
|
||||
SdkOidcPrompt::Create => Self::Create,
|
||||
SdkOidcPrompt::Unknown(value) => Self::Unknown { value: value.to_owned() },
|
||||
_ => Self::Unknown { value: value.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OidcPrompt> for SdkOidcPrompt {
|
||||
fn from(value: OidcPrompt) -> Self {
|
||||
match value {
|
||||
OidcPrompt::None => Self::None,
|
||||
OidcPrompt::Login => Self::Login,
|
||||
OidcPrompt::Consent => Self::Consent,
|
||||
OidcPrompt::SelectAccount => Self::SelectAccount,
|
||||
OidcPrompt::Create => Self::Create,
|
||||
OidcPrompt::Unknown { value } => Self::Unknown(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The rule used for users wishing to join this room.
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum JoinRule {
|
||||
/// Anyone can join the room without any prior action.
|
||||
Public,
|
||||
|
||||
/// A user who wishes to join the room must first receive an invite to the
|
||||
/// room from someone already inside of the room.
|
||||
Invite,
|
||||
|
||||
/// Users can join the room if they are invited, or they can request an
|
||||
/// invite to the room.
|
||||
///
|
||||
/// They can be allowed (invited) or denied (kicked/banned) access.
|
||||
Knock,
|
||||
|
||||
/// Reserved but not yet implemented by the Matrix specification.
|
||||
Private,
|
||||
|
||||
/// Users can join the room if they are invited, or if they meet any of the
|
||||
/// conditions described in a set of [`AllowRule`]s.
|
||||
Restricted { rules: Vec<AllowRule> },
|
||||
|
||||
/// Users can join the room if they are invited, or if they meet any of the
|
||||
/// conditions described in a set of [`AllowRule`]s, or they can request
|
||||
/// an invite to the room.
|
||||
KnockRestricted { rules: Vec<AllowRule> },
|
||||
|
||||
/// A custom join rule, up for interpretation by the consumer.
|
||||
Custom {
|
||||
/// The string representation for this custom rule.
|
||||
repr: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// An allow rule which defines a condition that allows joining a room.
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum AllowRule {
|
||||
/// Only a member of the `room_id` Room can join the one this rule is used
|
||||
/// in.
|
||||
RoomMembership { room_id: String },
|
||||
}
|
||||
|
||||
impl TryFrom<JoinRule> for ruma::events::room::join_rules::JoinRule {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: JoinRule) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
JoinRule::Public => Ok(Self::Public),
|
||||
JoinRule::Invite => Ok(Self::Invite),
|
||||
JoinRule::Knock => Ok(Self::Knock),
|
||||
JoinRule::Private => Ok(Self::Private),
|
||||
JoinRule::Restricted { rules } => {
|
||||
let rules = allow_rules_from(rules)?;
|
||||
Ok(Self::Restricted(ruma::events::room::join_rules::Restricted::new(rules)))
|
||||
}
|
||||
JoinRule::KnockRestricted { rules } => {
|
||||
let rules = allow_rules_from(rules)?;
|
||||
Ok(Self::KnockRestricted(ruma::events::room::join_rules::Restricted::new(rules)))
|
||||
}
|
||||
JoinRule::Custom { repr } => Ok(serde_json::from_str(&repr)?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn allow_rules_from(
|
||||
value: Vec<AllowRule>,
|
||||
) -> Result<Vec<ruma::events::room::join_rules::AllowRule>, ClientError> {
|
||||
let mut ret = Vec::with_capacity(value.len());
|
||||
for rule in value {
|
||||
let rule: Result<ruma::events::room::join_rules::AllowRule, ClientError> = rule.try_into();
|
||||
match rule {
|
||||
Ok(rule) => ret.push(rule),
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
impl TryFrom<AllowRule> for ruma::events::room::join_rules::AllowRule {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: AllowRule) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
AllowRule::RoomMembership { room_id } => {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
Ok(Self::RoomMembership(ruma::events::room::join_rules::RoomMembership::new(
|
||||
room_id,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ pub struct QrCodeData {
|
||||
inner: qrcode::QrCodeData,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl QrCodeData {
|
||||
/// Attempt to decode a slice of bytes into a [`QrCodeData`] object.
|
||||
///
|
||||
@@ -159,7 +159,7 @@ pub enum QrLoginProgress {
|
||||
Done,
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait QrLoginProgressListener: Sync + Send {
|
||||
fn on_update(&self, state: QrLoginProgress);
|
||||
}
|
||||
@@ -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,
|
||||
@@ -270,7 +271,7 @@ pub struct ClientBuilder {
|
||||
request_config: Option<RequestConfig>,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl ClientBuilder {
|
||||
#[uniffi::constructor]
|
||||
pub fn new() -> Arc<Self> {
|
||||
@@ -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?,
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ pub struct ElementWellKnown {
|
||||
}
|
||||
|
||||
/// Helper function to parse a string into a ElementWellKnown struct
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn make_element_well_known(string: String) -> Result<ElementWellKnown, ClientError> {
|
||||
serde_json::from_str(&string).map_err(ClientError::new)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use matrix_sdk::{
|
||||
encryption::{backups, recovery},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tracing::{error, info};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use super::RUNTIME;
|
||||
@@ -23,22 +24,22 @@ pub struct Encryption {
|
||||
pub(crate) _client: Arc<Client>,
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait BackupStateListener: Sync + Send {
|
||||
fn on_update(&self, status: BackupState);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait BackupSteadyStateListener: Sync + Send {
|
||||
fn on_update(&self, status: BackupUploadState);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RecoveryStateListener: Sync + Send {
|
||||
fn on_update(&self, status: RecoveryState);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait VerificationStateListener: Sync + Send {
|
||||
fn on_update(&self, status: VerificationState);
|
||||
}
|
||||
@@ -162,7 +163,7 @@ impl From<recovery::RecoveryState> for RecoveryState {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait EnableRecoveryProgressListener: Sync + Send {
|
||||
fn on_update(&self, status: EnableRecoveryProgress);
|
||||
}
|
||||
@@ -212,7 +213,7 @@ impl From<encryption::VerificationState> for VerificationState {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Encryption {
|
||||
/// Get the public ed25519 key of our own device. This is usually what is
|
||||
/// called the fingerprint of the device.
|
||||
@@ -398,6 +399,7 @@ impl Encryption {
|
||||
listener: Box<dyn VerificationStateListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let mut subscriber = self.inner.verification_state();
|
||||
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
while let Some(verification_state) = subscriber.next().await {
|
||||
listener.on_update(verification_state.into());
|
||||
@@ -413,16 +415,40 @@ impl Encryption {
|
||||
|
||||
/// Get the E2EE identity of a user.
|
||||
///
|
||||
/// Returns Ok(None) if this user does not exist.
|
||||
/// This method always tries to fetch the identity from the store, which we
|
||||
/// only have if the user is tracked, meaning that we are both members
|
||||
/// of the same encrypted room. If no user is found locally, a request will
|
||||
/// be made to the homeserver.
|
||||
///
|
||||
/// Returns an error if there was a problem contacting the crypto store, or
|
||||
/// if our client is not logged in.
|
||||
pub async fn get_user_identity(
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user that the identity belongs to.
|
||||
///
|
||||
/// Returns a `UserIdentity` if one is found. Returns an error if there
|
||||
/// was an issue with the crypto store or with the request to the
|
||||
/// homeserver.
|
||||
///
|
||||
/// This will always return `None` if the client hasn't been logged in.
|
||||
pub async fn user_identity(
|
||||
&self,
|
||||
user_id: String,
|
||||
) -> Result<Option<Arc<UserIdentity>>, ClientError> {
|
||||
let identity = self.inner.get_user_identity(user_id.as_str().try_into()?).await?;
|
||||
Ok(identity.map(|i| Arc::new(UserIdentity { inner: i })))
|
||||
match self.inner.get_user_identity(user_id.as_str().try_into()?).await {
|
||||
Ok(Some(identity)) => {
|
||||
return Ok(Some(Arc::new(UserIdentity { inner: identity })));
|
||||
}
|
||||
Ok(None) => {
|
||||
info!("No identity found in the store.");
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Failed fetching identity from the store: {}", error);
|
||||
}
|
||||
};
|
||||
|
||||
info!("Requesting identity from the server.");
|
||||
|
||||
let identity = self.inner.request_user_identity(user_id.as_str().try_into()?).await?;
|
||||
Ok(identity.map(|identity| Arc::new(UserIdentity { inner: identity })))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,7 +458,7 @@ pub struct UserIdentity {
|
||||
inner: matrix_sdk::encryption::identities::UserIdentity,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl UserIdentity {
|
||||
/// Remember this identity, ensuring it does not result in a pin violation.
|
||||
///
|
||||
@@ -461,6 +487,14 @@ impl UserIdentity {
|
||||
pub(crate) fn master_key(&self) -> Option<String> {
|
||||
self.inner.master_key().get_first_key().map(|k| k.to_base64())
|
||||
}
|
||||
|
||||
/// Is the user identity considered to be verified.
|
||||
///
|
||||
/// If the identity belongs to another user, our own user identity needs to
|
||||
/// be verified as well for the identity to be considered to be verified.
|
||||
pub fn is_verified(&self) -> bool {
|
||||
self.inner.is_verified()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
@@ -468,7 +502,7 @@ pub struct IdentityResetHandle {
|
||||
pub(crate) inner: matrix_sdk::encryption::recovery::IdentityResetHandle,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl IdentityResetHandle {
|
||||
/// Get the underlying [`CrossSigningResetAuthType`] this identity reset
|
||||
/// process is using.
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
use std::fmt::Display;
|
||||
use std::{collections::HashMap, fmt, fmt::Display};
|
||||
|
||||
use matrix_sdk::{
|
||||
encryption::CryptoStoreError, event_cache::EventCacheError, oidc::OidcError, reqwest,
|
||||
room::edit::EditError, send_queue::RoomSendQueueError, HttpError, IdParseError,
|
||||
NotificationSettingsError as SdkNotificationSettingsError, StoreError,
|
||||
NotificationSettingsError as SdkNotificationSettingsError,
|
||||
QueueWedgeError as SdkQueueWedgeError, StoreError,
|
||||
};
|
||||
use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline};
|
||||
use uniffi::UnexpectedUniFFICallbackError;
|
||||
|
||||
use crate::room_list::RoomListError;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ClientError {
|
||||
#[error("client error: {msg}")]
|
||||
@@ -128,6 +131,12 @@ impl From<RoomError> for ClientError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomListError> for ClientError {
|
||||
fn from(e: RoomListError) -> Self {
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EventCacheError> for ClientError {
|
||||
fn from(e: EventCacheError) -> Self {
|
||||
Self::new(e)
|
||||
@@ -146,6 +155,96 @@ impl From<RoomSendQueueError> for ClientError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Bindings version of the sdk type replacing OwnedUserId/DeviceIds with simple
|
||||
/// String.
|
||||
///
|
||||
/// Represent a failed to send unrecoverable error of an event sent via the
|
||||
/// send_queue. It is a serializable representation of a client error, see
|
||||
/// `From` implementation for more details. These errors can not be
|
||||
/// automatically retried, but yet some manual action can be taken before retry
|
||||
/// sending. If not the only solution is to delete the local event.
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum QueueWedgeError {
|
||||
/// This error occurs when there are some insecure devices in the room, and
|
||||
/// the current encryption setting prohibit sharing with them.
|
||||
InsecureDevices {
|
||||
/// The insecure devices as a Map of userID to deviceID.
|
||||
user_device_map: HashMap<String, Vec<String>>,
|
||||
},
|
||||
|
||||
/// This error occurs when a previously verified user is not anymore, and
|
||||
/// the current encryption setting prohibit sharing when it happens.
|
||||
IdentityViolations {
|
||||
/// The users that are expected to be verified but are not.
|
||||
users: Vec<String>,
|
||||
},
|
||||
|
||||
/// It is required to set up cross-signing and properly erify the current
|
||||
/// session before sending.
|
||||
CrossVerificationRequired,
|
||||
|
||||
/// Some media content to be sent has disappeared from the cache.
|
||||
MissingMediaContent,
|
||||
|
||||
/// Some mime type couldn't be parsed.
|
||||
InvalidMimeType { mime_type: String },
|
||||
|
||||
/// Other errors.
|
||||
GenericApiError { msg: String },
|
||||
}
|
||||
|
||||
/// Simple display implementation that strips out userIds/DeviceIds to avoid
|
||||
/// accidental logging.
|
||||
impl Display for QueueWedgeError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
QueueWedgeError::InsecureDevices { .. } => {
|
||||
f.write_str("There are insecure devices in the room")
|
||||
}
|
||||
QueueWedgeError::IdentityViolations { .. } => {
|
||||
f.write_str("Some users that were previously verified are not anymore")
|
||||
}
|
||||
QueueWedgeError::CrossVerificationRequired => {
|
||||
f.write_str("Own verification is required")
|
||||
}
|
||||
QueueWedgeError::MissingMediaContent => {
|
||||
f.write_str("Media to be sent disappeared from local storage")
|
||||
}
|
||||
QueueWedgeError::InvalidMimeType { mime_type } => {
|
||||
write!(f, "Invalid mime type '{mime_type}' for media upload")
|
||||
}
|
||||
QueueWedgeError::GenericApiError { msg } => f.write_str(msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SdkQueueWedgeError> for QueueWedgeError {
|
||||
fn from(value: SdkQueueWedgeError) -> Self {
|
||||
match value {
|
||||
SdkQueueWedgeError::InsecureDevices { user_device_map } => Self::InsecureDevices {
|
||||
user_device_map: user_device_map
|
||||
.iter()
|
||||
.map(|(user_id, devices)| {
|
||||
(
|
||||
user_id.to_string(),
|
||||
devices.iter().map(|device_id| device_id.to_string()).collect(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
SdkQueueWedgeError::IdentityViolations { users } => Self::IdentityViolations {
|
||||
users: users.iter().map(ruma::OwnedUserId::to_string).collect(),
|
||||
},
|
||||
SdkQueueWedgeError::CrossVerificationRequired => Self::CrossVerificationRequired,
|
||||
SdkQueueWedgeError::MissingMediaContent => Self::MissingMediaContent,
|
||||
SdkQueueWedgeError::InvalidMimeType { mime_type } => {
|
||||
Self::InvalidMimeType { mime_type }
|
||||
}
|
||||
SdkQueueWedgeError::GenericApiError { msg } => Self::GenericApiError { msg },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum RoomError {
|
||||
@@ -217,3 +316,8 @@ impl From<matrix_sdk::Error> for NotificationSettingsError {
|
||||
Self::Generic { msg: e.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Something has not been implemented yet.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("not implemented yet")]
|
||||
pub struct NotYetImplemented;
|
||||
|
||||
@@ -20,7 +20,7 @@ use crate::{
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct TimelineEvent(pub(crate) AnySyncTimelineEvent);
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl TimelineEvent {
|
||||
pub fn event_id(&self) -> String {
|
||||
self.0.event_id().to_string()
|
||||
@@ -105,7 +105,7 @@ impl TryFrom<AnySyncStateEvent> for StateEventContent {
|
||||
let original_content = get_state_event_original_content(content)?;
|
||||
StateEventContent::RoomMemberContent {
|
||||
user_id: state_key,
|
||||
membership_state: original_content.membership.into(),
|
||||
membership_state: original_content.membership.try_into()?,
|
||||
}
|
||||
}
|
||||
AnySyncStateEvent::RoomName(_) => StateEventContent::RoomName,
|
||||
|
||||
@@ -16,6 +16,7 @@ mod notification;
|
||||
mod notification_settings;
|
||||
mod platform;
|
||||
mod room;
|
||||
mod room_alias;
|
||||
mod room_directory_search;
|
||||
mod room_info;
|
||||
mod room_list;
|
||||
@@ -44,7 +45,7 @@ use self::{
|
||||
|
||||
uniffi::include_scaffolding!("api");
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
fn sdk_git_sha() -> String {
|
||||
env!("VERGEN_GIT_SHA").to_owned()
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ pub struct NotificationClient {
|
||||
pub(crate) _client: Arc<Client>,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl NotificationClient {
|
||||
/// See also documentation of
|
||||
/// `MatrixNotificationClient::get_notification`.
|
||||
|
||||
@@ -49,7 +49,7 @@ impl From<RoomNotificationMode> for SdkRoomNotificationMode {
|
||||
}
|
||||
|
||||
/// Delegate to notify of changes in push rules
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait NotificationSettingsDelegate: Sync + Send {
|
||||
fn settings_did_change(&self);
|
||||
}
|
||||
@@ -98,7 +98,7 @@ impl Drop for NotificationSettings {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl NotificationSettings {
|
||||
pub fn set_delegate(&self, delegate: Option<Box<dyn NotificationSettingsDelegate>>) {
|
||||
if let Some(delegate) = delegate {
|
||||
|
||||
@@ -242,7 +242,7 @@ pub struct TracingConfiguration {
|
||||
write_to_files: Option<TracingFileConfiguration>,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn setup_tracing(config: TracingConfiguration) {
|
||||
log_panics();
|
||||
|
||||
|
||||
@@ -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,16 +39,17 @@ 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,
|
||||
};
|
||||
|
||||
#[derive(Debug, uniffi::Enum)]
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum Membership {
|
||||
Invited,
|
||||
Joined,
|
||||
Left,
|
||||
Knocked,
|
||||
}
|
||||
|
||||
impl From<RoomState> for Membership {
|
||||
@@ -58,6 +58,7 @@ impl From<RoomState> for Membership {
|
||||
RoomState::Invited => Membership::Invited,
|
||||
RoomState::Joined => Membership::Joined,
|
||||
RoomState::Left => Membership::Left,
|
||||
RoomState::Knocked => Membership::Knocked,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,7 +81,7 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Room {
|
||||
pub fn id(&self) -> String {
|
||||
self.inner.room_id().to_string()
|
||||
@@ -161,7 +162,12 @@ impl Room {
|
||||
/// the user who invited the logged-in user to a room.
|
||||
pub async fn inviter(&self) -> Option<RoomMember> {
|
||||
if self.inner.state() == RoomState::Invited {
|
||||
self.inner.invite_details().await.ok().and_then(|a| a.inviter).map(|m| m.into())
|
||||
self.inner
|
||||
.invite_details()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|a| a.inviter)
|
||||
.and_then(|m| m.try_into().ok())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -271,7 +277,7 @@ impl Room {
|
||||
pub async fn member(&self, user_id: String) -> Result<RoomMember, ClientError> {
|
||||
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
|
||||
let member = self.inner.get_member(&user_id).await?.context("User not found")?;
|
||||
Ok(member.into())
|
||||
Ok(member.try_into().context("Unknown state membership")?)
|
||||
}
|
||||
|
||||
pub async fn member_avatar_url(&self, user_id: String) -> Result<Option<String>, ClientError> {
|
||||
@@ -627,7 +633,7 @@ impl Room {
|
||||
}
|
||||
|
||||
pub async fn get_power_levels(&self) -> Result<RoomPowerLevels, ClientError> {
|
||||
let power_levels = self.inner.room_power_levels().await?;
|
||||
let power_levels = self.inner.power_levels().await.map_err(matrix_sdk::Error::from)?;
|
||||
Ok(RoomPowerLevels::from(power_levels))
|
||||
}
|
||||
|
||||
@@ -783,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<_, _>>()?;
|
||||
|
||||
@@ -798,7 +802,7 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
self.inner.send_queue().unwedge(&transaction_id).await?;
|
||||
send_handle.try_resend().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -816,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() {
|
||||
@@ -834,32 +836,14 @@ 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.
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn matrix_to_room_alias_permalink(
|
||||
room_alias: String,
|
||||
) -> std::result::Result<String, ClientError> {
|
||||
@@ -915,17 +899,17 @@ impl From<RumaPowerLevels> for RoomPowerLevels {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomInfoListener: Sync + Send {
|
||||
fn call(&self, room_info: RoomInfo);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait TypingNotificationsListener: Sync + Send {
|
||||
fn call(&self, typing_user_ids: Vec<String>);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait IdentityStatusChangeListener: Sync + Send {
|
||||
fn call(&self, identity_status_change: Vec<IdentityStatusChange>);
|
||||
}
|
||||
@@ -941,7 +925,7 @@ impl RoomMembersIterator {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomMembersIterator {
|
||||
fn len(&self) -> u32 {
|
||||
self.chunk_iterator.len()
|
||||
@@ -950,7 +934,7 @@ impl RoomMembersIterator {
|
||||
fn next_chunk(&self, chunk_size: u32) -> Option<Vec<RoomMember>> {
|
||||
self.chunk_iterator
|
||||
.next(chunk_size)
|
||||
.map(|members| members.into_iter().map(|m| m.into()).collect())
|
||||
.map(|members| members.into_iter().filter_map(|m| m.try_into().ok()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
use matrix_sdk::RoomDisplayName;
|
||||
|
||||
/// 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 {
|
||||
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 {
|
||||
RoomDisplayName::Named(room_name).to_room_alias_name()
|
||||
}
|
||||
@@ -18,6 +18,7 @@ use std::{fmt::Debug, sync::Arc};
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::room_directory_search::RoomDirectorySearch as SdkRoomDirectorySearch;
|
||||
use ruma::ServerName;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use super::RUNTIME;
|
||||
@@ -68,6 +69,12 @@ impl From<matrix_sdk::room_directory_search::RoomDescription> for RoomDescriptio
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper for performing room searches in the room directory.
|
||||
/// The way this is intended to be used is:
|
||||
///
|
||||
/// 1. Register a callback using [`RoomDirectorySearch::results`].
|
||||
/// 2. Start the room search with [`RoomDirectorySearch::search`].
|
||||
/// 3. To get more results, use [`RoomDirectorySearch::next_page`].
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomDirectorySearch {
|
||||
pub(crate) inner: RwLock<SdkRoomDirectorySearch>,
|
||||
@@ -79,30 +86,51 @@ impl RoomDirectorySearch {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomDirectorySearch {
|
||||
/// Asks the server for the next page of the current search.
|
||||
pub async fn next_page(&self) -> Result<(), ClientError> {
|
||||
let mut inner = self.inner.write().await;
|
||||
inner.next_page().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn search(&self, filter: Option<String>, batch_size: u32) -> Result<(), ClientError> {
|
||||
/// Starts a filtered search for the server.
|
||||
///
|
||||
/// If the `filter` is not provided it will search for all the rooms.
|
||||
/// You can specify a `batch_size` to control the number of rooms to fetch
|
||||
/// per request.
|
||||
///
|
||||
/// If the `via_server` is not provided it will search in the current
|
||||
/// homeserver by default.
|
||||
///
|
||||
/// This method will clear the current search results and start a new one.
|
||||
pub async fn search(
|
||||
&self,
|
||||
filter: Option<String>,
|
||||
batch_size: u32,
|
||||
via_server_name: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let server = via_server_name.map(ServerName::parse).transpose()?;
|
||||
let mut inner = self.inner.write().await;
|
||||
inner.search(filter, batch_size).await?;
|
||||
inner.search(filter, batch_size, server).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the number of pages that have been loaded so far.
|
||||
pub async fn loaded_pages(&self) -> Result<u32, ClientError> {
|
||||
let inner = self.inner.read().await;
|
||||
Ok(inner.loaded_pages() as u32)
|
||||
}
|
||||
|
||||
/// Get whether the search is at the last page.
|
||||
pub async fn is_at_last_page(&self) -> Result<bool, ClientError> {
|
||||
let inner = self.inner.read().await;
|
||||
Ok(inner.is_at_last_page())
|
||||
}
|
||||
|
||||
/// Registers a callback to receive new search results when starting a
|
||||
/// search or getting new paginated results.
|
||||
pub async fn results(
|
||||
&self,
|
||||
listener: Box<dyn RoomDirectorySearchEntriesListener>,
|
||||
@@ -169,7 +197,7 @@ impl From<VectorDiff<matrix_sdk::room_directory_search::RoomDescription>>
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomDirectorySearchEntriesListener: Send + Sync + Debug {
|
||||
fn on_update(&self, room_entries_update: Vec<RoomDirectorySearchEntryUpdate>);
|
||||
}
|
||||
|
||||
@@ -67,7 +67,8 @@ impl RoomInfo {
|
||||
for (id, level) in power_levels_map.iter() {
|
||||
user_power_levels.insert(id.to_string(), *level);
|
||||
}
|
||||
let pinned_event_ids = room.pinned_event_ids().iter().map(|id| id.to_string()).collect();
|
||||
let pinned_event_ids =
|
||||
room.pinned_event_ids().unwrap_or_default().iter().map(|id| id.to_string()).collect();
|
||||
|
||||
Ok(Self {
|
||||
id: room.room_id().to_string(),
|
||||
@@ -90,7 +91,10 @@ impl RoomInfo {
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|details| details.inviter)
|
||||
.map(Into::into),
|
||||
.map(TryInto::try_into)
|
||||
.transpose()
|
||||
.ok()
|
||||
.flatten(),
|
||||
_ => None,
|
||||
},
|
||||
heroes: room.heroes().into_iter().map(Into::into).collect(),
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Duration};
|
||||
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{pin_mut, StreamExt, TryFutureExt};
|
||||
use matrix_sdk::{
|
||||
ruma::{
|
||||
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
assign, RoomId,
|
||||
},
|
||||
sliding_sync::http,
|
||||
use matrix_sdk::ruma::{
|
||||
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
RoomId,
|
||||
};
|
||||
use matrix_sdk_ui::{
|
||||
room_list_service::filters::{
|
||||
@@ -19,14 +18,17 @@ use matrix_sdk_ui::{
|
||||
timeline::default_event_filter,
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
use ruma::{OwnedRoomOrAliasId, OwnedServerName, ServerName};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
error::ClientError,
|
||||
room::{Membership, Room},
|
||||
room_info::RoomInfo,
|
||||
room_preview::RoomPreview,
|
||||
timeline::{EventTimelineItem, Timeline},
|
||||
timeline_event_filter::TimelineEventTypeFilter,
|
||||
utils::AsyncRuntimeDropped,
|
||||
TaskHandle, RUNTIME,
|
||||
};
|
||||
|
||||
@@ -51,7 +53,7 @@ pub enum RoomListError {
|
||||
#[error("Event cache ran into an error: {error}")]
|
||||
EventCache { error: String },
|
||||
#[error("The requested room doesn't match the membership requirements {expected:?}, observed {actual:?}")]
|
||||
IncorrectRoomMembership { expected: Membership, actual: Membership },
|
||||
IncorrectRoomMembership { expected: Vec<Membership>, actual: Membership },
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_ui::room_list_service::Error> for RoomListError {
|
||||
@@ -85,7 +87,7 @@ pub struct RoomListService {
|
||||
pub(crate) utd_hook: Option<Arc<UtdHookManager>>,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomListService {
|
||||
fn state(&self, listener: Box<dyn RoomListServiceStateListener>) -> Arc<TaskHandle> {
|
||||
let state_stream = self.inner.state();
|
||||
@@ -135,11 +137,7 @@ impl RoomListService {
|
||||
})))
|
||||
}
|
||||
|
||||
fn subscribe_to_rooms(
|
||||
&self,
|
||||
room_ids: Vec<String>,
|
||||
settings: Option<RoomSubscription>,
|
||||
) -> Result<(), RoomListError> {
|
||||
fn subscribe_to_rooms(&self, room_ids: Vec<String>) -> Result<(), RoomListError> {
|
||||
let room_ids = room_ids
|
||||
.into_iter()
|
||||
.map(|room_id| {
|
||||
@@ -147,10 +145,7 @@ impl RoomListService {
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
self.inner.subscribe_to_rooms(
|
||||
&room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>(),
|
||||
settings.map(Into::into),
|
||||
);
|
||||
self.inner.subscribe_to_rooms(&room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -162,7 +157,7 @@ pub struct RoomList {
|
||||
inner: Arc<matrix_sdk_ui::room_list_service::RoomList>,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomList {
|
||||
fn loading_state(
|
||||
&self,
|
||||
@@ -188,7 +183,6 @@ impl RoomList {
|
||||
listener: Box<dyn RoomListEntriesListener>,
|
||||
) -> Arc<RoomListEntriesWithDynamicAdaptersResult> {
|
||||
let this = self.clone();
|
||||
let client = self.room_list_service.inner.client();
|
||||
let utd_hook = self.room_list_service.utd_hook.clone();
|
||||
|
||||
// The following code deserves a bit of explanation.
|
||||
@@ -236,10 +230,7 @@ impl RoomList {
|
||||
// borrowing `this`, which is going to live long enough since it will live as
|
||||
// long as `entries_stream` and `dynamic_entries_controller`.
|
||||
let (entries_stream, dynamic_entries_controller) =
|
||||
this.inner.entries_with_dynamic_adapters(
|
||||
page_size.try_into().unwrap(),
|
||||
client.room_info_notable_update_receiver(),
|
||||
);
|
||||
this.inner.entries_with_dynamic_adapters(page_size.try_into().unwrap());
|
||||
|
||||
// FFI dance to make those values consumable by foreign language, nothing fancy
|
||||
// here, that's the real code for this method.
|
||||
@@ -292,7 +283,7 @@ pub struct RoomListEntriesWithDynamicAdaptersResult {
|
||||
entries_stream: Arc<TaskHandle>,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomListEntriesWithDynamicAdaptersResult {
|
||||
fn controller(&self) -> Arc<RoomListDynamicEntriesController> {
|
||||
self.controller.clone()
|
||||
@@ -370,17 +361,17 @@ impl From<matrix_sdk_ui::room_list_service::RoomListLoadingState> for RoomListLo
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomListServiceStateListener: Send + Sync + Debug {
|
||||
fn on_update(&self, state: RoomListServiceState);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomListLoadingStateListener: Send + Sync + Debug {
|
||||
fn on_update(&self, state: RoomListLoadingState);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomListServiceSyncIndicatorListener: Send + Sync + Debug {
|
||||
fn on_update(&self, sync_indicator: RoomListServiceSyncIndicator);
|
||||
}
|
||||
@@ -443,7 +434,7 @@ impl RoomListEntriesUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomListEntriesListener: Send + Sync + Debug {
|
||||
fn on_update(&self, room_entries_update: Vec<RoomListEntriesUpdate>);
|
||||
}
|
||||
@@ -461,7 +452,7 @@ impl RoomListDynamicEntriesController {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomListDynamicEntriesController {
|
||||
fn set_filter(&self, kind: RoomListEntriesDynamicFilterKind) -> bool {
|
||||
self.inner.set_filter(kind.into())
|
||||
@@ -549,7 +540,7 @@ impl RoomListItem {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomListItem {
|
||||
fn id(&self) -> String {
|
||||
self.inner.id().to_string()
|
||||
@@ -584,23 +575,60 @@ impl RoomListItem {
|
||||
}
|
||||
|
||||
/// Builds a `Room` FFI from an invited room without initializing its
|
||||
/// internal timeline
|
||||
/// internal timeline.
|
||||
///
|
||||
/// An error will be returned if the room is a state different than invited
|
||||
/// An error will be returned if the room is a state different than invited.
|
||||
///
|
||||
/// ⚠️ Holding on to this room instance after it has been joined is not
|
||||
/// safe. Use `full_room` instead
|
||||
/// safe. Use `full_room` instead.
|
||||
#[deprecated(note = "Please use `preview_room` instead.")]
|
||||
fn invited_room(&self) -> Result<Arc<Room>, RoomListError> {
|
||||
if !matches!(self.membership(), Membership::Invited) {
|
||||
return Err(RoomListError::IncorrectRoomMembership {
|
||||
expected: Membership::Invited,
|
||||
expected: vec![Membership::Invited],
|
||||
actual: self.membership(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Arc::new(Room::new(self.inner.inner_room().clone())))
|
||||
}
|
||||
|
||||
/// Builds a `RoomPreview` from a room list item. This is intended for
|
||||
/// invited or knocked rooms.
|
||||
///
|
||||
/// An error will be returned if the room is in a state other than invited
|
||||
/// or knocked.
|
||||
async fn preview_room(&self, via: Vec<String>) -> Result<Arc<RoomPreview>, ClientError> {
|
||||
// Validate parameters first.
|
||||
let server_names: Vec<OwnedServerName> = via
|
||||
.into_iter()
|
||||
.map(|server| ServerName::parse(server).map_err(ClientError::from))
|
||||
.collect::<Result<_, ClientError>>()?;
|
||||
|
||||
// Validate internal room state.
|
||||
let membership = self.membership();
|
||||
if !matches!(membership, Membership::Invited | Membership::Knocked) {
|
||||
return Err(RoomListError::IncorrectRoomMembership {
|
||||
expected: vec![Membership::Invited, Membership::Knocked],
|
||||
actual: membership,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
// Do the thing.
|
||||
let client = self.inner.client();
|
||||
let (room_or_alias_id, server_names) = if let Some(alias) = self.inner.canonical_alias() {
|
||||
let room_or_alias_id: OwnedRoomOrAliasId = alias.into();
|
||||
(room_or_alias_id, Vec::new())
|
||||
} else {
|
||||
let room_or_alias_id: OwnedRoomOrAliasId = self.inner.id().to_owned().into();
|
||||
(room_or_alias_id, server_names)
|
||||
};
|
||||
|
||||
let room_preview = client.get_room_preview(&room_or_alias_id, server_names).await?;
|
||||
|
||||
Ok(Arc::new(RoomPreview::new(AsyncRuntimeDropped::new(client), room_preview)))
|
||||
}
|
||||
|
||||
/// Build a full `Room` FFI object, filling its associated timeline.
|
||||
///
|
||||
/// An error will be returned if the room is a state different than joined
|
||||
@@ -608,7 +636,7 @@ impl RoomListItem {
|
||||
fn full_room(&self) -> Result<Arc<Room>, RoomListError> {
|
||||
if !matches!(self.membership(), Membership::Joined) {
|
||||
return Err(RoomListError::IncorrectRoomMembership {
|
||||
expected: Membership::Joined,
|
||||
expected: vec![Membership::Joined],
|
||||
actual: self.membership(),
|
||||
});
|
||||
}
|
||||
@@ -680,38 +708,13 @@ impl RoomListItem {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RequiredState {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomSubscription {
|
||||
pub required_state: Option<Vec<RequiredState>>,
|
||||
pub timeline_limit: u32,
|
||||
pub include_heroes: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<RoomSubscription> for http::request::RoomSubscription {
|
||||
fn from(val: RoomSubscription) -> Self {
|
||||
assign!(http::request::RoomSubscription::default(), {
|
||||
required_state: val.required_state.map(|r|
|
||||
r.into_iter().map(|s| (s.key.into(), s.value)).collect()
|
||||
).unwrap_or_default(),
|
||||
timeline_limit: val.timeline_limit.into(),
|
||||
include_heroes: val.include_heroes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct UnreadNotificationsCount {
|
||||
highlight_count: u32,
|
||||
notification_count: u32,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl UnreadNotificationsCount {
|
||||
fn highlight_count(&self) -> u32 {
|
||||
self.highlight_count
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use matrix_sdk::room::{RoomMember as SdkRoomMember, RoomMemberRole};
|
||||
use ruma::UserId;
|
||||
|
||||
use crate::error::ClientError;
|
||||
use crate::error::{ClientError, NotYetImplemented};
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum MembershipState {
|
||||
@@ -19,43 +19,58 @@ pub enum MembershipState {
|
||||
|
||||
/// The user has left.
|
||||
Leave,
|
||||
|
||||
/// A custom membership state value.
|
||||
Custom { value: String },
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::ruma::events::room::member::MembershipState> for MembershipState {
|
||||
fn from(m: matrix_sdk::ruma::events::room::member::MembershipState) -> Self {
|
||||
impl TryFrom<matrix_sdk::ruma::events::room::member::MembershipState> for MembershipState {
|
||||
type Error = NotYetImplemented;
|
||||
|
||||
fn try_from(
|
||||
m: matrix_sdk::ruma::events::room::member::MembershipState,
|
||||
) -> Result<Self, Self::Error> {
|
||||
match m {
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Ban => MembershipState::Ban,
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Invite => {
|
||||
MembershipState::Invite
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Ban => {
|
||||
Ok(MembershipState::Ban)
|
||||
}
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Invite => {
|
||||
Ok(MembershipState::Invite)
|
||||
}
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Join => {
|
||||
Ok(MembershipState::Join)
|
||||
}
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Join => MembershipState::Join,
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Knock => {
|
||||
MembershipState::Knock
|
||||
Ok(MembershipState::Knock)
|
||||
}
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Leave => {
|
||||
MembershipState::Leave
|
||||
Ok(MembershipState::Leave)
|
||||
}
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::_Custom(_) => {
|
||||
Ok(MembershipState::Custom { value: m.to_string() })
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("Other membership state change not yet implemented");
|
||||
Err(NotYetImplemented)
|
||||
}
|
||||
_ => unimplemented!(
|
||||
"Handle Custom case: https://github.com/matrix-org/matrix-rust-sdk/issues/1254"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn suggested_role_for_power_level(power_level: i64) -> RoomMemberRole {
|
||||
// It's not possible to expose the constructor on the Enum through Uniffi ☹️
|
||||
RoomMemberRole::suggested_role_for_power_level(power_level)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn suggested_power_level_for_role(role: RoomMemberRole) -> i64 {
|
||||
// It's not possible to expose methods on an Enum through Uniffi ☹️
|
||||
role.suggested_power_level()
|
||||
}
|
||||
|
||||
/// Generates a `matrix.to` permalink to the given userID.
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn matrix_to_user_permalink(user_id: String) -> Result<String, ClientError> {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
Ok(user_id.matrix_to_uri().to_string())
|
||||
@@ -74,18 +89,20 @@ pub struct RoomMember {
|
||||
pub suggested_role_for_power_level: RoomMemberRole,
|
||||
}
|
||||
|
||||
impl From<SdkRoomMember> for RoomMember {
|
||||
fn from(m: SdkRoomMember) -> Self {
|
||||
RoomMember {
|
||||
impl TryFrom<SdkRoomMember> for RoomMember {
|
||||
type Error = NotYetImplemented;
|
||||
|
||||
fn try_from(m: SdkRoomMember) -> Result<Self, Self::Error> {
|
||||
Ok(RoomMember {
|
||||
user_id: m.user_id().to_string(),
|
||||
display_name: m.display_name().map(|s| s.to_owned()),
|
||||
avatar_url: m.avatar_url().map(|a| a.to_string()),
|
||||
membership: m.membership().clone().into(),
|
||||
membership: m.membership().clone().try_into()?,
|
||||
is_name_ambiguous: m.name_ambiguous(),
|
||||
power_level: m.power_level(),
|
||||
normalized_power_level: m.normalized_power_level(),
|
||||
is_ignored: m.is_ignored(),
|
||||
suggested_role_for_power_level: m.suggested_role_for_power_level(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,73 @@
|
||||
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, RoomState};
|
||||
use ruma::space::SpaceRoomJoinRule;
|
||||
use anyhow::Context as _;
|
||||
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, Client};
|
||||
use ruma::{room::RoomType as RumaRoomType, space::SpaceRoomJoinRule};
|
||||
use tracing::warn;
|
||||
|
||||
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.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomPreview {
|
||||
inner: SdkRoomPreview,
|
||||
client: AsyncRuntimeDropped<Client>,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomPreview {
|
||||
/// Returns the room info the preview contains.
|
||||
pub fn info(&self) -> Result<RoomPreviewInfo, ClientError> {
|
||||
let info = &self.inner;
|
||||
Ok(RoomPreviewInfo {
|
||||
room_id: info.room_id.to_string(),
|
||||
canonical_alias: info.canonical_alias.as_ref().map(|alias| alias.to_string()),
|
||||
name: info.name.clone(),
|
||||
topic: info.topic.clone(),
|
||||
avatar_url: info.avatar_url.as_ref().map(|url| url.to_string()),
|
||||
num_joined_members: info.num_joined_members,
|
||||
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
|
||||
.join_rule
|
||||
.clone()
|
||||
.try_into()
|
||||
.map_err(|_| anyhow::anyhow!("unhandled SpaceRoomJoinRule kind"))?,
|
||||
is_direct: info.is_direct,
|
||||
})
|
||||
}
|
||||
|
||||
/// Leave the room if the room preview state is either joined, invited or
|
||||
/// knocked.
|
||||
///
|
||||
/// Will return an error otherwise.
|
||||
pub async fn leave(&self) -> Result<(), ClientError> {
|
||||
let room =
|
||||
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 {
|
||||
pub(crate) fn new(client: AsyncRuntimeDropped<Client>, inner: SdkRoomPreview) -> Self {
|
||||
Self { client, inner }
|
||||
}
|
||||
}
|
||||
|
||||
/// The preview of a room, be it invited/joined/left, or not.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomPreview {
|
||||
pub struct RoomPreviewInfo {
|
||||
/// The room id for this room.
|
||||
pub room_id: String,
|
||||
/// The canonical alias for the room.
|
||||
@@ -16,38 +80,60 @@ pub struct RoomPreview {
|
||||
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,
|
||||
/// Is the room joined by the current user?
|
||||
pub is_joined: bool,
|
||||
/// Is the current user invited to this room?
|
||||
pub is_invited: bool,
|
||||
/// is the join rule public for this room?
|
||||
pub is_public: bool,
|
||||
/// Can we knock (or restricted-knock) to this room?
|
||||
pub can_knock: 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 RoomPreview {
|
||||
pub(crate) fn from_sdk(preview: SdkRoomPreview) -> Self {
|
||||
Self {
|
||||
room_id: preview.room_id.to_string(),
|
||||
canonical_alias: preview.canonical_alias.map(|alias| alias.to_string()),
|
||||
name: preview.name,
|
||||
topic: preview.topic,
|
||||
avatar_url: preview.avatar_url.map(|url| url.to_string()),
|
||||
num_joined_members: preview.num_joined_members,
|
||||
room_type: preview.room_type.map(|room_type| room_type.to_string()),
|
||||
is_history_world_readable: preview.is_world_readable,
|
||||
is_joined: preview.state.map_or(false, |state| state == RoomState::Joined),
|
||||
is_invited: preview.state.map_or(false, |state| state == RoomState::Invited),
|
||||
is_public: preview.join_rule == SpaceRoomJoinRule::Public,
|
||||
can_knock: matches!(
|
||||
preview.join_rule,
|
||||
SpaceRoomJoinRule::KnockRestricted | SpaceRoomJoinRule::Knock
|
||||
),
|
||||
impl TryFrom<SpaceRoomJoinRule> for JoinRule {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(join_rule: SpaceRoomJoinRule) -> Result<Self, ()> {
|
||||
Ok(match join_rule {
|
||||
SpaceRoomJoinRule::Invite => JoinRule::Invite,
|
||||
SpaceRoomJoinRule::Knock => JoinRule::Knock,
|
||||
SpaceRoomJoinRule::Private => JoinRule::Private,
|
||||
SpaceRoomJoinRule::Restricted => JoinRule::Restricted { rules: Vec::new() },
|
||||
SpaceRoomJoinRule::KnockRestricted => JoinRule::KnockRestricted { rules: Vec::new() },
|
||||
SpaceRoomJoinRule::Public => JoinRule::Public,
|
||||
SpaceRoomJoinRule::_Custom(_) => JoinRule::Custom { repr: join_rule.to_string() },
|
||||
_ => {
|
||||
warn!("unhandled SpaceRoomJoinRule: {join_rule}");
|
||||
return Err(());
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ impl From<AuthData> for ruma::api::client::uiaa::AuthData {
|
||||
|
||||
/// Parse a matrix entity from a given URI, be it either
|
||||
/// a `matrix.to` link or a `matrix:` URI
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn parse_matrix_entity_from(uri: String) -> Option<MatrixEntity> {
|
||||
if let Ok(matrix_uri) = RumaMatrixUri::parse(&uri) {
|
||||
return Some(MatrixEntity {
|
||||
@@ -154,33 +154,33 @@ impl From<&RumaMatrixId> for MatrixId {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn media_source_from_url(url: String) -> Arc<MediaSource> {
|
||||
Arc::new(MediaSource::Plain(url.into()))
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn message_event_content_new(
|
||||
msgtype: MessageType,
|
||||
) -> Result<Arc<RoomMessageEventContentWithoutRelation>, ClientError> {
|
||||
Ok(Arc::new(RoomMessageEventContentWithoutRelation::new(msgtype.try_into()?)))
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn message_event_content_from_markdown(
|
||||
md: String,
|
||||
) -> Arc<RoomMessageEventContentWithoutRelation> {
|
||||
Arc::new(RoomMessageEventContentWithoutRelation::new(RumaMessageType::text_markdown(md)))
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn message_event_content_from_markdown_as_emote(
|
||||
md: String,
|
||||
) -> Arc<RoomMessageEventContentWithoutRelation> {
|
||||
Arc::new(RoomMessageEventContentWithoutRelation::new(RumaMessageType::emote_markdown(md)))
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn message_event_content_from_html(
|
||||
body: String,
|
||||
html_body: String,
|
||||
@@ -190,7 +190,7 @@ pub fn message_event_content_from_html(
|
||||
)))
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn message_event_content_from_html_as_emote(
|
||||
body: String,
|
||||
html_body: String,
|
||||
@@ -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>,
|
||||
@@ -918,7 +891,7 @@ impl From<RumaPollKind> for PollKind {
|
||||
|
||||
/// Creates a [`RoomMessageEventContentWithoutRelation`] given a
|
||||
/// [`MessageContent`] value.
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn content_without_relation_from_message(
|
||||
message: MessageContent,
|
||||
) -> Result<Arc<RoomMessageEventContentWithoutRelation>, ClientError> {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
encryption::{
|
||||
identities::UserIdentity,
|
||||
verification::{SasState, SasVerification, VerificationRequest},
|
||||
verification::{SasState, SasVerification, VerificationRequest, VerificationRequestState},
|
||||
Encryption,
|
||||
},
|
||||
ruma::events::{key::verification::VerificationMethod, AnyToDeviceEvent},
|
||||
};
|
||||
use ruma::UserId;
|
||||
use tracing::{error, info};
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::error::ClientError;
|
||||
@@ -20,7 +21,7 @@ pub struct SessionVerificationEmoji {
|
||||
description: String,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SessionVerificationEmoji {
|
||||
pub fn symbol(&self) -> String {
|
||||
self.symbol.clone()
|
||||
@@ -37,8 +38,20 @@ pub enum SessionVerificationData {
|
||||
Decimals { values: Vec<u16> },
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
/// Details about the incoming verification request
|
||||
#[derive(Debug, uniffi::Record)]
|
||||
pub struct SessionVerificationRequestDetails {
|
||||
sender_id: String,
|
||||
flow_id: String,
|
||||
device_id: String,
|
||||
display_name: Option<String>,
|
||||
/// First time this device was seen in milliseconds since epoch.
|
||||
first_seen_timestamp: u64,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SessionVerificationControllerDelegate: Sync + Send {
|
||||
fn did_receive_verification_request(&self, details: SessionVerificationRequestDetails);
|
||||
fn did_accept_verification_request(&self);
|
||||
fn did_start_sas_verification(&self);
|
||||
fn did_receive_verification_data(&self, data: SessionVerificationData);
|
||||
@@ -58,19 +71,53 @@ pub struct SessionVerificationController {
|
||||
sas_verification: Arc<RwLock<Option<SasVerification>>>,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SessionVerificationController {
|
||||
pub async fn is_verified(&self) -> Result<bool, ClientError> {
|
||||
let device =
|
||||
self.encryption.get_own_device().await?.context("Our own device is missing")?;
|
||||
|
||||
Ok(device.is_cross_signed_by_owner())
|
||||
}
|
||||
|
||||
pub fn set_delegate(&self, delegate: Option<Box<dyn SessionVerificationControllerDelegate>>) {
|
||||
*self.delegate.write().unwrap() = delegate;
|
||||
}
|
||||
|
||||
/// Set this particular request as the currently active one and register for
|
||||
/// events pertaining it.
|
||||
/// * `sender_id` - The user requesting verification.
|
||||
/// * `flow_id` - - The ID that uniquely identifies the verification flow.
|
||||
pub async fn acknowledge_verification_request(
|
||||
&self,
|
||||
sender_id: String,
|
||||
flow_id: String,
|
||||
) -> Result<(), ClientError> {
|
||||
let sender_id = UserId::parse(sender_id.clone())?;
|
||||
|
||||
let verification_request = self
|
||||
.encryption
|
||||
.get_verification_request(&sender_id, flow_id)
|
||||
.await
|
||||
.ok_or(ClientError::new("Unknown session verification request"))?;
|
||||
|
||||
*self.verification_request.write().unwrap() = Some(verification_request.clone());
|
||||
|
||||
RUNTIME.spawn(Self::listen_to_verification_request_changes(
|
||||
verification_request,
|
||||
self.sas_verification.clone(),
|
||||
self.delegate.clone(),
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Accept the previously acknowledged verification request
|
||||
pub async fn accept_verification_request(&self) -> Result<(), ClientError> {
|
||||
let verification_request = self.verification_request.read().unwrap().clone();
|
||||
|
||||
if let Some(verification_request) = verification_request {
|
||||
let methods = vec![VerificationMethod::SasV1];
|
||||
verification_request.accept_with_methods(methods).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Request verification for the current device
|
||||
pub async fn request_verification(&self) -> Result<(), ClientError> {
|
||||
let methods = vec![VerificationMethod::SasV1];
|
||||
let verification_request = self
|
||||
@@ -78,30 +125,41 @@ impl SessionVerificationController {
|
||||
.request_verification_with_methods(methods)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?;
|
||||
*self.verification_request.write().unwrap() = Some(verification_request);
|
||||
|
||||
*self.verification_request.write().unwrap() = Some(verification_request.clone());
|
||||
|
||||
RUNTIME.spawn(Self::listen_to_verification_request_changes(
|
||||
verification_request,
|
||||
self.sas_verification.clone(),
|
||||
self.delegate.clone(),
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Transition the current verification request into a SAS verification
|
||||
/// flow.
|
||||
pub async fn start_sas_verification(&self) -> Result<(), ClientError> {
|
||||
let verification_request = self.verification_request.read().unwrap().clone();
|
||||
|
||||
if let Some(verification) = verification_request {
|
||||
match verification.start_sas().await {
|
||||
Ok(Some(verification)) => {
|
||||
*self.sas_verification.write().unwrap() = Some(verification.clone());
|
||||
let Some(verification_request) = verification_request else {
|
||||
return Err(ClientError::new("Verification request missing."));
|
||||
};
|
||||
|
||||
if let Some(delegate) = &*self.delegate.read().unwrap() {
|
||||
delegate.did_start_sas_verification()
|
||||
}
|
||||
match verification_request.start_sas().await {
|
||||
Ok(Some(verification)) => {
|
||||
*self.sas_verification.write().unwrap() = Some(verification.clone());
|
||||
|
||||
let delegate = self.delegate.clone();
|
||||
RUNTIME.spawn(Self::listen_to_changes(delegate, verification));
|
||||
if let Some(delegate) = &*self.delegate.read().unwrap() {
|
||||
delegate.did_start_sas_verification()
|
||||
}
|
||||
_ => {
|
||||
if let Some(delegate) = &*self.delegate.read().unwrap() {
|
||||
delegate.did_fail()
|
||||
}
|
||||
|
||||
let delegate = self.delegate.clone();
|
||||
RUNTIME.spawn(Self::listen_to_sas_verification_changes(verification, delegate));
|
||||
}
|
||||
_ => {
|
||||
if let Some(delegate) = &*self.delegate.read().unwrap() {
|
||||
delegate.did_fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,31 +167,37 @@ impl SessionVerificationController {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Confirm that the short auth strings match on both sides.
|
||||
pub async fn approve_verification(&self) -> Result<(), ClientError> {
|
||||
let sas_verification = self.sas_verification.read().unwrap().clone();
|
||||
if let Some(sas_verification) = sas_verification {
|
||||
sas_verification.confirm().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
let Some(sas_verification) = sas_verification else {
|
||||
return Err(ClientError::new("SAS verification missing"));
|
||||
};
|
||||
|
||||
Ok(sas_verification.confirm().await?)
|
||||
}
|
||||
|
||||
/// Reject the short auth string
|
||||
pub async fn decline_verification(&self) -> Result<(), ClientError> {
|
||||
let sas_verification = self.sas_verification.read().unwrap().clone();
|
||||
if let Some(sas_verification) = sas_verification {
|
||||
sas_verification.mismatch().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
let Some(sas_verification) = sas_verification else {
|
||||
return Err(ClientError::new("SAS verification missing"));
|
||||
};
|
||||
|
||||
Ok(sas_verification.mismatch().await?)
|
||||
}
|
||||
|
||||
/// Cancel the current verification request
|
||||
pub async fn cancel_verification(&self) -> Result<(), ClientError> {
|
||||
let verification_request = self.verification_request.read().unwrap().clone();
|
||||
if let Some(verification) = verification_request {
|
||||
verification.cancel().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
let Some(verification_request) = verification_request else {
|
||||
return Err(ClientError::new("Verification request missing."));
|
||||
};
|
||||
|
||||
Ok(verification_request.cancel().await?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,58 +213,88 @@ impl SessionVerificationController {
|
||||
}
|
||||
|
||||
pub(crate) async fn process_to_device_message(&self, event: AnyToDeviceEvent) {
|
||||
match event {
|
||||
// TODO: Use the changes stream for this as well once we expose
|
||||
// VerificationRequest::changes() in the main crate.
|
||||
AnyToDeviceEvent::KeyVerificationStart(event) => {
|
||||
if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) {
|
||||
return;
|
||||
}
|
||||
if let Some(verification) = self
|
||||
.encryption
|
||||
.get_verification(
|
||||
self.user_identity.user_id(),
|
||||
event.content.transaction_id.as_str(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
if let Some(sas_verification) = verification.sas() {
|
||||
*self.sas_verification.write().unwrap() = Some(sas_verification.clone());
|
||||
if let AnyToDeviceEvent::KeyVerificationRequest(event) = event {
|
||||
info!("Received verification request: {:}", event.sender);
|
||||
|
||||
if sas_verification.accept().await.is_ok() {
|
||||
if let Some(delegate) = &*self.delegate.read().unwrap() {
|
||||
delegate.did_start_sas_verification()
|
||||
}
|
||||
let Some(request) = self
|
||||
.encryption
|
||||
.get_verification_request(&event.sender, &event.content.transaction_id)
|
||||
.await
|
||||
else {
|
||||
error!("Failed retrieving verification request");
|
||||
return;
|
||||
};
|
||||
|
||||
let delegate = self.delegate.clone();
|
||||
RUNTIME.spawn(Self::listen_to_changes(delegate, sas_verification));
|
||||
} else if let Some(delegate) = &*self.delegate.read().unwrap() {
|
||||
delegate.did_fail()
|
||||
if !request.is_self_verification() {
|
||||
info!("Received non-self verification request. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
let VerificationRequestState::Requested { other_device_data, .. } = request.state()
|
||||
else {
|
||||
error!("Received key verification event but the request is in the wrong state.");
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(delegate) = &*self.delegate.read().unwrap() {
|
||||
delegate.did_receive_verification_request(SessionVerificationRequestDetails {
|
||||
sender_id: request.other_user_id().into(),
|
||||
flow_id: request.flow_id().into(),
|
||||
device_id: other_device_data.device_id().into(),
|
||||
display_name: other_device_data.display_name().map(str::to_string),
|
||||
first_seen_timestamp: other_device_data.first_time_seen_ts().get().into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn listen_to_verification_request_changes(
|
||||
verification_request: VerificationRequest,
|
||||
sas_verification: Arc<RwLock<Option<SasVerification>>>,
|
||||
delegate: Delegate,
|
||||
) {
|
||||
let mut stream = verification_request.changes();
|
||||
|
||||
while let Some(state) = stream.next().await {
|
||||
match state {
|
||||
VerificationRequestState::Transitioned { verification } => {
|
||||
let Some(verification) = verification.sas() else {
|
||||
error!("Invalid, non-sas verification flow. Returning.");
|
||||
return;
|
||||
};
|
||||
|
||||
*sas_verification.write().unwrap() = Some(verification.clone());
|
||||
|
||||
if verification.accept().await.is_ok() {
|
||||
if let Some(delegate) = &*delegate.read().unwrap() {
|
||||
delegate.did_start_sas_verification()
|
||||
}
|
||||
|
||||
let delegate = delegate.clone();
|
||||
RUNTIME.spawn(Self::listen_to_sas_verification_changes(
|
||||
verification,
|
||||
delegate,
|
||||
));
|
||||
} else if let Some(delegate) = &*delegate.read().unwrap() {
|
||||
delegate.did_fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
AnyToDeviceEvent::KeyVerificationReady(event) => {
|
||||
if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) {
|
||||
return;
|
||||
VerificationRequestState::Ready { .. } => {
|
||||
if let Some(delegate) = &*delegate.read().unwrap() {
|
||||
delegate.did_accept_verification_request()
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(delegate) = &*self.delegate.read().unwrap() {
|
||||
delegate.did_accept_verification_request()
|
||||
VerificationRequestState::Cancelled(..) => {
|
||||
if let Some(delegate) = &*delegate.read().unwrap() {
|
||||
delegate.did_cancel();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_transaction_id_valid(&self, transaction_id: String) -> bool {
|
||||
match &*self.verification_request.read().unwrap() {
|
||||
Some(verification) => verification.flow_id() == transaction_id,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn listen_to_changes(delegate: Delegate, sas: SasVerification) {
|
||||
async fn listen_to_sas_verification_changes(sas: SasVerification, delegate: Delegate) {
|
||||
let mut stream = sas.changes();
|
||||
|
||||
while let Some(state) = stream.next().await {
|
||||
|
||||
@@ -51,7 +51,7 @@ impl From<MatrixSyncServiceState> for SyncServiceState {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SyncServiceStateObserver: Send + Sync + Debug {
|
||||
fn on_update(&self, state: SyncServiceState);
|
||||
}
|
||||
@@ -62,7 +62,7 @@ pub struct SyncService {
|
||||
utd_hook: Option<Arc<UtdHookManager>>,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SyncService {
|
||||
pub fn room_list_service(&self) -> Arc<RoomListService> {
|
||||
Arc::new(RoomListService {
|
||||
@@ -110,11 +110,11 @@ impl SyncServiceBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[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 })
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ impl SyncServiceBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait UnableToDecryptDelegate: Sync + Send {
|
||||
fn on_utd(&self, info: UnableToDecryptInfo);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ impl TaskHandle {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl TaskHandle {
|
||||
// Cancel a task handle.
|
||||
pub fn cancel(&self) {
|
||||
|
||||
@@ -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>,
|
||||
@@ -195,7 +203,7 @@ impl InReplyToDetails {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl InReplyToDetails {
|
||||
pub fn event_id(&self) -> String {
|
||||
self.event_id.clone()
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{collections::HashMap, fmt::Write as _, fs, sync::Arc};
|
||||
use std::{collections::HashMap, fmt::Write as _, fs, panic, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use as_variant::as_variant;
|
||||
@@ -31,7 +31,8 @@ use matrix_sdk::{
|
||||
Error,
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{
|
||||
EventItemOrigin, LiveBackPaginationStatus, Profile, RepliedToEvent, TimelineDetails,
|
||||
self, EventItemOrigin, LiveBackPaginationStatus, Profile, RepliedToEvent, TimelineDetails,
|
||||
TimelineUniqueId as SdkTimelineUniqueId,
|
||||
};
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
@@ -52,7 +53,7 @@ use ruma::{
|
||||
},
|
||||
AnyMessageLikeEventContent,
|
||||
},
|
||||
EventId, OwnedTransactionId,
|
||||
EventId,
|
||||
};
|
||||
use tokio::{
|
||||
sync::Mutex,
|
||||
@@ -80,6 +81,9 @@ use crate::{
|
||||
mod content;
|
||||
|
||||
pub use content::MessageContent;
|
||||
use matrix_sdk::utils::formatted_body_from;
|
||||
|
||||
use crate::error::QueueWedgeError;
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
#[repr(transparent)]
|
||||
@@ -97,37 +101,24 @@ impl Timeline {
|
||||
unsafe { Arc::from_raw(Arc::into_raw(inner) as _) }
|
||||
}
|
||||
|
||||
fn build_thumbnail_info(
|
||||
&self,
|
||||
thumbnail_url: String,
|
||||
thumbnail_info: ThumbnailInfo,
|
||||
) -> Result<Thumbnail, RoomError> {
|
||||
let thumbnail_data =
|
||||
fs::read(thumbnail_url).map_err(|_| RoomError::InvalidThumbnailData)?;
|
||||
|
||||
let base_thumbnail_info = BaseThumbnailInfo::try_from(&thumbnail_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
|
||||
let mime_str =
|
||||
thumbnail_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
Ok(Thumbnail {
|
||||
data: thumbnail_data,
|
||||
content_type: mime_type,
|
||||
info: Some(base_thumbnail_info),
|
||||
})
|
||||
}
|
||||
|
||||
async fn send_attachment(
|
||||
&self,
|
||||
filename: String,
|
||||
mime_type: Mime,
|
||||
mime_type: Option<String>,
|
||||
attachment_config: AttachmentConfig,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
use_send_queue: bool,
|
||||
) -> Result<(), RoomError> {
|
||||
let request = self.inner.send_attachment(filename, mime_type, attachment_config);
|
||||
let mime_str = mime_type.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let mut request = self.inner.send_attachment(filename, mime_type, attachment_config);
|
||||
|
||||
if use_send_queue {
|
||||
request = request.use_send_queue();
|
||||
}
|
||||
|
||||
if let Some(progress_watcher) = progress_watcher {
|
||||
let mut subscriber = request.subscribe_to_send_progress();
|
||||
RUNTIME.spawn(async move {
|
||||
@@ -142,7 +133,42 @@ impl Timeline {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
fn build_thumbnail_info(
|
||||
thumbnail_url: Option<String>,
|
||||
thumbnail_info: Option<ThumbnailInfo>,
|
||||
) -> Result<AttachmentConfig, RoomError> {
|
||||
match (thumbnail_url, thumbnail_info) {
|
||||
(None, None) => Ok(AttachmentConfig::new()),
|
||||
|
||||
(Some(thumbnail_url), Some(thumbnail_info)) => {
|
||||
let thumbnail_data =
|
||||
fs::read(thumbnail_url).map_err(|_| RoomError::InvalidThumbnailData)?;
|
||||
|
||||
let base_thumbnail_info = BaseThumbnailInfo::try_from(&thumbnail_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
|
||||
let mime_str =
|
||||
thumbnail_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let thumbnail = Thumbnail {
|
||||
data: thumbnail_data,
|
||||
content_type: mime_type,
|
||||
info: Some(base_thumbnail_info),
|
||||
};
|
||||
|
||||
Ok(AttachmentConfig::with_thumbnail(thumbnail))
|
||||
}
|
||||
|
||||
_ => {
|
||||
warn!("Ignoring thumbnail because either the thumbnail URL or info isn't defined");
|
||||
Ok(AttachmentConfig::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Timeline {
|
||||
pub async fn add_listener(&self, listener: Box<dyn TimelineListener>) -> Arc<TaskHandle> {
|
||||
let (timeline_items, timeline_stream) = self.inner.subscribe_batched().await;
|
||||
@@ -245,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())
|
||||
@@ -253,6 +279,7 @@ impl Timeline {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn send_image(
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
@@ -261,33 +288,32 @@ impl Timeline {
|
||||
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 mime_str =
|
||||
image_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let base_image_info = BaseImageInfo::try_from(&image_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
|
||||
let attachment_info = AttachmentInfo::Image(base_image_info);
|
||||
|
||||
let attachment_config = match (thumbnail_url, image_info.thumbnail_info) {
|
||||
(Some(thumbnail_url), Some(thumbnail_image_info)) => {
|
||||
let thumbnail =
|
||||
self.build_thumbnail_info(thumbnail_url, thumbnail_image_info)?;
|
||||
AttachmentConfig::with_thumbnail(thumbnail).info(attachment_info)
|
||||
}
|
||||
_ => AttachmentConfig::new().info(attachment_info),
|
||||
}
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption.map(Into::into));
|
||||
let attachment_config = build_thumbnail_info(thumbnail_url, image_info.thumbnail_info)?
|
||||
.info(attachment_info)
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption);
|
||||
|
||||
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
|
||||
self.send_attachment(
|
||||
url,
|
||||
image_info.mimetype,
|
||||
attachment_config,
|
||||
progress_watcher,
|
||||
use_send_queue,
|
||||
)
|
||||
.await
|
||||
}))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn send_video(
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
@@ -296,30 +322,28 @@ impl Timeline {
|
||||
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 mime_str =
|
||||
video_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let base_video_info: BaseVideoInfo = BaseVideoInfo::try_from(&video_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
|
||||
let attachment_info = AttachmentInfo::Video(base_video_info);
|
||||
|
||||
let attachment_config = match (thumbnail_url, video_info.thumbnail_info) {
|
||||
(Some(thumbnail_url), Some(thumbnail_image_info)) => {
|
||||
let thumbnail =
|
||||
self.build_thumbnail_info(thumbnail_url, thumbnail_image_info)?;
|
||||
AttachmentConfig::with_thumbnail(thumbnail).info(attachment_info)
|
||||
}
|
||||
_ => AttachmentConfig::new().info(attachment_info),
|
||||
}
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption.map(Into::into));
|
||||
let attachment_config = build_thumbnail_info(thumbnail_url, video_info.thumbnail_info)?
|
||||
.info(attachment_info)
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption.map(Into::into));
|
||||
|
||||
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
|
||||
self.send_attachment(
|
||||
url,
|
||||
video_info.mimetype,
|
||||
attachment_config,
|
||||
progress_watcher,
|
||||
use_send_queue,
|
||||
)
|
||||
.await
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -330,26 +354,32 @@ impl Timeline {
|
||||
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 mime_str =
|
||||
audio_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
|
||||
let attachment_info = AttachmentInfo::Audio(base_audio_info);
|
||||
|
||||
let attachment_config = AttachmentConfig::new()
|
||||
.info(attachment_info)
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption.map(Into::into));
|
||||
|
||||
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
|
||||
self.send_attachment(
|
||||
url,
|
||||
audio_info.mimetype,
|
||||
attachment_config,
|
||||
progress_watcher,
|
||||
use_send_queue,
|
||||
)
|
||||
.await
|
||||
}))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn send_voice_message(
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
@@ -358,24 +388,29 @@ impl Timeline {
|
||||
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 mime_str =
|
||||
audio_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
|
||||
let attachment_info =
|
||||
AttachmentInfo::Voice { audio_info: base_audio_info, waveform: Some(waveform) };
|
||||
|
||||
let attachment_config = AttachmentConfig::new()
|
||||
.info(attachment_info)
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption.map(Into::into));
|
||||
|
||||
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
|
||||
self.send_attachment(
|
||||
url,
|
||||
audio_info.mimetype,
|
||||
attachment_config,
|
||||
progress_watcher,
|
||||
use_send_queue,
|
||||
)
|
||||
.await
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -383,21 +418,31 @@ 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 mime_str =
|
||||
file_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let base_file_info: BaseFileInfo =
|
||||
BaseFileInfo::try_from(&file_info).map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
|
||||
let attachment_info = AttachmentInfo::File(base_file_info);
|
||||
let attachment_config = AttachmentConfig::new().info(attachment_info);
|
||||
|
||||
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
|
||||
let attachment_config = AttachmentConfig::new()
|
||||
.info(attachment_info)
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption.map(Into::into));
|
||||
|
||||
self.send_attachment(
|
||||
url,
|
||||
file_info.mimetype,
|
||||
attachment_config,
|
||||
progress_watcher,
|
||||
use_send_queue,
|
||||
)
|
||||
.await
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -493,11 +538,30 @@ impl Timeline {
|
||||
&self,
|
||||
event_or_transaction_id: EventOrTransactionId,
|
||||
new_content: EditedContent,
|
||||
) -> Result<bool, ClientError> {
|
||||
self.inner
|
||||
.edit_by_id(&(event_or_transaction_id.try_into()?), new_content.try_into()?)
|
||||
) -> Result<(), ClientError> {
|
||||
match self
|
||||
.inner
|
||||
.edit(&event_or_transaction_id.clone().try_into()?, new_content.clone().try_into()?)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
{
|
||||
Ok(()) => Ok(()),
|
||||
Err(timeline::Error::EventNotInTimeline(_)) => {
|
||||
// If we couldn't edit, assume it was an (remote) event that wasn't in the
|
||||
// timeline, and try to edit it via the room itself.
|
||||
let event_id = match event_or_transaction_id {
|
||||
EventOrTransactionId::EventId { event_id } => EventId::parse(event_id)?,
|
||||
EventOrTransactionId::TransactionId { .. } => {
|
||||
warn!("trying to apply an edit to a local echo that doesn't exist in this timeline, aborting");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let room = self.inner.room();
|
||||
let edit_event = room.make_edit_event(&event_id, new_content.try_into()?).await?;
|
||||
room.send_queue().send(edit_event).await?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err)?,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_location(
|
||||
@@ -530,19 +594,21 @@ impl Timeline {
|
||||
|
||||
/// Toggle a reaction on an event.
|
||||
///
|
||||
/// The `unique_id` parameter is a string returned by
|
||||
/// the `TimelineItem::unique_id()` method. As such, this method works both
|
||||
/// on local echoes and remote items.
|
||||
///
|
||||
/// Adds or redacts a reaction based on the state of the reaction at the
|
||||
/// time it is called.
|
||||
///
|
||||
/// This method works both on local echoes and remote items.
|
||||
///
|
||||
/// When redacting a previous reaction, the redaction reason is not set.
|
||||
///
|
||||
/// Ensures that only one reaction is sent at a time to avoid race
|
||||
/// conditions and spamming the homeserver with requests.
|
||||
pub async fn toggle_reaction(&self, unique_id: String, key: String) -> Result<(), ClientError> {
|
||||
self.inner.toggle_reaction(&unique_id, &key).await?;
|
||||
pub async fn toggle_reaction(
|
||||
&self,
|
||||
item_id: EventOrTransactionId,
|
||||
key: String,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner.toggle_reaction(&item_id.try_into()?, &key).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -573,26 +639,6 @@ impl Timeline {
|
||||
Ok(item.into())
|
||||
}
|
||||
|
||||
/// Get the current timeline item for the given transaction ID, if any.
|
||||
///
|
||||
/// This will always return a local echo, if found.
|
||||
///
|
||||
/// It's preferable to store the timeline items in the model for your UI, if
|
||||
/// possible, instead of just storing IDs and coming back to the timeline
|
||||
/// object to look up items.
|
||||
pub async fn get_event_timeline_item_by_transaction_id(
|
||||
&self,
|
||||
transaction_id: String,
|
||||
) -> Result<EventTimelineItem, ClientError> {
|
||||
let transaction_id: OwnedTransactionId = transaction_id.into();
|
||||
let item = self
|
||||
.inner
|
||||
.local_item_by_transaction_id(&transaction_id)
|
||||
.await
|
||||
.context("Item with given transaction ID not found")?;
|
||||
Ok(item.into())
|
||||
}
|
||||
|
||||
/// Redacts an event from the timeline.
|
||||
///
|
||||
/// Only works for events that exist as timeline items.
|
||||
@@ -607,10 +653,7 @@ impl Timeline {
|
||||
event_or_transaction_id: EventOrTransactionId,
|
||||
reason: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner
|
||||
.redact_by_id(&(event_or_transaction_id.try_into()?), reason.as_deref())
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
Ok(self.inner.redact(&(event_or_transaction_id.try_into()?), reason.as_deref()).await?)
|
||||
}
|
||||
|
||||
/// Load the reply details for the given event id.
|
||||
@@ -683,12 +726,19 @@ 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>>,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
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.
|
||||
///
|
||||
@@ -705,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)]
|
||||
@@ -723,12 +795,12 @@ pub enum FocusEventError {
|
||||
Other { msg: String },
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait TimelineListener: Sync + Send {
|
||||
fn on_update(&self, diff: Vec<Arc<TimelineDiff>>);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait PaginationStatusListener: Sync + Send {
|
||||
fn on_update(&self, status: LiveBackPaginationStatus);
|
||||
}
|
||||
@@ -778,7 +850,7 @@ impl TimelineDiff {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl TimelineDiff {
|
||||
pub fn change(&self) -> TimelineChange {
|
||||
match self {
|
||||
@@ -866,6 +938,23 @@ pub enum TimelineChange {
|
||||
Reset,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct TimelineUniqueId {
|
||||
id: String,
|
||||
}
|
||||
|
||||
impl From<&SdkTimelineUniqueId> for TimelineUniqueId {
|
||||
fn from(value: &SdkTimelineUniqueId) -> Self {
|
||||
Self { id: value.0.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&TimelineUniqueId> for SdkTimelineUniqueId {
|
||||
fn from(value: &TimelineUniqueId) -> Self {
|
||||
Self(value.id.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct TimelineItem(pub(crate) matrix_sdk_ui::timeline::TimelineItem);
|
||||
@@ -878,7 +967,7 @@ impl TimelineItem {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl TimelineItem {
|
||||
pub fn as_event(self: Arc<Self>) -> Option<EventTimelineItem> {
|
||||
let event_item = self.0.as_event()?;
|
||||
@@ -893,8 +982,9 @@ impl TimelineItem {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unique_id(&self) -> String {
|
||||
self.0.unique_id().to_owned()
|
||||
/// An opaque unique identifier for this timeline item.
|
||||
pub fn unique_id(&self) -> TimelineUniqueId {
|
||||
self.0.unique_id().into()
|
||||
}
|
||||
|
||||
pub fn fmt_debug(&self) -> String {
|
||||
@@ -908,42 +998,12 @@ pub enum EventSendState {
|
||||
/// The local event has not been sent yet.
|
||||
NotSentYet,
|
||||
|
||||
/// One or more verified users in the room has an unsigned device.
|
||||
///
|
||||
/// Happens only when the room key recipient strategy (as set by
|
||||
/// [`ClientBuilder::room_key_recipient_strategy`]) has
|
||||
/// [`error_on_verified_user_problem`](CollectStrategy::DeviceBasedStrategy::error_on_verified_user_problem) set.
|
||||
VerifiedUserHasUnsignedDevice {
|
||||
/// The unsigned devices belonging to verified users. A map from user ID
|
||||
/// to a list of device IDs.
|
||||
devices: HashMap<String, Vec<String>>,
|
||||
},
|
||||
|
||||
/// One or more verified users in the room has changed identity since they
|
||||
/// were verified.
|
||||
///
|
||||
/// Happens only when the room key recipient strategy (as set by
|
||||
/// [`ClientBuilder::room_key_recipient_strategy`]) has
|
||||
/// [`error_on_verified_user_problem`](CollectStrategy::DeviceBasedStrategy::error_on_verified_user_problem)
|
||||
/// set, or when using [`CollectStrategy::IdentityBasedStrategy`].
|
||||
VerifiedUserChangedIdentity {
|
||||
/// The users that were previously verified, but are no longer
|
||||
users: Vec<String>,
|
||||
},
|
||||
|
||||
/// The user does not have cross-signing set up, but
|
||||
/// [`CollectStrategy::IdentityBasedStrategy`] was used.
|
||||
CrossSigningNotSetup,
|
||||
|
||||
/// The current device is not verified, but
|
||||
/// [`CollectStrategy::IdentityBasedStrategy`] was used.
|
||||
SendingFromUnverifiedDevice,
|
||||
|
||||
/// The local event has been sent to the server, but unsuccessfully: The
|
||||
/// sending has failed.
|
||||
SendingFailed {
|
||||
/// Stringified error message.
|
||||
error: String,
|
||||
/// The error reason, with information for the user.
|
||||
error: QueueWedgeError,
|
||||
|
||||
/// Whether the error is considered recoverable or not.
|
||||
///
|
||||
/// An error that's recoverable will disable the room's send queue,
|
||||
@@ -951,6 +1011,7 @@ pub enum EventSendState {
|
||||
/// decides to cancel sending it.
|
||||
is_recoverable: bool,
|
||||
},
|
||||
|
||||
/// The local event has been sent successfully to the server.
|
||||
Sent { event_id: String },
|
||||
}
|
||||
@@ -962,46 +1023,17 @@ impl From<&matrix_sdk_ui::timeline::EventSendState> for EventSendState {
|
||||
match value {
|
||||
NotSentYet => Self::NotSentYet,
|
||||
SendingFailed { error, is_recoverable } => {
|
||||
event_send_state_from_sending_failed(error, *is_recoverable)
|
||||
let as_queue_wedge_error: matrix_sdk::QueueWedgeError = (&**error).into();
|
||||
Self::SendingFailed {
|
||||
is_recoverable: *is_recoverable,
|
||||
error: as_queue_wedge_error.into(),
|
||||
}
|
||||
}
|
||||
Sent { event_id } => Self::Sent { event_id: event_id.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn event_send_state_from_sending_failed(error: &Error, is_recoverable: bool) -> EventSendState {
|
||||
use matrix_sdk::crypto::{OlmError, SessionRecipientCollectionError::*};
|
||||
|
||||
match error {
|
||||
// Special-case the SessionRecipientCollectionErrors, to pass the information they contain
|
||||
// back to the application.
|
||||
Error::OlmError(OlmError::SessionRecipientCollectionError(error)) => match error {
|
||||
VerifiedUserHasUnsignedDevice(devices) => {
|
||||
let devices = devices
|
||||
.iter()
|
||||
.map(|(user_id, devices)| {
|
||||
(
|
||||
user_id.to_string(),
|
||||
devices.iter().map(|device_id| device_id.to_string()).collect(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
EventSendState::VerifiedUserHasUnsignedDevice { devices }
|
||||
}
|
||||
|
||||
VerifiedUserChangedIdentity(bad_users) => EventSendState::VerifiedUserChangedIdentity {
|
||||
users: bad_users.iter().map(|user_id| user_id.to_string()).collect(),
|
||||
},
|
||||
|
||||
CrossSigningNotSetup => EventSendState::CrossSigningNotSetup,
|
||||
|
||||
SendingFromUnverifiedDevice => EventSendState::SendingFromUnverifiedDevice,
|
||||
},
|
||||
|
||||
_ => EventSendState::SendingFailed { error: error.to_string(), is_recoverable },
|
||||
}
|
||||
}
|
||||
|
||||
/// Recommended decorations for decrypted messages, representing the message's
|
||||
/// authenticity properties.
|
||||
#[derive(uniffi::Enum, Clone)]
|
||||
@@ -1032,7 +1064,7 @@ impl From<SdkShieldState> for ShieldState {
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct EventTimelineItem {
|
||||
is_local: bool,
|
||||
/// Indicates that an event is remote.
|
||||
is_remote: bool,
|
||||
event_or_transaction_id: EventOrTransactionId,
|
||||
sender: String,
|
||||
@@ -1042,17 +1074,16 @@ pub struct EventTimelineItem {
|
||||
content: TimelineItemContent,
|
||||
timestamp: u64,
|
||||
reactions: Vec<Reaction>,
|
||||
debug_info_provider: Arc<EventTimelineItemDebugInfoProvider>,
|
||||
local_send_state: Option<EventSendState>,
|
||||
read_receipts: HashMap<String, Receipt>,
|
||||
origin: Option<EventItemOrigin>,
|
||||
can_be_replied_to: bool,
|
||||
shields_provider: Arc<EventShieldsProvider>,
|
||||
lazy_provider: Arc<LazyTimelineItemProvider>,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
|
||||
fn from(value: matrix_sdk_ui::timeline::EventTimelineItem) -> Self {
|
||||
let reactions = value
|
||||
fn from(item: matrix_sdk_ui::timeline::EventTimelineItem) -> Self {
|
||||
let reactions = item
|
||||
.reactions()
|
||||
.iter()
|
||||
.map(|(k, v)| Reaction {
|
||||
@@ -1066,28 +1097,25 @@ impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
|
||||
.collect(),
|
||||
})
|
||||
.collect();
|
||||
let value = Arc::new(value);
|
||||
let debug_info_provider = Arc::new(EventTimelineItemDebugInfoProvider(value.clone()));
|
||||
let shields_provider = Arc::new(EventShieldsProvider(value.clone()));
|
||||
let item = Arc::new(item);
|
||||
let lazy_provider = Arc::new(LazyTimelineItemProvider(item.clone()));
|
||||
let read_receipts =
|
||||
value.read_receipts().iter().map(|(k, v)| (k.to_string(), v.clone().into())).collect();
|
||||
item.read_receipts().iter().map(|(k, v)| (k.to_string(), v.clone().into())).collect();
|
||||
Self {
|
||||
is_local: value.is_local_echo(),
|
||||
is_remote: !value.is_local_echo(),
|
||||
event_or_transaction_id: value.identifier().into(),
|
||||
sender: value.sender().to_string(),
|
||||
sender_profile: value.sender_profile().into(),
|
||||
is_own: value.is_own(),
|
||||
is_editable: value.is_editable(),
|
||||
content: value.content().clone().into(),
|
||||
timestamp: value.timestamp().0.into(),
|
||||
is_remote: !item.is_local_echo(),
|
||||
event_or_transaction_id: item.identifier().into(),
|
||||
sender: item.sender().to_string(),
|
||||
sender_profile: item.sender_profile().into(),
|
||||
is_own: item.is_own(),
|
||||
is_editable: item.is_editable(),
|
||||
content: item.content().clone().into(),
|
||||
timestamp: item.timestamp().0.into(),
|
||||
reactions,
|
||||
debug_info_provider,
|
||||
local_send_state: value.send_state().map(|s| s.into()),
|
||||
local_send_state: item.send_state().map(|s| s.into()),
|
||||
read_receipts,
|
||||
origin: value.origin(),
|
||||
can_be_replied_to: value.can_be_replied_to(),
|
||||
shields_provider,
|
||||
origin: item.origin(),
|
||||
can_be_replied_to: item.can_be_replied_to(),
|
||||
lazy_provider,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1103,22 +1131,6 @@ impl From<ruma::events::receipt::Receipt> for Receipt {
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper to retrieve the debug info lazily instead of immediately
|
||||
/// transforming it for each timeline event.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct EventTimelineItemDebugInfoProvider(Arc<matrix_sdk_ui::timeline::EventTimelineItem>);
|
||||
|
||||
#[uniffi::export]
|
||||
impl EventTimelineItemDebugInfoProvider {
|
||||
fn get(&self) -> EventTimelineItemDebugInfo {
|
||||
EventTimelineItemDebugInfo {
|
||||
model: format!("{:#?}", self.0),
|
||||
original_json: self.0.original_json().map(|raw| raw.json().get().to_owned()),
|
||||
latest_edit_json: self.0.latest_edit_json().map(|raw| raw.json().get().to_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct EventTimelineItemDebugInfo {
|
||||
model: String,
|
||||
@@ -1202,13 +1214,30 @@ impl SendAttachmentJoinHandle {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SendAttachmentJoinHandle {
|
||||
/// Wait until the attachment has been sent.
|
||||
///
|
||||
/// If the sending had been cancelled, will return immediately.
|
||||
pub async fn join(&self) -> Result<(), RoomError> {
|
||||
let join_hdl = self.join_hdl.clone();
|
||||
RUNTIME.spawn(async move { (&mut *join_hdl.lock().await).await.unwrap() }).await.unwrap()
|
||||
let handle = self.join_hdl.clone();
|
||||
let mut locked_handle = handle.lock().await;
|
||||
let join_result = (&mut *locked_handle).await;
|
||||
match join_result {
|
||||
Ok(res) => res,
|
||||
Err(err) => {
|
||||
if err.is_cancelled() {
|
||||
return Ok(());
|
||||
}
|
||||
error!("task panicked! resuming panic from here.");
|
||||
panic::resume_unwind(err.into_panic());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel the current sending task.
|
||||
///
|
||||
/// A subsequent call to [`Self::join`] will return immediately.
|
||||
pub fn cancel(&self) {
|
||||
self.abort_hdl.abort();
|
||||
}
|
||||
@@ -1270,13 +1299,29 @@ impl TryFrom<EditedContent> for SdkEditedContent {
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper to retrieve the shields info lazily.
|
||||
/// Wrapper to retrieve some timeline item info lazily.
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct EventShieldsProvider(Arc<matrix_sdk_ui::timeline::EventTimelineItem>);
|
||||
pub struct LazyTimelineItemProvider(Arc<matrix_sdk_ui::timeline::EventTimelineItem>);
|
||||
|
||||
#[uniffi::export]
|
||||
impl EventShieldsProvider {
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl LazyTimelineItemProvider {
|
||||
/// Returns the shields for this event timeline item.
|
||||
fn get_shields(&self, strict: bool) -> Option<ShieldState> {
|
||||
self.0.get_shield(strict).map(Into::into)
|
||||
}
|
||||
|
||||
/// Returns some debug information for this event timeline item.
|
||||
fn debug_info(&self) -> EventTimelineItemDebugInfo {
|
||||
EventTimelineItemDebugInfo {
|
||||
model: format!("{:#?}", self.0),
|
||||
original_json: self.0.original_json().map(|raw| raw.json().get().to_owned()),
|
||||
latest_edit_json: self.0.latest_edit_json().map(|raw| raw.json().get().to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ pub struct TimelineEventTypeFilter {
|
||||
inner: InnerTimelineEventTypeFilter,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl TimelineEventTypeFilter {
|
||||
#[uniffi::constructor]
|
||||
pub fn include(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
|
||||
|
||||
@@ -19,7 +19,7 @@ use tracing_core::{identify_callsite, metadata::Kind as MetadataKind};
|
||||
/// level + target) it is called with. Please make sure that the number of
|
||||
/// different combinations of those parameters this can be called with is
|
||||
/// constant in the final executable.
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
fn log_event(file: String, line: Option<u32>, level: LogLevel, target: String, message: String) {
|
||||
static CALLSITES: Mutex<BTreeMap<MetadataId, &'static DefaultCallsite>> =
|
||||
Mutex::new(BTreeMap::new());
|
||||
@@ -96,7 +96,7 @@ fn span_or_event_enabled(callsite: &'static DefaultCallsite) -> bool {
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Span(tracing::Span);
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Span {
|
||||
/// Create a span originating at the given callsite (file, line and column).
|
||||
///
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{mem::ManuallyDrop, ops::Deref};
|
||||
|
||||
use async_compat::TOKIO1 as RUNTIME;
|
||||
use ruma::UInt;
|
||||
use tracing::warn;
|
||||
|
||||
@@ -21,3 +24,45 @@ pub(crate) fn u64_to_uint(u: u64) -> UInt {
|
||||
UInt::MAX
|
||||
})
|
||||
}
|
||||
|
||||
/// Tiny wrappers for data types that must be dropped in the context of an async
|
||||
/// runtime.
|
||||
///
|
||||
/// This is useful whenever such a data type may transitively call some
|
||||
/// runtime's `block_on` function in their `Drop` impl (since we lack async drop
|
||||
/// at the moment), like done in some `deadpool` drop impls.
|
||||
pub(crate) struct AsyncRuntimeDropped<T>(ManuallyDrop<T>);
|
||||
|
||||
impl<T> AsyncRuntimeDropped<T> {
|
||||
/// Create a new wrapper for this type that will be dropped under an async
|
||||
/// runtime.
|
||||
pub fn new(val: T) -> Self {
|
||||
Self(ManuallyDrop::new(val))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for AsyncRuntimeDropped<T> {
|
||||
fn drop(&mut self) {
|
||||
let _guard = RUNTIME.enter();
|
||||
// SAFETY: self.inner is never used again, which is the only requirement
|
||||
// for ManuallyDrop::drop to be used safely.
|
||||
unsafe {
|
||||
ManuallyDrop::drop(&mut self.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// What is an `AsyncRuntimeDropped<T>`, if not a `T` in disguise?
|
||||
impl<T> Deref for AsyncRuntimeDropped<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Clone for AsyncRuntimeDropped<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use matrix_sdk::{
|
||||
async_trait,
|
||||
widget::{MessageLikeEventFilter, StateEventFilter},
|
||||
};
|
||||
use ruma::events::MessageLikeEventType;
|
||||
use tracing::error;
|
||||
|
||||
use crate::{room::Room, RUNTIME};
|
||||
@@ -15,7 +16,7 @@ pub struct WidgetDriverAndHandle {
|
||||
pub handle: Arc<WidgetDriverHandle>,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn make_widget_driver(settings: WidgetSettings) -> Result<WidgetDriverAndHandle, ParseError> {
|
||||
let (driver, handle) = matrix_sdk::widget::WidgetDriver::new(settings.try_into()?);
|
||||
Ok(WidgetDriverAndHandle {
|
||||
@@ -29,7 +30,7 @@ pub fn make_widget_driver(settings: WidgetSettings) -> Result<WidgetDriverAndHan
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct WidgetDriver(Mutex<Option<matrix_sdk::widget::WidgetDriver>>);
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl WidgetDriver {
|
||||
pub async fn run(
|
||||
&self,
|
||||
@@ -96,7 +97,7 @@ impl From<matrix_sdk::widget::WidgetSettings> for WidgetSettings {
|
||||
/// * `room` - A matrix room which is used to query the logged in username
|
||||
/// * `props` - Properties from the client that can be used by a widget to adapt
|
||||
/// to the client. e.g. language, font-scale...
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub async fn generate_webview_url(
|
||||
widget_settings: WidgetSettings,
|
||||
room: Arc<Room>,
|
||||
@@ -241,7 +242,7 @@ impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElemen
|
||||
///
|
||||
/// * `props` - A struct containing the configuration parameters for a element
|
||||
/// call widget.
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn new_virtual_element_call_widget(
|
||||
props: VirtualElementCallWidgetOptions,
|
||||
) -> Result<WidgetSettings, ParseError> {
|
||||
@@ -261,13 +262,38 @@ pub fn new_virtual_element_call_widget(
|
||||
/// Editing and extending the capabilities from this function is also possible,
|
||||
/// but should only be done as temporal workarounds until this function is
|
||||
/// adjusted
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn get_element_call_required_permissions(
|
||||
own_user_id: String,
|
||||
own_device_id: String,
|
||||
) -> WidgetCapabilities {
|
||||
use ruma::events::StateEventType;
|
||||
|
||||
let read_send = vec![
|
||||
// To read and send rageshake requests from other room members
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "org.matrix.rageshake_request".to_owned(),
|
||||
},
|
||||
// To read and send encryption keys
|
||||
// TODO change this to the appropriate to-device version once ready
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "io.element.call.encryption_keys".to_owned(),
|
||||
},
|
||||
// To read and send custom EC reactions. They are different to normal `m.reaction`
|
||||
// because they can be send multiple times to the same event.
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "io.element.call.reaction".to_owned(),
|
||||
},
|
||||
// This allows send raise hand reactions.
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: MessageLikeEventType::Reaction.to_string(),
|
||||
},
|
||||
// This allows to detect if someone does not raise their hand anymore.
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: MessageLikeEventType::RoomRedaction.to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
WidgetCapabilities {
|
||||
read: vec![
|
||||
// To compute the current state of the matrixRTC session.
|
||||
@@ -278,19 +304,13 @@ pub fn get_element_call_required_permissions(
|
||||
WidgetEventFilter::StateWithType {
|
||||
event_type: StateEventType::RoomEncryption.to_string(),
|
||||
},
|
||||
// To read rageshake requests from other room members
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "org.matrix.rageshake_request".to_owned(),
|
||||
},
|
||||
// To read encryption keys
|
||||
// TODO change this to the appropriate to-device version once ready
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "io.element.call.encryption_keys".to_owned(),
|
||||
},
|
||||
// This allows the widget to check the room version, so it can know about
|
||||
// version-specific auth rules (namely MSC3779).
|
||||
WidgetEventFilter::StateWithType { event_type: StateEventType::RoomCreate.to_string() },
|
||||
],
|
||||
]
|
||||
.into_iter()
|
||||
.chain(read_send.clone())
|
||||
.collect(),
|
||||
send: vec![
|
||||
// To send the call participation state event (main MatrixRTC event).
|
||||
// This is required for legacy state events (using only one event for all devices with
|
||||
@@ -313,15 +333,10 @@ pub fn get_element_call_required_permissions(
|
||||
event_type: StateEventType::CallMember.to_string(),
|
||||
state_key: format!("_{own_user_id}_{own_device_id}"),
|
||||
},
|
||||
// To request other room members to send rageshakes
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "org.matrix.rageshake_request".to_owned(),
|
||||
},
|
||||
// To send this user's encryption keys
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "io.element.call.encryption_keys".to_owned(),
|
||||
},
|
||||
],
|
||||
]
|
||||
.into_iter()
|
||||
.chain(read_send)
|
||||
.collect(),
|
||||
requires_client: true,
|
||||
update_delayed_event: true,
|
||||
send_delayed_event: true,
|
||||
@@ -354,7 +369,7 @@ impl From<ClientProperties> for matrix_sdk::widget::ClientProperties {
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct WidgetDriverHandle(matrix_sdk::widget::WidgetDriverHandle);
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl WidgetDriverHandle {
|
||||
/// Receive a message from the widget driver.
|
||||
///
|
||||
@@ -417,7 +432,7 @@ impl From<matrix_sdk::widget::Capabilities> for WidgetCapabilities {
|
||||
}
|
||||
|
||||
/// Different kinds of filters that could be applied to the timeline events.
|
||||
#[derive(uniffi::Enum)]
|
||||
#[derive(uniffi::Enum, Clone)]
|
||||
pub enum WidgetEventFilter {
|
||||
/// Matches message-like events with the given `type`.
|
||||
MessageLikeWithType { event_type: String },
|
||||
@@ -469,7 +484,7 @@ impl From<matrix_sdk::widget::EventFilter> for WidgetEventFilter {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait WidgetCapabilitiesProvider: Send + Sync {
|
||||
fn acquire_capabilities(&self, capabilities: WidgetCapabilities) -> WidgetCapabilities;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# This git-cliff configuration file is used to generate weekly reports for This
|
||||
# Week in Matrix amongst others.
|
||||
|
||||
[changelog]
|
||||
header = """
|
||||
# This Week in the Matrix Rust SDK ({{ now() | date(format="%Y-%m-%d") }})
|
||||
"""
|
||||
body = """
|
||||
{% for commit in commits %}
|
||||
{% set_global commit_message = commit.message -%}
|
||||
{% for footer in commit.footers -%}
|
||||
{% if footer.token | lower == "changelog" -%}
|
||||
{% set_global commit_message = footer.value -%}
|
||||
{% elif footer.token | lower == "breaking-change" -%}
|
||||
{% set_global commit_message = footer.value -%}
|
||||
{% endif -%}
|
||||
{% endfor -%}
|
||||
- {{ commit_message | upper_first }}
|
||||
{% endfor %}
|
||||
"""
|
||||
trim = true
|
||||
footer = ""
|
||||
|
||||
[git]
|
||||
conventional_commits = true
|
||||
filter_unconventional = true
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/pull/${2}))"},
|
||||
]
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "Features" },
|
||||
{ message = "^fix", group = "Bug Fixes" },
|
||||
{ message = "^doc", group = "Documentation" },
|
||||
{ message = "^perf", group = "Performance" },
|
||||
{ message = "^refactor", group = "Refactor", skip = true },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^chore", skip = true },
|
||||
{ message = "^style", group = "Styling", skip = true },
|
||||
{ message = "^test", skip = true },
|
||||
{ message = "^ci", skip = true },
|
||||
]
|
||||
filter_commits = true
|
||||
tag_pattern = "[0-9]*"
|
||||
skip_tags = ""
|
||||
ignore_tags = ""
|
||||
date_order = false
|
||||
sort_commits = "newest"
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
# This git-cliff configuration file is used to generate release reports.
|
||||
|
||||
[changelog]
|
||||
# changelog header
|
||||
header = """
|
||||
# Changelog\n
|
||||
All notable changes to this project will be documented in this file.\n
|
||||
"""
|
||||
# template for the changelog body
|
||||
# https://keats.github.io/tera/docs/
|
||||
body = """
|
||||
{% if version %}\
|
||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
{% endif %}\
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | upper_first }}
|
||||
{% for commit in commits %}
|
||||
{% set_global commit_message = commit.message -%}
|
||||
{% set_global breaking = commit.breaking -%}
|
||||
{% for footer in commit.footers -%}
|
||||
{% if footer.token | lower == "changelog" -%}
|
||||
{% set_global commit_message = footer.value -%}
|
||||
{% elif footer.token | lower == "breaking-change" -%}
|
||||
{% set_global commit_message = footer.value -%}
|
||||
{% elif footer.token | lower == "security-impact" -%}
|
||||
{% set_global security_impact = footer.value -%}
|
||||
{% elif footer.token | lower == "cve" -%}
|
||||
{% set_global cve = footer.value -%}
|
||||
{% elif footer.token | lower == "github-advisory" -%}
|
||||
{% set_global github_advisory = footer.value -%}
|
||||
{% endif -%}
|
||||
{% endfor -%}
|
||||
- {% if breaking %}[**breaking**] {% endif %}{{ commit_message | upper_first }}
|
||||
{% if security_impact -%}
|
||||
(\
|
||||
*{{ security_impact | upper_first }}*\
|
||||
{% if cve -%}, [{{ cve | upper }}](https://www.cve.org/CVERecord?id={{ cve }}){% endif -%}\
|
||||
{% if github_advisory -%}, [{{ github_advisory | upper }}](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/{{ github_advisory }}){% endif -%}
|
||||
)
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
{% endfor %}\n
|
||||
"""
|
||||
# remove the leading and trailing whitespace from the template
|
||||
trim = true
|
||||
# changelog footer
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
"""
|
||||
|
||||
[git]
|
||||
# parse the commits based on https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# filter out the commits that are not conventional
|
||||
filter_unconventional = true
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/pull/${2}))"},
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ footer = "Security-Impact:", group = "Security" },
|
||||
{ footer = "CVE:", group = "Security" },
|
||||
{ footer = "GitHub-Advisory:", group = "Security" },
|
||||
{ message = "^feat", group = "Features" },
|
||||
{ message = "^fix", group = "Bug Fixes" },
|
||||
{ message = "^doc", group = "Documentation" },
|
||||
{ message = "^perf", group = "Performance" },
|
||||
{ message = "^refactor", group = "Refactor" },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^chore", skip = true },
|
||||
{ message = "^style", group = "Styling", skip = true },
|
||||
{ message = "^test", skip = true },
|
||||
{ message = "^ci", skip = true },
|
||||
]
|
||||
# forbid parsers from skipping breaking changes
|
||||
protect_breaking_commits = true
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
filter_commits = true
|
||||
# glob pattern for matching git tags
|
||||
tag_pattern = "[0-9]*"
|
||||
# regex for skipping tags
|
||||
skip_tags = ""
|
||||
# regex for ignoring tags
|
||||
ignore_tags = ""
|
||||
# sort the tags chronologically
|
||||
date_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
sort_commits = "oldest"
|
||||
@@ -1,21 +1,81 @@
|
||||
# unreleased
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [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,7 +61,9 @@ 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 }
|
||||
@@ -76,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"] }
|
||||
|
||||
@@ -14,22 +14,21 @@
|
||||
// limitations under the License.
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
fmt, iter,
|
||||
sync::Arc,
|
||||
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::{
|
||||
store::DynCryptoStore, CollectStrategy, DecryptionSettings, EncryptionSettings,
|
||||
EncryptionSyncChanges, OlmError, OlmMachine, ToDeviceRequest, TrustRequirement,
|
||||
EncryptionSyncChanges, OlmError, OlmMachine, RoomEventDecryptionResult, ToDeviceRequest,
|
||||
TrustRequirement,
|
||||
};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::events::{
|
||||
@@ -42,6 +41,7 @@ use ruma::{
|
||||
api::client as api,
|
||||
events::{
|
||||
ignored_user_list::IgnoredUserListEvent,
|
||||
marked_unread::MarkedUnreadEventContent,
|
||||
push_rules::{PushRulesEvent, PushRulesEventContent},
|
||||
room::{
|
||||
member::{MembershipState, RoomMemberEventContent, SyncRoomMemberEvent},
|
||||
@@ -68,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::DynEventCacheStore,
|
||||
event_cache::store::EventCacheStoreLock,
|
||||
response_processors::AccountDataProcessor,
|
||||
rooms::{
|
||||
normal::{RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons},
|
||||
@@ -92,25 +92,28 @@ use crate::{
|
||||
pub struct BaseClient {
|
||||
/// Database
|
||||
pub(crate) store: Store,
|
||||
|
||||
/// The store used by the event cache.
|
||||
event_cache_store: Arc<DynEventCacheStore>,
|
||||
event_cache_store: EventCacheStoreLock,
|
||||
|
||||
/// The store used for encryption.
|
||||
///
|
||||
/// This field is only meant to be used for `OlmMachine` initialization.
|
||||
/// All operations on it happen inside the `OlmMachine`.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
crypto_store: Arc<DynCryptoStore>,
|
||||
|
||||
/// The olm-machine that is created once the
|
||||
/// [`SessionMeta`][crate::session::SessionMeta] is set via
|
||||
/// [`BaseClient::set_session_meta`]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
olm_machine: Arc<RwLock<Option<OlmMachine>>>,
|
||||
|
||||
/// Observable of when a user is ignored/unignored.
|
||||
pub(crate) ignore_user_list_changes: SharedObservable<Vec<String>>,
|
||||
|
||||
/// A sender that is used to communicate changes to room information. Each
|
||||
/// event contains the room and a boolean whether this event should
|
||||
/// trigger a room list update.
|
||||
/// tick contains the room ID and the reasons that have generated this tick.
|
||||
pub(crate) room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
|
||||
|
||||
/// The strategy to use for picking recipient devices, when sending an
|
||||
@@ -134,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
|
||||
@@ -168,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 {
|
||||
@@ -202,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))
|
||||
}
|
||||
|
||||
@@ -228,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()
|
||||
}
|
||||
@@ -244,14 +249,13 @@ impl BaseClient {
|
||||
}
|
||||
|
||||
/// Get a reference to the store.
|
||||
#[allow(unknown_lints, clippy::explicit_auto_deref)]
|
||||
pub fn store(&self) -> &DynStateStore {
|
||||
&*self.store
|
||||
self.store.deref()
|
||||
}
|
||||
|
||||
/// Get a reference to the event cache store.
|
||||
pub fn event_cache_store(&self) -> &DynEventCacheStore {
|
||||
&*self.event_cache_store
|
||||
pub fn event_cache_store(&self) -> &EventCacheStoreLock {
|
||||
&self.event_cache_store
|
||||
}
|
||||
|
||||
/// Is the client logged in.
|
||||
@@ -343,6 +347,13 @@ impl BaseClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to decrypt the given raw event into a `SyncTimelineEvent`.
|
||||
///
|
||||
/// In the case of a decryption error, returns a `SyncTimelineEvent`
|
||||
/// representing the decryption error; in the case of problems with our
|
||||
/// application, returns `Err`.
|
||||
///
|
||||
/// Returns `Ok(None)` if encryption is not configured.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
async fn decrypt_sync_room_event(
|
||||
&self,
|
||||
@@ -355,24 +366,37 @@ impl BaseClient {
|
||||
let decryption_settings = DecryptionSettings {
|
||||
sender_device_trust_requirement: self.decryption_trust_requirement,
|
||||
};
|
||||
let event: SyncTimelineEvent =
|
||||
olm.decrypt_room_event(event.cast_ref(), room_id, &decryption_settings).await?.into();
|
||||
|
||||
if let Ok(AnySyncTimelineEvent::MessageLike(e)) = event.event.deserialize() {
|
||||
match &e {
|
||||
AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(
|
||||
original_event,
|
||||
)) => {
|
||||
if let MessageType::VerificationRequest(_) = &original_event.content.msgtype {
|
||||
self.handle_verification_event(&e, room_id).await?;
|
||||
let event = match olm
|
||||
.try_decrypt_room_event(event.cast_ref(), room_id, &decryption_settings)
|
||||
.await?
|
||||
{
|
||||
RoomEventDecryptionResult::Decrypted(decrypted) => {
|
||||
let event: SyncTimelineEvent = decrypted.into();
|
||||
|
||||
if let Ok(AnySyncTimelineEvent::MessageLike(e)) = event.raw().deserialize() {
|
||||
match &e {
|
||||
AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(
|
||||
original_event,
|
||||
)) => {
|
||||
if let MessageType::VerificationRequest(_) =
|
||||
&original_event.content.msgtype
|
||||
{
|
||||
self.handle_verification_event(&e, room_id).await?;
|
||||
}
|
||||
}
|
||||
_ if e.event_type().to_string().starts_with("m.key.verification") => {
|
||||
self.handle_verification_event(&e, room_id).await?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
_ if e.event_type().to_string().starts_with("m.key.verification") => {
|
||||
self.handle_verification_event(&e, room_id).await?;
|
||||
}
|
||||
_ => (),
|
||||
event
|
||||
}
|
||||
}
|
||||
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
|
||||
SyncTimelineEvent::new_utd_event(event.clone(), utd_info)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Some(event))
|
||||
}
|
||||
@@ -384,6 +408,7 @@ impl BaseClient {
|
||||
room: &Room,
|
||||
limited: bool,
|
||||
events: Vec<Raw<AnySyncTimelineEvent>>,
|
||||
ignore_state_events: bool,
|
||||
prev_batch: Option<String>,
|
||||
push_rules: &Ruleset,
|
||||
user_ids: &mut BTreeSet<OwnedUserId>,
|
||||
@@ -395,14 +420,16 @@ impl BaseClient {
|
||||
let mut timeline = Timeline::new(limited, prev_batch);
|
||||
let mut push_context = self.get_push_room_context(room, room_info, changes).await?;
|
||||
|
||||
for event in events {
|
||||
let mut event: SyncTimelineEvent = event.into();
|
||||
for raw_event in events {
|
||||
// Start by assuming we have a plaintext event. We'll replace it with a
|
||||
// decrypted or UTD event below if necessary.
|
||||
let mut event = SyncTimelineEvent::new(raw_event);
|
||||
|
||||
match event.event.deserialize() {
|
||||
match event.raw().deserialize() {
|
||||
Ok(e) => {
|
||||
#[allow(clippy::single_match)]
|
||||
match &e {
|
||||
AnySyncTimelineEvent::State(s) => {
|
||||
AnySyncTimelineEvent::State(s) if !ignore_state_events => {
|
||||
match s {
|
||||
AnySyncStateEvent::RoomMember(member) => {
|
||||
Box::pin(ambiguity_cache.handle_event(
|
||||
@@ -432,10 +459,12 @@ impl BaseClient {
|
||||
}
|
||||
}
|
||||
|
||||
let raw_event: Raw<AnySyncStateEvent> = event.event.clone().cast();
|
||||
let raw_event: Raw<AnySyncStateEvent> = event.raw().clone().cast();
|
||||
changes.add_state_event(room.room_id(), s.clone(), raw_event);
|
||||
}
|
||||
|
||||
AnySyncTimelineEvent::State(_) => { /* do nothing */ }
|
||||
|
||||
AnySyncTimelineEvent::MessageLike(
|
||||
AnySyncMessageLikeEvent::RoomRedaction(r),
|
||||
) => {
|
||||
@@ -443,8 +472,8 @@ impl BaseClient {
|
||||
room_info.room_version().unwrap_or(&RoomVersionId::V1);
|
||||
|
||||
if let Some(redacts) = r.redacts(room_version) {
|
||||
room_info.handle_redaction(r, event.event.cast_ref());
|
||||
let raw_event = event.event.clone().cast();
|
||||
room_info.handle_redaction(r, event.raw().cast_ref());
|
||||
let raw_event = event.raw().clone().cast();
|
||||
|
||||
changes.add_redaction(room.room_id(), redacts, raw_event);
|
||||
}
|
||||
@@ -455,10 +484,10 @@ impl BaseClient {
|
||||
AnySyncMessageLikeEvent::RoomEncrypted(
|
||||
SyncMessageLikeEvent::Original(_),
|
||||
) => {
|
||||
if let Ok(Some(e)) = Box::pin(
|
||||
self.decrypt_sync_room_event(&event.event, room.room_id()),
|
||||
if let Some(e) = Box::pin(
|
||||
self.decrypt_sync_room_event(event.raw(), room.room_id()),
|
||||
)
|
||||
.await
|
||||
.await?
|
||||
{
|
||||
event = e;
|
||||
}
|
||||
@@ -494,14 +523,14 @@ impl BaseClient {
|
||||
}
|
||||
|
||||
if let Some(context) = &push_context {
|
||||
let actions = push_rules.get_actions(&event.event, context);
|
||||
let actions = push_rules.get_actions(event.raw(), context);
|
||||
|
||||
if actions.iter().any(Action::should_notify) {
|
||||
notifications.entry(room.room_id().to_owned()).or_default().push(
|
||||
Notification {
|
||||
actions: actions.to_owned(),
|
||||
event: RawAnySyncOrStrippedTimelineEvent::Sync(
|
||||
event.event.clone(),
|
||||
event.raw().clone(),
|
||||
),
|
||||
},
|
||||
);
|
||||
@@ -520,11 +549,22 @@ impl BaseClient {
|
||||
Ok(timeline)
|
||||
}
|
||||
|
||||
/// Handles the stripped state events in `invite_state`, modifying the
|
||||
/// room's info and posting notifications as needed.
|
||||
///
|
||||
/// * `room` - The [`Room`] to modify.
|
||||
/// * `events` - The contents of `invite_state` in the form of list of pairs
|
||||
/// of raw stripped state events with their deserialized counterpart.
|
||||
/// * `push_rules` - The push rules for this room.
|
||||
/// * `room_info` - The current room's info.
|
||||
/// * `changes` - The accumulated list of changes to apply once the
|
||||
/// processing is finished.
|
||||
/// * `notifications` - Notifications to post for the current room.
|
||||
#[instrument(skip_all, fields(room_id = ?room_info.room_id))]
|
||||
pub(crate) async fn handle_invited_state(
|
||||
&self,
|
||||
room: &Room,
|
||||
events: &[Raw<AnyStrippedStateEvent>],
|
||||
events: &[(Raw<AnyStrippedStateEvent>, AnyStrippedStateEvent)],
|
||||
push_rules: &Ruleset,
|
||||
room_info: &mut RoomInfo,
|
||||
changes: &mut StateChanges,
|
||||
@@ -532,22 +572,12 @@ impl BaseClient {
|
||||
) -> Result<()> {
|
||||
let mut state_events = BTreeMap::new();
|
||||
|
||||
for raw_event in events {
|
||||
match raw_event.deserialize() {
|
||||
Ok(e) => {
|
||||
room_info.handle_stripped_state_event(&e);
|
||||
state_events
|
||||
.entry(e.event_type())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(e.state_key().to_owned(), raw_event.clone());
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
room_id = ?room_info.room_id,
|
||||
"Couldn't deserialize stripped state event: {err:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
for (raw_event, event) in events {
|
||||
room_info.handle_stripped_state_event(event);
|
||||
state_events
|
||||
.entry(event.event_type())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(event.state_key().to_owned(), raw_event.clone());
|
||||
}
|
||||
|
||||
changes.stripped_state.insert(room_info.room_id().to_owned(), state_events.clone());
|
||||
@@ -656,6 +686,25 @@ impl BaseClient {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to update the unread marker for stable and unstable prefixes.
|
||||
fn on_unread_marker(
|
||||
room_id: &RoomId,
|
||||
content: &MarkedUnreadEventContent,
|
||||
room_info: &mut RoomInfo,
|
||||
room_info_notable_updates: &mut BTreeMap<OwnedRoomId, RoomInfoNotableUpdateReasons>,
|
||||
) {
|
||||
if room_info.base_info.is_marked_unread != content.unread {
|
||||
// Notify the room list about a manual read marker change if the
|
||||
// value's changed.
|
||||
room_info_notable_updates
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.insert(RoomInfoNotableUpdateReasons::UNREAD_MARKER);
|
||||
}
|
||||
|
||||
room_info.base_info.is_marked_unread = content.unread;
|
||||
}
|
||||
|
||||
// Handle new events.
|
||||
for raw_event in events {
|
||||
match raw_event.deserialize() {
|
||||
@@ -665,19 +714,24 @@ impl BaseClient {
|
||||
match event {
|
||||
AnyRoomAccountDataEvent::MarkedUnread(event) => {
|
||||
on_room_info(room_id, changes, self, |room_info| {
|
||||
if room_info.base_info.is_marked_unread != event.content.unread {
|
||||
// Notify the room list about a manual read marker change if the
|
||||
// value's changed.
|
||||
room_info_notable_updates
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.insert(RoomInfoNotableUpdateReasons::UNREAD_MARKER);
|
||||
}
|
||||
|
||||
room_info.base_info.is_marked_unread = event.content.unread;
|
||||
on_unread_marker(
|
||||
room_id,
|
||||
&event.content,
|
||||
room_info,
|
||||
room_info_notable_updates,
|
||||
);
|
||||
});
|
||||
}
|
||||
AnyRoomAccountDataEvent::UnstableMarkedUnread(event) => {
|
||||
on_room_info(room_id, changes, self, |room_info| {
|
||||
on_unread_marker(
|
||||
room_id,
|
||||
&event.content.0,
|
||||
room_info,
|
||||
room_info_notable_updates,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
AnyRoomAccountDataEvent::Tag(event) => {
|
||||
on_room_info(room_id, changes, self, |room_info| {
|
||||
room_info.base_info.handle_notable_tags(&event.content.tags);
|
||||
@@ -762,6 +816,8 @@ impl BaseClient {
|
||||
room: &Room,
|
||||
) -> Option<(Box<LatestEvent>, usize)> {
|
||||
let enc_events = room.latest_encrypted_events();
|
||||
let power_levels = room.power_levels().await.ok();
|
||||
let power_levels_info = Some(room.own_user_id()).zip(power_levels.as_ref());
|
||||
|
||||
// Walk backwards through the encrypted events, looking for one we can decrypt
|
||||
for (i, event) in enc_events.iter().enumerate().rev() {
|
||||
@@ -773,15 +829,15 @@ impl BaseClient {
|
||||
|
||||
if let Ok(Some(decrypted)) = decrypt_sync_room_event.await {
|
||||
// We found an event we can decrypt
|
||||
if let Ok(any_sync_event) = decrypted.event.deserialize() {
|
||||
if let Ok(any_sync_event) = decrypted.raw().deserialize() {
|
||||
// We can deserialize it to find its type
|
||||
match is_suitable_for_latest_event(&any_sync_event) {
|
||||
match is_suitable_for_latest_event(&any_sync_event, power_levels_info) {
|
||||
PossibleLatestEvent::YesRoomMessage(_)
|
||||
| PossibleLatestEvent::YesPoll(_)
|
||||
| PossibleLatestEvent::YesCallInvite(_)
|
||||
| PossibleLatestEvent::YesCallNotify(_)
|
||||
| PossibleLatestEvent::YesSticker(_) => {
|
||||
// The event is the right type for us to use as latest_event
|
||||
| PossibleLatestEvent::YesSticker(_)
|
||||
| PossibleLatestEvent::YesKnockedStateEvent(_) => {
|
||||
return Some((Box::new(LatestEvent::new(decrypted)), i));
|
||||
}
|
||||
_ => (),
|
||||
@@ -792,6 +848,32 @@ impl BaseClient {
|
||||
None
|
||||
}
|
||||
|
||||
/// User has knocked on a room.
|
||||
///
|
||||
/// Update the internal and cached state accordingly. Return the final Room.
|
||||
pub async fn room_knocked(&self, room_id: &RoomId) -> Result<Room> {
|
||||
let room = self.store.get_or_create_room(
|
||||
room_id,
|
||||
RoomState::Knocked,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
);
|
||||
|
||||
if room.state() != RoomState::Knocked {
|
||||
let _sync_lock = self.sync_lock().lock().await;
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_as_knocked();
|
||||
room_info.mark_state_partially_synced();
|
||||
room_info.mark_members_missing(); // the own member event changed
|
||||
let mut changes = StateChanges::default();
|
||||
changes.add_room(room_info.clone());
|
||||
self.store.save_changes(&changes).await?; // Update the store
|
||||
room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
|
||||
}
|
||||
|
||||
Ok(room)
|
||||
}
|
||||
|
||||
/// User has joined a room.
|
||||
///
|
||||
/// Update the internal and cached state accordingly. Return the final Room.
|
||||
@@ -955,6 +1037,7 @@ impl BaseClient {
|
||||
&room,
|
||||
new_info.timeline.limited,
|
||||
new_info.timeline.events,
|
||||
false,
|
||||
new_info.timeline.prev_batch,
|
||||
&push_rules,
|
||||
&mut user_ids,
|
||||
@@ -1049,6 +1132,7 @@ impl BaseClient {
|
||||
&room,
|
||||
new_info.timeline.limited,
|
||||
new_info.timeline.events,
|
||||
false,
|
||||
new_info.timeline.prev_batch,
|
||||
&push_rules,
|
||||
&mut user_ids,
|
||||
@@ -1090,13 +1174,16 @@ impl BaseClient {
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
);
|
||||
|
||||
let invite_state =
|
||||
Self::deserialize_stripped_state_events(&new_info.invite_state.events);
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_as_invited();
|
||||
room_info.mark_state_fully_synced();
|
||||
|
||||
self.handle_invited_state(
|
||||
&room,
|
||||
&new_info.invite_state.events,
|
||||
&invite_state,
|
||||
&push_rules,
|
||||
&mut room_info,
|
||||
&mut changes,
|
||||
@@ -1109,6 +1196,34 @@ impl BaseClient {
|
||||
new_rooms.invite.insert(room_id, new_info);
|
||||
}
|
||||
|
||||
for (room_id, new_info) in response.rooms.knock {
|
||||
let room = self.store.get_or_create_room(
|
||||
&room_id,
|
||||
RoomState::Knocked,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
);
|
||||
|
||||
let knock_state = Self::deserialize_stripped_state_events(&new_info.knock_state.events);
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_as_knocked();
|
||||
room_info.mark_state_fully_synced();
|
||||
|
||||
self.handle_invited_state(
|
||||
&room,
|
||||
&knock_state,
|
||||
&push_rules,
|
||||
&mut room_info,
|
||||
&mut changes,
|
||||
&mut notifications,
|
||||
)
|
||||
.await?;
|
||||
|
||||
changes.add_room(room_info);
|
||||
|
||||
new_rooms.knocked.insert(room_id, new_info);
|
||||
}
|
||||
|
||||
account_data_processor.apply(&mut changes, &self.store).await;
|
||||
|
||||
changes.presence = response
|
||||
@@ -1217,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() {
|
||||
@@ -1248,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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1377,6 +1496,17 @@ impl BaseClient {
|
||||
self.store.room(room_id)
|
||||
}
|
||||
|
||||
/// Forget the room with the given room ID.
|
||||
///
|
||||
/// The room will be dropped from the room list and the store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The id of the room that should be forgotten.
|
||||
pub async fn forget_room(&self, room_id: &RoomId) -> StoreResult<()> {
|
||||
self.store.forget_room(room_id).await
|
||||
}
|
||||
|
||||
/// Get the olm machine.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub async fn olm_machine(&self) -> RwLockReadGuard<'_, Option<OlmMachine>> {
|
||||
@@ -1540,6 +1670,21 @@ impl BaseClient {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize_stripped_state_events(
|
||||
raw_events: &[Raw<AnyStrippedStateEvent>],
|
||||
) -> Vec<(Raw<AnyStrippedStateEvent>, AnyStrippedStateEvent)> {
|
||||
raw_events
|
||||
.iter()
|
||||
.filter_map(|raw_event| match raw_event.deserialize() {
|
||||
Ok(event) => Some((raw_event.clone(), event)),
|
||||
Err(e) => {
|
||||
warn!("Couldn't deserialize stripped state event: {e}");
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns a new receiver that gets future room info notable updates.
|
||||
///
|
||||
/// Learn more by reading the [`RoomInfoNotableUpdate`] type.
|
||||
@@ -1548,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>,
|
||||
@@ -1596,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]
|
||||
@@ -1728,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())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1804,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() },
|
||||
@@ -1862,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() },
|
||||
@@ -1922,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() },
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
use std::fmt;
|
||||
|
||||
pub use matrix_sdk_common::debug::*;
|
||||
use ruma::{api::client::sync::sync_events::v3::InvitedRoom, serde::Raw};
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::v3::{InvitedRoom, KnockedRoom},
|
||||
serde::Raw,
|
||||
};
|
||||
|
||||
/// A wrapper around a slice of `Raw` events that implements `Debug` in a way
|
||||
/// that only prints the event type of each item.
|
||||
@@ -46,6 +49,20 @@ impl<'a> fmt::Debug for DebugInvitedRoom<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around a knocked on room as found in `/sync` responses that
|
||||
/// implements `Debug` in a way that only prints the event ID and event type for
|
||||
/// the raw events contained in `knock_state`.
|
||||
pub struct DebugKnockedRoom<'a>(pub &'a KnockedRoom);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugKnockedRoom<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("KnockedRoom")
|
||||
.field("knock_state", &DebugListOfRawEvents(&self.0.knock_state.events))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DebugListOfRawEvents<'a, T>(pub &'a [Raw<T>]);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,4 +61,12 @@ pub enum Error {
|
||||
/// function with invalid parameters
|
||||
#[error("receive_all_members function was called with invalid parameters")]
|
||||
InvalidReceiveMembersParameters,
|
||||
|
||||
/// This request failed because the local data wasn't sufficient.
|
||||
#[error("Local cache doesn't contain all necessary data to perform the action.")]
|
||||
InsufficientData,
|
||||
|
||||
/// There was a [`serde_json`] deserialization error.
|
||||
#[error(transparent)]
|
||||
DeserializationError(#[from] serde_json::error::Error),
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
+136
-8
@@ -20,7 +20,7 @@ use ruma::{
|
||||
};
|
||||
|
||||
use super::DynEventCacheStore;
|
||||
use crate::media::{MediaFormat, MediaRequest, MediaThumbnailSettings};
|
||||
use crate::media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings};
|
||||
|
||||
/// `EventCacheStore` integration tests.
|
||||
///
|
||||
@@ -31,6 +31,9 @@ use crate::media::{MediaFormat, MediaRequest, MediaThumbnailSettings};
|
||||
pub trait EventCacheStoreIntegrationTests {
|
||||
/// Test media content storage.
|
||||
async fn test_media_content(&self);
|
||||
|
||||
/// Test replacing a MXID.
|
||||
async fn test_replace_media_key(&self);
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
@@ -38,11 +41,13 @@ pub trait EventCacheStoreIntegrationTests {
|
||||
impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
async fn test_media_content(&self) {
|
||||
let uri = mxc_uri!("mxc://localhost/media");
|
||||
let request_file =
|
||||
MediaRequest { source: MediaSource::Plain(uri.to_owned()), format: MediaFormat::File };
|
||||
let request_thumbnail = MediaRequest {
|
||||
let request_file = MediaRequestParameters {
|
||||
source: MediaSource::Plain(uri.to_owned()),
|
||||
format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
let request_thumbnail = MediaRequestParameters {
|
||||
source: MediaSource::Plain(uri.to_owned()),
|
||||
format: MediaFormat::Thumbnail(MediaThumbnailSettings::with_method(
|
||||
Method::Crop,
|
||||
uint!(100),
|
||||
uint!(100),
|
||||
@@ -50,7 +55,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
};
|
||||
|
||||
let other_uri = mxc_uri!("mxc://localhost/media-other");
|
||||
let request_other_file = MediaRequest {
|
||||
let request_other_file = MediaRequestParameters {
|
||||
source: MediaSource::Plain(other_uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
@@ -139,6 +144,44 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
"other media was removed"
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_replace_media_key(&self) {
|
||||
let uri = mxc_uri!("mxc://sendqueue.local/tr4n-s4ct-10n1-d");
|
||||
let req = MediaRequestParameters {
|
||||
source: MediaSource::Plain(uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
|
||||
let content = "hello".as_bytes().to_owned();
|
||||
|
||||
// Media isn't present in the cache.
|
||||
assert!(self.get_media_content(&req).await.unwrap().is_none(), "unexpected media found");
|
||||
|
||||
// Add the media.
|
||||
self.add_media_content(&req, content.clone()).await.expect("adding media failed");
|
||||
|
||||
// Sanity-check: media is found after adding it.
|
||||
assert_eq!(self.get_media_content(&req).await.unwrap().unwrap(), b"hello");
|
||||
|
||||
// Replacing a media request works.
|
||||
let new_uri = mxc_uri!("mxc://matrix.org/tr4n-s4ct-10n1-d");
|
||||
let new_req = MediaRequestParameters {
|
||||
source: MediaSource::Plain(new_uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
self.replace_media_key(&req, &new_req)
|
||||
.await
|
||||
.expect("replacing the media request key failed");
|
||||
|
||||
// Finding with the previous request doesn't work anymore.
|
||||
assert!(
|
||||
self.get_media_content(&req).await.unwrap().is_none(),
|
||||
"unexpected media found with the old key"
|
||||
);
|
||||
|
||||
// Finding with the new request does work.
|
||||
assert_eq!(self.get_media_content(&new_req).await.unwrap().unwrap(), b"hello");
|
||||
}
|
||||
}
|
||||
|
||||
/// Macro building to allow your `EventCacheStore` implementation to run the
|
||||
@@ -150,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,
|
||||
@@ -174,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;
|
||||
|
||||
@@ -184,6 +229,89 @@ macro_rules! event_cache_store_integration_tests {
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_media_content().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_replace_media_key() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_replace_media_key().await;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Macro generating tests for the event cache store, related to time (mostly
|
||||
/// for the cross-process lock).
|
||||
#[allow(unused_macros)]
|
||||
#[macro_export]
|
||||
macro_rules! event_cache_store_integration_tests_time {
|
||||
() => {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod event_cache_store_integration_tests_time {
|
||||
use std::time::Duration;
|
||||
|
||||
use matrix_sdk_test::async_test;
|
||||
use $crate::event_cache::store::IntoEventCacheStore;
|
||||
|
||||
use super::get_event_cache_store;
|
||||
|
||||
#[async_test]
|
||||
async fn test_lease_locks() {
|
||||
let store = get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
|
||||
let acquired0 = store.try_take_leased_lock(0, "key", "alice").await.unwrap();
|
||||
assert!(acquired0);
|
||||
|
||||
// Should extend the lease automatically (same holder).
|
||||
let acquired2 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
|
||||
assert!(acquired2);
|
||||
|
||||
// Should extend the lease automatically (same holder + time is ok).
|
||||
let acquired3 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
|
||||
assert!(acquired3);
|
||||
|
||||
// Another attempt at taking the lock should fail, because it's taken.
|
||||
let acquired4 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(!acquired4);
|
||||
|
||||
// Even if we insist.
|
||||
let acquired5 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(!acquired5);
|
||||
|
||||
// That's a nice test we got here, go take a little nap.
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
|
||||
// Still too early.
|
||||
let acquired55 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(!acquired55);
|
||||
|
||||
// Ok you can take another nap then.
|
||||
tokio::time::sleep(Duration::from_millis(250)).await;
|
||||
|
||||
// At some point, we do get the lock.
|
||||
let acquired6 = store.try_take_leased_lock(0, "key", "bob").await.unwrap();
|
||||
assert!(acquired6);
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(1)).await;
|
||||
|
||||
// The other gets it almost immediately too.
|
||||
let acquired7 = store.try_take_leased_lock(0, "key", "alice").await.unwrap();
|
||||
assert!(acquired7);
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(1)).await;
|
||||
|
||||
// But when we take a longer lease...
|
||||
let acquired8 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(acquired8);
|
||||
|
||||
// It blocks the other user.
|
||||
let acquired9 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
|
||||
assert!(!acquired9);
|
||||
|
||||
// We can hold onto our lease.
|
||||
let acquired10 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(acquired10);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
+46
-9
@@ -12,14 +12,16 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{num::NonZeroUsize, sync::RwLock as StdRwLock};
|
||||
use std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock, time::Instant};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::ring_buffer::RingBuffer;
|
||||
use matrix_sdk_common::{
|
||||
ring_buffer::RingBuffer, store_locks::memory_store_helper::try_take_leased_lock,
|
||||
};
|
||||
use ruma::{MxcUri, OwnedMxcUri};
|
||||
|
||||
use super::{EventCacheStore, EventCacheStoreError, Result};
|
||||
use crate::media::{MediaRequest, UniqueKey as _};
|
||||
use crate::media::{MediaRequestParameters, UniqueKey as _};
|
||||
|
||||
/// In-memory, non-persistent implementation of the `EventCacheStore`.
|
||||
///
|
||||
@@ -28,6 +30,7 @@ use crate::media::{MediaRequest, UniqueKey as _};
|
||||
#[derive(Debug)]
|
||||
pub struct MemoryStore {
|
||||
media: StdRwLock<RingBuffer<(OwnedMxcUri, String /* unique key */, Vec<u8>)>>,
|
||||
leases: StdRwLock<HashMap<String, (String, Instant)>>,
|
||||
}
|
||||
|
||||
// SAFETY: `new_unchecked` is safe because 20 is not zero.
|
||||
@@ -35,7 +38,10 @@ const NUMBER_OF_MEDIAS: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(20)
|
||||
|
||||
impl Default for MemoryStore {
|
||||
fn default() -> Self {
|
||||
Self { media: StdRwLock::new(RingBuffer::new(NUMBER_OF_MEDIAS)) }
|
||||
Self {
|
||||
media: StdRwLock::new(RingBuffer::new(NUMBER_OF_MEDIAS)),
|
||||
leases: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +57,20 @@ impl MemoryStore {
|
||||
impl EventCacheStore for MemoryStore {
|
||||
type Error = EventCacheStoreError;
|
||||
|
||||
async fn add_media_content(&self, request: &MediaRequest, data: Vec<u8>) -> Result<()> {
|
||||
async fn try_take_leased_lock(
|
||||
&self,
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Result<bool, Self::Error> {
|
||||
Ok(try_take_leased_lock(&self.leases, lease_duration_ms, key, holder))
|
||||
}
|
||||
|
||||
async fn add_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
data: Vec<u8>,
|
||||
) -> Result<()> {
|
||||
// Avoid duplication. Let's try to remove it first.
|
||||
self.remove_media_content(request).await?;
|
||||
// Now, let's add it.
|
||||
@@ -60,18 +79,35 @@ impl EventCacheStore for MemoryStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_media_content(&self, request: &MediaRequest) -> Result<Option<Vec<u8>>> {
|
||||
let media = self.media.read().unwrap();
|
||||
async fn replace_media_key(
|
||||
&self,
|
||||
from: &MediaRequestParameters,
|
||||
to: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error> {
|
||||
let expected_key = from.unique_key();
|
||||
|
||||
let mut medias = self.media.write().unwrap();
|
||||
if let Some((mxc, key, _)) = medias.iter_mut().find(|(_, key, _)| *key == expected_key) {
|
||||
*mxc = to.uri().to_owned();
|
||||
*key = to.unique_key();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_media_content(&self, request: &MediaRequestParameters) -> Result<Option<Vec<u8>>> {
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
let media = self.media.read().unwrap();
|
||||
Ok(media.iter().find_map(|(_media_uri, media_key, media_content)| {
|
||||
(media_key == &expected_key).then(|| media_content.to_owned())
|
||||
}))
|
||||
}
|
||||
|
||||
async fn remove_media_content(&self, request: &MediaRequest) -> Result<()> {
|
||||
let mut media = self.media.write().unwrap();
|
||||
async fn remove_media_content(&self, request: &MediaRequestParameters) -> Result<()> {
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
let mut media = self.media.write().unwrap();
|
||||
let Some(index) = media
|
||||
.iter()
|
||||
.position(|(_media_uri, media_key, _media_content)| media_key == &expected_key)
|
||||
@@ -113,4 +149,5 @@ mod tests {
|
||||
}
|
||||
|
||||
event_cache_store_integration_tests!();
|
||||
event_cache_store_integration_tests_time!();
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// 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.
|
||||
|
||||
//! The event cache stores holds events and downloaded media when the cache was
|
||||
//! activated to save bandwidth at the cost of increased storage space usage.
|
||||
//!
|
||||
//! Implementing the `EventCacheStore` trait, you can plug any storage backend
|
||||
//! into the event cache for the actual storage. By default this brings an
|
||||
//! in-memory store.
|
||||
|
||||
use std::{fmt, ops::Deref, str::Utf8Error, sync::Arc};
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
#[macro_use]
|
||||
pub mod integration_tests;
|
||||
mod memory_store;
|
||||
mod traits;
|
||||
|
||||
use matrix_sdk_common::store_locks::{
|
||||
BackingStore, CrossProcessStoreLock, CrossProcessStoreLockGuard, LockStoreError,
|
||||
};
|
||||
pub use matrix_sdk_store_encryption::Error as StoreEncryptionError;
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
pub use self::integration_tests::EventCacheStoreIntegrationTests;
|
||||
pub use self::{
|
||||
memory_store::MemoryStore,
|
||||
traits::{DynEventCacheStore, EventCacheStore, IntoEventCacheStore},
|
||||
};
|
||||
|
||||
/// The high-level public type to represent an `EventCacheStore` lock.
|
||||
#[derive(Clone)]
|
||||
pub struct EventCacheStoreLock {
|
||||
/// The inner cross process lock that is used to lock the `EventCacheStore`.
|
||||
cross_process_lock: CrossProcessStoreLock<LockableEventCacheStore>,
|
||||
|
||||
/// The store itself.
|
||||
///
|
||||
/// That's the only place where the store exists.
|
||||
store: Arc<DynEventCacheStore>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for EventCacheStoreLock {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.debug_struct("EventCacheStoreLock").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventCacheStoreLock {
|
||||
/// Create a new lock around the [`EventCacheStore`].
|
||||
///
|
||||
/// The `holder` argument represents the holder inside the
|
||||
/// [`CrossProcessStoreLock::new`].
|
||||
pub fn new<S>(store: S, holder: String) -> Self
|
||||
where
|
||||
S: IntoEventCacheStore,
|
||||
{
|
||||
let store = store.into_event_cache_store();
|
||||
|
||||
Self {
|
||||
cross_process_lock: CrossProcessStoreLock::new(
|
||||
LockableEventCacheStore(store.clone()),
|
||||
"default".to_owned(),
|
||||
holder,
|
||||
),
|
||||
store,
|
||||
}
|
||||
}
|
||||
|
||||
/// Acquire a spin lock (see [`CrossProcessStoreLock::spin_lock`]).
|
||||
pub async fn lock(&self) -> Result<EventCacheStoreLockGuard<'_>, LockStoreError> {
|
||||
let cross_process_lock_guard = self.cross_process_lock.spin_lock(None).await?;
|
||||
|
||||
Ok(EventCacheStoreLockGuard { cross_process_lock_guard, store: self.store.deref() })
|
||||
}
|
||||
}
|
||||
|
||||
/// An RAII implementation of a “scoped lock” of an [`EventCacheStoreLock`].
|
||||
/// When this structure is dropped (falls out of scope), the lock will be
|
||||
/// unlocked.
|
||||
pub struct EventCacheStoreLockGuard<'a> {
|
||||
/// The cross process lock guard.
|
||||
#[allow(unused)]
|
||||
cross_process_lock_guard: CrossProcessStoreLockGuard,
|
||||
|
||||
/// A reference to the store.
|
||||
store: &'a DynEventCacheStore,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for EventCacheStoreLockGuard<'a> {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.debug_struct("EventCacheStoreLockGuard").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for EventCacheStoreLockGuard<'a> {
|
||||
type Target = DynEventCacheStore;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.store
|
||||
}
|
||||
}
|
||||
|
||||
/// Event cache store specific error type.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum EventCacheStoreError {
|
||||
/// An error happened in the underlying database backend.
|
||||
#[error(transparent)]
|
||||
Backend(Box<dyn std::error::Error + Send + Sync>),
|
||||
|
||||
/// The store is locked with a passphrase and an incorrect passphrase
|
||||
/// was given.
|
||||
#[error("The event cache store failed to be unlocked")]
|
||||
Locked,
|
||||
|
||||
/// An unencrypted store was tried to be unlocked with a passphrase.
|
||||
#[error("The event cache store is not encrypted but tried to be opened with a passphrase")]
|
||||
Unencrypted,
|
||||
|
||||
/// The store failed to encrypt or decrypt some data.
|
||||
#[error("Error encrypting or decrypting data from the event cache store: {0}")]
|
||||
Encryption(#[from] StoreEncryptionError),
|
||||
|
||||
/// The store failed to encode or decode some data.
|
||||
#[error("Error encoding or decoding data from the event cache store: {0}")]
|
||||
Codec(#[from] Utf8Error),
|
||||
|
||||
/// The database format has changed in a backwards incompatible way.
|
||||
#[error(
|
||||
"The database format of the event cache store changed in an incompatible way, \
|
||||
current version: {0}, latest version: {1}"
|
||||
)]
|
||||
UnsupportedDatabaseVersion(usize, usize),
|
||||
}
|
||||
|
||||
impl EventCacheStoreError {
|
||||
/// Create a new [`Backend`][Self::Backend] error.
|
||||
///
|
||||
/// Shorthand for `EventCacheStoreError::Backend(Box::new(error))`.
|
||||
#[inline]
|
||||
pub fn backend<E>(error: E) -> Self
|
||||
where
|
||||
E: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
Self::Backend(Box::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
/// An `EventCacheStore` specific result type.
|
||||
pub type Result<T, E = EventCacheStoreError> = std::result::Result<T, E>;
|
||||
|
||||
/// A type that wraps the [`EventCacheStore`] but implements [`BackingStore`] to
|
||||
/// make it usable inside the cross process lock.
|
||||
#[derive(Clone, Debug)]
|
||||
struct LockableEventCacheStore(Arc<DynEventCacheStore>);
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
|
||||
impl BackingStore for LockableEventCacheStore {
|
||||
type LockError = EventCacheStoreError;
|
||||
|
||||
async fn try_lock(
|
||||
&self,
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> std::result::Result<bool, Self::LockError> {
|
||||
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await
|
||||
}
|
||||
}
|
||||
+66
-7
@@ -19,7 +19,7 @@ use matrix_sdk_common::AsyncTraitDeps;
|
||||
use ruma::MxcUri;
|
||||
|
||||
use super::EventCacheStoreError;
|
||||
use crate::media::MediaRequest;
|
||||
use crate::media::MediaRequestParameters;
|
||||
|
||||
/// An abstract trait that can be used to implement different store backends
|
||||
/// for the event cache of the SDK.
|
||||
@@ -29,6 +29,14 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
/// The error type used by this event cache store.
|
||||
type Error: fmt::Debug + Into<EventCacheStoreError>;
|
||||
|
||||
/// Try to take a lock using the given store.
|
||||
async fn try_take_leased_lock(
|
||||
&self,
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Result<bool, Self::Error>;
|
||||
|
||||
/// Add a media file's content in the media store.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -38,10 +46,35 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
/// * `content` - The content of the file.
|
||||
async fn add_media_content(
|
||||
&self,
|
||||
request: &MediaRequest,
|
||||
request: &MediaRequestParameters,
|
||||
content: Vec<u8>,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Replaces the given media's content key with another one.
|
||||
///
|
||||
/// This should be used whenever a temporary (local) MXID has been used, and
|
||||
/// it must now be replaced with its actual remote counterpart (after
|
||||
/// uploading some content, or creating an empty MXC URI).
|
||||
///
|
||||
/// ⚠ No check is performed to ensure that the media formats are consistent,
|
||||
/// i.e. it's possible to update with a thumbnail key a media that was
|
||||
/// keyed as a file before. The caller is responsible of ensuring that
|
||||
/// the replacement makes sense, according to their use case.
|
||||
///
|
||||
/// This should not raise an error when the `from` parameter points to an
|
||||
/// unknown media, and it should silently continue in this case.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `from` - The previous `MediaRequest` of the file.
|
||||
///
|
||||
/// * `to` - The new `MediaRequest` of the file.
|
||||
async fn replace_media_key(
|
||||
&self,
|
||||
from: &MediaRequestParameters,
|
||||
to: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Get a media file's content out of the media store.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -49,7 +82,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
/// * `request` - The `MediaRequest` of the file.
|
||||
async fn get_media_content(
|
||||
&self,
|
||||
request: &MediaRequest,
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error>;
|
||||
|
||||
/// Remove a media file's content from the media store.
|
||||
@@ -57,11 +90,17 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequest` of the file.
|
||||
async fn remove_media_content(&self, request: &MediaRequest) -> Result<(), Self::Error>;
|
||||
async fn remove_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// 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.
|
||||
@@ -83,22 +122,42 @@ impl<T: fmt::Debug> fmt::Debug for EraseEventCacheStoreError<T> {
|
||||
impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
|
||||
type Error = EventCacheStoreError;
|
||||
|
||||
async fn try_take_leased_lock(
|
||||
&self,
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Result<bool, Self::Error> {
|
||||
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn add_media_content(
|
||||
&self,
|
||||
request: &MediaRequest,
|
||||
request: &MediaRequestParameters,
|
||||
content: Vec<u8>,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.add_media_content(request, content).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn replace_media_key(
|
||||
&self,
|
||||
from: &MediaRequestParameters,
|
||||
to: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.replace_media_key(from, to).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn get_media_content(
|
||||
&self,
|
||||
request: &MediaRequest,
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
self.0.get_media_content(request).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn remove_media_content(&self, request: &MediaRequest) -> Result<(), Self::Error> {
|
||||
async fn remove_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.remove_media_content(request).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
// 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.
|
||||
|
||||
//! The event cache stores holds events and downloaded media when the cache was
|
||||
//! activated to save bandwidth at the cost of increased storage space usage.
|
||||
//!
|
||||
//! Implementing the `EventCacheStore` trait, you can plug any storage backend
|
||||
//! into the event cache for the actual storage. By default this brings an
|
||||
//! in-memory store.
|
||||
|
||||
use std::str::Utf8Error;
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
#[macro_use]
|
||||
pub mod integration_tests;
|
||||
mod memory_store;
|
||||
mod traits;
|
||||
|
||||
pub use matrix_sdk_store_encryption::Error as StoreEncryptionError;
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
pub use self::integration_tests::EventCacheStoreIntegrationTests;
|
||||
pub use self::{
|
||||
memory_store::MemoryStore,
|
||||
traits::{DynEventCacheStore, EventCacheStore, IntoEventCacheStore},
|
||||
};
|
||||
|
||||
/// Event cache store specific error type.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum EventCacheStoreError {
|
||||
/// An error happened in the underlying database backend.
|
||||
#[error(transparent)]
|
||||
Backend(Box<dyn std::error::Error + Send + Sync>),
|
||||
|
||||
/// The store is locked with a passphrase and an incorrect passphrase
|
||||
/// was given.
|
||||
#[error("The event cache store failed to be unlocked")]
|
||||
Locked,
|
||||
|
||||
/// An unencrypted store was tried to be unlocked with a passphrase.
|
||||
#[error("The event cache store is not encrypted but tried to be opened with a passphrase")]
|
||||
Unencrypted,
|
||||
|
||||
/// The store failed to encrypt or decrypt some data.
|
||||
#[error("Error encrypting or decrypting data from the event cache store: {0}")]
|
||||
Encryption(#[from] StoreEncryptionError),
|
||||
|
||||
/// The store failed to encode or decode some data.
|
||||
#[error("Error encoding or decoding data from the event cache store: {0}")]
|
||||
Codec(#[from] Utf8Error),
|
||||
|
||||
/// The database format has changed in a backwards incompatible way.
|
||||
#[error(
|
||||
"The database format of the event cache store changed in an incompatible way, \
|
||||
current version: {0}, latest version: {1}"
|
||||
)]
|
||||
UnsupportedDatabaseVersion(usize, usize),
|
||||
}
|
||||
|
||||
impl EventCacheStoreError {
|
||||
/// Create a new [`Backend`][Self::Backend] error.
|
||||
///
|
||||
/// Shorthand for `EventCacheStoreError::Backend(Box::new(error))`.
|
||||
#[inline]
|
||||
pub fn backend<E>(error: E) -> Self
|
||||
where
|
||||
E: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
Self::Backend(Box::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
/// An `EventCacheStore` specific result type.
|
||||
pub type Result<T, E = EventCacheStoreError> = std::result::Result<T, E>;
|
||||
@@ -12,7 +12,17 @@ use ruma::events::{
|
||||
room::message::SyncRoomMessageEvent,
|
||||
AnySyncMessageLikeEvent, AnySyncTimelineEvent,
|
||||
};
|
||||
use ruma::{events::sticker::SyncStickerEvent, MxcUri, OwnedEventId};
|
||||
use ruma::{
|
||||
events::{
|
||||
room::{
|
||||
member::{MembershipState, SyncRoomMemberEvent},
|
||||
power_levels::RoomPowerLevels,
|
||||
},
|
||||
sticker::SyncStickerEvent,
|
||||
AnySyncStateEvent,
|
||||
},
|
||||
MxcUri, OwnedEventId, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::MinimalRoomMemberEvent;
|
||||
@@ -37,6 +47,10 @@ pub enum PossibleLatestEvent<'a> {
|
||||
/// This message is suitable - it's a call notification
|
||||
YesCallNotify(&'a SyncCallNotifyEvent),
|
||||
|
||||
/// This state event is suitable - it's a knock membership change
|
||||
/// that can be handled by the current user.
|
||||
YesKnockedStateEvent(&'a SyncRoomMemberEvent),
|
||||
|
||||
// Later: YesState(),
|
||||
// Later: YesReaction(),
|
||||
/// Not suitable - it's a state event
|
||||
@@ -50,7 +64,10 @@ pub enum PossibleLatestEvent<'a> {
|
||||
/// Decide whether an event could be stored as the latest event in a room.
|
||||
/// Returns a LatestEvent representing our decision.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLatestEvent<'_> {
|
||||
pub fn is_suitable_for_latest_event<'a>(
|
||||
event: &'a AnySyncTimelineEvent,
|
||||
power_levels_info: Option<(&'a UserId, &'a RoomPowerLevels)>,
|
||||
) -> PossibleLatestEvent<'a> {
|
||||
match event {
|
||||
// Suitable - we have an m.room.message that was not redacted or edited
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => {
|
||||
@@ -102,8 +119,29 @@ pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLat
|
||||
// suitable
|
||||
AnySyncTimelineEvent::MessageLike(_) => PossibleLatestEvent::NoUnsupportedMessageLikeType,
|
||||
|
||||
// We don't currently support state events
|
||||
AnySyncTimelineEvent::State(_) => PossibleLatestEvent::NoUnsupportedEventType,
|
||||
// We don't currently support most state events
|
||||
AnySyncTimelineEvent::State(state) => {
|
||||
// But we make an exception for knocked state events *if* the current user
|
||||
// can either accept or decline them
|
||||
if let AnySyncStateEvent::RoomMember(member) = state {
|
||||
if matches!(member.membership(), MembershipState::Knock) {
|
||||
let can_accept_or_decline_knocks = match power_levels_info {
|
||||
Some((own_user_id, room_power_levels)) => {
|
||||
room_power_levels.user_can_invite(own_user_id)
|
||||
|| room_power_levels.user_can_kick(own_user_id)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
// The current user can act on the knock changes, so they should be
|
||||
// displayed
|
||||
if can_accept_or_decline_knocks {
|
||||
return PossibleLatestEvent::YesKnockedStateEvent(member);
|
||||
}
|
||||
}
|
||||
}
|
||||
PossibleLatestEvent::NoUnsupportedEventType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,7 +365,7 @@ mod tests {
|
||||
));
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Original(m)) =
|
||||
is_suitable_for_latest_event(&event)
|
||||
is_suitable_for_latest_event(&event, None)
|
||||
);
|
||||
|
||||
assert_eq!(m.content.msgtype.msgtype(), "m.image");
|
||||
@@ -350,7 +388,7 @@ mod tests {
|
||||
));
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesPoll(SyncMessageLikeEvent::Original(m)) =
|
||||
is_suitable_for_latest_event(&event)
|
||||
is_suitable_for_latest_event(&event, None)
|
||||
);
|
||||
|
||||
assert_eq!(m.content.poll_start().question.text, "do you like rust?");
|
||||
@@ -374,7 +412,7 @@ mod tests {
|
||||
));
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) =
|
||||
is_suitable_for_latest_event(&event)
|
||||
is_suitable_for_latest_event(&event, None)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -396,7 +434,7 @@ mod tests {
|
||||
));
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesCallNotify(SyncMessageLikeEvent::Original(_)) =
|
||||
is_suitable_for_latest_event(&event)
|
||||
is_suitable_for_latest_event(&event, None)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -417,7 +455,7 @@ mod tests {
|
||||
));
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event),
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::YesSticker(SyncStickerEvent::Original(_))
|
||||
);
|
||||
}
|
||||
@@ -439,7 +477,7 @@ mod tests {
|
||||
));
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event),
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::NoUnsupportedMessageLikeType
|
||||
);
|
||||
}
|
||||
@@ -467,7 +505,7 @@ mod tests {
|
||||
));
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event),
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Redacted(_))
|
||||
);
|
||||
}
|
||||
@@ -489,7 +527,10 @@ mod tests {
|
||||
}),
|
||||
));
|
||||
|
||||
assert_matches!(is_suitable_for_latest_event(&event), PossibleLatestEvent::NoEncrypted);
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::NoEncrypted
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -506,7 +547,7 @@ mod tests {
|
||||
));
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event),
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::NoUnsupportedEventType
|
||||
);
|
||||
}
|
||||
@@ -530,7 +571,7 @@ mod tests {
|
||||
));
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event),
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::NoUnsupportedMessageLikeType
|
||||
);
|
||||
}
|
||||
@@ -561,9 +602,12 @@ mod tests {
|
||||
json!({
|
||||
"latest_event": {
|
||||
"event": {
|
||||
"encryption_info": null,
|
||||
"event": {
|
||||
"event_id": "$1"
|
||||
"kind": {
|
||||
"PlainText": {
|
||||
"event": {
|
||||
"event_id": "$1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -577,6 +621,23 @@ mod tests {
|
||||
assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
|
||||
|
||||
// The previous format can also be deserialized.
|
||||
let serialized = json!({
|
||||
"latest_event": {
|
||||
"event": {
|
||||
"encryption_info": null,
|
||||
"event": {
|
||||
"event_id": "$1"
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
|
||||
assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
|
||||
assert!(deserialized.latest_event.sender_profile.is_none());
|
||||
assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
|
||||
|
||||
// The even older format can also be deserialized.
|
||||
let serialized = json!({
|
||||
"latest_event": event
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))]
|
||||
#![warn(missing_docs, missing_debug_implementations)]
|
||||
|
||||
pub use matrix_sdk_common::*;
|
||||
@@ -27,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;
|
||||
@@ -55,12 +56,12 @@ 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,
|
||||
};
|
||||
pub use store::{
|
||||
ComposerDraft, ComposerDraftType, StateChanges, StateStore, StateStoreDataKey,
|
||||
ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey,
|
||||
StateStoreDataValue, StoreError,
|
||||
};
|
||||
pub use utils::{
|
||||
|
||||
@@ -14,6 +14,7 @@ use ruma::{
|
||||
},
|
||||
MxcUri, UInt,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const UNIQUE_SEPARATOR: &str = "_";
|
||||
|
||||
@@ -25,7 +26,7 @@ pub trait UniqueKey {
|
||||
}
|
||||
|
||||
/// The requested format of a media file.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum MediaFormat {
|
||||
/// The file that was uploaded.
|
||||
File,
|
||||
@@ -43,9 +44,9 @@ impl UniqueKey for MediaFormat {
|
||||
}
|
||||
}
|
||||
|
||||
/// The requested size of a media thumbnail.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MediaThumbnailSize {
|
||||
/// The desired settings of a media thumbnail.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct MediaThumbnailSettings {
|
||||
/// The desired resizing method.
|
||||
pub method: Method,
|
||||
|
||||
@@ -56,19 +57,6 @@ pub struct MediaThumbnailSize {
|
||||
/// The desired height of the thumbnail. The actual thumbnail may not match
|
||||
/// the size specified.
|
||||
pub height: UInt,
|
||||
}
|
||||
|
||||
impl UniqueKey for MediaThumbnailSize {
|
||||
fn unique_key(&self) -> String {
|
||||
format!("{}{UNIQUE_SEPARATOR}{}x{}", self.method, self.width, self.height)
|
||||
}
|
||||
}
|
||||
|
||||
/// The desired settings of a media thumbnail.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MediaThumbnailSettings {
|
||||
/// The desired size of the thumbnail.
|
||||
pub size: MediaThumbnailSize,
|
||||
|
||||
/// If we want to request an animated thumbnail from the homeserver.
|
||||
///
|
||||
@@ -82,14 +70,24 @@ pub struct MediaThumbnailSettings {
|
||||
impl MediaThumbnailSettings {
|
||||
/// Constructs a new `MediaThumbnailSettings` with the given method, width
|
||||
/// and height.
|
||||
pub fn new(method: Method, width: UInt, height: UInt) -> Self {
|
||||
Self { size: MediaThumbnailSize { method, width, height }, animated: false }
|
||||
///
|
||||
/// Requests a non-animated thumbnail by default.
|
||||
pub fn with_method(method: Method, width: UInt, height: UInt) -> Self {
|
||||
Self { method, width, height, animated: false }
|
||||
}
|
||||
|
||||
/// Constructs a new `MediaThumbnailSettings` with the given width and
|
||||
/// height.
|
||||
///
|
||||
/// Requests scaling, and a non-animated thumbnail.
|
||||
pub fn new(width: UInt, height: UInt) -> Self {
|
||||
Self { method: Method::Scale, width, height, animated: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl UniqueKey for MediaThumbnailSettings {
|
||||
fn unique_key(&self) -> String {
|
||||
let mut key = self.size.unique_key();
|
||||
let mut key = format!("{}{UNIQUE_SEPARATOR}{}x{}", self.method, self.width, self.height);
|
||||
|
||||
if self.animated {
|
||||
key.push_str(UNIQUE_SEPARATOR);
|
||||
@@ -109,9 +107,11 @@ impl UniqueKey for MediaSource {
|
||||
}
|
||||
}
|
||||
|
||||
/// A request for media data.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MediaRequest {
|
||||
/// Parameters for a request for retrieve media data.
|
||||
///
|
||||
/// This is used as a key in the media cache too.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct MediaRequestParameters {
|
||||
/// The source of the media file.
|
||||
pub source: MediaSource,
|
||||
|
||||
@@ -119,7 +119,7 @@ pub struct MediaRequest {
|
||||
pub format: MediaFormat,
|
||||
}
|
||||
|
||||
impl MediaRequest {
|
||||
impl MediaRequestParameters {
|
||||
/// Get the [`MxcUri`] from `Self`.
|
||||
pub fn uri(&self) -> &MxcUri {
|
||||
match &self.source {
|
||||
@@ -129,7 +129,7 @@ impl MediaRequest {
|
||||
}
|
||||
}
|
||||
|
||||
impl UniqueKey for MediaRequest {
|
||||
impl UniqueKey for MediaRequestParameters {
|
||||
fn unique_key(&self) -> String {
|
||||
format!("{}{UNIQUE_SEPARATOR}{}", self.source.unique_key(), self.format.unique_key())
|
||||
}
|
||||
@@ -225,14 +225,14 @@ mod tests {
|
||||
fn test_media_request_url() {
|
||||
let mxc_uri = mxc_uri!("mxc://homeserver/media");
|
||||
|
||||
let plain = MediaRequest {
|
||||
let plain = MediaRequestParameters {
|
||||
source: MediaSource::Plain(mxc_uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
|
||||
assert_eq!(plain.uri(), mxc_uri);
|
||||
|
||||
let file = MediaRequest {
|
||||
let file = MediaRequestParameters {
|
||||
source: MediaSource::Encrypted(Box::new(
|
||||
serde_json::from_value(json!({
|
||||
"url": mxc_uri,
|
||||
|
||||
@@ -203,7 +203,7 @@ impl RoomReadReceipts {
|
||||
/// Returns whether a new event triggered a new unread/notification/mention.
|
||||
#[inline(always)]
|
||||
fn process_event(&mut self, event: &SyncTimelineEvent, user_id: &UserId) {
|
||||
if marks_as_unread(&event.event, user_id) {
|
||||
if marks_as_unread(event.raw(), user_id) {
|
||||
self.num_unread += 1;
|
||||
}
|
||||
|
||||
@@ -408,7 +408,7 @@ impl ReceiptSelector {
|
||||
fn try_match_implicit(&mut self, user_id: &UserId, new_events: &[SyncTimelineEvent]) {
|
||||
for ev in new_events {
|
||||
// Get the `sender` field, if any, or skip this event.
|
||||
let Ok(Some(sender)) = ev.event.get_field::<OwnedUserId>("sender") else { continue };
|
||||
let Ok(Some(sender)) = ev.raw().get_field::<OwnedUserId>("sender") else { continue };
|
||||
if sender == user_id {
|
||||
// Get the event id, if any, or skip this event.
|
||||
let Some(event_id) = ev.event_id() else { continue };
|
||||
|
||||
@@ -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>>,
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ pub use normal::{
|
||||
Room, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomState,
|
||||
RoomStateFilter,
|
||||
};
|
||||
use regex::Regex;
|
||||
use ruma::{
|
||||
assign,
|
||||
events::{
|
||||
@@ -49,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
|
||||
@@ -64,14 +65,48 @@ pub enum DisplayName {
|
||||
Empty,
|
||||
}
|
||||
|
||||
impl fmt::Display for DisplayName {
|
||||
const WHITESPACE_REGEX: &str = r"\s+";
|
||||
const INVALID_SYMBOLS_REGEX: &str = r"[#,:\{\}\\]+";
|
||||
|
||||
impl RoomDisplayName {
|
||||
/// Transforms the current display name into the name part of a
|
||||
/// `RoomAliasId`.
|
||||
pub fn to_room_alias_name(&self) -> String {
|
||||
let room_name = match self {
|
||||
Self::Named(name) => name,
|
||||
Self::Aliased(name) => name,
|
||||
Self::Calculated(name) => name,
|
||||
Self::EmptyWas(name) => name,
|
||||
Self::Empty => "",
|
||||
};
|
||||
|
||||
let whitespace_regex =
|
||||
Regex::new(WHITESPACE_REGEX).expect("`WHITESPACE_REGEX` should be valid");
|
||||
let symbol_regex =
|
||||
Regex::new(INVALID_SYMBOLS_REGEX).expect("`INVALID_SYMBOLS_REGEX` should be valid");
|
||||
|
||||
// Replace whitespaces with `-`
|
||||
let sanitised = whitespace_regex.replace_all(room_name, "-");
|
||||
// Remove non-ASCII characters and ASCII control characters
|
||||
let sanitised =
|
||||
String::from_iter(sanitised.chars().filter(|c| c.is_ascii() && !c.is_ascii_control()));
|
||||
// Remove other problematic ASCII symbols
|
||||
let sanitised = symbol_regex.replace_all(&sanitised, "");
|
||||
// Lowercased
|
||||
sanitised.to_lowercase()
|
||||
}
|
||||
}
|
||||
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -541,6 +576,7 @@ mod tests {
|
||||
use ruma::events::tag::{TagInfo, TagName, Tags};
|
||||
|
||||
use super::{BaseRoomInfo, RoomNotableTags};
|
||||
use crate::RoomDisplayName;
|
||||
|
||||
#[test]
|
||||
fn test_handle_notable_tags_favourite() {
|
||||
@@ -571,4 +607,36 @@ mod tests {
|
||||
base_room_info.handle_notable_tags(&tags);
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_room_alias_from_room_display_name_lowercases() {
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
RoomDisplayName::Named("#Room,{Alias}:".to_owned()).to_room_alias_name()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ use std::{
|
||||
use bitflags::bitflags;
|
||||
use eyeball::{SharedObservable, Subscriber};
|
||||
use futures_util::{Stream, StreamExt};
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEventKind;
|
||||
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
|
||||
use matrix_sdk_common::ring_buffer::RingBuffer;
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
@@ -41,6 +43,7 @@ use ruma::{
|
||||
join_rules::JoinRule,
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
pinned_events::RoomPinnedEventsEventContent,
|
||||
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
||||
redaction::SyncRoomRedactionEvent,
|
||||
tombstone::RoomTombstoneEventContent,
|
||||
},
|
||||
@@ -58,18 +61,18 @@ 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},
|
||||
sync::UnreadNotificationsCount,
|
||||
MinimalStateEvent, OriginalMinimalStateEvent, RoomMemberships,
|
||||
Error, MinimalStateEvent, OriginalMinimalStateEvent, RoomMemberships,
|
||||
};
|
||||
|
||||
/// Indicates that a notable update of `RoomInfo` has been applied, and why.
|
||||
@@ -182,15 +185,17 @@ impl RoomSummary {
|
||||
}
|
||||
|
||||
/// Enum keeping track in which state the room is, e.g. if our own user is
|
||||
/// joined, invited, or has left the room.
|
||||
/// joined, RoomState::Invited, or has left the room.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub enum RoomState {
|
||||
/// The room is in a joined state.
|
||||
Joined,
|
||||
/// The room is in a left state.
|
||||
Left,
|
||||
/// The room is in a invited state.
|
||||
/// The room is in an invited state.
|
||||
Invited,
|
||||
/// The room is in a knocked state.
|
||||
Knocked,
|
||||
}
|
||||
|
||||
impl From<&MembershipState> for RoomState {
|
||||
@@ -201,7 +206,7 @@ impl From<&MembershipState> for RoomState {
|
||||
MembershipState::Ban => Self::Left,
|
||||
MembershipState::Invite => Self::Invited,
|
||||
MembershipState::Join => Self::Joined,
|
||||
MembershipState::Knock => Self::Left,
|
||||
MembershipState::Knock => Self::Knocked,
|
||||
MembershipState::Leave => Self::Left,
|
||||
_ => panic!("Unexpected MembershipState: {}", membership_state),
|
||||
}
|
||||
@@ -273,6 +278,11 @@ impl Room {
|
||||
self.inner.read().room_state
|
||||
}
|
||||
|
||||
/// Get the previous state of the room, if it had any.
|
||||
pub fn prev_state(&self) -> Option<RoomState> {
|
||||
self.inner.read().prev_room_state
|
||||
}
|
||||
|
||||
/// Whether this room's [`RoomType`] is `m.space`.
|
||||
pub fn is_space(&self) -> bool {
|
||||
self.inner.read().room_type().is_some_and(|t| *t == RoomType::Space)
|
||||
@@ -436,6 +446,9 @@ impl Room {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: implement logic once we have the stripped events as we'd have with an Invite
|
||||
RoomState::Knocked => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,6 +509,17 @@ impl Room {
|
||||
self.inner.read().base_info.max_power_level
|
||||
}
|
||||
|
||||
/// Get the current power levels of this room.
|
||||
pub async fn power_levels(&self) -> Result<RoomPowerLevels, Error> {
|
||||
Ok(self
|
||||
.store
|
||||
.get_state_event_static::<RoomPowerLevelsEventContent>(self.room_id())
|
||||
.await?
|
||||
.ok_or(Error::InsufficientData)?
|
||||
.deserialize()?
|
||||
.power_levels())
|
||||
}
|
||||
|
||||
/// Get the `m.room.name` of this room.
|
||||
///
|
||||
/// The returned string may be empty if the event has been redacted, or it's
|
||||
@@ -548,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());
|
||||
@@ -567,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()
|
||||
@@ -679,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()
|
||||
}
|
||||
|
||||
@@ -795,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();
|
||||
@@ -859,8 +882,8 @@ impl Room {
|
||||
/// Get the `RoomMember` with the given `user_id`.
|
||||
///
|
||||
/// Returns `None` if the member was never part of this room, otherwise
|
||||
/// return a `RoomMember` that can be in a joined, invited, left, banned
|
||||
/// state.
|
||||
/// return a `RoomMember` that can be in a joined, RoomState::Invited, left,
|
||||
/// banned state.
|
||||
///
|
||||
/// Async because it can read from storage.
|
||||
pub async fn get_member(&self, user_id: &UserId) -> StoreResult<Option<RoomMember>> {
|
||||
@@ -876,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)))
|
||||
@@ -887,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);
|
||||
@@ -995,7 +1018,7 @@ impl Room {
|
||||
}
|
||||
|
||||
/// Returns the current pinned event ids for this room.
|
||||
pub fn pinned_event_ids(&self) -> Vec<OwnedEventId> {
|
||||
pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
|
||||
self.inner.read().pinned_event_ids()
|
||||
}
|
||||
}
|
||||
@@ -1032,6 +1055,9 @@ pub struct RoomInfo {
|
||||
/// The state of the room.
|
||||
pub(crate) room_state: RoomState,
|
||||
|
||||
/// The previous state of the room, if any.
|
||||
pub(crate) prev_room_state: Option<RoomState>,
|
||||
|
||||
/// The unread notifications counts, as returned by the server.
|
||||
///
|
||||
/// These might be incorrect for encrypted rooms, since the server doesn't
|
||||
@@ -1076,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")]
|
||||
@@ -1117,6 +1143,7 @@ impl RoomInfo {
|
||||
version: 1,
|
||||
room_id: room_id.into(),
|
||||
room_state,
|
||||
prev_room_state: None,
|
||||
notification_counts: Default::default(),
|
||||
summary: Default::default(),
|
||||
members_synced: false,
|
||||
@@ -1137,22 +1164,30 @@ impl RoomInfo {
|
||||
|
||||
/// Mark this Room as joined.
|
||||
pub fn mark_as_joined(&mut self) {
|
||||
self.room_state = RoomState::Joined;
|
||||
self.set_state(RoomState::Joined);
|
||||
}
|
||||
|
||||
/// Mark this Room as left.
|
||||
pub fn mark_as_left(&mut self) {
|
||||
self.room_state = RoomState::Left;
|
||||
self.set_state(RoomState::Left);
|
||||
}
|
||||
|
||||
/// Mark this Room as invited.
|
||||
pub fn mark_as_invited(&mut self) {
|
||||
self.room_state = RoomState::Invited;
|
||||
self.set_state(RoomState::Invited);
|
||||
}
|
||||
|
||||
/// Mark this Room as knocked.
|
||||
pub fn mark_as_knocked(&mut self) {
|
||||
self.set_state(RoomState::Knocked);
|
||||
}
|
||||
|
||||
/// Set the membership RoomState of this Room
|
||||
pub fn set_state(&mut self, room_state: RoomState) {
|
||||
self.room_state = room_state;
|
||||
if room_state != self.room_state {
|
||||
self.prev_room_state = Some(self.room_state);
|
||||
self.room_state = room_state;
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark this Room as having all the members synced.
|
||||
@@ -1261,9 +1296,12 @@ impl RoomInfo {
|
||||
if let Some(latest_event) = &mut self.latest_event {
|
||||
tracing::trace!("Checking if redaction applies to latest event");
|
||||
if latest_event.event_id().as_deref() == Some(redacts) {
|
||||
match apply_redaction(&latest_event.event().event, _raw, room_version) {
|
||||
match apply_redaction(latest_event.event().raw(), _raw, room_version) {
|
||||
Some(redacted) => {
|
||||
latest_event.event_mut().event = redacted;
|
||||
// Even if the original event was encrypted, redaction removes all its
|
||||
// fields so it cannot possibly be successfully decrypted after redaction.
|
||||
latest_event.event_mut().kind =
|
||||
TimelineEventKind::PlainText { event: redacted };
|
||||
debug!("Redacted latest event");
|
||||
}
|
||||
None => {
|
||||
@@ -1557,8 +1595,8 @@ impl RoomInfo {
|
||||
}
|
||||
|
||||
/// Returns the current pinned event ids for this room.
|
||||
pub fn pinned_event_ids(&self) -> Vec<OwnedEventId> {
|
||||
self.base_info.pinned_events.clone().map(|c| c.pinned).unwrap_or_default()
|
||||
pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
|
||||
self.base_info.pinned_events.clone().map(|c| c.pinned)
|
||||
}
|
||||
|
||||
/// Checks if an `EventId` is currently pinned.
|
||||
@@ -1685,6 +1723,8 @@ bitflags! {
|
||||
const INVITED = 0b00000010;
|
||||
/// The room is in a left state.
|
||||
const LEFT = 0b00000100;
|
||||
/// The room is in a knocked state.
|
||||
const KNOCKED = 0b00001000;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1699,6 +1739,7 @@ impl RoomStateFilter {
|
||||
RoomState::Joined => Self::JOINED,
|
||||
RoomState::Left => Self::LEFT,
|
||||
RoomState::Invited => Self::INVITED,
|
||||
RoomState::Knocked => Self::KNOCKED,
|
||||
};
|
||||
|
||||
self.contains(bit_state)
|
||||
@@ -1725,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);
|
||||
|
||||
@@ -1747,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1808,8 +1852,9 @@ mod tests {
|
||||
use crate::latest_event::LatestEvent;
|
||||
use crate::{
|
||||
rooms::RoomNotableTags,
|
||||
store::{IntoStateStore, MemoryStore, StateChanges, StateStore},
|
||||
BaseClient, DisplayName, MinimalStateEvent, OriginalMinimalStateEvent, SessionMeta,
|
||||
store::{IntoStateStore, MemoryStore, StateChanges, StateStore, StoreConfig},
|
||||
BaseClient, MinimalStateEvent, OriginalMinimalStateEvent, RoomDisplayName,
|
||||
RoomInfoNotableUpdateReasons, SessionMeta,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -1827,6 +1872,7 @@ mod tests {
|
||||
version: 1,
|
||||
room_id: room_id!("!gda78o:server.tld").into(),
|
||||
room_state: RoomState::Invited,
|
||||
prev_room_state: None,
|
||||
notification_counts: UnreadNotificationsCount {
|
||||
highlight_count: 1,
|
||||
notification_count: 2,
|
||||
@@ -1844,9 +1890,9 @@ mod tests {
|
||||
last_prev_batch: Some("pb".to_owned()),
|
||||
sync_info: SyncInfo::FullySynced,
|
||||
encryption_state_synced: true,
|
||||
latest_event: Some(Box::new(LatestEvent::new(
|
||||
Raw::from_json_string(json!({"sender": "@u:i.uk"}).to_string()).unwrap().into(),
|
||||
))),
|
||||
latest_event: Some(Box::new(LatestEvent::new(SyncTimelineEvent::new(
|
||||
Raw::from_json_string(json!({"sender": "@u:i.uk"}).to_string()).unwrap(),
|
||||
)))),
|
||||
base_info: Box::new(
|
||||
assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")])) }),
|
||||
),
|
||||
@@ -1861,6 +1907,7 @@ mod tests {
|
||||
"version": 1,
|
||||
"room_id": "!gda78o:server.tld",
|
||||
"room_state": "Invited",
|
||||
"prev_room_state": null,
|
||||
"notification_counts": {
|
||||
"highlight_count": 1,
|
||||
"notification_count": 2,
|
||||
@@ -1880,10 +1927,7 @@ mod tests {
|
||||
"encryption_state_synced": true,
|
||||
"latest_event": {
|
||||
"event": {
|
||||
"encryption_info": null,
|
||||
"event": {
|
||||
"sender": "@u:i.uk",
|
||||
},
|
||||
"kind": {"PlainText": {"event": {"sender": "@u:i.uk"}}},
|
||||
},
|
||||
},
|
||||
"base_info": {
|
||||
@@ -1932,6 +1976,7 @@ mod tests {
|
||||
let info_json = json!({
|
||||
"room_id": "!gda78o:server.tld",
|
||||
"room_state": "Invited",
|
||||
"prev_room_state": null,
|
||||
"notification_counts": {
|
||||
"highlight_count": 1,
|
||||
"notification_count": 2,
|
||||
@@ -2008,7 +2053,8 @@ mod tests {
|
||||
|
||||
let info_json = json!({
|
||||
"room_id": "!gda78o:server.tld",
|
||||
"room_state": "Invited",
|
||||
"room_state": "Joined",
|
||||
"prev_room_state": "Invited",
|
||||
"notification_counts": {
|
||||
"highlight_count": 1,
|
||||
"notification_count": 2,
|
||||
@@ -2048,7 +2094,8 @@ mod tests {
|
||||
let info: RoomInfo = serde_json::from_value(info_json).unwrap();
|
||||
|
||||
assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
|
||||
assert_eq!(info.room_state, RoomState::Invited);
|
||||
assert_eq!(info.room_state, RoomState::Joined);
|
||||
assert_eq!(info.prev_room_state, Some(RoomState::Invited));
|
||||
assert_eq!(info.notification_counts.highlight_count, 1);
|
||||
assert_eq!(info.notification_counts.notification_count, 2);
|
||||
assert_eq!(
|
||||
@@ -2081,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(),
|
||||
@@ -2093,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(
|
||||
@@ -2171,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(
|
||||
@@ -2286,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]
|
||||
@@ -2296,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())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2307,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]
|
||||
@@ -2333,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]
|
||||
@@ -2343,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())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2354,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())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2402,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())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2424,7 +2475,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap(),
|
||||
DisplayName::Calculated("Matthew".to_owned())
|
||||
RoomDisplayName::Calculated("Matthew".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2454,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())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2479,7 +2530,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap(),
|
||||
DisplayName::Calculated("Matthew".to_owned())
|
||||
RoomDisplayName::Calculated("Matthew".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2534,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())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2583,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())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2613,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())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2627,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(
|
||||
@@ -3009,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]
|
||||
@@ -3166,4 +3219,32 @@ mod tests {
|
||||
let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
|
||||
assert_eq!(new_room_info.version, 1);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_prev_room_state_is_updated() {
|
||||
let (_store, room) = make_room_test_helper(RoomState::Invited);
|
||||
assert_eq!(room.prev_state(), None);
|
||||
assert_eq!(room.state(), RoomState::Invited);
|
||||
|
||||
// Invited -> Joined
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_as_joined();
|
||||
room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
|
||||
assert_eq!(room.prev_state(), Some(RoomState::Invited));
|
||||
assert_eq!(room.state(), RoomState::Joined);
|
||||
|
||||
// No change when the same state is used
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_as_joined();
|
||||
room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
|
||||
assert_eq!(room.prev_state(), Some(RoomState::Invited));
|
||||
assert_eq!(room.state(), RoomState::Joined);
|
||||
|
||||
// Joined -> Left
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_as_left();
|
||||
room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
|
||||
assert_eq!(room.prev_state(), Some(RoomState::Joined));
|
||||
assert_eq!(room.state(), RoomState::Left);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,13 @@ use ruma::api::client::sync::sync_events::v5;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::events::AnyToDeviceEvent;
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::v3::{self, InvitedRoom},
|
||||
events::{AnyRoomAccountDataEvent, AnySyncStateEvent, AnySyncTimelineEvent},
|
||||
api::client::sync::sync_events::v3::{self, InvitedRoom, KnockedRoom},
|
||||
events::{
|
||||
room::member::MembershipState, AnyRoomAccountDataEvent, AnyStrippedStateEvent,
|
||||
AnySyncStateEvent, StateEventType,
|
||||
},
|
||||
serde::Raw,
|
||||
JsOption, OwnedRoomId, RoomId, UInt,
|
||||
JsOption, OwnedRoomId, RoomId, UInt, UserId,
|
||||
};
|
||||
use tracing::{debug, error, instrument, trace, warn};
|
||||
|
||||
@@ -47,6 +50,7 @@ use crate::{
|
||||
normal::{RoomHero, RoomInfoNotableUpdateReasons},
|
||||
RoomState,
|
||||
},
|
||||
ruma::assign,
|
||||
store::{ambiguity_map::AmbiguityCache, StateChanges, Store},
|
||||
sync::{JoinedRoomUpdate, LeftRoomUpdate, Notification, RoomUpdates, SyncResponse},
|
||||
Room, RoomInfo,
|
||||
@@ -167,13 +171,20 @@ impl BaseClient {
|
||||
let mut notifications = Default::default();
|
||||
let mut rooms_account_data = extensions.account_data.rooms.clone();
|
||||
|
||||
let user_id = self
|
||||
.session_meta()
|
||||
.expect("Sliding sync shouldn't run without an authenticated user.")
|
||||
.user_id
|
||||
.to_owned();
|
||||
|
||||
for (room_id, response_room_data) in rooms {
|
||||
let (room_info, joined_room, left_room, invited_room) = self
|
||||
let (room_info, joined_room, left_room, invited_room, knocked_room) = self
|
||||
.process_sliding_sync_room(
|
||||
room_id,
|
||||
response_room_data,
|
||||
&mut rooms_account_data,
|
||||
&store,
|
||||
&user_id,
|
||||
&account_data_processor,
|
||||
&mut changes,
|
||||
&mut room_info_notable_updates,
|
||||
@@ -196,6 +207,10 @@ impl BaseClient {
|
||||
if let Some(invited_room) = invited_room {
|
||||
new_rooms.invite.insert(room_id.clone(), invited_room);
|
||||
}
|
||||
|
||||
if let Some(knocked_room) = knocked_room {
|
||||
new_rooms.knocked.insert(room_id.clone(), knocked_room);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle read receipts and typing notifications independently of the rooms:
|
||||
@@ -260,7 +275,7 @@ impl BaseClient {
|
||||
.or_insert_with(LeftRoomUpdate::default)
|
||||
.account_data
|
||||
.append(&mut raw.to_vec()),
|
||||
RoomState::Invited => {}
|
||||
RoomState::Invited | RoomState::Knocked => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -341,27 +356,32 @@ impl BaseClient {
|
||||
room_data: &http::response::Room,
|
||||
rooms_account_data: &mut BTreeMap<OwnedRoomId, Vec<Raw<AnyRoomAccountDataEvent>>>,
|
||||
store: &Store,
|
||||
user_id: &UserId,
|
||||
account_data_processor: &AccountDataProcessor,
|
||||
changes: &mut StateChanges,
|
||||
room_info_notable_updates: &mut BTreeMap<OwnedRoomId, RoomInfoNotableUpdateReasons>,
|
||||
notifications: &mut BTreeMap<OwnedRoomId, Vec<Notification>>,
|
||||
ambiguity_cache: &mut AmbiguityCache,
|
||||
with_msc4186: bool,
|
||||
) -> Result<(RoomInfo, Option<JoinedRoomUpdate>, Option<LeftRoomUpdate>, Option<InvitedRoom>)>
|
||||
{
|
||||
) -> Result<(
|
||||
RoomInfo,
|
||||
Option<JoinedRoomUpdate>,
|
||||
Option<LeftRoomUpdate>,
|
||||
Option<InvitedRoom>,
|
||||
Option<KnockedRoom>,
|
||||
)> {
|
||||
// This method may change `room_data` (see the terrible hack describes below)
|
||||
// with `timestamp` and `invite_state. We don't want to change the `room_data`
|
||||
// from outside this method, hence `Cow` is perfectly suited here.
|
||||
let mut room_data = Cow::Borrowed(room_data);
|
||||
|
||||
let (raw_state_events, state_events): (Vec<_>, Vec<_>) = {
|
||||
let mut state_events = Vec::new();
|
||||
|
||||
// Read state events from the `required_state` field.
|
||||
state_events.extend(Self::deserialize_state_events(&room_data.required_state));
|
||||
let state_events = Self::deserialize_state_events(&room_data.required_state);
|
||||
|
||||
// Read state events from the `timeline` field.
|
||||
state_events.extend(Self::deserialize_state_events_from_timeline(&room_data.timeline));
|
||||
// Don't read state events from the `timeline` field, because they might be
|
||||
// incomplete or staled already. We must only read state events from
|
||||
// `required_state`.
|
||||
|
||||
state_events.into_iter().unzip()
|
||||
};
|
||||
@@ -403,14 +423,22 @@ impl BaseClient {
|
||||
}
|
||||
}
|
||||
|
||||
let stripped_state: Option<Vec<(Raw<AnyStrippedStateEvent>, AnyStrippedStateEvent)>> =
|
||||
room_data
|
||||
.invite_state
|
||||
.as_ref()
|
||||
.map(|invite_state| Self::deserialize_stripped_state_events(invite_state));
|
||||
|
||||
#[allow(unused_mut)] // Required for some feature flag combinations
|
||||
let (mut room, mut room_info, invited_room) = self.process_sliding_sync_room_membership(
|
||||
room_data.as_ref(),
|
||||
&state_events,
|
||||
store,
|
||||
room_id,
|
||||
room_info_notable_updates,
|
||||
);
|
||||
let (mut room, mut room_info, invited_room, knocked_room) = self
|
||||
.process_sliding_sync_room_membership(
|
||||
&state_events,
|
||||
stripped_state.as_ref(),
|
||||
store,
|
||||
user_id,
|
||||
room_id,
|
||||
room_info_notable_updates,
|
||||
);
|
||||
|
||||
room_info.mark_state_partially_synced();
|
||||
|
||||
@@ -429,7 +457,8 @@ impl BaseClient {
|
||||
|
||||
let push_rules = self.get_push_rules(account_data_processor).await?;
|
||||
|
||||
if let Some(invite_state) = &room_data.invite_state {
|
||||
// This will be used for both invited and knocked rooms.
|
||||
if let Some(invite_state) = &stripped_state {
|
||||
self.handle_invited_state(
|
||||
&room,
|
||||
invite_state,
|
||||
@@ -454,6 +483,7 @@ impl BaseClient {
|
||||
&room,
|
||||
room_data.limited,
|
||||
room_data.timeline.clone(),
|
||||
true,
|
||||
room_data.prev_batch.clone(),
|
||||
&push_rules,
|
||||
&mut user_ids,
|
||||
@@ -512,6 +542,7 @@ impl BaseClient {
|
||||
)),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -525,9 +556,12 @@ impl BaseClient {
|
||||
ambiguity_changes,
|
||||
)),
|
||||
None,
|
||||
None,
|
||||
)),
|
||||
|
||||
RoomState::Invited => Ok((room_info, None, None, invited_room)),
|
||||
RoomState::Invited => Ok((room_info, None, None, invited_room, None)),
|
||||
|
||||
RoomState::Knocked => Ok((room_info, None, None, None, knocked_room)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -538,13 +572,14 @@ impl BaseClient {
|
||||
/// otherwise. https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/sync-v3/proposals/3575-sync.md#room-list-parameters
|
||||
fn process_sliding_sync_room_membership(
|
||||
&self,
|
||||
room_data: &http::response::Room,
|
||||
state_events: &[AnySyncStateEvent],
|
||||
stripped_state: Option<&Vec<(Raw<AnyStrippedStateEvent>, AnyStrippedStateEvent)>>,
|
||||
store: &Store,
|
||||
user_id: &UserId,
|
||||
room_id: &RoomId,
|
||||
room_info_notable_updates: &mut BTreeMap<OwnedRoomId, RoomInfoNotableUpdateReasons>,
|
||||
) -> (Room, RoomInfo, Option<InvitedRoom>) {
|
||||
if let Some(invite_state) = &room_data.invite_state {
|
||||
) -> (Room, RoomInfo, Option<InvitedRoom>, Option<KnockedRoom>) {
|
||||
if let Some(stripped_state) = stripped_state {
|
||||
let room = store.get_or_create_room(
|
||||
room_id,
|
||||
RoomState::Invited,
|
||||
@@ -552,20 +587,34 @@ impl BaseClient {
|
||||
);
|
||||
let mut room_info = room.clone_info();
|
||||
|
||||
// We don't actually know what events are inside invite_state. In theory, they
|
||||
// might not contain an m.room.member event, or they might set the
|
||||
// membership to something other than invite. This would be very
|
||||
// weird behaviour by the server, because invite_state is supposed
|
||||
// to contain an m.room.member. We will call handle_invited_state, which will
|
||||
// reflect any information found in the real events inside
|
||||
// invite_state, but we default to considering this room invited
|
||||
// simply because invite_state exists. This is needed in the normal
|
||||
// case, because the sliding sync server tries to send minimal state,
|
||||
// meaning that we normally actually just receive {"type": "m.room.member"} with
|
||||
// no content at all.
|
||||
room_info.mark_as_invited();
|
||||
// We need to find the membership event since it could be for either an invited
|
||||
// or knocked room
|
||||
let membership_event_content = stripped_state.iter().find_map(|(_, event)| {
|
||||
if let AnyStrippedStateEvent::RoomMember(membership_event) = event {
|
||||
if membership_event.state_key == user_id {
|
||||
return Some(membership_event.content.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
});
|
||||
|
||||
(room, room_info, Some(InvitedRoom::from(v3::InviteState::from(invite_state.clone()))))
|
||||
if let Some(membership_event_content) = membership_event_content {
|
||||
if membership_event_content.membership == MembershipState::Knock {
|
||||
// If we have a `Knock` membership state, set the room as such
|
||||
room_info.mark_as_knocked();
|
||||
let raw_events = stripped_state.iter().map(|(raw, _)| raw.clone()).collect();
|
||||
let knock_state = assign!(v3::KnockState::default(), { events: raw_events });
|
||||
let knocked_room =
|
||||
assign!(KnockedRoom::default(), { knock_state: knock_state });
|
||||
return (room, room_info, None, Some(knocked_room));
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise assume it's an invited room
|
||||
room_info.mark_as_invited();
|
||||
let raw_events = stripped_state.iter().map(|(raw, _)| raw.clone()).collect::<Vec<_>>();
|
||||
let invited_room = InvitedRoom::from(v3::InviteState::from(raw_events));
|
||||
(room, room_info, Some(invited_room), None)
|
||||
} else {
|
||||
let room = store.get_or_create_room(
|
||||
room_id,
|
||||
@@ -591,7 +640,7 @@ impl BaseClient {
|
||||
room_info_notable_updates,
|
||||
);
|
||||
|
||||
(room, room_info, None)
|
||||
(room, room_info, None, None)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -629,33 +678,6 @@ impl BaseClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize_state_events_from_timeline(
|
||||
raw_events: &[Raw<AnySyncTimelineEvent>],
|
||||
) -> Vec<(Raw<AnySyncStateEvent>, AnySyncStateEvent)> {
|
||||
raw_events
|
||||
.iter()
|
||||
.filter_map(|raw_event| {
|
||||
// If it contains `state_key`, we assume it's a state event.
|
||||
if raw_event.get_field::<serde::de::IgnoredAny>("state_key").transpose().is_some() {
|
||||
match raw_event.deserialize_as::<AnySyncStateEvent>() {
|
||||
Ok(event) => {
|
||||
// SAFETY: Casting `AnySyncTimelineEvent` to `AnySyncStateEvent` is safe
|
||||
// because we checked that there is a `state_key`.
|
||||
Some((raw_event.clone().cast(), event))
|
||||
}
|
||||
|
||||
Err(error) => {
|
||||
warn!("Couldn't deserialize state event from timeline: {error}");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the most recent decrypted event and cache it in the supplied RoomInfo.
|
||||
@@ -673,17 +695,41 @@ 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());
|
||||
|
||||
// Try to get room power levels from the current changes
|
||||
let power_levels_from_changes = || {
|
||||
let state_changes = changes?.state.get(room_info.room_id())?;
|
||||
let room_power_levels_state =
|
||||
state_changes.get(&StateEventType::RoomPowerLevels)?.values().next()?;
|
||||
match room_power_levels_state.deserialize().ok()? {
|
||||
AnySyncStateEvent::RoomPowerLevels(ev) => Some(ev.power_levels()),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
|
||||
// If we didn't get any info, try getting it from local data
|
||||
let power_levels = match power_levels_from_changes() {
|
||||
Some(power_levels) => Some(power_levels),
|
||||
None => room.power_levels().await.ok(),
|
||||
};
|
||||
|
||||
let power_levels_info = Some(room.own_user_id()).zip(power_levels.as_ref());
|
||||
|
||||
for event in events.iter().rev() {
|
||||
if let Ok(timeline_event) = event.event.deserialize() {
|
||||
match is_suitable_for_latest_event(&timeline_event) {
|
||||
if let Ok(timeline_event) = event.raw().deserialize() {
|
||||
match is_suitable_for_latest_event(&timeline_event, power_levels_info) {
|
||||
PossibleLatestEvent::YesRoomMessage(_)
|
||||
| PossibleLatestEvent::YesPoll(_)
|
||||
| PossibleLatestEvent::YesCallInvite(_)
|
||||
| PossibleLatestEvent::YesCallNotify(_)
|
||||
| PossibleLatestEvent::YesSticker(_) => {
|
||||
| PossibleLatestEvent::YesSticker(_)
|
||||
| PossibleLatestEvent::YesKnockedStateEvent(_) => {
|
||||
// We found a suitable latest event. Store it.
|
||||
|
||||
// In order to make the latest event fast to read, we want to keep the
|
||||
@@ -710,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)
|
||||
})
|
||||
},
|
||||
)
|
||||
});
|
||||
@@ -757,7 +805,7 @@ async fn cache_latest_events(
|
||||
// Check how many encrypted events we have seen. Only store another if we
|
||||
// haven't already stored the maximum number.
|
||||
if encrypted_events.len() < encrypted_events.capacity() {
|
||||
encrypted_events.push(event.event.clone());
|
||||
encrypted_events.push(event.raw().clone());
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
@@ -854,7 +902,10 @@ mod tests {
|
||||
};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, ring_buffer::RingBuffer};
|
||||
use matrix_sdk_common::{
|
||||
deserialized_responses::{SyncTimelineEvent, UnableToDecryptInfo, UnableToDecryptReason},
|
||||
ring_buffer::RingBuffer,
|
||||
};
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::UnreadNotificationsCount,
|
||||
@@ -982,6 +1033,7 @@ mod tests {
|
||||
assert!(sync_resp.rooms.join.contains_key(room_id));
|
||||
assert!(!sync_resp.rooms.leave.contains_key(room_id));
|
||||
assert!(!sync_resp.rooms.invite.contains_key(room_id));
|
||||
assert!(!sync_resp.rooms.knocked.contains_key(room_id));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
@@ -1038,6 +1090,7 @@ mod tests {
|
||||
assert!(!sync_resp.rooms.join.contains_key(room_id));
|
||||
assert!(!sync_resp.rooms.leave.contains_key(room_id));
|
||||
assert!(sync_resp.rooms.invite.contains_key(room_id));
|
||||
assert!(!sync_resp.rooms.knocked.contains_key(room_id));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
@@ -1066,6 +1119,78 @@ mod tests {
|
||||
assert_eq!(client_room.compute_display_name().await.unwrap().to_string(), "The Name");
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_receiving_a_knocked_room_membership_event_creates_a_knocked_room() {
|
||||
// Given a logged-in client,
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
let user_id = client.session_meta().unwrap().user_id.to_owned();
|
||||
|
||||
// When the room is properly set as knocked with the current user id as state
|
||||
// key,
|
||||
let mut room = http::response::Room::new();
|
||||
set_room_knocked(&mut room, &user_id);
|
||||
|
||||
let response = response_with_room(room_id, room);
|
||||
client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync");
|
||||
|
||||
// The room is knocked.
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
assert_eq!(client_room.state(), RoomState::Knocked);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_receiving_a_knocked_room_membership_event_with_wrong_state_key_creates_an_invited_room(
|
||||
) {
|
||||
// Given a logged-in client,
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
let user_id = user_id!("@w:e.uk");
|
||||
|
||||
// When the room is set as knocked with a random user id as state key,
|
||||
let mut room = http::response::Room::new();
|
||||
set_room_knocked(&mut room, user_id);
|
||||
|
||||
let response = response_with_room(room_id, room);
|
||||
client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync");
|
||||
|
||||
// The room is invited since the membership event doesn't belong to the current
|
||||
// user.
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
assert_eq!(client_room.state(), RoomState::Invited);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_receiving_an_unknown_room_membership_event_in_invite_state_creates_an_invited_room(
|
||||
) {
|
||||
// Given a logged-in client,
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
let user_id = client.session_meta().unwrap().user_id.to_owned();
|
||||
|
||||
// When the room has the wrong membership state in its invite_state
|
||||
let mut room = http::response::Room::new();
|
||||
let event = Raw::new(&json!({
|
||||
"type": "m.room.member",
|
||||
"sender": user_id,
|
||||
"content": {
|
||||
"is_direct": true,
|
||||
"membership": "join",
|
||||
},
|
||||
"state_key": user_id,
|
||||
}))
|
||||
.expect("Failed to make raw event")
|
||||
.cast();
|
||||
room.invite_state = Some(vec![event]);
|
||||
|
||||
let response = response_with_room(room_id, room);
|
||||
client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync");
|
||||
|
||||
// The room is marked as invited.
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
assert_eq!(client_room.state(), RoomState::Invited);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_left_a_room_from_required_state_event() {
|
||||
// Given a logged-in client
|
||||
@@ -1096,6 +1221,7 @@ mod tests {
|
||||
assert!(!sync_resp.rooms.join.contains_key(room_id));
|
||||
assert!(sync_resp.rooms.leave.contains_key(room_id));
|
||||
assert!(!sync_resp.rooms.invite.contains_key(room_id));
|
||||
assert!(!sync_resp.rooms.knocked.contains_key(room_id));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
@@ -1137,6 +1263,7 @@ mod tests {
|
||||
assert!(!sync_resp.rooms.join.contains_key(room_id));
|
||||
assert!(sync_resp.rooms.leave.contains_key(room_id));
|
||||
assert!(!sync_resp.rooms.invite.contains_key(room_id));
|
||||
assert!(!sync_resp.rooms.knocked.contains_key(room_id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1160,8 +1287,8 @@ mod tests {
|
||||
let response = response_with_room(room_id, room);
|
||||
client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync");
|
||||
|
||||
// The room is left.
|
||||
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
|
||||
// The room is NOT left because state events from `timeline` must be IGNORED!
|
||||
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Joined);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
@@ -1637,6 +1764,119 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_last_knock_event_from_sliding_sync_is_cached_if_user_has_permissions() {
|
||||
let own_user_id = user_id!("@me:e.uk");
|
||||
// Given a logged-in client
|
||||
let client = logged_in_base_client(Some(own_user_id)).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
|
||||
// Give the current user invite or kick permissions in this room
|
||||
let power_levels = json!({
|
||||
"sender":"@alice:example.com",
|
||||
"state_key":"",
|
||||
"type":"m.room.power_levels",
|
||||
"event_id": "$idb",
|
||||
"origin_server_ts": 12344445,
|
||||
"content":{ "invite": 100, "kick": 100, "users": { own_user_id: 100 } },
|
||||
"room_id": room_id,
|
||||
});
|
||||
|
||||
// And a knock member state event
|
||||
let knock_event = json!({
|
||||
"sender":"@alice:example.com",
|
||||
"state_key":"@alice:example.com",
|
||||
"type":"m.room.member",
|
||||
"event_id": "$ida",
|
||||
"origin_server_ts": 12344446,
|
||||
"content":{"membership": "knock"},
|
||||
"room_id": room_id,
|
||||
});
|
||||
|
||||
// When the sliding sync response contains a timeline
|
||||
let events = &[knock_event];
|
||||
let mut room = room_with_timeline(events);
|
||||
room.required_state.push(Raw::new(&power_levels).unwrap().cast());
|
||||
let response = response_with_room(room_id, room);
|
||||
client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync");
|
||||
|
||||
// Then the room holds the latest knock state event
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
assert_eq!(
|
||||
ev_id(client_room.latest_event().map(|latest_event| latest_event.event().clone())),
|
||||
"$ida"
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_last_knock_event_from_sliding_sync_is_not_cached_without_permissions() {
|
||||
let own_user_id = user_id!("@me:e.uk");
|
||||
// Given a logged-in client
|
||||
let client = logged_in_base_client(Some(own_user_id)).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
|
||||
// Set the user as a user with no permission to invite or kick other users in
|
||||
// this room
|
||||
let power_levels = json!({
|
||||
"sender":"@alice:example.com",
|
||||
"state_key":"",
|
||||
"type":"m.room.power_levels",
|
||||
"event_id": "$idb",
|
||||
"origin_server_ts": 12344445,
|
||||
"content":{ "invite": 50, "kick": 50, "users": { own_user_id: 0 } },
|
||||
"room_id": room_id,
|
||||
});
|
||||
|
||||
// And a knock member state event
|
||||
let knock_event = json!({
|
||||
"sender":"@alice:example.com",
|
||||
"state_key":"@alice:example.com",
|
||||
"type":"m.room.member",
|
||||
"event_id": "$ida",
|
||||
"origin_server_ts": 12344446,
|
||||
"content":{"membership": "knock"},
|
||||
"room_id": room_id,
|
||||
});
|
||||
|
||||
// When the sliding sync response contains a timeline
|
||||
let events = &[knock_event];
|
||||
let mut room = room_with_timeline(events);
|
||||
room.required_state.push(Raw::new(&power_levels).unwrap().cast());
|
||||
let response = response_with_room(room_id, room);
|
||||
client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync");
|
||||
|
||||
// Then the room doesn't hold the knock state event as the latest event
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
assert!(client_room.latest_event().is_none());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_last_non_knock_member_state_event_from_sliding_sync_is_not_cached() {
|
||||
// Given a logged-in client
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
// And a join member state event
|
||||
let join_event = json!({
|
||||
"sender":"@alice:example.com",
|
||||
"state_key":"@alice:example.com",
|
||||
"type":"m.room.member",
|
||||
"event_id": "$ida",
|
||||
"origin_server_ts": 12344446,
|
||||
"content":{"membership": "join"},
|
||||
"room_id": room_id,
|
||||
});
|
||||
|
||||
// When the sliding sync response contains a timeline
|
||||
let events = &[join_event];
|
||||
let room = room_with_timeline(events);
|
||||
let response = response_with_room(room_id, room);
|
||||
client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync");
|
||||
|
||||
// Then the room doesn't hold the join state event as the latest event
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
assert!(client_room.latest_event().is_none());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_cached_latest_event_can_be_redacted() {
|
||||
// Given a logged-in client
|
||||
@@ -1683,7 +1923,7 @@ mod tests {
|
||||
|
||||
// But it's now redacted
|
||||
assert_matches!(
|
||||
latest_event.event().event.deserialize().unwrap(),
|
||||
latest_event.event().raw().deserialize().unwrap(),
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
|
||||
SyncRoomMessageEvent::Redacted(_)
|
||||
))
|
||||
@@ -2275,7 +2515,7 @@ mod tests {
|
||||
// The newly created room has no pinned event ids
|
||||
let room = client.get_room(room_id).unwrap();
|
||||
let pinned_event_ids = room.pinned_event_ids();
|
||||
assert!(pinned_event_ids.is_empty());
|
||||
assert_matches!(pinned_event_ids, None);
|
||||
|
||||
// Load new pinned event id
|
||||
let mut room_response = http::response::Room::new();
|
||||
@@ -2288,7 +2528,7 @@ mod tests {
|
||||
let response = response_with_room(room_id, room_response);
|
||||
client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync");
|
||||
|
||||
let pinned_event_ids = room.pinned_event_ids();
|
||||
let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
|
||||
assert_eq!(pinned_event_ids.len(), 1);
|
||||
assert_eq!(pinned_event_ids[0], pinned_event_id);
|
||||
|
||||
@@ -2302,7 +2542,7 @@ mod tests {
|
||||
));
|
||||
let response = response_with_room(room_id, room_response);
|
||||
client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync");
|
||||
let pinned_event_ids = room.pinned_event_ids();
|
||||
let pinned_event_ids = room.pinned_event_ids().unwrap();
|
||||
assert!(pinned_event_ids.is_empty());
|
||||
}
|
||||
|
||||
@@ -2396,7 +2636,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn make_encrypted_event(id: &str) -> SyncTimelineEvent {
|
||||
SyncTimelineEvent::new(
|
||||
SyncTimelineEvent::new_utd_event(
|
||||
Raw::from_json_string(
|
||||
json!({
|
||||
"type": "m.room.encrypted",
|
||||
@@ -2414,6 +2654,10 @@ mod tests {
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap(),
|
||||
UnableToDecryptInfo {
|
||||
session_id: Some("".to_owned()),
|
||||
reason: UnableToDecryptReason::MissingMegolmSession,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2579,6 +2823,25 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
fn set_room_knocked(room: &mut http::response::Room, knocker: &UserId) {
|
||||
// MSC3575 shows an almost-empty event to indicate that we are invited to a
|
||||
// room. Just the type is supplied.
|
||||
|
||||
let evt = Raw::new(&json!({
|
||||
"type": "m.room.member",
|
||||
"sender": knocker,
|
||||
"content": {
|
||||
"is_direct": true,
|
||||
"membership": "knock",
|
||||
},
|
||||
"state_key": knocker,
|
||||
}))
|
||||
.expect("Failed to make raw event")
|
||||
.cast();
|
||||
|
||||
room.invite_state = Some(vec![evt]);
|
||||
}
|
||||
|
||||
fn set_room_joined(room: &mut http::response::Room, user_id: &UserId) {
|
||||
room.required_state.push(make_membership_event(user_id, MembershipState::Join));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -33,10 +33,13 @@ use ruma::{
|
||||
};
|
||||
use serde_json::{json, value::Value as JsonValue};
|
||||
|
||||
use super::{DependentQueuedEventKind, DynStateStore, ServerCapabilities};
|
||||
use super::{
|
||||
send_queue::SentRequestKey, DependentQueuedRequestKind, DisplayName, DynStateStore,
|
||||
ServerCapabilities,
|
||||
};
|
||||
use crate::{
|
||||
deserialized_responses::MemberEvent,
|
||||
store::{traits::ChildTransactionId, Result, SerializableEventContent, StateStoreExt},
|
||||
store::{ChildTransactionId, QueueWedgeError, Result, SerializableEventContent, StateStoreExt},
|
||||
RoomInfo, RoomMemberships, RoomState, StateChanges, StateStoreDataKey, StateStoreDataValue,
|
||||
};
|
||||
|
||||
@@ -83,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.
|
||||
@@ -137,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());
|
||||
|
||||
@@ -252,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());
|
||||
@@ -286,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"
|
||||
);
|
||||
@@ -958,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?;
|
||||
@@ -986,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
|
||||
@@ -1141,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());
|
||||
@@ -1163,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);
|
||||
@@ -1178,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);
|
||||
@@ -1202,7 +1212,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let room_id = room_id!("!test_send_queue:localhost");
|
||||
|
||||
// No queued event in store at first.
|
||||
let events = self.load_send_queue_events(room_id).await.unwrap();
|
||||
let events = self.load_send_queue_requests(room_id).await.unwrap();
|
||||
assert!(events.is_empty());
|
||||
|
||||
// Saving one thing should work.
|
||||
@@ -1210,20 +1220,20 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let event0 =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("msg0").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_event(room_id, txn0.clone(), event0).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_events(room_id).await.unwrap();
|
||||
let pending = self.load_send_queue_requests(room_id).await.unwrap();
|
||||
|
||||
assert_eq!(pending.len(), 1);
|
||||
{
|
||||
assert_eq!(pending[0].transaction_id, txn0);
|
||||
|
||||
let deserialized = pending[0].event.deserialize().unwrap();
|
||||
let deserialized = pending[0].as_event().unwrap().deserialize().unwrap();
|
||||
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
|
||||
assert_eq!(content.body(), "msg0");
|
||||
|
||||
assert!(!pending[0].is_wedged);
|
||||
assert!(!pending[0].is_wedged());
|
||||
}
|
||||
|
||||
// Saving another three things should work.
|
||||
@@ -1234,11 +1244,11 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.save_send_queue_event(room_id, txn, event).await.unwrap();
|
||||
self.save_send_queue_request(room_id, txn, event.into(), 0).await.unwrap();
|
||||
}
|
||||
|
||||
// Reading all the events should work.
|
||||
let pending = self.load_send_queue_events(room_id).await.unwrap();
|
||||
let pending = self.load_send_queue_requests(room_id).await.unwrap();
|
||||
|
||||
// All the events should be retrieved, in the same order.
|
||||
assert_eq!(pending.len(), 4);
|
||||
@@ -1246,27 +1256,36 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
assert_eq!(pending[0].transaction_id, txn0);
|
||||
|
||||
for i in 0..4 {
|
||||
let deserialized = pending[i].event.deserialize().unwrap();
|
||||
let deserialized = pending[i].as_event().unwrap().deserialize().unwrap();
|
||||
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
|
||||
assert_eq!(content.body(), format!("msg{i}"));
|
||||
assert!(!pending[i].is_wedged);
|
||||
assert!(!pending[i].is_wedged());
|
||||
}
|
||||
|
||||
// Marking an event as wedged works.
|
||||
let txn2 = &pending[2].transaction_id;
|
||||
self.update_send_queue_event_status(room_id, txn2, true).await.unwrap();
|
||||
self.update_send_queue_request_status(
|
||||
room_id,
|
||||
txn2,
|
||||
Some(QueueWedgeError::GenericApiError { msg: "Oops".to_owned() }),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// And it is reflected.
|
||||
let pending = self.load_send_queue_events(room_id).await.unwrap();
|
||||
let pending = self.load_send_queue_requests(room_id).await.unwrap();
|
||||
|
||||
// All the events should be retrieved, in the same order.
|
||||
assert_eq!(pending.len(), 4);
|
||||
assert_eq!(pending[0].transaction_id, txn0);
|
||||
assert_eq!(pending[2].transaction_id, *txn2);
|
||||
assert!(pending[2].is_wedged);
|
||||
assert!(pending[2].is_wedged());
|
||||
let error = pending[2].clone().error.unwrap();
|
||||
let generic_error = assert_matches!(error, QueueWedgeError::GenericApiError { msg } => msg);
|
||||
assert_eq!(generic_error, "Oops");
|
||||
for i in 0..4 {
|
||||
if i != 2 {
|
||||
assert!(!pending[i].is_wedged);
|
||||
assert!(!pending[i].is_wedged());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1275,37 +1294,37 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
&RoomMessageEventContent::text_plain("wow that's a cool test").into(),
|
||||
)
|
||||
.unwrap();
|
||||
self.update_send_queue_event(room_id, txn2, event0).await.unwrap();
|
||||
self.update_send_queue_request(room_id, txn2, event0.into()).await.unwrap();
|
||||
|
||||
// And it is reflected.
|
||||
let pending = self.load_send_queue_events(room_id).await.unwrap();
|
||||
let pending = self.load_send_queue_requests(room_id).await.unwrap();
|
||||
|
||||
assert_eq!(pending.len(), 4);
|
||||
{
|
||||
assert_eq!(pending[2].transaction_id, *txn2);
|
||||
|
||||
let deserialized = pending[2].event.deserialize().unwrap();
|
||||
let deserialized = pending[2].as_event().unwrap().deserialize().unwrap();
|
||||
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
|
||||
assert_eq!(content.body(), "wow that's a cool test");
|
||||
|
||||
assert!(!pending[2].is_wedged);
|
||||
assert!(!pending[2].is_wedged());
|
||||
|
||||
for i in 0..4 {
|
||||
if i != 2 {
|
||||
let deserialized = pending[i].event.deserialize().unwrap();
|
||||
let deserialized = pending[i].as_event().unwrap().deserialize().unwrap();
|
||||
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
|
||||
assert_eq!(content.body(), format!("msg{i}"));
|
||||
|
||||
assert!(!pending[i].is_wedged);
|
||||
assert!(!pending[i].is_wedged());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Removing an event works.
|
||||
self.remove_send_queue_event(room_id, &txn0).await.unwrap();
|
||||
self.remove_send_queue_request(room_id, &txn0).await.unwrap();
|
||||
|
||||
// And it is reflected.
|
||||
let pending = self.load_send_queue_events(room_id).await.unwrap();
|
||||
let pending = self.load_send_queue_requests(room_id).await.unwrap();
|
||||
|
||||
assert_eq!(pending.len(), 3);
|
||||
assert_eq!(pending[1].transaction_id, *txn2);
|
||||
@@ -1323,7 +1342,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let event =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("room2").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_event(room_id2, txn.clone(), event).await.unwrap();
|
||||
self.save_send_queue_request(room_id2, txn.clone(), event.into(), 0).await.unwrap();
|
||||
}
|
||||
|
||||
// Add and remove one event for room3.
|
||||
@@ -1333,19 +1352,77 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let event =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("room3").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_event(room_id3, txn.clone(), event).await.unwrap();
|
||||
self.save_send_queue_request(room_id3, txn.clone(), event.into(), 0).await.unwrap();
|
||||
|
||||
self.remove_send_queue_event(room_id3, &txn).await.unwrap();
|
||||
self.remove_send_queue_request(room_id3, &txn).await.unwrap();
|
||||
}
|
||||
|
||||
// Query all the rooms which have unsent events. Per the previous steps,
|
||||
// it should be room1 and room2, not room3.
|
||||
let outstanding_rooms = self.load_rooms_with_unsent_events().await.unwrap();
|
||||
let outstanding_rooms = self.load_rooms_with_unsent_requests().await.unwrap();
|
||||
assert_eq!(outstanding_rooms.len(), 2);
|
||||
assert!(outstanding_rooms.iter().any(|room| room == room_id));
|
||||
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");
|
||||
|
||||
@@ -1354,53 +1431,61 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let event0 =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("hey").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_event(room_id, txn0.clone(), event0).await.unwrap();
|
||||
self.save_send_queue_request(room_id, txn0.clone(), event0.into(), 0).await.unwrap();
|
||||
|
||||
// No dependents, to start with.
|
||||
assert!(self.list_dependent_send_queue_events(room_id).await.unwrap().is_empty());
|
||||
assert!(self.load_dependent_queued_requests(room_id).await.unwrap().is_empty());
|
||||
|
||||
// Save a redaction for that event.
|
||||
let child_txn = ChildTransactionId::new();
|
||||
self.save_dependent_send_queue_event(
|
||||
self.save_dependent_queued_request(
|
||||
room_id,
|
||||
&txn0,
|
||||
child_txn.clone(),
|
||||
DependentQueuedEventKind::Redact,
|
||||
DependentQueuedRequestKind::RedactEvent,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// It worked.
|
||||
let dependents = self.list_dependent_send_queue_events(room_id).await.unwrap();
|
||||
let dependents = self.load_dependent_queued_requests(room_id).await.unwrap();
|
||||
assert_eq!(dependents.len(), 1);
|
||||
assert_eq!(dependents[0].parent_transaction_id, txn0);
|
||||
assert_eq!(dependents[0].own_transaction_id, child_txn);
|
||||
assert!(dependents[0].event_id.is_none());
|
||||
assert_matches!(dependents[0].kind, DependentQueuedEventKind::Redact);
|
||||
assert!(dependents[0].parent_key.is_none());
|
||||
assert_matches!(dependents[0].kind, DependentQueuedRequestKind::RedactEvent);
|
||||
|
||||
// Update the event id.
|
||||
let event_id = owned_event_id!("$1");
|
||||
let num_updated =
|
||||
self.update_dependent_send_queue_event(room_id, &txn0, event_id.clone()).await.unwrap();
|
||||
let num_updated = self
|
||||
.update_dependent_queued_request(
|
||||
room_id,
|
||||
&txn0,
|
||||
SentRequestKey::Event(event_id.clone()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(num_updated, 1);
|
||||
|
||||
// It worked.
|
||||
let dependents = self.list_dependent_send_queue_events(room_id).await.unwrap();
|
||||
let dependents = self.load_dependent_queued_requests(room_id).await.unwrap();
|
||||
assert_eq!(dependents.len(), 1);
|
||||
assert_eq!(dependents[0].parent_transaction_id, txn0);
|
||||
assert_eq!(dependents[0].own_transaction_id, child_txn);
|
||||
assert_eq!(dependents[0].event_id.as_ref(), Some(&event_id));
|
||||
assert_matches!(dependents[0].kind, DependentQueuedEventKind::Redact);
|
||||
assert_matches!(dependents[0].parent_key.as_ref(), Some(SentRequestKey::Event(eid)) => {
|
||||
assert_eq!(*eid, event_id);
|
||||
});
|
||||
assert_matches!(dependents[0].kind, DependentQueuedRequestKind::RedactEvent);
|
||||
|
||||
// Now remove it.
|
||||
let removed = self
|
||||
.remove_dependent_send_queue_event(room_id, &dependents[0].own_transaction_id)
|
||||
.remove_dependent_queued_request(room_id, &dependents[0].own_transaction_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(removed);
|
||||
|
||||
// It worked.
|
||||
assert!(self.list_dependent_send_queue_events(room_id).await.unwrap().is_empty());
|
||||
assert!(self.load_dependent_queued_requests(room_id).await.unwrap().is_empty());
|
||||
|
||||
// Now, inserting a dependent event and removing the original send queue event
|
||||
// will NOT remove the dependent event.
|
||||
@@ -1408,23 +1493,23 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let event1 =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("hey2").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_event(room_id, txn1.clone(), event1).await.unwrap();
|
||||
self.save_send_queue_request(room_id, txn1.clone(), event1.into(), 0).await.unwrap();
|
||||
|
||||
self.save_dependent_send_queue_event(
|
||||
self.save_dependent_queued_request(
|
||||
room_id,
|
||||
&txn0,
|
||||
ChildTransactionId::new(),
|
||||
DependentQueuedEventKind::Redact,
|
||||
DependentQueuedRequestKind::RedactEvent,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(self.list_dependent_send_queue_events(room_id).await.unwrap().len(), 1);
|
||||
assert_eq!(self.load_dependent_queued_requests(room_id).await.unwrap().len(), 1);
|
||||
|
||||
self.save_dependent_send_queue_event(
|
||||
self.save_dependent_queued_request(
|
||||
room_id,
|
||||
&txn1,
|
||||
ChildTransactionId::new(),
|
||||
DependentQueuedEventKind::Edit {
|
||||
DependentQueuedRequestKind::EditEvent {
|
||||
new_content: SerializableEventContent::new(
|
||||
&RoomMessageEventContent::text_plain("edit").into(),
|
||||
)
|
||||
@@ -1433,14 +1518,14 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(self.list_dependent_send_queue_events(room_id).await.unwrap().len(), 2);
|
||||
assert_eq!(self.load_dependent_queued_requests(room_id).await.unwrap().len(), 2);
|
||||
|
||||
// Remove event0 / txn0.
|
||||
let removed = self.remove_send_queue_event(room_id, &txn0).await.unwrap();
|
||||
let removed = self.remove_send_queue_request(room_id, &txn0).await.unwrap();
|
||||
assert!(removed);
|
||||
|
||||
// This has removed none of the dependent events.
|
||||
let dependents = self.list_dependent_send_queue_events(room_id).await.unwrap();
|
||||
let dependents = self.load_dependent_queued_requests(room_id).await.unwrap();
|
||||
assert_eq!(dependents.len(), 2);
|
||||
}
|
||||
}
|
||||
@@ -1590,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();
|
||||
|
||||
@@ -36,16 +36,15 @@ use ruma::{
|
||||
use tracing::{debug, instrument, trace, warn};
|
||||
|
||||
use super::{
|
||||
traits::{
|
||||
ChildTransactionId, ComposerDraft, QueuedEvent, SerializableEventContent,
|
||||
ServerCapabilities,
|
||||
},
|
||||
DependentQueuedEvent, DependentQueuedEventKind, Result, RoomInfo, StateChanges, StateStore,
|
||||
StoreError,
|
||||
send_queue::{ChildTransactionId, QueuedRequest, SentRequestKey},
|
||||
traits::{ComposerDraft, ServerCapabilities},
|
||||
DependentQueuedRequest, DependentQueuedRequestKind, QueuedRequestKind, Result, RoomInfo,
|
||||
StateChanges, StateStore, StoreError,
|
||||
};
|
||||
use crate::{
|
||||
deserialized_responses::RawAnySyncOrStrippedState, MinimalRoomMemberEvent, RoomMemberships,
|
||||
StateStoreDataKey, StateStoreDataValue,
|
||||
deserialized_responses::{DisplayName, RawAnySyncOrStrippedState},
|
||||
store::QueueWedgeError,
|
||||
MinimalRoomMemberEvent, RoomMemberships, StateStoreDataKey, StateStoreDataValue,
|
||||
};
|
||||
|
||||
/// In-memory, non-persistent implementation of the `StateStore`.
|
||||
@@ -63,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<
|
||||
@@ -90,8 +89,8 @@ pub struct MemoryStore {
|
||||
>,
|
||||
>,
|
||||
custom: StdRwLock<HashMap<Vec<u8>, Vec<u8>>>,
|
||||
send_queue_events: StdRwLock<BTreeMap<OwnedRoomId, Vec<QueuedEvent>>>,
|
||||
dependent_send_queue_events: StdRwLock<BTreeMap<OwnedRoomId, Vec<DependentQueuedEvent>>>,
|
||||
send_queue_events: StdRwLock<BTreeMap<OwnedRoomId, Vec<QueuedRequest>>>,
|
||||
dependent_send_queue_events: StdRwLock<BTreeMap<OwnedRoomId, Vec<DependentQueuedRequest>>>,
|
||||
}
|
||||
|
||||
impl MemoryStore {
|
||||
@@ -703,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
|
||||
@@ -717,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(
|
||||
@@ -804,26 +800,27 @@ impl StateStore for MemoryStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn save_send_queue_event(
|
||||
async fn save_send_queue_request(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
transaction_id: OwnedTransactionId,
|
||||
event: SerializableEventContent,
|
||||
kind: QueuedRequestKind,
|
||||
priority: usize,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.send_queue_events
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.push(QueuedEvent { event, transaction_id, is_wedged: false });
|
||||
.push(QueuedRequest { kind, transaction_id, error: None, priority });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_send_queue_event(
|
||||
async fn update_send_queue_request(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
transaction_id: &TransactionId,
|
||||
content: SerializableEventContent,
|
||||
kind: QueuedRequestKind,
|
||||
) -> Result<bool, Self::Error> {
|
||||
if let Some(entry) = self
|
||||
.send_queue_events
|
||||
@@ -834,15 +831,15 @@ impl StateStore for MemoryStore {
|
||||
.iter_mut()
|
||||
.find(|item| item.transaction_id == transaction_id)
|
||||
{
|
||||
entry.event = content;
|
||||
entry.is_wedged = false;
|
||||
entry.kind = kind;
|
||||
entry.error = None;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_send_queue_event(
|
||||
async fn remove_send_queue_request(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
transaction_id: &TransactionId,
|
||||
@@ -865,18 +862,22 @@ impl StateStore for MemoryStore {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn load_send_queue_events(
|
||||
async fn load_send_queue_requests(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Vec<QueuedEvent>, Self::Error> {
|
||||
Ok(self.send_queue_events.write().unwrap().entry(room_id.to_owned()).or_default().clone())
|
||||
) -> Result<Vec<QueuedRequest>, Self::Error> {
|
||||
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_event_status(
|
||||
async fn update_send_queue_request_status(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
transaction_id: &TransactionId,
|
||||
wedged: bool,
|
||||
error: Option<QueueWedgeError>,
|
||||
) -> Result<(), Self::Error> {
|
||||
if let Some(entry) = self
|
||||
.send_queue_events
|
||||
@@ -887,50 +888,50 @@ impl StateStore for MemoryStore {
|
||||
.iter_mut()
|
||||
.find(|item| item.transaction_id == transaction_id)
|
||||
{
|
||||
entry.is_wedged = wedged;
|
||||
entry.error = error;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_rooms_with_unsent_events(&self) -> Result<Vec<OwnedRoomId>, Self::Error> {
|
||||
async fn load_rooms_with_unsent_requests(&self) -> Result<Vec<OwnedRoomId>, Self::Error> {
|
||||
Ok(self.send_queue_events.read().unwrap().keys().cloned().collect())
|
||||
}
|
||||
|
||||
async fn save_dependent_send_queue_event(
|
||||
async fn save_dependent_queued_request(
|
||||
&self,
|
||||
room: &RoomId,
|
||||
parent_transaction_id: &TransactionId,
|
||||
own_transaction_id: ChildTransactionId,
|
||||
content: DependentQueuedEventKind,
|
||||
content: DependentQueuedRequestKind,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.dependent_send_queue_events.write().unwrap().entry(room.to_owned()).or_default().push(
|
||||
DependentQueuedEvent {
|
||||
DependentQueuedRequest {
|
||||
kind: content,
|
||||
parent_transaction_id: parent_transaction_id.to_owned(),
|
||||
own_transaction_id,
|
||||
event_id: None,
|
||||
parent_key: None,
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_dependent_send_queue_event(
|
||||
async fn update_dependent_queued_request(
|
||||
&self,
|
||||
room: &RoomId,
|
||||
parent_txn_id: &TransactionId,
|
||||
event_id: OwnedEventId,
|
||||
sent_parent_key: SentRequestKey,
|
||||
) -> Result<usize, Self::Error> {
|
||||
let mut dependent_send_queue_events = self.dependent_send_queue_events.write().unwrap();
|
||||
let dependents = dependent_send_queue_events.entry(room.to_owned()).or_default();
|
||||
let mut num_updated = 0;
|
||||
for d in dependents.iter_mut().filter(|item| item.parent_transaction_id == parent_txn_id) {
|
||||
d.event_id = Some(event_id.clone());
|
||||
d.parent_key = Some(sent_parent_key.clone());
|
||||
num_updated += 1;
|
||||
}
|
||||
Ok(num_updated)
|
||||
}
|
||||
|
||||
async fn remove_dependent_send_queue_event(
|
||||
async fn remove_dependent_queued_request(
|
||||
&self,
|
||||
room: &RoomId,
|
||||
txn_id: &ChildTransactionId,
|
||||
@@ -949,10 +950,10 @@ impl StateStore for MemoryStore {
|
||||
///
|
||||
/// This returns absolutely all the dependent send queue events, whether
|
||||
/// they have an event id or not.
|
||||
async fn list_dependent_send_queue_events(
|
||||
async fn load_dependent_queued_requests(
|
||||
&self,
|
||||
room: &RoomId,
|
||||
) -> Result<Vec<DependentQueuedEvent>, Self::Error> {
|
||||
) -> Result<Vec<DependentQueuedRequest>, Self::Error> {
|
||||
Ok(self.dependent_send_queue_events.read().unwrap().get(room).cloned().unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,7 @@ impl RoomInfoV1 {
|
||||
version: 0,
|
||||
room_id,
|
||||
room_state: room_type,
|
||||
prev_room_state: None,
|
||||
notification_counts,
|
||||
summary,
|
||||
members_synced,
|
||||
|
||||
@@ -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::{DynEventCacheStore, IntoEventCacheStore},
|
||||
deserialized_responses::DisplayName,
|
||||
event_cache::store as event_cache_store,
|
||||
rooms::{normal::RoomInfoNotableUpdate, RoomInfo, RoomState},
|
||||
MinimalRoomMemberEvent, Room, RoomStateFilter, SessionMeta,
|
||||
};
|
||||
@@ -68,16 +67,20 @@ use crate::{
|
||||
pub(crate) mod ambiguity_map;
|
||||
mod memory_store;
|
||||
pub mod migration_helpers;
|
||||
mod send_queue;
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
pub use self::integration_tests::StateStoreIntegrationTests;
|
||||
pub use self::{
|
||||
memory_store::MemoryStore,
|
||||
send_queue::{
|
||||
ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind,
|
||||
FinishUploadThumbnailInfo, QueueWedgeError, QueuedRequest, QueuedRequestKind,
|
||||
SentMediaInfo, SentRequestKey, SerializableEventContent,
|
||||
},
|
||||
traits::{
|
||||
ChildTransactionId, ComposerDraft, ComposerDraftType, DependentQueuedEvent,
|
||||
DependentQueuedEventKind, DynStateStore, IntoStateStore, QueuedEvent,
|
||||
SerializableEventContent, ServerCapabilities, StateStore, StateStoreDataKey,
|
||||
StateStoreDataValue, StateStoreExt,
|
||||
ComposerDraft, ComposerDraftType, DynStateStore, IntoStateStore, ServerCapabilities,
|
||||
StateStore, StateStoreDataKey, StateStoreDataValue, StateStoreExt,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -263,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()
|
||||
}
|
||||
@@ -304,6 +306,17 @@ impl Store {
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Forget the room with the given room ID.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The id of the room that should be forgotten.
|
||||
pub(crate) async fn forget_room(&self, room_id: &RoomId) -> Result<()> {
|
||||
self.inner.remove_room(room_id).await?;
|
||||
self.rooms.write().unwrap().remove(room_id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
@@ -372,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 {
|
||||
@@ -468,14 +481,16 @@ 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 {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub(crate) crypto_store: Arc<DynCryptoStore>,
|
||||
pub(crate) state_store: Arc<DynStateStore>,
|
||||
pub(crate) event_cache_store: Arc<DynEventCacheStore>,
|
||||
pub(crate) event_cache_store: event_cache_store::EventCacheStoreLock,
|
||||
cross_process_store_locks_holder_name: String,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
@@ -487,14 +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: crate::event_cache_store::MemoryStore::new()
|
||||
.into_event_cache_store(),
|
||||
event_cache_store: event_cache_store::EventCacheStoreLock::new(
|
||||
event_cache_store::MemoryStore::new(),
|
||||
cross_process_store_locks_holder_name.clone(),
|
||||
),
|
||||
cross_process_store_locks_holder_name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -514,14 +535,14 @@ impl StoreConfig {
|
||||
}
|
||||
|
||||
/// Set a custom implementation of an `EventCacheStore`.
|
||||
pub fn event_cache_store(mut self, event_cache_store: impl IntoEventCacheStore) -> Self {
|
||||
self.event_cache_store = event_cache_store.into_event_cache_store();
|
||||
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,
|
||||
self.cross_process_store_locks_holder_name.clone(),
|
||||
);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for StoreConfig {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,189 +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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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")
|
||||
}
|
||||
|
||||
/// 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;
|
||||
@@ -249,6 +197,33 @@ mod tests {
|
||||
assert_eq!(map.get(&'c'), Some(&'G'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove() {
|
||||
let mut map = ObservableMap::<char, char>::new();
|
||||
|
||||
assert!(map.get(&'a').is_none());
|
||||
assert!(map.get(&'b').is_none());
|
||||
assert!(map.get(&'c').is_none());
|
||||
|
||||
// new items
|
||||
map.insert('a', 'e');
|
||||
map.insert('b', 'f');
|
||||
|
||||
assert_eq!(map.get(&'a'), Some(&'e'));
|
||||
assert_eq!(map.get(&'b'), Some(&'f'));
|
||||
assert!(map.get(&'c').is_none());
|
||||
|
||||
// remove one item
|
||||
assert_eq!(map.remove(&'b'), Some('f'));
|
||||
|
||||
assert_eq!(map.get(&'a'), Some(&'e'));
|
||||
assert_eq!(map.get(&'b'), None);
|
||||
assert_eq!(map.get(&'c'), None);
|
||||
|
||||
// remove a non-existent item
|
||||
assert_eq!(map.remove(&'c'), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter() {
|
||||
let mut map = ObservableMap::<char, char>::new();
|
||||
@@ -264,7 +239,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn test_stream() {
|
||||
let mut map = ObservableMap::<char, char>::new();
|
||||
@@ -293,6 +267,12 @@ mod tests {
|
||||
|
||||
assert_pending!(stream);
|
||||
|
||||
// remove one item
|
||||
map.remove(&'b');
|
||||
assert_next_eq!(stream, vec![VectorDiff::Remove { index: 0 }]);
|
||||
|
||||
assert_pending!(stream);
|
||||
|
||||
drop(map);
|
||||
assert_closed!(stream);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
// 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.
|
||||
|
||||
//! All data types related to the send queue.
|
||||
|
||||
use std::{collections::BTreeMap, fmt, ops::Deref};
|
||||
|
||||
use as_variant::as_variant;
|
||||
use ruma::{
|
||||
events::{
|
||||
room::{message::RoomMessageEventContent, MediaSource},
|
||||
AnyMessageLikeEventContent, EventContent as _, RawExt as _,
|
||||
},
|
||||
serde::Raw,
|
||||
OwnedDeviceId, OwnedEventId, OwnedTransactionId, OwnedUserId, TransactionId, UInt,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::media::MediaRequestParameters;
|
||||
|
||||
/// A thin wrapper to serialize a `AnyMessageLikeEventContent`.
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct SerializableEventContent {
|
||||
event: Raw<AnyMessageLikeEventContent>,
|
||||
event_type: String,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for SerializableEventContent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// Don't include the event in the debug display.
|
||||
f.debug_struct("SerializedEventContent")
|
||||
.field("event_type", &self.event_type)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializableEventContent {
|
||||
/// Create a [`SerializableEventContent`] from a raw
|
||||
/// [`AnyMessageLikeEventContent`] along with its type.
|
||||
pub fn from_raw(event: Raw<AnyMessageLikeEventContent>, event_type: String) -> Self {
|
||||
Self { event_type, event }
|
||||
}
|
||||
|
||||
/// Create a [`SerializableEventContent`] from an
|
||||
/// [`AnyMessageLikeEventContent`].
|
||||
pub fn new(event: &AnyMessageLikeEventContent) -> Result<Self, serde_json::Error> {
|
||||
Ok(Self::from_raw(Raw::new(event)?, event.event_type().to_string()))
|
||||
}
|
||||
|
||||
/// Convert a [`SerializableEventContent`] back into a
|
||||
/// [`AnyMessageLikeEventContent`].
|
||||
pub fn deserialize(&self) -> Result<AnyMessageLikeEventContent, serde_json::Error> {
|
||||
self.event.deserialize_with_type(self.event_type.clone().into())
|
||||
}
|
||||
|
||||
/// Returns the raw event content along with its type.
|
||||
///
|
||||
/// Useful for callers manipulating custom events.
|
||||
pub fn raw(&self) -> (&Raw<AnyMessageLikeEventContent>, &str) {
|
||||
(&self.event, &self.event_type)
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind of a send queue request.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum QueuedRequestKind {
|
||||
/// An event to be sent via the send queue.
|
||||
Event {
|
||||
/// The content of the message-like event we'd like to send.
|
||||
content: SerializableEventContent,
|
||||
},
|
||||
|
||||
/// Content to upload on the media server.
|
||||
///
|
||||
/// The bytes must be stored in the media cache, and are identified by the
|
||||
/// cache key.
|
||||
MediaUpload {
|
||||
/// Content type of the media to be uploaded.
|
||||
///
|
||||
/// Stored as a `String` because `Mime` which we'd really want to use
|
||||
/// here, is not serializable. Oh well.
|
||||
content_type: String,
|
||||
|
||||
/// The cache key used to retrieve the media's bytes in the event cache
|
||||
/// store.
|
||||
cache_key: MediaRequestParameters,
|
||||
|
||||
/// An optional media source for a thumbnail already uploaded.
|
||||
thumbnail_source: Option<MediaSource>,
|
||||
|
||||
/// To which media event transaction does this upload relate?
|
||||
related_to: OwnedTransactionId,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<SerializableEventContent> for QueuedRequestKind {
|
||||
fn from(content: SerializableEventContent) -> Self {
|
||||
Self::Event { content }
|
||||
}
|
||||
}
|
||||
|
||||
/// A request to be sent with a send queue.
|
||||
#[derive(Clone)]
|
||||
pub struct QueuedRequest {
|
||||
/// The kind of queued request we're going to send.
|
||||
pub kind: QueuedRequestKind,
|
||||
|
||||
/// Unique transaction id for the queued request, acting as a key.
|
||||
pub transaction_id: OwnedTransactionId,
|
||||
|
||||
/// Error returned when the request couldn't be sent and is stuck in the
|
||||
/// unrecoverable state.
|
||||
///
|
||||
/// `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 {
|
||||
/// Returns `Some` if the queued request is about sending an event.
|
||||
pub fn as_event(&self) -> Option<&SerializableEventContent> {
|
||||
as_variant!(&self.kind, QueuedRequestKind::Event { content } => content)
|
||||
}
|
||||
|
||||
/// True if the request couldn't be sent because of an unrecoverable API
|
||||
/// error. See [`Self::error`] for more details on the reason.
|
||||
pub fn is_wedged(&self) -> bool {
|
||||
self.error.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a failed to send unrecoverable error of an event sent via the
|
||||
/// send queue.
|
||||
///
|
||||
/// It is a serializable representation of a client error, see
|
||||
/// `From` implementation for more details. These errors can not be
|
||||
/// automatically retried, but yet some manual action can be taken before retry
|
||||
/// sending. If not the only solution is to delete the local event.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)]
|
||||
pub enum QueueWedgeError {
|
||||
/// This error occurs when there are some insecure devices in the room, and
|
||||
/// the current encryption setting prohibits sharing with them.
|
||||
#[error("There are insecure devices in the room")]
|
||||
InsecureDevices {
|
||||
/// The insecure devices as a Map of userID to deviceID.
|
||||
user_device_map: BTreeMap<OwnedUserId, Vec<OwnedDeviceId>>,
|
||||
},
|
||||
|
||||
/// This error occurs when a previously verified user is not anymore, and
|
||||
/// the current encryption setting prohibits sharing when it happens.
|
||||
#[error("Some users that were previously verified are not anymore")]
|
||||
IdentityViolations {
|
||||
/// The users that are expected to be verified but are not.
|
||||
users: Vec<OwnedUserId>,
|
||||
},
|
||||
|
||||
/// It is required to set up cross-signing and properly verify the current
|
||||
/// session before sending.
|
||||
#[error("Own verification is required")]
|
||||
CrossVerificationRequired,
|
||||
|
||||
/// Media content was cached in the media store, but has disappeared before
|
||||
/// we could upload it.
|
||||
#[error("Media content disappeared")]
|
||||
MissingMediaContent,
|
||||
|
||||
/// We tried to upload some media content with an unknown mime type.
|
||||
#[error("Invalid mime type '{mime_type}' for media")]
|
||||
InvalidMimeType {
|
||||
/// The observed mime type that's expected to be invalid.
|
||||
mime_type: String,
|
||||
},
|
||||
|
||||
/// Other errors.
|
||||
#[error("Other unrecoverable error: {msg}")]
|
||||
GenericApiError {
|
||||
/// Description of the error.
|
||||
msg: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// The specific user intent that characterizes a
|
||||
/// [`DependentQueuedRequestKind`].
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum DependentQueuedRequestKind {
|
||||
/// The event should be edited.
|
||||
EditEvent {
|
||||
/// The new event for the content.
|
||||
new_content: SerializableEventContent,
|
||||
},
|
||||
|
||||
/// The event should be redacted/aborted/removed.
|
||||
RedactEvent,
|
||||
|
||||
/// The event should be reacted to, with the given key.
|
||||
ReactEvent {
|
||||
/// Key used for the reaction.
|
||||
key: String,
|
||||
},
|
||||
|
||||
/// Upload a file that had a thumbnail.
|
||||
UploadFileWithThumbnail {
|
||||
/// Content type for the file itself (not the thumbnail).
|
||||
content_type: String,
|
||||
|
||||
/// Media request necessary to retrieve the file itself (not the
|
||||
/// thumbnail).
|
||||
cache_key: MediaRequestParameters,
|
||||
|
||||
/// To which media transaction id does this upload relate to?
|
||||
related_to: OwnedTransactionId,
|
||||
},
|
||||
|
||||
/// Finish an upload by updating references to the media cache and sending
|
||||
/// the final media event with the remote MXC URIs.
|
||||
FinishUpload {
|
||||
/// Local echo for the event (containing the local MXC URIs).
|
||||
local_echo: RoomMessageEventContent,
|
||||
|
||||
/// Transaction id for the file upload.
|
||||
file_upload: OwnedTransactionId,
|
||||
|
||||
/// Information about the thumbnail, if present.
|
||||
thumbnail_info: Option<FinishUploadThumbnailInfo>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Detailed record about a thumbnail used when finishing a media upload.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct FinishUploadThumbnailInfo {
|
||||
/// Transaction id for the thumbnail upload.
|
||||
pub txn: OwnedTransactionId,
|
||||
/// Thumbnail's width.
|
||||
pub width: UInt,
|
||||
/// Thumbnail's height.
|
||||
pub height: UInt,
|
||||
}
|
||||
|
||||
/// A transaction id identifying a [`DependentQueuedRequest`] rather than its
|
||||
/// parent [`QueuedRequest`].
|
||||
///
|
||||
/// This thin wrapper adds some safety to some APIs, making it possible to
|
||||
/// distinguish between the parent's `TransactionId` and the dependent event's
|
||||
/// own `TransactionId`.
|
||||
#[repr(transparent)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct ChildTransactionId(OwnedTransactionId);
|
||||
|
||||
impl ChildTransactionId {
|
||||
/// Returns a new [`ChildTransactionId`].
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
Self(TransactionId::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ChildTransactionId {
|
||||
type Target = TransactionId;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ChildTransactionId {
|
||||
fn from(val: String) -> Self {
|
||||
Self(val.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ChildTransactionId> for OwnedTransactionId {
|
||||
fn from(val: ChildTransactionId) -> Self {
|
||||
val.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OwnedTransactionId> for ChildTransactionId {
|
||||
fn from(val: OwnedTransactionId) -> Self {
|
||||
Self(val)
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about a media (and its thumbnail) that have been sent to an
|
||||
/// homeserver.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SentMediaInfo {
|
||||
/// File that was uploaded by this request.
|
||||
///
|
||||
/// If the request related to a thumbnail upload, this contains the
|
||||
/// thumbnail media source.
|
||||
pub file: MediaSource,
|
||||
|
||||
/// Optional thumbnail previously uploaded, when uploading a file.
|
||||
///
|
||||
/// When uploading a thumbnail, this is set to `None`.
|
||||
pub thumbnail: Option<MediaSource>,
|
||||
}
|
||||
|
||||
/// A unique key (identifier) indicating that a transaction has been
|
||||
/// successfully sent to the server.
|
||||
///
|
||||
/// The owning child transactions can now be resolved.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum SentRequestKey {
|
||||
/// The parent transaction returned an event when it succeeded.
|
||||
Event(OwnedEventId),
|
||||
|
||||
/// The parent transaction returned an uploaded resource URL.
|
||||
Media(SentMediaInfo),
|
||||
}
|
||||
|
||||
impl SentRequestKey {
|
||||
/// Converts the current parent key into an event id, if possible.
|
||||
pub fn into_event_id(self) -> Option<OwnedEventId> {
|
||||
as_variant!(self, Self::Event)
|
||||
}
|
||||
|
||||
/// Converts the current parent key into information about a sent media, if
|
||||
/// possible.
|
||||
pub fn into_media(self) -> Option<SentMediaInfo> {
|
||||
as_variant!(self, Self::Media)
|
||||
}
|
||||
}
|
||||
|
||||
/// A request to be sent, depending on a [`QueuedRequest`] to be sent first.
|
||||
///
|
||||
/// Depending on whether the parent request has been sent or not, this will
|
||||
/// either update the local echo in the storage, or materialize an equivalent
|
||||
/// request implementing the user intent to the homeserver.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct DependentQueuedRequest {
|
||||
/// Unique identifier for this dependent queued request.
|
||||
///
|
||||
/// Useful for deletion.
|
||||
pub own_transaction_id: ChildTransactionId,
|
||||
|
||||
/// The kind of user intent.
|
||||
pub kind: DependentQueuedRequestKind,
|
||||
|
||||
/// Transaction id for the parent's local echo / used in the server request.
|
||||
///
|
||||
/// Note: this is the transaction id used for the depended-on request, i.e.
|
||||
/// the one that was originally sent and that's being modified with this
|
||||
/// dependent request.
|
||||
pub parent_transaction_id: OwnedTransactionId,
|
||||
|
||||
/// If the parent request has been sent, the parent's request identifier
|
||||
/// returned by the server once the local echo has been sent out.
|
||||
pub parent_key: Option<SentRequestKey>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for QueuedRequest {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// Hide the content from the debug log.
|
||||
f.debug_struct("QueuedRequest")
|
||||
.field("transaction_id", &self.transaction_id)
|
||||
.field("is_wedged", &self.is_wedged())
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,8 @@
|
||||
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
fmt,
|
||||
ops::Deref,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
@@ -29,11 +28,10 @@ use ruma::{
|
||||
events::{
|
||||
presence::PresenceEvent,
|
||||
receipt::{Receipt, ReceiptThread, ReceiptType},
|
||||
AnyGlobalAccountDataEvent, AnyMessageLikeEventContent, AnyRoomAccountDataEvent,
|
||||
EmptyStateKey, EventContent as _, GlobalAccountDataEvent, GlobalAccountDataEventContent,
|
||||
GlobalAccountDataEventType, RawExt as _, RedactContent, RedactedStateEventContent,
|
||||
RoomAccountDataEvent, RoomAccountDataEventContent, RoomAccountDataEventType,
|
||||
StateEventType, StaticEventContent, StaticStateEventContent,
|
||||
AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, EmptyStateKey, GlobalAccountDataEvent,
|
||||
GlobalAccountDataEventContent, GlobalAccountDataEventType, RedactContent,
|
||||
RedactedStateEventContent, RoomAccountDataEvent, RoomAccountDataEventContent,
|
||||
RoomAccountDataEventType, StateEventType, StaticEventContent, StaticStateEventContent,
|
||||
},
|
||||
serde::Raw,
|
||||
time::SystemTime,
|
||||
@@ -42,9 +40,15 @@ use ruma::{
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{StateChanges, StoreError};
|
||||
use super::{
|
||||
send_queue::SentRequestKey, ChildTransactionId, DependentQueuedRequest,
|
||||
DependentQueuedRequestKind, QueueWedgeError, QueuedRequest, QueuedRequestKind, StateChanges,
|
||||
StoreError,
|
||||
};
|
||||
use crate::{
|
||||
deserialized_responses::{RawAnySyncOrStrippedState, RawMemberEvent, RawSyncOrStrippedState},
|
||||
deserialized_responses::{
|
||||
DisplayName, RawAnySyncOrStrippedState, RawMemberEvent, RawSyncOrStrippedState,
|
||||
},
|
||||
MinimalRoomMemberEvent, RoomInfo, RoomMemberships,
|
||||
};
|
||||
|
||||
@@ -204,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.
|
||||
@@ -217,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.
|
||||
///
|
||||
@@ -342,7 +346,7 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
/// * `room_id` - The `RoomId` of the room to delete.
|
||||
async fn remove_room(&self, room_id: &RoomId) -> Result<(), Self::Error>;
|
||||
|
||||
/// Save an event to be sent by a send queue later.
|
||||
/// Save a request to be sent by a send queue later (e.g. sending an event).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@@ -351,95 +355,109 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
/// (and its transaction). Note: this is expected to be randomly generated
|
||||
/// and thus unique.
|
||||
/// * `content` - Serializable event content to be sent.
|
||||
async fn save_send_queue_event(
|
||||
async fn save_send_queue_request(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
transaction_id: OwnedTransactionId,
|
||||
content: SerializableEventContent,
|
||||
request: QueuedRequestKind,
|
||||
priority: usize,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Updates a send queue event with the given content, and resets its wedged
|
||||
/// status to false.
|
||||
/// Updates a send queue request with the given content, and resets its
|
||||
/// error status.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The `RoomId` of the send queue's room.
|
||||
/// * `transaction_id` - The unique key identifying the event to be sent
|
||||
/// * `transaction_id` - The unique key identifying the request to be sent
|
||||
/// (and its transaction).
|
||||
/// * `content` - Serializable event content to replace the original one.
|
||||
///
|
||||
/// Returns true if an event has been updated, or false otherwise.
|
||||
async fn update_send_queue_event(
|
||||
/// Returns true if a request has been updated, or false otherwise.
|
||||
async fn update_send_queue_request(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
transaction_id: &TransactionId,
|
||||
content: SerializableEventContent,
|
||||
content: QueuedRequestKind,
|
||||
) -> Result<bool, Self::Error>;
|
||||
|
||||
/// Remove an event previously inserted with [`Self::save_send_queue_event`]
|
||||
/// from the database, based on its transaction id.
|
||||
/// Remove a request previously inserted with
|
||||
/// [`Self::save_send_queue_request`] from the database, based on its
|
||||
/// transaction id.
|
||||
///
|
||||
/// Returns true if an event has been removed, or false otherwise.
|
||||
async fn remove_send_queue_event(
|
||||
/// Returns true if something has been removed, or false otherwise.
|
||||
async fn remove_send_queue_request(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
transaction_id: &TransactionId,
|
||||
) -> Result<bool, Self::Error>;
|
||||
|
||||
/// Loads all the send queue events for the given room.
|
||||
async fn load_send_queue_events(
|
||||
/// 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,
|
||||
) -> Result<Vec<QueuedEvent>, Self::Error>;
|
||||
) -> Result<Vec<QueuedRequest>, Self::Error>;
|
||||
|
||||
/// Updates the send queue wedged status for a given send queue event.
|
||||
async fn update_send_queue_event_status(
|
||||
/// Updates the send queue error status (wedge) for a given send queue
|
||||
/// request.
|
||||
async fn update_send_queue_request_status(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
transaction_id: &TransactionId,
|
||||
wedged: bool,
|
||||
error: Option<QueueWedgeError>,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Loads all the rooms which have any pending events in their send queue.
|
||||
async fn load_rooms_with_unsent_events(&self) -> Result<Vec<OwnedRoomId>, Self::Error>;
|
||||
/// Loads all the rooms which have any pending requests in their send queue.
|
||||
async fn load_rooms_with_unsent_requests(&self) -> Result<Vec<OwnedRoomId>, Self::Error>;
|
||||
|
||||
/// Add a new entry to the list of dependent send queue event for an event.
|
||||
async fn save_dependent_send_queue_event(
|
||||
/// Add a new entry to the list of dependent send queue requests for a
|
||||
/// parent request.
|
||||
async fn save_dependent_queued_request(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
parent_txn_id: &TransactionId,
|
||||
own_txn_id: ChildTransactionId,
|
||||
content: DependentQueuedEventKind,
|
||||
content: DependentQueuedRequestKind,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Update a set of dependent send queue events with an event id,
|
||||
/// effectively marking them as ready.
|
||||
/// Update a set of dependent send queue requests with a key identifying the
|
||||
/// homeserver's response, effectively marking them as ready.
|
||||
///
|
||||
/// Returns the number of updated events.
|
||||
async fn update_dependent_send_queue_event(
|
||||
/// ⚠ Beware! There's no verification applied that the parent key type is
|
||||
/// compatible with the dependent event type. The invalid state may be
|
||||
/// lazily filtered out in `load_dependent_queued_requests`.
|
||||
///
|
||||
/// Returns the number of updated requests.
|
||||
async fn update_dependent_queued_request(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
parent_txn_id: &TransactionId,
|
||||
event_id: OwnedEventId,
|
||||
sent_parent_key: SentRequestKey,
|
||||
) -> Result<usize, Self::Error>;
|
||||
|
||||
/// Remove a specific dependent send queue event by id.
|
||||
/// Remove a specific dependent send queue request by id.
|
||||
///
|
||||
/// Returns true if the dependent send queue event has been indeed removed.
|
||||
async fn remove_dependent_send_queue_event(
|
||||
/// Returns true if the dependent send queue request has been indeed
|
||||
/// removed.
|
||||
async fn remove_dependent_queued_request(
|
||||
&self,
|
||||
room: &RoomId,
|
||||
own_txn_id: &ChildTransactionId,
|
||||
) -> Result<bool, Self::Error>;
|
||||
|
||||
/// List all the dependent send queue events.
|
||||
/// List all the dependent send queue requests.
|
||||
///
|
||||
/// This returns absolutely all the dependent send queue events, whether
|
||||
/// they have an event id or not. They must be returned in insertion order.
|
||||
async fn list_dependent_send_queue_events(
|
||||
/// This returns absolutely all the dependent send queue requests, whether
|
||||
/// they have a parent event id or not. As a contract for implementors, they
|
||||
/// must be returned in insertion order.
|
||||
async fn load_dependent_queued_requests(
|
||||
&self,
|
||||
room: &RoomId,
|
||||
) -> Result<Vec<DependentQueuedEvent>, Self::Error>;
|
||||
) -> Result<Vec<DependentQueuedRequest>, Self::Error>;
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
@@ -551,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)
|
||||
}
|
||||
@@ -559,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)
|
||||
}
|
||||
|
||||
@@ -625,93 +643,97 @@ impl<T: StateStore> StateStore for EraseStateStoreError<T> {
|
||||
self.0.remove_room(room_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn save_send_queue_event(
|
||||
async fn save_send_queue_request(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
transaction_id: OwnedTransactionId,
|
||||
content: SerializableEventContent,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.save_send_queue_event(room_id, transaction_id, content).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn update_send_queue_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
transaction_id: &TransactionId,
|
||||
content: SerializableEventContent,
|
||||
) -> Result<bool, Self::Error> {
|
||||
self.0.update_send_queue_event(room_id, transaction_id, content).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn remove_send_queue_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
transaction_id: &TransactionId,
|
||||
) -> Result<bool, Self::Error> {
|
||||
self.0.remove_send_queue_event(room_id, transaction_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn load_send_queue_events(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Vec<QueuedEvent>, Self::Error> {
|
||||
self.0.load_send_queue_events(room_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn update_send_queue_event_status(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
transaction_id: &TransactionId,
|
||||
wedged: bool,
|
||||
content: QueuedRequestKind,
|
||||
priority: usize,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0
|
||||
.update_send_queue_event_status(room_id, transaction_id, wedged)
|
||||
.save_send_queue_request(room_id, transaction_id, content, priority)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn load_rooms_with_unsent_events(&self) -> Result<Vec<OwnedRoomId>, Self::Error> {
|
||||
self.0.load_rooms_with_unsent_events().await.map_err(Into::into)
|
||||
async fn update_send_queue_request(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
transaction_id: &TransactionId,
|
||||
content: QueuedRequestKind,
|
||||
) -> Result<bool, Self::Error> {
|
||||
self.0.update_send_queue_request(room_id, transaction_id, content).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn save_dependent_send_queue_event(
|
||||
async fn remove_send_queue_request(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
transaction_id: &TransactionId,
|
||||
) -> Result<bool, Self::Error> {
|
||||
self.0.remove_send_queue_request(room_id, transaction_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn load_send_queue_requests(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Vec<QueuedRequest>, Self::Error> {
|
||||
self.0.load_send_queue_requests(room_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn update_send_queue_request_status(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
transaction_id: &TransactionId,
|
||||
error: Option<QueueWedgeError>,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0
|
||||
.update_send_queue_request_status(room_id, transaction_id, error)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn load_rooms_with_unsent_requests(&self) -> Result<Vec<OwnedRoomId>, Self::Error> {
|
||||
self.0.load_rooms_with_unsent_requests().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn save_dependent_queued_request(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
parent_txn_id: &TransactionId,
|
||||
own_txn_id: ChildTransactionId,
|
||||
content: DependentQueuedEventKind,
|
||||
content: DependentQueuedRequestKind,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0
|
||||
.save_dependent_send_queue_event(room_id, parent_txn_id, own_txn_id, content)
|
||||
.save_dependent_queued_request(room_id, parent_txn_id, own_txn_id, content)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn update_dependent_send_queue_event(
|
||||
async fn update_dependent_queued_request(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
parent_txn_id: &TransactionId,
|
||||
event_id: OwnedEventId,
|
||||
sent_parent_key: SentRequestKey,
|
||||
) -> Result<usize, Self::Error> {
|
||||
self.0
|
||||
.update_dependent_send_queue_event(room_id, parent_txn_id, event_id)
|
||||
.update_dependent_queued_request(room_id, parent_txn_id, sent_parent_key)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn remove_dependent_send_queue_event(
|
||||
async fn remove_dependent_queued_request(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
own_txn_id: &ChildTransactionId,
|
||||
) -> Result<bool, Self::Error> {
|
||||
self.0.remove_dependent_send_queue_event(room_id, own_txn_id).await.map_err(Into::into)
|
||||
self.0.remove_dependent_queued_request(room_id, own_txn_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn list_dependent_send_queue_events(
|
||||
async fn load_dependent_queued_requests(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Vec<DependentQueuedEvent>, Self::Error> {
|
||||
self.0.list_dependent_send_queue_events(room_id).await.map_err(Into::into)
|
||||
) -> Result<Vec<DependentQueuedRequest>, Self::Error> {
|
||||
self.0.load_dependent_queued_requests(room_id).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1100,165 +1122,6 @@ impl StateStoreDataKey<'_> {
|
||||
pub const COMPOSER_DRAFT: &'static str = "composer_draft";
|
||||
}
|
||||
|
||||
/// A thin wrapper to serialize a `AnyMessageLikeEventContent`.
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct SerializableEventContent {
|
||||
event: Raw<AnyMessageLikeEventContent>,
|
||||
event_type: String,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for SerializableEventContent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// Don't include the event in the debug display.
|
||||
f.debug_struct("SerializedEventContent")
|
||||
.field("event_type", &self.event_type)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializableEventContent {
|
||||
/// Create a [`SerializableEventContent`] from a raw
|
||||
/// [`AnyMessageLikeEventContent`] along with its type.
|
||||
pub fn from_raw(event: Raw<AnyMessageLikeEventContent>, event_type: String) -> Self {
|
||||
Self { event_type, event }
|
||||
}
|
||||
|
||||
/// Create a [`SerializableEventContent`] from an
|
||||
/// [`AnyMessageLikeEventContent`].
|
||||
pub fn new(event: &AnyMessageLikeEventContent) -> Result<Self, serde_json::Error> {
|
||||
Ok(Self::from_raw(Raw::new(event)?, event.event_type().to_string()))
|
||||
}
|
||||
|
||||
/// Convert a [`SerializableEventContent`] back into a
|
||||
/// [`AnyMessageLikeEventContent`].
|
||||
pub fn deserialize(&self) -> Result<AnyMessageLikeEventContent, serde_json::Error> {
|
||||
self.event.deserialize_with_type(self.event_type.clone().into())
|
||||
}
|
||||
|
||||
/// Returns the raw event content along with its type.
|
||||
///
|
||||
/// Useful for callers manipulating custom events.
|
||||
pub fn raw(self) -> (Raw<AnyMessageLikeEventContent>, String) {
|
||||
(self.event, self.event_type)
|
||||
}
|
||||
}
|
||||
|
||||
/// An event to be sent with a send queue.
|
||||
#[derive(Clone)]
|
||||
pub struct QueuedEvent {
|
||||
/// The content of the message-like event we'd like to send.
|
||||
pub event: SerializableEventContent,
|
||||
|
||||
/// Unique transaction id for the queued event, acting as a key.
|
||||
pub transaction_id: OwnedTransactionId,
|
||||
|
||||
/// If the event couldn't be sent because of an API error, it's marked as
|
||||
/// wedged, and won't ever be peeked for sending. The only option is to
|
||||
/// remove it.
|
||||
pub is_wedged: bool,
|
||||
}
|
||||
|
||||
/// The specific user intent that characterizes a [`DependentQueuedEvent`].
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum DependentQueuedEventKind {
|
||||
/// The event should be edited.
|
||||
Edit {
|
||||
/// The new event for the content.
|
||||
new_content: SerializableEventContent,
|
||||
},
|
||||
|
||||
/// The event should be redacted/aborted/removed.
|
||||
Redact,
|
||||
|
||||
/// The event should be reacted to, with the given key.
|
||||
React {
|
||||
/// Key used for the reaction.
|
||||
key: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// A transaction id identifying a [`DependentQueuedEvent`] rather than its
|
||||
/// parent [`QueuedEvent`].
|
||||
///
|
||||
/// This thin wrapper adds some safety to some APIs, making it possible to
|
||||
/// distinguish between the parent's `TransactionId` and the dependent event's
|
||||
/// own `TransactionId`.
|
||||
#[repr(transparent)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct ChildTransactionId(OwnedTransactionId);
|
||||
|
||||
impl ChildTransactionId {
|
||||
/// Returns a new [`ChildTransactionId`].
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
Self(TransactionId::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ChildTransactionId {
|
||||
type Target = TransactionId;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ChildTransactionId {
|
||||
fn from(val: String) -> Self {
|
||||
Self(val.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ChildTransactionId> for OwnedTransactionId {
|
||||
fn from(val: ChildTransactionId) -> Self {
|
||||
val.0
|
||||
}
|
||||
}
|
||||
|
||||
/// An event to be sent, depending on a [`QueuedEvent`] to be sent first.
|
||||
///
|
||||
/// Depending on whether the event has been sent or not, this will either update
|
||||
/// the local echo in the storage, or send an event equivalent to the user
|
||||
/// intent to the homeserver.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct DependentQueuedEvent {
|
||||
/// Unique identifier for this dependent queued event.
|
||||
///
|
||||
/// Useful for deletion.
|
||||
pub own_transaction_id: ChildTransactionId,
|
||||
|
||||
/// The kind of user intent.
|
||||
pub kind: DependentQueuedEventKind,
|
||||
|
||||
/// Transaction id for the parent's local echo / used in the server request.
|
||||
///
|
||||
/// Note: this is the transaction id used for the depended-on event, i.e.
|
||||
/// the one that was originally sent and that's being modified with this
|
||||
/// dependent event.
|
||||
pub parent_transaction_id: OwnedTransactionId,
|
||||
|
||||
/// If the parent event has been sent, the parent's event identifier
|
||||
/// returned by the server once the local echo has been sent out.
|
||||
///
|
||||
/// Note: this is the event id used for the depended-on event after it's
|
||||
/// been sent, not for a possible event that could have been sent
|
||||
/// because of this [`DependentQueuedEvent`].
|
||||
pub event_id: Option<OwnedEventId>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for QueuedEvent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// Hide the content from the debug log.
|
||||
f.debug_struct("QueuedEvent")
|
||||
.field("transaction_id", &self.transaction_id)
|
||||
.field("is_wedged", &self.is_wedged)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{now_timestamp_ms, ServerCapabilities};
|
||||
|
||||
@@ -19,7 +19,7 @@ use std::{collections::BTreeMap, fmt};
|
||||
use matrix_sdk_common::{debug::DebugRawEvent, deserialized_responses::SyncTimelineEvent};
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::{
|
||||
v3::InvitedRoom as InvitedRoomUpdate,
|
||||
v3::{InvitedRoom as InvitedRoomUpdate, KnockedRoom as KnockedRoomUpdate},
|
||||
UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
},
|
||||
events::{
|
||||
@@ -33,7 +33,7 @@ use ruma::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
debug::{DebugInvitedRoom, DebugListOfRawEvents, DebugListOfRawEventsNoId},
|
||||
debug::{DebugInvitedRoom, DebugKnockedRoom, DebugListOfRawEvents, DebugListOfRawEventsNoId},
|
||||
deserialized_responses::{AmbiguityChange, RawAnySyncOrStrippedTimelineEvent},
|
||||
store::Store,
|
||||
};
|
||||
@@ -77,6 +77,8 @@ pub struct RoomUpdates {
|
||||
pub join: BTreeMap<OwnedRoomId, JoinedRoomUpdate>,
|
||||
/// The rooms that the user has been invited to.
|
||||
pub invite: BTreeMap<OwnedRoomId, InvitedRoomUpdate>,
|
||||
/// The rooms that the user has knocked on.
|
||||
pub knocked: BTreeMap<OwnedRoomId, KnockedRoomUpdate>,
|
||||
}
|
||||
|
||||
impl RoomUpdates {
|
||||
@@ -89,6 +91,7 @@ impl RoomUpdates {
|
||||
.keys()
|
||||
.chain(self.join.keys())
|
||||
.chain(self.invite.keys())
|
||||
.chain(self.knocked.keys())
|
||||
.filter_map(|room_id| store.room(room_id))
|
||||
{
|
||||
let _ = room.compute_display_name().await;
|
||||
@@ -103,6 +106,7 @@ impl fmt::Debug for RoomUpdates {
|
||||
.field("leave", &self.leave)
|
||||
.field("join", &self.join)
|
||||
.field("invite", &DebugInvitedRoomUpdates(&self.invite))
|
||||
.field("knocked", &DebugKnockedRoomUpdates(&self.knocked))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -250,6 +254,15 @@ impl<'a> fmt::Debug for DebugInvitedRoomUpdates<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
struct DebugKnockedRoomUpdates<'a>(&'a BTreeMap<OwnedRoomId, KnockedRoomUpdate>);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugKnockedRoomUpdates<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_map().entries(self.0.iter().map(|(k, v)| (k, DebugKnockedRoom(v)))).finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A notification triggered by a sync response.
|
||||
#[derive(Clone)]
|
||||
pub struct Notification {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
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 }
|
||||
|
||||
@@ -297,87 +297,77 @@ pub struct EncryptionInfo {
|
||||
pub verification_state: VerificationState,
|
||||
}
|
||||
|
||||
/// A customized version of a room event coming from a sync that holds optional
|
||||
/// encryption info.
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
/// Represents a matrix room event that has been returned from `/sync`,
|
||||
/// after initial processing.
|
||||
///
|
||||
/// Previously, this differed from [`TimelineEvent`] by wrapping an
|
||||
/// [`AnySyncTimelineEvent`] instead of an [`AnyTimelineEvent`], but nowadays
|
||||
/// they are essentially identical, and one of them should probably be removed.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct SyncTimelineEvent {
|
||||
/// The actual event.
|
||||
pub event: Raw<AnySyncTimelineEvent>,
|
||||
/// The encryption info about the event. Will be `None` if the event was not
|
||||
/// encrypted.
|
||||
pub encryption_info: Option<EncryptionInfo>,
|
||||
/// The event itself, together with any information on decryption.
|
||||
pub kind: TimelineEventKind,
|
||||
|
||||
/// The push actions associated with this event.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub push_actions: Vec<Action>,
|
||||
/// The encryption info about the events bundled in the `unsigned` object.
|
||||
///
|
||||
/// Will be `None` if no bundled event was encrypted.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub unsigned_encryption_info: Option<BTreeMap<UnsignedEventLocation, UnsignedDecryptionResult>>,
|
||||
}
|
||||
|
||||
impl SyncTimelineEvent {
|
||||
/// Create a new `SyncTimelineEvent` from the given raw event.
|
||||
///
|
||||
/// This is a convenience constructor for when you don't need to set
|
||||
/// `encryption_info` or `push_action`, for example inside a test.
|
||||
/// This is a convenience constructor for a plaintext event when you don't
|
||||
/// need to set `push_action`, for example inside a test.
|
||||
pub fn new(event: Raw<AnySyncTimelineEvent>) -> Self {
|
||||
Self { event, encryption_info: None, push_actions: vec![], unsigned_encryption_info: None }
|
||||
Self { kind: TimelineEventKind::PlainText { event }, push_actions: vec![] }
|
||||
}
|
||||
|
||||
/// Create a new `SyncTimelineEvent` from the given raw event and push
|
||||
/// actions.
|
||||
///
|
||||
/// This is a convenience constructor for when you don't need to set
|
||||
/// `encryption_info`, for example inside a test.
|
||||
/// This is a convenience constructor for a plaintext event, for example
|
||||
/// inside a test.
|
||||
pub fn new_with_push_actions(
|
||||
event: Raw<AnySyncTimelineEvent>,
|
||||
push_actions: Vec<Action>,
|
||||
) -> Self {
|
||||
Self { event, encryption_info: None, push_actions, unsigned_encryption_info: None }
|
||||
Self { kind: TimelineEventKind::PlainText { event }, push_actions }
|
||||
}
|
||||
|
||||
/// Create a new `SyncTimelineEvent` to represent the given decryption
|
||||
/// failure.
|
||||
pub fn new_utd_event(event: Raw<AnySyncTimelineEvent>, utd_info: UnableToDecryptInfo) -> Self {
|
||||
Self { kind: TimelineEventKind::UnableToDecrypt { event, utd_info }, push_actions: vec![] }
|
||||
}
|
||||
|
||||
/// Get the event id of this `SyncTimelineEvent` if the event has any valid
|
||||
/// id.
|
||||
pub fn event_id(&self) -> Option<OwnedEventId> {
|
||||
self.event.get_field::<OwnedEventId>("event_id").ok().flatten()
|
||||
self.kind.event_id()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for SyncTimelineEvent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let SyncTimelineEvent { event, encryption_info, push_actions, unsigned_encryption_info } =
|
||||
self;
|
||||
let mut s = f.debug_struct("SyncTimelineEvent");
|
||||
s.field("event", &DebugRawEvent(event));
|
||||
s.maybe_field("encryption_info", encryption_info);
|
||||
if !push_actions.is_empty() {
|
||||
s.field("push_actions", push_actions);
|
||||
}
|
||||
s.maybe_field("unsigned_encryption_info", unsigned_encryption_info);
|
||||
s.finish()
|
||||
/// Returns a reference to the (potentially decrypted) Matrix event inside
|
||||
/// this `TimelineEvent`.
|
||||
pub fn raw(&self) -> &Raw<AnySyncTimelineEvent> {
|
||||
self.kind.raw()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Raw<AnySyncTimelineEvent>> for SyncTimelineEvent {
|
||||
fn from(inner: Raw<AnySyncTimelineEvent>) -> Self {
|
||||
Self::new(inner)
|
||||
/// If the event was a decrypted event that was successfully decrypted, get
|
||||
/// its encryption info. Otherwise, `None`.
|
||||
pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
|
||||
self.kind.encryption_info()
|
||||
}
|
||||
|
||||
/// Takes ownership of this `TimelineEvent`, returning the (potentially
|
||||
/// decrypted) Matrix event within.
|
||||
pub fn into_raw(self) -> Raw<AnySyncTimelineEvent> {
|
||||
self.kind.into_raw()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TimelineEvent> for SyncTimelineEvent {
|
||||
fn from(o: TimelineEvent) -> Self {
|
||||
// This conversion is unproblematic since a `SyncTimelineEvent` is just a
|
||||
// `TimelineEvent` without the `room_id`. By converting the raw value in
|
||||
// this way, we simply cause the `room_id` field in the json to be
|
||||
// ignored by a subsequent deserialization.
|
||||
Self {
|
||||
event: o.event.cast(),
|
||||
encryption_info: o.encryption_info,
|
||||
push_actions: o.push_actions.unwrap_or_default(),
|
||||
unsigned_encryption_info: o.unsigned_encryption_info,
|
||||
}
|
||||
Self { kind: o.kind, push_actions: o.push_actions.unwrap_or_default() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,60 +378,208 @@ impl From<DecryptedRoomEvent> for SyncTimelineEvent {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
impl<'de> Deserialize<'de> for SyncTimelineEvent {
|
||||
/// Custom deserializer for [`SyncTimelineEvent`], to support older formats.
|
||||
///
|
||||
/// Ideally we might use an untagged enum and then convert from that;
|
||||
/// however, that doesn't work due to a [serde bug](https://github.com/serde-rs/json/issues/497).
|
||||
///
|
||||
/// Instead, we first deserialize into an unstructured JSON map, and then
|
||||
/// inspect the json to figure out which format we have.
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
// First, deserialize to an unstructured JSON map
|
||||
let value = Map::<String, Value>::deserialize(deserializer)?;
|
||||
|
||||
// If we have a top-level `event`, it's V0
|
||||
if value.contains_key("event") {
|
||||
let v0: SyncTimelineEventDeserializationHelperV0 =
|
||||
serde_json::from_value(Value::Object(value)).map_err(|e| {
|
||||
serde::de::Error::custom(format!(
|
||||
"Unable to deserialize V0-format SyncTimelineEvent: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
Ok(v0.into())
|
||||
}
|
||||
// Otherwise, it's V1
|
||||
else {
|
||||
let v1: SyncTimelineEventDeserializationHelperV1 =
|
||||
serde_json::from_value(Value::Object(value)).map_err(|e| {
|
||||
serde::de::Error::custom(format!(
|
||||
"Unable to deserialize V1-format SyncTimelineEvent: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
Ok(v1.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a matrix room event that has been returned from a Matrix
|
||||
/// client-server API endpoint such as `/messages`, after initial processing.
|
||||
///
|
||||
/// The "initial processing" includes an attempt to decrypt encrypted events, so
|
||||
/// the main thing this adds over [`AnyTimelineEvent`] is information on
|
||||
/// encryption.
|
||||
///
|
||||
/// Previously, this differed from [`SyncTimelineEvent`] by wrapping an
|
||||
/// [`AnyTimelineEvent`] instead of an [`AnySyncTimelineEvent`], but nowadays
|
||||
/// they are essentially identical, and one of them should probably be removed.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TimelineEvent {
|
||||
/// The actual event.
|
||||
pub event: Raw<AnyTimelineEvent>,
|
||||
/// The encryption info about the event. Will be `None` if the event was not
|
||||
/// encrypted.
|
||||
pub encryption_info: Option<EncryptionInfo>,
|
||||
/// The event itself, together with any information on decryption.
|
||||
pub kind: TimelineEventKind,
|
||||
|
||||
/// The push actions associated with this event, if we had sufficient
|
||||
/// context to compute them.
|
||||
pub push_actions: Option<Vec<Action>>,
|
||||
/// The encryption info about the events bundled in the `unsigned` object.
|
||||
///
|
||||
/// Will be `None` if no bundled event was encrypted.
|
||||
pub unsigned_encryption_info: Option<BTreeMap<UnsignedEventLocation, UnsignedDecryptionResult>>,
|
||||
}
|
||||
|
||||
impl TimelineEvent {
|
||||
/// Create a new `TimelineEvent` from the given raw event.
|
||||
///
|
||||
/// This is a convenience constructor for when you don't need to set
|
||||
/// `encryption_info` or `push_action`, for example inside a test.
|
||||
/// This is a convenience constructor for a plaintext event when you don't
|
||||
/// need to set `push_action`, for example inside a test.
|
||||
pub fn new(event: Raw<AnyTimelineEvent>) -> Self {
|
||||
Self { event, encryption_info: None, push_actions: None, unsigned_encryption_info: None }
|
||||
Self {
|
||||
// This conversion is unproblematic since a `SyncTimelineEvent` is just a
|
||||
// `TimelineEvent` without the `room_id`. By converting the raw value in
|
||||
// this way, we simply cause the `room_id` field in the json to be
|
||||
// ignored by a subsequent deserialization.
|
||||
kind: TimelineEventKind::PlainText { event: event.cast() },
|
||||
push_actions: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `TimelineEvent` to represent the given decryption failure.
|
||||
pub fn new_utd_event(event: Raw<AnySyncTimelineEvent>, utd_info: UnableToDecryptInfo) -> Self {
|
||||
Self { kind: TimelineEventKind::UnableToDecrypt { event, utd_info }, push_actions: None }
|
||||
}
|
||||
|
||||
/// Returns a reference to the (potentially decrypted) Matrix event inside
|
||||
/// this `TimelineEvent`.
|
||||
pub fn raw(&self) -> &Raw<AnySyncTimelineEvent> {
|
||||
self.kind.raw()
|
||||
}
|
||||
|
||||
/// If the event was a decrypted event that was successfully decrypted, get
|
||||
/// its encryption info. Otherwise, `None`.
|
||||
pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
|
||||
self.kind.encryption_info()
|
||||
}
|
||||
|
||||
/// Takes ownership of this `TimelineEvent`, returning the (potentially
|
||||
/// decrypted) Matrix event within.
|
||||
pub fn into_raw(self) -> Raw<AnySyncTimelineEvent> {
|
||||
self.kind.into_raw()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DecryptedRoomEvent> for TimelineEvent {
|
||||
fn from(decrypted: DecryptedRoomEvent) -> Self {
|
||||
Self {
|
||||
// Casting from the more specific `AnyMessageLikeEvent` (i.e. an event without a
|
||||
// `state_key`) to a more generic `AnyTimelineEvent` (i.e. one that may contain
|
||||
// a `state_key`) is safe.
|
||||
event: decrypted.event.cast(),
|
||||
encryption_info: Some(decrypted.encryption_info),
|
||||
push_actions: None,
|
||||
unsigned_encryption_info: decrypted.unsigned_encryption_info,
|
||||
Self { kind: TimelineEventKind::Decrypted(decrypted), push_actions: None }
|
||||
}
|
||||
}
|
||||
|
||||
/// The event within a [`TimelineEvent`] or [`SyncTimelineEvent`], together with
|
||||
/// encryption data.
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub enum TimelineEventKind {
|
||||
/// A successfully-decrypted encrypted event.
|
||||
Decrypted(DecryptedRoomEvent),
|
||||
|
||||
/// An encrypted event which could not be decrypted.
|
||||
UnableToDecrypt {
|
||||
/// The `m.room.encrypted` event. Depending on the source of the event,
|
||||
/// it could actually be an [`AnyTimelineEvent`] (i.e., it may
|
||||
/// have a `room_id` property).
|
||||
event: Raw<AnySyncTimelineEvent>,
|
||||
|
||||
/// Information on the reason we failed to decrypt
|
||||
utd_info: UnableToDecryptInfo,
|
||||
},
|
||||
|
||||
/// An unencrypted event.
|
||||
PlainText {
|
||||
/// The actual event. Depending on the source of the event, it could
|
||||
/// actually be a [`AnyTimelineEvent`] (which differs from
|
||||
/// [`AnySyncTimelineEvent`] by the addition of a `room_id` property).
|
||||
event: Raw<AnySyncTimelineEvent>,
|
||||
},
|
||||
}
|
||||
|
||||
impl TimelineEventKind {
|
||||
/// Returns a reference to the (potentially decrypted) Matrix event inside
|
||||
/// this `TimelineEvent`.
|
||||
pub fn raw(&self) -> &Raw<AnySyncTimelineEvent> {
|
||||
match self {
|
||||
// It is safe to cast from an `AnyMessageLikeEvent` (i.e. JSON which does
|
||||
// *not* contain a `state_key` and *does* contain a `room_id`) into an
|
||||
// `AnySyncTimelineEvent` (i.e. JSON which *may* contain a `state_key` and is *not*
|
||||
// expected to contain a `room_id`). It just means that the `room_id` will be ignored
|
||||
// in a future deserialization.
|
||||
TimelineEventKind::Decrypted(d) => d.event.cast_ref(),
|
||||
TimelineEventKind::UnableToDecrypt { event, .. } => event.cast_ref(),
|
||||
TimelineEventKind::PlainText { event } => event,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
match self {
|
||||
TimelineEventKind::Decrypted(d) => Some(&d.encryption_info),
|
||||
TimelineEventKind::UnableToDecrypt { .. } => None,
|
||||
TimelineEventKind::PlainText { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes ownership of this `TimelineEvent`, returning the (potentially
|
||||
/// decrypted) Matrix event within.
|
||||
pub fn into_raw(self) -> Raw<AnySyncTimelineEvent> {
|
||||
match self {
|
||||
// It is safe to cast from an `AnyMessageLikeEvent` (i.e. JSON which does
|
||||
// *not* contain a `state_key` and *does* contain a `room_id`) into an
|
||||
// `AnySyncTimelineEvent` (i.e. JSON which *may* contain a `state_key` and is *not*
|
||||
// expected to contain a `room_id`). It just means that the `room_id` will be ignored
|
||||
// in a future deserialization.
|
||||
TimelineEventKind::Decrypted(d) => d.event.cast(),
|
||||
TimelineEventKind::UnableToDecrypt { event, .. } => event.cast(),
|
||||
TimelineEventKind::PlainText { event } => event,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for TimelineEvent {
|
||||
impl fmt::Debug for TimelineEventKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let TimelineEvent { event, encryption_info, push_actions, unsigned_encryption_info } = self;
|
||||
let mut s = f.debug_struct("TimelineEvent");
|
||||
s.field("event", &DebugRawEvent(event));
|
||||
s.maybe_field("encryption_info", encryption_info);
|
||||
if let Some(push_actions) = &push_actions {
|
||||
if !push_actions.is_empty() {
|
||||
s.field("push_actions", push_actions);
|
||||
match &self {
|
||||
Self::PlainText { event } => f
|
||||
.debug_struct("TimelineEventDecryptionResult::PlainText")
|
||||
.field("event", &DebugRawEvent(event))
|
||||
.finish(),
|
||||
|
||||
Self::UnableToDecrypt { event, utd_info } => f
|
||||
.debug_struct("TimelineEventDecryptionResult::UnableToDecrypt")
|
||||
.field("event", &DebugRawEvent(event))
|
||||
.field("utd_info", &utd_info)
|
||||
.finish(),
|
||||
|
||||
Self::Decrypted(decrypted) => {
|
||||
f.debug_tuple("TimelineEventDecryptionResult::Decrypted").field(decrypted).finish()
|
||||
}
|
||||
}
|
||||
s.maybe_field("unsigned_encryption_info", unsigned_encryption_info);
|
||||
s.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,6 +596,7 @@ pub struct DecryptedRoomEvent {
|
||||
/// object.
|
||||
///
|
||||
/// Will be `None` if no bundled event was encrypted.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub unsigned_encryption_info: Option<BTreeMap<UnsignedEventLocation, UnsignedDecryptionResult>>,
|
||||
}
|
||||
|
||||
@@ -520,18 +659,161 @@ pub struct UnableToDecryptInfo {
|
||||
/// `m.megolm.v1.aes-sha2` algorithm.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub session_id: Option<String>,
|
||||
|
||||
/// Reason code for the decryption failure
|
||||
#[serde(default = "unknown_utd_reason")]
|
||||
pub reason: UnableToDecryptReason,
|
||||
}
|
||||
|
||||
fn unknown_utd_reason() -> UnableToDecryptReason {
|
||||
UnableToDecryptReason::Unknown
|
||||
}
|
||||
|
||||
/// Reason code for a decryption failure
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum UnableToDecryptReason {
|
||||
/// The reason for the decryption failure is unknown. This is only intended
|
||||
/// for use when deserializing old UnableToDecryptInfo instances.
|
||||
#[doc(hidden)]
|
||||
Unknown,
|
||||
|
||||
/// The `m.room.encrypted` event that should have been decrypted is
|
||||
/// malformed in some way (e.g. unsupported algorithm, missing fields,
|
||||
/// unknown megolm message type).
|
||||
MalformedEncryptedEvent,
|
||||
|
||||
/// Decryption failed because we're missing the megolm session that was used
|
||||
/// to encrypt the event.
|
||||
///
|
||||
/// TODO: support withheld codes?
|
||||
MissingMegolmSession,
|
||||
|
||||
/// Decryption failed because, while we have the megolm session that was
|
||||
/// used to encrypt the message, it is ratcheted too far forward.
|
||||
UnknownMegolmMessageIndex,
|
||||
|
||||
/// We found the Megolm session, but were unable to decrypt the event using
|
||||
/// that session for some reason (e.g. incorrect MAC).
|
||||
///
|
||||
/// This represents all `vodozemac::megolm::DecryptionError`s, except
|
||||
/// `UnknownMessageIndex`, which is represented as
|
||||
/// `UnknownMegolmMessageIndex`.
|
||||
MegolmDecryptionFailure,
|
||||
|
||||
/// The event could not be deserialized after decryption.
|
||||
PayloadDeserializationFailure,
|
||||
|
||||
/// Decryption failed because of a mismatch between the identity keys of the
|
||||
/// device we received the room key from and the identity keys recorded in
|
||||
/// the plaintext of the room key to-device message.
|
||||
MismatchedIdentityKeys,
|
||||
|
||||
/// An encrypted message wasn't decrypted, because the sender's
|
||||
/// cross-signing identity did not satisfy the requested
|
||||
/// `TrustRequirement`.
|
||||
SenderIdentityNotTrusted(VerificationLevel),
|
||||
}
|
||||
|
||||
impl UnableToDecryptReason {
|
||||
/// Returns true if this UTD is due to a missing room key (and hence might
|
||||
/// resolve itself if we wait a bit.)
|
||||
pub fn is_missing_room_key(&self) -> bool {
|
||||
matches!(self, Self::MissingMegolmSession | Self::UnknownMegolmMessageIndex)
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserialization helper for [`SyncTimelineEvent`], for the modern format.
|
||||
///
|
||||
/// This has the exact same fields as [`SyncTimelineEvent`] itself, but has a
|
||||
/// regular `Deserialize` implementation.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SyncTimelineEventDeserializationHelperV1 {
|
||||
/// The event itself, together with any information on decryption.
|
||||
kind: TimelineEventKind,
|
||||
|
||||
/// The push actions associated with this event.
|
||||
#[serde(default)]
|
||||
push_actions: Vec<Action>,
|
||||
}
|
||||
|
||||
impl From<SyncTimelineEventDeserializationHelperV1> for SyncTimelineEvent {
|
||||
fn from(value: SyncTimelineEventDeserializationHelperV1) -> Self {
|
||||
let SyncTimelineEventDeserializationHelperV1 { kind, push_actions } = value;
|
||||
SyncTimelineEvent { kind, push_actions }
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserialization helper for [`SyncTimelineEvent`], for an older format.
|
||||
#[derive(Deserialize)]
|
||||
struct SyncTimelineEventDeserializationHelperV0 {
|
||||
/// The actual event.
|
||||
event: Raw<AnySyncTimelineEvent>,
|
||||
|
||||
/// The encryption info about the event. Will be `None` if the event
|
||||
/// was not encrypted.
|
||||
encryption_info: Option<EncryptionInfo>,
|
||||
|
||||
/// The push actions associated with this event.
|
||||
#[serde(default)]
|
||||
push_actions: Vec<Action>,
|
||||
|
||||
/// The encryption info about the events bundled in the `unsigned`
|
||||
/// object.
|
||||
///
|
||||
/// Will be `None` if no bundled event was encrypted.
|
||||
unsigned_encryption_info: Option<BTreeMap<UnsignedEventLocation, UnsignedDecryptionResult>>,
|
||||
}
|
||||
|
||||
impl From<SyncTimelineEventDeserializationHelperV0> for SyncTimelineEvent {
|
||||
fn from(value: SyncTimelineEventDeserializationHelperV0) -> Self {
|
||||
let SyncTimelineEventDeserializationHelperV0 {
|
||||
event,
|
||||
encryption_info,
|
||||
push_actions,
|
||||
unsigned_encryption_info,
|
||||
} = value;
|
||||
|
||||
let kind = match encryption_info {
|
||||
Some(encryption_info) => {
|
||||
TimelineEventKind::Decrypted(DecryptedRoomEvent {
|
||||
// We cast from `Raw<AnySyncTimelineEvent>` to
|
||||
// `Raw<AnyMessageLikeEvent>`, which means
|
||||
// we are asserting that it contains a room_id.
|
||||
// That *should* be ok, because if this is genuinely a decrypted
|
||||
// room event (as the encryption_info indicates), then it will have
|
||||
// a room_id.
|
||||
event: event.cast(),
|
||||
encryption_info,
|
||||
unsigned_encryption_info,
|
||||
})
|
||||
}
|
||||
|
||||
None => TimelineEventKind::PlainText { event },
|
||||
};
|
||||
|
||||
SyncTimelineEvent { kind, push_actions }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use ruma::{
|
||||
event_id,
|
||||
events::{room::message::RoomMessageEventContent, AnySyncTimelineEvent},
|
||||
serde::Raw,
|
||||
user_id,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
use super::{SyncTimelineEvent, TimelineEvent, VerificationState};
|
||||
use super::{
|
||||
AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, TimelineEvent,
|
||||
TimelineEventKind, UnableToDecryptInfo, UnableToDecryptReason, UnsignedDecryptionResult,
|
||||
UnsignedEventLocation, VerificationState,
|
||||
};
|
||||
use crate::deserialized_responses::{DeviceLinkProblem, VerificationLevel};
|
||||
|
||||
fn example_event() -> serde_json::Value {
|
||||
@@ -561,7 +843,7 @@ mod tests {
|
||||
let converted_room_event: SyncTimelineEvent = room_event.into();
|
||||
|
||||
let converted_event: AnySyncTimelineEvent =
|
||||
converted_room_event.event.deserialize().unwrap();
|
||||
converted_room_event.raw().deserialize().unwrap();
|
||||
|
||||
assert_eq!(converted_event.event_id(), "$xxxxx:example.org");
|
||||
assert_eq!(converted_event.sender(), "@carl:example.com");
|
||||
@@ -606,4 +888,149 @@ mod tests {
|
||||
VerificationState::Unverified(VerificationLevel::UnsignedDevice)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_timeline_event_serialisation() {
|
||||
let room_event = SyncTimelineEvent {
|
||||
kind: TimelineEventKind::Decrypted(DecryptedRoomEvent {
|
||||
event: Raw::new(&example_event()).unwrap().cast(),
|
||||
encryption_info: EncryptionInfo {
|
||||
sender: user_id!("@sender:example.com").to_owned(),
|
||||
sender_device: None,
|
||||
algorithm_info: AlgorithmInfo::MegolmV1AesSha2 {
|
||||
curve25519_key: "xxx".to_owned(),
|
||||
sender_claimed_keys: Default::default(),
|
||||
},
|
||||
verification_state: VerificationState::Verified,
|
||||
},
|
||||
unsigned_encryption_info: Some(BTreeMap::from([(
|
||||
UnsignedEventLocation::RelationsReplace,
|
||||
UnsignedDecryptionResult::UnableToDecrypt(UnableToDecryptInfo {
|
||||
session_id: Some("xyz".to_owned()),
|
||||
reason: UnableToDecryptReason::MalformedEncryptedEvent,
|
||||
}),
|
||||
)])),
|
||||
}),
|
||||
push_actions: Default::default(),
|
||||
};
|
||||
|
||||
let serialized = serde_json::to_value(&room_event).unwrap();
|
||||
|
||||
// Test that the serialization is as expected
|
||||
assert_eq!(
|
||||
serialized,
|
||||
json!({
|
||||
"kind": {
|
||||
"Decrypted": {
|
||||
"event": {
|
||||
"content": {"body": "secret", "msgtype": "m.text"},
|
||||
"event_id": "$xxxxx:example.org",
|
||||
"origin_server_ts": 2189,
|
||||
"room_id": "!someroom:example.com",
|
||||
"sender": "@carl:example.com",
|
||||
"type": "m.room.message",
|
||||
},
|
||||
"encryption_info": {
|
||||
"sender": "@sender:example.com",
|
||||
"sender_device": null,
|
||||
"algorithm_info": {
|
||||
"MegolmV1AesSha2": {
|
||||
"curve25519_key": "xxx",
|
||||
"sender_claimed_keys": {}
|
||||
}
|
||||
},
|
||||
"verification_state": "Verified",
|
||||
},
|
||||
"unsigned_encryption_info": {
|
||||
"RelationsReplace": {"UnableToDecrypt": {
|
||||
"session_id": "xyz",
|
||||
"reason": "MalformedEncryptedEvent",
|
||||
}}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// And it can be properly deserialized from the new format.
|
||||
let event: SyncTimelineEvent = serde_json::from_value(serialized).unwrap();
|
||||
assert_eq!(event.event_id(), Some(event_id!("$xxxxx:example.org").to_owned()));
|
||||
assert_matches!(
|
||||
event.encryption_info().unwrap().algorithm_info,
|
||||
AlgorithmInfo::MegolmV1AesSha2 { .. }
|
||||
);
|
||||
|
||||
// Test that the previous format can also be deserialized.
|
||||
let serialized = json!({
|
||||
"event": {
|
||||
"content": {"body": "secret", "msgtype": "m.text"},
|
||||
"event_id": "$xxxxx:example.org",
|
||||
"origin_server_ts": 2189,
|
||||
"room_id": "!someroom:example.com",
|
||||
"sender": "@carl:example.com",
|
||||
"type": "m.room.message",
|
||||
},
|
||||
"encryption_info": {
|
||||
"sender": "@sender:example.com",
|
||||
"sender_device": null,
|
||||
"algorithm_info": {
|
||||
"MegolmV1AesSha2": {
|
||||
"curve25519_key": "xxx",
|
||||
"sender_claimed_keys": {}
|
||||
}
|
||||
},
|
||||
"verification_state": "Verified",
|
||||
},
|
||||
});
|
||||
let event: SyncTimelineEvent = serde_json::from_value(serialized).unwrap();
|
||||
assert_eq!(event.event_id(), Some(event_id!("$xxxxx:example.org").to_owned()));
|
||||
assert_matches!(
|
||||
event.encryption_info().unwrap().algorithm_info,
|
||||
AlgorithmInfo::MegolmV1AesSha2 { .. }
|
||||
);
|
||||
|
||||
// Test that the previous format, with an undecryptable unsigned event, can also
|
||||
// be deserialized.
|
||||
let serialized = json!({
|
||||
"event": {
|
||||
"content": {"body": "secret", "msgtype": "m.text"},
|
||||
"event_id": "$xxxxx:example.org",
|
||||
"origin_server_ts": 2189,
|
||||
"room_id": "!someroom:example.com",
|
||||
"sender": "@carl:example.com",
|
||||
"type": "m.room.message",
|
||||
},
|
||||
"encryption_info": {
|
||||
"sender": "@sender:example.com",
|
||||
"sender_device": null,
|
||||
"algorithm_info": {
|
||||
"MegolmV1AesSha2": {
|
||||
"curve25519_key": "xxx",
|
||||
"sender_claimed_keys": {}
|
||||
}
|
||||
},
|
||||
"verification_state": "Verified",
|
||||
},
|
||||
"unsigned_encryption_info": {
|
||||
"RelationsReplace": {"UnableToDecrypt": {"session_id": "xyz"}}
|
||||
}
|
||||
});
|
||||
let event: SyncTimelineEvent = serde_json::from_value(serialized).unwrap();
|
||||
assert_eq!(event.event_id(), Some(event_id!("$xxxxx:example.org").to_owned()));
|
||||
assert_matches!(
|
||||
event.encryption_info().unwrap().algorithm_info,
|
||||
AlgorithmInfo::MegolmV1AesSha2 { .. }
|
||||
);
|
||||
assert_matches!(event.kind, TimelineEventKind::Decrypted(decrypted) => {
|
||||
assert_matches!(decrypted.unsigned_encryption_info, Some(map) => {
|
||||
assert_eq!(map.len(), 1);
|
||||
let (location, result) = map.into_iter().next().unwrap();
|
||||
assert_eq!(location, UnsignedEventLocation::RelationsReplace);
|
||||
assert_matches!(result, UnsignedDecryptionResult::UnableToDecrypt(utd_info) => {
|
||||
assert_eq!(utd_info.session_id, Some("xyz".to_owned()));
|
||||
assert_eq!(utd_info.reason, UnableToDecryptReason::Unknown);
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
+175
-52
@@ -14,7 +14,7 @@
|
||||
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
ops::ControlFlow,
|
||||
ops::{ControlFlow, Not},
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ use eyeball_im::VectorDiff;
|
||||
|
||||
use super::{
|
||||
updates::{ReaderToken, Update, UpdatesInner},
|
||||
ChunkContent, ChunkIdentifier, Iter,
|
||||
ChunkContent, ChunkIdentifier, Iter, Position,
|
||||
};
|
||||
|
||||
/// A type alias to represent a chunk's length. This is purely for commodity.
|
||||
@@ -253,7 +253,8 @@ impl UpdateToVectorDiff {
|
||||
//
|
||||
// From the `VectorDiff` “point of view”, this optimisation aims at avoiding
|
||||
// removing items to push them again later.
|
||||
let mut mute_push_items = false;
|
||||
let mut reattaching = false;
|
||||
let mut detaching = false;
|
||||
|
||||
for update in updates {
|
||||
match update {
|
||||
@@ -329,46 +330,22 @@ impl UpdateToVectorDiff {
|
||||
}
|
||||
|
||||
Update::PushItems { at: position, items } => {
|
||||
let expected_chunk_identifier = position.chunk_identifier();
|
||||
let number_of_chunks = self.chunks.len();
|
||||
let (offset, (chunk_index, chunk_length)) = self.map_to_offset(position);
|
||||
|
||||
let (chunk_index, offset, chunk_length) = {
|
||||
let control_flow = self.chunks.iter_mut().enumerate().try_fold(
|
||||
position.index(),
|
||||
|offset, (chunk_index, (chunk_identifier, chunk_length))| {
|
||||
if chunk_identifier == &expected_chunk_identifier {
|
||||
ControlFlow::Break((chunk_index, offset, chunk_length))
|
||||
} else {
|
||||
ControlFlow::Continue(offset + *chunk_length)
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
match control_flow {
|
||||
// Chunk has been found, and all values have been calculated as
|
||||
// expected.
|
||||
ControlFlow::Break(values) => values,
|
||||
|
||||
// Chunk has not been found.
|
||||
ControlFlow::Continue(..) => {
|
||||
// SAFETY: Assuming `LinkedChunk` and `ObservableUpdates` are not
|
||||
// buggy, and assuming `Self::chunks` is correctly initialized, it
|
||||
// is not possible to push items on a chunk that does not exist. If
|
||||
// this predicate fails, it means `LinkedChunk` or
|
||||
// `ObservableUpdates` contain a bug.
|
||||
panic!("Pushing items: The chunk is not found");
|
||||
}
|
||||
}
|
||||
};
|
||||
let is_pushing_back =
|
||||
chunk_index + 1 == number_of_chunks && position.index() >= *chunk_length;
|
||||
|
||||
// Add the number of items to the chunk in `self.chunks`.
|
||||
*chunk_length += items.len();
|
||||
|
||||
// See `mute_push_items` to learn more.
|
||||
if mute_push_items {
|
||||
// See `reattaching` to learn more.
|
||||
if reattaching {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Optimisation: we can emit a `VectorDiff::Append` in this particular case.
|
||||
if chunk_index + 1 == self.chunks.len() {
|
||||
if is_pushing_back && detaching.not() {
|
||||
diffs.push(VectorDiff::Append { values: items.into() });
|
||||
}
|
||||
// No optimisation: let's emit `VectorDiff::Insert`.
|
||||
@@ -379,15 +356,30 @@ impl UpdateToVectorDiff {
|
||||
}
|
||||
}
|
||||
|
||||
Update::DetachLastItems { at } => {
|
||||
let expected_chunk_identifier = at.chunk_identifier();
|
||||
let new_length = at.index();
|
||||
Update::RemoveItem { at: position } => {
|
||||
let (offset, (_chunk_index, chunk_length)) = self.map_to_offset(position);
|
||||
|
||||
let length = self
|
||||
// Remove one item to the chunk in `self.chunks`.
|
||||
*chunk_length -= 1;
|
||||
|
||||
// See `reattaching` to learn more.
|
||||
if reattaching {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Let's emit a `VectorDiff::Remove`.
|
||||
diffs.push(VectorDiff::Remove { index: offset });
|
||||
}
|
||||
|
||||
Update::DetachLastItems { at: position } => {
|
||||
let expected_chunk_identifier = position.chunk_identifier();
|
||||
let new_length = position.index();
|
||||
|
||||
let chunk_length = self
|
||||
.chunks
|
||||
.iter_mut()
|
||||
.find_map(|(chunk_identifier, length)| {
|
||||
(*chunk_identifier == expected_chunk_identifier).then_some(length)
|
||||
.find_map(|(chunk_identifier, chunk_length)| {
|
||||
(*chunk_identifier == expected_chunk_identifier).then_some(chunk_length)
|
||||
})
|
||||
// SAFETY: Assuming `LinkedChunk` and `ObservableUpdates` are not buggy, and
|
||||
// assuming `Self::chunks` is correctly initialized, it is not possible to
|
||||
@@ -395,23 +387,63 @@ impl UpdateToVectorDiff {
|
||||
// it means `LinkedChunk` or `ObservableUpdates` contain a bug.
|
||||
.expect("Detach last items: The chunk is not found");
|
||||
|
||||
*length = new_length;
|
||||
*chunk_length = new_length;
|
||||
|
||||
// Entering the _detaching_ mode.
|
||||
detaching = true;
|
||||
}
|
||||
|
||||
Update::StartReattachItems => {
|
||||
// Entering the `reattaching` mode.
|
||||
mute_push_items = true;
|
||||
// Entering the _reattaching_ mode.
|
||||
reattaching = true;
|
||||
}
|
||||
|
||||
Update::EndReattachItems => {
|
||||
// Exiting the `reattaching` mode.
|
||||
mute_push_items = false;
|
||||
// Exiting the _reattaching_ mode.
|
||||
reattaching = false;
|
||||
|
||||
// Exiting the _detaching_ mode.
|
||||
detaching = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
diffs
|
||||
}
|
||||
|
||||
fn map_to_offset(&mut self, position: &Position) -> (usize, (usize, &mut usize)) {
|
||||
let expected_chunk_identifier = position.chunk_identifier();
|
||||
|
||||
let (offset, (chunk_index, chunk_length)) = {
|
||||
let control_flow = self.chunks.iter_mut().enumerate().try_fold(
|
||||
position.index(),
|
||||
|offset, (chunk_index, (chunk_identifier, chunk_length))| {
|
||||
if chunk_identifier == &expected_chunk_identifier {
|
||||
ControlFlow::Break((offset, (chunk_index, chunk_length)))
|
||||
} else {
|
||||
ControlFlow::Continue(offset + *chunk_length)
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
match control_flow {
|
||||
// Chunk has been found, and all values have been calculated as
|
||||
// expected.
|
||||
ControlFlow::Break(values) => values,
|
||||
|
||||
// Chunk has not been found.
|
||||
ControlFlow::Continue(..) => {
|
||||
// SAFETY: Assuming `LinkedChunk` and `ObservableUpdates` are not buggy, and
|
||||
// assuming `Self::chunks` is correctly initialized, it is not possible to work
|
||||
// on a chunk that does not exist. If this predicate fails, it means
|
||||
// `LinkedChunk` or `ObservableUpdates` contain a bug.
|
||||
panic!("The chunk is not found");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
(offset, (chunk_index, chunk_length))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -420,7 +452,10 @@ mod tests {
|
||||
|
||||
use imbl::{vector, Vector};
|
||||
|
||||
use super::{super::LinkedChunk, VectorDiff};
|
||||
use super::{
|
||||
super::{EmptyChunk, LinkedChunk},
|
||||
VectorDiff,
|
||||
};
|
||||
|
||||
fn apply_and_assert_eq<Item>(
|
||||
accumulator: &mut Vector<Item>,
|
||||
@@ -435,6 +470,9 @@ mod tests {
|
||||
match diff {
|
||||
VectorDiff::Insert { index, value } => accumulator.insert(index, value),
|
||||
VectorDiff::Append { values } => accumulator.append(values),
|
||||
VectorDiff::Remove { index } => {
|
||||
accumulator.remove(index);
|
||||
}
|
||||
diff => unimplemented!("{diff:?}"),
|
||||
}
|
||||
}
|
||||
@@ -578,15 +616,83 @@ mod tests {
|
||||
&[VectorDiff::Insert { index: 0, value: 'm' }],
|
||||
);
|
||||
|
||||
let removed_item = linked_chunk
|
||||
.remove_item_at(
|
||||
linked_chunk.item_position(|item| *item == 'c').unwrap(),
|
||||
EmptyChunk::Remove,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(removed_item, 'c');
|
||||
assert_items_eq!(
|
||||
linked_chunk,
|
||||
['m', 'a', 'w'] ['x'] ['y', 'z', 'b'] ['d'] ['i', 'j', 'k'] ['l'] ['e', 'f', 'g'] ['h']
|
||||
);
|
||||
|
||||
// From an `ObservableVector` point of view, it would look like:
|
||||
//
|
||||
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
||||
// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|
||||
// | m | a | w | x | y | z | b | d | i | j | k | l | e | f | g | h |
|
||||
// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|
||||
// ^
|
||||
// |
|
||||
// `c` has been removed
|
||||
apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Remove { index: 7 }]);
|
||||
|
||||
let removed_item = linked_chunk
|
||||
.remove_item_at(
|
||||
linked_chunk.item_position(|item| *item == 'z').unwrap(),
|
||||
EmptyChunk::Remove,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(removed_item, 'z');
|
||||
assert_items_eq!(
|
||||
linked_chunk,
|
||||
['m', 'a', 'w'] ['x'] ['y', 'b'] ['d'] ['i', 'j', 'k'] ['l'] ['e', 'f', 'g'] ['h']
|
||||
);
|
||||
|
||||
// From an `ObservableVector` point of view, it would look like:
|
||||
//
|
||||
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
||||
// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|
||||
// | m | a | w | x | y | b | d | i | j | k | l | e | f | g | h |
|
||||
// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|
||||
// ^
|
||||
// |
|
||||
// `z` has been removed
|
||||
apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Remove { index: 5 }]);
|
||||
|
||||
linked_chunk
|
||||
.insert_items_at(['z'], linked_chunk.item_position(|item| *item == 'h').unwrap())
|
||||
.unwrap();
|
||||
|
||||
assert_items_eq!(
|
||||
linked_chunk,
|
||||
['m', 'a', 'w'] ['x'] ['y', 'b'] ['d'] ['i', 'j', 'k'] ['l'] ['e', 'f', 'g'] ['z', 'h']
|
||||
);
|
||||
|
||||
// From an `ObservableVector` point of view, it would look like:
|
||||
//
|
||||
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
||||
// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|
||||
// | m | a | w | x | y | b | d | i | j | k | l | e | f | g | z | h |
|
||||
// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|
||||
// ^^^^
|
||||
// |
|
||||
// new!
|
||||
apply_and_assert_eq(
|
||||
&mut accumulator,
|
||||
as_vector.take(),
|
||||
&[VectorDiff::Insert { index: 14, value: 'z' }],
|
||||
);
|
||||
|
||||
drop(linked_chunk);
|
||||
assert!(as_vector.take().is_empty());
|
||||
|
||||
// Finally, ensure the “reconstitued” vector is the one expected.
|
||||
assert_eq!(
|
||||
accumulator,
|
||||
vector![
|
||||
'm', 'a', 'w', 'x', 'y', 'z', 'b', 'c', 'd', 'i', 'j', 'k', 'l', 'e', 'f', 'g', 'h'
|
||||
]
|
||||
vector!['m', 'a', 'w', 'x', 'y', 'b', 'd', 'i', 'j', 'k', 'l', 'e', 'f', 'g', 'z', 'h']
|
||||
);
|
||||
}
|
||||
|
||||
@@ -622,6 +728,7 @@ mod tests {
|
||||
PushItems { items: Vec<char> },
|
||||
PushGap,
|
||||
ReplaceLastGap { items: Vec<char> },
|
||||
RemoveItem { item: char },
|
||||
}
|
||||
|
||||
fn as_vector_operation_strategy() -> impl Strategy<Value = AsVectorOperation> {
|
||||
@@ -633,13 +740,16 @@ mod tests {
|
||||
|
||||
1 => prop::collection::vec(prop::char::ranges(vec!['a'..='z', 'A'..='Z'].into()), 0..=25)
|
||||
.prop_map(|items| AsVectorOperation::ReplaceLastGap { items }),
|
||||
|
||||
1 => prop::char::ranges(vec!['a'..='z', 'A'..='Z'].into())
|
||||
.prop_map(|item| AsVectorOperation::RemoveItem { item }),
|
||||
]
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn as_vector_is_correct(
|
||||
operations in prop::collection::vec(as_vector_operation_strategy(), 10..=50)
|
||||
operations in prop::collection::vec(as_vector_operation_strategy(), 50..=200)
|
||||
) {
|
||||
let mut linked_chunk = LinkedChunk::<10, char, ()>::new_with_update_history();
|
||||
let mut as_vector = linked_chunk.as_vector().unwrap();
|
||||
@@ -662,7 +772,17 @@ mod tests {
|
||||
continue;
|
||||
};
|
||||
|
||||
linked_chunk.replace_gap_at(items, gap_identifier).unwrap();
|
||||
linked_chunk.replace_gap_at(items, gap_identifier).expect("Failed to replace a gap");
|
||||
}
|
||||
|
||||
AsVectorOperation::RemoveItem { item: expected_item } => {
|
||||
let Some(position) = linked_chunk
|
||||
.items().find_map(|(position, item)| (*item == expected_item).then_some(position))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
linked_chunk.remove_item_at(position, EmptyChunk::Remove).expect("Failed to remove an item");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -678,6 +798,9 @@ mod tests {
|
||||
|
||||
vector_from_diffs.append(&mut values);
|
||||
}
|
||||
VectorDiff::Remove { index } => {
|
||||
vector_from_diffs.remove(index);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
+421
-3
@@ -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!()
|
||||
};
|
||||
|
||||
@@ -406,6 +407,88 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove item at a specified position in the [`LinkedChunk`].
|
||||
///
|
||||
/// `position` must point to a valid item, otherwise the method returns
|
||||
/// `Err`.
|
||||
///
|
||||
/// The chunk containing the item represented by `position` may be empty
|
||||
/// once the item has been removed. In this case, the chunk can be removed
|
||||
/// if `empty_chunk` contains [`EmptyChunk::Remove`], otherwise the chunk is
|
||||
/// kept if `empty_chunk` contains [`EmptyChunk::Keep`].
|
||||
pub fn remove_item_at(
|
||||
&mut self,
|
||||
position: Position,
|
||||
empty_chunk: EmptyChunk,
|
||||
) -> Result<Item, Error> {
|
||||
let chunk_identifier = position.chunk_identifier();
|
||||
let item_index = position.index();
|
||||
|
||||
let mut chunk_ptr = None;
|
||||
let removed_item;
|
||||
|
||||
{
|
||||
let chunk = self
|
||||
.links
|
||||
.chunk_mut(chunk_identifier)
|
||||
.ok_or(Error::InvalidChunkIdentifier { identifier: chunk_identifier })?;
|
||||
|
||||
let can_unlink_chunk = match &mut chunk.content {
|
||||
ChunkContent::Gap(..) => {
|
||||
return Err(Error::ChunkIsAGap { identifier: chunk_identifier })
|
||||
}
|
||||
|
||||
ChunkContent::Items(current_items) => {
|
||||
let current_items_length = current_items.len();
|
||||
|
||||
if item_index > current_items_length {
|
||||
return Err(Error::InvalidItemIndex { index: item_index });
|
||||
}
|
||||
|
||||
removed_item = current_items.remove(item_index);
|
||||
|
||||
if let Some(updates) = self.updates.as_mut() {
|
||||
updates
|
||||
.push(Update::RemoveItem { at: Position(chunk_identifier, item_index) })
|
||||
}
|
||||
|
||||
current_items.is_empty()
|
||||
}
|
||||
};
|
||||
|
||||
// If removing empty chunk is desired, and if the `chunk` can be unlinked, and
|
||||
// if the `chunk` is not the first one, we can remove it.
|
||||
if empty_chunk.remove() && can_unlink_chunk && chunk.is_first_chunk().not() {
|
||||
// Unlink `chunk`.
|
||||
chunk.unlink(&mut self.updates);
|
||||
|
||||
chunk_ptr = Some(chunk.as_ptr());
|
||||
|
||||
// We need to update `self.last` if and only if `chunk` _is_ the last chunk. The
|
||||
// new last chunk is the chunk before `chunk`.
|
||||
if chunk.is_last_chunk() {
|
||||
self.links.last = chunk.previous;
|
||||
}
|
||||
}
|
||||
|
||||
self.length -= 1;
|
||||
|
||||
// Stop borrowing `chunk`.
|
||||
}
|
||||
|
||||
if let Some(chunk_ptr) = chunk_ptr {
|
||||
// `chunk` has been unlinked.
|
||||
|
||||
// Re-box the chunk, and let Rust does its job.
|
||||
//
|
||||
// SAFETY: `chunk` is unlinked and not borrowed anymore. `LinkedChunk` doesn't
|
||||
// use it anymore, it's a leak. It is time to re-`Box` it and drop it.
|
||||
let _chunk_boxed = unsafe { Box::from_raw(chunk_ptr.as_ptr()) };
|
||||
}
|
||||
|
||||
Ok(removed_item)
|
||||
}
|
||||
|
||||
/// Insert a gap at a specified position in the [`LinkedChunk`].
|
||||
///
|
||||
/// Because the `position` can be invalid, this method returns a
|
||||
@@ -852,6 +935,12 @@ impl ChunkIdentifierGenerator {
|
||||
#[repr(transparent)]
|
||||
pub struct ChunkIdentifier(u64);
|
||||
|
||||
impl PartialEq<u64> for ChunkIdentifier {
|
||||
fn eq(&self, other: &u64) -> bool {
|
||||
self.0 == *other
|
||||
}
|
||||
}
|
||||
|
||||
/// The position of something inside a [`Chunk`].
|
||||
///
|
||||
/// It's a pair of a chunk position and an item index.
|
||||
@@ -868,6 +957,15 @@ impl Position {
|
||||
pub fn index(&self) -> usize {
|
||||
self.1
|
||||
}
|
||||
|
||||
/// Decrement the index part (see [`Self::index`]), i.e. subtract 1.
|
||||
///
|
||||
/// # Panic
|
||||
///
|
||||
/// This method will panic if it will underflow, i.e. if the index is 0.
|
||||
pub fn decrement_index(&mut self) {
|
||||
self.1 = self.1.checked_sub(1).expect("Cannot decrement the index because it's already 0");
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over a [`LinkedChunk`] that traverses the chunk in backward
|
||||
@@ -1247,6 +1345,22 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// A type representing what to do when the system has to handle an empty chunk.
|
||||
#[derive(Debug)]
|
||||
pub enum EmptyChunk {
|
||||
/// Keep the empty chunk.
|
||||
Keep,
|
||||
|
||||
/// Remove the empty chunk.
|
||||
Remove,
|
||||
}
|
||||
|
||||
impl EmptyChunk {
|
||||
fn remove(&self) -> bool {
|
||||
matches!(self, Self::Remove)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::ops::Not;
|
||||
@@ -1254,8 +1368,8 @@ mod tests {
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
use super::{
|
||||
Chunk, ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, Error, LinkedChunk,
|
||||
Position,
|
||||
Chunk, ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, EmptyChunk, Error,
|
||||
LinkedChunk, Position,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -1845,6 +1959,310 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_item_at() -> Result<(), Error> {
|
||||
use super::Update::*;
|
||||
|
||||
let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
|
||||
linked_chunk.push_items_back(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k']);
|
||||
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e', 'f'] ['g', 'h', 'i'] ['j', 'k']);
|
||||
assert_eq!(linked_chunk.len(), 11);
|
||||
|
||||
// Ignore previous updates.
|
||||
let _ = linked_chunk.updates().unwrap().take();
|
||||
|
||||
// Remove the last item of the middle chunk, 3 times. The chunk is empty after
|
||||
// that. The chunk is removed.
|
||||
{
|
||||
let position_of_f = linked_chunk.item_position(|item| *item == 'f').unwrap();
|
||||
let removed_item = linked_chunk.remove_item_at(position_of_f, EmptyChunk::Remove)?;
|
||||
|
||||
assert_eq!(removed_item, 'f');
|
||||
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e'] ['g', 'h', 'i'] ['j', 'k']);
|
||||
assert_eq!(linked_chunk.len(), 10);
|
||||
|
||||
let position_of_e = linked_chunk.item_position(|item| *item == 'e').unwrap();
|
||||
let removed_item = linked_chunk.remove_item_at(position_of_e, EmptyChunk::Remove)?;
|
||||
|
||||
assert_eq!(removed_item, 'e');
|
||||
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d'] ['g', 'h', 'i'] ['j', 'k']);
|
||||
assert_eq!(linked_chunk.len(), 9);
|
||||
|
||||
let position_of_d = linked_chunk.item_position(|item| *item == 'd').unwrap();
|
||||
let removed_item = linked_chunk.remove_item_at(position_of_d, EmptyChunk::Remove)?;
|
||||
|
||||
assert_eq!(removed_item, 'd');
|
||||
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['g', 'h', 'i'] ['j', 'k']);
|
||||
assert_eq!(linked_chunk.len(), 8);
|
||||
|
||||
assert_eq!(
|
||||
linked_chunk.updates().unwrap().take(),
|
||||
&[
|
||||
RemoveItem { at: Position(ChunkIdentifier(1), 2) },
|
||||
RemoveItem { at: Position(ChunkIdentifier(1), 1) },
|
||||
RemoveItem { at: Position(ChunkIdentifier(1), 0) },
|
||||
RemoveChunk(ChunkIdentifier(1)),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Remove the first item of the first chunk, 3 times. The chunk is empty after
|
||||
// that. The chunk is NOT removed because it's the first chunk.
|
||||
{
|
||||
let first_position = linked_chunk.item_position(|item| *item == 'a').unwrap();
|
||||
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
|
||||
|
||||
assert_eq!(removed_item, 'a');
|
||||
assert_items_eq!(linked_chunk, ['b', 'c'] ['g', 'h', 'i'] ['j', 'k']);
|
||||
assert_eq!(linked_chunk.len(), 7);
|
||||
|
||||
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
|
||||
|
||||
assert_eq!(removed_item, 'b');
|
||||
assert_items_eq!(linked_chunk, ['c'] ['g', 'h', 'i'] ['j', 'k']);
|
||||
assert_eq!(linked_chunk.len(), 6);
|
||||
|
||||
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
|
||||
|
||||
assert_eq!(removed_item, 'c');
|
||||
assert_items_eq!(linked_chunk, [] ['g', 'h', 'i'] ['j', 'k']);
|
||||
assert_eq!(linked_chunk.len(), 5);
|
||||
|
||||
assert_eq!(
|
||||
linked_chunk.updates().unwrap().take(),
|
||||
&[
|
||||
RemoveItem { at: Position(ChunkIdentifier(0), 0) },
|
||||
RemoveItem { at: Position(ChunkIdentifier(0), 0) },
|
||||
RemoveItem { at: Position(ChunkIdentifier(0), 0) },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Remove the first item of the middle chunk, 3 times. The chunk is empty after
|
||||
// that. The chunk is removed.
|
||||
{
|
||||
let first_position = linked_chunk.item_position(|item| *item == 'g').unwrap();
|
||||
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
|
||||
|
||||
assert_eq!(removed_item, 'g');
|
||||
assert_items_eq!(linked_chunk, [] ['h', 'i'] ['j', 'k']);
|
||||
assert_eq!(linked_chunk.len(), 4);
|
||||
|
||||
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
|
||||
|
||||
assert_eq!(removed_item, 'h');
|
||||
assert_items_eq!(linked_chunk, [] ['i'] ['j', 'k']);
|
||||
assert_eq!(linked_chunk.len(), 3);
|
||||
|
||||
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
|
||||
|
||||
assert_eq!(removed_item, 'i');
|
||||
assert_items_eq!(linked_chunk, [] ['j', 'k']);
|
||||
assert_eq!(linked_chunk.len(), 2);
|
||||
|
||||
assert_eq!(
|
||||
linked_chunk.updates().unwrap().take(),
|
||||
&[
|
||||
RemoveItem { at: Position(ChunkIdentifier(2), 0) },
|
||||
RemoveItem { at: Position(ChunkIdentifier(2), 0) },
|
||||
RemoveItem { at: Position(ChunkIdentifier(2), 0) },
|
||||
RemoveChunk(ChunkIdentifier(2)),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Remove the last item of the last chunk, twice. The chunk is empty after that.
|
||||
// The chunk is removed.
|
||||
{
|
||||
let position_of_k = linked_chunk.item_position(|item| *item == 'k').unwrap();
|
||||
let removed_item = linked_chunk.remove_item_at(position_of_k, EmptyChunk::Remove)?;
|
||||
|
||||
assert_eq!(removed_item, 'k');
|
||||
#[rustfmt::skip]
|
||||
assert_items_eq!(linked_chunk, [] ['j']);
|
||||
assert_eq!(linked_chunk.len(), 1);
|
||||
|
||||
let position_of_j = linked_chunk.item_position(|item| *item == 'j').unwrap();
|
||||
let removed_item = linked_chunk.remove_item_at(position_of_j, EmptyChunk::Remove)?;
|
||||
|
||||
assert_eq!(removed_item, 'j');
|
||||
assert_items_eq!(linked_chunk, []);
|
||||
assert_eq!(linked_chunk.len(), 0);
|
||||
|
||||
assert_eq!(
|
||||
linked_chunk.updates().unwrap().take(),
|
||||
&[
|
||||
RemoveItem { at: Position(ChunkIdentifier(3), 1) },
|
||||
RemoveItem { at: Position(ChunkIdentifier(3), 0) },
|
||||
RemoveChunk(ChunkIdentifier(3)),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Add a couple more items, delete one, add a gap, and delete more items.
|
||||
{
|
||||
linked_chunk.push_items_back(['a', 'b', 'c', 'd']);
|
||||
|
||||
#[rustfmt::skip]
|
||||
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d']);
|
||||
assert_eq!(linked_chunk.len(), 4);
|
||||
|
||||
let position_of_c = linked_chunk.item_position(|item| *item == 'c').unwrap();
|
||||
linked_chunk.insert_gap_at((), position_of_c)?;
|
||||
|
||||
assert_items_eq!(linked_chunk, ['a', 'b'] [-] ['c'] ['d']);
|
||||
assert_eq!(linked_chunk.len(), 4);
|
||||
|
||||
// Ignore updates.
|
||||
let _ = linked_chunk.updates().unwrap().take();
|
||||
|
||||
let position_of_c = linked_chunk.item_position(|item| *item == 'c').unwrap();
|
||||
let removed_item = linked_chunk.remove_item_at(position_of_c, EmptyChunk::Remove)?;
|
||||
|
||||
assert_eq!(removed_item, 'c');
|
||||
assert_items_eq!(linked_chunk, ['a', 'b'] [-] ['d']);
|
||||
assert_eq!(linked_chunk.len(), 3);
|
||||
|
||||
let position_of_d = linked_chunk.item_position(|item| *item == 'd').unwrap();
|
||||
let removed_item = linked_chunk.remove_item_at(position_of_d, EmptyChunk::Remove)?;
|
||||
|
||||
assert_eq!(removed_item, 'd');
|
||||
assert_items_eq!(linked_chunk, ['a', 'b'] [-]);
|
||||
assert_eq!(linked_chunk.len(), 2);
|
||||
|
||||
let first_position = linked_chunk.item_position(|item| *item == 'a').unwrap();
|
||||
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
|
||||
|
||||
assert_eq!(removed_item, 'a');
|
||||
assert_items_eq!(linked_chunk, ['b'] [-]);
|
||||
assert_eq!(linked_chunk.len(), 1);
|
||||
|
||||
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
|
||||
|
||||
assert_eq!(removed_item, 'b');
|
||||
assert_items_eq!(linked_chunk, [] [-]);
|
||||
assert_eq!(linked_chunk.len(), 0);
|
||||
|
||||
assert_eq!(
|
||||
linked_chunk.updates().unwrap().take(),
|
||||
&[
|
||||
RemoveItem { at: Position(ChunkIdentifier(6), 0) },
|
||||
RemoveChunk(ChunkIdentifier(6)),
|
||||
RemoveItem { at: Position(ChunkIdentifier(4), 0) },
|
||||
RemoveChunk(ChunkIdentifier(4)),
|
||||
RemoveItem { at: Position(ChunkIdentifier(0), 0) },
|
||||
RemoveItem { at: Position(ChunkIdentifier(0), 0) },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_item_at_and_keep_empty_chunks() -> Result<(), Error> {
|
||||
use super::Update::*;
|
||||
|
||||
let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
|
||||
linked_chunk.push_items_back(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']);
|
||||
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e', 'f'] ['g', 'h']);
|
||||
assert_eq!(linked_chunk.len(), 8);
|
||||
|
||||
// Ignore previous updates.
|
||||
let _ = linked_chunk.updates().unwrap().take();
|
||||
|
||||
// Remove all items from the same chunk. The chunk is empty after that. The
|
||||
// chunk is NOT removed because we asked to keep it.
|
||||
{
|
||||
let position = linked_chunk.item_position(|item| *item == 'd').unwrap();
|
||||
let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?;
|
||||
|
||||
assert_eq!(removed_item, 'd');
|
||||
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['e', 'f'] ['g', 'h']);
|
||||
assert_eq!(linked_chunk.len(), 7);
|
||||
|
||||
let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?;
|
||||
|
||||
assert_eq!(removed_item, 'e');
|
||||
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['f'] ['g', 'h']);
|
||||
assert_eq!(linked_chunk.len(), 6);
|
||||
|
||||
let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?;
|
||||
|
||||
assert_eq!(removed_item, 'f');
|
||||
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] [] ['g', 'h']);
|
||||
assert_eq!(linked_chunk.len(), 5);
|
||||
|
||||
assert_eq!(
|
||||
linked_chunk.updates().unwrap().take(),
|
||||
&[
|
||||
RemoveItem { at: Position(ChunkIdentifier(1), 0) },
|
||||
RemoveItem { at: Position(ChunkIdentifier(1), 0) },
|
||||
RemoveItem { at: Position(ChunkIdentifier(1), 0) },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Remove all items from the same chunk. The chunk is empty after that. The
|
||||
// chunk is NOT removed because we asked to keep it.
|
||||
{
|
||||
let position = linked_chunk.item_position(|item| *item == 'g').unwrap();
|
||||
let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?;
|
||||
|
||||
assert_eq!(removed_item, 'g');
|
||||
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] [] ['h']);
|
||||
assert_eq!(linked_chunk.len(), 4);
|
||||
|
||||
let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?;
|
||||
|
||||
assert_eq!(removed_item, 'h');
|
||||
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] [] []);
|
||||
assert_eq!(linked_chunk.len(), 3);
|
||||
|
||||
assert_eq!(
|
||||
linked_chunk.updates().unwrap().take(),
|
||||
&[
|
||||
RemoveItem { at: Position(ChunkIdentifier(2), 0) },
|
||||
RemoveItem { at: Position(ChunkIdentifier(2), 0) },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Remove all items from the same chunk. The chunk is empty after that. The
|
||||
// chunk is NOT removed because we asked to keep it.
|
||||
{
|
||||
let position = linked_chunk.item_position(|item| *item == 'a').unwrap();
|
||||
let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?;
|
||||
|
||||
assert_eq!(removed_item, 'a');
|
||||
assert_items_eq!(linked_chunk, ['b', 'c'] [] []);
|
||||
assert_eq!(linked_chunk.len(), 2);
|
||||
|
||||
let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?;
|
||||
|
||||
assert_eq!(removed_item, 'b');
|
||||
assert_items_eq!(linked_chunk, ['c'] [] []);
|
||||
assert_eq!(linked_chunk.len(), 1);
|
||||
|
||||
let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?;
|
||||
|
||||
assert_eq!(removed_item, 'c');
|
||||
assert_items_eq!(linked_chunk, [] [] []);
|
||||
assert_eq!(linked_chunk.len(), 0);
|
||||
|
||||
assert_eq!(
|
||||
linked_chunk.updates().unwrap().take(),
|
||||
&[
|
||||
RemoveItem { at: Position(ChunkIdentifier(0), 0) },
|
||||
RemoveItem { at: Position(ChunkIdentifier(0), 0) },
|
||||
RemoveItem { at: Position(ChunkIdentifier(0), 0) },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_gap_at() -> Result<(), Error> {
|
||||
use super::Update::*;
|
||||
+195
-11
@@ -76,6 +76,12 @@ pub enum Update<Item, Gap> {
|
||||
items: Vec<Item>,
|
||||
},
|
||||
|
||||
/// An item has been removed inside a chunk of kind Items.
|
||||
RemoveItem {
|
||||
/// The [`Position`] of the item.
|
||||
at: Position,
|
||||
},
|
||||
|
||||
/// The last items of a chunk have been detached, i.e. the chunk has been
|
||||
/// truncated.
|
||||
DetachLastItems {
|
||||
@@ -322,6 +328,22 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<Item, Gap> Drop for UpdatesSubscriber<Item, Gap> {
|
||||
fn drop(&mut self) {
|
||||
// Remove `Self::token` from `UpdatesInner::last_index_per_reader`.
|
||||
// This is important so that the garbage collector can do its jobs correctly
|
||||
// without a dead dangling reader token.
|
||||
if let Some(updates) = self.updates.upgrade() {
|
||||
let mut updates = updates.write().unwrap();
|
||||
|
||||
// Remove the reader token from `UpdatesInner`.
|
||||
// It's safe to ignore the result of `remove` here: `None` means the token was
|
||||
// already removed (note: it should be unreachable).
|
||||
let _ = updates.last_index_per_reader.remove(&self.token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
@@ -495,7 +517,7 @@ mod tests {
|
||||
// | d | e | f | g | h | i |
|
||||
// +---+---+---+---+---+---+
|
||||
//
|
||||
// “main” will have its index updated from 3 to 0.
|
||||
// “main” will have its index updated from 0 to 3.
|
||||
// “other” will have its index updated from 6 to 3.
|
||||
{
|
||||
let updates = linked_chunk.updates().unwrap();
|
||||
@@ -563,20 +585,20 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
struct CounterWaker {
|
||||
number_of_wakeup: Mutex<usize>,
|
||||
}
|
||||
|
||||
impl Wake for CounterWaker {
|
||||
fn wake(self: Arc<Self>) {
|
||||
*self.number_of_wakeup.lock().unwrap() += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_updates_stream() {
|
||||
use super::Update::*;
|
||||
|
||||
struct CounterWaker {
|
||||
number_of_wakeup: Mutex<usize>,
|
||||
}
|
||||
|
||||
impl Wake for CounterWaker {
|
||||
fn wake(self: Arc<Self>) {
|
||||
*self.number_of_wakeup.lock().unwrap() += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let counter_waker = Arc::new(CounterWaker { number_of_wakeup: Mutex::new(0) });
|
||||
let waker = counter_waker.clone().into();
|
||||
let mut context = Context::from_waker(&waker);
|
||||
@@ -646,4 +668,166 @@ mod tests {
|
||||
// Wakers calls have not changed.
|
||||
assert_eq!(*counter_waker.number_of_wakeup.lock().unwrap(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_updates_multiple_streams() {
|
||||
use super::Update::*;
|
||||
|
||||
let counter_waker1 = Arc::new(CounterWaker { number_of_wakeup: Mutex::new(0) });
|
||||
let counter_waker2 = Arc::new(CounterWaker { number_of_wakeup: Mutex::new(0) });
|
||||
|
||||
let waker1 = counter_waker1.clone().into();
|
||||
let waker2 = counter_waker2.clone().into();
|
||||
|
||||
let mut context1 = Context::from_waker(&waker1);
|
||||
let mut context2 = Context::from_waker(&waker2);
|
||||
|
||||
let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
|
||||
|
||||
let updates_subscriber1 = linked_chunk.updates().unwrap().subscribe();
|
||||
pin_mut!(updates_subscriber1);
|
||||
|
||||
// Scope for `updates_subscriber2`.
|
||||
let updates_subscriber2_token = {
|
||||
let updates_subscriber2 = linked_chunk.updates().unwrap().subscribe();
|
||||
pin_mut!(updates_subscriber2);
|
||||
|
||||
// No update, streams are pending.
|
||||
assert_matches!(updates_subscriber1.as_mut().poll_next(&mut context1), Poll::Pending);
|
||||
assert_eq!(*counter_waker1.number_of_wakeup.lock().unwrap(), 0);
|
||||
assert_matches!(updates_subscriber2.as_mut().poll_next(&mut context2), Poll::Pending);
|
||||
assert_eq!(*counter_waker2.number_of_wakeup.lock().unwrap(), 0);
|
||||
|
||||
// Let's generate an update.
|
||||
linked_chunk.push_items_back(['a']);
|
||||
|
||||
// The wakers must have been called.
|
||||
assert_eq!(*counter_waker1.number_of_wakeup.lock().unwrap(), 1);
|
||||
assert_eq!(*counter_waker2.number_of_wakeup.lock().unwrap(), 1);
|
||||
|
||||
// There is an update! Right after that, the streams are pending again.
|
||||
assert_matches!(
|
||||
updates_subscriber1.as_mut().poll_next(&mut context1),
|
||||
Poll::Ready(Some(items)) => {
|
||||
assert_eq!(
|
||||
items,
|
||||
&[PushItems { at: Position(ChunkIdentifier(0), 0), items: vec!['a'] }]
|
||||
);
|
||||
}
|
||||
);
|
||||
assert_matches!(updates_subscriber1.as_mut().poll_next(&mut context1), Poll::Pending);
|
||||
assert_matches!(
|
||||
updates_subscriber2.as_mut().poll_next(&mut context2),
|
||||
Poll::Ready(Some(items)) => {
|
||||
assert_eq!(
|
||||
items,
|
||||
&[PushItems { at: Position(ChunkIdentifier(0), 0), items: vec!['a'] }]
|
||||
);
|
||||
}
|
||||
);
|
||||
assert_matches!(updates_subscriber2.as_mut().poll_next(&mut context2), Poll::Pending);
|
||||
|
||||
// Let's generate two other updates.
|
||||
linked_chunk.push_items_back(['b']);
|
||||
linked_chunk.push_items_back(['c']);
|
||||
|
||||
// A waker is consumed when called. The first call to `push_items_back` will
|
||||
// call and consume the wakers. The second call to `push_items_back` will do
|
||||
// nothing as the wakers have been consumed. New wakers will be registered on
|
||||
// polling.
|
||||
//
|
||||
// So, the waker must have been called only once for the two updates.
|
||||
assert_eq!(*counter_waker1.number_of_wakeup.lock().unwrap(), 2);
|
||||
assert_eq!(*counter_waker2.number_of_wakeup.lock().unwrap(), 2);
|
||||
|
||||
// Let's poll `updates_subscriber1` only.
|
||||
assert_matches!(
|
||||
updates_subscriber1.as_mut().poll_next(&mut context1),
|
||||
Poll::Ready(Some(items)) => {
|
||||
assert_eq!(
|
||||
items,
|
||||
&[
|
||||
PushItems { at: Position(ChunkIdentifier(0), 1), items: vec!['b'] },
|
||||
PushItems { at: Position(ChunkIdentifier(0), 2), items: vec!['c'] },
|
||||
]
|
||||
);
|
||||
}
|
||||
);
|
||||
assert_matches!(updates_subscriber1.as_mut().poll_next(&mut context1), Poll::Pending);
|
||||
|
||||
// For the sake of this test, we also need to advance the main reader token.
|
||||
let _ = linked_chunk.updates().unwrap().take();
|
||||
let _ = linked_chunk.updates().unwrap().take();
|
||||
|
||||
// If we inspect the garbage collector state, `a`, `b` and `c` should still be
|
||||
// present because not all of them have been consumed by `updates_subscriber2`
|
||||
// yet.
|
||||
{
|
||||
let updates = linked_chunk.updates().unwrap();
|
||||
|
||||
let inner = updates.inner.read().unwrap();
|
||||
|
||||
// Inspect number of updates in memory.
|
||||
// We get 2 because the garbage collector runs before data are taken, not after:
|
||||
// `updates_subscriber2` has read `a` only, so `b` and `c` remain.
|
||||
assert_eq!(inner.len(), 2);
|
||||
|
||||
// Inspect the indices.
|
||||
let indices = &inner.last_index_per_reader;
|
||||
|
||||
assert_eq!(indices.get(&updates_subscriber1.token), Some(&2));
|
||||
assert_eq!(indices.get(&updates_subscriber2.token), Some(&0));
|
||||
}
|
||||
|
||||
// Poll `updates_subscriber1` again: there is no new update so it must be
|
||||
// pending.
|
||||
assert_matches!(updates_subscriber1.as_mut().poll_next(&mut context1), Poll::Pending);
|
||||
|
||||
// The state of the garbage collector is unchanged: `a`, `b` and `c` are still
|
||||
// in memory.
|
||||
{
|
||||
let updates = linked_chunk.updates().unwrap();
|
||||
|
||||
let inner = updates.inner.read().unwrap();
|
||||
|
||||
// Inspect number of updates in memory. Value is unchanged.
|
||||
assert_eq!(inner.len(), 2);
|
||||
|
||||
// Inspect the indices. They are unchanged.
|
||||
let indices = &inner.last_index_per_reader;
|
||||
|
||||
assert_eq!(indices.get(&updates_subscriber1.token), Some(&2));
|
||||
assert_eq!(indices.get(&updates_subscriber2.token), Some(&0));
|
||||
}
|
||||
|
||||
updates_subscriber2.token
|
||||
// Drop `updates_subscriber2`!
|
||||
};
|
||||
|
||||
// `updates_subscriber2` has been dropped. Poll `updates_subscriber1` again:
|
||||
// still no new update, but it will run the garbage collector again, and this
|
||||
// time `updates_subscriber2` is not “retaining” `b` and `c`. The garbage
|
||||
// collector must be empty.
|
||||
assert_matches!(updates_subscriber1.as_mut().poll_next(&mut context1), Poll::Pending);
|
||||
|
||||
// Inspect the garbage collector.
|
||||
{
|
||||
let updates = linked_chunk.updates().unwrap();
|
||||
|
||||
let inner = updates.inner.read().unwrap();
|
||||
|
||||
// Inspect number of updates in memory.
|
||||
assert_eq!(inner.len(), 0);
|
||||
|
||||
// Inspect the indices.
|
||||
let indices = &inner.last_index_per_reader;
|
||||
|
||||
assert_eq!(indices.get(&updates_subscriber1.token), Some(&0));
|
||||
assert_eq!(indices.get(&updates_subscriber2_token), None); // token is unknown!
|
||||
}
|
||||
|
||||
// When dropping the `LinkedChunk`, it closes the stream.
|
||||
drop(linked_chunk);
|
||||
assert_matches!(updates_subscriber1.as_mut().poll_next(&mut context1), Poll::Ready(None));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user