Compare commits
267 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb573117e1 | |||
| ff7077b742 | |||
| bb70229dd8 | |||
| 03947618ff | |||
| b18e7d71ed | |||
| 612ba6fa29 | |||
| db39c6bea6 | |||
| 5f3b56a987 | |||
| 373709fb38 | |||
| 5d8ad3a4a9 | |||
| 0ca35d6c4a | |||
| daeffc07b3 | |||
| bd15f4ecbe | |||
| f17f4e2bf6 | |||
| 177ec1216f | |||
| 512a2d2662 | |||
| 95582a6c3c | |||
| 866b5fea40 | |||
| 34ea42aec0 | |||
| cae7e43b91 | |||
| 34d15a4d37 | |||
| f6cb8186c6 | |||
| 47044b1a23 | |||
| 05d46e6027 | |||
| 338769508e | |||
| 93ebae6601 | |||
| 780c264e59 | |||
| 9a899c1cb1 | |||
| 2703f7f7d4 | |||
| 8d2e672996 | |||
| 5a25e65da3 | |||
| c197808b42 | |||
| ed34719295 | |||
| a052a79aaf | |||
| b6542477bb | |||
| a573b650c9 | |||
| 789bd317b3 | |||
| 2b39476d9b | |||
| 6dcefe49c2 | |||
| 150d9e4b05 | |||
| 54bd1d7931 | |||
| 7ae31d0cb1 | |||
| f7f58dfd71 | |||
| 780a4630e4 | |||
| 3356e0cc82 | |||
| fda374ee81 | |||
| 0264e49968 | |||
| d42c449612 | |||
| 925d10f2ff | |||
| 4402f59e74 | |||
| 20184552a8 | |||
| 832fedb05e | |||
| eeb14f6cbe | |||
| a562f73b1e | |||
| 7295f29055 | |||
| 723d7973d5 | |||
| d5e7a9c949 | |||
| 8f064581d6 | |||
| 634edf2b65 | |||
| 935e4df927 | |||
| 1d72d2774f | |||
| 1e72131e7f | |||
| e8b3949db3 | |||
| c501a39ad4 | |||
| a04f9187f8 | |||
| 32e2070f56 | |||
| 4ee96aaffc | |||
| 0783cf89ba | |||
| cf02e694f2 | |||
| cf178d603c | |||
| ee94c86164 | |||
| 3526761580 | |||
| 9a08975c8e | |||
| 6b56c9efd8 | |||
| 0f2ada0958 | |||
| 0d17ea353f | |||
| 13e26b13e7 | |||
| 72f1bd6180 | |||
| e32ea1627e | |||
| ed1f2e29ed | |||
| 92cb18207e | |||
| 80f6b8d2cd | |||
| 05969fefde | |||
| 81c962238a | |||
| 56218ee5d7 | |||
| aa9138b281 | |||
| 6f231523b3 | |||
| 943b3fbd91 | |||
| 40ff880597 | |||
| 0647be1bc3 | |||
| b069b20e18 | |||
| 91b73a2b16 | |||
| 14d0f6877a | |||
| a2210bce48 | |||
| 68cb85a2b2 | |||
| 72fcc50f80 | |||
| 5721c3622d | |||
| 50eb46dc82 | |||
| 8aae16ffd7 | |||
| e402ed4ce8 | |||
| a1a04ee513 | |||
| affdc25256 | |||
| 8db78efbbc | |||
| d8184e72eb | |||
| 3bd57d4307 | |||
| 42193f1b06 | |||
| a277e6d37f | |||
| bf6fa4cd55 | |||
| 6501a44e6a | |||
| ee30008f38 | |||
| 22cb8a1878 | |||
| 111f916a78 | |||
| a6e1f05957 | |||
| 0b64c68191 | |||
| 713039279c | |||
| d317e5d73c | |||
| ee93c278df | |||
| 1009ea86ae | |||
| 7d8e7af308 | |||
| 136522c694 | |||
| 6801811226 | |||
| a4434d79c9 | |||
| e0b1b5dc05 | |||
| 1a63d8f0b7 | |||
| 5bf3b11edf | |||
| 8f1722f2a8 | |||
| 5d95387935 | |||
| bd93a9a40e | |||
| 5cde4a6630 | |||
| de5511f009 | |||
| 9bdd9fa831 | |||
| 48bb3dbbe7 | |||
| b8bf847fc1 | |||
| 17812b6949 | |||
| bab979aaf4 | |||
| 42778dc79d | |||
| a948be9c85 | |||
| 9c381c1022 | |||
| 9002f82659 | |||
| 5f7fb4699a | |||
| 5907104e0e | |||
| d7dff5b026 | |||
| cabde8ed11 | |||
| b02fd92ad0 | |||
| 9be8578aff | |||
| 4f28dd85bf | |||
| 74119e8861 | |||
| e76b8f7e15 | |||
| 31bd5c6790 | |||
| 50f036d283 | |||
| 8c73f0c655 | |||
| 8de76deb1b | |||
| b65728d46f | |||
| 0b4b4ea791 | |||
| 552ab81739 | |||
| d49d12249a | |||
| ed1d406b72 | |||
| 80a48f53ad | |||
| 51cfaaacee | |||
| 2f9866cf04 | |||
| 7de74e2c04 | |||
| 019de4ffa0 | |||
| 9f1e3c179b | |||
| 17e17f0b9c | |||
| 5da36d13c8 | |||
| cce322f9c8 | |||
| ed3b03f454 | |||
| 27e1cded2e | |||
| ad3d1fb6b3 | |||
| d2fecb6701 | |||
| 685386df13 | |||
| f94b202341 | |||
| d1a6956e77 | |||
| 2d2215edbe | |||
| bcd0d20e2f | |||
| ba5881355d | |||
| 1072d0a019 | |||
| 783c86aa78 | |||
| 5564fe8852 | |||
| e1f0037fd5 | |||
| daa984f7de | |||
| aa0eb760de | |||
| 9ed65bc321 | |||
| ce95b6089f | |||
| c6ba71ae33 | |||
| e57d38cf57 | |||
| 9bea0cff24 | |||
| 197da2c585 | |||
| d2ecd745f6 | |||
| e99939db85 | |||
| 600a708e7b | |||
| a94a5f1716 | |||
| 46064680ce | |||
| 6fe5acfc97 | |||
| 3369903766 | |||
| a0c86d9645 | |||
| 7a454888a3 | |||
| 37f52e1c6c | |||
| 185423539e | |||
| 9e20659d5d | |||
| 7783188769 | |||
| 514af54c4c | |||
| ad615b7612 | |||
| a1b7906a7d | |||
| 79c8d2c345 | |||
| dcf6af405d | |||
| bb598b61a5 | |||
| 1c554c4912 | |||
| 21f8b7ed31 | |||
| 23ee8e25dd | |||
| 1098095846 | |||
| 3e7d7e8a31 | |||
| 2c45316bcb | |||
| 8dc7c1f876 | |||
| db84936dcd | |||
| 75d7d07013 | |||
| d4d5f45edc | |||
| d0257d1cb2 | |||
| ecf44348cf | |||
| cc8bc05537 | |||
| 728d646ce2 | |||
| ca397dca0f | |||
| 1fbe6815c3 | |||
| c61f70727f | |||
| 2abbf58825 | |||
| b979b2ea1e | |||
| 24b968ad39 | |||
| faa8aa2b9c | |||
| db9ee9d87b | |||
| 1dbb494b94 | |||
| fe52b4cb78 | |||
| 5519442ad8 | |||
| 88363d8033 | |||
| fb5d8f29ac | |||
| 912b121d27 | |||
| 2e975d9b19 | |||
| edc93e62b4 | |||
| 9d6ffa951f | |||
| 079ec023b7 | |||
| e55a1c7e00 | |||
| ddd737e4d8 | |||
| 38a15afc9c | |||
| fa93daabd2 | |||
| 6b0987385e | |||
| 48fbda844f | |||
| bc70f3c051 | |||
| d2f255d613 | |||
| bf86b168d7 | |||
| e5ca44bb04 | |||
| 1f563c964c | |||
| 9a9730d59e | |||
| af3ce4b32b | |||
| 03f0c3a001 | |||
| 639833acf1 | |||
| 60893d2797 | |||
| 9e45111d8b | |||
| 0080f17c1f | |||
| fa47af3dd6 | |||
| c4ff07124b | |||
| 900cf5d071 | |||
| 8a6ced0e8f | |||
| f20401c657 | |||
| b987fc1de2 | |||
| efeac2ef39 | |||
| 6b80055bd2 | |||
| 0af53e99ee | |||
| bc0c2a6be2 |
@@ -24,6 +24,7 @@ allow = [
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
]
|
||||
exceptions = [
|
||||
@@ -54,6 +55,8 @@ 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
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-06-25
|
||||
toolchain: nightly-2024-11-26
|
||||
components: rustfmt
|
||||
|
||||
- name: Run Benchmarks
|
||||
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
test-apple:
|
||||
name: matrix-rust-components-swift
|
||||
needs: xtask
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
@@ -175,7 +175,7 @@ jobs:
|
||||
run: swift test
|
||||
|
||||
- name: Build Framework
|
||||
run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=dev
|
||||
run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=reldbg
|
||||
|
||||
complement-crypto:
|
||||
name: "Run Complement Crypto tests"
|
||||
@@ -186,7 +186,7 @@ jobs:
|
||||
|
||||
test-crypto-apple-framework-generation:
|
||||
name: Generate Crypto FFI Apple XCFramework
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
|
||||
@@ -288,7 +288,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-06-25
|
||||
toolchain: nightly-2024-11-26
|
||||
components: rustfmt
|
||||
|
||||
- name: Cargo fmt
|
||||
@@ -304,7 +304,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check the spelling of the files in our repo
|
||||
uses: crate-ci/typos@v1.27.3
|
||||
uses: crate-ci/typos@v1.28.3
|
||||
|
||||
clippy:
|
||||
name: Run clippy
|
||||
@@ -323,7 +323,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-06-25
|
||||
toolchain: nightly-2024-11-26
|
||||
components: clippy
|
||||
|
||||
- name: Load cache
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-06-25
|
||||
toolchain: nightly-2024-11-26
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
env:
|
||||
RUSTDOCFLAGS: "--enable-index-page -Zunstable-options --cfg docsrs -Dwarnings"
|
||||
run:
|
||||
cargo doc --no-deps --workspace --features docsrs
|
||||
cargo doc --no-deps --workspace --features docsrs --exclude=xtask
|
||||
|
||||
- name: Upload artifact
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
os-name: 🐧
|
||||
cachekey-id: linux
|
||||
|
||||
- os: macos-14
|
||||
- os: macos-15
|
||||
os-name: 🍏
|
||||
cachekey-id: macos
|
||||
|
||||
|
||||
+45
-36
@@ -45,9 +45,46 @@ that is, just the branch name.)
|
||||
|
||||
# Writing changelog entries
|
||||
|
||||
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.
|
||||
Our goal is to maintain clear, concise, and informative changelogs that
|
||||
accurately document changes in the project. Changelog entries should be written
|
||||
manually for each crate in the `/crates/$CRATE_NAME/Changelog.md` file.
|
||||
|
||||
Be sure to include a link to the pull request for additional context. A
|
||||
well-written changelog entry should be understandable even to those who may not
|
||||
be deeply familiar with the project. Provide enough context to ensure clarity
|
||||
and ease of understanding.
|
||||
|
||||
A couple of examples of bad changelog entry would look like:
|
||||
|
||||
```markdown
|
||||
- Fixed a panic.
|
||||
```
|
||||
|
||||
```markdown
|
||||
- Added the Bar function to Foo.
|
||||
```
|
||||
|
||||
A good example of a changelog entry could look like the following:
|
||||
|
||||
```markdown
|
||||
- Use the inviter's server name and the server name from the room alias as
|
||||
fallback values for the via parameter when requesting the room summary from
|
||||
the homeserver. This ensures requests succeed even when the room being
|
||||
previewed is hosted on a federated server.
|
||||
([#4357](https://github.com/matrix-org/matrix-rust-sdk/pull/4357))
|
||||
```
|
||||
|
||||
For security-related changelog entries, please include the following additional
|
||||
details alongside the pull request number:
|
||||
|
||||
* Impact: Clearly describe the issue's potential impact on users or systems.
|
||||
* CVE Number: If available, include the CVE (Common Vulnerabilities and Exposures) identifier.
|
||||
* GitHub Advisory Link: Provide a link to the corresponding GitHub security advisory for further context.
|
||||
|
||||
```markdown
|
||||
- Use a constant-time Base64 encoder for secret key material to mitigate
|
||||
side-channel attacks leaking secret key material ([#156](https://github.com/matrix-org/vodozemac/pull/156)) (Low, [CVE-2024-40640](https://www.cve.org/CVERecord?id=CVE-2024-40640), [GHSA-j8cm-g7r6-hfpq](https://github.com/matrix-org/vodozemac/security/advisories/GHSA-j8cm-g7r6-hfpq)).
|
||||
```
|
||||
|
||||
## Commit message format
|
||||
|
||||
@@ -74,45 +111,20 @@ The type of changes which will be included in changelogs is one of the following
|
||||
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.
|
||||
vulnerability metadata, which should also be reflected in the corresponding
|
||||
changelog entry.
|
||||
|
||||
Security issues have some additional git-trailers:
|
||||
The metadata must be included in the following 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.
|
||||
|
||||
Please include all of the fields that are available.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
@@ -131,9 +143,6 @@ 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
|
||||
|
||||
Generated
+515
-231
File diff suppressed because it is too large
Load Diff
+54
-33
@@ -18,34 +18,45 @@ default-members = ["benchmarks", "crates/*", "labs/*"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
rust-version = "1.76"
|
||||
rust-version = "1.82"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.68"
|
||||
assert-json-diff = "2"
|
||||
anyhow = "1.0.93"
|
||||
aquamarine = "0.6.0"
|
||||
assert-json-diff = "2.0.2"
|
||||
assert_matches = "1.5.0"
|
||||
assert_matches2 = "0.1.1"
|
||||
assert_matches2 = "0.1.2"
|
||||
async-rx = "0.1.3"
|
||||
async-stream = "0.3.3"
|
||||
async-trait = "0.1.60"
|
||||
async-stream = "0.3.5"
|
||||
async-trait = "0.1.83"
|
||||
as_variant = "1.2.0"
|
||||
base64 = "0.22.0"
|
||||
byteorder = "1.4.3"
|
||||
base64 = "0.22.1"
|
||||
byteorder = "1.5.0"
|
||||
chrono = "0.4.38"
|
||||
eyeball = { version = "0.8.8", features = ["tracing"] }
|
||||
eyeball-im = { version = "0.5.1", features = ["tracing"] }
|
||||
eyeball-im-util = "0.7.0"
|
||||
futures-core = "0.3.28"
|
||||
futures-core = "0.3.31"
|
||||
futures-executor = "0.3.21"
|
||||
futures-util = "0.3.26"
|
||||
growable-bloom-filter = "2.1.0"
|
||||
futures-util = "0.3.31"
|
||||
gloo-timers = "0.3.0"
|
||||
growable-bloom-filter = "2.1.1"
|
||||
hkdf = "0.12.4"
|
||||
hmac = "0.12.1"
|
||||
http = "1.1.0"
|
||||
imbl = "3.0.0"
|
||||
itertools = "0.12.0"
|
||||
once_cell = "1.16.0"
|
||||
pin-project-lite = "0.2.9"
|
||||
indexmap = "2.6.0"
|
||||
itertools = "0.13.0"
|
||||
js-sys = "0.3.69"
|
||||
mime = "0.3.17"
|
||||
once_cell = "1.20.2"
|
||||
pbkdf2 = { version = "0.12.2" }
|
||||
pin-project-lite = "0.2.15"
|
||||
proptest = { version = "1.5.0", default-features = false, features = ["std"] }
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.12.4", default-features = false }
|
||||
ruma = { version = "0.11.1", features = [
|
||||
rmp-serde = "1.3.0"
|
||||
ruma = { version = "0.12.0", features = [
|
||||
"client-api-c",
|
||||
"compat-upload-signatures",
|
||||
"compat-user-id",
|
||||
@@ -58,38 +69,45 @@ ruma = { version = "0.11.1", features = [
|
||||
"unstable-msc3489",
|
||||
"unstable-msc4075",
|
||||
"unstable-msc4140",
|
||||
"unstable-msc4171",
|
||||
] }
|
||||
ruma-common = "0.14.1"
|
||||
ruma-common = "0.15.0"
|
||||
serde = "1.0.151"
|
||||
serde_html_form = "0.2.0"
|
||||
serde_json = "1.0.91"
|
||||
sha2 = "0.10.8"
|
||||
similar-asserts = "1.5.0"
|
||||
similar-asserts = "1.6.0"
|
||||
stream_assert = "0.1.1"
|
||||
thiserror = "1.0.38"
|
||||
tokio = { version = "1.39.1", default-features = false, features = ["sync"] }
|
||||
tempfile = "3.9.0"
|
||||
thiserror = "2.0.3"
|
||||
tokio = { version = "1.41.1", default-features = false, features = ["sync"] }
|
||||
tokio-stream = "0.1.14"
|
||||
tracing = { version = "0.1.40", default-features = false, features = ["std"] }
|
||||
tracing-core = "0.1.32"
|
||||
tracing-subscriber = "0.3.18"
|
||||
unicode-normalization = "0.1.24"
|
||||
uniffi = { version = "0.28.0" }
|
||||
uniffi_bindgen = { version = "0.28.0" }
|
||||
url = "2.5.0"
|
||||
vodozemac = { version = "0.8.0", features = ["insecure-pk-encryption"] }
|
||||
wiremock = "0.6.0"
|
||||
zeroize = "1.6.0"
|
||||
url = "2.5.4"
|
||||
uuid = "1.11.0"
|
||||
vodozemac = { version = "0.8.1", features = ["insecure-pk-encryption"] }
|
||||
wasm-bindgen = "0.2.84"
|
||||
wasm-bindgen-test = "0.3.33"
|
||||
web-sys = "0.3.69"
|
||||
wiremock = "0.6.2"
|
||||
zeroize = "1.8.1"
|
||||
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.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 = { path = "crates/matrix-sdk", version = "0.9.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.9.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.9.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.9.0" }
|
||||
matrix-sdk-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-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.9.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.9.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.9.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.9.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.7.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.8.0", default-features = false }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.9.0", default-features = false }
|
||||
|
||||
# Default release profile, select with `--release`
|
||||
[profile.release]
|
||||
@@ -131,7 +149,10 @@ paranoid-android = { git = "https://github.com/element-hq/paranoid-android.git",
|
||||
[workspace.lints.rust]
|
||||
rust_2018_idioms = "warn"
|
||||
semicolon_in_expressions_from_macros = "warn"
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(tarpaulin_include)', # Used by tarpaulin (code coverage)
|
||||
'cfg(ruma_unstable_exhaustive_types)', # Used by Ruma's EventContent derive macro
|
||||
] }
|
||||
unused_extern_crates = "warn"
|
||||
unused_import_braces = "warn"
|
||||
unused_qualifications = "warn"
|
||||
|
||||
@@ -24,10 +24,6 @@ The rust-sdk consists of multiple crates that can be picked at your convenience:
|
||||
- **matrix-sdk-crypto** - No (network) IO encryption state machine that can be
|
||||
used to add Matrix E2EE support to your client or client library.
|
||||
|
||||
## Minimum Supported Rust Version (MSRV)
|
||||
|
||||
These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.70`.
|
||||
|
||||
## Status
|
||||
|
||||
The library is in an alpha state, things that are implemented generally work but
|
||||
|
||||
+2
-2
@@ -17,8 +17,8 @@ The procedure is as follows:
|
||||
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.
|
||||
2. Prepare the release. This will update the `README.md`, set the versions in
|
||||
the `CHANGELOG.md` file, and bump the version in the `Cargo.toml` file.
|
||||
|
||||
```bash
|
||||
cargo xtask release prepare --execute minor|patch|rc
|
||||
|
||||
@@ -23,7 +23,7 @@ tokio = { workspace = true, default-features = false, features = ["rt-multi-thre
|
||||
wiremock = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
pprof = { version = "0.13.0", features = ["flamegraph", "criterion"] }
|
||||
pprof = { version = "0.14.0", features = ["flamegraph", "criterion"] }
|
||||
|
||||
[[bench]]
|
||||
name = "crypto_bench"
|
||||
|
||||
@@ -2,15 +2,16 @@ use std::{sync::Arc, time::Duration};
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::{
|
||||
config::SyncSettings,
|
||||
test_utils::{events::EventFactory, logged_in_client_with_server},
|
||||
utils::IntoRawStateEventContent,
|
||||
config::SyncSettings, test_utils::logged_in_client_with_server, utils::IntoRawStateEventContent,
|
||||
};
|
||||
use matrix_sdk_base::{
|
||||
store::StoreConfig, BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore,
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteStateStore;
|
||||
use matrix_sdk_test::{EventBuilder, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder};
|
||||
use matrix_sdk_test::{
|
||||
event_factory::EventFactory, EventBuilder, JoinedRoomBuilder, StateTestEvent,
|
||||
SyncResponseBuilder,
|
||||
};
|
||||
use matrix_sdk_ui::{timeline::TimelineFocus, Timeline};
|
||||
use ruma::{
|
||||
api::client::membership::get_member_events,
|
||||
|
||||
@@ -13,6 +13,7 @@ let package = Package(
|
||||
],
|
||||
products: [
|
||||
.library(name: "MatrixRustSDK",
|
||||
type: .dynamic,
|
||||
targets: ["MatrixRustSDK"]),
|
||||
],
|
||||
targets: [
|
||||
|
||||
@@ -71,10 +71,6 @@ $ cp ../../target/aarch64-linux-android/debug/libmatrix_crypto.so \
|
||||
/home/example/matrix-sdk-android/src/main/jniLibs/aarch64/libuniffi_olm.so
|
||||
```
|
||||
|
||||
## Minimum Supported Rust Version (MSRV)
|
||||
|
||||
These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.62`.
|
||||
|
||||
## License
|
||||
|
||||
[Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::{env, error::Error};
|
||||
use std::{env, error::Error, path::PathBuf, process::Command};
|
||||
|
||||
use vergen::EmitBuilder;
|
||||
|
||||
/// Adds a temporary workaround for an issue with the Rust compiler and Android
|
||||
/// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717.
|
||||
/// The workaround comes from: https://github.com/mozilla/application-services/pull/5442
|
||||
/// The workaround is based on: https://github.com/mozilla/application-services/pull/5442
|
||||
///
|
||||
/// IMPORTANT: if you modify this, make sure to modify
|
||||
/// [../matrix-sdk-ffi/build.rs] too!
|
||||
@@ -12,26 +12,45 @@ fn setup_x86_64_android_workaround() {
|
||||
let target_os = env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not set");
|
||||
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").expect("CARGO_CFG_TARGET_ARCH not set");
|
||||
if target_arch == "x86_64" && target_os == "android" {
|
||||
let android_ndk_home = env::var("ANDROID_NDK_HOME").expect("ANDROID_NDK_HOME not set");
|
||||
let build_os = match env::consts::OS {
|
||||
"linux" => "linux",
|
||||
"macos" => "darwin",
|
||||
"windows" => "windows",
|
||||
_ => panic!(
|
||||
"Unsupported OS. You must use either Linux, MacOS or Windows to build the crate."
|
||||
),
|
||||
};
|
||||
const DEFAULT_CLANG_VERSION: &str = "18";
|
||||
let clang_version =
|
||||
env::var("NDK_CLANG_VERSION").unwrap_or_else(|_| DEFAULT_CLANG_VERSION.to_owned());
|
||||
let linux_x86_64_lib_dir = format!(
|
||||
"toolchains/llvm/prebuilt/{build_os}-x86_64/lib/clang/{clang_version}/lib/linux/"
|
||||
// Configure rust to statically link against the `libclang_rt.builtins` supplied
|
||||
// with clang.
|
||||
|
||||
// cargo-ndk sets CC_x86_64-linux-android to the path to `clang`, within the
|
||||
// Android NDK.
|
||||
let clang_path = PathBuf::from(
|
||||
env::var("CC_x86_64-linux-android").expect("CC_x86_64-linux-android not set"),
|
||||
);
|
||||
println!("cargo:rustc-link-search={android_ndk_home}/{linux_x86_64_lib_dir}");
|
||||
|
||||
// clang_path should now look something like
|
||||
// `.../sdk/ndk/28.0.12674087/toolchains/llvm/prebuilt/linux-x86_64/bin/clang`.
|
||||
// We strip `/bin/clang` from the end to get the toolchain path.
|
||||
let toolchain_path = clang_path
|
||||
.ancestors()
|
||||
.nth(2)
|
||||
.expect("could not find NDK toolchain path")
|
||||
.to_str()
|
||||
.expect("NDK toolchain path is not valid UTF-8");
|
||||
|
||||
let clang_version = get_clang_major_version(&clang_path);
|
||||
|
||||
println!("cargo:rustc-link-search={toolchain_path}/lib/clang/{clang_version}/lib/linux/");
|
||||
println!("cargo:rustc-link-lib=static=clang_rt.builtins-x86_64-android");
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the clang binary at `clang_path`, and return its major version number
|
||||
fn get_clang_major_version(clang_path: &PathBuf) -> String {
|
||||
let clang_output =
|
||||
Command::new(clang_path).arg("-dumpversion").output().expect("failed to start clang");
|
||||
|
||||
if !clang_output.status.success() {
|
||||
panic!("failed to run clang: {}", String::from_utf8_lossy(&clang_output.stderr));
|
||||
}
|
||||
|
||||
let clang_version = String::from_utf8(clang_output.stdout).expect("clang output is not utf8");
|
||||
clang_version.split('.').next().expect("could not parse clang output").to_owned()
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
setup_x86_64_android_workaround();
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
use std::{mem::ManuallyDrop, sync::Arc};
|
||||
|
||||
use matrix_sdk_crypto::dehydrated_devices::{
|
||||
DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices,
|
||||
RehydratedDevice as InnerRehydratedDevice,
|
||||
use matrix_sdk_crypto::{
|
||||
dehydrated_devices::{
|
||||
DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices,
|
||||
RehydratedDevice as InnerRehydratedDevice,
|
||||
},
|
||||
store::DehydratedDeviceKey as InnerDehydratedDeviceKey,
|
||||
};
|
||||
use ruma::{api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw, OwnedDeviceId};
|
||||
use serde_json::json;
|
||||
use tokio::runtime::Handle;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{CryptoStoreError, DehydratedDeviceKey};
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
@@ -22,6 +26,8 @@ pub enum DehydrationError {
|
||||
Store(#[from] matrix_sdk_crypto::CryptoStoreError),
|
||||
#[error("The pickle key has an invalid length, expected 32 bytes, got {0}")]
|
||||
PickleKeyLength(usize),
|
||||
#[error(transparent)]
|
||||
Rand(#[from] rand::Error),
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::dehydrated_devices::DehydrationError> for DehydrationError {
|
||||
@@ -33,6 +39,9 @@ impl From<matrix_sdk_crypto::dehydrated_devices::DehydrationError> for Dehydrati
|
||||
Self::MissingSigningKey(e)
|
||||
}
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::Store(e) => Self::Store(e),
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::PickleKeyLength(l) => {
|
||||
Self::PickleKeyLength(l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,14 +75,14 @@ impl DehydratedDevices {
|
||||
|
||||
pub fn rehydrate(
|
||||
&self,
|
||||
pickle_key: Vec<u8>,
|
||||
pickle_key: &DehydratedDeviceKey,
|
||||
device_id: String,
|
||||
device_data: String,
|
||||
) -> Result<Arc<RehydratedDevice>, DehydrationError> {
|
||||
let device_data: Raw<_> = serde_json::from_str(&device_data)?;
|
||||
let device_id: OwnedDeviceId = device_id.into();
|
||||
|
||||
let mut key = get_pickle_key(&pickle_key)?;
|
||||
let key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?;
|
||||
|
||||
let ret = RehydratedDevice {
|
||||
runtime: self.runtime.to_owned(),
|
||||
@@ -85,10 +94,41 @@ impl DehydratedDevices {
|
||||
}
|
||||
.into();
|
||||
|
||||
key.zeroize();
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Get the cached dehydrated device pickle key if any.
|
||||
///
|
||||
/// None if the key was not previously cached (via
|
||||
/// [`Self::save_dehydrated_device_pickle_key`]).
|
||||
///
|
||||
/// Should be used to periodically rotate the dehydrated device to avoid
|
||||
/// OTK exhaustion and accumulation of to_device messages.
|
||||
pub fn get_dehydrated_device_key(
|
||||
&self,
|
||||
) -> Result<Option<crate::DehydratedDeviceKey>, CryptoStoreError> {
|
||||
Ok(self
|
||||
.runtime
|
||||
.block_on(self.inner.get_dehydrated_device_pickle_key())?
|
||||
.map(crate::DehydratedDeviceKey::from))
|
||||
}
|
||||
|
||||
/// Store the dehydrated device pickle key in the crypto store.
|
||||
///
|
||||
/// This is useful if the client wants to periodically rotate dehydrated
|
||||
/// devices to avoid OTK exhaustion and accumulated to_device problems.
|
||||
pub fn save_dehydrated_device_key(
|
||||
&self,
|
||||
pickle_key: &crate::DehydratedDeviceKey,
|
||||
) -> Result<(), CryptoStoreError> {
|
||||
let pickle_key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?;
|
||||
Ok(self.runtime.block_on(self.inner.save_dehydrated_device_pickle_key(&pickle_key))?)
|
||||
}
|
||||
|
||||
/// Deletes the previously stored dehydrated device pickle key.
|
||||
pub fn delete_dehydrated_device_key(&self) -> Result<(), CryptoStoreError> {
|
||||
Ok(self.runtime.block_on(self.inner.delete_dehydrated_device_pickle_key())?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
@@ -138,15 +178,13 @@ impl DehydratedDevice {
|
||||
pub fn keys_for_upload(
|
||||
&self,
|
||||
device_display_name: String,
|
||||
pickle_key: Vec<u8>,
|
||||
pickle_key: &DehydratedDeviceKey,
|
||||
) -> Result<UploadDehydratedDeviceRequest, DehydrationError> {
|
||||
let mut key = get_pickle_key(&pickle_key)?;
|
||||
let key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?;
|
||||
|
||||
let request =
|
||||
self.runtime.block_on(self.inner.keys_for_upload(device_display_name, &key))?;
|
||||
|
||||
key.zeroize();
|
||||
|
||||
Ok(request.into())
|
||||
}
|
||||
}
|
||||
@@ -177,15 +215,36 @@ impl From<dehydrated_device::put_dehydrated_device::unstable::Request>
|
||||
}
|
||||
}
|
||||
|
||||
fn get_pickle_key(pickle_key: &[u8]) -> Result<Box<[u8; 32]>, DehydrationError> {
|
||||
let pickle_key_length = pickle_key.len();
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{dehydrated_devices::DehydrationError, DehydratedDeviceKey};
|
||||
|
||||
if pickle_key_length == 32 {
|
||||
let mut key = Box::new([0u8; 32]);
|
||||
key.copy_from_slice(pickle_key);
|
||||
#[test]
|
||||
fn test_creating_dehydrated_key() {
|
||||
let result = DehydratedDeviceKey::new();
|
||||
assert!(result.is_ok());
|
||||
let dehydrated_device_key = result.unwrap();
|
||||
let base_64 = dehydrated_device_key.to_base64();
|
||||
let inner_bytes = dehydrated_device_key.inner;
|
||||
|
||||
Ok(key)
|
||||
} else {
|
||||
Err(DehydrationError::PickleKeyLength(pickle_key_length))
|
||||
let copy = DehydratedDeviceKey::from_slice(&inner_bytes).unwrap();
|
||||
|
||||
assert_eq!(base_64, copy.to_base64());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_creating_dehydrated_key_failure() {
|
||||
let bytes = [0u8; 24];
|
||||
|
||||
let pickle_key = DehydratedDeviceKey::from_slice(&bytes);
|
||||
|
||||
assert!(pickle_key.is_err());
|
||||
|
||||
match pickle_key {
|
||||
Err(DehydrationError::PickleKeyLength(pickle_key_length)) => {
|
||||
assert_eq!(bytes.len(), pickle_key_length);
|
||||
}
|
||||
_ => panic!("Should have failed!"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use matrix_sdk_crypto::{
|
||||
store::CryptoStoreError as InnerStoreError, KeyExportError, MegolmError, OlmError,
|
||||
SecretImportError as RustSecretImportError, SignatureError as InnerSignatureError,
|
||||
store::{CryptoStoreError as InnerStoreError, DehydrationError as InnerDehydrationError},
|
||||
KeyExportError, MegolmError, OlmError, SecretImportError as RustSecretImportError,
|
||||
SignatureError as InnerSignatureError,
|
||||
};
|
||||
use matrix_sdk_sqlite::OpenStoreError;
|
||||
use ruma::{IdParseError, OwnedUserId};
|
||||
@@ -57,6 +58,8 @@ pub enum CryptoStoreError {
|
||||
InvalidUserId(String, IdParseError),
|
||||
#[error(transparent)]
|
||||
Identifier(#[from] IdParseError),
|
||||
#[error(transparent)]
|
||||
DehydrationError(#[from] InnerDehydrationError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
@@ -112,7 +115,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_withheld_error_mapping() {
|
||||
use matrix_sdk_crypto::types::events::room_key_withheld::WithheldCode;
|
||||
use matrix_sdk_common::deserialized_responses::WithheldCode;
|
||||
|
||||
let inner_error = MegolmError::MissingRoomKey(Some(WithheldCode::Unverified));
|
||||
|
||||
|
||||
@@ -36,7 +36,10 @@ pub use machine::{KeyRequestPair, OlmMachine, SignatureVerification};
|
||||
use matrix_sdk_common::deserialized_responses::{ShieldState as RustShieldState, ShieldStateCode};
|
||||
use matrix_sdk_crypto::{
|
||||
olm::{IdentityKeys, InboundGroupSession, SenderData, Session},
|
||||
store::{Changes, CryptoStore, PendingChanges, RoomSettings as RustRoomSettings},
|
||||
store::{
|
||||
Changes, CryptoStore, DehydratedDeviceKey as InnerDehydratedDeviceKey, PendingChanges,
|
||||
RoomSettings as RustRoomSettings,
|
||||
},
|
||||
types::{
|
||||
DeviceKey, DeviceKeys, EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey,
|
||||
},
|
||||
@@ -62,6 +65,8 @@ pub use verification::{
|
||||
};
|
||||
use vodozemac::{Curve25519PublicKey, Ed25519PublicKey};
|
||||
|
||||
use crate::dehydrated_devices::DehydrationError;
|
||||
|
||||
/// Struct collecting data that is important to migrate to the rust-sdk
|
||||
#[derive(Deserialize, Serialize, uniffi::Record)]
|
||||
pub struct MigrationData {
|
||||
@@ -822,6 +827,39 @@ impl TryFrom<matrix_sdk_crypto::store::BackupKeys> for BackupKeys {
|
||||
}
|
||||
}
|
||||
|
||||
/// Dehydrated device key
|
||||
#[derive(uniffi::Record, Clone)]
|
||||
pub struct DehydratedDeviceKey {
|
||||
pub(crate) inner: Vec<u8>,
|
||||
}
|
||||
|
||||
impl DehydratedDeviceKey {
|
||||
/// Generates a new random pickle key.
|
||||
pub fn new() -> Result<Self, DehydrationError> {
|
||||
let inner = InnerDehydratedDeviceKey::new()?;
|
||||
Ok(inner.into())
|
||||
}
|
||||
|
||||
/// Creates a new dehydration pickle key from the given slice.
|
||||
///
|
||||
/// Fail if the slice length is not 32.
|
||||
pub fn from_slice(slice: &[u8]) -> Result<Self, DehydrationError> {
|
||||
let inner = InnerDehydratedDeviceKey::from_slice(slice)?;
|
||||
Ok(inner.into())
|
||||
}
|
||||
|
||||
/// Export the [`DehydratedDeviceKey`] as a base64 encoded string.
|
||||
pub fn to_base64(&self) -> String {
|
||||
let inner = InnerDehydratedDeviceKey::from_slice(&self.inner).unwrap();
|
||||
inner.to_base64()
|
||||
}
|
||||
}
|
||||
impl From<InnerDehydratedDeviceKey> for DehydratedDeviceKey {
|
||||
fn from(pickle_key: InnerDehydratedDeviceKey) -> Self {
|
||||
DehydratedDeviceKey { inner: pickle_key.into() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::store::RoomKeyCounts> for RoomKeyCounts {
|
||||
fn from(count: matrix_sdk_crypto::store::RoomKeyCounts) -> Self {
|
||||
Self { total: count.total as i64, backed_up: count.backed_up as i64 }
|
||||
|
||||
@@ -17,8 +17,8 @@ use matrix_sdk_crypto::{
|
||||
decrypt_room_key_export, encrypt_room_key_export,
|
||||
olm::ExportedRoomKey,
|
||||
store::{BackupDecryptionKey, Changes},
|
||||
DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, ToDeviceRequest,
|
||||
UserIdentity as SdkUserIdentity,
|
||||
types::requests::ToDeviceRequest,
|
||||
DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, UserIdentity as SdkUserIdentity,
|
||||
};
|
||||
use ruma::{
|
||||
api::{
|
||||
|
||||
@@ -4,9 +4,12 @@ use std::collections::HashMap;
|
||||
|
||||
use http::Response;
|
||||
use matrix_sdk_crypto::{
|
||||
CrossSigningBootstrapRequests, IncomingResponse, KeysBackupRequest, OutgoingRequest,
|
||||
OutgoingVerificationRequest as SdkVerificationRequest, RoomMessageRequest, ToDeviceRequest,
|
||||
UploadSigningKeysRequest as RustUploadSigningKeysRequest,
|
||||
types::requests::{
|
||||
AnyIncomingResponse, KeysBackupRequest, OutgoingRequest,
|
||||
OutgoingVerificationRequest as SdkVerificationRequest, RoomMessageRequest, ToDeviceRequest,
|
||||
UploadSigningKeysRequest as RustUploadSigningKeysRequest,
|
||||
},
|
||||
CrossSigningBootstrapRequests,
|
||||
};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
@@ -136,7 +139,7 @@ pub enum Request {
|
||||
|
||||
impl From<OutgoingRequest> for Request {
|
||||
fn from(r: OutgoingRequest) -> Self {
|
||||
use matrix_sdk_crypto::OutgoingRequests::*;
|
||||
use matrix_sdk_crypto::types::requests::AnyOutgoingRequest::*;
|
||||
|
||||
match r.request() {
|
||||
KeysUpload(u) => {
|
||||
@@ -338,16 +341,16 @@ impl From<RoomMessageResponse> for OwnedResponse {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a OwnedResponse> for IncomingResponse<'a> {
|
||||
impl<'a> From<&'a OwnedResponse> for AnyIncomingResponse<'a> {
|
||||
fn from(r: &'a OwnedResponse) -> Self {
|
||||
match r {
|
||||
OwnedResponse::KeysClaim(r) => IncomingResponse::KeysClaim(r),
|
||||
OwnedResponse::KeysQuery(r) => IncomingResponse::KeysQuery(r),
|
||||
OwnedResponse::KeysUpload(r) => IncomingResponse::KeysUpload(r),
|
||||
OwnedResponse::ToDevice(r) => IncomingResponse::ToDevice(r),
|
||||
OwnedResponse::SignatureUpload(r) => IncomingResponse::SignatureUpload(r),
|
||||
OwnedResponse::KeysBackup(r) => IncomingResponse::KeysBackup(r),
|
||||
OwnedResponse::RoomMessage(r) => IncomingResponse::RoomMessage(r),
|
||||
OwnedResponse::KeysClaim(r) => AnyIncomingResponse::KeysClaim(r),
|
||||
OwnedResponse::KeysQuery(r) => AnyIncomingResponse::KeysQuery(r),
|
||||
OwnedResponse::KeysUpload(r) => AnyIncomingResponse::KeysUpload(r),
|
||||
OwnedResponse::ToDevice(r) => AnyIncomingResponse::ToDevice(r),
|
||||
OwnedResponse::SignatureUpload(r) => AnyIncomingResponse::SignatureUpload(r),
|
||||
OwnedResponse::KeysBackup(r) => AnyIncomingResponse::KeysBackup(r),
|
||||
OwnedResponse::RoomMessage(r) => AnyIncomingResponse::RoomMessage(r),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,3 +33,4 @@ Additions:
|
||||
|
||||
- Add `Encryption::get_user_identity` which returns `UserIdentity`
|
||||
- Add `ClientBuilder::room_key_recipient_strategy`
|
||||
- Add `Room::send_raw`
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::{env, error::Error};
|
||||
use std::{env, error::Error, path::PathBuf, process::Command};
|
||||
|
||||
use vergen::EmitBuilder;
|
||||
|
||||
/// Adds a temporary workaround for an issue with the Rust compiler and Android
|
||||
/// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717.
|
||||
/// The workaround comes from: https://github.com/mozilla/application-services/pull/5442
|
||||
/// The workaround is based on: https://github.com/mozilla/application-services/pull/5442
|
||||
///
|
||||
/// IMPORTANT: if you modify this, make sure to modify
|
||||
/// [../matrix-sdk-crypto-ffi/build.rs] too!
|
||||
@@ -12,26 +12,45 @@ fn setup_x86_64_android_workaround() {
|
||||
let target_os = env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not set");
|
||||
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").expect("CARGO_CFG_TARGET_ARCH not set");
|
||||
if target_arch == "x86_64" && target_os == "android" {
|
||||
let android_ndk_home = env::var("ANDROID_NDK_HOME").expect("ANDROID_NDK_HOME not set");
|
||||
let build_os = match env::consts::OS {
|
||||
"linux" => "linux",
|
||||
"macos" => "darwin",
|
||||
"windows" => "windows",
|
||||
_ => panic!(
|
||||
"Unsupported OS. You must use either Linux, MacOS or Windows to build the crate."
|
||||
),
|
||||
};
|
||||
const DEFAULT_CLANG_VERSION: &str = "18";
|
||||
let clang_version =
|
||||
env::var("NDK_CLANG_VERSION").unwrap_or_else(|_| DEFAULT_CLANG_VERSION.to_owned());
|
||||
let linux_x86_64_lib_dir = format!(
|
||||
"toolchains/llvm/prebuilt/{build_os}-x86_64/lib/clang/{clang_version}/lib/linux/"
|
||||
// Configure rust to statically link against the `libclang_rt.builtins` supplied
|
||||
// with clang.
|
||||
|
||||
// cargo-ndk sets CC_x86_64-linux-android to the path to `clang`, within the
|
||||
// Android NDK.
|
||||
let clang_path = PathBuf::from(
|
||||
env::var("CC_x86_64-linux-android").expect("CC_x86_64-linux-android not set"),
|
||||
);
|
||||
println!("cargo:rustc-link-search={android_ndk_home}/{linux_x86_64_lib_dir}");
|
||||
|
||||
// clang_path should now look something like
|
||||
// `.../sdk/ndk/28.0.12674087/toolchains/llvm/prebuilt/linux-x86_64/bin/clang`.
|
||||
// We strip `/bin/clang` from the end to get the toolchain path.
|
||||
let toolchain_path = clang_path
|
||||
.ancestors()
|
||||
.nth(2)
|
||||
.expect("could not find NDK toolchain path")
|
||||
.to_str()
|
||||
.expect("NDK toolchain path is not valid UTF-8");
|
||||
|
||||
let clang_version = get_clang_major_version(&clang_path);
|
||||
|
||||
println!("cargo:rustc-link-search={toolchain_path}/lib/clang/{clang_version}/lib/linux/");
|
||||
println!("cargo:rustc-link-lib=static=clang_rt.builtins-x86_64-android");
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the clang binary at `clang_path`, and return its major version number
|
||||
fn get_clang_major_version(clang_path: &PathBuf) -> String {
|
||||
let clang_output =
|
||||
Command::new(clang_path).arg("-dumpversion").output().expect("failed to start clang");
|
||||
|
||||
if !clang_output.status.success() {
|
||||
panic!("failed to run clang: {}", String::from_utf8_lossy(&clang_output.stderr));
|
||||
}
|
||||
|
||||
let clang_version = String::from_utf8(clang_output.stdout).expect("clang output is not utf8");
|
||||
clang_version.split('.').next().expect("could not parse clang output").to_owned()
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
setup_x86_64_android_workaround();
|
||||
uniffi::generate_scaffolding("./src/api.udl").expect("Building the UDL file failed");
|
||||
|
||||
@@ -13,10 +13,3 @@ interface RoomMessageEventContentWithoutRelation {
|
||||
interface ClientError {
|
||||
Generic(string msg);
|
||||
};
|
||||
|
||||
interface MediaSource {
|
||||
[Name=from_json, Throws=ClientError]
|
||||
constructor(string json);
|
||||
string to_json();
|
||||
string url();
|
||||
};
|
||||
|
||||
@@ -32,9 +32,7 @@ use matrix_sdk::{
|
||||
user_directory::search_users,
|
||||
},
|
||||
events::{
|
||||
room::{
|
||||
avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent, MediaSource,
|
||||
},
|
||||
room::{avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent},
|
||||
AnyInitialStateEvent, AnyToDeviceEvent, InitialStateEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
@@ -55,7 +53,12 @@ use ruma::{
|
||||
},
|
||||
events::{
|
||||
ignored_user_list::IgnoredUserListEventContent,
|
||||
room::{join_rules::RoomJoinRulesEventContent, power_levels::RoomPowerLevelsEventContent},
|
||||
room::{
|
||||
join_rules::{
|
||||
AllowRule as RumaAllowRule, JoinRule as RumaJoinRule, RoomJoinRulesEventContent,
|
||||
},
|
||||
power_levels::RoomPowerLevelsEventContent,
|
||||
},
|
||||
GlobalAccountDataEventType,
|
||||
},
|
||||
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
|
||||
@@ -76,7 +79,7 @@ use crate::{
|
||||
notification_settings::NotificationSettings,
|
||||
room_directory_search::RoomDirectorySearch,
|
||||
room_preview::RoomPreview,
|
||||
ruma::AuthData,
|
||||
ruma::{AuthData, MediaSource},
|
||||
sync_service::{SyncService, SyncServiceBuilder},
|
||||
task_handle::TaskHandle,
|
||||
utils::AsyncRuntimeDropped,
|
||||
@@ -117,7 +120,7 @@ impl TryFrom<PusherKind> for RumaPusherKind {
|
||||
let mut ruma_data = RumaHttpPusherData::new(data.url);
|
||||
if let Some(payload) = data.default_payload {
|
||||
let json: Value = serde_json::from_str(&payload)?;
|
||||
ruma_data.default_payload = json;
|
||||
ruma_data.data.insert("default_payload".to_owned(), json);
|
||||
}
|
||||
ruma_data.format = data.format.map(Into::into);
|
||||
Ok(Self::Http(ruma_data))
|
||||
@@ -450,7 +453,7 @@ impl Client {
|
||||
.inner
|
||||
.media()
|
||||
.get_media_file(
|
||||
&MediaRequestParameters { source, format: MediaFormat::File },
|
||||
&MediaRequestParameters { source: source.media_source, format: MediaFormat::File },
|
||||
filename,
|
||||
&mime_type,
|
||||
use_cache,
|
||||
@@ -723,7 +726,7 @@ impl Client {
|
||||
&self,
|
||||
media_source: Arc<MediaSource>,
|
||||
) -> Result<Vec<u8>, ClientError> {
|
||||
let source = (*media_source).clone();
|
||||
let source = (*media_source).clone().media_source;
|
||||
|
||||
debug!(?source, "requesting media file");
|
||||
Ok(self
|
||||
@@ -739,9 +742,9 @@ impl Client {
|
||||
width: u64,
|
||||
height: u64,
|
||||
) -> Result<Vec<u8>, ClientError> {
|
||||
let source = (*media_source).clone();
|
||||
let source = (*media_source).clone().media_source;
|
||||
|
||||
debug!(source = ?media_source, width, height, "requesting media thumbnail");
|
||||
debug!(?source, width, height, "requesting media thumbnail");
|
||||
Ok(self
|
||||
.inner
|
||||
.media()
|
||||
@@ -1630,7 +1633,7 @@ impl TryFrom<Session> for AuthSession {
|
||||
user: user_session,
|
||||
};
|
||||
|
||||
Ok(AuthSession::Oidc(session))
|
||||
Ok(AuthSession::Oidc(session.into()))
|
||||
} else {
|
||||
// Create a regular Matrix Session.
|
||||
let session = matrix_sdk::matrix_auth::MatrixSession {
|
||||
@@ -1917,9 +1920,13 @@ pub enum AllowRule {
|
||||
/// Only a member of the `room_id` Room can join the one this rule is used
|
||||
/// in.
|
||||
RoomMembership { room_id: String },
|
||||
|
||||
/// A custom allow rule implementation, containing its JSON representation
|
||||
/// as a `String`.
|
||||
Custom { json: String },
|
||||
}
|
||||
|
||||
impl TryFrom<JoinRule> for ruma::events::room::join_rules::JoinRule {
|
||||
impl TryFrom<JoinRule> for RumaJoinRule {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: JoinRule) -> Result<Self, Self::Error> {
|
||||
@@ -1929,11 +1936,11 @@ impl TryFrom<JoinRule> for ruma::events::room::join_rules::JoinRule {
|
||||
JoinRule::Knock => Ok(Self::Knock),
|
||||
JoinRule::Private => Ok(Self::Private),
|
||||
JoinRule::Restricted { rules } => {
|
||||
let rules = allow_rules_from(rules)?;
|
||||
let rules = ruma_allow_rules_from_ffi(rules)?;
|
||||
Ok(Self::Restricted(ruma::events::room::join_rules::Restricted::new(rules)))
|
||||
}
|
||||
JoinRule::KnockRestricted { rules } => {
|
||||
let rules = allow_rules_from(rules)?;
|
||||
let rules = ruma_allow_rules_from_ffi(rules)?;
|
||||
Ok(Self::KnockRestricted(ruma::events::room::join_rules::Restricted::new(rules)))
|
||||
}
|
||||
JoinRule::Custom { repr } => Ok(serde_json::from_str(&repr)?),
|
||||
@@ -1941,12 +1948,10 @@ impl TryFrom<JoinRule> for ruma::events::room::join_rules::JoinRule {
|
||||
}
|
||||
}
|
||||
|
||||
fn allow_rules_from(
|
||||
value: Vec<AllowRule>,
|
||||
) -> Result<Vec<ruma::events::room::join_rules::AllowRule>, ClientError> {
|
||||
fn ruma_allow_rules_from_ffi(value: Vec<AllowRule>) -> Result<Vec<RumaAllowRule>, 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();
|
||||
let rule: Result<RumaAllowRule, ClientError> = rule.try_into();
|
||||
match rule {
|
||||
Ok(rule) => ret.push(rule),
|
||||
Err(error) => return Err(error),
|
||||
@@ -1955,7 +1960,7 @@ fn allow_rules_from(
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
impl TryFrom<AllowRule> for ruma::events::room::join_rules::AllowRule {
|
||||
impl TryFrom<AllowRule> for RumaAllowRule {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: AllowRule) -> Result<Self, Self::Error> {
|
||||
@@ -1966,6 +1971,54 @@ impl TryFrom<AllowRule> for ruma::events::room::join_rules::AllowRule {
|
||||
room_id,
|
||||
)))
|
||||
}
|
||||
AllowRule::Custom { json } => Ok(Self::_Custom(Box::new(serde_json::from_str(&json)?))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<RumaJoinRule> for JoinRule {
|
||||
type Error = String;
|
||||
fn try_from(value: RumaJoinRule) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
RumaJoinRule::Knock => Ok(JoinRule::Knock),
|
||||
RumaJoinRule::Public => Ok(JoinRule::Public),
|
||||
RumaJoinRule::Private => Ok(JoinRule::Private),
|
||||
RumaJoinRule::KnockRestricted(restricted) => {
|
||||
let rules = restricted.allow.into_iter().map(TryInto::try_into).collect::<Result<
|
||||
Vec<_>,
|
||||
Self::Error,
|
||||
>>(
|
||||
)?;
|
||||
Ok(JoinRule::KnockRestricted { rules })
|
||||
}
|
||||
RumaJoinRule::Restricted(restricted) => {
|
||||
let rules = restricted.allow.into_iter().map(TryInto::try_into).collect::<Result<
|
||||
Vec<_>,
|
||||
Self::Error,
|
||||
>>(
|
||||
)?;
|
||||
Ok(JoinRule::Restricted { rules })
|
||||
}
|
||||
RumaJoinRule::Invite => Ok(JoinRule::Invite),
|
||||
RumaJoinRule::_Custom(_) => Ok(JoinRule::Custom { repr: value.as_str().to_owned() }),
|
||||
_ => Err(format!("Unknown JoinRule: {:?}", value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<RumaAllowRule> for AllowRule {
|
||||
type Error = String;
|
||||
fn try_from(value: RumaAllowRule) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
RumaAllowRule::RoomMembership(membership) => {
|
||||
Ok(AllowRule::RoomMembership { room_id: membership.room_id.to_string() })
|
||||
}
|
||||
RumaAllowRule::_Custom(repr) => {
|
||||
let json = serde_json::to_string(&repr)
|
||||
.map_err(|e| format!("Couldn't serialize custom AllowRule: {e:?}"))?;
|
||||
Ok(Self::Custom { json })
|
||||
}
|
||||
_ => Err(format!("Invalid AllowRule: {:?}", value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use matrix_sdk::{
|
||||
CollectStrategy, TrustRequirement,
|
||||
},
|
||||
encryption::{BackupDownloadStrategy, EncryptionSettings},
|
||||
event_cache::EventCacheError,
|
||||
reqwest::Certificate,
|
||||
ruma::{ServerName, UserId},
|
||||
sliding_sync::{
|
||||
@@ -202,6 +203,8 @@ pub enum ClientBuildError {
|
||||
SlidingSyncVersion(VersionBuilderError),
|
||||
#[error(transparent)]
|
||||
Sdk(MatrixClientBuildError),
|
||||
#[error(transparent)]
|
||||
EventCache(#[from] EventCacheError),
|
||||
#[error("Failed to build the client: {message}")]
|
||||
Generic { message: String },
|
||||
}
|
||||
@@ -269,6 +272,10 @@ pub struct ClientBuilder {
|
||||
room_key_recipient_strategy: CollectStrategy,
|
||||
decryption_trust_requirement: TrustRequirement,
|
||||
request_config: Option<RequestConfig>,
|
||||
|
||||
/// Whether to enable use of the event cache store, for reloading events
|
||||
/// when building timelines et al.
|
||||
use_event_cache_persistent_storage: bool,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
@@ -299,9 +306,27 @@ impl ClientBuilder {
|
||||
room_key_recipient_strategy: Default::default(),
|
||||
decryption_trust_requirement: TrustRequirement::Untrusted,
|
||||
request_config: Default::default(),
|
||||
use_event_cache_persistent_storage: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether to use the event cache persistent storage or not.
|
||||
///
|
||||
/// This is a temporary feature flag, for testing the event cache's
|
||||
/// persistent storage. Follow new developments in https://github.com/matrix-org/matrix-rust-sdk/issues/3280.
|
||||
///
|
||||
/// This is disabled by default. When disabled, a one-time cleanup is
|
||||
/// performed when creating the client, and it will clear all the events
|
||||
/// previously stored in the event cache.
|
||||
///
|
||||
/// When enabled, it will attempt to store events in the event cache as
|
||||
/// they're received, and reuse them when reconstructing timelines.
|
||||
pub fn use_event_cache_persistent_storage(self: Arc<Self>, value: bool) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.use_event_cache_persistent_storage = value;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn cross_process_store_locks_holder_name(
|
||||
self: Arc<Self>,
|
||||
holder_name: String,
|
||||
@@ -624,6 +649,19 @@ impl ClientBuilder {
|
||||
|
||||
let sdk_client = inner_builder.build().await?;
|
||||
|
||||
if builder.use_event_cache_persistent_storage {
|
||||
// Enable the persistent storage \o/
|
||||
sdk_client.event_cache().enable_storage()?;
|
||||
} else {
|
||||
// Get rid of all the previous events, if any.
|
||||
let store = sdk_client
|
||||
.event_cache_store()
|
||||
.lock()
|
||||
.await
|
||||
.map_err(EventCacheError::LockingStorage)?;
|
||||
store.clear_all_rooms_chunks().await.map_err(EventCacheError::Storage)?;
|
||||
}
|
||||
|
||||
Ok(Arc::new(
|
||||
Client::new(sdk_client, builder.enable_oidc_refresh_lock, builder.session_delegate)
|
||||
.await?,
|
||||
|
||||
@@ -254,7 +254,7 @@ impl Encryption {
|
||||
/// Therefore it is necessary to poll the server for an answer every time
|
||||
/// you want to differentiate between those two states.
|
||||
pub async fn backup_exists_on_server(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.backups().exists_on_server().await?)
|
||||
Ok(self.inner.backups().fetch_exists_on_server().await?)
|
||||
}
|
||||
|
||||
pub fn recovery_state(&self) -> RecoveryState {
|
||||
|
||||
@@ -3,7 +3,10 @@ use matrix_sdk::IdParseError;
|
||||
use matrix_sdk_ui::timeline::TimelineEventItemId;
|
||||
use ruma::{
|
||||
events::{
|
||||
room::{message::Relation, redaction::SyncRoomRedactionEvent},
|
||||
room::{
|
||||
message::{MessageType as RumaMessageType, Relation},
|
||||
redaction::SyncRoomRedactionEvent,
|
||||
},
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent,
|
||||
MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
|
||||
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
|
||||
@@ -202,7 +205,7 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
|
||||
_ => None,
|
||||
});
|
||||
MessageLikeEventContent::RoomMessage {
|
||||
message_type: original_content.msgtype.into(),
|
||||
message_type: original_content.msgtype.try_into()?,
|
||||
in_reply_to_event_id,
|
||||
}
|
||||
}
|
||||
@@ -356,6 +359,39 @@ impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, uniffi::Enum)]
|
||||
pub enum RoomMessageEventMessageType {
|
||||
Audio,
|
||||
Emote,
|
||||
File,
|
||||
Image,
|
||||
Location,
|
||||
Notice,
|
||||
ServerNotice,
|
||||
Text,
|
||||
Video,
|
||||
VerificationRequest,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl From<RumaMessageType> for RoomMessageEventMessageType {
|
||||
fn from(val: ruma::events::room::message::MessageType) -> Self {
|
||||
match val {
|
||||
RumaMessageType::Audio { .. } => Self::Audio,
|
||||
RumaMessageType::Emote { .. } => Self::Emote,
|
||||
RumaMessageType::File { .. } => Self::File,
|
||||
RumaMessageType::Image { .. } => Self::Image,
|
||||
RumaMessageType::Location { .. } => Self::Location,
|
||||
RumaMessageType::Notice { .. } => Self::Notice,
|
||||
RumaMessageType::ServerNotice { .. } => Self::ServerNotice,
|
||||
RumaMessageType::Text { .. } => Self::Text,
|
||||
RumaMessageType::Video { .. } => Self::Video,
|
||||
RumaMessageType::VerificationRequest { .. } => Self::VerificationRequest,
|
||||
_ => Self::Other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains the 2 possible identifiers of an event, either it has a remote
|
||||
/// event id or a local transaction id, never both or none.
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// TODO: target-os conditional would be good.
|
||||
|
||||
#![allow(unused_qualifications, clippy::new_without_default)]
|
||||
#![allow(clippy::empty_line_after_doc_comments)] // Needed because uniffi macros contain empty
|
||||
// lines after docs.
|
||||
|
||||
mod authentication;
|
||||
mod chunk_iterator;
|
||||
@@ -33,13 +35,11 @@ mod utils;
|
||||
mod widget;
|
||||
|
||||
use async_compat::TOKIO1 as RUNTIME;
|
||||
use matrix_sdk::ruma::events::room::{
|
||||
message::RoomMessageEventContentWithoutRelation, MediaSource,
|
||||
};
|
||||
use matrix_sdk::ruma::events::room::message::RoomMessageEventContentWithoutRelation;
|
||||
|
||||
use self::{
|
||||
error::ClientError,
|
||||
ruma::{MediaSourceExt, Mentions, RoomMessageEventContentWithoutRelationExt},
|
||||
ruma::{Mentions, RoomMessageEventContentWithoutRelationExt},
|
||||
task_handle::TaskHandle,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{collections::HashMap, pin::pin, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use futures_util::StreamExt;
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use matrix_sdk::{
|
||||
crypto::LocalTrust,
|
||||
event_cache::paginator::PaginatorError,
|
||||
@@ -11,7 +11,7 @@ use matrix_sdk::{
|
||||
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType,
|
||||
RoomHero as SdkRoomHero, RoomMemberships, RoomState,
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{PaginationError, RoomExt, TimelineFocus};
|
||||
use matrix_sdk_ui::timeline::{default_event_filter, PaginationError, RoomExt, TimelineFocus};
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
api::client::room::report_content,
|
||||
@@ -23,7 +23,7 @@ use ruma::{
|
||||
message::RoomMessageEventContentWithoutRelation,
|
||||
power_levels::RoomPowerLevels as RumaPowerLevels, MediaSource,
|
||||
},
|
||||
TimelineEventType,
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent, TimelineEventType,
|
||||
},
|
||||
EventId, Int, OwnedDeviceId, OwnedUserId, RoomAliasId, UserId,
|
||||
};
|
||||
@@ -34,12 +34,12 @@ use super::RUNTIME;
|
||||
use crate::{
|
||||
chunk_iterator::ChunkIterator,
|
||||
error::{ClientError, MediaInfoError, RoomError},
|
||||
event::{MessageLikeEventType, StateEventType},
|
||||
event::{MessageLikeEventType, RoomMessageEventMessageType, StateEventType},
|
||||
identity_status_change::IdentityStatusChange,
|
||||
room_info::RoomInfo,
|
||||
room_member::RoomMember,
|
||||
ruma::{ImageInfo, Mentions, NotifyType},
|
||||
timeline::{FocusEventError, ReceiptType, SendHandle, Timeline},
|
||||
timeline::{DateDividerMode, FocusEventError, ReceiptType, SendHandle, Timeline},
|
||||
utils::u64_to_uint,
|
||||
TaskHandle,
|
||||
};
|
||||
@@ -50,6 +50,7 @@ pub enum Membership {
|
||||
Joined,
|
||||
Left,
|
||||
Knocked,
|
||||
Banned,
|
||||
}
|
||||
|
||||
impl From<RoomState> for Membership {
|
||||
@@ -59,6 +60,7 @@ impl From<RoomState> for Membership {
|
||||
RoomState::Joined => Membership::Joined,
|
||||
RoomState::Left => Membership::Left,
|
||||
RoomState::Knocked => Membership::Knocked,
|
||||
RoomState::Banned => Membership::Banned,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,6 +262,51 @@ impl Room {
|
||||
Ok(Timeline::new(timeline))
|
||||
}
|
||||
|
||||
/// A timeline instance that can be configured to only include RoomMessage
|
||||
/// type events and filter those further based on their message type.
|
||||
///
|
||||
/// Virtual timeline items will still be provided and the
|
||||
/// `default_event_filter` will be applied before everything else.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `internal_id_prefix` - An optional String that will be prepended to
|
||||
/// all the timeline item's internal IDs, making it possible to
|
||||
/// distinguish different timeline instances from each other.
|
||||
///
|
||||
/// * `allowed_message_types` - A list of `RoomMessageEventMessageType` that
|
||||
/// will be allowed to appear in the timeline
|
||||
pub async fn message_filtered_timeline(
|
||||
&self,
|
||||
internal_id_prefix: Option<String>,
|
||||
allowed_message_types: Vec<RoomMessageEventMessageType>,
|
||||
date_divider_mode: DateDividerMode,
|
||||
) -> Result<Arc<Timeline>, ClientError> {
|
||||
let mut builder = matrix_sdk_ui::timeline::Timeline::builder(&self.inner);
|
||||
|
||||
if let Some(internal_id_prefix) = internal_id_prefix {
|
||||
builder = builder.with_internal_id_prefix(internal_id_prefix);
|
||||
}
|
||||
|
||||
builder = builder.with_date_divider_mode(date_divider_mode.into());
|
||||
|
||||
builder = builder.event_filter(move |event, room_version_id| {
|
||||
default_event_filter(event, room_version_id)
|
||||
&& match event {
|
||||
AnySyncTimelineEvent::MessageLike(msg) => match msg.original_content() {
|
||||
Some(AnyMessageLikeEventContent::RoomMessage(content)) => {
|
||||
allowed_message_types.contains(&content.msgtype.into())
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
|
||||
let timeline = builder.build().await?;
|
||||
Ok(Timeline::new(timeline))
|
||||
}
|
||||
|
||||
pub fn is_encrypted(&self) -> Result<bool, ClientError> {
|
||||
Ok(RUNTIME.block_on(self.inner.is_encrypted())?)
|
||||
}
|
||||
@@ -336,6 +383,22 @@ impl Room {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a raw event to the room.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event_type` - The type of the event to send.
|
||||
///
|
||||
/// * `content` - The content of the event to send encoded as JSON string.
|
||||
pub async fn send_raw(&self, event_type: String, content: String) -> Result<(), ClientError> {
|
||||
let content_json: serde_json::Value = serde_json::from_str(&content)
|
||||
.map_err(|e| ClientError::Generic { msg: format!("Failed to parse JSON: {e}") })?;
|
||||
|
||||
self.inner.send_raw(&event_type, content_json).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Redacts an event from the room.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -840,6 +903,125 @@ impl Room {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear the event cache storage for the current room.
|
||||
///
|
||||
/// This will remove all the information related to the event cache, in
|
||||
/// memory and in the persisted storage, if enabled.
|
||||
pub async fn clear_event_cache_storage(&self) -> Result<(), ClientError> {
|
||||
let (room_event_cache, _drop_handles) = self.inner.event_cache().await?;
|
||||
room_event_cache.clear().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Subscribes to requests to join this room (knock member events), using a
|
||||
/// `listener` to be notified of the changes.
|
||||
///
|
||||
/// The current requests to join the room will be emitted immediately
|
||||
/// when subscribing, along with a [`TaskHandle`] to cancel the
|
||||
/// subscription.
|
||||
pub async fn subscribe_to_knock_requests(
|
||||
self: Arc<Self>,
|
||||
listener: Box<dyn KnockRequestsListener>,
|
||||
) -> Result<Arc<TaskHandle>, ClientError> {
|
||||
let stream = self.inner.subscribe_to_knock_requests().await?;
|
||||
|
||||
let handle = Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
pin_mut!(stream);
|
||||
while let Some(requests) = stream.next().await {
|
||||
listener.call(requests.into_iter().map(Into::into).collect());
|
||||
}
|
||||
})));
|
||||
|
||||
Ok(handle)
|
||||
}
|
||||
|
||||
/// Return a debug representation for the internal room events data
|
||||
/// structure, one line per entry in the resulting vector.
|
||||
pub async fn room_events_debug_string(&self) -> Result<Vec<String>, ClientError> {
|
||||
let (cache, _drop_guards) = self.inner.event_cache().await?;
|
||||
Ok(cache.debug_string().await)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::room::knock_requests::KnockRequest> for KnockRequest {
|
||||
fn from(request: matrix_sdk::room::knock_requests::KnockRequest) -> Self {
|
||||
Self {
|
||||
event_id: request.event_id.to_string(),
|
||||
user_id: request.member_info.user_id.to_string(),
|
||||
room_id: request.room_id().to_string(),
|
||||
display_name: request.member_info.display_name.clone(),
|
||||
avatar_url: request.member_info.avatar_url.as_ref().map(|url| url.to_string()),
|
||||
reason: request.member_info.reason.clone(),
|
||||
timestamp: request.timestamp.map(|ts| ts.into()),
|
||||
is_seen: request.is_seen,
|
||||
actions: Arc::new(KnockRequestActions { inner: request }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A listener for receiving new requests to a join a room.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait KnockRequestsListener: Send + Sync {
|
||||
fn call(&self, join_requests: Vec<KnockRequest>);
|
||||
}
|
||||
|
||||
/// An FFI representation of a request to join a room.
|
||||
#[derive(Debug, Clone, uniffi::Record)]
|
||||
pub struct KnockRequest {
|
||||
/// The event id of the event that contains the `knock` membership change.
|
||||
pub event_id: String,
|
||||
/// The user id of the user who's requesting to join the room.
|
||||
pub user_id: String,
|
||||
/// The room id of the room whose access was requested.
|
||||
pub room_id: String,
|
||||
/// The optional display name of the user who's requesting to join the room.
|
||||
pub display_name: Option<String>,
|
||||
/// The optional avatar url of the user who's requesting to join the room.
|
||||
pub avatar_url: Option<String>,
|
||||
/// An optional reason why the user wants join the room.
|
||||
pub reason: Option<String>,
|
||||
/// The timestamp when this request was created.
|
||||
pub timestamp: Option<u64>,
|
||||
/// Whether the knock request has been marked as `seen` so it can be
|
||||
/// filtered by the client.
|
||||
pub is_seen: bool,
|
||||
/// A set of actions to perform for this knock request.
|
||||
pub actions: Arc<KnockRequestActions>,
|
||||
}
|
||||
|
||||
/// A set of actions to perform for a knock request.
|
||||
#[derive(Debug, Clone, uniffi::Object)]
|
||||
pub struct KnockRequestActions {
|
||||
inner: matrix_sdk::room::knock_requests::KnockRequest,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl KnockRequestActions {
|
||||
/// Accepts the knock request by inviting the user to the room.
|
||||
pub async fn accept(&self) -> Result<(), ClientError> {
|
||||
self.inner.accept().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Declines the knock request by kicking the user from the room with an
|
||||
/// optional reason.
|
||||
pub async fn decline(&self, reason: Option<String>) -> Result<(), ClientError> {
|
||||
self.inner.decline(reason.as_deref()).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Declines the knock request by banning the user from the room with an
|
||||
/// optional reason.
|
||||
pub async fn decline_and_ban(&self, reason: Option<String>) -> Result<(), ClientError> {
|
||||
self.inner.decline_and_ban(reason.as_deref()).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Marks the knock request as 'seen'.
|
||||
///
|
||||
/// **IMPORTANT**: this won't update the current reference to this request,
|
||||
/// a new one with the updated value should be emitted instead.
|
||||
pub async fn mark_as_seen(&self) -> Result<(), ClientError> {
|
||||
self.inner.mark_as_seen().await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a `matrix.to` permalink to the given room alias.
|
||||
@@ -973,7 +1155,7 @@ impl TryFrom<ImageInfo> for RumaAvatarImageInfo {
|
||||
|
||||
fn try_from(value: ImageInfo) -> Result<Self, MediaInfoError> {
|
||||
let thumbnail_url = if let Some(media_source) = value.thumbnail_source {
|
||||
match media_source.as_ref() {
|
||||
match &media_source.as_ref().media_source {
|
||||
MediaSource::Plain(mxc_uri) => Some(mxc_uri.clone()),
|
||||
MediaSource::Encrypted(_) => return Err(MediaInfoError::InvalidField),
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use matrix_sdk::RoomState;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
client::JoinRule,
|
||||
notification_settings::RoomNotificationMode,
|
||||
room::{Membership, RoomHero},
|
||||
room_member::RoomMember,
|
||||
@@ -54,8 +56,10 @@ pub struct RoomInfo {
|
||||
/// Events causing mentions/highlights for the user, according to their
|
||||
/// notification settings.
|
||||
num_unread_mentions: u64,
|
||||
/// The currently pinned event ids
|
||||
/// The currently pinned event ids.
|
||||
pinned_event_ids: Vec<String>,
|
||||
/// The join rule for this room, if known.
|
||||
join_rule: Option<JoinRule>,
|
||||
}
|
||||
|
||||
impl RoomInfo {
|
||||
@@ -70,6 +74,11 @@ impl RoomInfo {
|
||||
let pinned_event_ids =
|
||||
room.pinned_event_ids().unwrap_or_default().iter().map(|id| id.to_string()).collect();
|
||||
|
||||
let join_rule = room.join_rule().try_into();
|
||||
if let Err(e) = &join_rule {
|
||||
warn!("Failed to parse join rule: {:?}", e);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
id: room.room_id().to_string(),
|
||||
creator: room.creator().as_ref().map(ToString::to_string),
|
||||
@@ -118,6 +127,7 @@ impl RoomInfo {
|
||||
num_unread_notifications: room.num_unread_notifications(),
|
||||
num_unread_mentions: room.num_unread_mentions(),
|
||||
pinned_event_ids,
|
||||
join_rule: join_rule.ok(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -616,7 +616,8 @@ impl RoomListItem {
|
||||
|
||||
// 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, mut 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 {
|
||||
@@ -624,6 +625,16 @@ impl RoomListItem {
|
||||
(room_or_alias_id, server_names)
|
||||
};
|
||||
|
||||
// If no server names are provided and the room's membership is invited,
|
||||
// add the server name from the sender's user id as a fallback value
|
||||
if server_names.is_empty() {
|
||||
if let Ok(invite_details) = self.inner.invite_details().await {
|
||||
if let Some(inviter) = invite_details.inviter {
|
||||
server_names.push(inviter.user_id().server_name().to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let room_preview = client.get_room_preview(&room_or_alias_id, server_names).await?;
|
||||
|
||||
Ok(Arc::new(RoomPreview::new(AsyncRuntimeDropped::new(client), room_preview)))
|
||||
|
||||
@@ -4,7 +4,10 @@ use ruma::{room::RoomType as RumaRoomType, space::SpaceRoomJoinRule};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
client::JoinRule, error::ClientError, room::Membership, room_member::RoomMember,
|
||||
client::JoinRule,
|
||||
error::ClientError,
|
||||
room::{Membership, RoomHero},
|
||||
room_member::RoomMember,
|
||||
utils::AsyncRuntimeDropped,
|
||||
};
|
||||
|
||||
@@ -38,6 +41,10 @@ impl RoomPreview {
|
||||
.try_into()
|
||||
.map_err(|_| anyhow::anyhow!("unhandled SpaceRoomJoinRule kind"))?,
|
||||
is_direct: info.is_direct,
|
||||
heroes: info
|
||||
.heroes
|
||||
.as_ref()
|
||||
.map(|heroes| heroes.iter().map(|h| h.to_owned().into()).collect()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -85,13 +92,15 @@ pub struct RoomPreviewInfo {
|
||||
/// The room type (space, custom) or nothing, if it's a regular room.
|
||||
pub room_type: RoomType,
|
||||
/// Is the history world-readable for this room?
|
||||
pub is_history_world_readable: bool,
|
||||
pub is_history_world_readable: Option<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>,
|
||||
/// Room heroes.
|
||||
pub heroes: Option<Vec<RoomHero>>,
|
||||
}
|
||||
|
||||
impl TryFrom<SpaceRoomJoinRule> for JoinRule {
|
||||
|
||||
@@ -15,9 +15,7 @@
|
||||
use std::{collections::BTreeSet, sync::Arc, time::Duration};
|
||||
|
||||
use extension_trait::extension_trait;
|
||||
use matrix_sdk::attachment::{
|
||||
BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo,
|
||||
};
|
||||
use matrix_sdk::attachment::{BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo};
|
||||
use ruma::{
|
||||
assign,
|
||||
events::{
|
||||
@@ -42,7 +40,8 @@ use ruma::{
|
||||
VideoInfo as RumaVideoInfo,
|
||||
VideoMessageEventContent as RumaVideoMessageEventContent,
|
||||
},
|
||||
ImageInfo as RumaImageInfo, MediaSource, ThumbnailInfo as RumaThumbnailInfo,
|
||||
ImageInfo as RumaImageInfo, MediaSource as RumaMediaSource,
|
||||
ThumbnailInfo as RumaThumbnailInfo,
|
||||
},
|
||||
},
|
||||
matrix_uri::MatrixId as RumaMatrixId,
|
||||
@@ -154,11 +153,6 @@ impl From<&RumaMatrixId> for MatrixId {
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn media_source_from_url(url: String) -> Arc<MediaSource> {
|
||||
Arc::new(MediaSource::Plain(url.into()))
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn message_event_content_new(
|
||||
msgtype: MessageType,
|
||||
@@ -200,21 +194,84 @@ pub fn message_event_content_from_html_as_emote(
|
||||
)))
|
||||
}
|
||||
|
||||
#[extension_trait]
|
||||
pub impl MediaSourceExt for MediaSource {
|
||||
fn from_json(json: String) -> Result<MediaSource, ClientError> {
|
||||
let res = serde_json::from_str(&json)?;
|
||||
Ok(res)
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct MediaSource {
|
||||
pub(crate) media_source: RumaMediaSource,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl MediaSource {
|
||||
#[uniffi::constructor]
|
||||
pub fn from_url(url: String) -> Result<Arc<MediaSource>, ClientError> {
|
||||
let media_source = RumaMediaSource::Plain(url.into());
|
||||
media_source.verify()?;
|
||||
|
||||
Ok(Arc::new(MediaSource { media_source }))
|
||||
}
|
||||
|
||||
fn to_json(&self) -> String {
|
||||
serde_json::to_string(self).expect("Media source should always be serializable ")
|
||||
pub fn url(&self) -> String {
|
||||
self.media_source.url()
|
||||
}
|
||||
|
||||
// Used on Element X Android
|
||||
#[uniffi::constructor]
|
||||
pub fn from_json(json: String) -> Result<Arc<Self>, ClientError> {
|
||||
let media_source: RumaMediaSource = serde_json::from_str(&json)?;
|
||||
media_source.verify()?;
|
||||
|
||||
Ok(Arc::new(MediaSource { media_source }))
|
||||
}
|
||||
|
||||
// Used on Element X Android
|
||||
pub fn to_json(&self) -> String {
|
||||
serde_json::to_string(&self.media_source)
|
||||
.expect("Media source should always be serializable ")
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<RumaMediaSource> for MediaSource {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: RumaMediaSource) -> Result<Self, Self::Error> {
|
||||
value.verify()?;
|
||||
Ok(Self { media_source: value })
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&RumaMediaSource> for MediaSource {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: &RumaMediaSource) -> Result<Self, Self::Error> {
|
||||
value.verify()?;
|
||||
Ok(Self { media_source: value.clone() })
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MediaSource> for RumaMediaSource {
|
||||
fn from(value: MediaSource) -> Self {
|
||||
value.media_source
|
||||
}
|
||||
}
|
||||
|
||||
#[extension_trait]
|
||||
pub(crate) impl MediaSourceExt for RumaMediaSource {
|
||||
fn verify(&self) -> Result<(), ClientError> {
|
||||
match self {
|
||||
RumaMediaSource::Plain(url) => {
|
||||
url.validate().map_err(|e| ClientError::Generic { msg: e.to_string() })?;
|
||||
}
|
||||
RumaMediaSource::Encrypted(file) => {
|
||||
file.url.validate().map_err(|e| ClientError::Generic { msg: e.to_string() })?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn url(&self) -> String {
|
||||
match self {
|
||||
MediaSource::Plain(url) => url.to_string(),
|
||||
MediaSource::Encrypted(file) => file.url.to_string(),
|
||||
RumaMediaSource::Plain(url) => url.to_string(),
|
||||
RumaMediaSource::Encrypted(file) => file.url.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -280,7 +337,7 @@ fn get_body_and_filename(filename: String, caption: Option<String>) -> (String,
|
||||
}
|
||||
|
||||
impl TryFrom<MessageType> for RumaMessageType {
|
||||
type Error = serde_json::Error;
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: MessageType) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
@@ -292,7 +349,7 @@ 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(body, (*content.source).clone())
|
||||
RumaImageMessageEventContent::new(body, (*content.source).clone().into())
|
||||
.info(content.info.map(Into::into).map(Box::new));
|
||||
event_content.formatted = content.formatted_caption.map(Into::into);
|
||||
event_content.filename = filename;
|
||||
@@ -301,7 +358,7 @@ impl TryFrom<MessageType> for RumaMessageType {
|
||||
MessageType::Audio { content } => {
|
||||
let (body, filename) = get_body_and_filename(content.filename, content.caption);
|
||||
let mut event_content =
|
||||
RumaAudioMessageEventContent::new(body, (*content.source).clone())
|
||||
RumaAudioMessageEventContent::new(body, (*content.source).clone().into())
|
||||
.info(content.info.map(Into::into).map(Box::new));
|
||||
event_content.formatted = content.formatted_caption.map(Into::into);
|
||||
event_content.filename = filename;
|
||||
@@ -310,7 +367,7 @@ impl TryFrom<MessageType> for RumaMessageType {
|
||||
MessageType::Video { content } => {
|
||||
let (body, filename) = get_body_and_filename(content.filename, content.caption);
|
||||
let mut event_content =
|
||||
RumaVideoMessageEventContent::new(body, (*content.source).clone())
|
||||
RumaVideoMessageEventContent::new(body, (*content.source).clone().into())
|
||||
.info(content.info.map(Into::into).map(Box::new));
|
||||
event_content.formatted = content.formatted_caption.map(Into::into);
|
||||
event_content.filename = filename;
|
||||
@@ -319,7 +376,7 @@ impl TryFrom<MessageType> for RumaMessageType {
|
||||
MessageType::File { content } => {
|
||||
let (body, filename) = get_body_and_filename(content.filename, content.caption);
|
||||
let mut event_content =
|
||||
RumaFileMessageEventContent::new(body, (*content.source).clone())
|
||||
RumaFileMessageEventContent::new(body, (*content.source).clone().into())
|
||||
.info(content.info.map(Into::into).map(Box::new));
|
||||
event_content.formatted = content.formatted_caption.map(Into::into);
|
||||
event_content.filename = filename;
|
||||
@@ -345,9 +402,11 @@ impl TryFrom<MessageType> for RumaMessageType {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RumaMessageType> for MessageType {
|
||||
fn from(value: RumaMessageType) -> Self {
|
||||
match value {
|
||||
impl TryFrom<RumaMessageType> for MessageType {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: RumaMessageType) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
RumaMessageType::Emote(c) => MessageType::Emote {
|
||||
content: EmoteMessageContent {
|
||||
body: c.body.clone(),
|
||||
@@ -359,16 +418,17 @@ impl From<RumaMessageType> for MessageType {
|
||||
filename: c.filename().to_owned(),
|
||||
caption: c.caption().map(ToString::to_string),
|
||||
formatted_caption: c.formatted_caption().map(Into::into),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
source: Arc::new(c.source.try_into()?),
|
||||
info: c.info.as_deref().map(TryInto::try_into).transpose()?,
|
||||
},
|
||||
},
|
||||
|
||||
RumaMessageType::Audio(c) => MessageType::Audio {
|
||||
content: AudioMessageContent {
|
||||
filename: c.filename().to_owned(),
|
||||
caption: c.caption().map(ToString::to_string),
|
||||
formatted_caption: c.formatted_caption().map(Into::into),
|
||||
source: Arc::new(c.source.clone()),
|
||||
source: Arc::new(c.source.try_into()?),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
audio: c.audio.map(Into::into),
|
||||
voice: c.voice.map(Into::into),
|
||||
@@ -379,8 +439,8 @@ impl From<RumaMessageType> for MessageType {
|
||||
filename: c.filename().to_owned(),
|
||||
caption: c.caption().map(ToString::to_string),
|
||||
formatted_caption: c.formatted_caption().map(Into::into),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
source: Arc::new(c.source.try_into()?),
|
||||
info: c.info.as_deref().map(TryInto::try_into).transpose()?,
|
||||
},
|
||||
},
|
||||
RumaMessageType::File(c) => MessageType::File {
|
||||
@@ -388,8 +448,8 @@ impl From<RumaMessageType> for MessageType {
|
||||
filename: c.filename().to_owned(),
|
||||
caption: c.caption().map(ToString::to_string),
|
||||
formatted_caption: c.formatted_caption().map(Into::into),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
source: Arc::new(c.source.try_into()?),
|
||||
info: c.info.as_deref().map(TryInto::try_into).transpose()?,
|
||||
},
|
||||
},
|
||||
RumaMessageType::Notice(c) => MessageType::Notice {
|
||||
@@ -425,7 +485,7 @@ impl From<RumaMessageType> for MessageType {
|
||||
msgtype: value.msgtype().to_owned(),
|
||||
body: value.body().to_owned(),
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,7 +580,7 @@ impl From<ImageInfo> for RumaImageInfo {
|
||||
mimetype: value.mimetype,
|
||||
size: value.size.map(u64_to_uint),
|
||||
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
|
||||
blurhash: value.blurhash,
|
||||
})
|
||||
}
|
||||
@@ -625,7 +685,7 @@ impl From<VideoInfo> for RumaVideoInfo {
|
||||
mimetype: value.mimetype,
|
||||
size: value.size.map(u64_to_uint),
|
||||
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
|
||||
blurhash: value.blurhash,
|
||||
})
|
||||
}
|
||||
@@ -668,7 +728,7 @@ impl From<FileInfo> for RumaFileInfo {
|
||||
mimetype: value.mimetype,
|
||||
size: value.size.map(u64_to_uint),
|
||||
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -703,21 +763,6 @@ impl From<ThumbnailInfo> for RumaThumbnailInfo {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&ThumbnailInfo> for BaseThumbnailInfo {
|
||||
type Error = MediaInfoError;
|
||||
|
||||
fn try_from(value: &ThumbnailInfo) -> Result<Self, MediaInfoError> {
|
||||
let height = UInt::try_from(value.height.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
let width = UInt::try_from(value.width.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
let size = UInt::try_from(value.size.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
|
||||
Ok(BaseThumbnailInfo { height: Some(height), width: Some(width), size: Some(size) })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct NoticeMessageContent {
|
||||
pub body: String,
|
||||
@@ -790,8 +835,10 @@ pub enum MessageFormat {
|
||||
Unknown { format: String },
|
||||
}
|
||||
|
||||
impl From<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
|
||||
fn from(info: &matrix_sdk::ruma::events::room::ImageInfo) -> Self {
|
||||
impl TryFrom<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(info: &matrix_sdk::ruma::events::room::ImageInfo) -> Result<Self, Self::Error> {
|
||||
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
@@ -799,15 +846,20 @@ impl From<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
|
||||
size: info.size.map(Into::into),
|
||||
});
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
mimetype: info.mimetype.clone(),
|
||||
size: info.size.map(Into::into),
|
||||
thumbnail_info,
|
||||
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
|
||||
thumbnail_source: info
|
||||
.thumbnail_source
|
||||
.as_ref()
|
||||
.map(TryInto::try_into)
|
||||
.transpose()?
|
||||
.map(Arc::new),
|
||||
blurhash: info.blurhash.clone(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -821,8 +873,10 @@ impl From<&RumaAudioInfo> for AudioInfo {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RumaVideoInfo> for VideoInfo {
|
||||
fn from(info: &RumaVideoInfo) -> Self {
|
||||
impl TryFrom<&RumaVideoInfo> for VideoInfo {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(info: &RumaVideoInfo) -> Result<Self, Self::Error> {
|
||||
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
@@ -830,21 +884,28 @@ impl From<&RumaVideoInfo> for VideoInfo {
|
||||
size: info.size.map(Into::into),
|
||||
});
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
duration: info.duration,
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
mimetype: info.mimetype.clone(),
|
||||
size: info.size.map(Into::into),
|
||||
thumbnail_info,
|
||||
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
|
||||
thumbnail_source: info
|
||||
.thumbnail_source
|
||||
.as_ref()
|
||||
.map(TryInto::try_into)
|
||||
.transpose()?
|
||||
.map(Arc::new),
|
||||
blurhash: info.blurhash.clone(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RumaFileInfo> for FileInfo {
|
||||
fn from(info: &RumaFileInfo) -> Self {
|
||||
impl TryFrom<&RumaFileInfo> for FileInfo {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(info: &RumaFileInfo) -> Result<Self, Self::Error> {
|
||||
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
@@ -852,12 +913,17 @@ impl From<&RumaFileInfo> for FileInfo {
|
||||
size: info.size.map(Into::into),
|
||||
});
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
mimetype: info.mimetype.clone(),
|
||||
size: info.size.map(Into::into),
|
||||
thumbnail_info,
|
||||
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
|
||||
}
|
||||
thumbnail_source: info
|
||||
.thumbnail_source
|
||||
.as_ref()
|
||||
.map(TryInto::try_into)
|
||||
.transpose()?
|
||||
.map(Arc::new),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -201,6 +201,22 @@ pub struct UnableToDecryptInfo {
|
||||
/// What we know about what caused this UTD. E.g. was this event sent when
|
||||
/// we were not a member of this room?
|
||||
pub cause: UtdCause,
|
||||
|
||||
/// The difference between the event creation time (`origin_server_ts`) and
|
||||
/// the time our device was created. If negative, this event was sent
|
||||
/// *before* our device was created.
|
||||
pub event_local_age_millis: i64,
|
||||
|
||||
/// Whether the user had verified their own identity at the point they
|
||||
/// received the UTD event.
|
||||
pub user_trusts_own_identity: bool,
|
||||
|
||||
/// The homeserver of the user that sent the undecryptable event.
|
||||
pub sender_homeserver: String,
|
||||
|
||||
/// Our local user's own homeserver, or `None` if the client is not logged
|
||||
/// in.
|
||||
pub own_homeserver: Option<String>,
|
||||
}
|
||||
|
||||
impl From<SdkUnableToDecryptInfo> for UnableToDecryptInfo {
|
||||
@@ -209,6 +225,10 @@ impl From<SdkUnableToDecryptInfo> for UnableToDecryptInfo {
|
||||
event_id: value.event_id.to_string(),
|
||||
time_to_decrypt_ms: value.time_to_decrypt.map(|ttd| ttd.as_millis() as u64),
|
||||
cause: value.cause,
|
||||
event_local_age_millis: value.event_local_age_millis,
|
||||
user_trusts_own_identity: value.user_trusts_own_identity,
|
||||
sender_homeserver: value.sender_homeserver.to_string(),
|
||||
own_homeserver: value.own_homeserver.map(String::from),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,26 +16,55 @@ 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, FullStateEventContent};
|
||||
use ruma::events::{room::MediaSource as RumaMediaSource, EventContent, FullStateEventContent};
|
||||
|
||||
use super::ProfileDetails;
|
||||
use crate::ruma::{ImageInfo, Mentions, MessageType, PollKind};
|
||||
use crate::{
|
||||
error::ClientError,
|
||||
ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind},
|
||||
};
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent {
|
||||
fn from(value: matrix_sdk_ui::timeline::TimelineItemContent) -> Self {
|
||||
use matrix_sdk_ui::timeline::TimelineItemContent as Content;
|
||||
|
||||
match value {
|
||||
Content::Message(message) => TimelineItemContent::Message { content: message.into() },
|
||||
Content::Message(message) => {
|
||||
let msgtype = message.msgtype().msgtype().to_owned();
|
||||
|
||||
match TryInto::<MessageContent>::try_into(message) {
|
||||
Ok(message) => TimelineItemContent::Message { content: message },
|
||||
Err(error) => TimelineItemContent::FailedToParseMessageLike {
|
||||
event_type: msgtype,
|
||||
error: error.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Content::RedactedMessage => TimelineItemContent::RedactedMessage,
|
||||
|
||||
Content::Sticker(sticker) => {
|
||||
let content = sticker.content();
|
||||
TimelineItemContent::Sticker {
|
||||
body: content.body.clone(),
|
||||
info: (&content.info).into(),
|
||||
source: Arc::new(MediaSource::from(content.source.clone())),
|
||||
|
||||
let media_source = RumaMediaSource::from(content.source.clone());
|
||||
|
||||
if let Err(error) = media_source.verify() {
|
||||
return TimelineItemContent::FailedToParseMessageLike {
|
||||
event_type: sticker.content().event_type().to_string(),
|
||||
error: error.to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
match TryInto::<ImageInfo>::try_into(&content.info) {
|
||||
Ok(info) => TimelineItemContent::Sticker {
|
||||
body: content.body.clone(),
|
||||
info,
|
||||
source: Arc::new(MediaSource { media_source }),
|
||||
},
|
||||
Err(error) => TimelineItemContent::FailedToParseMessageLike {
|
||||
event_type: sticker.content().event_type().to_string(),
|
||||
error: error.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,16 +146,18 @@ pub struct MessageContent {
|
||||
pub mentions: Option<Mentions>,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::Message> for MessageContent {
|
||||
fn from(value: matrix_sdk_ui::timeline::Message) -> Self {
|
||||
Self {
|
||||
msg_type: value.msgtype().clone().into(),
|
||||
impl TryFrom<matrix_sdk_ui::timeline::Message> for MessageContent {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: matrix_sdk_ui::timeline::Message) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
msg_type: value.msgtype().clone().try_into()?,
|
||||
body: value.body().to_owned(),
|
||||
in_reply_to: value.in_reply_to().map(|r| Arc::new(r.clone().into())),
|
||||
is_edited: value.is_edited(),
|
||||
thread_root: value.thread_root().map(|id| id.to_string()),
|
||||
mentions: value.mentions().cloned().map(|m| m.into()),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ use matrix_sdk::crypto::CollectStrategy;
|
||||
use matrix_sdk::{
|
||||
attachment::{
|
||||
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
|
||||
BaseThumbnailInfo, BaseVideoInfo, Thumbnail,
|
||||
BaseVideoInfo, Thumbnail,
|
||||
},
|
||||
deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode},
|
||||
room::edit::EditedContent as SdkEditedContent,
|
||||
@@ -53,7 +53,7 @@ use ruma::{
|
||||
},
|
||||
AnyMessageLikeEventContent,
|
||||
},
|
||||
EventId,
|
||||
EventId, UInt,
|
||||
};
|
||||
use tokio::{
|
||||
sync::Mutex,
|
||||
@@ -144,19 +144,26 @@ fn build_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 height = thumbnail_info
|
||||
.height
|
||||
.and_then(|u| UInt::try_from(u).ok())
|
||||
.ok_or(RoomError::InvalidAttachmentData)?;
|
||||
let width = thumbnail_info
|
||||
.width
|
||||
.and_then(|u| UInt::try_from(u).ok())
|
||||
.ok_or(RoomError::InvalidAttachmentData)?;
|
||||
let size = thumbnail_info
|
||||
.size
|
||||
.and_then(|u| UInt::try_from(u).ok())
|
||||
.ok_or(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),
|
||||
};
|
||||
let thumbnail =
|
||||
Thumbnail { data: thumbnail_data, content_type: mime_type, height, width, size };
|
||||
|
||||
Ok(AttachmentConfig::with_thumbnail(thumbnail))
|
||||
}
|
||||
@@ -545,6 +552,7 @@ impl Timeline {
|
||||
.await
|
||||
{
|
||||
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.
|
||||
@@ -560,7 +568,8 @@ impl Timeline {
|
||||
room.send_queue().send(edit_event).await?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err)?,
|
||||
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -977,7 +986,7 @@ impl TimelineItem {
|
||||
pub fn as_virtual(self: Arc<Self>) -> Option<VirtualTimelineItem> {
|
||||
use matrix_sdk_ui::timeline::VirtualTimelineItem as VItem;
|
||||
match self.0.as_virtual()? {
|
||||
VItem::DayDivider(ts) => Some(VirtualTimelineItem::DayDivider { ts: ts.0.into() }),
|
||||
VItem::DateDivider(ts) => Some(VirtualTimelineItem::DateDivider { ts: ts.0.into() }),
|
||||
VItem::ReadMarker => Some(VirtualTimelineItem::ReadMarker),
|
||||
}
|
||||
}
|
||||
@@ -1246,8 +1255,9 @@ impl SendAttachmentJoinHandle {
|
||||
/// A [`TimelineItem`](super::TimelineItem) that doesn't correspond to an event.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum VirtualTimelineItem {
|
||||
/// A divider between messages of two days.
|
||||
DayDivider {
|
||||
/// A divider between messages of different day or month depending on
|
||||
/// timeline settings.
|
||||
DateDivider {
|
||||
/// A timestamp in milliseconds since Unix Epoch on that day in local
|
||||
/// time.
|
||||
ts: u64,
|
||||
@@ -1278,6 +1288,7 @@ impl From<ReceiptType> for ruma::api::client::receipt::create_receipt::v3::Recei
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum EditedContent {
|
||||
RoomMessage { content: Arc<RoomMessageEventContentWithoutRelation> },
|
||||
MediaCaption { caption: Option<String>, formatted_caption: Option<FormattedBody> },
|
||||
PollStart { poll_data: PollData },
|
||||
}
|
||||
|
||||
@@ -1288,6 +1299,12 @@ impl TryFrom<EditedContent> for SdkEditedContent {
|
||||
EditedContent::RoomMessage { content } => {
|
||||
Ok(SdkEditedContent::RoomMessage((*content).clone()))
|
||||
}
|
||||
EditedContent::MediaCaption { caption, formatted_caption } => {
|
||||
Ok(SdkEditedContent::MediaCaption {
|
||||
caption,
|
||||
formatted_caption: formatted_caption.map(Into::into),
|
||||
})
|
||||
}
|
||||
EditedContent::PollStart { poll_data } => {
|
||||
let block: UnstablePollStartContentBlock = poll_data.clone().try_into()?;
|
||||
Ok(SdkEditedContent::PollStart {
|
||||
@@ -1299,6 +1316,23 @@ impl TryFrom<EditedContent> for SdkEditedContent {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a caption edit.
|
||||
///
|
||||
/// If no `formatted_caption` is provided, then it's assumed the `caption`
|
||||
/// represents valid Markdown that can be used as the formatted caption.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
fn create_caption_edit(
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
) -> EditedContent {
|
||||
let formatted_caption =
|
||||
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
|
||||
EditedContent::MediaCaption {
|
||||
caption,
|
||||
formatted_caption: formatted_caption.as_ref().map(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper to retrieve some timeline item info lazily.
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct LazyTimelineItemProvider(Arc<matrix_sdk_ui::timeline::EventTimelineItem>);
|
||||
@@ -1325,3 +1359,20 @@ impl LazyTimelineItemProvider {
|
||||
self.0.local_echo_send_handle().map(|handle| Arc::new(SendHandle::new(handle)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes how date dividers get inserted, either in between each day or in
|
||||
/// between each month
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum DateDividerMode {
|
||||
Daily,
|
||||
Monthly,
|
||||
}
|
||||
|
||||
impl From<DateDividerMode> for matrix_sdk_ui::timeline::DateDividerMode {
|
||||
fn from(value: DateDividerMode) -> Self {
|
||||
match value {
|
||||
DateDividerMode::Daily => Self::Daily,
|
||||
DateDividerMode::Monthly => Self::Monthly,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
# 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
@@ -1,91 +0,0 @@
|
||||
# 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"
|
||||
@@ -2,6 +2,27 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
<!-- next-header -->
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
## [0.9.0] - 2024-12-18
|
||||
|
||||
### Features
|
||||
|
||||
- Introduced support for
|
||||
[MSC4171](https://github.com/matrix-org/matrix-rust-sdk/pull/4335), enabling
|
||||
the designation of certain users as service members. These flagged users are
|
||||
excluded from the room display name calculation.
|
||||
([#4335](https://github.com/matrix-org/matrix-rust-sdk/pull/4335))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix an off-by-one error in the `ObservableMap` when the `remove()` method is
|
||||
called. Previously, items following the removed item were not shifted left by
|
||||
one position, leaving them at incorrect indices.
|
||||
([#4346](https://github.com/matrix-org/matrix-rust-sdk/pull/4346))
|
||||
|
||||
## [0.8.0] - 2024-11-19
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -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.8.0"
|
||||
version = "0.9.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
@@ -30,7 +30,7 @@ uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"]
|
||||
# Private feature, see
|
||||
# https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823 for the gory
|
||||
# details.
|
||||
test-send-sync = []
|
||||
test-send-sync = ["matrix-sdk-crypto?/test-send-sync"]
|
||||
|
||||
# "message-ids" feature doesn't do anything and is deprecated.
|
||||
message-ids = []
|
||||
@@ -49,9 +49,9 @@ as_variant = { workspace = true }
|
||||
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 }
|
||||
bitflags = { version = "2.6.0", features = ["serde"] }
|
||||
decancer = "3.2.8"
|
||||
eyeball = { workspace = true, features = ["async-lock"] }
|
||||
eyeball-im = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
growable-bloom-filter = { workspace = true }
|
||||
@@ -61,9 +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"
|
||||
regex = "1.11.1"
|
||||
ruma = { workspace = true, features = ["canonical-json", "unstable-msc3381", "unstable-msc2867", "rand"] }
|
||||
unicode-normalization = "0.1.24"
|
||||
unicode-normalization = { workspace = true }
|
||||
serde = { workspace = true, features = ["rc"] }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
@@ -85,7 +85,7 @@ similar-asserts = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.33"
|
||||
wasm-bindgen-test = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -26,8 +26,8 @@ use eyeball_im::{Vector, VectorDiff};
|
||||
use futures_util::Stream;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_crypto::{
|
||||
store::DynCryptoStore, CollectStrategy, DecryptionSettings, EncryptionSettings,
|
||||
EncryptionSyncChanges, OlmError, OlmMachine, RoomEventDecryptionResult, ToDeviceRequest,
|
||||
store::DynCryptoStore, types::requests::ToDeviceRequest, CollectStrategy, DecryptionSettings,
|
||||
EncryptionSettings, EncryptionSyncChanges, OlmError, OlmMachine, RoomEventDecryptionResult,
|
||||
TrustRequirement,
|
||||
};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
@@ -1459,10 +1459,14 @@ impl BaseClient {
|
||||
pub async fn share_room_key(&self, room_id: &RoomId) -> Result<Vec<Arc<ToDeviceRequest>>> {
|
||||
match self.olm_machine().await.as_ref() {
|
||||
Some(o) => {
|
||||
let (history_visibility, settings) = self
|
||||
.get_room(room_id)
|
||||
.map(|r| (r.history_visibility(), r.encryption_settings()))
|
||||
.unwrap_or((HistoryVisibility::Joined, None));
|
||||
let Some(room) = self.get_room(room_id) else {
|
||||
return Err(Error::InsufficientData);
|
||||
};
|
||||
|
||||
let history_visibility = room.history_visibility_or_default();
|
||||
let Some(room_encryption_event) = room.encryption_settings() else {
|
||||
return Err(Error::EncryptionNotEnabled);
|
||||
};
|
||||
|
||||
// Don't share the group session with members that are invited
|
||||
// if the history visibility is set to `Joined`
|
||||
@@ -1474,9 +1478,8 @@ impl BaseClient {
|
||||
|
||||
let members = self.store.get_user_ids(room_id, filter).await?;
|
||||
|
||||
let settings = settings.ok_or(Error::EncryptionNotEnabled)?;
|
||||
let settings = EncryptionSettings::new(
|
||||
settings,
|
||||
room_encryption_event,
|
||||
history_visibility,
|
||||
self.room_key_recipient_strategy.clone(),
|
||||
);
|
||||
|
||||
@@ -27,7 +27,7 @@ use ruma::{
|
||||
pub struct DebugListOfRawEventsNoId<'a, T>(pub &'a [Raw<T>]);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a, T> fmt::Debug for DebugListOfRawEventsNoId<'a, T> {
|
||||
impl<T> fmt::Debug for DebugListOfRawEventsNoId<'_, T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut list = f.debug_list();
|
||||
list.entries(self.0.iter().map(DebugRawEventNoId));
|
||||
@@ -41,7 +41,7 @@ impl<'a, T> fmt::Debug for DebugListOfRawEventsNoId<'a, T> {
|
||||
pub struct DebugInvitedRoom<'a>(pub &'a InvitedRoom);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugInvitedRoom<'a> {
|
||||
impl fmt::Debug for DebugInvitedRoom<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("InvitedRoom")
|
||||
.field("invite_state", &DebugListOfRawEvents(&self.0.invite_state.events))
|
||||
@@ -55,7 +55,7 @@ impl<'a> fmt::Debug for DebugInvitedRoom<'a> {
|
||||
pub struct DebugKnockedRoom<'a>(pub &'a KnockedRoom);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugKnockedRoom<'a> {
|
||||
impl fmt::Debug for DebugKnockedRoom<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("KnockedRoom")
|
||||
.field("knock_state", &DebugListOfRawEvents(&self.0.knock_state.events))
|
||||
@@ -66,7 +66,7 @@ impl<'a> fmt::Debug for DebugKnockedRoom<'a> {
|
||||
pub(crate) struct DebugListOfRawEvents<'a, T>(pub &'a [Raw<T>]);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a, T> fmt::Debug for DebugListOfRawEvents<'a, T> {
|
||||
impl<T> fmt::Debug for DebugListOfRawEvents<'_, T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut list = f.debug_list();
|
||||
list.entries(self.0.iter().map(DebugRawEvent));
|
||||
|
||||
@@ -30,7 +30,7 @@ use ruma::{
|
||||
StateEventContent, StaticStateEventContent, StrippedStateEvent, SyncStateEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UserId,
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UInt, UserId,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use unicode_normalization::UnicodeNormalization;
|
||||
@@ -160,12 +160,12 @@ impl PartialEq for DisplayName {
|
||||
|
||||
impl DisplayName {
|
||||
/// Regex pattern matching an MXID.
|
||||
const MXID_PATTERN: &str = "@.+[:.].+";
|
||||
const MXID_PATTERN: &'static 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}]";
|
||||
const LEFT_TO_RIGHT_PATTERN: &'static str = "[\u{202a}-\u{202f}\u{200e}\u{200f}]";
|
||||
|
||||
/// Regex pattern matching bunch of unicode control characters and otherwise
|
||||
/// misleading/invisible characters.
|
||||
@@ -176,7 +176,7 @@ impl DisplayName {
|
||||
/// * 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 =
|
||||
const HIDDEN_CHARACTERS_PATTERN: &'static 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.
|
||||
@@ -476,6 +476,23 @@ impl MemberEvent {
|
||||
.unwrap_or_else(|| self.user_id().localpart()),
|
||||
)
|
||||
}
|
||||
|
||||
/// The optional reason why the membership changed.
|
||||
pub fn reason(&self) -> Option<&str> {
|
||||
match self {
|
||||
MemberEvent::Sync(SyncStateEvent::Original(c)) => c.content.reason.as_deref(),
|
||||
MemberEvent::Stripped(e) => e.content.reason.as_deref(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The optional timestamp for this member event.
|
||||
pub fn timestamp(&self) -> Option<UInt> {
|
||||
match self {
|
||||
MemberEvent::Sync(SyncStateEvent::Original(c)) => Some(c.origin_server_ts.0),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SyncOrStrippedState<RoomPowerLevelsEventContent> {
|
||||
|
||||
@@ -14,13 +14,85 @@
|
||||
|
||||
//! Trait and macro of integration tests for `EventCacheStore` implementations.
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
deserialized_responses::{
|
||||
AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, TimelineEventKind,
|
||||
VerificationState,
|
||||
},
|
||||
linked_chunk::{ChunkContent, LinkedChunk, LinkedChunkBuilder, Position, RawChunk, Update},
|
||||
};
|
||||
use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID};
|
||||
use ruma::{
|
||||
api::client::media::get_content_thumbnail::v3::Method, events::room::MediaSource, mxc_uri, uint,
|
||||
api::client::media::get_content_thumbnail::v3::Method, events::room::MediaSource, mxc_uri,
|
||||
push::Action, room_id, uint, RoomId,
|
||||
};
|
||||
|
||||
use super::DynEventCacheStore;
|
||||
use crate::media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings};
|
||||
use crate::{
|
||||
event_cache::{Event, Gap},
|
||||
media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},
|
||||
};
|
||||
|
||||
/// Create a test event with all data filled, for testing that linked chunk
|
||||
/// correctly stores event data.
|
||||
///
|
||||
/// Keep in sync with [`check_test_event`].
|
||||
pub fn make_test_event(room_id: &RoomId, content: &str) -> SyncTimelineEvent {
|
||||
let encryption_info = EncryptionInfo {
|
||||
sender: (*ALICE).into(),
|
||||
sender_device: None,
|
||||
algorithm_info: AlgorithmInfo::MegolmV1AesSha2 {
|
||||
curve25519_key: "1337".to_owned(),
|
||||
sender_claimed_keys: Default::default(),
|
||||
},
|
||||
verification_state: VerificationState::Verified,
|
||||
};
|
||||
|
||||
let event = EventFactory::new()
|
||||
.text_msg(content)
|
||||
.room(room_id)
|
||||
.sender(*ALICE)
|
||||
.into_raw_timeline()
|
||||
.cast();
|
||||
|
||||
SyncTimelineEvent {
|
||||
kind: TimelineEventKind::Decrypted(DecryptedRoomEvent {
|
||||
event,
|
||||
encryption_info,
|
||||
unsigned_encryption_info: None,
|
||||
}),
|
||||
push_actions: vec![Action::Notify],
|
||||
}
|
||||
}
|
||||
|
||||
/// Check that an event created with [`make_test_event`] contains the expected
|
||||
/// data.
|
||||
///
|
||||
/// Keep in sync with [`make_test_event`].
|
||||
#[track_caller]
|
||||
pub fn check_test_event(event: &SyncTimelineEvent, text: &str) {
|
||||
// Check push actions.
|
||||
let actions = &event.push_actions;
|
||||
assert_eq!(actions.len(), 1);
|
||||
assert_matches!(&actions[0], Action::Notify);
|
||||
|
||||
// Check content.
|
||||
assert_matches!(&event.kind, TimelineEventKind::Decrypted(d) => {
|
||||
// Check encryption fields.
|
||||
assert_eq!(d.encryption_info.sender, *ALICE);
|
||||
assert_matches!(&d.encryption_info.algorithm_info, AlgorithmInfo::MegolmV1AesSha2 { curve25519_key, .. } => {
|
||||
assert_eq!(curve25519_key, "1337");
|
||||
});
|
||||
|
||||
// Check event.
|
||||
let deserialized = d.event.deserialize().unwrap();
|
||||
assert_matches!(deserialized, ruma::events::AnyMessageLikeEvent::RoomMessage(msg) => {
|
||||
assert_eq!(msg.as_original().unwrap().content.body(), text);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// `EventCacheStore` integration tests.
|
||||
///
|
||||
@@ -34,6 +106,21 @@ pub trait EventCacheStoreIntegrationTests {
|
||||
|
||||
/// Test replacing a MXID.
|
||||
async fn test_replace_media_key(&self);
|
||||
|
||||
/// Test handling updates to a linked chunk and reloading these updates from
|
||||
/// the store.
|
||||
async fn test_handle_updates_and_rebuild_linked_chunk(&self);
|
||||
|
||||
/// Test that rebuilding a linked chunk from an empty store doesn't return
|
||||
/// anything.
|
||||
async fn test_rebuild_empty_linked_chunk(&self);
|
||||
|
||||
/// Test that clear all the rooms' linked chunks works.
|
||||
async fn test_clear_all_rooms_chunks(&self);
|
||||
}
|
||||
|
||||
fn rebuild_linked_chunk(raws: Vec<RawChunk<Event, Gap>>) -> Option<LinkedChunk<3, Event, Gap>> {
|
||||
LinkedChunkBuilder::from_raw_parts(raws).build().unwrap()
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
@@ -83,6 +170,11 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
Some(&content),
|
||||
"media not found though added"
|
||||
);
|
||||
assert_eq!(
|
||||
self.get_media_content_for_uri(uri).await.unwrap().as_ref(),
|
||||
Some(&content),
|
||||
"media not found by URI though added"
|
||||
);
|
||||
|
||||
// Let's remove the media.
|
||||
self.remove_media_content(&request_file).await.expect("removing media failed");
|
||||
@@ -92,6 +184,10 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
self.get_media_content(&request_file).await.unwrap().is_none(),
|
||||
"media still there after removing"
|
||||
);
|
||||
assert!(
|
||||
self.get_media_content_for_uri(uri).await.unwrap().is_none(),
|
||||
"media still found by URI after removing"
|
||||
);
|
||||
|
||||
// Let's add the media again.
|
||||
self.add_media_content(&request_file, content.clone())
|
||||
@@ -116,6 +212,12 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
"thumbnail not found"
|
||||
);
|
||||
|
||||
// We get a file with the URI, we don't know which one.
|
||||
assert!(
|
||||
self.get_media_content_for_uri(uri).await.unwrap().is_some(),
|
||||
"media not found by URI though two where added"
|
||||
);
|
||||
|
||||
// Let's add another media with a different URI.
|
||||
self.add_media_content(&request_other_file, other_content.clone())
|
||||
.await
|
||||
@@ -127,6 +229,11 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
Some(&other_content),
|
||||
"other file not found"
|
||||
);
|
||||
assert_eq!(
|
||||
self.get_media_content_for_uri(other_uri).await.unwrap().as_ref(),
|
||||
Some(&other_content),
|
||||
"other file not found by URI"
|
||||
);
|
||||
|
||||
// Let's remove media based on URI.
|
||||
self.remove_media_content_for_uri(uri).await.expect("removing all media for uri failed");
|
||||
@@ -143,6 +250,14 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
self.get_media_content(&request_other_file).await.unwrap().is_some(),
|
||||
"other media was removed"
|
||||
);
|
||||
assert!(
|
||||
self.get_media_content_for_uri(uri).await.unwrap().is_none(),
|
||||
"media found by URI wasn't removed"
|
||||
);
|
||||
assert!(
|
||||
self.get_media_content_for_uri(other_uri).await.unwrap().is_some(),
|
||||
"other media found by URI was removed"
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_replace_media_key(&self) {
|
||||
@@ -182,6 +297,149 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
// Finding with the new request does work.
|
||||
assert_eq!(self.get_media_content(&new_req).await.unwrap().unwrap(), b"hello");
|
||||
}
|
||||
|
||||
async fn test_handle_updates_and_rebuild_linked_chunk(&self) {
|
||||
use matrix_sdk_common::linked_chunk::ChunkIdentifier as CId;
|
||||
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
room_id,
|
||||
vec![
|
||||
// new chunk
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![
|
||||
make_test_event(room_id, "hello"),
|
||||
make_test_event(room_id, "world"),
|
||||
],
|
||||
},
|
||||
// a gap chunk
|
||||
Update::NewGapChunk {
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "parmesan".to_owned() },
|
||||
},
|
||||
// another items chunk
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(2), 0),
|
||||
items: vec![make_test_event(room_id, "sup")],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// The linked chunk is correctly reloaded.
|
||||
let raws = self.reload_linked_chunk(room_id).await.unwrap();
|
||||
let lc = rebuild_linked_chunk(raws).expect("linked chunk not empty");
|
||||
|
||||
let mut chunks = lc.chunks();
|
||||
|
||||
{
|
||||
let first = chunks.next().unwrap();
|
||||
// Note: we can't assert the previous/next chunks, as these fields and their
|
||||
// getters are private.
|
||||
assert_eq!(first.identifier(), CId::new(0));
|
||||
|
||||
assert_matches!(first.content(), ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 2);
|
||||
check_test_event(&events[0], "hello");
|
||||
check_test_event(&events[1], "world");
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let second = chunks.next().unwrap();
|
||||
assert_eq!(second.identifier(), CId::new(1));
|
||||
|
||||
assert_matches!(second.content(), ChunkContent::Gap(gap) => {
|
||||
assert_eq!(gap.prev_token, "parmesan");
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let third = chunks.next().unwrap();
|
||||
assert_eq!(third.identifier(), CId::new(2));
|
||||
|
||||
assert_matches!(third.content(), ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 1);
|
||||
check_test_event(&events[0], "sup");
|
||||
});
|
||||
}
|
||||
|
||||
assert!(chunks.next().is_none());
|
||||
}
|
||||
|
||||
async fn test_rebuild_empty_linked_chunk(&self) {
|
||||
// When I rebuild a linked chunk from an empty store, it's empty.
|
||||
let raw_parts = self.reload_linked_chunk(&DEFAULT_TEST_ROOM_ID).await.unwrap();
|
||||
assert!(rebuild_linked_chunk(raw_parts).is_none());
|
||||
}
|
||||
|
||||
async fn test_clear_all_rooms_chunks(&self) {
|
||||
use matrix_sdk_common::linked_chunk::ChunkIdentifier as CId;
|
||||
|
||||
let r0 = room_id!("!r0:matrix.org");
|
||||
let r1 = room_id!("!r1:matrix.org");
|
||||
|
||||
// Add updates for the first room.
|
||||
self.handle_linked_chunk_updates(
|
||||
r0,
|
||||
vec![
|
||||
// new chunk
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![make_test_event(r0, "hello"), make_test_event(r0, "world")],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Add updates for the second room.
|
||||
self.handle_linked_chunk_updates(
|
||||
r1,
|
||||
vec![
|
||||
// Empty items chunk.
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// a gap chunk
|
||||
Update::NewGapChunk {
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "bleu d'auvergne".to_owned() },
|
||||
},
|
||||
// another items chunk
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(2), 0),
|
||||
items: vec![make_test_event(r0, "yummy")],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Sanity check: both linked chunks can be reloaded.
|
||||
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r0).await.unwrap()).is_some());
|
||||
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r1).await.unwrap()).is_some());
|
||||
|
||||
// Clear the chunks.
|
||||
self.clear_all_rooms_chunks().await.unwrap();
|
||||
|
||||
// Both rooms now have no linked chunk.
|
||||
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r0).await.unwrap()).is_none());
|
||||
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r1).await.unwrap()).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
/// Macro building to allow your `EventCacheStore` implementation to run the
|
||||
@@ -236,6 +494,27 @@ macro_rules! event_cache_store_integration_tests {
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_replace_media_key().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_handle_updates_and_rebuild_linked_chunk() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_handle_updates_and_rebuild_linked_chunk().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_rebuild_empty_linked_chunk() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_rebuild_empty_linked_chunk().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_clear_all_rooms_chunks() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_clear_all_rooms_chunks().await;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,12 +16,17 @@ use std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock, ti
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
ring_buffer::RingBuffer, store_locks::memory_store_helper::try_take_leased_lock,
|
||||
linked_chunk::{relational::RelationalLinkedChunk, RawChunk, Update},
|
||||
ring_buffer::RingBuffer,
|
||||
store_locks::memory_store_helper::try_take_leased_lock,
|
||||
};
|
||||
use ruma::{MxcUri, OwnedMxcUri};
|
||||
use ruma::{MxcUri, OwnedMxcUri, RoomId};
|
||||
|
||||
use super::{EventCacheStore, EventCacheStoreError, Result};
|
||||
use crate::media::{MediaRequestParameters, UniqueKey as _};
|
||||
use crate::{
|
||||
event_cache::{Event, Gap},
|
||||
media::{MediaRequestParameters, UniqueKey as _},
|
||||
};
|
||||
|
||||
/// In-memory, non-persistent implementation of the `EventCacheStore`.
|
||||
///
|
||||
@@ -29,8 +34,14 @@ use crate::media::{MediaRequestParameters, UniqueKey as _};
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[derive(Debug)]
|
||||
pub struct MemoryStore {
|
||||
media: StdRwLock<RingBuffer<(OwnedMxcUri, String /* unique key */, Vec<u8>)>>,
|
||||
leases: StdRwLock<HashMap<String, (String, Instant)>>,
|
||||
inner: StdRwLock<MemoryStoreInner>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MemoryStoreInner {
|
||||
media: RingBuffer<(OwnedMxcUri, String /* unique key */, Vec<u8>)>,
|
||||
leases: HashMap<String, (String, Instant)>,
|
||||
events: RelationalLinkedChunk<Event, Gap>,
|
||||
}
|
||||
|
||||
// SAFETY: `new_unchecked` is safe because 20 is not zero.
|
||||
@@ -39,8 +50,11 @@ 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)),
|
||||
leases: Default::default(),
|
||||
inner: StdRwLock::new(MemoryStoreInner {
|
||||
media: RingBuffer::new(NUMBER_OF_MEDIAS),
|
||||
leases: Default::default(),
|
||||
events: RelationalLinkedChunk::new(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,7 +77,36 @@ impl EventCacheStore for MemoryStore {
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Result<bool, Self::Error> {
|
||||
Ok(try_take_leased_lock(&self.leases, lease_duration_ms, key, holder))
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
Ok(try_take_leased_lock(&mut inner.leases, lease_duration_ms, key, holder))
|
||||
}
|
||||
|
||||
async fn handle_linked_chunk_updates(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
updates: Vec<Update<Event, Gap>>,
|
||||
) -> Result<(), Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
inner.events.apply_updates(room_id, updates);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn reload_linked_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
inner
|
||||
.events
|
||||
.reload_chunks(room_id)
|
||||
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
|
||||
}
|
||||
|
||||
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error> {
|
||||
self.inner.write().unwrap().events.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_media_content(
|
||||
@@ -73,8 +116,10 @@ impl EventCacheStore for MemoryStore {
|
||||
) -> Result<()> {
|
||||
// Avoid duplication. Let's try to remove it first.
|
||||
self.remove_media_content(request).await?;
|
||||
|
||||
// Now, let's add it.
|
||||
self.media.write().unwrap().push((request.uri().to_owned(), request.unique_key(), data));
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
inner.media.push((request.uri().to_owned(), request.unique_key(), data));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -86,8 +131,10 @@ impl EventCacheStore for MemoryStore {
|
||||
) -> 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) {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
if let Some((mxc, key, _)) = inner.media.iter_mut().find(|(_, key, _)| *key == expected_key)
|
||||
{
|
||||
*mxc = to.uri().to_owned();
|
||||
*key = to.unique_key();
|
||||
}
|
||||
@@ -98,8 +145,9 @@ impl EventCacheStore for MemoryStore {
|
||||
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)| {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
Ok(inner.media.iter().find_map(|(_media_uri, media_key, media_content)| {
|
||||
(media_key == &expected_key).then(|| media_content.to_owned())
|
||||
}))
|
||||
}
|
||||
@@ -107,23 +155,38 @@ impl EventCacheStore for MemoryStore {
|
||||
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
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
let Some(index) = inner
|
||||
.media
|
||||
.iter()
|
||||
.position(|(_media_uri, media_key, _media_content)| media_key == &expected_key)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
media.remove(index);
|
||||
inner.media.remove(index);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_media_content_for_uri(
|
||||
&self,
|
||||
uri: &MxcUri,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
Ok(inner.media.iter().find_map(|(media_uri, _media_key, media_content)| {
|
||||
(media_uri == uri).then(|| media_content.to_owned())
|
||||
}))
|
||||
}
|
||||
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
|
||||
let mut media = self.media.write().unwrap();
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
let expected_key = uri.to_owned();
|
||||
let positions = media
|
||||
let positions = inner
|
||||
.media
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(position, (media_uri, _media_key, _media_content))| {
|
||||
@@ -133,7 +196,7 @@ impl EventCacheStore for MemoryStore {
|
||||
|
||||
// Iterate in reverse-order so that positions stay valid after first removals.
|
||||
for position in positions.into_iter().rev() {
|
||||
media.remove(position);
|
||||
inner.media.remove(position);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -36,14 +36,14 @@ pub use matrix_sdk_store_encryption::Error as StoreEncryptionError;
|
||||
pub use self::integration_tests::EventCacheStoreIntegrationTests;
|
||||
pub use self::{
|
||||
memory_store::MemoryStore,
|
||||
traits::{DynEventCacheStore, EventCacheStore, IntoEventCacheStore},
|
||||
traits::{DynEventCacheStore, EventCacheStore, IntoEventCacheStore, DEFAULT_CHUNK_CAPACITY},
|
||||
};
|
||||
|
||||
/// 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>,
|
||||
cross_process_lock: Arc<CrossProcessStoreLock<LockableEventCacheStore>>,
|
||||
|
||||
/// The store itself.
|
||||
///
|
||||
@@ -70,11 +70,11 @@ impl EventCacheStoreLock {
|
||||
let store = store.into_event_cache_store();
|
||||
|
||||
Self {
|
||||
cross_process_lock: CrossProcessStoreLock::new(
|
||||
cross_process_lock: Arc::new(CrossProcessStoreLock::new(
|
||||
LockableEventCacheStore(store.clone()),
|
||||
"default".to_owned(),
|
||||
holder,
|
||||
),
|
||||
)),
|
||||
store,
|
||||
}
|
||||
}
|
||||
@@ -100,13 +100,13 @@ pub struct EventCacheStoreLockGuard<'a> {
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for EventCacheStoreLockGuard<'a> {
|
||||
impl fmt::Debug for EventCacheStoreLockGuard<'_> {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.debug_struct("EventCacheStoreLockGuard").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for EventCacheStoreLockGuard<'a> {
|
||||
impl Deref for EventCacheStoreLockGuard<'_> {
|
||||
type Target = DynEventCacheStore;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
@@ -138,12 +138,23 @@ pub enum EventCacheStoreError {
|
||||
#[error("Error encoding or decoding data from the event cache store: {0}")]
|
||||
Codec(#[from] Utf8Error),
|
||||
|
||||
/// The store failed to serialize or deserialize some data.
|
||||
#[error("Error serializing or deserializing data from the event cache store: {0}")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
|
||||
/// 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),
|
||||
|
||||
/// The store contains invalid data.
|
||||
#[error("The store contains invalid data: {details}")]
|
||||
InvalidData {
|
||||
/// Details why the data contained in the store was invalid.
|
||||
details: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl EventCacheStoreError {
|
||||
|
||||
@@ -15,11 +15,22 @@
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::AsyncTraitDeps;
|
||||
use ruma::MxcUri;
|
||||
use matrix_sdk_common::{
|
||||
linked_chunk::{RawChunk, Update},
|
||||
AsyncTraitDeps,
|
||||
};
|
||||
use ruma::{MxcUri, RoomId};
|
||||
|
||||
use super::EventCacheStoreError;
|
||||
use crate::media::MediaRequestParameters;
|
||||
use crate::{
|
||||
event_cache::{Event, Gap},
|
||||
media::MediaRequestParameters,
|
||||
};
|
||||
|
||||
/// A default capacity for linked chunks, when manipulating in conjunction with
|
||||
/// an `EventCacheStore` implementation.
|
||||
// TODO: move back?
|
||||
pub const DEFAULT_CHUNK_CAPACITY: usize = 128;
|
||||
|
||||
/// An abstract trait that can be used to implement different store backends
|
||||
/// for the event cache of the SDK.
|
||||
@@ -37,6 +48,28 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
holder: &str,
|
||||
) -> Result<bool, Self::Error>;
|
||||
|
||||
/// An [`Update`] reflects an operation that has happened inside a linked
|
||||
/// chunk. The linked chunk is used by the event cache to store the events
|
||||
/// in-memory. This method aims at forwarding this update inside this store.
|
||||
async fn handle_linked_chunk_updates(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
updates: Vec<Update<Event, Gap>>,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Return all the raw components of a linked chunk, so the caller may
|
||||
/// reconstruct the linked chunk later.
|
||||
async fn reload_linked_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error>;
|
||||
|
||||
/// Clear persisted events for all the rooms.
|
||||
///
|
||||
/// This will empty and remove all the linked chunks stored previously,
|
||||
/// using the above [`Self::handle_linked_chunk_updates`] methods.
|
||||
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error>;
|
||||
|
||||
/// Add a media file's content in the media store.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -95,6 +128,23 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Get a media file's content associated to an `MxcUri` from the
|
||||
/// media store.
|
||||
///
|
||||
/// In theory, there could be several files stored using the same URI and a
|
||||
/// different `MediaFormat`. This API is meant to be used with a media file
|
||||
/// that has only been stored with a single format.
|
||||
///
|
||||
/// If there are several media files for a given URI in different formats,
|
||||
/// this API will only return one of them. Which one is left as an
|
||||
/// implementation detail.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `uri` - The `MxcUri` of the media file.
|
||||
async fn get_media_content_for_uri(&self, uri: &MxcUri)
|
||||
-> Result<Option<Vec<u8>>, Self::Error>;
|
||||
|
||||
/// Remove all the media files' content associated to an `MxcUri` from the
|
||||
/// media store.
|
||||
///
|
||||
@@ -131,6 +181,25 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
|
||||
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn handle_linked_chunk_updates(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
updates: Vec<Update<Event, Gap>>,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.handle_linked_chunk_updates(room_id, updates).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn reload_linked_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
|
||||
self.0.reload_linked_chunk(room_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error> {
|
||||
self.0.clear_all_rooms_chunks().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn add_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
@@ -161,6 +230,13 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
|
||||
self.0.remove_media_content(request).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn get_media_content_for_uri(
|
||||
&self,
|
||||
uri: &MxcUri,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
self.0.get_media_content_for_uri(uri).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error> {
|
||||
self.0.remove_media_content_for_uri(uri).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ pub fn is_suitable_for_latest_event<'a>(
|
||||
// Check if this is a replacement for another message. If it is, ignore it
|
||||
if let Some(original_message) = message.as_original() {
|
||||
let is_replacement =
|
||||
original_message.content.relates_to.as_ref().map_or(false, |relates_to| {
|
||||
original_message.content.relates_to.as_ref().is_some_and(|relates_to| {
|
||||
if let Some(relation_type) = relates_to.rel_type() {
|
||||
relation_type == RelationType::Replacement
|
||||
} else {
|
||||
@@ -83,12 +83,13 @@ pub fn is_suitable_for_latest_event<'a>(
|
||||
});
|
||||
|
||||
if is_replacement {
|
||||
return PossibleLatestEvent::NoUnsupportedMessageLikeType;
|
||||
PossibleLatestEvent::NoUnsupportedMessageLikeType
|
||||
} else {
|
||||
PossibleLatestEvent::YesRoomMessage(message)
|
||||
}
|
||||
return PossibleLatestEvent::YesRoomMessage(message);
|
||||
} else {
|
||||
PossibleLatestEvent::YesRoomMessage(message)
|
||||
}
|
||||
|
||||
return PossibleLatestEvent::YesRoomMessage(message);
|
||||
}
|
||||
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(poll)) => {
|
||||
|
||||
@@ -438,7 +438,7 @@ fn events_intersects<'a>(
|
||||
let previous_events_ids = BTreeSet::from_iter(previous_events.filter_map(|ev| ev.event_id()));
|
||||
new_events
|
||||
.iter()
|
||||
.any(|ev| ev.event_id().map_or(false, |event_id| previous_events_ids.contains(&event_id)))
|
||||
.any(|ev| ev.event_id().is_some_and(|event_id| previous_events_ids.contains(&event_id)))
|
||||
}
|
||||
|
||||
/// Given a set of events coming from sync, for a room, update the
|
||||
|
||||
@@ -18,9 +18,11 @@ use std::{
|
||||
};
|
||||
|
||||
use ruma::{
|
||||
events::{AnyGlobalAccountDataEvent, GlobalAccountDataEventType},
|
||||
events::{
|
||||
direct::OwnedDirectUserIdentifier, AnyGlobalAccountDataEvent, GlobalAccountDataEventType,
|
||||
},
|
||||
serde::Raw,
|
||||
OwnedUserId, RoomId,
|
||||
RoomId,
|
||||
};
|
||||
use tracing::{debug, instrument, trace, warn};
|
||||
|
||||
@@ -94,10 +96,10 @@ impl AccountDataProcessor {
|
||||
for event in events {
|
||||
let AnyGlobalAccountDataEvent::Direct(direct_event) = event else { continue };
|
||||
|
||||
let mut new_dms = HashMap::<&RoomId, HashSet<OwnedUserId>>::new();
|
||||
for (user_id, rooms) in direct_event.content.iter() {
|
||||
let mut new_dms = HashMap::<&RoomId, HashSet<OwnedDirectUserIdentifier>>::new();
|
||||
for (user_identifier, rooms) in direct_event.content.iter() {
|
||||
for room_id in rooms {
|
||||
new_dms.entry(room_id).or_default().insert(user_id.clone());
|
||||
new_dms.entry(room_id).or_default().insert(user_identifier.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#![allow(clippy::assign_op_pattern)] // triggered by bitflags! usage
|
||||
#![allow(clippy::assign_op_pattern)] // Triggered by bitflags! usage
|
||||
|
||||
mod members;
|
||||
pub(crate) mod normal;
|
||||
@@ -21,6 +21,7 @@ use ruma::{
|
||||
events::{
|
||||
beacon_info::BeaconInfoEventContent,
|
||||
call::member::{CallMemberEventContent, CallMemberStateKey},
|
||||
direct::OwnedDirectUserIdentifier,
|
||||
macros::EventContent,
|
||||
room::{
|
||||
avatar::RoomAvatarEventContent,
|
||||
@@ -127,7 +128,7 @@ pub struct BaseRoomInfo {
|
||||
pub(crate) create: Option<MinimalStateEvent<RoomCreateWithCreatorEventContent>>,
|
||||
/// A list of user ids this room is considered as direct message, if this
|
||||
/// room is a DM.
|
||||
pub(crate) dm_targets: HashSet<OwnedUserId>,
|
||||
pub(crate) dm_targets: HashSet<OwnedDirectUserIdentifier>,
|
||||
/// The `m.room.encryption` event content that enabled E2EE in this room.
|
||||
pub(crate) encryption: Option<RoomEncryptionEventContent>,
|
||||
/// The guest access policy of this room.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -269,7 +269,7 @@ impl BaseClient {
|
||||
.or_insert_with(JoinedRoomUpdate::default)
|
||||
.account_data
|
||||
.append(&mut raw.to_vec()),
|
||||
RoomState::Left => new_rooms
|
||||
RoomState::Left | RoomState::Banned => new_rooms
|
||||
.leave
|
||||
.entry(room_id.to_owned())
|
||||
.or_insert_with(LeftRoomUpdate::default)
|
||||
@@ -546,7 +546,7 @@ impl BaseClient {
|
||||
))
|
||||
}
|
||||
|
||||
RoomState::Left => Ok((
|
||||
RoomState::Left | RoomState::Banned => Ok((
|
||||
room_info,
|
||||
None,
|
||||
Some(LeftRoomUpdate::new(
|
||||
@@ -911,7 +911,7 @@ mod tests {
|
||||
api::client::sync::sync_events::UnreadNotificationsCount,
|
||||
assign, event_id,
|
||||
events::{
|
||||
direct::DirectEventContent,
|
||||
direct::{DirectEventContent, DirectUserIdentifier, OwnedDirectUserIdentifier},
|
||||
room::{
|
||||
avatar::RoomAvatarEventContent,
|
||||
canonical_alias::RoomCanonicalAliasEventContent,
|
||||
@@ -1247,7 +1247,7 @@ mod tests {
|
||||
room.required_state.push(make_state_event(
|
||||
user_b_id,
|
||||
user_a_id.as_str(),
|
||||
RoomMemberEventContent::new(membership),
|
||||
RoomMemberEventContent::new(membership.clone()),
|
||||
None,
|
||||
));
|
||||
let response = response_with_room(room_id, room);
|
||||
@@ -1256,8 +1256,17 @@ mod tests {
|
||||
.await
|
||||
.expect("Failed to process sync");
|
||||
|
||||
// The room is left.
|
||||
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
|
||||
match membership {
|
||||
MembershipState::Leave => {
|
||||
// The room is left.
|
||||
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
|
||||
}
|
||||
MembershipState::Ban => {
|
||||
// The room is banned.
|
||||
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Banned);
|
||||
}
|
||||
_ => panic!("Unexpected membership state found: {membership}"),
|
||||
}
|
||||
|
||||
// And it is added to the list of left rooms only.
|
||||
assert!(!sync_resp.rooms.join.contains_key(room_id));
|
||||
@@ -1337,7 +1346,7 @@ mod tests {
|
||||
create_dm(&client, room_id, user_a_id, user_b_id, MembershipState::Join).await;
|
||||
|
||||
// (Sanity: B is a direct target, and is in Join state)
|
||||
assert!(direct_targets(&client, room_id).contains(user_b_id));
|
||||
assert!(direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id)));
|
||||
assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Join);
|
||||
|
||||
// When B leaves
|
||||
@@ -1346,7 +1355,7 @@ mod tests {
|
||||
// Then B is still a direct target, and is in Leave state (B is a direct target
|
||||
// because we want to return to our old DM in the UI even if the other
|
||||
// user left, so we can reinvite them. See https://github.com/matrix-org/matrix-rust-sdk/issues/2017)
|
||||
assert!(direct_targets(&client, room_id).contains(user_b_id));
|
||||
assert!(direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id)));
|
||||
assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Leave);
|
||||
}
|
||||
|
||||
@@ -1362,7 +1371,7 @@ mod tests {
|
||||
create_dm(&client, room_id, user_a_id, user_b_id, MembershipState::Invite).await;
|
||||
|
||||
// (Sanity: B is a direct target, and is in Invite state)
|
||||
assert!(direct_targets(&client, room_id).contains(user_b_id));
|
||||
assert!(direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id)));
|
||||
assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Invite);
|
||||
|
||||
// When B declines the invitation (i.e. leaves)
|
||||
@@ -1371,7 +1380,7 @@ mod tests {
|
||||
// Then B is still a direct target, and is in Leave state (B is a direct target
|
||||
// because we want to return to our old DM in the UI even if the other
|
||||
// user left, so we can reinvite them. See https://github.com/matrix-org/matrix-rust-sdk/issues/2017)
|
||||
assert!(direct_targets(&client, room_id).contains(user_b_id));
|
||||
assert!(direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id)));
|
||||
assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Leave);
|
||||
}
|
||||
|
||||
@@ -1389,7 +1398,7 @@ mod tests {
|
||||
assert_eq!(membership(&client, room_id, user_a_id).await, MembershipState::Join);
|
||||
|
||||
// (Sanity: B is a direct target, and is in Join state)
|
||||
assert!(direct_targets(&client, room_id).contains(user_b_id));
|
||||
assert!(direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id)));
|
||||
assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Join);
|
||||
|
||||
let room = client.get_room(room_id).unwrap();
|
||||
@@ -1413,7 +1422,7 @@ mod tests {
|
||||
assert_eq!(membership(&client, room_id, user_a_id).await, MembershipState::Join);
|
||||
|
||||
// (Sanity: B is a direct target, and is in Join state)
|
||||
assert!(direct_targets(&client, room_id).contains(user_b_id));
|
||||
assert!(direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id)));
|
||||
assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Invite);
|
||||
|
||||
let room = client.get_room(room_id).unwrap();
|
||||
@@ -2558,9 +2567,10 @@ mod tests {
|
||||
let mut room_response = http::response::Room::new();
|
||||
set_room_joined(&mut room_response, user_a_id);
|
||||
let mut response = response_with_room(room_id_1, room_response);
|
||||
let mut direct_content = BTreeMap::new();
|
||||
direct_content.insert(user_a_id.to_owned(), vec![room_id_1.to_owned()]);
|
||||
direct_content.insert(user_b_id.to_owned(), vec![room_id_2.to_owned()]);
|
||||
let mut direct_content: BTreeMap<OwnedDirectUserIdentifier, Vec<OwnedRoomId>> =
|
||||
BTreeMap::new();
|
||||
direct_content.insert(user_a_id.into(), vec![room_id_1.to_owned()]);
|
||||
direct_content.insert(user_b_id.into(), vec![room_id_2.to_owned()]);
|
||||
response
|
||||
.extensions
|
||||
.account_data
|
||||
@@ -2656,7 +2666,7 @@ mod tests {
|
||||
.unwrap(),
|
||||
UnableToDecryptInfo {
|
||||
session_id: Some("".to_owned()),
|
||||
reason: UnableToDecryptReason::MissingMegolmSession,
|
||||
reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None },
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -2671,7 +2681,7 @@ mod tests {
|
||||
member.membership().clone()
|
||||
}
|
||||
|
||||
fn direct_targets(client: &BaseClient, room_id: &RoomId) -> HashSet<OwnedUserId> {
|
||||
fn direct_targets(client: &BaseClient, room_id: &RoomId) -> HashSet<OwnedDirectUserIdentifier> {
|
||||
let room = client.get_room(room_id).expect("Room not found!");
|
||||
room.direct_targets()
|
||||
}
|
||||
@@ -2730,8 +2740,9 @@ mod tests {
|
||||
user_id: OwnedUserId,
|
||||
room_ids: Vec<OwnedRoomId>,
|
||||
) {
|
||||
let mut direct_content = BTreeMap::new();
|
||||
direct_content.insert(user_id, room_ids);
|
||||
let mut direct_content: BTreeMap<OwnedDirectUserIdentifier, Vec<OwnedRoomId>> =
|
||||
BTreeMap::new();
|
||||
direct_content.insert(user_id.into(), room_ids);
|
||||
response
|
||||
.extensions
|
||||
.account_data
|
||||
|
||||
@@ -90,6 +90,8 @@ pub trait StateStoreIntegrationTests {
|
||||
async fn test_send_queue_priority(&self);
|
||||
/// Test operations related to send queue dependents.
|
||||
async fn test_send_queue_dependents(&self);
|
||||
/// Test an update to a send queue dependent request.
|
||||
async fn test_update_send_queue_dependent(&self);
|
||||
/// Test saving/restoring server capabilities.
|
||||
async fn test_server_capabilities_saving(&self);
|
||||
}
|
||||
@@ -972,6 +974,24 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
|
||||
self.populate().await?;
|
||||
|
||||
{
|
||||
// Add a send queue request in that room.
|
||||
let txn = TransactionId::new();
|
||||
let ev =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("sup").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id, txn.clone(), ev.into(), 0).await?;
|
||||
|
||||
// Add a single dependent queue request.
|
||||
self.save_dependent_queued_request(
|
||||
room_id,
|
||||
&txn,
|
||||
ChildTransactionId::new(),
|
||||
DependentQueuedRequestKind::RedactEvent,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
self.remove_room(room_id).await?;
|
||||
|
||||
assert_eq!(self.get_room_infos().await?.len(), 1, "room is still there");
|
||||
@@ -1023,6 +1043,8 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
.is_empty(),
|
||||
"still event recepts in the store"
|
||||
);
|
||||
assert!(self.load_send_queue_requests(room_id).await?.is_empty());
|
||||
assert!(self.load_dependent_queued_requests(room_id).await?.is_empty());
|
||||
|
||||
self.remove_room(stripped_room_id).await?;
|
||||
|
||||
@@ -1458,7 +1480,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
// Update the event id.
|
||||
let event_id = owned_event_id!("$1");
|
||||
let num_updated = self
|
||||
.update_dependent_queued_request(
|
||||
.mark_dependent_queued_requests_as_ready(
|
||||
room_id,
|
||||
&txn0,
|
||||
SentRequestKey::Event(event_id.clone()),
|
||||
@@ -1528,6 +1550,54 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let dependents = self.load_dependent_queued_requests(room_id).await.unwrap();
|
||||
assert_eq!(dependents.len(), 2);
|
||||
}
|
||||
|
||||
async fn test_update_send_queue_dependent(&self) {
|
||||
let room_id = room_id!("!test_send_queue_dependents:localhost");
|
||||
|
||||
let txn = TransactionId::new();
|
||||
|
||||
// Save a dependent redaction for an event.
|
||||
let child_txn = ChildTransactionId::new();
|
||||
|
||||
self.save_dependent_queued_request(
|
||||
room_id,
|
||||
&txn,
|
||||
child_txn.clone(),
|
||||
DependentQueuedRequestKind::RedactEvent,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// It worked.
|
||||
let dependents = self.load_dependent_queued_requests(room_id).await.unwrap();
|
||||
assert_eq!(dependents.len(), 1);
|
||||
assert_eq!(dependents[0].parent_transaction_id, txn);
|
||||
assert_eq!(dependents[0].own_transaction_id, child_txn);
|
||||
assert!(dependents[0].parent_key.is_none());
|
||||
assert_matches!(dependents[0].kind, DependentQueuedRequestKind::RedactEvent);
|
||||
|
||||
// Make it a reaction, instead of a redaction.
|
||||
self.update_dependent_queued_request(
|
||||
room_id,
|
||||
&child_txn,
|
||||
DependentQueuedRequestKind::ReactEvent { key: "👍".to_owned() },
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// It worked.
|
||||
let dependents = self.load_dependent_queued_requests(room_id).await.unwrap();
|
||||
assert_eq!(dependents.len(), 1);
|
||||
assert_eq!(dependents[0].parent_transaction_id, txn);
|
||||
assert_eq!(dependents[0].own_transaction_id, child_txn);
|
||||
assert!(dependents[0].parent_key.is_none());
|
||||
assert_matches!(
|
||||
&dependents[0].kind,
|
||||
DependentQueuedRequestKind::ReactEvent { key } => {
|
||||
assert_eq!(key, "👍");
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Macro building to allow your StateStore implementation to run the entire
|
||||
@@ -1686,6 +1756,12 @@ macro_rules! statestore_integration_tests {
|
||||
let store = get_store().await.expect("creating store failed").into_state_store();
|
||||
store.test_send_queue_dependents().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_update_send_queue_dependent() {
|
||||
let store = get_store().await.expect("creating store failed").into_state_store();
|
||||
store.test_update_send_queue_dependent().await;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,7 @@ use std::{
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
use ruma::{
|
||||
events::{
|
||||
direct::OwnedDirectUserIdentifier,
|
||||
room::{
|
||||
avatar::RoomAvatarEventContent,
|
||||
canonical_alias::RoomCanonicalAliasEventContent,
|
||||
@@ -200,12 +201,17 @@ impl BaseRoomInfoV1 {
|
||||
MinimalStateEvent::Redacted(ev) => MinimalStateEvent::Redacted(ev),
|
||||
});
|
||||
|
||||
let mut converted_dm_targets = HashSet::new();
|
||||
for dm_target in dm_targets {
|
||||
converted_dm_targets.insert(OwnedDirectUserIdentifier::from(dm_target));
|
||||
}
|
||||
|
||||
Box::new(BaseRoomInfo {
|
||||
avatar,
|
||||
beacons: BTreeMap::new(),
|
||||
canonical_alias,
|
||||
create,
|
||||
dm_targets,
|
||||
dm_targets: converted_dm_targets,
|
||||
encryption,
|
||||
guest_access,
|
||||
history_visibility,
|
||||
|
||||
@@ -138,6 +138,13 @@ where
|
||||
L: Hash + Eq + ?Sized,
|
||||
{
|
||||
let position = self.mapping.remove(key)?;
|
||||
|
||||
// Reindex every mapped entry that is after the position we're looking to
|
||||
// remove.
|
||||
for mapped_pos in self.mapping.values_mut().filter(|pos| **pos > position) {
|
||||
*mapped_pos = mapped_pos.saturating_sub(1);
|
||||
}
|
||||
|
||||
Some(self.values.remove(position))
|
||||
}
|
||||
}
|
||||
@@ -195,6 +202,12 @@ mod tests {
|
||||
assert_eq!(map.get(&'a'), Some(&'E'));
|
||||
assert_eq!(map.get(&'b'), Some(&'f'));
|
||||
assert_eq!(map.get(&'c'), Some(&'G'));
|
||||
|
||||
// remove non-last item
|
||||
assert_eq!(map.remove(&'b'), Some('f'));
|
||||
|
||||
// get_or_create item after the removed one
|
||||
assert_eq!(map.get_or_create(&'c', || 'G'), &'G');
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -208,20 +221,26 @@ mod tests {
|
||||
// new items
|
||||
map.insert('a', 'e');
|
||||
map.insert('b', 'f');
|
||||
map.insert('c', 'g');
|
||||
|
||||
assert_eq!(map.get(&'a'), Some(&'e'));
|
||||
assert_eq!(map.get(&'b'), Some(&'f'));
|
||||
assert!(map.get(&'c').is_none());
|
||||
assert_eq!(map.get(&'c'), Some(&'g'));
|
||||
assert!(map.get(&'d').is_none());
|
||||
|
||||
// remove one item
|
||||
assert_eq!(map.remove(&'b'), Some('f'));
|
||||
// remove last item
|
||||
assert_eq!(map.remove(&'c'), Some('g'));
|
||||
|
||||
assert_eq!(map.get(&'a'), Some(&'e'));
|
||||
assert_eq!(map.get(&'b'), None);
|
||||
assert_eq!(map.get(&'b'), Some(&'f'));
|
||||
assert_eq!(map.get(&'c'), None);
|
||||
|
||||
// remove a non-existent item
|
||||
assert_eq!(map.remove(&'c'), None);
|
||||
|
||||
// remove a non-last item
|
||||
assert_eq!(map.remove(&'a'), Some('e'));
|
||||
assert_eq!(map.get(&'b'), Some(&'f'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -248,9 +248,15 @@ pub struct FinishUploadThumbnailInfo {
|
||||
/// Transaction id for the thumbnail upload.
|
||||
pub txn: OwnedTransactionId,
|
||||
/// Thumbnail's width.
|
||||
pub width: UInt,
|
||||
///
|
||||
/// Used previously, kept for backwards compatibility.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub width: Option<UInt>,
|
||||
/// Thumbnail's height.
|
||||
pub height: UInt,
|
||||
///
|
||||
/// Used previously, kept for backwards compatibility.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub height: Option<UInt>,
|
||||
}
|
||||
|
||||
/// A transaction id identifying a [`DependentQueuedRequest`] rather than its
|
||||
@@ -367,6 +373,27 @@ pub struct DependentQueuedRequest {
|
||||
pub parent_key: Option<SentRequestKey>,
|
||||
}
|
||||
|
||||
impl DependentQueuedRequest {
|
||||
/// Does the dependent request represent a new event that is *not*
|
||||
/// aggregated, aka it is going to be its own item in a timeline?
|
||||
pub fn is_own_event(&self) -> bool {
|
||||
match self.kind {
|
||||
DependentQueuedRequestKind::EditEvent { .. }
|
||||
| DependentQueuedRequestKind::RedactEvent
|
||||
| DependentQueuedRequestKind::ReactEvent { .. }
|
||||
| DependentQueuedRequestKind::UploadFileWithThumbnail { .. } => {
|
||||
// These are all aggregated events, or non-visible items (file upload producing
|
||||
// a new MXC ID).
|
||||
false
|
||||
}
|
||||
DependentQueuedRequestKind::FinishUpload { .. } => {
|
||||
// This one graduates into a new media event.
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for QueuedRequest {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
|
||||
@@ -424,21 +424,31 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
content: DependentQueuedRequestKind,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Update a set of dependent send queue requests with a key identifying the
|
||||
/// homeserver's response, effectively marking them as ready.
|
||||
/// Mark a set of dependent send queue requests as ready, using a key
|
||||
/// identifying the homeserver's response.
|
||||
///
|
||||
/// ⚠ 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(
|
||||
async fn mark_dependent_queued_requests_as_ready(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
parent_txn_id: &TransactionId,
|
||||
sent_parent_key: SentRequestKey,
|
||||
) -> Result<usize, Self::Error>;
|
||||
|
||||
/// Update a dependent send queue request with the new content.
|
||||
///
|
||||
/// Returns true if the request was found and could be updated.
|
||||
async fn update_dependent_queued_request(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
own_transaction_id: &ChildTransactionId,
|
||||
new_content: DependentQueuedRequestKind,
|
||||
) -> Result<bool, Self::Error>;
|
||||
|
||||
/// Remove a specific dependent send queue request by id.
|
||||
///
|
||||
/// Returns true if the dependent send queue request has been indeed
|
||||
@@ -709,14 +719,14 @@ impl<T: StateStore> StateStore for EraseStateStoreError<T> {
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn update_dependent_queued_request(
|
||||
async fn mark_dependent_queued_requests_as_ready(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
parent_txn_id: &TransactionId,
|
||||
sent_parent_key: SentRequestKey,
|
||||
) -> Result<usize, Self::Error> {
|
||||
self.0
|
||||
.update_dependent_queued_request(room_id, parent_txn_id, sent_parent_key)
|
||||
.mark_dependent_queued_requests_as_ready(room_id, parent_txn_id, sent_parent_key)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
@@ -735,6 +745,18 @@ impl<T: StateStore> StateStore for EraseStateStoreError<T> {
|
||||
) -> Result<Vec<DependentQueuedRequest>, Self::Error> {
|
||||
self.0.load_dependent_queued_requests(room_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn update_dependent_queued_request(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
own_transaction_id: &ChildTransactionId,
|
||||
new_content: DependentQueuedRequestKind,
|
||||
) -> Result<bool, Self::Error> {
|
||||
self.0
|
||||
.update_dependent_queued_request(room_id, own_transaction_id, new_content)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience functionality for state stores.
|
||||
@@ -1000,6 +1022,9 @@ pub enum StateStoreDataValue {
|
||||
///
|
||||
/// [`ComposerDraft`]: Self::ComposerDraft
|
||||
ComposerDraft(ComposerDraft),
|
||||
|
||||
/// A list of knock request ids marked as seen in a room.
|
||||
SeenKnockRequests(BTreeMap<OwnedEventId, OwnedUserId>),
|
||||
}
|
||||
|
||||
/// Current draft of the composer for the room.
|
||||
@@ -1066,6 +1091,11 @@ impl StateStoreDataValue {
|
||||
pub fn into_server_capabilities(self) -> Option<ServerCapabilities> {
|
||||
as_variant!(self, Self::ServerCapabilities)
|
||||
}
|
||||
|
||||
/// Get this value if it is the data for the ignored join requests.
|
||||
pub fn into_seen_knock_requests(self) -> Option<BTreeMap<OwnedEventId, OwnedUserId>> {
|
||||
as_variant!(self, Self::SeenKnockRequests)
|
||||
}
|
||||
}
|
||||
|
||||
/// A key for key-value data.
|
||||
@@ -1095,6 +1125,9 @@ pub enum StateStoreDataKey<'a> {
|
||||
///
|
||||
/// [`ComposerDraft`]: Self::ComposerDraft
|
||||
ComposerDraft(&'a RoomId),
|
||||
|
||||
/// A list of knock request ids marked as seen in a room.
|
||||
SeenKnockRequests(&'a RoomId),
|
||||
}
|
||||
|
||||
impl StateStoreDataKey<'_> {
|
||||
@@ -1120,6 +1153,10 @@ impl StateStoreDataKey<'_> {
|
||||
/// Key prefix to use for the [`ComposerDraft`][Self::ComposerDraft]
|
||||
/// variant.
|
||||
pub const COMPOSER_DRAFT: &'static str = "composer_draft";
|
||||
|
||||
/// Key prefix to use for the
|
||||
/// [`SeenKnockRequests`][Self::SeenKnockRequests] variant.
|
||||
pub const SEEN_KNOCK_REQUESTS: &'static str = "seen_knock_requests";
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -248,7 +248,7 @@ impl Timeline {
|
||||
struct DebugInvitedRoomUpdates<'a>(&'a BTreeMap<OwnedRoomId, InvitedRoomUpdate>);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugInvitedRoomUpdates<'a> {
|
||||
impl fmt::Debug for DebugInvitedRoomUpdates<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_map().entries(self.0.iter().map(|(k, v)| (k, DebugInvitedRoom(v)))).finish()
|
||||
}
|
||||
@@ -257,7 +257,7 @@ 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> {
|
||||
impl fmt::Debug for DebugKnockedRoomUpdates<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_map().entries(self.0.iter().map(|(k, v)| (k, DebugKnockedRoom(v)))).finish()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,32 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
<!-- next-header -->
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
## [0.9.0] - 2024-12-18
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Change the behavior of `LinkedChunk::new_with_update_history()` to emit an
|
||||
`Update::NewItemsChunk` when a new, initial empty, chunk is created.
|
||||
([#4327](https://github.com/matrix-org/matrix-rust-sdk/pull/4321))
|
||||
|
||||
- [**breaking**] Make `Room::history_visibility()` return an Option, and
|
||||
introduce `Room::history_visibility_or_default()` to return a better
|
||||
sensible default, according to the spec.
|
||||
([#4325](https://github.com/matrix-org/matrix-rust-sdk/pull/4325))
|
||||
|
||||
- Clear the internal state of the `AsVector` struct if an `Update::Clear`
|
||||
state has been received.
|
||||
([#4321](https://github.com/matrix-org/matrix-rust-sdk/pull/4321))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Document that a decrypted raw event always has a room id.
|
||||
([#728e1fd](https://github.com/matrix-org/matrix-rust-sdk/commit/728e1fda2ae9f1bfa87df162aa553040be705223))
|
||||
|
||||
## [0.8.0] - 2024-11-19
|
||||
|
||||
### Refactor
|
||||
|
||||
@@ -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.8.0"
|
||||
version = "0.9.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
default-target = "x86_64-unknown-linux-gnu"
|
||||
@@ -36,19 +36,25 @@ uniffi = { workspace = true, optional = true }
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
futures-util = { workspace = true, features = ["channel"] }
|
||||
wasm-bindgen-futures = { version = "0.4.33", optional = true }
|
||||
gloo-timers = { version = "0.3.0", features = ["futures"] }
|
||||
web-sys = { version = "0.3.60", features = ["console"] }
|
||||
gloo-timers = { workspace = true, features = ["futures"] }
|
||||
web-sys = { workspace = true, features = ["console"] }
|
||||
tracing-subscriber = { workspace = true, features = ["fmt", "ansi"] }
|
||||
wasm-bindgen = "0.2.84"
|
||||
wasm-bindgen = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = { workspace = true }
|
||||
proptest = { version = "1.4.0", default-features = false, features = ["std"] }
|
||||
matrix-sdk-test = { workspace = true }
|
||||
wasm-bindgen-test = "0.3.33"
|
||||
proptest = { workspace = true }
|
||||
matrix-sdk-test-macros = { path = "../../testing/matrix-sdk-test-macros" }
|
||||
wasm-bindgen-test = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
# Enable the test macro.
|
||||
tokio = { workspace = true, features = ["rt", "macros"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
js-sys = "0.3.64"
|
||||
# Enable the JS feature for getrandom.
|
||||
getrandom = { version = "0.2.6", default-features = false, features = ["js"] }
|
||||
js-sys = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -17,7 +17,10 @@ use std::{collections::BTreeMap, fmt};
|
||||
use ruma::{
|
||||
events::{AnyMessageLikeEvent, AnySyncTimelineEvent, AnyTimelineEvent},
|
||||
push::Action,
|
||||
serde::{JsonObject, Raw},
|
||||
serde::{
|
||||
AsRefStr, AsStrAsRefStr, DebugAsRefStr, DeserializeFromCowStr, FromString, JsonObject, Raw,
|
||||
SerializeAsRefStr,
|
||||
},
|
||||
DeviceKeyAlgorithm, OwnedDeviceId, OwnedEventId, OwnedUserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -176,6 +179,7 @@ pub enum VerificationLevel {
|
||||
|
||||
/// The message was sent by a user identity we have not verified, but the
|
||||
/// user was previously verified.
|
||||
#[serde(alias = "PreviouslyVerified")]
|
||||
VerificationViolation,
|
||||
|
||||
/// The message was sent by a device not linked to (signed by) any user
|
||||
@@ -259,6 +263,7 @@ pub enum ShieldStateCode {
|
||||
/// An unencrypted event in an encrypted room.
|
||||
SentInClear,
|
||||
/// The sender was previously verified but changed their identity.
|
||||
#[serde(alias = "PreviouslyVerified")]
|
||||
VerificationViolation,
|
||||
}
|
||||
|
||||
@@ -587,6 +592,11 @@ impl fmt::Debug for TimelineEventKind {
|
||||
/// A successfully-decrypted encrypted event.
|
||||
pub struct DecryptedRoomEvent {
|
||||
/// The decrypted event.
|
||||
///
|
||||
/// Note: it's not an error that this contains an `AnyMessageLikeEvent`: an
|
||||
/// encrypted payload *always contains* a room id, by the [spec].
|
||||
///
|
||||
/// [spec]: https://spec.matrix.org/v1.12/client-server-api/#mmegolmv1aes-sha2
|
||||
pub event: Raw<AnyMessageLikeEvent>,
|
||||
|
||||
/// The encryption info about the event.
|
||||
@@ -661,7 +671,7 @@ pub struct UnableToDecryptInfo {
|
||||
pub session_id: Option<String>,
|
||||
|
||||
/// Reason code for the decryption failure
|
||||
#[serde(default = "unknown_utd_reason")]
|
||||
#[serde(default = "unknown_utd_reason", deserialize_with = "deserialize_utd_reason")]
|
||||
pub reason: UnableToDecryptReason,
|
||||
}
|
||||
|
||||
@@ -669,6 +679,24 @@ fn unknown_utd_reason() -> UnableToDecryptReason {
|
||||
UnableToDecryptReason::Unknown
|
||||
}
|
||||
|
||||
/// Provides basic backward compatibility for deserializing older serialized
|
||||
/// `UnableToDecryptReason` values.
|
||||
pub fn deserialize_utd_reason<'de, D>(d: D) -> Result<UnableToDecryptReason, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
// Start by deserializing as to an untyped JSON value.
|
||||
let v: serde_json::Value = Deserialize::deserialize(d)?;
|
||||
// Backwards compatibility: `MissingMegolmSession` used to be stored without the
|
||||
// withheld code.
|
||||
if v.as_str().is_some_and(|s| s == "MissingMegolmSession") {
|
||||
return Ok(UnableToDecryptReason::MissingMegolmSession { withheld_code: None });
|
||||
}
|
||||
// Otherwise, use the derived deserialize impl to turn the JSON into a
|
||||
// UnableToDecryptReason
|
||||
serde_json::from_value::<UnableToDecryptReason>(v).map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
/// Reason code for a decryption failure
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum UnableToDecryptReason {
|
||||
@@ -684,9 +712,11 @@ pub enum UnableToDecryptReason {
|
||||
|
||||
/// Decryption failed because we're missing the megolm session that was used
|
||||
/// to encrypt the event.
|
||||
///
|
||||
/// TODO: support withheld codes?
|
||||
MissingMegolmSession,
|
||||
MissingMegolmSession {
|
||||
/// If the key was withheld on purpose, the associated code. `None`
|
||||
/// means no withheld code was received.
|
||||
withheld_code: Option<WithheldCode>,
|
||||
},
|
||||
|
||||
/// Decryption failed because, while we have the megolm session that was
|
||||
/// used to encrypt the message, it is ratcheted too far forward.
|
||||
@@ -718,7 +748,86 @@ 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)
|
||||
// In case of MissingMegolmSession with a withheld code we return false here
|
||||
// given that this API is used to decide if waiting a bit will help.
|
||||
matches!(
|
||||
self,
|
||||
Self::MissingMegolmSession { withheld_code: None } | Self::UnknownMegolmMessageIndex
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A machine-readable code for why a Megolm key was not sent.
|
||||
///
|
||||
/// Normally sent as the payload of an [`m.room_key.withheld`](https://spec.matrix.org/v1.12/client-server-api/#mroom_keywithheld) to-device message.
|
||||
#[derive(
|
||||
Clone,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
AsStrAsRefStr,
|
||||
AsRefStr,
|
||||
FromString,
|
||||
DebugAsRefStr,
|
||||
SerializeAsRefStr,
|
||||
DeserializeFromCowStr,
|
||||
)]
|
||||
pub enum WithheldCode {
|
||||
/// the user/device was blacklisted.
|
||||
#[ruma_enum(rename = "m.blacklisted")]
|
||||
Blacklisted,
|
||||
|
||||
/// the user/devices is unverified.
|
||||
#[ruma_enum(rename = "m.unverified")]
|
||||
Unverified,
|
||||
|
||||
/// The user/device is not allowed have the key. For example, this would
|
||||
/// usually be sent in response to a key request if the user was not in
|
||||
/// the room when the message was sent.
|
||||
#[ruma_enum(rename = "m.unauthorised")]
|
||||
Unauthorised,
|
||||
|
||||
/// Sent in reply to a key request if the device that the key is requested
|
||||
/// from does not have the requested key.
|
||||
#[ruma_enum(rename = "m.unavailable")]
|
||||
Unavailable,
|
||||
|
||||
/// An olm session could not be established.
|
||||
/// This may happen, for example, if the sender was unable to obtain a
|
||||
/// one-time key from the recipient.
|
||||
#[ruma_enum(rename = "m.no_olm")]
|
||||
NoOlm,
|
||||
|
||||
#[doc(hidden)]
|
||||
_Custom(PrivOwnedStr),
|
||||
}
|
||||
|
||||
impl fmt::Display for WithheldCode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
let string = match self {
|
||||
WithheldCode::Blacklisted => "The sender has blocked you.",
|
||||
WithheldCode::Unverified => "The sender has disabled encrypting to unverified devices.",
|
||||
WithheldCode::Unauthorised => "You are not authorised to read the message.",
|
||||
WithheldCode::Unavailable => "The requested key was not found.",
|
||||
WithheldCode::NoOlm => "Unable to establish a secure channel.",
|
||||
_ => self.as_str(),
|
||||
};
|
||||
|
||||
f.write_str(string)
|
||||
}
|
||||
}
|
||||
|
||||
// The Ruma macro expects the type to have this name.
|
||||
// The payload is counter intuitively made public in order to avoid having
|
||||
// multiple copies of this struct.
|
||||
#[doc(hidden)]
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct PrivOwnedStr(pub Box<str>);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for PrivOwnedStr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -812,9 +921,9 @@ mod tests {
|
||||
use super::{
|
||||
AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, TimelineEvent,
|
||||
TimelineEventKind, UnableToDecryptInfo, UnableToDecryptReason, UnsignedDecryptionResult,
|
||||
UnsignedEventLocation, VerificationState,
|
||||
UnsignedEventLocation, VerificationState, WithheldCode,
|
||||
};
|
||||
use crate::deserialized_responses::{DeviceLinkProblem, VerificationLevel};
|
||||
use crate::deserialized_responses::{DeviceLinkProblem, ShieldStateCode, VerificationLevel};
|
||||
|
||||
fn example_event() -> serde_json::Value {
|
||||
json!({
|
||||
@@ -889,6 +998,74 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verification_level_deserializes() {
|
||||
// Given a JSON VerificationLevel
|
||||
#[derive(Deserialize)]
|
||||
struct Container {
|
||||
verification_level: VerificationLevel,
|
||||
}
|
||||
let container = json!({ "verification_level": "VerificationViolation" });
|
||||
|
||||
// When we deserialize it
|
||||
let deserialized: Container = serde_json::from_value(container)
|
||||
.expect("We can deserialize the old PreviouslyVerified value");
|
||||
|
||||
// Then it is populated correctly
|
||||
assert_eq!(deserialized.verification_level, VerificationLevel::VerificationViolation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verification_level_deserializes_from_old_previously_verified_value() {
|
||||
// Given a JSON VerificationLevel with the old value PreviouslyVerified
|
||||
#[derive(Deserialize)]
|
||||
struct Container {
|
||||
verification_level: VerificationLevel,
|
||||
}
|
||||
let container = json!({ "verification_level": "PreviouslyVerified" });
|
||||
|
||||
// When we deserialize it
|
||||
let deserialized: Container = serde_json::from_value(container)
|
||||
.expect("We can deserialize the old PreviouslyVerified value");
|
||||
|
||||
// Then it is migrated to the new value
|
||||
assert_eq!(deserialized.verification_level, VerificationLevel::VerificationViolation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shield_state_code_deserializes() {
|
||||
// Given a JSON ShieldStateCode with value VerificationViolation
|
||||
#[derive(Deserialize)]
|
||||
struct Container {
|
||||
shield_state_code: ShieldStateCode,
|
||||
}
|
||||
let container = json!({ "shield_state_code": "VerificationViolation" });
|
||||
|
||||
// When we deserialize it
|
||||
let deserialized: Container = serde_json::from_value(container)
|
||||
.expect("We can deserialize the old PreviouslyVerified value");
|
||||
|
||||
// Then it is populated correctly
|
||||
assert_eq!(deserialized.shield_state_code, ShieldStateCode::VerificationViolation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shield_state_code_deserializes_from_old_previously_verified_value() {
|
||||
// Given a JSON ShieldStateCode with the old value PreviouslyVerified
|
||||
#[derive(Deserialize)]
|
||||
struct Container {
|
||||
shield_state_code: ShieldStateCode,
|
||||
}
|
||||
let container = json!({ "shield_state_code": "PreviouslyVerified" });
|
||||
|
||||
// When we deserialize it
|
||||
let deserialized: Container = serde_json::from_value(container)
|
||||
.expect("We can deserialize the old PreviouslyVerified value");
|
||||
|
||||
// Then it is migrated to the new value
|
||||
assert_eq!(deserialized.shield_state_code, ShieldStateCode::VerificationViolation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_timeline_event_serialisation() {
|
||||
let room_event = SyncTimelineEvent {
|
||||
@@ -1033,4 +1210,111 @@ mod tests {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_timeline_event_deserialisation_migration_for_withheld() {
|
||||
// Old serialized version was
|
||||
// "utd_info": {
|
||||
// "reason": "MissingMegolmSession",
|
||||
// "session_id": "session000"
|
||||
// }
|
||||
|
||||
// The new version would be
|
||||
// "utd_info": {
|
||||
// "reason": {
|
||||
// "MissingMegolmSession": {
|
||||
// "withheld_code": null
|
||||
// }
|
||||
// },
|
||||
// "session_id": "session000"
|
||||
// }
|
||||
|
||||
let serialized = json!({
|
||||
"kind": {
|
||||
"UnableToDecrypt": {
|
||||
"event": {
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "AwgAEoABzL1JYhqhjW9jXrlT3M6H8mJ4qffYtOQOnPuAPNxsuG20oiD/Fnpv6jnQGhU6YbV9pNM+1mRnTvxW3CbWOPjLKqCWTJTc7Q0vDEVtYePg38ncXNcwMmfhgnNAoW9S7vNs8C003x3yUl6NeZ8bH+ci870BZL+kWM/lMl10tn6U7snNmSjnE3ckvRdO+11/R4//5VzFQpZdf4j036lNSls/WIiI67Fk9iFpinz9xdRVWJFVdrAiPFwb8L5xRZ8aX+e2JDMlc1eW8gk",
|
||||
"device_id": "SKCGPNUWAU",
|
||||
"sender_key": "Gim/c7uQdSXyrrUbmUOrBT6sMC0gO7QSLmOK6B7NOm0",
|
||||
"session_id": "hgLyeSqXfb8vc5AjQLsg6TSHVu0HJ7HZ4B6jgMvxkrs"
|
||||
},
|
||||
"event_id": "$xxxxx:example.org",
|
||||
"origin_server_ts": 2189,
|
||||
"room_id": "!someroom:example.com",
|
||||
"sender": "@carl:example.com",
|
||||
"type": "m.room.message"
|
||||
},
|
||||
"utd_info": {
|
||||
"reason": "MissingMegolmSession",
|
||||
"session_id": "session000"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let result = serde_json::from_value(serialized);
|
||||
assert!(result.is_ok());
|
||||
|
||||
// should have migrated to the new format
|
||||
let event: SyncTimelineEvent = result.unwrap();
|
||||
assert_matches!(
|
||||
event.kind,
|
||||
TimelineEventKind::UnableToDecrypt { utd_info, .. }=> {
|
||||
assert_matches!(
|
||||
utd_info.reason,
|
||||
UnableToDecryptReason::MissingMegolmSession { withheld_code: None }
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unable_to_decrypt_info_migration_for_withheld() {
|
||||
let old_format = json!({
|
||||
"reason": "MissingMegolmSession",
|
||||
"session_id": "session000"
|
||||
});
|
||||
|
||||
let deserialized = serde_json::from_value::<UnableToDecryptInfo>(old_format).unwrap();
|
||||
let session_id = Some("session000".to_owned());
|
||||
|
||||
assert_eq!(deserialized.session_id, session_id);
|
||||
assert_eq!(
|
||||
deserialized.reason,
|
||||
UnableToDecryptReason::MissingMegolmSession { withheld_code: None },
|
||||
);
|
||||
|
||||
let new_format = json!({
|
||||
"session_id": "session000",
|
||||
"reason": {
|
||||
"MissingMegolmSession": {
|
||||
"withheld_code": null
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let deserialized = serde_json::from_value::<UnableToDecryptInfo>(new_format).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
deserialized.reason,
|
||||
UnableToDecryptReason::MissingMegolmSession { withheld_code: None },
|
||||
);
|
||||
assert_eq!(deserialized.session_id, session_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unable_to_decrypt_reason_is_missing_room_key() {
|
||||
let reason = UnableToDecryptReason::MissingMegolmSession { withheld_code: None };
|
||||
assert!(reason.is_missing_room_key());
|
||||
|
||||
let reason = UnableToDecryptReason::MissingMegolmSession {
|
||||
withheld_code: Some(WithheldCode::Blacklisted),
|
||||
};
|
||||
assert!(!reason.is_missing_room_key());
|
||||
|
||||
let reason = UnableToDecryptReason::UnknownMegolmMessageIndex;
|
||||
assert!(reason.is_missing_room_key());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ impl<T: 'static> Future for JoinHandle<T> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk_test::async_test;
|
||||
use matrix_sdk_test_macros::async_test;
|
||||
|
||||
use super::spawn;
|
||||
|
||||
|
||||
@@ -306,7 +306,7 @@ impl<'a> JsFieldVisitor<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> tracing::field::Visit for JsFieldVisitor<'a> {
|
||||
impl tracing::field::Visit for JsFieldVisitor<'_> {
|
||||
fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
|
||||
if self.result.is_err() {
|
||||
return;
|
||||
@@ -351,7 +351,7 @@ pub fn make_tracing_subscriber(logger: Option<JsLogger>) -> JsLoggingSubscriber
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use matrix_sdk_test::async_test;
|
||||
use matrix_sdk_test_macros::async_test;
|
||||
use tracing::{debug, subscriber::with_default};
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
|
||||
|
||||
@@ -302,6 +302,12 @@ impl UpdateToVectorDiff {
|
||||
self.chunks.insert(next_chunk_index, (*new, 0));
|
||||
}
|
||||
|
||||
// First chunk!
|
||||
(None, None) if self.chunks.is_empty() => {
|
||||
self.chunks.push_back((*new, 0));
|
||||
}
|
||||
|
||||
// Impossible state.
|
||||
(None, None) => {
|
||||
unreachable!(
|
||||
"Inserting new chunk with no previous nor next chunk identifiers \
|
||||
@@ -405,6 +411,14 @@ impl UpdateToVectorDiff {
|
||||
// Exiting the _detaching_ mode.
|
||||
detaching = false;
|
||||
}
|
||||
|
||||
Update::Clear => {
|
||||
// Clean `self.chunks`.
|
||||
self.chunks.clear();
|
||||
|
||||
// Let's straightforwardly emit a `VectorDiff::Clear`.
|
||||
diffs.push(VectorDiff::Clear);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,10 +464,11 @@ impl UpdateToVectorDiff {
|
||||
mod tests {
|
||||
use std::fmt::Debug;
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use imbl::{vector, Vector};
|
||||
|
||||
use super::{
|
||||
super::{EmptyChunk, LinkedChunk},
|
||||
super::{ChunkIdentifierGenerator, EmptyChunk, LinkedChunk},
|
||||
VectorDiff,
|
||||
};
|
||||
|
||||
@@ -473,6 +488,7 @@ mod tests {
|
||||
VectorDiff::Remove { index } => {
|
||||
accumulator.remove(index);
|
||||
}
|
||||
VectorDiff::Clear => accumulator.clear(),
|
||||
diff => unimplemented!("{diff:?}"),
|
||||
}
|
||||
}
|
||||
@@ -686,14 +702,72 @@ mod tests {
|
||||
&[VectorDiff::Insert { index: 14, value: 'z' }],
|
||||
);
|
||||
|
||||
drop(linked_chunk);
|
||||
assert!(as_vector.take().is_empty());
|
||||
|
||||
// Finally, ensure the “reconstitued” vector is the one expected.
|
||||
// Ensure the “reconstitued” vector is the one expected.
|
||||
assert_eq!(
|
||||
accumulator,
|
||||
vector!['m', 'a', 'w', 'x', 'y', 'b', 'd', 'i', 'j', 'k', 'l', 'e', 'f', 'g', 'z', 'h']
|
||||
);
|
||||
|
||||
// Let's try to clear the linked chunk now.
|
||||
linked_chunk.clear();
|
||||
|
||||
apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Clear]);
|
||||
assert!(accumulator.is_empty());
|
||||
|
||||
drop(linked_chunk);
|
||||
assert!(as_vector.take().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_as_vector_with_update_clear() {
|
||||
let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
|
||||
let mut as_vector = linked_chunk.as_vector().unwrap();
|
||||
|
||||
{
|
||||
// 1 initial chunk in the `UpdateToVectorDiff` mapper.
|
||||
let chunks = &as_vector.mapper.chunks;
|
||||
assert_eq!(chunks.len(), 1);
|
||||
assert_eq!(chunks[0].0, ChunkIdentifierGenerator::FIRST_IDENTIFIER);
|
||||
assert_eq!(chunks[0].1, 0);
|
||||
|
||||
assert!(as_vector.take().is_empty());
|
||||
}
|
||||
|
||||
linked_chunk.push_items_back(['a', 'b', 'c', 'd']);
|
||||
|
||||
{
|
||||
let diffs = as_vector.take();
|
||||
assert_eq!(diffs.len(), 2);
|
||||
assert_matches!(&diffs[0], VectorDiff::Append { .. });
|
||||
assert_matches!(&diffs[1], VectorDiff::Append { .. });
|
||||
|
||||
// 2 chunks in the `UpdateToVectorDiff` mapper.
|
||||
assert_eq!(as_vector.mapper.chunks.len(), 2);
|
||||
}
|
||||
|
||||
linked_chunk.clear();
|
||||
|
||||
{
|
||||
let diffs = as_vector.take();
|
||||
assert_eq!(diffs.len(), 1);
|
||||
assert_matches!(&diffs[0], VectorDiff::Clear);
|
||||
|
||||
// 1 chunk in the `UpdateToVectorDiff` mapper.
|
||||
let chunks = &as_vector.mapper.chunks;
|
||||
assert_eq!(chunks.len(), 1);
|
||||
assert_eq!(chunks[0].0, ChunkIdentifierGenerator::FIRST_IDENTIFIER);
|
||||
assert_eq!(chunks[0].1, 0);
|
||||
}
|
||||
|
||||
// And we can push again.
|
||||
linked_chunk.push_items_back(['a', 'b', 'c', 'd']);
|
||||
|
||||
{
|
||||
let diffs = as_vector.take();
|
||||
assert_eq!(diffs.len(), 2);
|
||||
assert_matches!(&diffs[0], VectorDiff::Append { .. });
|
||||
assert_matches!(&diffs[1], VectorDiff::Append { .. });
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,481 @@
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
marker::PhantomData,
|
||||
};
|
||||
|
||||
use tracing::error;
|
||||
|
||||
use super::{
|
||||
Chunk, ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, Ends, LinkedChunk,
|
||||
ObservableUpdates, RawChunk,
|
||||
};
|
||||
|
||||
/// A temporary chunk representation in the [`LinkedChunkBuilder`].
|
||||
///
|
||||
/// Instead of using linking the chunks with pointers, this uses
|
||||
/// [`ChunkIdentifier`] as the temporary links to the previous and next chunks,
|
||||
/// which will get resolved later when re-building the full data structure. This
|
||||
/// allows using chunks that references other chunks that aren't known yet.
|
||||
struct TemporaryChunk<Item, Gap> {
|
||||
id: ChunkIdentifier,
|
||||
previous: Option<ChunkIdentifier>,
|
||||
next: Option<ChunkIdentifier>,
|
||||
content: ChunkContent<Item, Gap>,
|
||||
}
|
||||
|
||||
/// A data structure to rebuild a linked chunk from its raw representation.
|
||||
///
|
||||
/// A linked chunk can be rebuilt incrementally from its internal
|
||||
/// representation, with the chunks being added *in any order*, as long as they
|
||||
/// form a single connected component eventually (viz., there's no
|
||||
/// subgraphs/sublists isolated from the one final linked list). If they don't,
|
||||
/// then the final call to [`LinkedChunkBuilder::build()`] will result in an
|
||||
/// error).
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct LinkedChunkBuilder<const CAP: usize, Item, Gap> {
|
||||
/// Work-in-progress chunks.
|
||||
chunks: BTreeMap<ChunkIdentifier, TemporaryChunk<Item, Gap>>,
|
||||
|
||||
/// Is the final `LinkedChunk` expected to include an update history, as if
|
||||
/// it were created with [`LinkedChunk::new_with_update_history`]?
|
||||
build_with_update_history: bool,
|
||||
}
|
||||
|
||||
impl<const CAP: usize, Item, Gap> Default for LinkedChunkBuilder<CAP, Item, Gap> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<const CAP: usize, Item, Gap> LinkedChunkBuilder<CAP, Item, Gap> {
|
||||
/// Create an empty [`LinkedChunkBuilder`] with no update history.
|
||||
pub fn new() -> Self {
|
||||
Self { chunks: Default::default(), build_with_update_history: false }
|
||||
}
|
||||
|
||||
/// Stash a gap chunk with its content.
|
||||
///
|
||||
/// This can be called even if the previous and next chunks have not been
|
||||
/// added yet. Resolving these chunks will happen at the time of calling
|
||||
/// [`LinkedChunkBuilder::build()`].
|
||||
pub fn push_gap(
|
||||
&mut self,
|
||||
previous: Option<ChunkIdentifier>,
|
||||
id: ChunkIdentifier,
|
||||
next: Option<ChunkIdentifier>,
|
||||
content: Gap,
|
||||
) {
|
||||
let chunk = TemporaryChunk { id, previous, next, content: ChunkContent::Gap(content) };
|
||||
self.chunks.insert(id, chunk);
|
||||
}
|
||||
|
||||
/// Stash an item chunk with its contents.
|
||||
///
|
||||
/// This can be called even if the previous and next chunks have not been
|
||||
/// added yet. Resolving these chunks will happen at the time of calling
|
||||
/// [`LinkedChunkBuilder::build()`].
|
||||
pub fn push_items(
|
||||
&mut self,
|
||||
previous: Option<ChunkIdentifier>,
|
||||
id: ChunkIdentifier,
|
||||
next: Option<ChunkIdentifier>,
|
||||
items: impl IntoIterator<Item = Item>,
|
||||
) {
|
||||
let chunk = TemporaryChunk {
|
||||
id,
|
||||
previous,
|
||||
next,
|
||||
content: ChunkContent::Items(items.into_iter().collect()),
|
||||
};
|
||||
self.chunks.insert(id, chunk);
|
||||
}
|
||||
|
||||
/// Request that the resulting linked chunk will have an update history, as
|
||||
/// if it were created with [`LinkedChunk::new_with_update_history`].
|
||||
pub fn with_update_history(&mut self) {
|
||||
self.build_with_update_history = true;
|
||||
}
|
||||
|
||||
/// Run all error checks before reconstructing the full linked chunk.
|
||||
///
|
||||
/// Must be called after checking `self.chunks` isn't empty in
|
||||
/// [`Self::build`].
|
||||
///
|
||||
/// Returns the identifier of the first chunk.
|
||||
fn check_consistency(&mut self) -> Result<ChunkIdentifier, LinkedChunkBuilderError> {
|
||||
// Look for the first id.
|
||||
let first_id =
|
||||
self.chunks.iter().find_map(|(id, chunk)| chunk.previous.is_none().then_some(*id));
|
||||
|
||||
// There's no first chunk, but we've checked that `self.chunks` isn't empty:
|
||||
// it's a malformed list.
|
||||
let Some(first_id) = first_id else {
|
||||
return Err(LinkedChunkBuilderError::MissingFirstChunk);
|
||||
};
|
||||
|
||||
// We're going to iterate from the first to the last chunk.
|
||||
// Keep track of chunks we've already visited.
|
||||
let mut visited = HashSet::new();
|
||||
|
||||
// Start from the first chunk.
|
||||
let mut maybe_cur = Some(first_id);
|
||||
|
||||
while let Some(cur) = maybe_cur {
|
||||
// The chunk must be referenced in `self.chunks`.
|
||||
let Some(chunk) = self.chunks.get(&cur) else {
|
||||
return Err(LinkedChunkBuilderError::MissingChunk { id: cur });
|
||||
};
|
||||
|
||||
if let ChunkContent::Items(items) = &chunk.content {
|
||||
if items.len() > CAP {
|
||||
return Err(LinkedChunkBuilderError::ChunkTooLarge { id: cur });
|
||||
}
|
||||
}
|
||||
|
||||
// If it's not the first chunk,
|
||||
if cur != first_id {
|
||||
// It must have a previous link.
|
||||
let Some(prev) = chunk.previous else {
|
||||
return Err(LinkedChunkBuilderError::MultipleFirstChunks {
|
||||
first_candidate: first_id,
|
||||
second_candidate: cur,
|
||||
});
|
||||
};
|
||||
|
||||
// And we must have visited its predecessor at this point, since we've
|
||||
// iterated from the first chunk.
|
||||
if !visited.contains(&prev) {
|
||||
return Err(LinkedChunkBuilderError::MissingChunk { id: prev });
|
||||
}
|
||||
}
|
||||
|
||||
// Add the current chunk to the list of seen chunks.
|
||||
if !visited.insert(cur) {
|
||||
// If we didn't insert, then it was already visited: there's a cycle!
|
||||
return Err(LinkedChunkBuilderError::Cycle { repeated: cur });
|
||||
}
|
||||
|
||||
// Move on to the next chunk. If it's none, we'll quit the loop.
|
||||
maybe_cur = chunk.next;
|
||||
}
|
||||
|
||||
// If there are more chunks than those we've visited: some of them were not
|
||||
// linked to the "main" branch of the linked list, so we had multiple connected
|
||||
// components.
|
||||
if visited.len() != self.chunks.len() {
|
||||
return Err(LinkedChunkBuilderError::MultipleConnectedComponents);
|
||||
}
|
||||
|
||||
Ok(first_id)
|
||||
}
|
||||
|
||||
pub fn build(mut self) -> Result<Option<LinkedChunk<CAP, Item, Gap>>, LinkedChunkBuilderError> {
|
||||
if self.chunks.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Run checks.
|
||||
let first_id = self.check_consistency()?;
|
||||
|
||||
// We're now going to iterate from the first to the last chunk. As we're doing
|
||||
// this, we're also doing a few other things:
|
||||
//
|
||||
// - rebuilding the final `Chunk`s one by one, that will be linked using
|
||||
// pointers,
|
||||
// - counting items from the item chunks we'll encounter,
|
||||
// - finding the max `ChunkIdentifier` (`max_chunk_id`).
|
||||
|
||||
let mut max_chunk_id = first_id.index();
|
||||
|
||||
// Small helper to graduate a temporary chunk into a final one. As we're doing
|
||||
// this, we're also updating the maximum chunk id (that will be used to
|
||||
// set up the id generator), and the number of items in this chunk.
|
||||
|
||||
let mut graduate_chunk = |id: ChunkIdentifier| {
|
||||
let temp = self.chunks.remove(&id)?;
|
||||
|
||||
// Update the maximum chunk identifier, while we're around.
|
||||
max_chunk_id = max_chunk_id.max(id.index());
|
||||
|
||||
// Graduate the current temporary chunk into a final chunk.
|
||||
let chunk_ptr = Chunk::new_leaked(id, temp.content);
|
||||
|
||||
Some((temp.next, chunk_ptr))
|
||||
};
|
||||
|
||||
let Some((mut next_chunk_id, first_chunk_ptr)) = graduate_chunk(first_id) else {
|
||||
// Can't really happen, but oh well.
|
||||
return Err(LinkedChunkBuilderError::MissingFirstChunk);
|
||||
};
|
||||
|
||||
let mut prev_chunk_ptr = first_chunk_ptr;
|
||||
|
||||
while let Some(id) = next_chunk_id {
|
||||
let Some((new_next, mut chunk_ptr)) = graduate_chunk(id) else {
|
||||
// Can't really happen, but oh well.
|
||||
return Err(LinkedChunkBuilderError::MissingChunk { id });
|
||||
};
|
||||
|
||||
let chunk = unsafe { chunk_ptr.as_mut() };
|
||||
|
||||
// Link the current chunk to its previous one.
|
||||
let prev_chunk = unsafe { prev_chunk_ptr.as_mut() };
|
||||
prev_chunk.next = Some(chunk_ptr);
|
||||
chunk.previous = Some(prev_chunk_ptr);
|
||||
|
||||
// Prepare for the next iteration.
|
||||
prev_chunk_ptr = chunk_ptr;
|
||||
next_chunk_id = new_next;
|
||||
}
|
||||
|
||||
debug_assert!(self.chunks.is_empty());
|
||||
|
||||
// Maintain the convention that `Ends::last` may be unset.
|
||||
let last_chunk_ptr = prev_chunk_ptr;
|
||||
let last_chunk_ptr =
|
||||
if first_chunk_ptr == last_chunk_ptr { None } else { Some(last_chunk_ptr) };
|
||||
let links = Ends { first: first_chunk_ptr, last: last_chunk_ptr };
|
||||
|
||||
let chunk_identifier_generator =
|
||||
ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier::new(
|
||||
max_chunk_id,
|
||||
));
|
||||
|
||||
let updates =
|
||||
if self.build_with_update_history { Some(ObservableUpdates::new()) } else { None };
|
||||
|
||||
Ok(Some(LinkedChunk { links, chunk_identifier_generator, updates, marker: PhantomData }))
|
||||
}
|
||||
|
||||
/// Fills a linked chunk builder from all the given raw parts.
|
||||
pub fn from_raw_parts(raws: Vec<RawChunk<Item, Gap>>) -> Self {
|
||||
let mut this = Self::new();
|
||||
for raw in raws {
|
||||
match raw.content {
|
||||
ChunkContent::Gap(gap) => {
|
||||
this.push_gap(raw.previous, raw.identifier, raw.next, gap);
|
||||
}
|
||||
ChunkContent::Items(vec) => {
|
||||
this.push_items(raw.previous, raw.identifier, raw.next, vec);
|
||||
}
|
||||
}
|
||||
}
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum LinkedChunkBuilderError {
|
||||
#[error("chunk with id {} is too large", id.index())]
|
||||
ChunkTooLarge { id: ChunkIdentifier },
|
||||
|
||||
#[error("there's no first chunk")]
|
||||
MissingFirstChunk,
|
||||
|
||||
#[error("there are multiple first chunks")]
|
||||
MultipleFirstChunks { first_candidate: ChunkIdentifier, second_candidate: ChunkIdentifier },
|
||||
|
||||
#[error("unable to resolve chunk with id {}", id.index())]
|
||||
MissingChunk { id: ChunkIdentifier },
|
||||
|
||||
#[error("rebuilt chunks form a cycle: repeated identifier: {}", repeated.index())]
|
||||
Cycle { repeated: ChunkIdentifier },
|
||||
|
||||
#[error("multiple connected components")]
|
||||
MultipleConnectedComponents,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
use super::LinkedChunkBuilder;
|
||||
use crate::linked_chunk::{ChunkIdentifier, LinkedChunkBuilderError};
|
||||
|
||||
#[test]
|
||||
fn test_empty() {
|
||||
let lcb = LinkedChunkBuilder::<3, char, char>::new();
|
||||
|
||||
// Building an empty linked chunk works, and returns `None`.
|
||||
let lc = lcb.build().unwrap();
|
||||
assert!(lc.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_success() {
|
||||
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
|
||||
|
||||
let cid0 = ChunkIdentifier::new(0);
|
||||
let cid1 = ChunkIdentifier::new(1);
|
||||
// Note: cid2 is missing on purpose, to confirm that it's fine to have holes in
|
||||
// the chunk id space.
|
||||
let cid3 = ChunkIdentifier::new(3);
|
||||
|
||||
// Check that we can successfully create a linked chunk, independently of the
|
||||
// order in which chunks are added.
|
||||
//
|
||||
// The final chunk will contain [cid0 <-> cid1 <-> cid3], in this order.
|
||||
|
||||
// Adding chunk cid0.
|
||||
lcb.push_items(None, cid0, Some(cid1), vec!['a', 'b', 'c']);
|
||||
// Adding chunk cid3.
|
||||
lcb.push_items(Some(cid1), cid3, None, vec!['d', 'e']);
|
||||
// Adding chunk cid1.
|
||||
lcb.push_gap(Some(cid0), cid1, Some(cid3), 'g');
|
||||
|
||||
let mut lc =
|
||||
lcb.build().expect("building works").expect("returns a non-empty linked chunk");
|
||||
|
||||
// Check the entire content first.
|
||||
assert_items_eq!(lc, ['a', 'b', 'c'] [-] ['d', 'e']);
|
||||
|
||||
// Run checks on the first chunk.
|
||||
let mut chunks = lc.chunks();
|
||||
let first_chunk = chunks.next().unwrap();
|
||||
{
|
||||
assert!(first_chunk.previous().is_none());
|
||||
assert_eq!(first_chunk.identifier(), cid0);
|
||||
}
|
||||
|
||||
// Run checks on the second chunk.
|
||||
let second_chunk = chunks.next().unwrap();
|
||||
{
|
||||
assert_eq!(second_chunk.identifier(), first_chunk.next().unwrap().identifier());
|
||||
assert_eq!(second_chunk.previous().unwrap().identifier(), first_chunk.identifier());
|
||||
assert_eq!(second_chunk.identifier(), cid1);
|
||||
}
|
||||
|
||||
// Run checks on the third chunk.
|
||||
let third_chunk = chunks.next().unwrap();
|
||||
{
|
||||
assert_eq!(third_chunk.identifier(), second_chunk.next().unwrap().identifier());
|
||||
assert_eq!(third_chunk.previous().unwrap().identifier(), second_chunk.identifier());
|
||||
assert!(third_chunk.next().is_none());
|
||||
assert_eq!(third_chunk.identifier(), cid3);
|
||||
}
|
||||
|
||||
// There's no more chunk.
|
||||
assert!(chunks.next().is_none());
|
||||
|
||||
// The linked chunk had 5 items.
|
||||
assert_eq!(lc.num_items(), 5);
|
||||
|
||||
// Now, if we add a new chunk, its identifier should be the previous one we used
|
||||
// + 1.
|
||||
lc.push_gap_back('h');
|
||||
|
||||
let last_chunk = lc.chunks().last().unwrap();
|
||||
assert_eq!(last_chunk.identifier(), ChunkIdentifier::new(cid3.index() + 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chunk_too_large() {
|
||||
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
|
||||
|
||||
let cid0 = ChunkIdentifier::new(0);
|
||||
|
||||
// Adding a chunk with 4 items will fail, because the max capacity specified in
|
||||
// the builder generics is 3.
|
||||
lcb.push_items(None, cid0, None, vec!['a', 'b', 'c', 'd']);
|
||||
|
||||
let res = lcb.build();
|
||||
assert_matches!(res, Err(LinkedChunkBuilderError::ChunkTooLarge { id }) => {
|
||||
assert_eq!(id, cid0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_first_chunk() {
|
||||
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
|
||||
|
||||
let cid0 = ChunkIdentifier::new(0);
|
||||
let cid1 = ChunkIdentifier::new(1);
|
||||
let cid2 = ChunkIdentifier::new(2);
|
||||
|
||||
lcb.push_gap(Some(cid2), cid0, Some(cid1), 'g');
|
||||
lcb.push_items(Some(cid0), cid1, Some(cid2), ['a', 'b', 'c']);
|
||||
lcb.push_items(Some(cid1), cid2, Some(cid0), ['d', 'e', 'f']);
|
||||
|
||||
let res = lcb.build();
|
||||
assert_matches!(res, Err(LinkedChunkBuilderError::MissingFirstChunk));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_first_chunks() {
|
||||
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
|
||||
|
||||
let cid0 = ChunkIdentifier::new(0);
|
||||
let cid1 = ChunkIdentifier::new(1);
|
||||
|
||||
lcb.push_gap(None, cid0, Some(cid1), 'g');
|
||||
// Second chunk lies and pretends to be the first too.
|
||||
lcb.push_items(None, cid1, Some(cid0), ['a', 'b', 'c']);
|
||||
|
||||
let res = lcb.build();
|
||||
assert_matches!(res, Err(LinkedChunkBuilderError::MultipleFirstChunks { first_candidate, second_candidate }) => {
|
||||
assert_eq!(first_candidate, cid0);
|
||||
assert_eq!(second_candidate, cid1);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_chunk() {
|
||||
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
|
||||
|
||||
let cid0 = ChunkIdentifier::new(0);
|
||||
let cid1 = ChunkIdentifier::new(1);
|
||||
lcb.push_gap(None, cid0, Some(cid1), 'g');
|
||||
|
||||
let res = lcb.build();
|
||||
assert_matches!(res, Err(LinkedChunkBuilderError::MissingChunk { id }) => {
|
||||
assert_eq!(id, cid1);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cycle() {
|
||||
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
|
||||
|
||||
let cid0 = ChunkIdentifier::new(0);
|
||||
let cid1 = ChunkIdentifier::new(1);
|
||||
lcb.push_gap(None, cid0, Some(cid1), 'g');
|
||||
lcb.push_gap(Some(cid0), cid1, Some(cid0), 'g');
|
||||
|
||||
let res = lcb.build();
|
||||
assert_matches!(res, Err(LinkedChunkBuilderError::Cycle { repeated }) => {
|
||||
assert_eq!(repeated, cid0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_connected_components() {
|
||||
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
|
||||
|
||||
let cid0 = ChunkIdentifier::new(0);
|
||||
let cid1 = ChunkIdentifier::new(1);
|
||||
let cid2 = ChunkIdentifier::new(2);
|
||||
|
||||
// cid0 and cid1 are linked to each other.
|
||||
lcb.push_gap(None, cid0, Some(cid1), 'g');
|
||||
lcb.push_items(Some(cid0), cid1, None, ['a', 'b', 'c']);
|
||||
// cid2 stands on its own.
|
||||
lcb.push_items(None, cid2, None, ['d', 'e', 'f']);
|
||||
|
||||
let res = lcb.build();
|
||||
assert_matches!(res, Err(LinkedChunkBuilderError::MultipleConnectedComponents));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,898 @@
|
||||
// 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.
|
||||
|
||||
//! Implementation for a _relational linked chunk_, see
|
||||
//! [`RelationalLinkedChunk`].
|
||||
|
||||
use ruma::{OwnedRoomId, RoomId};
|
||||
|
||||
use super::{ChunkContent, RawChunk};
|
||||
use crate::linked_chunk::{ChunkIdentifier, Position, Update};
|
||||
|
||||
/// A row of the [`RelationalLinkedChunk::chunks`].
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct ChunkRow {
|
||||
room_id: OwnedRoomId,
|
||||
previous_chunk: Option<ChunkIdentifier>,
|
||||
chunk: ChunkIdentifier,
|
||||
next_chunk: Option<ChunkIdentifier>,
|
||||
}
|
||||
|
||||
/// A row of the [`RelationalLinkedChunk::items`].
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct ItemRow<Item, Gap> {
|
||||
room_id: OwnedRoomId,
|
||||
position: Position,
|
||||
item: Either<Item, Gap>,
|
||||
}
|
||||
|
||||
/// Kind of item.
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Either<Item, Gap> {
|
||||
/// The content is an item.
|
||||
Item(Item),
|
||||
|
||||
/// The content is a gap.
|
||||
Gap(Gap),
|
||||
}
|
||||
|
||||
/// A [`LinkedChunk`] but with a relational layout, similar to what we
|
||||
/// would have in a database.
|
||||
///
|
||||
/// This is used by memory stores. The idea is to have a data layout that is
|
||||
/// similar for memory stores and for relational database stores, to represent a
|
||||
/// [`LinkedChunk`].
|
||||
///
|
||||
/// This type is also designed to receive [`Update`]. Applying `Update`s
|
||||
/// directly on a [`LinkedChunk`] is not ideal and particularly not trivial as
|
||||
/// the `Update`s do _not_ match the internal data layout of the `LinkedChunk`,
|
||||
/// they have been designed for storages, like a relational database for
|
||||
/// example.
|
||||
///
|
||||
/// This type is not as performant as [`LinkedChunk`] (in terms of memory
|
||||
/// layout, CPU caches etc.). It is only designed to be used in memory stores,
|
||||
/// which are mostly used for test purposes or light usage of the SDK.
|
||||
///
|
||||
/// [`LinkedChunk`]: super::LinkedChunk
|
||||
#[derive(Debug)]
|
||||
pub struct RelationalLinkedChunk<Item, Gap> {
|
||||
/// Chunks.
|
||||
chunks: Vec<ChunkRow>,
|
||||
|
||||
/// Items.
|
||||
items: Vec<ItemRow<Item, Gap>>,
|
||||
}
|
||||
|
||||
impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
|
||||
/// Create a new relational linked chunk.
|
||||
pub fn new() -> Self {
|
||||
Self { chunks: Vec::new(), items: Vec::new() }
|
||||
}
|
||||
|
||||
/// Removes all the chunks and items from this relational linked chunk.
|
||||
pub fn clear(&mut self) {
|
||||
self.chunks.clear();
|
||||
self.items.clear();
|
||||
}
|
||||
|
||||
/// Apply [`Update`]s. That's the only way to write data inside this
|
||||
/// relational linked chunk.
|
||||
pub fn apply_updates(&mut self, room_id: &RoomId, updates: Vec<Update<Item, Gap>>) {
|
||||
for update in updates {
|
||||
match update {
|
||||
Update::NewItemsChunk { previous, new, next } => {
|
||||
insert_chunk(&mut self.chunks, room_id, previous, new, next);
|
||||
}
|
||||
|
||||
Update::NewGapChunk { previous, new, next, gap } => {
|
||||
insert_chunk(&mut self.chunks, room_id, previous, new, next);
|
||||
self.items.push(ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(new, 0),
|
||||
item: Either::Gap(gap),
|
||||
});
|
||||
}
|
||||
|
||||
Update::RemoveChunk(chunk_identifier) => {
|
||||
remove_chunk(&mut self.chunks, room_id, chunk_identifier);
|
||||
|
||||
let indices_to_remove = self
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(
|
||||
|(nth, ItemRow { room_id: room_id_candidate, position, .. })| {
|
||||
(room_id == room_id_candidate
|
||||
&& position.chunk_identifier() == chunk_identifier)
|
||||
.then_some(nth)
|
||||
},
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for index_to_remove in indices_to_remove.into_iter().rev() {
|
||||
self.items.remove(index_to_remove);
|
||||
}
|
||||
}
|
||||
|
||||
Update::PushItems { mut at, items } => {
|
||||
for item in items {
|
||||
self.items.push(ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: at,
|
||||
item: Either::Item(item),
|
||||
});
|
||||
at.increment_index();
|
||||
}
|
||||
}
|
||||
|
||||
Update::RemoveItem { at } => {
|
||||
let mut entry_to_remove = None;
|
||||
|
||||
for (nth, ItemRow { room_id: room_id_candidate, position, .. }) in
|
||||
self.items.iter_mut().enumerate()
|
||||
{
|
||||
// Filter by room ID.
|
||||
if room_id != room_id_candidate {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the item to remove.
|
||||
if *position == at {
|
||||
debug_assert!(entry_to_remove.is_none(), "Found the same entry twice");
|
||||
|
||||
entry_to_remove = Some(nth);
|
||||
}
|
||||
|
||||
// Update all items that come _after_ `at` to shift their index.
|
||||
if position.chunk_identifier() == at.chunk_identifier()
|
||||
&& position.index() > at.index()
|
||||
{
|
||||
position.decrement_index();
|
||||
}
|
||||
}
|
||||
|
||||
self.items.remove(entry_to_remove.expect("Remove an unknown item"));
|
||||
}
|
||||
|
||||
Update::DetachLastItems { at } => {
|
||||
let indices_to_remove = self
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(
|
||||
|(nth, ItemRow { room_id: room_id_candidate, position, .. })| {
|
||||
(room_id == room_id_candidate
|
||||
&& position.chunk_identifier() == at.chunk_identifier()
|
||||
&& position.index() >= at.index())
|
||||
.then_some(nth)
|
||||
},
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for index_to_remove in indices_to_remove.into_iter().rev() {
|
||||
self.items.remove(index_to_remove);
|
||||
}
|
||||
}
|
||||
|
||||
Update::StartReattachItems | Update::EndReattachItems => { /* nothing */ }
|
||||
|
||||
Update::Clear => {
|
||||
self.chunks.clear();
|
||||
self.items.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_chunk(
|
||||
chunks: &mut Vec<ChunkRow>,
|
||||
room_id: &RoomId,
|
||||
previous: Option<ChunkIdentifier>,
|
||||
new: ChunkIdentifier,
|
||||
next: Option<ChunkIdentifier>,
|
||||
) {
|
||||
// Find the previous chunk, and update its next chunk.
|
||||
if let Some(previous) = previous {
|
||||
let entry_for_previous_chunk = chunks
|
||||
.iter_mut()
|
||||
.find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| {
|
||||
room_id == room_id_candidate && *chunk == previous
|
||||
})
|
||||
.expect("Previous chunk should be present");
|
||||
|
||||
// Link the chunk.
|
||||
entry_for_previous_chunk.next_chunk = Some(new);
|
||||
}
|
||||
|
||||
// Find the next chunk, and update its previous chunk.
|
||||
if let Some(next) = next {
|
||||
let entry_for_next_chunk = chunks
|
||||
.iter_mut()
|
||||
.find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| {
|
||||
room_id == room_id_candidate && *chunk == next
|
||||
})
|
||||
.expect("Next chunk should be present");
|
||||
|
||||
// Link the chunk.
|
||||
entry_for_next_chunk.previous_chunk = Some(new);
|
||||
}
|
||||
|
||||
// Insert the chunk.
|
||||
chunks.push(ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: previous,
|
||||
chunk: new,
|
||||
next_chunk: next,
|
||||
});
|
||||
}
|
||||
|
||||
fn remove_chunk(
|
||||
chunks: &mut Vec<ChunkRow>,
|
||||
room_id: &RoomId,
|
||||
chunk_to_remove: ChunkIdentifier,
|
||||
) {
|
||||
let entry_nth_to_remove = chunks
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(nth, ChunkRow { room_id: room_id_candidate, chunk, .. })| {
|
||||
(room_id == room_id_candidate && *chunk == chunk_to_remove).then_some(nth)
|
||||
})
|
||||
.expect("Remove an unknown chunk");
|
||||
|
||||
let ChunkRow { room_id, previous_chunk: previous, next_chunk: next, .. } =
|
||||
chunks.remove(entry_nth_to_remove);
|
||||
|
||||
// Find the previous chunk, and update its next chunk.
|
||||
if let Some(previous) = previous {
|
||||
let entry_for_previous_chunk = chunks
|
||||
.iter_mut()
|
||||
.find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| {
|
||||
&room_id == room_id_candidate && *chunk == previous
|
||||
})
|
||||
.expect("Previous chunk should be present");
|
||||
|
||||
// Insert the chunk.
|
||||
entry_for_previous_chunk.next_chunk = next;
|
||||
}
|
||||
|
||||
// Find the next chunk, and update its previous chunk.
|
||||
if let Some(next) = next {
|
||||
let entry_for_next_chunk = chunks
|
||||
.iter_mut()
|
||||
.find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| {
|
||||
&room_id == room_id_candidate && *chunk == next
|
||||
})
|
||||
.expect("Next chunk should be present");
|
||||
|
||||
// Insert the chunk.
|
||||
entry_for_next_chunk.previous_chunk = previous;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Item, Gap> RelationalLinkedChunk<Item, Gap>
|
||||
where
|
||||
Gap: Clone,
|
||||
Item: Clone,
|
||||
{
|
||||
/// Reloads the chunks.
|
||||
///
|
||||
/// Return an error result if the data was malformed in the struct, with a
|
||||
/// string message explaining details about the error.
|
||||
pub fn reload_chunks(&self, room_id: &RoomId) -> Result<Vec<RawChunk<Item, Gap>>, String> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
for chunk_row in self.chunks.iter().filter(|chunk| chunk.room_id == room_id) {
|
||||
// Find all items that correspond to the chunk.
|
||||
let mut items = self
|
||||
.items
|
||||
.iter()
|
||||
.filter(|row| {
|
||||
row.room_id == room_id && row.position.chunk_identifier() == chunk_row.chunk
|
||||
})
|
||||
.peekable();
|
||||
|
||||
// Look at the first chunk item type, to reconstruct the chunk at hand.
|
||||
let Some(first) = items.peek() else {
|
||||
// The only possibility is that we created an empty items chunk; mark it as
|
||||
// such, and continue.
|
||||
result.push(RawChunk {
|
||||
content: ChunkContent::Items(Vec::new()),
|
||||
previous: chunk_row.previous_chunk,
|
||||
identifier: chunk_row.chunk,
|
||||
next: chunk_row.next_chunk,
|
||||
});
|
||||
continue;
|
||||
};
|
||||
|
||||
match &first.item {
|
||||
Either::Item(_) => {
|
||||
// Collect all the related items.
|
||||
let mut collected_items = Vec::new();
|
||||
for row in items {
|
||||
match &row.item {
|
||||
Either::Item(item) => {
|
||||
collected_items.push((item.clone(), row.position.index()))
|
||||
}
|
||||
Either::Gap(_) => {
|
||||
return Err(format!(
|
||||
"unexpected gap in items chunk {}",
|
||||
chunk_row.chunk.index()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort them by their position.
|
||||
collected_items.sort_unstable_by_key(|(_item, index)| *index);
|
||||
|
||||
result.push(RawChunk {
|
||||
content: ChunkContent::Items(
|
||||
collected_items.into_iter().map(|(item, _index)| item).collect(),
|
||||
),
|
||||
previous: chunk_row.previous_chunk,
|
||||
identifier: chunk_row.chunk,
|
||||
next: chunk_row.next_chunk,
|
||||
});
|
||||
}
|
||||
|
||||
Either::Gap(gap) => {
|
||||
assert!(items.next().is_some(), "we just peeked the gap");
|
||||
|
||||
// We shouldn't have more than one item row for this chunk.
|
||||
if items.next().is_some() {
|
||||
return Err(format!(
|
||||
"there shouldn't be more than one item row attached in gap chunk {}",
|
||||
chunk_row.chunk.index()
|
||||
));
|
||||
}
|
||||
|
||||
result.push(RawChunk {
|
||||
content: ChunkContent::Gap(gap.clone()),
|
||||
previous: chunk_row.previous_chunk,
|
||||
identifier: chunk_row.chunk,
|
||||
next: chunk_row.next_chunk,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Item, Gap> Default for RelationalLinkedChunk<Item, Gap> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruma::room_id;
|
||||
|
||||
use super::{ChunkIdentifier as CId, *};
|
||||
use crate::linked_chunk::LinkedChunkBuilder;
|
||||
|
||||
#[test]
|
||||
fn test_new_items_chunk() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
vec![
|
||||
// 0
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// 1 after 0
|
||||
Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
|
||||
// 2 before 0
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(2), next: Some(CId::new(0)) },
|
||||
// 3 between 2 and 0
|
||||
Update::NewItemsChunk {
|
||||
previous: Some(CId::new(2)),
|
||||
new: CId::new(3),
|
||||
next: Some(CId::new(0)),
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
// Chunks are correctly linked.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.chunks,
|
||||
&[
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: Some(CId::new(3)),
|
||||
chunk: CId::new(0),
|
||||
next_chunk: Some(CId::new(1))
|
||||
},
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: Some(CId::new(0)),
|
||||
chunk: CId::new(1),
|
||||
next_chunk: None
|
||||
},
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: None,
|
||||
chunk: CId::new(2),
|
||||
next_chunk: Some(CId::new(3))
|
||||
},
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: Some(CId::new(2)),
|
||||
chunk: CId::new(3),
|
||||
next_chunk: Some(CId::new(0))
|
||||
},
|
||||
],
|
||||
);
|
||||
// Items have not been modified.
|
||||
assert!(relational_linked_chunk.items.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_gap_chunk() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
vec![
|
||||
// 0
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// 1 after 0
|
||||
Update::NewGapChunk {
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: None,
|
||||
gap: (),
|
||||
},
|
||||
// 2 after 1
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
],
|
||||
);
|
||||
|
||||
// Chunks are correctly linked.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.chunks,
|
||||
&[
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: None,
|
||||
chunk: CId::new(0),
|
||||
next_chunk: Some(CId::new(1))
|
||||
},
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: Some(CId::new(0)),
|
||||
chunk: CId::new(1),
|
||||
next_chunk: Some(CId::new(2))
|
||||
},
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: Some(CId::new(1)),
|
||||
chunk: CId::new(2),
|
||||
next_chunk: None
|
||||
},
|
||||
],
|
||||
);
|
||||
// Items contains the gap.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.items,
|
||||
&[ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(1), 0),
|
||||
item: Either::Gap(())
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_chunk() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
vec![
|
||||
// 0
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// 1 after 0
|
||||
Update::NewGapChunk {
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: None,
|
||||
gap: (),
|
||||
},
|
||||
// 2 after 1
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
// remove 1
|
||||
Update::RemoveChunk(CId::new(1)),
|
||||
],
|
||||
);
|
||||
|
||||
// Chunks are correctly linked.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.chunks,
|
||||
&[
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: None,
|
||||
chunk: CId::new(0),
|
||||
next_chunk: Some(CId::new(2))
|
||||
},
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: Some(CId::new(0)),
|
||||
chunk: CId::new(2),
|
||||
next_chunk: None
|
||||
},
|
||||
],
|
||||
);
|
||||
// Items no longer contains the gap.
|
||||
assert!(relational_linked_chunk.items.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_items() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
vec![
|
||||
// new chunk (this is not mandatory for this test, but let's try to be realistic)
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] },
|
||||
// new chunk (to test new items are pushed in the correct chunk)
|
||||
Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
|
||||
// new items on 1
|
||||
Update::PushItems { at: Position::new(CId::new(1), 0), items: vec!['x', 'y', 'z'] },
|
||||
// new items on 0 again
|
||||
Update::PushItems { at: Position::new(CId::new(0), 3), items: vec!['d', 'e'] },
|
||||
],
|
||||
);
|
||||
|
||||
// Chunks are correctly linked.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.chunks,
|
||||
&[
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: None,
|
||||
chunk: CId::new(0),
|
||||
next_chunk: Some(CId::new(1))
|
||||
},
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: Some(CId::new(0)),
|
||||
chunk: CId::new(1),
|
||||
next_chunk: None
|
||||
},
|
||||
],
|
||||
);
|
||||
// Items contains the pushed items.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.items,
|
||||
&[
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 0),
|
||||
item: Either::Item('a')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 1),
|
||||
item: Either::Item('b')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 2),
|
||||
item: Either::Item('c')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(1), 0),
|
||||
item: Either::Item('x')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(1), 1),
|
||||
item: Either::Item('y')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(1), 2),
|
||||
item: Either::Item('z')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 3),
|
||||
item: Either::Item('d')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 4),
|
||||
item: Either::Item('e')
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_item() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
vec![
|
||||
// new chunk (this is not mandatory for this test, but let's try to be realistic)
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec!['a', 'b', 'c', 'd', 'e'],
|
||||
},
|
||||
// remove an item: 'a'
|
||||
Update::RemoveItem { at: Position::new(CId::new(0), 0) },
|
||||
// remove an item: 'd'
|
||||
Update::RemoveItem { at: Position::new(CId::new(0), 2) },
|
||||
],
|
||||
);
|
||||
|
||||
// Chunks are correctly linked.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.chunks,
|
||||
&[ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: None,
|
||||
chunk: CId::new(0),
|
||||
next_chunk: None
|
||||
}],
|
||||
);
|
||||
// Items contains the pushed items.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.items,
|
||||
&[
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 0),
|
||||
item: Either::Item('b')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 1),
|
||||
item: Either::Item('c')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 2),
|
||||
item: Either::Item('e')
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detach_last_items() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
vec![
|
||||
// new chunk
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// new chunk
|
||||
Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec!['a', 'b', 'c', 'd', 'e'],
|
||||
},
|
||||
// new items on 1
|
||||
Update::PushItems { at: Position::new(CId::new(1), 0), items: vec!['x', 'y', 'z'] },
|
||||
// detach last items on 0
|
||||
Update::DetachLastItems { at: Position::new(CId::new(0), 2) },
|
||||
],
|
||||
);
|
||||
|
||||
// Chunks are correctly linked.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.chunks,
|
||||
&[
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: None,
|
||||
chunk: CId::new(0),
|
||||
next_chunk: Some(CId::new(1))
|
||||
},
|
||||
ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: Some(CId::new(0)),
|
||||
chunk: CId::new(1),
|
||||
next_chunk: None
|
||||
},
|
||||
],
|
||||
);
|
||||
// Items contains the pushed items.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.items,
|
||||
&[
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 0),
|
||||
item: Either::Item('a')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 1),
|
||||
item: Either::Item('b')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(1), 0),
|
||||
item: Either::Item('x')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(1), 1),
|
||||
item: Either::Item('y')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(1), 2),
|
||||
item: Either::Item('z')
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_start_and_end_reattach_items() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
|
||||
relational_linked_chunk
|
||||
.apply_updates(room_id, vec![Update::StartReattachItems, Update::EndReattachItems]);
|
||||
|
||||
// Nothing happened.
|
||||
assert!(relational_linked_chunk.chunks.is_empty());
|
||||
assert!(relational_linked_chunk.items.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
vec![
|
||||
// new chunk (this is not mandatory for this test, but let's try to be realistic)
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] },
|
||||
],
|
||||
);
|
||||
|
||||
// Chunks are correctly linked.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.chunks,
|
||||
&[ChunkRow {
|
||||
room_id: room_id.to_owned(),
|
||||
previous_chunk: None,
|
||||
chunk: CId::new(0),
|
||||
next_chunk: None,
|
||||
}],
|
||||
);
|
||||
// Items contains the pushed items.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.items,
|
||||
&[
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 0),
|
||||
item: Either::Item('a')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 1),
|
||||
item: Either::Item('b')
|
||||
},
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(0), 2),
|
||||
item: Either::Item('c')
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
// Now, time for a clean up.
|
||||
relational_linked_chunk.apply_updates(room_id, vec![Update::Clear]);
|
||||
assert!(relational_linked_chunk.chunks.is_empty());
|
||||
assert!(relational_linked_chunk.items.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reload_empty_linked_chunk() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
|
||||
// When I reload the linked chunk components from an empty store,
|
||||
let relational_linked_chunk = RelationalLinkedChunk::<char, char>::new();
|
||||
let result = relational_linked_chunk.reload_chunks(room_id).unwrap();
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reload_linked_chunk_with_empty_items() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, char>::new();
|
||||
|
||||
// When I store an empty items chunks,
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
vec![Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }],
|
||||
);
|
||||
|
||||
// It correctly gets reloaded as such.
|
||||
let raws = relational_linked_chunk.reload_chunks(room_id).unwrap();
|
||||
let lc = LinkedChunkBuilder::<3, _, _>::from_raw_parts(raws)
|
||||
.build()
|
||||
.expect("building succeeds")
|
||||
.expect("this leads to a non-empty linked chunk");
|
||||
|
||||
assert_items_eq!(lc, []);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rebuild_linked_chunk() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, char>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
vec![
|
||||
// new chunk
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] },
|
||||
// a gap chunk
|
||||
Update::NewGapChunk {
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: None,
|
||||
gap: 'g',
|
||||
},
|
||||
// another items chunk
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems { at: Position::new(CId::new(2), 0), items: vec!['d', 'e', 'f'] },
|
||||
],
|
||||
);
|
||||
|
||||
let raws = relational_linked_chunk.reload_chunks(room_id).unwrap();
|
||||
let lc = LinkedChunkBuilder::<3, _, _>::from_raw_parts(raws)
|
||||
.build()
|
||||
.expect("building succeeds")
|
||||
.expect("this leads to a non-empty linked chunk");
|
||||
|
||||
// The linked chunk is correctly reloaded.
|
||||
assert_items_eq!(lc, ['a', 'b', 'c'] [-] ['d', 'e', 'f']);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,9 @@ use super::{ChunkIdentifier, Position};
|
||||
///
|
||||
/// These updates are useful to store a `LinkedChunk` in another form of
|
||||
/// storage, like a database or something similar.
|
||||
///
|
||||
/// [`LinkedChunk`]: super::LinkedChunk
|
||||
/// [`LinkedChunk::updates`]: super::LinkedChunk::updates
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Update<Item, Gap> {
|
||||
/// A new chunk of kind Items has been created.
|
||||
@@ -96,11 +99,17 @@ pub enum Update<Item, Gap> {
|
||||
|
||||
/// Reattaching items (see [`Self::StartReattachItems`]) is finished.
|
||||
EndReattachItems,
|
||||
|
||||
/// All chunks have been cleared, i.e. all items and all gaps have been
|
||||
/// dropped.
|
||||
Clear,
|
||||
}
|
||||
|
||||
/// A collection of [`Update`]s that can be observed.
|
||||
///
|
||||
/// Get a value for this type with [`LinkedChunk::updates`].
|
||||
///
|
||||
/// [`LinkedChunk::updates`]: super::LinkedChunk::updates
|
||||
#[derive(Debug)]
|
||||
pub struct ObservableUpdates<Item, Gap> {
|
||||
pub(super) inner: Arc<RwLock<UpdatesInner<Item, Gap>>>,
|
||||
@@ -120,7 +129,7 @@ impl<Item, Gap> ObservableUpdates<Item, Gap> {
|
||||
/// Take new updates.
|
||||
///
|
||||
/// Updates that have been taken will not be read again.
|
||||
pub(super) fn take(&mut self) -> Vec<Update<Item, Gap>>
|
||||
pub fn take(&mut self) -> Vec<Update<Item, Gap>>
|
||||
where
|
||||
Item: Clone,
|
||||
Gap: Clone,
|
||||
@@ -375,7 +384,21 @@ mod tests {
|
||||
other_token
|
||||
};
|
||||
|
||||
// There is no new update yet.
|
||||
// There is an initial update.
|
||||
{
|
||||
let updates = linked_chunk.updates().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
updates.take(),
|
||||
&[NewItemsChunk { previous: None, new: ChunkIdentifier(0), next: None }],
|
||||
);
|
||||
assert_eq!(
|
||||
updates.inner.write().unwrap().take_with_token(other_token),
|
||||
&[NewItemsChunk { previous: None, new: ChunkIdentifier(0), next: None }],
|
||||
);
|
||||
}
|
||||
|
||||
// No new update.
|
||||
{
|
||||
let updates = linked_chunk.updates().unwrap();
|
||||
|
||||
@@ -608,7 +631,16 @@ mod tests {
|
||||
let updates_subscriber = linked_chunk.updates().unwrap().subscribe();
|
||||
pin_mut!(updates_subscriber);
|
||||
|
||||
// No update, stream is pending.
|
||||
// Initial update, stream is ready.
|
||||
assert_matches!(
|
||||
updates_subscriber.as_mut().poll_next(&mut context),
|
||||
Poll::Ready(Some(items)) => {
|
||||
assert_eq!(
|
||||
items,
|
||||
&[NewItemsChunk { previous: None, new: ChunkIdentifier(0), next: None }]
|
||||
);
|
||||
}
|
||||
);
|
||||
assert_matches!(updates_subscriber.as_mut().poll_next(&mut context), Poll::Pending);
|
||||
assert_eq!(*counter_waker.number_of_wakeup.lock().unwrap(), 0);
|
||||
|
||||
@@ -642,6 +674,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
linked_chunk.updates().unwrap().take(),
|
||||
&[
|
||||
NewItemsChunk { previous: None, new: ChunkIdentifier(0), next: None },
|
||||
PushItems { at: Position(ChunkIdentifier(0), 0), items: vec!['a'] },
|
||||
PushItems { at: Position(ChunkIdentifier(0), 1), items: vec!['b'] },
|
||||
PushItems { at: Position(ChunkIdentifier(0), 2), items: vec!['c'] },
|
||||
@@ -692,9 +725,28 @@ mod tests {
|
||||
let updates_subscriber2 = linked_chunk.updates().unwrap().subscribe();
|
||||
pin_mut!(updates_subscriber2);
|
||||
|
||||
// No update, streams are pending.
|
||||
// Initial updates, streams are ready.
|
||||
assert_matches!(
|
||||
updates_subscriber1.as_mut().poll_next(&mut context1),
|
||||
Poll::Ready(Some(items)) => {
|
||||
assert_eq!(
|
||||
items,
|
||||
&[NewItemsChunk { previous: None, new: ChunkIdentifier(0), next: None }]
|
||||
);
|
||||
}
|
||||
);
|
||||
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::Ready(Some(items)) => {
|
||||
assert_eq!(
|
||||
items,
|
||||
&[NewItemsChunk { previous: None, new: ChunkIdentifier(0), next: None }]
|
||||
);
|
||||
}
|
||||
);
|
||||
assert_matches!(updates_subscriber2.as_mut().poll_next(&mut context2), Poll::Pending);
|
||||
assert_eq!(*counter_waker2.number_of_wakeup.lock().unwrap(), 0);
|
||||
|
||||
|
||||
@@ -343,7 +343,7 @@ mod tests {
|
||||
};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk_test::async_test;
|
||||
use matrix_sdk_test_macros::async_test;
|
||||
use tokio::{
|
||||
spawn,
|
||||
time::{sleep, Duration},
|
||||
@@ -361,7 +361,7 @@ mod tests {
|
||||
|
||||
impl TestStore {
|
||||
fn try_take_leased_lock(&self, lease_duration_ms: u32, key: &str, holder: &str) -> bool {
|
||||
try_take_leased_lock(&self.leases, lease_duration_ms, key, holder)
|
||||
try_take_leased_lock(&mut self.leases.write().unwrap(), lease_duration_ms, key, holder)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,12 +502,11 @@ mod tests {
|
||||
pub mod memory_store_helper {
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
sync::RwLock,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
pub fn try_take_leased_lock(
|
||||
leases: &RwLock<HashMap<String, (String, Instant)>>,
|
||||
leases: &mut HashMap<String, (String, Instant)>,
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
@@ -515,7 +514,7 @@ pub mod memory_store_helper {
|
||||
let now = Instant::now();
|
||||
let expiration = now + Duration::from_millis(lease_duration_ms.into());
|
||||
|
||||
match leases.write().unwrap().entry(key.to_owned()) {
|
||||
match leases.entry(key.to_owned()) {
|
||||
// There is an existing holder.
|
||||
Entry::Occupied(mut entry) => {
|
||||
let (current_holder, current_expiration) = entry.get_mut();
|
||||
|
||||
@@ -62,7 +62,7 @@ where
|
||||
pub(crate) mod tests {
|
||||
use std::{future, time::Duration};
|
||||
|
||||
use matrix_sdk_test::async_test;
|
||||
use matrix_sdk_test_macros::async_test;
|
||||
|
||||
use super::timeout;
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ macro_rules! timer {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[matrix_sdk_test::async_test]
|
||||
#[matrix_sdk_test_macros::async_test]
|
||||
async fn test_timer_name() {
|
||||
use tracing::{span, Level};
|
||||
|
||||
|
||||
@@ -2,6 +2,35 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
<!-- next-header -->
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
## [0.9.0] - 2024-12-18
|
||||
|
||||
- Expose new API `DehydratedDevices::get_dehydrated_device_pickle_key`, `DehydratedDevices::save_dehydrated_device_pickle_key`
|
||||
and `DehydratedDevices::delete_dehydrated_device_pickle_key` to store/load the dehydrated device pickle key.
|
||||
This allows client to automatically rotate the dehydrated device to avoid one-time-keys exhaustion and to_device accumulation.
|
||||
[**breaking**] `DehydratedDevices::keys_for_upload` and `DehydratedDevices::rehydrate` now use the `DehydratedDeviceKey`
|
||||
as parameter instead of a raw byte array. Use `DehydratedDeviceKey::from_bytes` to migrate.
|
||||
([#4383](https://github.com/matrix-org/matrix-rust-sdk/pull/4383))
|
||||
|
||||
- Add extra logging in `OtherUserIdentity::pin_current_master_key` and
|
||||
`OtherUserIdentity::withdraw_verification`.
|
||||
([#4415](https://github.com/matrix-org/matrix-rust-sdk/pull/4415))
|
||||
|
||||
- Added new `UtdCause` variants `WithheldForUnverifiedOrInsecureDevice` and `WithheldBySender`.
|
||||
These variants provide clearer categorization for expected Unable-To-Decrypt (UTD) errors
|
||||
when the sender either did not wish to share or was unable to share the room_key.
|
||||
([#4305](https://github.com/matrix-org/matrix-rust-sdk/pull/4305))
|
||||
|
||||
- `UtdCause` has two new variants that replace the existing `HistoricalMessage`:
|
||||
`HistoricalMessageAndBackupIsDisabled` and `HistoricalMessageAndDeviceIsUnverified`.
|
||||
These give more detail about what went wrong and allow us to suggest to users
|
||||
what actions they can take to fix the problem. See the doc comments on these
|
||||
variants for suggested wording.
|
||||
([#4384](https://github.com/matrix-org/matrix-rust-sdk/pull/4384))
|
||||
|
||||
## [0.8.0] - 2024-11-19
|
||||
|
||||
### Features
|
||||
@@ -66,6 +95,12 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Fix [#4424](https://github.com/matrix-org/matrix-rust-sdk/issues/4424) Failed
|
||||
storage upgrade for "PreviouslyVerifiedButNoLonger". This bug caused errors to
|
||||
occur when loading crypto information from storage, which typically prevented
|
||||
apps from starting correctly.
|
||||
([#4430](https://github.com/matrix-org/matrix-rust-sdk/pull/4430))
|
||||
|
||||
- Add new method `OlmMachine::try_decrypt_room_event`.
|
||||
([#4116](https://github.com/matrix-org/matrix-rust-sdk/pull/4116))
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ name = "matrix-sdk-crypto"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = { workspace = true }
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
@@ -23,6 +23,11 @@ experimental-algorithms = []
|
||||
uniffi = ["dep:uniffi"]
|
||||
_disable-minimum-rotation-period-ms = []
|
||||
|
||||
# Private feature, see
|
||||
# https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823 for the gory
|
||||
# details.
|
||||
test-send-sync = []
|
||||
|
||||
# "message-ids" feature doesn't do anything and is deprecated.
|
||||
message-ids = []
|
||||
|
||||
@@ -30,38 +35,39 @@ message-ids = []
|
||||
testing = ["matrix-sdk-test"]
|
||||
|
||||
[dependencies]
|
||||
aes = "0.8.1"
|
||||
aes = "0.8.4"
|
||||
aquamarine = { workspace = true }
|
||||
as_variant = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
bs58 = { version = "0.5.0" }
|
||||
bs58 = { version = "0.5.1" }
|
||||
byteorder = { workspace = true }
|
||||
cfg-if = "1.0"
|
||||
ctr = "0.9.1"
|
||||
ctr = "0.9.2"
|
||||
eyeball = { workspace = true }
|
||||
futures-core = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
hkdf = "0.12.3"
|
||||
hmac = "0.12.1"
|
||||
hkdf = { workspace = true }
|
||||
hmac = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
js_option = "0.1.1"
|
||||
matrix-sdk-qrcode = { workspace = true, optional = true }
|
||||
matrix-sdk-common = { workspace = true }
|
||||
matrix-sdk-test = { workspace = true, optional = true } # feature = testing only
|
||||
pbkdf2 = { version = "0.12.2", default-features = false }
|
||||
pbkdf2 = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
rmp-serde = "1.1.1"
|
||||
rmp-serde = { workspace = true }
|
||||
ruma = { workspace = true, features = ["rand", "canonical-json", "unstable-msc3814"] }
|
||||
serde = { workspace = true, features = ["derive", "rc"] }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
subtle = "2.5.0"
|
||||
time = { version = "0.3.34", features = ["formatting"] }
|
||||
subtle = "2.6.1"
|
||||
time = { version = "0.3.36", features = ["formatting"] }
|
||||
tokio-stream = { workspace = true, features = ["sync"] }
|
||||
tokio = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true, features = ["attributes"] }
|
||||
url = { workspace = true }
|
||||
ulid = { version = "1.0.0" }
|
||||
ulid = { version = "1.1.3" }
|
||||
uniffi = { workspace = true, optional = true }
|
||||
vodozemac = { workspace = true }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
@@ -78,10 +84,10 @@ assert_matches = { workspace = true }
|
||||
assert_matches2 = { workspace = true }
|
||||
futures-executor = { workspace = true }
|
||||
http = { workspace = true }
|
||||
indoc = "2.0.1"
|
||||
indoc = "2.0.5"
|
||||
matrix-sdk-test = { workspace = true }
|
||||
proptest = { version = "1.0.0", default-features = false, features = ["std"] }
|
||||
similar-asserts = "1.5.0"
|
||||
proptest = { workspace = true }
|
||||
similar-asserts = { workspace = true }
|
||||
# required for async_test macro
|
||||
stream_assert = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
A no-network-IO implementation of a state machine that handles E2EE for
|
||||
[Matrix] clients.
|
||||
|
||||
# Usage
|
||||
A no-network-IO implementation of a state machine that handles end-to-end
|
||||
encryption for [Matrix] clients.
|
||||
|
||||
If you're just trying to write a Matrix client or bot in Rust, you're probably
|
||||
looking for [matrix-sdk] instead.
|
||||
|
||||
However, if you're looking to add E2EE to an existing Matrix client or library,
|
||||
read on.
|
||||
However, if you're looking to add end-to-end encryption to an existing Matrix
|
||||
client or library, read on.
|
||||
|
||||
The state machine works in a push/pull manner:
|
||||
|
||||
@@ -52,28 +50,42 @@ async fn main() -> Result<(), OlmError> {
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
It is recommended to use the [tutorial] to understand how end-to-end encryption
|
||||
works in Matrix and how to add end-to-end encryption support in your Matrix
|
||||
client library.
|
||||
|
||||
[Matrix]: https://matrix.org/
|
||||
[matrix-sdk]: https://github.com/matrix-org/matrix-rust-sdk/
|
||||
|
||||
# Room key forwarding algorithm
|
||||
|
||||
The decision tree below visualizes the way this crate decides whether a message
|
||||
key ("room key") will be [forwarded][forwarded_room_key] to a requester upon a
|
||||
key request, provided the `automatic-room-key-forwarding` feature is enabled.
|
||||
Key forwarding is sometimes also referred to as key *gossiping*.
|
||||
|
||||
[forwarded_room_key]: <https://spec.matrix.org/v1.10/client-server-api/#mforwarded_room_key>
|
||||
|
||||

|
||||
|
||||
|
||||
# Crate Feature Flags
|
||||
|
||||
The following crate feature flags are available:
|
||||
|
||||
* `qrcode`: Enbles QRcode generation and reading code
|
||||
| Feature | Default | Description |
|
||||
| ------------------- | :-----: | -------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `qrcode` | No | Enables QR code based interactive verification |
|
||||
| `js` | No | Enables JavaScript API usage for things like the current system time on WASM (does nothing on other targets) |
|
||||
| `testing` | No | Provides facilities and functions for tests, in particular for integration testing store implementations. ATTENTION: do not ever use outside of tests, we do not provide any stability warantees on these, these are merely helpers. If you find you _need_ any function provided here outside of tests, please open a Github Issue and inform us about your use case for us to consider. |
|
||||
|
||||
* `testing`: Provides facilities and functions for tests, in particular for integration testing store implementations. ATTENTION: do not ever use outside of tests, we do not provide any stability warantees on these, these are merely helpers. If you find you _need_ any function provided here outside of tests, please open a Github Issue and inform us about your use case for us to consider.
|
||||
|
||||
* `_disable-minimum-rotation-period-ms`: Do not use except for testing. Disables the floor on the rotation period of room keys.
|
||||
# Enabling logging
|
||||
|
||||
Users of the `matrix-sdk-crypto` crate can enable log output by depending on the
|
||||
`tracing-subscriber` crate and including the following line in their
|
||||
application (e.g. at the start of `main`):
|
||||
|
||||
```no_compile
|
||||
tracing_subscriber::fmt::init();
|
||||
```
|
||||
|
||||
The log output is controlled via the `RUST_LOG` environment variable by
|
||||
setting it to one of the `error`, `warn`, `info`, `debug` or `trace` levels.
|
||||
The output is printed to stdout.
|
||||
|
||||
The `RUST_LOG` variable also supports a more advanced syntax for filtering
|
||||
log output more precisely, for instance with crate-level granularity. For
|
||||
more information on this, check out the [tracing-subscriber documentation].
|
||||
|
||||
[tracing-subscriber documentation]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/
|
||||
|
||||
@@ -38,8 +38,8 @@ use tracing::{debug, info, instrument, trace, warn};
|
||||
use crate::{
|
||||
olm::{BackedUpRoomKey, ExportedRoomKey, InboundGroupSession, SignedJsonObject},
|
||||
store::{BackupDecryptionKey, BackupKeys, Changes, RoomKeyCounts, Store},
|
||||
types::{MegolmV1AuthData, RoomKeyBackupInfo, Signatures},
|
||||
CryptoStoreError, Device, KeysBackupRequest, RoomKeyImportResult, SignatureError,
|
||||
types::{requests::KeysBackupRequest, MegolmV1AuthData, RoomKeyBackupInfo, Signatures},
|
||||
CryptoStoreError, Device, RoomKeyImportResult, SignatureError,
|
||||
};
|
||||
|
||||
mod keys;
|
||||
|
||||
@@ -57,7 +57,7 @@ use tracing::{instrument, trace};
|
||||
use vodozemac::LibolmPickleError;
|
||||
|
||||
use crate::{
|
||||
store::{CryptoStoreWrapper, MemoryStore, RoomKeyInfo, Store},
|
||||
store::{Changes, CryptoStoreWrapper, DehydratedDeviceKey, MemoryStore, RoomKeyInfo, Store},
|
||||
verification::VerificationMachine,
|
||||
Account, CryptoStoreError, EncryptionSyncChanges, OlmError, OlmMachine, SignatureError,
|
||||
};
|
||||
@@ -69,6 +69,10 @@ pub enum DehydrationError {
|
||||
#[error(transparent)]
|
||||
Pickle(#[from] LibolmPickleError),
|
||||
|
||||
/// The pickle key has an invalid length
|
||||
#[error("The pickle key has an invalid length, expected 32 bytes, got {0}")]
|
||||
PickleKeyLength(usize),
|
||||
|
||||
/// The dehydrated device could not be signed by our user identity,
|
||||
/// we're missing the self-signing key.
|
||||
#[error("The self-signing key is missing, can't create a dehydrated device")]
|
||||
@@ -132,15 +136,49 @@ impl DehydratedDevices {
|
||||
/// private keys of the device.
|
||||
pub async fn rehydrate(
|
||||
&self,
|
||||
pickle_key: &[u8; 32],
|
||||
pickle_key: &DehydratedDeviceKey,
|
||||
device_id: &DeviceId,
|
||||
device_data: Raw<DehydratedDeviceData>,
|
||||
) -> Result<RehydratedDevice, DehydrationError> {
|
||||
let pickle_key = expand_pickle_key(pickle_key, device_id);
|
||||
let pickle_key = expand_pickle_key(pickle_key.inner.as_ref(), device_id);
|
||||
let rehydrated = self.inner.rehydrate(&pickle_key, device_id, device_data).await?;
|
||||
|
||||
Ok(RehydratedDevice { rehydrated, original: self.inner.to_owned() })
|
||||
}
|
||||
|
||||
/// Get the cached dehydrated device pickle key if any.
|
||||
///
|
||||
/// None if the key was not previously cached (via
|
||||
/// [`DehydratedDevices::save_dehydrated_device_pickle_key`]).
|
||||
///
|
||||
/// Should be used to periodically rotate the dehydrated device to avoid
|
||||
/// one-time keys exhaustion and accumulation of to_device messages.
|
||||
pub async fn get_dehydrated_device_pickle_key(
|
||||
&self,
|
||||
) -> Result<Option<DehydratedDeviceKey>, DehydrationError> {
|
||||
Ok(self.inner.store().load_dehydrated_device_pickle_key().await?)
|
||||
}
|
||||
|
||||
/// Store the dehydrated device pickle key in the crypto store.
|
||||
///
|
||||
/// This is useful if the client wants to periodically rotate dehydrated
|
||||
/// devices to avoid one-time keys exhaustion and accumulated to_device
|
||||
/// problems.
|
||||
pub async fn save_dehydrated_device_pickle_key(
|
||||
&self,
|
||||
dehydrated_device_pickle_key: &DehydratedDeviceKey,
|
||||
) -> Result<(), DehydrationError> {
|
||||
let changes = Changes {
|
||||
dehydrated_device_pickle_key: Some(dehydrated_device_pickle_key.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
Ok(self.inner.store().save_changes(changes).await?)
|
||||
}
|
||||
|
||||
/// Deletes the previously stored dehydrated device pickle key.
|
||||
pub async fn delete_dehydrated_device_pickle_key(&self) -> Result<(), DehydrationError> {
|
||||
Ok(self.inner.store().delete_dehydrated_device_pickle_key().await?)
|
||||
}
|
||||
}
|
||||
|
||||
/// A rehydraded device.
|
||||
@@ -170,7 +208,7 @@ impl RehydratedDevice {
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use anyhow::Result;
|
||||
/// # use matrix_sdk_crypto::OlmMachine;
|
||||
/// # use matrix_sdk_crypto::{ OlmMachine, store::DehydratedDeviceKey };
|
||||
/// # use ruma::{api::client::dehydrated_device, DeviceId};
|
||||
/// # async fn example() -> Result<()> {
|
||||
/// # let machine: OlmMachine = unimplemented!();
|
||||
@@ -184,9 +222,9 @@ impl RehydratedDevice {
|
||||
/// ) -> Result<dehydrated_device::get_events::unstable::Response> {
|
||||
/// todo!("Download the to-device events of the dehydrated device");
|
||||
/// }
|
||||
///
|
||||
/// // Don't use a zero key for real.
|
||||
/// let pickle_key = [0u8; 32];
|
||||
/// // Get the cached dehydrated key (got it after verification/recovery)
|
||||
/// let pickle_key = machine
|
||||
/// .dehydrated_devices().get_dehydrated_device_pickle_key().await?.unwrap();
|
||||
///
|
||||
/// // Fetch the dehydrated device from the server.
|
||||
/// let response = get_dehydrated_device().await?;
|
||||
@@ -285,11 +323,13 @@ impl DehydratedDevice {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use matrix_sdk_crypto::OlmMachine;
|
||||
/// # async fn example() -> anyhow::Result<()> {
|
||||
/// # use matrix_sdk_crypto::OlmMachine; /// #
|
||||
/// use matrix_sdk_crypto::store::DehydratedDeviceKey;
|
||||
///
|
||||
/// async fn example() -> anyhow::Result<()> {
|
||||
/// # let machine: OlmMachine = unimplemented!();
|
||||
/// // Don't use a zero key for real.
|
||||
/// let pickle_key = [0u8; 32];
|
||||
/// // Create a new random key
|
||||
/// let pickle_key = DehydratedDeviceKey::new()?;
|
||||
///
|
||||
/// // Create the dehydrated device.
|
||||
/// let device = machine.dehydrated_devices().create().await?;
|
||||
@@ -299,6 +339,9 @@ impl DehydratedDevice {
|
||||
/// .keys_for_upload("Dehydrated device".to_owned(), &pickle_key)
|
||||
/// .await?;
|
||||
///
|
||||
/// // Save the key if you want to later one rotate the dehydrated device
|
||||
/// machine.dehydrated_devices().save_dehydrated_device_pickle_key(&pickle_key).await.unwrap();
|
||||
///
|
||||
/// // Send the request out using your HTTP client.
|
||||
/// // client.send(request).await?;
|
||||
/// # Ok(())
|
||||
@@ -314,7 +357,7 @@ impl DehydratedDevice {
|
||||
pub async fn keys_for_upload(
|
||||
&self,
|
||||
initial_device_display_name: String,
|
||||
pickle_key: &[u8; 32],
|
||||
pickle_key: &DehydratedDeviceKey,
|
||||
) -> Result<put_dehydrated_device::unstable::Request, DehydrationError> {
|
||||
let mut transaction = self.store.transaction().await;
|
||||
|
||||
@@ -330,7 +373,8 @@ impl DehydratedDevice {
|
||||
|
||||
trace!("Creating an upload request for a dehydrated device");
|
||||
|
||||
let pickle_key = expand_pickle_key(pickle_key, &self.store.static_account().device_id);
|
||||
let pickle_key =
|
||||
expand_pickle_key(pickle_key.inner.as_ref(), &self.store.static_account().device_id);
|
||||
let device_id = self.store.static_account().device_id.clone();
|
||||
let device_data = account.dehydrate(&pickle_key);
|
||||
let initial_device_display_name = Some(initial_device_display_name);
|
||||
@@ -393,12 +437,15 @@ mod tests {
|
||||
tests::to_device_requests_to_content,
|
||||
},
|
||||
olm::OutboundGroupSession,
|
||||
store::DehydratedDeviceKey,
|
||||
types::{events::ToDeviceEvent, DeviceKeys as DeviceKeysType},
|
||||
utilities::json_convert,
|
||||
EncryptionSettings, OlmMachine,
|
||||
};
|
||||
|
||||
const PICKLE_KEY: &[u8; 32] = &[0u8; 32];
|
||||
fn pickle_key() -> DehydratedDeviceKey {
|
||||
DehydratedDeviceKey::from_bytes(&[0u8; 32])
|
||||
}
|
||||
|
||||
fn user_id() -> &'static UserId {
|
||||
user_id!("@alice:localhost")
|
||||
@@ -467,7 +514,7 @@ mod tests {
|
||||
let dehydrated_device = olm_machine.dehydrated_devices().create().await.unwrap();
|
||||
|
||||
let request = dehydrated_device
|
||||
.keys_for_upload("Foo".to_owned(), PICKLE_KEY)
|
||||
.keys_for_upload("Foo".to_owned(), &pickle_key())
|
||||
.await
|
||||
.expect("We should be able to create a request to upload a dehydrated device");
|
||||
|
||||
@@ -497,7 +544,7 @@ mod tests {
|
||||
let dehydrated_device = alice.dehydrated_devices().create().await.unwrap();
|
||||
|
||||
let mut request = dehydrated_device
|
||||
.keys_for_upload("Foo".to_owned(), PICKLE_KEY)
|
||||
.keys_for_upload("Foo".to_owned(), &pickle_key())
|
||||
.await
|
||||
.expect("We should be able to create a request to upload a dehydrated device");
|
||||
|
||||
@@ -531,7 +578,7 @@ mod tests {
|
||||
// Rehydrate the device.
|
||||
let rehydrated = bob
|
||||
.dehydrated_devices()
|
||||
.rehydrate(PICKLE_KEY, &request.device_id, request.device_data)
|
||||
.rehydrate(&pickle_key(), &request.device_id, request.device_data)
|
||||
.await
|
||||
.expect("We should be able to rehydrate the device");
|
||||
|
||||
@@ -561,4 +608,43 @@ mod tests {
|
||||
"The session ids of the imported room key and the outbound group session should match"
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_dehydrated_device_pickle_key_cache() {
|
||||
let alice = get_olm_machine().await;
|
||||
|
||||
let dehydrated_manager = alice.dehydrated_devices();
|
||||
|
||||
let stored_key = dehydrated_manager.get_dehydrated_device_pickle_key().await.unwrap();
|
||||
assert!(stored_key.is_none());
|
||||
|
||||
let pickle_key = DehydratedDeviceKey::new().unwrap();
|
||||
|
||||
dehydrated_manager.save_dehydrated_device_pickle_key(&pickle_key).await.unwrap();
|
||||
|
||||
let stored_key =
|
||||
dehydrated_manager.get_dehydrated_device_pickle_key().await.unwrap().unwrap();
|
||||
assert_eq!(stored_key.to_base64(), pickle_key.to_base64());
|
||||
|
||||
let dehydrated_device = dehydrated_manager.create().await.unwrap();
|
||||
|
||||
let request = dehydrated_device
|
||||
.keys_for_upload("Foo".to_owned(), &stored_key)
|
||||
.await
|
||||
.expect("We should be able to create a request to upload a dehydrated device");
|
||||
|
||||
// Rehydrate the device.
|
||||
dehydrated_manager
|
||||
.rehydrate(&stored_key, &request.device_id, request.device_data)
|
||||
.await
|
||||
.expect("We should be able to rehydrate the device");
|
||||
|
||||
dehydrated_manager
|
||||
.delete_dehydrated_device_pickle_key()
|
||||
.await
|
||||
.expect("Should be able to delete the dehydrated device key");
|
||||
|
||||
let stored_key = dehydrated_manager.get_dehydrated_device_pickle_key().await.unwrap();
|
||||
assert!(stored_key.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::VerificationLevel;
|
||||
use matrix_sdk_common::deserialized_responses::{VerificationLevel, WithheldCode};
|
||||
use ruma::{CanonicalJsonError, IdParseError, OwnedDeviceId, OwnedRoomId, OwnedUserId};
|
||||
use serde::{ser::SerializeMap, Serializer};
|
||||
use serde_json::Error as SerdeError;
|
||||
@@ -22,10 +22,7 @@ use thiserror::Error;
|
||||
use vodozemac::{Curve25519PublicKey, Ed25519PublicKey};
|
||||
|
||||
use super::store::CryptoStoreError;
|
||||
use crate::{
|
||||
olm::SessionExportError,
|
||||
types::{events::room_key_withheld::WithheldCode, SignedKey},
|
||||
};
|
||||
use crate::{olm::SessionExportError, types::SignedKey};
|
||||
#[cfg(doc)]
|
||||
use crate::{CollectStrategy, Device, LocalTrust, OtherUserIdentity};
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ impl<'a, R: 'a + Read + std::fmt::Debug> std::fmt::Debug for AttachmentDecryptor
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, R: Read> Read for AttachmentDecryptor<'a, R> {
|
||||
impl<R: Read> Read for AttachmentDecryptor<'_, R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
let read_bytes = self.inner.read(buf)?;
|
||||
|
||||
|
||||
@@ -45,16 +45,18 @@ use crate::{
|
||||
error::{EventError, OlmError, OlmResult},
|
||||
identities::IdentityManager,
|
||||
olm::{InboundGroupSession, Session},
|
||||
requests::{OutgoingRequest, ToDeviceRequest},
|
||||
session_manager::GroupSessionCache,
|
||||
store::{Changes, CryptoStoreError, SecretImportError, Store, StoreCache},
|
||||
types::events::{
|
||||
forwarded_room_key::ForwardedRoomKeyContent,
|
||||
olm_v1::{DecryptedForwardedRoomKeyEvent, DecryptedSecretSendEvent},
|
||||
room::encrypted::EncryptedEvent,
|
||||
room_key_request::RoomKeyRequestEvent,
|
||||
secret_send::SecretSendContent,
|
||||
EventType,
|
||||
types::{
|
||||
events::{
|
||||
forwarded_room_key::ForwardedRoomKeyContent,
|
||||
olm_v1::{DecryptedForwardedRoomKeyEvent, DecryptedSecretSendEvent},
|
||||
room::encrypted::EncryptedEvent,
|
||||
room_key_request::RoomKeyRequestEvent,
|
||||
secret_send::SecretSendContent,
|
||||
EventType,
|
||||
},
|
||||
requests::{OutgoingRequest, ToDeviceRequest},
|
||||
},
|
||||
Device, MegolmError,
|
||||
};
|
||||
@@ -616,7 +618,6 @@ impl GossipMachine {
|
||||
/// i.
|
||||
/// - `Err(x)`: Should *refuse* to share the session. `x` is the reason for
|
||||
/// the refusal.
|
||||
|
||||
#[cfg(feature = "automatic-room-key-forwarding")]
|
||||
async fn should_share_key(
|
||||
&self,
|
||||
@@ -1116,6 +1117,7 @@ mod tests {
|
||||
use crate::{
|
||||
gossiping::KeyForwardDecision,
|
||||
olm::OutboundGroupSession,
|
||||
types::requests::AnyOutgoingRequest,
|
||||
types::{
|
||||
events::{
|
||||
forwarded_room_key::ForwardedRoomKeyContent, olm_v1::AnyDecryptedOlmEvent,
|
||||
@@ -1123,7 +1125,7 @@ mod tests {
|
||||
},
|
||||
EventEncryptionAlgorithm,
|
||||
},
|
||||
EncryptionSettings, OutgoingRequests,
|
||||
EncryptionSettings,
|
||||
};
|
||||
use crate::{
|
||||
identities::{DeviceData, IdentityManager, LocalTrust},
|
||||
@@ -1310,7 +1312,7 @@ mod tests {
|
||||
|
||||
fn extract_content<'a>(
|
||||
recipient: &UserId,
|
||||
request: &'a crate::OutgoingRequest,
|
||||
request: &'a crate::types::requests::OutgoingRequest,
|
||||
) -> &'a Raw<ruma::events::AnyToDeviceEventContent> {
|
||||
request
|
||||
.request()
|
||||
@@ -1343,7 +1345,7 @@ mod tests {
|
||||
fn request_to_event<C>(
|
||||
recipient: &UserId,
|
||||
sender: &UserId,
|
||||
request: &crate::OutgoingRequest,
|
||||
request: &crate::types::requests::OutgoingRequest,
|
||||
) -> crate::types::events::ToDeviceEvent<C>
|
||||
where
|
||||
C: crate::types::events::EventType
|
||||
@@ -2064,7 +2066,7 @@ mod tests {
|
||||
assert_eq!(bob_machine.outgoing_to_device_requests().await.unwrap().len(), 1);
|
||||
assert_matches!(
|
||||
bob_machine.outgoing_to_device_requests().await.unwrap()[0].request(),
|
||||
OutgoingRequests::KeysClaim(_)
|
||||
AnyOutgoingRequest::KeysClaim(_)
|
||||
);
|
||||
assert!(!bob_machine.inner.users_for_key_claim.read().unwrap().is_empty());
|
||||
assert!(!bob_machine.inner.wait_queue.is_empty());
|
||||
|
||||
@@ -36,10 +36,12 @@ use ruma::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
requests::{OutgoingRequest, ToDeviceRequest},
|
||||
types::events::{
|
||||
olm_v1::DecryptedSecretSendEvent,
|
||||
room_key_request::{RoomKeyRequestContent, RoomKeyRequestEvent, SupportedKeyInfo},
|
||||
types::{
|
||||
events::{
|
||||
olm_v1::DecryptedSecretSendEvent,
|
||||
room_key_request::{RoomKeyRequestContent, RoomKeyRequestEvent, SupportedKeyInfo},
|
||||
},
|
||||
requests::{OutgoingRequest, ToDeviceRequest},
|
||||
},
|
||||
Device,
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ use std::{
|
||||
},
|
||||
};
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::WithheldCode;
|
||||
use ruma::{
|
||||
api::client::keys::upload_signatures::v3::Request as SignatureUploadRequest,
|
||||
events::{key::verification::VerificationMethod, AnyToDeviceEventContent},
|
||||
@@ -48,13 +49,13 @@ use crate::{
|
||||
types::{
|
||||
events::{
|
||||
forwarded_room_key::ForwardedRoomKeyContent,
|
||||
room::encrypted::ToDeviceEncryptedEventContent, room_key_withheld::WithheldCode,
|
||||
EventType,
|
||||
room::encrypted::ToDeviceEncryptedEventContent, EventType,
|
||||
},
|
||||
requests::{OutgoingVerificationRequest, ToDeviceRequest},
|
||||
DeviceKey, DeviceKeys, EventEncryptionAlgorithm, Signatures, SignedKey,
|
||||
},
|
||||
verification::VerificationMachine,
|
||||
Account, OutgoingVerificationRequest, Sas, ToDeviceRequest, VerificationRequest,
|
||||
Account, Sas, VerificationRequest,
|
||||
};
|
||||
|
||||
pub enum MaybeEncryptedRoomKey {
|
||||
|
||||
@@ -33,12 +33,14 @@ use crate::{
|
||||
error::OlmResult,
|
||||
identities::{DeviceData, OtherUserIdentityData, OwnUserIdentityData, UserIdentityData},
|
||||
olm::{InboundGroupSession, PrivateCrossSigningIdentity, SenderDataFinder, SenderDataType},
|
||||
requests::KeysQueryRequest,
|
||||
store::{
|
||||
caches::SequenceNumber, Changes, DeviceChanges, IdentityChanges, KeyQueryManager,
|
||||
Result as StoreResult, Store, StoreCache, StoreCacheGuard, UserKeyQueryResult,
|
||||
},
|
||||
types::{CrossSigningKey, DeviceKeys, MasterPubkey, SelfSigningPubkey, UserSigningPubkey},
|
||||
types::{
|
||||
requests::KeysQueryRequest, CrossSigningKey, DeviceKeys, MasterPubkey, SelfSigningPubkey,
|
||||
UserSigningPubkey,
|
||||
},
|
||||
CryptoStoreError, LocalTrust, OwnUserIdentity, SignatureError, UserIdentity,
|
||||
};
|
||||
|
||||
@@ -548,7 +550,7 @@ impl IdentityManager {
|
||||
// First time seen, create the identity. The current MSK will be pinned.
|
||||
let identity = OtherUserIdentityData::new(master_key, self_signing)?;
|
||||
let is_verified = maybe_verified_own_identity
|
||||
.map_or(false, |own_user_identity| own_user_identity.is_identity_signed(&identity));
|
||||
.is_some_and(|own_user_identity| own_user_identity.is_identity_signed(&identity));
|
||||
if is_verified {
|
||||
identity.mark_as_previously_verified();
|
||||
}
|
||||
@@ -1228,9 +1230,8 @@ pub(crate) mod testing {
|
||||
identities::IdentityManager,
|
||||
olm::{Account, PrivateCrossSigningIdentity},
|
||||
store::{CryptoStoreWrapper, MemoryStore, PendingChanges, Store},
|
||||
types::DeviceKeys,
|
||||
types::{requests::UploadSigningKeysRequest, DeviceKeys},
|
||||
verification::VerificationMachine,
|
||||
UploadSigningKeysRequest,
|
||||
};
|
||||
|
||||
pub fn user_id() -> &'static UserId {
|
||||
|
||||
@@ -31,14 +31,16 @@ use ruma::{
|
||||
};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde_json::Value;
|
||||
use tracing::error;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{
|
||||
error::SignatureError,
|
||||
store::{Changes, IdentityChanges, Store},
|
||||
types::{MasterPubkey, SelfSigningPubkey, UserSigningPubkey},
|
||||
types::{
|
||||
requests::OutgoingVerificationRequest, MasterPubkey, SelfSigningPubkey, UserSigningPubkey,
|
||||
},
|
||||
verification::VerificationMachine,
|
||||
CryptoStoreError, DeviceData, OutgoingVerificationRequest, VerificationRequest,
|
||||
CryptoStoreError, DeviceData, VerificationRequest,
|
||||
};
|
||||
|
||||
/// Enum over the different user identity types we can have.
|
||||
@@ -390,6 +392,7 @@ impl OtherUserIdentity {
|
||||
|
||||
/// Pin the current identity (public part of the master signing key).
|
||||
pub async fn pin_current_master_key(&self) -> Result<(), CryptoStoreError> {
|
||||
info!(master_key = ?self.master_key.get_first_key(), "Pinning current identity for user '{}'", self.user_id());
|
||||
self.inner.pin();
|
||||
let to_save = UserIdentityData::Other(self.inner.clone());
|
||||
let changes = Changes {
|
||||
@@ -425,6 +428,7 @@ impl OtherUserIdentity {
|
||||
|
||||
/// Remove the requirement for this identity to be verified.
|
||||
pub async fn withdraw_verification(&self) -> Result<(), CryptoStoreError> {
|
||||
info!(master_key = ?self.master_key.get_first_key(), "Withdrawing verification status and pinning current identity for user '{}'", self.user_id());
|
||||
self.inner.withdraw_verification();
|
||||
let to_save = UserIdentityData::Other(self.inner.clone());
|
||||
let changes = Changes {
|
||||
@@ -435,16 +439,20 @@ impl OtherUserIdentity {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Test helper
|
||||
/// Test helper that marks that an identity has been previously verified and
|
||||
/// persist the change in the store.
|
||||
#[cfg(test)]
|
||||
pub async fn mark_as_previously_verified(&self) -> Result<(), CryptoStoreError> {
|
||||
self.inner.mark_as_previously_verified();
|
||||
|
||||
let to_save = UserIdentityData::Other(self.inner.clone());
|
||||
let changes = Changes {
|
||||
identities: IdentityChanges { changed: vec![to_save], ..Default::default() },
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
self.verification_machine.store.inner().save_changes(changes).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -854,8 +862,8 @@ impl OtherUserIdentityData {
|
||||
|
||||
// Check if the new master_key is signed by our own **verified**
|
||||
// user_signing_key. If the identity was verified we remember it.
|
||||
let updated_is_verified = maybe_verified_own_user_signing_key
|
||||
.map_or(false, |own_user_signing_key| {
|
||||
let updated_is_verified =
|
||||
maybe_verified_own_user_signing_key.is_some_and(|own_user_signing_key| {
|
||||
own_user_signing_key.verify_master_key(&master_key).is_ok()
|
||||
});
|
||||
|
||||
@@ -914,6 +922,7 @@ enum OwnUserIdentityVerifiedState {
|
||||
NeverVerified,
|
||||
|
||||
/// We previously verified this identity, but it has changed.
|
||||
#[serde(alias = "PreviouslyVerifiedButNoLonger")]
|
||||
VerificationViolation,
|
||||
|
||||
/// We have verified the current identity.
|
||||
@@ -1533,26 +1542,10 @@ pub(crate) mod tests {
|
||||
/// that we can deserialize boolean values.
|
||||
#[test]
|
||||
fn test_deserialize_own_user_identity_bool_verified() {
|
||||
let mut json = json!({
|
||||
"user_id": "@example:localhost",
|
||||
"master_key": {
|
||||
"user_id":"@example:localhost",
|
||||
"usage":["master"],
|
||||
"keys":{"ed25519:rJ2TAGkEOP6dX41Ksll6cl8K3J48l8s/59zaXyvl2p0":"rJ2TAGkEOP6dX41Ksll6cl8K3J48l8s/59zaXyvl2p0"},
|
||||
},
|
||||
"self_signing_key": {
|
||||
"user_id":"@example:localhost",
|
||||
"usage":["self_signing"],
|
||||
"keys":{"ed25519:0C8lCBxrvrv/O7BQfsKnkYogHZX3zAgw3RfJuyiq210":"0C8lCBxrvrv/O7BQfsKnkYogHZX3zAgw3RfJuyiq210"}
|
||||
},
|
||||
"user_signing_key": {
|
||||
"user_id":"@example:localhost",
|
||||
"usage":["user_signing"],
|
||||
"keys":{"ed25519:DU9z4gBFKFKCk7a13sW9wjT0Iyg7Hqv5f0BPM7DEhPo":"DU9z4gBFKFKCk7a13sW9wjT0Iyg7Hqv5f0BPM7DEhPo"}
|
||||
},
|
||||
"verified": false
|
||||
});
|
||||
let mut json = own_user_identity_data();
|
||||
|
||||
// Set `"verified": false`
|
||||
*json.get_mut("verified").unwrap() = false.into();
|
||||
let id: OwnUserIdentityData = serde_json::from_value(json.clone()).unwrap();
|
||||
assert_eq!(*id.verified.read().unwrap(), OwnUserIdentityVerifiedState::NeverVerified);
|
||||
|
||||
@@ -1562,6 +1555,38 @@ pub(crate) mod tests {
|
||||
assert_eq!(*id.verified.read().unwrap(), OwnUserIdentityVerifiedState::Verified);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_own_user_identity_verified_state_verification_violation_deserializes() {
|
||||
// Given data containing verified: VerificationViolation
|
||||
let mut json = own_user_identity_data();
|
||||
*json.get_mut("verified").unwrap() = "VerificationViolation".into();
|
||||
|
||||
// When we deserialize
|
||||
let id: OwnUserIdentityData = serde_json::from_value(json.clone()).unwrap();
|
||||
|
||||
// Then the value is correctly populated
|
||||
assert_eq!(
|
||||
*id.verified.read().unwrap(),
|
||||
OwnUserIdentityVerifiedState::VerificationViolation
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_own_user_identity_verified_state_previously_verified_deserializes() {
|
||||
// Given data containing verified: PreviouslyVerifiedButNoLonger
|
||||
let mut json = own_user_identity_data();
|
||||
*json.get_mut("verified").unwrap() = "PreviouslyVerifiedButNoLonger".into();
|
||||
|
||||
// When we deserialize
|
||||
let id: OwnUserIdentityData = serde_json::from_value(json.clone()).unwrap();
|
||||
|
||||
// Then the old value is re-interpreted as VerificationViolation
|
||||
assert_eq!(
|
||||
*id.verified.read().unwrap(),
|
||||
OwnUserIdentityVerifiedState::VerificationViolation
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn own_identity_check_signatures() {
|
||||
let response = own_key_query();
|
||||
@@ -1937,4 +1962,26 @@ pub(crate) mod tests {
|
||||
assert!(!own_identity.was_previously_verified());
|
||||
assert!(!own_identity.has_verification_violation());
|
||||
}
|
||||
|
||||
fn own_user_identity_data() -> Value {
|
||||
json!({
|
||||
"user_id": "@example:localhost",
|
||||
"master_key": {
|
||||
"user_id":"@example:localhost",
|
||||
"usage":["master"],
|
||||
"keys":{"ed25519:rJ2TAGkEOP6dX41Ksll6cl8K3J48l8s/59zaXyvl2p0":"rJ2TAGkEOP6dX41Ksll6cl8K3J48l8s/59zaXyvl2p0"},
|
||||
},
|
||||
"self_signing_key": {
|
||||
"user_id":"@example:localhost",
|
||||
"usage":["self_signing"],
|
||||
"keys":{"ed25519:0C8lCBxrvrv/O7BQfsKnkYogHZX3zAgw3RfJuyiq210":"0C8lCBxrvrv/O7BQfsKnkYogHZX3zAgw3RfJuyiq210"}
|
||||
},
|
||||
"user_signing_key": {
|
||||
"user_id":"@example:localhost",
|
||||
"usage":["user_signing"],
|
||||
"keys":{"ed25519:DU9z4gBFKFKCk7a13sW9wjT0Iyg7Hqv5f0BPM7DEhPo":"DU9z4gBFKFKCk7a13sW9wjT0Iyg7Hqv5f0BPM7DEhPo"}
|
||||
},
|
||||
"verified": false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
// Copyright 2024 Damir Jelić
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -26,7 +27,6 @@ mod gossiping;
|
||||
mod identities;
|
||||
mod machine;
|
||||
pub mod olm;
|
||||
pub mod requests;
|
||||
pub mod secret_storage;
|
||||
mod session_manager;
|
||||
pub mod store;
|
||||
@@ -95,10 +95,6 @@ use matrix_sdk_common::deserialized_responses::{DecryptedRoomEvent, UnableToDecr
|
||||
#[cfg(feature = "qrcode")]
|
||||
pub use matrix_sdk_qrcode;
|
||||
pub use olm::{Account, CrossSigningStatus, EncryptionSettings, Session};
|
||||
pub use requests::{
|
||||
IncomingResponse, KeysBackupRequest, KeysQueryRequest, OutgoingRequest, OutgoingRequests,
|
||||
OutgoingVerificationRequest, RoomMessageRequest, ToDeviceRequest, UploadSigningKeysRequest,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use session_manager::CollectStrategy;
|
||||
pub use store::{
|
||||
@@ -114,7 +110,7 @@ pub use verification::{QrVerification, QrVerificationState, ScanError};
|
||||
pub use vodozemac;
|
||||
|
||||
/// The version of the matrix-sdk-cypto crate being used
|
||||
pub static VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
#[cfg(test)]
|
||||
matrix_sdk_test::init_tracing_for_tests!();
|
||||
@@ -156,3 +152,944 @@ pub enum RoomEventDecryptionResult {
|
||||
/// We were unable to decrypt the event
|
||||
UnableToDecrypt(UnableToDecryptInfo),
|
||||
}
|
||||
|
||||
#[cfg_attr(doc, aquamarine::aquamarine)]
|
||||
/// A step by step guide that explains how to include [end-to-end-encryption]
|
||||
/// support in a [Matrix] client library.
|
||||
///
|
||||
/// This crate implements a [sans-network-io](https://sans-io.readthedocs.io/)
|
||||
/// state machine that allows you to add [end-to-end-encryption] support to a
|
||||
/// [Matrix] client library.
|
||||
///
|
||||
/// This guide aims to provide a comprehensive understanding of end-to-end
|
||||
/// encryption in Matrix without any prior knowledge requirements. However, it
|
||||
/// is recommended that the reader has a basic understanding of Matrix and its
|
||||
/// [client-server specification] for a more informed and efficient learning
|
||||
/// experience.
|
||||
///
|
||||
/// The [introductory](#introduction) section provides a simplified explanation
|
||||
/// of end-to-end encryption and its implementation in Matrix for those who may
|
||||
/// not have prior knowledge. If you already have a solid understanding of
|
||||
/// end-to-end encryption, including the [Olm] and [Megolm] protocols, you may
|
||||
/// choose to skip directly to the [Getting Started](#getting-started) section.
|
||||
///
|
||||
/// # Table of Contents
|
||||
/// 1. [Introduction](#introduction)
|
||||
/// 2. [Getting started](#getting-started)
|
||||
/// 3. [Decrypting room events](#decryption)
|
||||
/// 4. [Encrypting room events](#encryption)
|
||||
/// 5. [Interactively verifying devices and user identities](#verification)
|
||||
///
|
||||
/// # Introduction
|
||||
///
|
||||
/// Welcome to the first part of this guide, where we will introduce the
|
||||
/// fundamental concepts of end-to-end encryption and its implementation in
|
||||
/// Matrix.
|
||||
///
|
||||
/// This section will provide a clear and concise overview of what
|
||||
/// end-to-end encryption is and why it is important for secure communication.
|
||||
/// You will also learn about how Matrix uses end-to-end encryption to protect
|
||||
/// the privacy and security of its users' communications. Whether you are new
|
||||
/// to the topic or simply want to improve your understanding, this section will
|
||||
/// serve as a solid foundation for the rest of the guide.
|
||||
///
|
||||
/// Let's dive in!
|
||||
///
|
||||
/// ## Notation
|
||||
///
|
||||
/// ## End-to-end-encryption
|
||||
///
|
||||
/// End-to-end encryption (E2EE) is a method of secure communication where only
|
||||
/// the communicating devices, also known as "the ends," can read the data being
|
||||
/// transmitted. This means that the data is encrypted on one device, and can
|
||||
/// only be decrypted on the other device. The server is used only as a
|
||||
/// transport mechanism to deliver messages between devices.
|
||||
///
|
||||
/// The following chart displays how communication between two clients using a
|
||||
/// server in the middle usually works.
|
||||
///
|
||||
/// ```mermaid
|
||||
/// flowchart LR
|
||||
/// alice[Alice]
|
||||
/// bob[Bob]
|
||||
/// subgraph Homeserver
|
||||
/// direction LR
|
||||
/// outbox[Alice outbox]
|
||||
/// inbox[Bob inbox]
|
||||
/// outbox -. unencrypted .-> inbox
|
||||
/// end
|
||||
///
|
||||
/// alice -- encrypted --> outbox
|
||||
/// inbox -- encrypted --> bob
|
||||
/// ```
|
||||
///
|
||||
/// The next chart, instead, displays how the same flow is happening in a
|
||||
/// end-to-end-encrypted world.
|
||||
///
|
||||
/// ```mermaid
|
||||
/// flowchart LR
|
||||
/// alice[Alice]
|
||||
/// bob[Bob]
|
||||
/// subgraph Homeserver
|
||||
/// direction LR
|
||||
/// outbox[Alice outbox]
|
||||
/// inbox[Bob inbox]
|
||||
/// outbox == encrypted ==> inbox
|
||||
/// end
|
||||
///
|
||||
/// alice == encrypted ==> outbox
|
||||
/// inbox == encrypted ==> bob
|
||||
/// ```
|
||||
///
|
||||
/// Note that the path from the outbox to the inbox is now encrypted as well.
|
||||
///
|
||||
/// Alice and Bob have created a secure communication channel
|
||||
/// through which they can exchange messages confidentially, without the risk of
|
||||
/// the server accessing the contents of their messages.
|
||||
///
|
||||
/// ## Publishing cryptographic identities of devices
|
||||
///
|
||||
/// If Alice and Bob want to establish a secure channel over which they can
|
||||
/// exchange messages, they first need learn about each others cryptographic
|
||||
/// identities. This is achieved by using the homeserver as a public key
|
||||
/// directory.
|
||||
///
|
||||
/// A public key directory is used to store and distribute public keys of users
|
||||
/// in an end-to-end encrypted system. The basic idea behind a public key
|
||||
/// directory is that it allows users to easily discover and download the public
|
||||
/// keys of other users with whom they wish to establish an end-to-end encrypted
|
||||
/// communication.
|
||||
///
|
||||
/// Each user generates a pair of public and private keys. The user then uploads
|
||||
/// their public key to the public key directory. Other users can then search
|
||||
/// the directory to find the public key of the user they wish to communicate
|
||||
/// with, and download it to their own device.
|
||||
///
|
||||
/// ```mermaid
|
||||
/// flowchart LR
|
||||
/// alice[Alice]
|
||||
/// subgraph homeserver[Homeserver]
|
||||
/// direction LR
|
||||
/// directory[(Public key directory)]
|
||||
/// end
|
||||
/// bob[Bob]
|
||||
///
|
||||
/// alice -- upload keys --> directory
|
||||
/// directory -- download keys --> bob
|
||||
/// ```
|
||||
///
|
||||
/// Once a user has the other user's public key, they can use it to establish an
|
||||
/// end-to-end encrypted channel using a [key-agreement protocol].
|
||||
///
|
||||
/// ## Using the Triple Diffie-Hellman key-agreement protocol
|
||||
///
|
||||
/// In the triple Diffie-Hellman key agreement protocol (3DH in short), each
|
||||
/// user generates a long-term identity key pair and a set of one-time prekeys.
|
||||
/// When two users want to establish a shared secret key, they exchange their
|
||||
/// public identity keys and one of their prekeys. These public keys are then
|
||||
/// used in a [Diffie-Hellman] key exchange to compute a shared secret key.
|
||||
///
|
||||
/// The use of one-time prekeys ensures that the shared secret key is different
|
||||
/// for each session, even if the same identity keys are used.
|
||||
///
|
||||
/// ```mermaid
|
||||
/// flowchart LR
|
||||
/// subgraph alice_keys[Alice Keys]
|
||||
/// direction TB
|
||||
/// alice_key[Alice's identity key]
|
||||
/// alice_base_key[Alice's one-time key]
|
||||
/// end
|
||||
///
|
||||
/// subgraph bob_keys[Bob Keys]
|
||||
/// direction TB
|
||||
/// bob_key[Bob's identity key]
|
||||
/// bob_one_time[Bob's one-time key]
|
||||
/// end
|
||||
///
|
||||
/// alice_key <--> bob_one_time
|
||||
/// alice_base_key <--> bob_one_time
|
||||
/// alice_base_key <--> bob_key
|
||||
/// ```
|
||||
///
|
||||
/// Similar to [X3DH] (Extended Triple Diffie-Hellman) key agreement protocol
|
||||
///
|
||||
/// ## Speeding up encryption for large groups
|
||||
///
|
||||
/// In the previous section we learned how to utilize a key agreement protocol
|
||||
/// to establish secure 1-to-1 encrypted communication channels. These channels
|
||||
/// allow us to encrypt a message for each device separately.
|
||||
///
|
||||
/// One critical property of these channels is that, if you want to send a
|
||||
/// message to a group of devices, we'll need to encrypt the message for each
|
||||
/// device individually.
|
||||
///
|
||||
/// TODO Explain how megolm fits into this
|
||||
///
|
||||
/// # Getting started
|
||||
///
|
||||
/// Before we start writing any code, let us get familiar with the basic
|
||||
/// principle upon which this library is built.
|
||||
///
|
||||
/// The central piece of the library is the [`OlmMachine`] which acts as a state
|
||||
/// machine which consumes data that gets received from the homeserver and
|
||||
/// outputs data which should be sent to the homeserver.
|
||||
///
|
||||
/// ## Push/pull mechanism
|
||||
///
|
||||
/// The [`OlmMachine`] at the heart of it acts as a state machine that operates
|
||||
/// in a push/pull manner. HTTP responses which were received from the
|
||||
/// homeserver get forwarded into the [`OlmMachine`] and in turn the internal
|
||||
/// state gets updated which produces HTTP requests that need to be sent to the
|
||||
/// homeserver.
|
||||
///
|
||||
/// In a manner, we're pulling data from the server, we update our internal
|
||||
/// state based on the data and in turn push data back to the server.
|
||||
///
|
||||
/// ```mermaid
|
||||
/// flowchart LR
|
||||
/// homeserver[Homeserver]
|
||||
/// client[OlmMachine]
|
||||
///
|
||||
/// homeserver -- pull --> client
|
||||
/// client -- push --> homeserver
|
||||
/// ```
|
||||
///
|
||||
/// ## Initializing the state machine
|
||||
///
|
||||
/// ```
|
||||
/// use anyhow::Result;
|
||||
/// use matrix_sdk_crypto::OlmMachine;
|
||||
/// use ruma::user_id;
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<()> {
|
||||
/// let user_id = user_id!("@alice:localhost");
|
||||
/// let device_id = "DEVICEID".into();
|
||||
///
|
||||
/// let machine = OlmMachine::new(user_id, device_id).await;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// This will create a [`OlmMachine`] that does not persist any data TODO
|
||||
/// ```ignore
|
||||
/// use anyhow::Result;
|
||||
/// use matrix_sdk_crypto::OlmMachine;
|
||||
/// use matrix_sdk_sqlite::SqliteCryptoStore;
|
||||
/// use ruma::user_id;
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<()> {
|
||||
/// let user_id = user_id!("@alice:localhost");
|
||||
/// let device_id = "DEVICEID".into();
|
||||
///
|
||||
/// let store = SqliteCryptoStore::open("/home/example/matrix-client/", None).await?;
|
||||
///
|
||||
/// let machine = OlmMachine::with_store(user_id, device_id, store).await;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// # Decryption
|
||||
///
|
||||
/// In the world of encrypted communication, it is common to start with the
|
||||
/// encryption step when implementing a protocol. However, in the case of adding
|
||||
/// end-to-end encryption support to a Matrix client library, a simpler approach
|
||||
/// is to first focus on the decryption process. This is because there are
|
||||
/// already Matrix clients in existence that support encryption, which means
|
||||
/// that our client library can simply receive encrypted messages and then
|
||||
/// decrypt them.
|
||||
///
|
||||
/// In this section, we will guide you through the minimal steps
|
||||
/// necessary to get the decryption process up and running using the
|
||||
/// matrix-sdk-crypto Rust crate. By the end of this section you should have a
|
||||
/// Matrix client that is able to decrypt room events that other clients have
|
||||
/// sent.
|
||||
///
|
||||
/// To enable decryption the following three steps are needed:
|
||||
///
|
||||
/// 1. [The cryptographic identity of your device needs to be published to the
|
||||
/// homeserver](#uploading-identity-and-one-time-keys).
|
||||
/// 2. [Decryption keys coming in from other devices need to be processed and
|
||||
/// stored](#receiving-room-keys-and-related-changes).
|
||||
/// 3. [Individual messages need to be decrypted](#decrypting-room-events).
|
||||
///
|
||||
/// The simplified flowchart
|
||||
/// ```mermaid
|
||||
/// graph TD
|
||||
/// sync[Sync with the homeserver]
|
||||
/// receive_changes[Push E2EE related changes into the state machine]
|
||||
/// send_outgoing_requests[Send all outgoing requests to the homeserver]
|
||||
/// decrypt[Process the rest of the sync]
|
||||
///
|
||||
/// sync --> receive_changes;
|
||||
/// receive_changes --> send_outgoing_requests;
|
||||
/// send_outgoing_requests --> decrypt;
|
||||
/// decrypt -- repeat --> sync;
|
||||
/// ```
|
||||
///
|
||||
/// ## Uploading identity and one-time keys.
|
||||
///
|
||||
/// To enable end-to-end encryption in a Matrix client, the first step is to
|
||||
/// announce the support for it to other users in the network. This is done by
|
||||
/// publishing the client's long-term device keys and a set of one-time prekeys
|
||||
/// to the Matrix homeserver. The homeserver then makes this information
|
||||
/// available to other devices in the network.
|
||||
///
|
||||
/// The long-term device keys and one-time prekeys allow other devices to
|
||||
/// encrypt messages specifically for your device.
|
||||
///
|
||||
/// To achieve this, you will need to extract any requests that need to be sent
|
||||
/// to the homeserver from the [`OlmMachine`] and send them to the homeserver.
|
||||
/// The following snippet showcases how to achieve this using the
|
||||
/// [`OlmMachine::outgoing_requests()`] method:
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use std::collections::BTreeMap;
|
||||
/// # use ruma::api::client::keys::upload_keys::v3::Response;
|
||||
/// # use anyhow::Result;
|
||||
/// # use matrix_sdk_crypto::{OlmMachine, types::requests::OutgoingRequest};
|
||||
/// # async fn send_request(request: OutgoingRequest) -> Result<Response> {
|
||||
/// # let response = unimplemented!();
|
||||
/// # Ok(response)
|
||||
/// # }
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<()> {
|
||||
/// # let machine: OlmMachine = unimplemented!();
|
||||
/// // Get all the outgoing requests.
|
||||
/// let outgoing_requests = machine.outgoing_requests().await?;
|
||||
///
|
||||
/// // Send each request to the server and push the response into the state machine.
|
||||
/// // You can safely send these requests out in parallel.
|
||||
/// for request in outgoing_requests {
|
||||
/// let request_id = request.request_id();
|
||||
/// // Send the request to the server and await a response.
|
||||
/// let response = send_request(request).await?;
|
||||
/// // Push the response into the state machine.
|
||||
/// machine.mark_request_as_sent(&request_id, &response).await?;
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// #### 🔒 Locking rule
|
||||
///
|
||||
/// It's important to note that the outgoing requests method in the
|
||||
/// [`OlmMachine`], while thread-safe, may return the same request multiple
|
||||
/// times if it is called multiple times before the request has been marked as
|
||||
/// sent. To prevent this issue, it is advisable to encapsulate the outgoing
|
||||
/// request handling logic into a separate helper method and protect it from
|
||||
/// being called multiple times concurrently using a lock.
|
||||
///
|
||||
/// This helps to ensure that the request is only handled once and prevents
|
||||
/// multiple identical requests from being sent.
|
||||
///
|
||||
/// Additionally, if an error occurs while sending a request using the
|
||||
/// [`OlmMachine::outgoing_requests()`] method, the request will be
|
||||
/// naturally retried the next time the method is called.
|
||||
///
|
||||
/// A more complete example, which uses a helper method, might look like this:
|
||||
/// ```no_run
|
||||
/// # use std::collections::BTreeMap;
|
||||
/// # use ruma::api::client::keys::upload_keys::v3::Response;
|
||||
/// # use anyhow::Result;
|
||||
/// # use matrix_sdk_crypto::{OlmMachine, types::requests::OutgoingRequest};
|
||||
/// # async fn send_request(request: &OutgoingRequest) -> Result<Response> {
|
||||
/// # let response = unimplemented!();
|
||||
/// # Ok(response)
|
||||
/// # }
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<()> {
|
||||
/// struct Client {
|
||||
/// outgoing_requests_lock: tokio::sync::Mutex<()>,
|
||||
/// olm_machine: OlmMachine,
|
||||
/// }
|
||||
///
|
||||
/// async fn process_outgoing_requests(client: &Client) -> Result<()> {
|
||||
/// // Let's acquire a lock so we know that we don't send out the same request out multiple
|
||||
/// // times.
|
||||
/// let guard = client.outgoing_requests_lock.lock().await;
|
||||
///
|
||||
/// for request in client.olm_machine.outgoing_requests().await? {
|
||||
/// let request_id = request.request_id();
|
||||
///
|
||||
/// match send_request(&request).await {
|
||||
/// Ok(response) => {
|
||||
/// client.olm_machine.mark_request_as_sent(&request_id, &response).await?;
|
||||
/// }
|
||||
/// Err(error) => {
|
||||
/// // It's OK to ignore transient HTTP errors since requests will be retried.
|
||||
/// eprintln!(
|
||||
/// "Error while sending out a end-to-end encryption \
|
||||
/// related request: {error:?}"
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Once we have the helper method that processes our outgoing requests we can
|
||||
/// structure our sync method as follows:
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use anyhow::Result;
|
||||
/// # use matrix_sdk_crypto::OlmMachine;
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<()> {
|
||||
/// # struct Client {
|
||||
/// # outgoing_requests_lock: tokio::sync::Mutex<()>,
|
||||
/// # olm_machine: OlmMachine,
|
||||
/// # }
|
||||
/// # async fn process_outgoing_requests(client: &Client) -> Result<()> {
|
||||
/// # unimplemented!();
|
||||
/// # }
|
||||
/// # async fn send_out_sync_request(client: &Client) -> Result<()> {
|
||||
/// # unimplemented!();
|
||||
/// # }
|
||||
/// async fn sync(client: &Client) -> Result<()> {
|
||||
/// // This is happening at the top of the method so we advertise our
|
||||
/// // end-to-end encryption capabilities as soon as possible.
|
||||
/// process_outgoing_requests(client).await?;
|
||||
///
|
||||
/// // We can sync with the homeserver now.
|
||||
/// let response = send_out_sync_request(client).await?;
|
||||
///
|
||||
/// // Process the sync response here.
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Receiving room keys and related changes
|
||||
///
|
||||
/// The next step in our implementation is to forward messages that were sent
|
||||
/// directly to the client's device, and state updates about the one-time
|
||||
/// prekeys, to the [`OlmMachine`]. This is achieved using
|
||||
/// the [`OlmMachine::receive_sync_changes()`] method.
|
||||
///
|
||||
/// The method performs two tasks:
|
||||
///
|
||||
/// 1. It processes and, if necessary, decrypts each [to-device] event that was
|
||||
/// pushed into it, and returns the decrypted events. The original events are
|
||||
/// replaced with their decrypted versions.
|
||||
///
|
||||
/// 2. It produces internal state changes that may trigger the creation of new
|
||||
/// outgoing requests. For example, if the server informs the client that its
|
||||
/// one-time prekeys have been depleted, the OlmMachine will create an
|
||||
/// outgoing request to replenish them.
|
||||
///
|
||||
/// Our updated sync method now looks like this:
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use anyhow::Result;
|
||||
/// # use matrix_sdk_crypto::{EncryptionSyncChanges, OlmMachine};
|
||||
/// # use ruma::api::client::sync::sync_events::v3::Response;
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<()> {
|
||||
/// # struct Client {
|
||||
/// # outgoing_requests_lock: tokio::sync::Mutex<()>,
|
||||
/// # olm_machine: OlmMachine,
|
||||
/// # }
|
||||
/// # async fn process_outgoing_requests(client: &Client) -> Result<()> {
|
||||
/// # unimplemented!();
|
||||
/// # }
|
||||
/// # async fn send_out_sync_request(client: &Client) -> Result<Response> {
|
||||
/// # unimplemented!();
|
||||
/// # }
|
||||
/// async fn sync(client: &Client) -> Result<()> {
|
||||
/// process_outgoing_requests(client).await?;
|
||||
///
|
||||
/// let response = send_out_sync_request(client).await?;
|
||||
///
|
||||
/// let sync_changes = EncryptionSyncChanges {
|
||||
/// to_device_events: response.to_device.events,
|
||||
/// changed_devices: &response.device_lists,
|
||||
/// one_time_keys_counts: &response.device_one_time_keys_count,
|
||||
/// unused_fallback_keys: response.device_unused_fallback_key_types.as_deref(),
|
||||
/// next_batch_token: Some(response.next_batch),
|
||||
/// };
|
||||
///
|
||||
/// // Push the sync changes into the OlmMachine, make sure that this is
|
||||
/// // happening before the `next_batch` token of the sync is persisted.
|
||||
/// let to_device_events = client
|
||||
/// .olm_machine
|
||||
/// .receive_sync_changes(sync_changes)
|
||||
/// .await?;
|
||||
///
|
||||
/// // Send the outgoing requests out that the sync changes produced.
|
||||
/// process_outgoing_requests(client).await?;
|
||||
///
|
||||
/// // Process the rest of the sync response here.
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// It is important to note that the names of the fields in the response shown
|
||||
/// in the example match the names of the fields specified in the [sync]
|
||||
/// response specification.
|
||||
///
|
||||
/// It is critical to note that due to the ephemeral nature of to-device
|
||||
/// events[[1]], it is important to process these events before persisting the
|
||||
/// `next_batch` sync token. This is because if the `next_batch` sync token is
|
||||
/// persisted before processing the to-device events, some messages might be
|
||||
/// lost, leading to decryption failures.
|
||||
///
|
||||
/// ## Decrypting room events
|
||||
///
|
||||
/// The final step in the decryption process is to decrypt the room events that
|
||||
/// are received from the server. To do this, the encrypted events must be
|
||||
/// passed to the [`OlmMachine`], which will use the keys that were previously
|
||||
/// exchanged between devices to decrypt the events. The decrypted events can
|
||||
/// then be processed and displayed to the user in the Matrix client.
|
||||
///
|
||||
/// Room message [events] can be decrypted using the
|
||||
/// [`OlmMachine::decrypt_room_event()`] method:
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use std::collections::BTreeMap;
|
||||
/// # use anyhow::Result;
|
||||
/// # use matrix_sdk_crypto::{OlmMachine, DecryptionSettings, TrustRequirement};
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<()> {
|
||||
/// # let encrypted = unimplemented!();
|
||||
/// # let room_id = unimplemented!();
|
||||
/// # let machine: OlmMachine = unimplemented!();
|
||||
/// # let settings = DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
|
||||
/// // Decrypt your room events now.
|
||||
/// let decrypted = machine
|
||||
/// .decrypt_room_event(encrypted, room_id, &settings)
|
||||
/// .await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
/// It's worth mentioning that the [`OlmMachine::decrypt_room_event()`] method
|
||||
/// is designed to be thread-safe and can be safely called concurrently. This
|
||||
/// means that room message [events] can be processed in parallel, improving the
|
||||
/// overall efficiency of the end-to-end encryption implementation.
|
||||
///
|
||||
/// By allowing room message [events] to be processed concurrently, the client's
|
||||
/// implementation can take full advantage of the capabilities of modern
|
||||
/// hardware and achieve better performance, especially when dealing with a
|
||||
/// large number of messages at once.
|
||||
///
|
||||
/// # Encryption
|
||||
///
|
||||
/// In this section of the guide, we will focus on enabling the encryption of
|
||||
/// messages in our Matrix client library. Up until this point, we have been
|
||||
/// discussing the process of decrypting messages that have been encrypted by
|
||||
/// other devices. Now, we will shift our focus to the process of encrypting
|
||||
/// messages on the client side, so that they can be securely transmitted over
|
||||
/// the Matrix network to other devices.
|
||||
///
|
||||
/// This section will guide you through the steps required to set up the
|
||||
/// encryption process, including establishing the necessary sessions and
|
||||
/// encrypting messages using the Megolm group session. The specific steps are
|
||||
/// outlined bellow:
|
||||
///
|
||||
/// 1. [Cryptographic devices of other users need to be
|
||||
/// discovered](#tracking-users)
|
||||
///
|
||||
/// 2. [Secure channels between the devices need to be
|
||||
/// established](#establishing-end-to-end-encrypted-channels)
|
||||
///
|
||||
/// 3. [A room key needs to be exchanged with the group](#exchanging-room-keys)
|
||||
///
|
||||
/// 4. [Individual messages need to be encrypted using the room
|
||||
/// key](#encrypting-room-events)
|
||||
///
|
||||
/// The process for enabling encryption in a two-device scenario is also
|
||||
/// depicted in the following sequence diagram:
|
||||
///
|
||||
/// ```mermaid
|
||||
/// sequenceDiagram
|
||||
/// actor Alice
|
||||
/// participant Homeserver
|
||||
/// actor Bob
|
||||
///
|
||||
/// Alice->>Homeserver: Download Bob's one-time prekey
|
||||
/// Homeserver->>Alice: Bob's one-time prekey
|
||||
/// Alice->>Alice: Encrypt the room key
|
||||
/// Alice->>Homeserver: Send the room key to each of Bob's devices
|
||||
/// Homeserver->>Bob: Deliver the room key
|
||||
/// Alice->>Alice: Encrypt the message
|
||||
/// Alice->>Homeserver: Send the encrypted message
|
||||
/// Homeserver->>Bob: Deliver the encrypted message
|
||||
/// ```
|
||||
///
|
||||
/// In the following subsections, we will provide a step-by-step guide on how to
|
||||
/// enable the encryption of messages using the OlmMachine. We will outline the
|
||||
/// specific method calls and usage patterns that are required to establish the
|
||||
/// necessary sessions, encrypt messages, and send them over the Matrix network.
|
||||
///
|
||||
/// ## Tracking users
|
||||
///
|
||||
/// The first step in the process of encrypting a message and sending it to a
|
||||
/// device is to discover the devices that the recipient user has. This can be
|
||||
/// achieved by sending a request to the homeserver to retrieve a list of the
|
||||
/// recipient's device keys. The response to this request will include the
|
||||
/// device keys for all of the devices that belong to the recipient, as well as
|
||||
/// information about their current status and whether or not they support
|
||||
/// end-to-end encryption.
|
||||
///
|
||||
/// The process for discovering and keeping track of devices for a user is
|
||||
/// outlined in the Matrix specification in the "[Tracking the device list for a
|
||||
/// user]" section.
|
||||
///
|
||||
/// A simplified sequence diagram of the process can also be found bellow.
|
||||
///
|
||||
/// ```mermaid
|
||||
/// sequenceDiagram
|
||||
/// actor Alice
|
||||
/// participant Homeserver
|
||||
///
|
||||
/// Alice->>Homeserver: Sync with the homeserver
|
||||
/// Homeserver->>Alice: Users whose device list has changed
|
||||
/// Alice->>Alice: Mark user's devicel list as outdated
|
||||
/// Alice->>Homeserver: Ask the server for the new device list of all the outdated users
|
||||
/// Alice->>Alice: Update the local device list and mark the users as up-to-date
|
||||
/// ```
|
||||
///
|
||||
/// The OlmMachine refers to users whose devices we are tracking as "tracked
|
||||
/// users" and utilizes the [`OlmMachine::update_tracked_users()`] method to
|
||||
/// start considering users to be tracked. Keeping the above diagram in mind, we
|
||||
/// can now update our sync method as follows:
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use anyhow::Result;
|
||||
/// # use std::ops::Deref;
|
||||
/// # use matrix_sdk_crypto::{EncryptionSyncChanges, OlmMachine};
|
||||
/// # use ruma::api::client::sync::sync_events::v3::{Response, JoinedRoom};
|
||||
/// # use ruma::{OwnedUserId, serde::Raw, events::AnySyncStateEvent};
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<()> {
|
||||
/// # struct Client {
|
||||
/// # outgoing_requests_lock: tokio::sync::Mutex<()>,
|
||||
/// # olm_machine: OlmMachine,
|
||||
/// # }
|
||||
/// # async fn process_outgoing_requests(client: &Client) -> Result<()> {
|
||||
/// # unimplemented!();
|
||||
/// # }
|
||||
/// # async fn send_out_sync_request(client: &Client) -> Result<Response> {
|
||||
/// # unimplemented!();
|
||||
/// # }
|
||||
/// # fn is_member_event_of_a_joined_user(event: &Raw<AnySyncStateEvent>) -> bool {
|
||||
/// # true
|
||||
/// # }
|
||||
/// # fn get_user_id(event: &Raw<AnySyncStateEvent>) -> OwnedUserId {
|
||||
/// # unimplemented!();
|
||||
/// # }
|
||||
/// # fn is_room_encrypted(room: &JoinedRoom) -> bool {
|
||||
/// # true
|
||||
/// # }
|
||||
/// async fn sync(client: &Client) -> Result<()> {
|
||||
/// process_outgoing_requests(client).await?;
|
||||
///
|
||||
/// let response = send_out_sync_request(client).await?;
|
||||
///
|
||||
/// let sync_changes = EncryptionSyncChanges {
|
||||
/// to_device_events: response.to_device.events,
|
||||
/// changed_devices: &response.device_lists,
|
||||
/// one_time_keys_counts: &response.device_one_time_keys_count,
|
||||
/// unused_fallback_keys: response.device_unused_fallback_key_types.as_deref(),
|
||||
/// next_batch_token: Some(response.next_batch),
|
||||
/// };
|
||||
///
|
||||
/// // Push the sync changes into the OlmMachine, make sure that this is
|
||||
/// // happening before the `next_batch` token of the sync is persisted.
|
||||
/// let to_device_events = client
|
||||
/// .olm_machine
|
||||
/// .receive_sync_changes(sync_changes)
|
||||
/// .await?;
|
||||
///
|
||||
/// // Send the outgoing requests out that the sync changes produced.
|
||||
/// process_outgoing_requests(client).await?;
|
||||
///
|
||||
/// // Collect all the joined and invited users of our end-to-end encrypted rooms here.
|
||||
/// let mut users = Vec::new();
|
||||
///
|
||||
/// for (_, room) in &response.rooms.join {
|
||||
/// // For simplicity reasons we're only looking at the state field of a joined room, but
|
||||
/// // the events in the timeline are important as well.
|
||||
/// for event in &room.state.events {
|
||||
/// if is_member_event_of_a_joined_user(event) && is_room_encrypted(room) {
|
||||
/// let user_id = get_user_id(event);
|
||||
/// users.push(user_id);
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Mark all the users that we consider to be in a end-to-end encrypted room with us to be
|
||||
/// // tracked. We need to know about all the devices each user has so we can later encrypt
|
||||
/// // messages for each of their devices.
|
||||
/// client.olm_machine.update_tracked_users(users.iter().map(Deref::deref)).await?;
|
||||
///
|
||||
/// // Process the rest of the sync response here.
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Now that we have discovered the devices of the users we'd like to
|
||||
/// communicate with in an end-to-end encrypted manner, we can start considering
|
||||
/// encrypting messages for those devices. This concludes the sync processing
|
||||
/// method, we are now ready to move on to the next section, which will explain
|
||||
/// how to begin the encryption process.
|
||||
///
|
||||
/// ## Establishing end-to-end encrypted channels
|
||||
///
|
||||
/// In the [Triple
|
||||
/// Diffie-Hellman](#using-the-triple-diffie-hellman-key-agreement-protocol)
|
||||
/// section, we described the need for two Curve25519 keys from the recipient
|
||||
/// device to establish a 1-to-1 secure channel: the long-term identity key of a
|
||||
/// device and a one-time prekey. In the previous section, we started tracking
|
||||
/// the device keys, including the long-term identity key that we need. The next
|
||||
/// step is to download the one-time prekey on an on-demand basis and establish
|
||||
/// the 1-to-1 secure channel.
|
||||
///
|
||||
/// To accomplish this, we can use the [`OlmMachine::get_missing_sessions()`]
|
||||
/// method in bulk, which will claim the one-time prekey for all the devices of
|
||||
/// a user that we're not already sharing a 1-to-1 encrypted channel with.
|
||||
///
|
||||
/// #### 🔒 Locking rule
|
||||
///
|
||||
/// As with the [`OlmMachine::outgoing_requests()`] method, it is necessary to
|
||||
/// protect this method with a lock, otherwise we will be creating more 1-to-1
|
||||
/// encrypted channels than necessary.
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use std::collections::{BTreeMap, HashSet};
|
||||
/// # use std::ops::Deref;
|
||||
/// # use anyhow::Result;
|
||||
/// # use ruma::UserId;
|
||||
/// # use ruma::api::client::keys::claim_keys::v3::{Response, Request};
|
||||
/// # use matrix_sdk_crypto::OlmMachine;
|
||||
/// # async fn send_request(request: &Request) -> Result<Response> {
|
||||
/// # let response = unimplemented!();
|
||||
/// # Ok(response)
|
||||
/// # }
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<()> {
|
||||
/// # let users: HashSet<&UserId> = HashSet::new();
|
||||
/// # let machine: OlmMachine = unimplemented!();
|
||||
/// // Mark all the users that are part of an encrypted room as tracked
|
||||
/// if let Some((request_id, request)) =
|
||||
/// machine.get_missing_sessions(users.iter().map(Deref::deref)).await?
|
||||
/// {
|
||||
/// let response = send_request(&request).await?;
|
||||
/// machine.mark_request_as_sent(&request_id, &response).await?;
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// With the ability to exchange messages directly with devices, we can now
|
||||
/// start sharing room keys over the 1-to-1 encrypted channel.
|
||||
///
|
||||
/// ## Exchanging room keys
|
||||
///
|
||||
/// To exchange a room key with our group, we will once again take a bulk
|
||||
/// approach. The [`OlmMachine::share_room_key()`] method is used to accomplish
|
||||
/// this step. This method will create a new room key, if necessary, and encrypt
|
||||
/// it for each device belonging to the users provided as an argument. It will
|
||||
/// then output an array of sendToDevice requests that we must send to the
|
||||
/// server, and mark the requests as sent.
|
||||
///
|
||||
/// #### 🔒 Locking rule
|
||||
///
|
||||
/// Like some of the previous methods, OlmMachine::share_room_key() needs to be
|
||||
/// protected by a lock to prevent the possibility of creating and sending
|
||||
/// multiple room keys simultaneously for the same group. The lock can be
|
||||
/// implemented on a per-room basis, which allows for parallel room key
|
||||
/// exchanges across different rooms.
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use std::collections::{BTreeMap, HashSet};
|
||||
/// # use std::ops::Deref;
|
||||
/// # use anyhow::Result;
|
||||
/// # use ruma::UserId;
|
||||
/// # use ruma::api::client::keys::claim_keys::v3::{Response, Request};
|
||||
/// # use matrix_sdk_crypto::{OlmMachine, types::requests::ToDeviceRequest, EncryptionSettings};
|
||||
/// # async fn send_request(request: &ToDeviceRequest) -> Result<Response> {
|
||||
/// # let response = unimplemented!();
|
||||
/// # Ok(response)
|
||||
/// # }
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<()> {
|
||||
/// # let users: HashSet<&UserId> = HashSet::new();
|
||||
/// # let room_id = unimplemented!();
|
||||
/// # let settings = EncryptionSettings::default();
|
||||
/// # let machine: OlmMachine = unimplemented!();
|
||||
/// // Let's share a room key with our group.
|
||||
/// let requests = machine.share_room_key(
|
||||
/// room_id,
|
||||
/// users.iter().map(Deref::deref),
|
||||
/// EncryptionSettings::default(),
|
||||
/// ).await?;
|
||||
///
|
||||
/// // Make sure each request is sent out
|
||||
/// for request in requests {
|
||||
/// let request_id = &request.txn_id;
|
||||
/// let response = send_request(&request).await?;
|
||||
/// machine.mark_request_as_sent(&request_id, &response).await?;
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// In order to ensure that room keys are rotated and exchanged when needed, the
|
||||
/// [`OlmMachine::share_room_key()`] method should be called before sending
|
||||
/// each room message in an end-to-end encrypted room. If a room key has
|
||||
/// already been exchanged, the method becomes a no-op.
|
||||
///
|
||||
/// ## Encrypting room events
|
||||
///
|
||||
/// After the room key has been successfully shared, a plaintext can be
|
||||
/// encrypted.
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use anyhow::Result;
|
||||
/// # use matrix_sdk_crypto::{DecryptionSettings, OlmMachine, TrustRequirement};
|
||||
/// # use ruma::events::{AnyMessageLikeEventContent, room::message::RoomMessageEventContent};
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<()> {
|
||||
/// # let room_id = unimplemented!();
|
||||
/// # let event = unimplemented!();
|
||||
/// # let machine: OlmMachine = unimplemented!();
|
||||
/// # let settings = DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
|
||||
/// let content = AnyMessageLikeEventContent::RoomMessage(RoomMessageEventContent::text_plain("It's a secret to everybody."));
|
||||
/// let encrypted_content = machine.encrypt_room_event(room_id, content).await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Appendix: Combining the session creation and room key exchange
|
||||
///
|
||||
/// The steps from the previous three sections should combined into a single
|
||||
/// method that is used to send messages.
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use std::collections::{BTreeMap, HashSet};
|
||||
/// # use std::ops::Deref;
|
||||
/// # use anyhow::Result;
|
||||
/// # use serde_json::json;
|
||||
/// # use ruma::{UserId, RoomId, serde::Raw};
|
||||
/// # use ruma::api::client::keys::claim_keys::v3::{Response, Request};
|
||||
/// # use matrix_sdk_crypto::{EncryptionSettings, OlmMachine, types::requests::ToDeviceRequest};
|
||||
/// # use tokio::sync::MutexGuard;
|
||||
/// # async fn send_request(request: &Request) -> Result<Response> {
|
||||
/// # let response = unimplemented!();
|
||||
/// # Ok(response)
|
||||
/// # }
|
||||
/// # async fn send_to_device_request(request: &ToDeviceRequest) -> Result<Response> {
|
||||
/// # let response = unimplemented!();
|
||||
/// # Ok(response)
|
||||
/// # }
|
||||
/// # async fn acquire_per_room_lock(room_id: &RoomId) -> MutexGuard<()> {
|
||||
/// # unimplemented!();
|
||||
/// # }
|
||||
/// # async fn get_joined_members(room_id: &RoomId) -> Vec<&UserId> {
|
||||
/// # unimplemented!();
|
||||
/// # }
|
||||
/// # fn is_room_encrypted(room_id: &RoomId) -> bool {
|
||||
/// # true
|
||||
/// # }
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<()> {
|
||||
/// # let users: HashSet<&UserId> = HashSet::new();
|
||||
/// # let machine: OlmMachine = unimplemented!();
|
||||
/// struct Client {
|
||||
/// session_establishment_lock: tokio::sync::Mutex<()>,
|
||||
/// olm_machine: OlmMachine,
|
||||
/// }
|
||||
///
|
||||
/// async fn establish_sessions(client: &Client, users: &[&UserId]) -> Result<()> {
|
||||
/// if let Some((request_id, request)) =
|
||||
/// client.olm_machine.get_missing_sessions(users.iter().map(Deref::deref)).await?
|
||||
/// {
|
||||
/// let response = send_request(&request).await?;
|
||||
/// client.olm_machine.mark_request_as_sent(&request_id, &response).await?;
|
||||
/// }
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
///
|
||||
/// async fn share_room_key(machine: &OlmMachine, room_id: &RoomId, users: &[&UserId]) -> Result<()> {
|
||||
/// let _lock = acquire_per_room_lock(room_id).await;
|
||||
///
|
||||
/// let requests = machine.share_room_key(
|
||||
/// room_id,
|
||||
/// users.iter().map(Deref::deref),
|
||||
/// EncryptionSettings::default(),
|
||||
/// ).await?;
|
||||
///
|
||||
/// // Make sure each request is sent out
|
||||
/// for request in requests {
|
||||
/// let request_id = &request.txn_id;
|
||||
/// let response = send_to_device_request(&request).await?;
|
||||
/// machine.mark_request_as_sent(&request_id, &response).await?;
|
||||
/// }
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
///
|
||||
/// async fn send_message(client: &Client, room_id: &RoomId, message: &str) -> Result<()> {
|
||||
/// let mut content = json!({
|
||||
/// "body": message,
|
||||
/// "msgtype": "m.text",
|
||||
/// });
|
||||
///
|
||||
/// if is_room_encrypted(room_id) {
|
||||
/// let content = Raw::new(&json!({
|
||||
/// "body": message,
|
||||
/// "msgtype": "m.text",
|
||||
/// }))?.cast();
|
||||
///
|
||||
/// let users = get_joined_members(room_id).await;
|
||||
///
|
||||
/// establish_sessions(client, &users).await?;
|
||||
/// share_room_key(&client.olm_machine, room_id, &users).await?;
|
||||
///
|
||||
/// let encrypted = client
|
||||
/// .olm_machine
|
||||
/// .encrypt_room_event_raw(room_id, "m.room.message", &content)
|
||||
/// .await?;
|
||||
/// }
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// TODO
|
||||
///
|
||||
/// [Matrix]: https://matrix.org/
|
||||
/// [Olm]: https://gitlab.matrix.org/matrix-org/olm/-/blob/master/docs/olm.md
|
||||
/// [Diffie-Hellman]: https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
|
||||
/// [Megolm]: https://gitlab.matrix.org/matrix-org/olm/blob/master/docs/megolm.md
|
||||
/// [end-to-end-encryption]: https://en.wikipedia.org/wiki/End-to-end_encryption
|
||||
/// [homeserver]: https://spec.matrix.org/unstable/#architecture
|
||||
/// [key-agreement protocol]: https://en.wikipedia.org/wiki/Key-agreement_protocol
|
||||
/// [client-server specification]: https://matrix.org/docs/spec/client_server/
|
||||
/// [forward secrecy]: https://en.wikipedia.org/wiki/Forward_secrecy
|
||||
/// [replay attacks]: https://en.wikipedia.org/wiki/Replay_attack
|
||||
/// [Tracking the device list for a user]: https://spec.matrix.org/unstable/client-server-api/#tracking-the-device-list-for-a-user
|
||||
/// [X3DH]: https://signal.org/docs/specifications/x3dh/
|
||||
/// [to-device]: https://spec.matrix.org/unstable/client-server-api/#send-to-device-messaging
|
||||
/// [sync]: https://spec.matrix.org/unstable/client-server-api/#get_matrixclientv3sync
|
||||
/// [events]: https://spec.matrix.org/unstable/client-server-api/#events
|
||||
///
|
||||
/// [1]: https://spec.matrix.org/unstable/client-server-api/#server-behaviour-4
|
||||
pub mod tutorial {}
|
||||
|
||||
@@ -70,7 +70,6 @@ use crate::{
|
||||
KnownSenderData, OlmDecryptionInfo, PrivateCrossSigningIdentity, SenderData,
|
||||
SenderDataFinder, SessionType, StaticAccountData,
|
||||
},
|
||||
requests::{IncomingResponse, OutgoingRequest, UploadSigningKeysRequest},
|
||||
session_manager::{GroupSessionManager, SessionManager},
|
||||
store::{
|
||||
Changes, CryptoStoreWrapper, DeviceChanges, IdentityChanges, IntoCryptoStore, MemoryStore,
|
||||
@@ -90,12 +89,16 @@ use crate::{
|
||||
},
|
||||
ToDeviceEvents,
|
||||
},
|
||||
requests::{
|
||||
AnyIncomingResponse, KeysQueryRequest, OutgoingRequest, ToDeviceRequest,
|
||||
UploadSigningKeysRequest,
|
||||
},
|
||||
EventEncryptionAlgorithm, Signatures,
|
||||
},
|
||||
utilities::timestamp_to_iso8601,
|
||||
verification::{Verification, VerificationMachine, VerificationRequest},
|
||||
CrossSigningKeyExport, CryptoStoreError, DecryptionSettings, DeviceData, KeysQueryRequest,
|
||||
LocalTrust, RoomEventDecryptionResult, SignatureError, ToDeviceRequest, TrustRequirement,
|
||||
CrossSigningKeyExport, CryptoStoreError, DecryptionSettings, DeviceData, LocalTrust,
|
||||
RoomEventDecryptionResult, SignatureError, TrustRequirement,
|
||||
};
|
||||
|
||||
/// State machine implementation of the Olm/Megolm encryption protocol used for
|
||||
@@ -576,34 +579,34 @@ impl OlmMachine {
|
||||
pub async fn mark_request_as_sent<'a>(
|
||||
&self,
|
||||
request_id: &TransactionId,
|
||||
response: impl Into<IncomingResponse<'a>>,
|
||||
response: impl Into<AnyIncomingResponse<'a>>,
|
||||
) -> OlmResult<()> {
|
||||
match response.into() {
|
||||
IncomingResponse::KeysUpload(response) => {
|
||||
AnyIncomingResponse::KeysUpload(response) => {
|
||||
Box::pin(self.receive_keys_upload_response(response)).await?;
|
||||
}
|
||||
IncomingResponse::KeysQuery(response) => {
|
||||
AnyIncomingResponse::KeysQuery(response) => {
|
||||
Box::pin(self.receive_keys_query_response(request_id, response)).await?;
|
||||
}
|
||||
IncomingResponse::KeysClaim(response) => {
|
||||
AnyIncomingResponse::KeysClaim(response) => {
|
||||
Box::pin(
|
||||
self.inner.session_manager.receive_keys_claim_response(request_id, response),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
IncomingResponse::ToDevice(_) => {
|
||||
AnyIncomingResponse::ToDevice(_) => {
|
||||
Box::pin(self.mark_to_device_request_as_sent(request_id)).await?;
|
||||
}
|
||||
IncomingResponse::SigningKeysUpload(_) => {
|
||||
AnyIncomingResponse::SigningKeysUpload(_) => {
|
||||
Box::pin(self.receive_cross_signing_upload_response()).await?;
|
||||
}
|
||||
IncomingResponse::SignatureUpload(_) => {
|
||||
AnyIncomingResponse::SignatureUpload(_) => {
|
||||
self.inner.verification_machine.mark_request_as_sent(request_id);
|
||||
}
|
||||
IncomingResponse::RoomMessage(_) => {
|
||||
AnyIncomingResponse::RoomMessage(_) => {
|
||||
self.inner.verification_machine.mark_request_as_sent(request_id);
|
||||
}
|
||||
IncomingResponse::KeysBackup(_) => {
|
||||
AnyIncomingResponse::KeysBackup(_) => {
|
||||
Box::pin(self.inner.backup_machine.mark_request_as_sent(request_id)).await?;
|
||||
}
|
||||
};
|
||||
@@ -2311,8 +2314,8 @@ impl OlmMachine {
|
||||
/// incremented and updated it in the database. Otherwise, `false`.
|
||||
///
|
||||
/// * The (possibly updated) generation counter.
|
||||
pub async fn maintain_crypto_store_generation<'a>(
|
||||
&'a self,
|
||||
pub async fn maintain_crypto_store_generation(
|
||||
&'_ self,
|
||||
generation: &Mutex<Option<u64>>,
|
||||
) -> StoreResult<(bool, u64)> {
|
||||
let mut gen_guard = generation.lock().await;
|
||||
@@ -2579,7 +2582,9 @@ fn megolm_error_to_utd_info(
|
||||
let reason = match error {
|
||||
EventError(_) => UnableToDecryptReason::MalformedEncryptedEvent,
|
||||
Decode(_) => UnableToDecryptReason::MalformedEncryptedEvent,
|
||||
MissingRoomKey(_) => UnableToDecryptReason::MissingMegolmSession,
|
||||
MissingRoomKey(maybe_withheld) => {
|
||||
UnableToDecryptReason::MissingMegolmSession { withheld_code: maybe_withheld }
|
||||
}
|
||||
Decryption(DecryptionError::UnknownMessageIndex(_, _)) => {
|
||||
UnableToDecryptReason::UnknownMegolmMessageIndex
|
||||
}
|
||||
|
||||
@@ -34,8 +34,9 @@ use ruma::{
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
store::Changes, types::events::ToDeviceEvent, CrossSigningBootstrapRequests, DeviceData,
|
||||
OlmMachine, OutgoingRequests,
|
||||
store::Changes,
|
||||
types::{events::ToDeviceEvent, requests::AnyOutgoingRequest},
|
||||
CrossSigningBootstrapRequests, DeviceData, OlmMachine,
|
||||
};
|
||||
|
||||
/// These keys need to be periodically uploaded to the server.
|
||||
@@ -214,7 +215,7 @@ pub fn bootstrap_requests_to_keys_query_response(
|
||||
// And if we have a device, add that
|
||||
if let Some(dk) = bootstrap_requests
|
||||
.upload_keys_req
|
||||
.and_then(|req| as_variant!(req.request.as_ref(), OutgoingRequests::KeysUpload).cloned())
|
||||
.and_then(|req| as_variant!(req.request.as_ref(), AnyOutgoingRequest::KeysUpload).cloned())
|
||||
.and_then(|keys_upload_request| keys_upload_request.device_keys)
|
||||
{
|
||||
let user_id: String = dk.get_field("user_id").unwrap().unwrap();
|
||||
|
||||
@@ -42,11 +42,12 @@ use crate::{
|
||||
room::encrypted::{EncryptedEvent, RoomEventEncryptionScheme},
|
||||
ToDeviceEvent,
|
||||
},
|
||||
requests::AnyOutgoingRequest,
|
||||
CrossSigningKey, DeviceKeys, EventEncryptionAlgorithm, MasterPubkey, SelfSigningPubkey,
|
||||
},
|
||||
utilities::json_convert,
|
||||
CryptoStoreError, DecryptionSettings, DeviceData, EncryptionSettings, LocalTrust, OlmMachine,
|
||||
OtherUserIdentityData, OutgoingRequests, TrustRequirement, UserIdentity,
|
||||
OtherUserIdentityData, TrustRequirement, UserIdentity,
|
||||
};
|
||||
|
||||
#[async_test]
|
||||
@@ -497,7 +498,7 @@ async fn set_up_alice_cross_signing(alice: &OlmMachine, bob: &OlmMachine) {
|
||||
upload_signing_keys_req.self_signing_key.unwrap().try_into().unwrap();
|
||||
let upload_keys_req = cross_signing_requests.upload_keys_req.unwrap().clone();
|
||||
assert_let!(
|
||||
OutgoingRequests::KeysUpload(device_upload_request) = upload_keys_req.request.as_ref()
|
||||
AnyOutgoingRequest::KeysUpload(device_upload_request) = upload_keys_req.request.as_ref()
|
||||
);
|
||||
bob.store()
|
||||
.save_device_data(&[DeviceData::try_from(
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{fmt::Debug, iter, pin::Pin};
|
||||
|
||||
@@ -23,6 +21,7 @@ use matrix_sdk_test::async_test;
|
||||
use ruma::{room_id, user_id, RoomId, TransactionId, UserId};
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
|
||||
|
||||
use crate::{
|
||||
machine::{
|
||||
@@ -305,13 +304,16 @@ where
|
||||
/// Given the `room_keys_received_stream`, check that there is a pending update,
|
||||
/// and pop it.
|
||||
fn get_room_key_received_update(
|
||||
room_keys_received_stream: &mut Pin<Box<impl Stream<Item = Vec<RoomKeyInfo>>>>,
|
||||
room_keys_received_stream: &mut Pin<
|
||||
Box<impl Stream<Item = Result<Vec<RoomKeyInfo>, BroadcastStreamRecvError>>>,
|
||||
>,
|
||||
) -> RoomKeyInfo {
|
||||
room_keys_received_stream
|
||||
.next()
|
||||
.now_or_never()
|
||||
.flatten()
|
||||
.expect("We should have received an update of room key infos")
|
||||
.unwrap()
|
||||
.pop()
|
||||
.expect("Received an empty room key info update")
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ use futures_util::{pin_mut, FutureExt, StreamExt};
|
||||
use itertools::Itertools;
|
||||
use matrix_sdk_common::deserialized_responses::{
|
||||
UnableToDecryptInfo, UnableToDecryptReason, UnsignedDecryptionResult, UnsignedEventLocation,
|
||||
WithheldCode,
|
||||
};
|
||||
use matrix_sdk_test::{async_test, message_like_event_content, ruma_response_from_json, test_json};
|
||||
use ruma::{
|
||||
@@ -61,17 +62,16 @@ use crate::{
|
||||
types::{
|
||||
events::{
|
||||
room::encrypted::{EncryptedToDeviceEvent, ToDeviceEncryptedEventContent},
|
||||
room_key_withheld::{
|
||||
MegolmV1AesSha2WithheldContent, RoomKeyWithheldContent, WithheldCode,
|
||||
},
|
||||
room_key_withheld::{MegolmV1AesSha2WithheldContent, RoomKeyWithheldContent},
|
||||
ToDeviceEvent,
|
||||
},
|
||||
requests::{AnyOutgoingRequest, ToDeviceRequest},
|
||||
DeviceKeys, SignedKey, SigningKeys,
|
||||
},
|
||||
utilities::json_convert,
|
||||
verification::tests::bob_id,
|
||||
Account, DecryptionSettings, DeviceData, EncryptionSettings, MegolmError, OlmError,
|
||||
OutgoingRequests, RoomEventDecryptionResult, ToDeviceRequest, TrustRequirement,
|
||||
RoomEventDecryptionResult, TrustRequirement,
|
||||
};
|
||||
|
||||
mod decryption_verification_state;
|
||||
@@ -448,7 +448,7 @@ async fn test_request_missing_secrets() {
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter(|outgoing| match outgoing.request.as_ref() {
|
||||
OutgoingRequests::ToDeviceRequest(request) => {
|
||||
AnyOutgoingRequest::ToDeviceRequest(request) => {
|
||||
request.event_type.to_string() == "m.secret.request"
|
||||
}
|
||||
_ => false,
|
||||
@@ -479,7 +479,7 @@ async fn test_request_missing_secrets_cross_signed() {
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter(|outgoing| match outgoing.request.as_ref() {
|
||||
OutgoingRequests::ToDeviceRequest(request) => {
|
||||
AnyOutgoingRequest::ToDeviceRequest(request) => {
|
||||
request.event_type.to_string() == "m.secret.request"
|
||||
}
|
||||
_ => false,
|
||||
@@ -530,7 +530,8 @@ async fn test_megolm_encryption() {
|
||||
.next()
|
||||
.now_or_never()
|
||||
.flatten()
|
||||
.expect("We should have received an update of room key infos");
|
||||
.expect("We should have received an update of room key infos")
|
||||
.unwrap();
|
||||
assert_eq!(room_keys.len(), 1);
|
||||
assert_eq!(room_keys[0].session_id, group_session.session_id());
|
||||
|
||||
@@ -682,7 +683,12 @@ async fn test_withheld_unverified() {
|
||||
bob.try_decrypt_room_event(&room_event, room_id, &decryption_settings).await.unwrap();
|
||||
assert_let!(RoomEventDecryptionResult::UnableToDecrypt(utd_info) = decrypt_result);
|
||||
assert!(utd_info.session_id.is_some());
|
||||
assert_eq!(utd_info.reason, UnableToDecryptReason::MissingMegolmSession);
|
||||
assert_eq!(
|
||||
utd_info.reason,
|
||||
UnableToDecryptReason::MissingMegolmSession {
|
||||
withheld_code: Some(WithheldCode::Unverified)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// Test what happens when we feed an unencrypted event into the decryption
|
||||
@@ -1361,7 +1367,7 @@ async fn test_unsigned_decryption() {
|
||||
replace_encryption_result,
|
||||
UnsignedDecryptionResult::UnableToDecrypt(UnableToDecryptInfo {
|
||||
session_id: Some(second_room_key_session_id),
|
||||
reason: UnableToDecryptReason::MissingMegolmSession,
|
||||
reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None },
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1467,7 +1473,7 @@ async fn test_unsigned_decryption() {
|
||||
thread_encryption_result,
|
||||
UnsignedDecryptionResult::UnableToDecrypt(UnableToDecryptInfo {
|
||||
session_id: Some(third_room_key_session_id),
|
||||
reason: UnableToDecryptReason::MissingMegolmSession,
|
||||
reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None },
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ use crate::{
|
||||
test_helpers::{get_machine_pair, get_machine_pair_with_session},
|
||||
tests,
|
||||
},
|
||||
types::events::ToDeviceEvent,
|
||||
types::{events::ToDeviceEvent, requests::ToDeviceRequest},
|
||||
utilities::json_convert,
|
||||
EncryptionSyncChanges, OlmError, ToDeviceRequest,
|
||||
EncryptionSyncChanges, OlmError,
|
||||
};
|
||||
|
||||
#[async_test]
|
||||
|
||||
@@ -63,7 +63,6 @@ use crate::{
|
||||
error::{EventError, OlmResult, SessionCreationError},
|
||||
identities::DeviceData,
|
||||
olm::SenderData,
|
||||
requests::UploadSigningKeysRequest,
|
||||
store::{Changes, DeviceChanges, Store},
|
||||
types::{
|
||||
events::{
|
||||
@@ -73,6 +72,7 @@ use crate::{
|
||||
ToDeviceEncryptedEventContent,
|
||||
},
|
||||
},
|
||||
requests::UploadSigningKeysRequest,
|
||||
CrossSigningKey, DeviceKeys, EventEncryptionAlgorithm, MasterPubkey, OneTimeKey, SignedKey,
|
||||
},
|
||||
OlmError, SignatureError,
|
||||
|
||||
@@ -23,6 +23,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::WithheldCode;
|
||||
use ruma::{
|
||||
events::{
|
||||
room::{encryption::RoomEncryptionEventContent, history_visibility::HistoryVisibility},
|
||||
@@ -54,11 +55,12 @@ use crate::{
|
||||
MegolmV1AesSha2Content, RoomEncryptedEventContent, RoomEventEncryptionScheme,
|
||||
},
|
||||
room_key::{MegolmV1AesSha2Content as MegolmV1AesSha2RoomKeyContent, RoomKeyContent},
|
||||
room_key_withheld::{RoomKeyWithheldContent, WithheldCode},
|
||||
room_key_withheld::RoomKeyWithheldContent,
|
||||
},
|
||||
requests::ToDeviceRequest,
|
||||
EventEncryptionAlgorithm,
|
||||
},
|
||||
DeviceData, ToDeviceRequest,
|
||||
DeviceData,
|
||||
};
|
||||
|
||||
const ONE_HOUR: Duration = Duration::from_secs(60 * 60);
|
||||
|
||||
@@ -215,6 +215,7 @@ enum SenderDataReader {
|
||||
legacy_session: bool,
|
||||
},
|
||||
|
||||
#[serde(alias = "SenderUnverifiedButPreviouslyVerified")]
|
||||
VerificationViolation(KnownSenderData),
|
||||
|
||||
SenderUnverified(KnownSenderData),
|
||||
@@ -286,7 +287,10 @@ mod tests {
|
||||
use vodozemac::Ed25519PublicKey;
|
||||
|
||||
use super::SenderData;
|
||||
use crate::types::{DeviceKeys, Signatures};
|
||||
use crate::{
|
||||
olm::KnownSenderData,
|
||||
types::{DeviceKeys, Signatures},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn serializing_unknown_device_correctly_preserves_owner_check_failed_if_true() {
|
||||
@@ -360,6 +364,47 @@ mod tests {
|
||||
assert_let!(SenderData::SenderVerified { .. } = end);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserializing_sender_unverified_but_previously_verified_migrates_to_verification_violation()
|
||||
{
|
||||
let json = r#"
|
||||
{
|
||||
"SenderUnverifiedButPreviouslyVerified":{
|
||||
"user_id":"@u:s.co",
|
||||
"master_key":[
|
||||
150,140,249,139,141,29,63,230,179,14,213,175,176,61,11,255,
|
||||
26,103,10,51,100,154,183,47,181,117,87,204,33,215,241,92
|
||||
],
|
||||
"master_key_verified":true
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
let end: SenderData = serde_json::from_str(json).expect("Failed to parse!");
|
||||
assert_let!(SenderData::VerificationViolation(KnownSenderData { user_id, .. }) = end);
|
||||
assert_eq!(user_id, owned_user_id!("@u:s.co"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserializing_verification_violation() {
|
||||
let json = r#"
|
||||
{
|
||||
"VerificationViolation":{
|
||||
"user_id":"@u:s.co",
|
||||
"master_key":[
|
||||
150,140,249,139,141,29,63,230,179,14,213,175,176,61,11,255,
|
||||
26,103,10,51,100,154,183,47,181,117,87,204,33,215,241,92
|
||||
],
|
||||
"master_key_verified":true
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
let end: SenderData = serde_json::from_str(json).expect("Failed to parse!");
|
||||
assert_let!(SenderData::VerificationViolation(KnownSenderData { user_id, .. }) = end);
|
||||
assert_eq!(user_id, owned_user_id!("@u:s.co"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equal_sessions_have_same_trust_level() {
|
||||
let unknown = SenderData::unknown();
|
||||
|
||||
@@ -32,9 +32,11 @@ use vodozemac::Ed25519Signature;
|
||||
use super::StaticAccountData;
|
||||
use crate::{
|
||||
error::SignatureError,
|
||||
requests::UploadSigningKeysRequest,
|
||||
store::SecretImportError,
|
||||
types::{DeviceKeys, MasterPubkey, SelfSigningPubkey, UserSigningPubkey},
|
||||
types::{
|
||||
requests::UploadSigningKeysRequest, DeviceKeys, MasterPubkey, SelfSigningPubkey,
|
||||
UserSigningPubkey,
|
||||
},
|
||||
Account, DeviceData, OtherUserIdentityData, OwnUserIdentity, OwnUserIdentityData,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,439 +0,0 @@
|
||||
// Copyright 2020 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.
|
||||
|
||||
//! Modules containing customized request types.
|
||||
|
||||
use std::{collections::BTreeMap, iter, sync::Arc, time::Duration};
|
||||
|
||||
#[cfg(test)]
|
||||
use as_variant::as_variant;
|
||||
use ruma::{
|
||||
api::client::{
|
||||
backup::{add_backup_keys::v3::Response as KeysBackupResponse, RoomKeyBackup},
|
||||
keys::{
|
||||
claim_keys::v3::{Request as KeysClaimRequest, Response as KeysClaimResponse},
|
||||
get_keys::v3::Response as KeysQueryResponse,
|
||||
upload_keys::v3::{Request as KeysUploadRequest, Response as KeysUploadResponse},
|
||||
upload_signatures::v3::{
|
||||
Request as SignatureUploadRequest, Response as SignatureUploadResponse,
|
||||
},
|
||||
upload_signing_keys::v3::Response as SigningKeysUploadResponse,
|
||||
},
|
||||
message::send_message_event::v3::Response as RoomMessageResponse,
|
||||
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
|
||||
},
|
||||
events::{
|
||||
AnyMessageLikeEventContent, AnyToDeviceEventContent, EventContent, ToDeviceEventType,
|
||||
},
|
||||
serde::Raw,
|
||||
to_device::DeviceIdOrAllDevices,
|
||||
OwnedDeviceId, OwnedRoomId, OwnedTransactionId, OwnedUserId, TransactionId, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::types::CrossSigningKey;
|
||||
|
||||
/// Customized version of
|
||||
/// `ruma_client_api::to_device::send_event_to_device::v3::Request`
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct ToDeviceRequest {
|
||||
/// Type of event being sent to each device.
|
||||
pub event_type: ToDeviceEventType,
|
||||
|
||||
/// A request identifier unique to the access token used to send the
|
||||
/// request.
|
||||
pub txn_id: OwnedTransactionId,
|
||||
|
||||
/// A map of users to devices to a content for a message event to be
|
||||
/// sent to the user's device. Individual message events can be sent
|
||||
/// to devices, but all events must be of the same type.
|
||||
/// The content's type for this field will be updated in a future
|
||||
/// release, until then you can create a value using
|
||||
/// `serde_json::value::to_raw_value`.
|
||||
pub messages:
|
||||
BTreeMap<OwnedUserId, BTreeMap<DeviceIdOrAllDevices, Raw<AnyToDeviceEventContent>>>,
|
||||
}
|
||||
|
||||
impl ToDeviceRequest {
|
||||
/// Create a new owned to-device request
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `recipient` - The ID of the user that should receive this to-device
|
||||
/// event.
|
||||
///
|
||||
/// * `recipient_device` - The device that should receive this to-device
|
||||
/// event, or all devices.
|
||||
///
|
||||
/// * `event_type` - The type of the event content that is getting sent out.
|
||||
///
|
||||
/// * `content` - The content of the to-device event.
|
||||
pub fn new(
|
||||
recipient: &UserId,
|
||||
recipient_device: impl Into<DeviceIdOrAllDevices>,
|
||||
event_type: &str,
|
||||
content: Raw<AnyToDeviceEventContent>,
|
||||
) -> Self {
|
||||
let event_type = ToDeviceEventType::from(event_type);
|
||||
let user_messages = iter::once((recipient_device.into(), content)).collect();
|
||||
let messages = iter::once((recipient.to_owned(), user_messages)).collect();
|
||||
|
||||
ToDeviceRequest { event_type, txn_id: TransactionId::new(), messages }
|
||||
}
|
||||
|
||||
pub(crate) fn for_recipients(
|
||||
recipient: &UserId,
|
||||
recipient_devices: Vec<OwnedDeviceId>,
|
||||
content: &AnyToDeviceEventContent,
|
||||
txn_id: OwnedTransactionId,
|
||||
) -> Self {
|
||||
let event_type = content.event_type();
|
||||
let raw_content = Raw::new(content).expect("Failed to serialize to-device event");
|
||||
|
||||
if recipient_devices.is_empty() {
|
||||
Self::new(
|
||||
recipient,
|
||||
DeviceIdOrAllDevices::AllDevices,
|
||||
&event_type.to_string(),
|
||||
raw_content,
|
||||
)
|
||||
} else {
|
||||
let device_messages = recipient_devices
|
||||
.into_iter()
|
||||
.map(|d| (DeviceIdOrAllDevices::DeviceId(d), raw_content.clone()))
|
||||
.collect();
|
||||
|
||||
let messages = iter::once((recipient.to_owned(), device_messages)).collect();
|
||||
|
||||
ToDeviceRequest { event_type, txn_id, messages }
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_id_raw(
|
||||
recipient: &UserId,
|
||||
recipient_device: impl Into<DeviceIdOrAllDevices>,
|
||||
content: Raw<AnyToDeviceEventContent>,
|
||||
event_type: ToDeviceEventType,
|
||||
txn_id: OwnedTransactionId,
|
||||
) -> Self {
|
||||
let user_messages = iter::once((recipient_device.into(), content)).collect();
|
||||
let messages = iter::once((recipient.to_owned(), user_messages)).collect();
|
||||
|
||||
ToDeviceRequest { event_type, txn_id, messages }
|
||||
}
|
||||
|
||||
pub(crate) fn with_id(
|
||||
recipient: &UserId,
|
||||
recipient_device: impl Into<DeviceIdOrAllDevices>,
|
||||
content: &AnyToDeviceEventContent,
|
||||
txn_id: OwnedTransactionId,
|
||||
) -> Self {
|
||||
let event_type = content.event_type();
|
||||
let raw_content = Raw::new(content).expect("Failed to serialize to-device event");
|
||||
|
||||
let user_messages = iter::once((recipient_device.into(), raw_content)).collect();
|
||||
let messages = iter::once((recipient.to_owned(), user_messages)).collect();
|
||||
|
||||
ToDeviceRequest { event_type, txn_id, messages }
|
||||
}
|
||||
|
||||
/// Get the number of unique messages this request contains.
|
||||
///
|
||||
/// *Note*: A single message may be sent to multiple devices, so this may or
|
||||
/// may not be the number of devices that will receive the messages as well.
|
||||
pub fn message_count(&self) -> usize {
|
||||
self.messages.values().map(|d| d.len()).sum()
|
||||
}
|
||||
}
|
||||
|
||||
/// Request that will publish a cross signing identity.
|
||||
///
|
||||
/// This uploads the public cross signing key triplet.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UploadSigningKeysRequest {
|
||||
/// The user's master key.
|
||||
pub master_key: Option<CrossSigningKey>,
|
||||
/// The user's self-signing key. Must be signed with the accompanied master,
|
||||
/// or by the user's most recently uploaded master key if no master key
|
||||
/// is included in the request.
|
||||
pub self_signing_key: Option<CrossSigningKey>,
|
||||
/// The user's user-signing key. Must be signed with the accompanied master,
|
||||
/// or by the user's most recently uploaded master key if no master key
|
||||
/// is included in the request.
|
||||
pub user_signing_key: Option<CrossSigningKey>,
|
||||
}
|
||||
|
||||
/// Customized version of
|
||||
/// `ruma_client_api::keys::get_keys::v3::Request`, without any
|
||||
/// references.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KeysQueryRequest {
|
||||
/// The time (in milliseconds) to wait when downloading keys from remote
|
||||
/// servers. 10 seconds is the recommended default.
|
||||
pub timeout: Option<Duration>,
|
||||
|
||||
/// The keys to be downloaded. An empty list indicates all devices for
|
||||
/// the corresponding user.
|
||||
pub device_keys: BTreeMap<OwnedUserId, Vec<OwnedDeviceId>>,
|
||||
}
|
||||
|
||||
impl KeysQueryRequest {
|
||||
pub(crate) fn new(users: impl Iterator<Item = OwnedUserId>) -> Self {
|
||||
let device_keys = users.map(|u| (u, Vec::new())).collect();
|
||||
|
||||
Self { timeout: None, device_keys }
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum over the different outgoing requests we can have.
|
||||
#[derive(Debug)]
|
||||
pub enum OutgoingRequests {
|
||||
/// The `/keys/upload` request, uploading device and one-time keys.
|
||||
KeysUpload(KeysUploadRequest),
|
||||
/// The `/keys/query` request, fetching the device and cross signing keys of
|
||||
/// other users.
|
||||
KeysQuery(KeysQueryRequest),
|
||||
/// The request to claim one-time keys for a user/device pair from the
|
||||
/// server, after the response is received an 1-to-1 Olm session will be
|
||||
/// established with the user/device pair.
|
||||
KeysClaim(KeysClaimRequest),
|
||||
/// The to-device requests, this request is used for a couple of different
|
||||
/// things, the main use is key requests/forwards and interactive device
|
||||
/// verification.
|
||||
ToDeviceRequest(ToDeviceRequest),
|
||||
/// Signature upload request, this request is used after a successful device
|
||||
/// or user verification is done.
|
||||
SignatureUpload(SignatureUploadRequest),
|
||||
/// A room message request, usually for sending in-room interactive
|
||||
/// verification events.
|
||||
RoomMessage(RoomMessageRequest),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl OutgoingRequests {
|
||||
pub fn to_device(&self) -> Option<&ToDeviceRequest> {
|
||||
as_variant!(self, Self::ToDeviceRequest)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeysQueryRequest> for OutgoingRequests {
|
||||
fn from(request: KeysQueryRequest) -> Self {
|
||||
Self::KeysQuery(request)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeysClaimRequest> for OutgoingRequests {
|
||||
fn from(r: KeysClaimRequest) -> Self {
|
||||
Self::KeysClaim(r)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeysUploadRequest> for OutgoingRequests {
|
||||
fn from(request: KeysUploadRequest) -> Self {
|
||||
Self::KeysUpload(request)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ToDeviceRequest> for OutgoingRequests {
|
||||
fn from(request: ToDeviceRequest) -> Self {
|
||||
Self::ToDeviceRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomMessageRequest> for OutgoingRequests {
|
||||
fn from(request: RoomMessageRequest) -> Self {
|
||||
Self::RoomMessage(request)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SignatureUploadRequest> for OutgoingRequests {
|
||||
fn from(request: SignatureUploadRequest) -> Self {
|
||||
Self::SignatureUpload(request)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OutgoingVerificationRequest> for OutgoingRequest {
|
||||
fn from(r: OutgoingVerificationRequest) -> Self {
|
||||
Self { request_id: r.request_id().to_owned(), request: Arc::new(r.into()) }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SignatureUploadRequest> for OutgoingRequest {
|
||||
fn from(r: SignatureUploadRequest) -> Self {
|
||||
Self { request_id: TransactionId::new(), request: Arc::new(r.into()) }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeysUploadRequest> for OutgoingRequest {
|
||||
fn from(r: KeysUploadRequest) -> Self {
|
||||
Self { request_id: TransactionId::new(), request: Arc::new(r.into()) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum over all the incoming responses we need to receive.
|
||||
#[derive(Debug)]
|
||||
pub enum IncomingResponse<'a> {
|
||||
/// The `/keys/upload` response, notifying us about the amount of uploaded
|
||||
/// one-time keys.
|
||||
KeysUpload(&'a KeysUploadResponse),
|
||||
/// The `/keys/query` response, giving us the device and cross signing keys
|
||||
/// of other users.
|
||||
KeysQuery(&'a KeysQueryResponse),
|
||||
/// The to-device response, an empty response.
|
||||
ToDevice(&'a ToDeviceResponse),
|
||||
/// The key claiming requests, giving us new one-time keys of other users so
|
||||
/// new Olm sessions can be created.
|
||||
KeysClaim(&'a KeysClaimResponse),
|
||||
/// The cross signing `/keys/upload` response, marking our private cross
|
||||
/// signing identity as shared.
|
||||
SigningKeysUpload(&'a SigningKeysUploadResponse),
|
||||
/// The cross signing signature upload response.
|
||||
SignatureUpload(&'a SignatureUploadResponse),
|
||||
/// A room message response, usually for interactive verifications.
|
||||
RoomMessage(&'a RoomMessageResponse),
|
||||
/// Response for the server-side room key backup request.
|
||||
KeysBackup(&'a KeysBackupResponse),
|
||||
}
|
||||
|
||||
impl<'a> From<&'a KeysUploadResponse> for IncomingResponse<'a> {
|
||||
fn from(response: &'a KeysUploadResponse) -> Self {
|
||||
IncomingResponse::KeysUpload(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a KeysBackupResponse> for IncomingResponse<'a> {
|
||||
fn from(response: &'a KeysBackupResponse) -> Self {
|
||||
IncomingResponse::KeysBackup(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a KeysQueryResponse> for IncomingResponse<'a> {
|
||||
fn from(response: &'a KeysQueryResponse) -> Self {
|
||||
IncomingResponse::KeysQuery(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ToDeviceResponse> for IncomingResponse<'a> {
|
||||
fn from(response: &'a ToDeviceResponse) -> Self {
|
||||
IncomingResponse::ToDevice(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a RoomMessageResponse> for IncomingResponse<'a> {
|
||||
fn from(response: &'a RoomMessageResponse) -> Self {
|
||||
IncomingResponse::RoomMessage(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a KeysClaimResponse> for IncomingResponse<'a> {
|
||||
fn from(response: &'a KeysClaimResponse) -> Self {
|
||||
IncomingResponse::KeysClaim(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a SignatureUploadResponse> for IncomingResponse<'a> {
|
||||
fn from(response: &'a SignatureUploadResponse) -> Self {
|
||||
IncomingResponse::SignatureUpload(response)
|
||||
}
|
||||
}
|
||||
|
||||
/// Outgoing request type, holds the unique ID of the request and the actual
|
||||
/// request.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OutgoingRequest {
|
||||
/// The unique id of a request, needs to be passed when receiving a
|
||||
/// response.
|
||||
pub(crate) request_id: OwnedTransactionId,
|
||||
/// The underlying outgoing request.
|
||||
pub(crate) request: Arc<OutgoingRequests>,
|
||||
}
|
||||
|
||||
impl OutgoingRequest {
|
||||
/// Get the unique id of this request.
|
||||
pub fn request_id(&self) -> &TransactionId {
|
||||
&self.request_id
|
||||
}
|
||||
|
||||
/// Get the underlying outgoing request.
|
||||
pub fn request(&self) -> &OutgoingRequests {
|
||||
&self.request
|
||||
}
|
||||
}
|
||||
|
||||
/// Customized owned request type for sending out room messages.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RoomMessageRequest {
|
||||
/// The room to send the event to.
|
||||
pub room_id: OwnedRoomId,
|
||||
|
||||
/// The transaction ID for this event.
|
||||
///
|
||||
/// Clients should generate an ID unique across requests with the
|
||||
/// same access token; it will be used by the server to ensure
|
||||
/// idempotency of requests.
|
||||
pub txn_id: OwnedTransactionId,
|
||||
|
||||
/// The event content to send.
|
||||
pub content: AnyMessageLikeEventContent,
|
||||
}
|
||||
|
||||
/// A request that will back up a batch of room keys to the server.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KeysBackupRequest {
|
||||
/// The backup version that these room keys should be part of.
|
||||
pub version: String,
|
||||
/// The map from room id to a backed up room key that we're going to upload
|
||||
/// to the server.
|
||||
pub rooms: BTreeMap<OwnedRoomId, RoomKeyBackup>,
|
||||
}
|
||||
|
||||
/// An enum over the different outgoing verification based requests.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum OutgoingVerificationRequest {
|
||||
/// The to-device verification request variant.
|
||||
ToDevice(ToDeviceRequest),
|
||||
/// The in-room verification request variant.
|
||||
InRoom(RoomMessageRequest),
|
||||
}
|
||||
|
||||
impl OutgoingVerificationRequest {
|
||||
/// Get the unique id of this request.
|
||||
pub fn request_id(&self) -> &TransactionId {
|
||||
match self {
|
||||
OutgoingVerificationRequest::ToDevice(t) => &t.txn_id,
|
||||
OutgoingVerificationRequest::InRoom(r) => &r.txn_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ToDeviceRequest> for OutgoingVerificationRequest {
|
||||
fn from(r: ToDeviceRequest) -> Self {
|
||||
OutgoingVerificationRequest::ToDevice(r)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomMessageRequest> for OutgoingVerificationRequest {
|
||||
fn from(r: RoomMessageRequest) -> Self {
|
||||
OutgoingVerificationRequest::InRoom(r)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OutgoingVerificationRequest> for OutgoingRequests {
|
||||
fn from(request: OutgoingVerificationRequest) -> Self {
|
||||
match request {
|
||||
OutgoingVerificationRequest::ToDevice(r) => OutgoingRequests::ToDeviceRequest(r),
|
||||
OutgoingVerificationRequest::InRoom(r) => OutgoingRequests::RoomMessage(r),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ use std::{
|
||||
|
||||
use futures_util::future::join_all;
|
||||
use itertools::Itertools;
|
||||
use matrix_sdk_common::executor::spawn;
|
||||
use matrix_sdk_common::{deserialized_responses::WithheldCode, executor::spawn};
|
||||
use ruma::{
|
||||
events::{AnyMessageLikeEventContent, ToDeviceEventType},
|
||||
serde::Raw,
|
||||
@@ -41,8 +41,8 @@ use crate::{
|
||||
ShareInfo, ShareState,
|
||||
},
|
||||
store::{Changes, CryptoStoreWrapper, Result as StoreResult, Store},
|
||||
types::events::{room::encrypted::RoomEncryptedEventContent, room_key_withheld::WithheldCode},
|
||||
Device, DeviceData, EncryptionSettings, OlmError, ToDeviceRequest,
|
||||
types::{events::room::encrypted::RoomEncryptedEventContent, requests::ToDeviceRequest},
|
||||
Device, DeviceData, EncryptionSettings, OlmError,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -779,6 +779,7 @@ mod tests {
|
||||
};
|
||||
|
||||
use assert_matches2::assert_let;
|
||||
use matrix_sdk_common::deserialized_responses::WithheldCode;
|
||||
use matrix_sdk_test::{async_test, ruma_response_from_json};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
@@ -801,14 +802,12 @@ mod tests {
|
||||
types::{
|
||||
events::{
|
||||
room::encrypted::EncryptedToDeviceEvent,
|
||||
room_key_withheld::{
|
||||
RoomKeyWithheldContent::{self, MegolmV1AesSha2},
|
||||
WithheldCode,
|
||||
},
|
||||
room_key_withheld::RoomKeyWithheldContent::{self, MegolmV1AesSha2},
|
||||
},
|
||||
requests::ToDeviceRequest,
|
||||
DeviceKeys, EventEncryptionAlgorithm,
|
||||
},
|
||||
EncryptionSettings, LocalTrust, OlmMachine, ToDeviceRequest,
|
||||
EncryptionSettings, LocalTrust, OlmMachine,
|
||||
};
|
||||
|
||||
fn alice_id() -> &'static UserId {
|
||||
|
||||
@@ -19,6 +19,7 @@ use std::{
|
||||
};
|
||||
|
||||
use itertools::{Either, Itertools};
|
||||
use matrix_sdk_common::deserialized_responses::WithheldCode;
|
||||
use ruma::{DeviceId, OwnedDeviceId, OwnedUserId, UserId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{debug, instrument, trace};
|
||||
@@ -27,7 +28,6 @@ use super::OutboundGroupSession;
|
||||
use crate::{
|
||||
error::{OlmResult, SessionRecipientCollectionError},
|
||||
store::Store,
|
||||
types::events::room_key_withheld::WithheldCode,
|
||||
DeviceData, EncryptionSettings, LocalTrust, OlmError, OwnUserIdentityData, UserIdentityData,
|
||||
};
|
||||
#[cfg(doc)]
|
||||
@@ -517,6 +517,7 @@ mod tests {
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use assert_matches2::assert_let;
|
||||
use matrix_sdk_common::deserialized_responses::WithheldCode;
|
||||
use matrix_sdk_test::{
|
||||
async_test, test_json,
|
||||
test_json::keys_query_sets::{
|
||||
@@ -536,7 +537,6 @@ mod tests {
|
||||
group_sessions::share_strategy::collect_session_recipients, CollectStrategy,
|
||||
},
|
||||
testing::simulate_key_query_response_for_verification,
|
||||
types::events::room_key_withheld::WithheldCode,
|
||||
CrossSigningKeyExport, EncryptionSettings, LocalTrust, OlmError, OlmMachine,
|
||||
};
|
||||
|
||||
@@ -1368,7 +1368,7 @@ mod tests {
|
||||
machine
|
||||
.mark_request_as_sent(
|
||||
&TransactionId::new(),
|
||||
crate::IncomingResponse::KeysQuery(&kq_response),
|
||||
crate::types::requests::AnyIncomingResponse::KeysQuery(&kq_response),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user