Compare commits
412 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 18b169ca7e | |||
| b9ce4059fb | |||
| e8dcb5d250 | |||
| d0c01006e4 | |||
| dc98bf7633 | |||
| ae57156252 | |||
| 3b09c60e20 | |||
| fcdb63dcbe | |||
| a095872083 | |||
| d68895f24a | |||
| ab81388018 | |||
| c5436ed73e | |||
| 2e7721b36c | |||
| a514019d7c | |||
| 2f8f39795f | |||
| b1ef15c346 | |||
| 829b6a7624 | |||
| 7b2cd8e434 | |||
| d567a45bee | |||
| 9c08cd8973 | |||
| e0ceef33f8 | |||
| 72d133260c | |||
| 91e0c76a2f | |||
| 7fac1d246d | |||
| 6762e70880 | |||
| 6b93f6698b | |||
| c92a89d571 | |||
| 684f228e70 | |||
| 0f9faad48a | |||
| d7550ec645 | |||
| ea5645869e | |||
| 8eed17bbfd | |||
| 40c08335ee | |||
| ae5ec0fa26 | |||
| 880f754f32 | |||
| 4d23b6490d | |||
| e0e531c737 | |||
| 82926d6f08 | |||
| f45c9aa3a7 | |||
| 7966dd0544 | |||
| 54e6a7d8d1 | |||
| 995ec618df | |||
| f9864b7ef4 | |||
| d23eae262e | |||
| 52090bb199 | |||
| 40e3cd3c22 | |||
| a58a74eaa7 | |||
| 1c549a3ca1 | |||
| a7bef8870f | |||
| 5e946108fe | |||
| 34ccd26ee6 | |||
| 3eeb046e62 | |||
| 6f84a44a1c | |||
| cf16978b15 | |||
| 6a30a802bb | |||
| db477a84bf | |||
| 96fbbd3cd8 | |||
| bbbcec5963 | |||
| df98b71836 | |||
| 0d901e4a86 | |||
| 9d7afaaa1c | |||
| 2fc616645f | |||
| b8f6ab066d | |||
| bb7e6cb562 | |||
| 3e81514d07 | |||
| bfcf47743e | |||
| 69448cca61 | |||
| 5daf2922b7 | |||
| c81a56c22b | |||
| f891bd13cb | |||
| a50a570fc1 | |||
| ec112ca32d | |||
| 15fdf1e86e | |||
| 3d6d798ca3 | |||
| 935ffa5aea | |||
| 1c19e7477c | |||
| 6a1576a085 | |||
| a1f028c54a | |||
| f2576e80ec | |||
| e8f705d76f | |||
| 049993d37e | |||
| 14366e85b1 | |||
| 849d705cd1 | |||
| d4adc81fe0 | |||
| 577a8feb12 | |||
| 7fb3d216f6 | |||
| 07808b4301 | |||
| f9e7d16347 | |||
| c98b2a1b3f | |||
| b44a1e46c4 | |||
| f17c3c5af4 | |||
| 5448192ea4 | |||
| 74800e20b4 | |||
| 8157193aef | |||
| 679c99aa76 | |||
| 6fa76e4b12 | |||
| 8b33806496 | |||
| 3d114aea50 | |||
| 72dcf5ed46 | |||
| 58748bec3a | |||
| f0b6225e40 | |||
| 17a58684f6 | |||
| f8c468d6fa | |||
| 39d1ed9bc6 | |||
| 3d1d1c8f6d | |||
| 5ad958722f | |||
| bedcbfd7ff | |||
| 743dec9a65 | |||
| f1a2093cfc | |||
| 8057991aee | |||
| d45ef567d4 | |||
| 42ee967b46 | |||
| 4c7575bc9e | |||
| 5e927f8109 | |||
| f91ee36245 | |||
| 0f8fc53019 | |||
| be3af5e0d4 | |||
| 38bbdf0547 | |||
| b188a157af | |||
| 9ccbac0c0e | |||
| 137fc9cfbb | |||
| 40a4c9a7e1 | |||
| ad358955fd | |||
| 0ee89c86ab | |||
| 216f0df945 | |||
| 129e9e173e | |||
| f7df0ebf97 | |||
| 2f78701374 | |||
| 872bded711 | |||
| 5196298e5f | |||
| 308526a6bc | |||
| 12f94a3fd2 | |||
| 3c873262c7 | |||
| 9689c4a40a | |||
| 57e7ae488e | |||
| a9ffe5fd72 | |||
| 59c29801e5 | |||
| 0a822c1a06 | |||
| 4b5e1c6676 | |||
| ecb9d4d2e8 | |||
| 9ade32fcd0 | |||
| 8b4a01ea54 | |||
| d5d5b9ee01 | |||
| a3dd594c9e | |||
| 98c331466e | |||
| e8877fd987 | |||
| babf16f15a | |||
| 000419cdf3 | |||
| 89d661ca8c | |||
| 1624d798ee | |||
| 726218000a | |||
| 08dcb267b3 | |||
| da0a32b088 | |||
| e22a833057 | |||
| 8aba664578 | |||
| e117a3d22f | |||
| c2f50fd8a5 | |||
| dc90c77c7d | |||
| 6c9038eb4f | |||
| 2b9b4cc589 | |||
| dd0336ee72 | |||
| 0095912091 | |||
| e6774a34da | |||
| 8ad52e34ea | |||
| 60a7bf0c3f | |||
| 8b31d8f6a3 | |||
| e21dd763e8 | |||
| 31df84f5a1 | |||
| e68bdf8460 | |||
| 6ca1f16f48 | |||
| 8c5d878172 | |||
| d6239d614a | |||
| 5af084c8c9 | |||
| dc450ac25a | |||
| cf375dd753 | |||
| ff935df136 | |||
| 0d080935cf | |||
| 4d140d8155 | |||
| cef1f8c5cb | |||
| 6a054d6c74 | |||
| 1f89efb88d | |||
| e83c09e425 | |||
| a85dac1f52 | |||
| a0bc9aafcf | |||
| 8217f967d4 | |||
| 47c9585606 | |||
| 20e09531fb | |||
| d92b33f959 | |||
| a74bcfab8f | |||
| 6dcc744b48 | |||
| 9d1c296657 | |||
| 3c7683ea53 | |||
| cd03a58083 | |||
| 4a1249fa96 | |||
| 06732ca71a | |||
| 115c7578d4 | |||
| 9f3e7debb1 | |||
| 58d2ae4c39 | |||
| 8a847a99d4 | |||
| 3d642356c6 | |||
| b4b0f3a203 | |||
| ca99977207 | |||
| dd02274883 | |||
| ad2e3a3b8f | |||
| 96119f9a30 | |||
| 737e06b581 | |||
| 4ecd599c15 | |||
| c6521a8aaf | |||
| 4046a59786 | |||
| d931cd0ea7 | |||
| a14488617e | |||
| 1de51614f1 | |||
| 6cc98ee9f7 | |||
| 3a98d46bfa | |||
| 1558858bde | |||
| e4d2f62d48 | |||
| 70f48be582 | |||
| 836c643769 | |||
| a48099d5ac | |||
| 09d8be7b4c | |||
| 03b8cabc22 | |||
| 07372c475c | |||
| a00e4089e8 | |||
| 1a24b21d42 | |||
| f51496fa0f | |||
| c6ce9c560b | |||
| 60af16ada8 | |||
| 159fb73b0a | |||
| 0fa0f2329d | |||
| 3b64d18c99 | |||
| f67fd87e57 | |||
| 5e64da660c | |||
| d152ce13a0 | |||
| d021020ee6 | |||
| 7e5f22ba9e | |||
| 6bc6ea4e72 | |||
| 585ae29868 | |||
| 3919c2a89a | |||
| 7c85e7aa4f | |||
| 4b845e17c8 | |||
| 394124cda5 | |||
| bbf9bf2c0b | |||
| 67327a0365 | |||
| 5e40426b99 | |||
| 0f264cac6e | |||
| 3c1d0b37e5 | |||
| 62231878cc | |||
| 22c99f30f3 | |||
| a7efff9849 | |||
| bc9192f818 | |||
| 0722ed9d8f | |||
| 1aa933cfd6 | |||
| e0ab16f979 | |||
| a9c999af72 | |||
| 8156413132 | |||
| e551efec8d | |||
| 877a7d678f | |||
| 457af2a2f8 | |||
| 7b38c442c7 | |||
| 201b818cc8 | |||
| 1f98e0cd19 | |||
| 21c59c95c4 | |||
| 4025c11e73 | |||
| dc6130562a | |||
| d1a14f895e | |||
| b680705d15 | |||
| 2b1ee853fc | |||
| ef137730cb | |||
| 45caaffb26 | |||
| 50c3217353 | |||
| b5dafd9798 | |||
| 5a39fd051b | |||
| ff52cf36dd | |||
| 25d217cc6f | |||
| 2116ad82df | |||
| fe4109cb9a | |||
| 41f107e5ba | |||
| ab699a90f1 | |||
| ddee7f8ccd | |||
| 2ab5ab527b | |||
| 53e3b90436 | |||
| 08e1d3876b | |||
| c3179ea5ed | |||
| 9676daee5a | |||
| 798cece4a2 | |||
| 06b387101b | |||
| 675963ec4b | |||
| d30dae3322 | |||
| bdb640a126 | |||
| ea28234d95 | |||
| c74295c604 | |||
| ec30e7b85c | |||
| fd17c28ebb | |||
| 841131f127 | |||
| a22d592bf1 | |||
| 1a32aa59a6 | |||
| a99df7e1d8 | |||
| 3e37f9d0ad | |||
| 2689e2d25a | |||
| 2cfba4cd9b | |||
| 1bce2af93c | |||
| 47c8df0ef8 | |||
| 9f32dfe9a0 | |||
| 040fd6c736 | |||
| 4dac175db0 | |||
| 5faf97cf99 | |||
| 7236b80b3b | |||
| 79b0941687 | |||
| ad001e475f | |||
| 5106d55be9 | |||
| 9771b99395 | |||
| 2c287e706f | |||
| fffff783d4 | |||
| b047bd0dc6 | |||
| 28b3b6aedf | |||
| 171974a44b | |||
| f53302a7a0 | |||
| dd709682d7 | |||
| 991e0cd395 | |||
| abcc05f889 | |||
| f3e636ea42 | |||
| 1d47507faa | |||
| cc7f6243c6 | |||
| e7e9d5b746 | |||
| b4146caac8 | |||
| 1cb51f49be | |||
| fea0e0d373 | |||
| 72911c66ad | |||
| dc047854d4 | |||
| f51a008921 | |||
| 3c5bcce217 | |||
| 422fd19d10 | |||
| 0ea07e11e9 | |||
| 059a6fa573 | |||
| 07656c2e26 | |||
| 940325574b | |||
| 9f8824b9a5 | |||
| cd141c5b84 | |||
| 9596aa0830 | |||
| 11424ce443 | |||
| cd4ec90b38 | |||
| 4680354abd | |||
| 145d6c5782 | |||
| a955af61e1 | |||
| 2a78b5b67a | |||
| 9d29c36531 | |||
| ed9c7d90b4 | |||
| 2af23d052c | |||
| fb80e06839 | |||
| b8f9cba5e7 | |||
| d119b01322 | |||
| 85833c74ba | |||
| 5b20136a50 | |||
| 362ca2bd59 | |||
| 0a9a849826 | |||
| 7126fc8a29 | |||
| f4e612ca9e | |||
| 6ab11a0323 | |||
| 76626db613 | |||
| bcea1d32e6 | |||
| 346f11319c | |||
| 937b223627 | |||
| 000d8514f6 | |||
| 72692b7b33 | |||
| 0f84d482b9 | |||
| c609150a3e | |||
| 2f46a6c8a0 | |||
| 7bdddc9d35 | |||
| 5113f114a7 | |||
| 9d96d6ead2 | |||
| c340a7187a | |||
| 0aece695dc | |||
| b2210292bf | |||
| f0ab6cb1a4 | |||
| c2eeca3f33 | |||
| cc974dd3c9 | |||
| 8b2a8e7265 | |||
| 7cad237dc6 | |||
| 72a3972303 | |||
| 2e590e2f67 | |||
| 224e437a78 | |||
| 8a9cae4af3 | |||
| 22a15f1342 | |||
| 3ab4584dfe | |||
| a3238cdadf | |||
| a884b2c696 | |||
| ec0d7b4311 | |||
| e8c2d27c9e | |||
| bff600a937 | |||
| 404a982503 | |||
| e904a98735 | |||
| b55e79fdac | |||
| 717116cc05 | |||
| 0ad4df2031 | |||
| 891e9813b1 | |||
| 19b21fdd49 | |||
| 307fa355ad | |||
| 351053fef5 | |||
| 8c735c602a | |||
| 7ffc390cea | |||
| 05b67df6e2 | |||
| f3f3d968b5 | |||
| bde1d4a353 | |||
| 4f6ddcd072 | |||
| b99188dd59 | |||
| e2fee14ced | |||
| 9fca8f0007 | |||
| ca0fc3cf6d | |||
| 378f50d8b5 | |||
| 485bb0790e | |||
| 0e9ce0271e | |||
| c0294d5e33 |
@@ -2,3 +2,12 @@
|
||||
retries = { backoff = "exponential", count = 3, delay = "1s", jitter = true }
|
||||
# kill the slow tests if they still aren't up after 180s
|
||||
slow-timeout = { period = "60s", terminate-after = 3 }
|
||||
|
||||
[profile.ci]
|
||||
retries = { backoff = "exponential", count = 4, delay = "1s", jitter = true }
|
||||
# kill the slow tests if they still aren't up after 180s
|
||||
slow-timeout = { period = "60s", terminate-after = 3 }
|
||||
|
||||
[profile.ci.junit]
|
||||
path = "junit.xml"
|
||||
store-success-output = true
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2025-02-20
|
||||
toolchain: nightly-2025-06-27
|
||||
components: rustfmt
|
||||
|
||||
- name: Run Benchmarks
|
||||
|
||||
@@ -175,7 +175,7 @@ jobs:
|
||||
run: swift test
|
||||
|
||||
- name: Build Framework
|
||||
run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=reldbg
|
||||
run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=dev --ios-deployment-target=18.0
|
||||
|
||||
complement-crypto:
|
||||
name: "Run Complement Crypto tests"
|
||||
|
||||
@@ -246,7 +246,7 @@ jobs:
|
||||
components: clippy
|
||||
|
||||
- name: Install wasm-pack
|
||||
uses: qmaru/wasm-pack-action@v0.5.0
|
||||
uses: qmaru/wasm-pack-action@v0.5.1
|
||||
if: '!matrix.check_only'
|
||||
with:
|
||||
version: v0.10.3
|
||||
@@ -290,7 +290,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check the spelling of the files in our repo
|
||||
uses: crate-ci/typos@v1.33.1
|
||||
uses: crate-ci/typos@v1.34.0
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
@@ -309,7 +309,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2025-02-20
|
||||
toolchain: nightly-2025-06-27
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Load cache
|
||||
@@ -397,4 +397,3 @@ jobs:
|
||||
- name: Compile benchmarks (no run)
|
||||
run: |
|
||||
cargo bench --profile dev --no-run
|
||||
|
||||
|
||||
@@ -17,8 +17,12 @@ env:
|
||||
RUST_LOG: info,matrix_sdk=trace
|
||||
|
||||
jobs:
|
||||
xtask:
|
||||
uses: ./.github/workflows/xtask.yml
|
||||
|
||||
code_coverage:
|
||||
name: Code Coverage
|
||||
needs: xtask
|
||||
runs-on: "ubuntu-latest"
|
||||
|
||||
# run several docker containers with the same networking stack so the hostname 'synapse'
|
||||
@@ -35,6 +39,11 @@ jobs:
|
||||
- 8008:8008
|
||||
|
||||
steps:
|
||||
# This CI workflow can run into space issue, so we're cleaning up some
|
||||
# space here.
|
||||
- name: Create some more space
|
||||
run: rm -rf /opt/hostedtoolcache
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -56,23 +65,25 @@ jobs:
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: "coverage"
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install tarpaulin
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-tarpaulin
|
||||
- name: Install cargo-llvm-cov
|
||||
uses: taiki-e/install-action@cargo-llvm-cov
|
||||
|
||||
# set up backend for integration tests
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Run tarpaulin
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Create the coverage report
|
||||
run: |
|
||||
rustup run stable cargo tarpaulin \
|
||||
--skip-clean --profile cov --out xml \
|
||||
--features experimental-widgets,testing
|
||||
target/debug/xtask ci coverage -o codecov
|
||||
env:
|
||||
CARGO_PROFILE_COV_INHERITS: 'dev'
|
||||
CARGO_PROFILE_COV_DEBUG: 1
|
||||
@@ -89,6 +100,11 @@ jobs:
|
||||
echo "Storing commit SHA ${{ github.event.pull_request.head.sha }}"
|
||||
echo "${{ github.event.pull_request.head.sha }}" > commit_sha.txt
|
||||
|
||||
- name: Move the JUnit file into the root directory
|
||||
shell: bash
|
||||
run: |
|
||||
mv target/nextest/ci/junit.xml ./junit.xml
|
||||
|
||||
# This stores the coverage report and metadata in artifacts.
|
||||
# The actual upload to Codecov is executed by a different workflow `upload_coverage.yml`.
|
||||
# The reason for this split is because `on.pull_request` workflows don't have access to secrets.
|
||||
@@ -97,7 +113,8 @@ jobs:
|
||||
with:
|
||||
name: codecov_report
|
||||
path: |
|
||||
cobertura.xml
|
||||
coverage.xml
|
||||
junit.xml
|
||||
pr_number.txt
|
||||
commit_sha.txt
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@115870536a85eaf050e369291c7895748ff12aea
|
||||
uses: tj-actions/changed-files@v46.0.5
|
||||
- name: Detect long path
|
||||
env:
|
||||
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} # ignore the deleted files
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2025-02-20
|
||||
toolchain: nightly-2025-06-27
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
@@ -77,3 +77,20 @@ jobs:
|
||||
working-directory: ${{ github.workspace }}/repo_root
|
||||
# Location where coverage report files are searched for
|
||||
directory: ${{ github.workspace }}
|
||||
|
||||
- name: Upload test results to Codecov
|
||||
uses: codecov/test-results-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
|
||||
# Manual overrides for these parameters are needed because automatic detection
|
||||
# in codecov-action does not work for non-`pull_request` workflows.
|
||||
# In `main` branch push, these default to empty strings since we want to run
|
||||
# the analysis on HEAD.
|
||||
override_commit: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
|
||||
override_pr: ${{ steps.parse_previous_artifacts.outputs.override_pr || '' }}
|
||||
working-directory: ${{ github.workspace }}/repo_root
|
||||
|
||||
# Location where coverage report files are searched for
|
||||
directory: ${{ github.workspace }}
|
||||
|
||||
Generated
+58
-32
@@ -877,6 +877,16 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console_error_panic_hook"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
@@ -1173,9 +1183,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deadpool-sqlite"
|
||||
version = "0.10.0"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d84a12c51972a50e54895427e43743da9737af66395a609283be01ec72efd9fb"
|
||||
checksum = "9e531d0beb6d12daa84df0482bf89e06c7ed059551ae1d7313dc7531d37778fb"
|
||||
dependencies = [
|
||||
"deadpool 0.12.1",
|
||||
"deadpool-sync",
|
||||
@@ -2689,9 +2699,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.31.0"
|
||||
version = "0.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad8935b44e7c13394a179a438e0cebba0fe08fe01b54f152e29a93b5cf993fd4"
|
||||
checksum = "947e6816f7825b2b45027c2c32e7085da9934defa535de4a6a46b10a4d5257fa"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
@@ -2864,7 +2874,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matrix-sdk"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"anymap2",
|
||||
@@ -2940,8 +2950,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matrix-sdk-base"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"as_variant",
|
||||
"assert_matches",
|
||||
"assert_matches2",
|
||||
@@ -2976,7 +2987,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matrix-sdk-common"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"assert_matches",
|
||||
"assert_matches2",
|
||||
@@ -3007,7 +3018,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matrix-sdk-crypto"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"anyhow",
|
||||
@@ -3089,11 +3100,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matrix-sdk-ffi"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"as_variant",
|
||||
"async-compat",
|
||||
"console_error_panic_hook",
|
||||
"extension-trait",
|
||||
"eyeball-im",
|
||||
"futures-util",
|
||||
@@ -3135,7 +3147,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matrix-sdk-indexeddb"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_matches",
|
||||
@@ -3204,7 +3216,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matrix-sdk-qrcode"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"image",
|
||||
@@ -3216,7 +3228,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matrix-sdk-sqlite"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"assert_matches",
|
||||
@@ -3236,6 +3248,7 @@ dependencies = [
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"similar-asserts",
|
||||
"tempfile",
|
||||
"thiserror 2.0.11",
|
||||
@@ -3246,7 +3259,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matrix-sdk-store-encryption"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@@ -3266,7 +3279,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matrix-sdk-test"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"ctor",
|
||||
@@ -3288,7 +3301,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matrix-sdk-test-macros"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
@@ -3296,7 +3309,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matrix-sdk-ui"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"as_variant",
|
||||
@@ -3426,6 +3439,7 @@ dependencies = [
|
||||
"crossterm",
|
||||
"futures-util",
|
||||
"imbl",
|
||||
"indexmap",
|
||||
"itertools 0.14.0",
|
||||
"matrix-sdk",
|
||||
"matrix-sdk-base",
|
||||
@@ -4448,9 +4462,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma"
|
||||
version = "0.12.3"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d910a9b75cbf0e88f74295997c1a41c3ab7a117879a029c72db815192c167a0d"
|
||||
checksum = "c1d47e42b7dea75a468dea63a230f51331c58d690ca018ea1c6ac782ea98880c"
|
||||
dependencies = [
|
||||
"assign",
|
||||
"js_int",
|
||||
@@ -4465,9 +4479,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-client-api"
|
||||
version = "0.20.3"
|
||||
version = "0.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc4ff88a70a3d1e7a2c5b51cca7499cb889b42687608ab664b9a216c49314d"
|
||||
checksum = "3a9e9c613cfda4923b851c5d8bc442305905bee4f0c2b924564b00e71636c8d4"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"assign",
|
||||
@@ -4489,9 +4503,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-common"
|
||||
version = "0.15.2"
|
||||
version = "0.15.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b75da013b362664c3e161662902e5da3f77e990525681b59c6035bac27e87b4"
|
||||
checksum = "387e1898e868d32ff7b205e7db327361d5dcf635c00a8ae5865068607595a9cf"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"base64",
|
||||
@@ -4522,9 +4536,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-events"
|
||||
version = "0.30.3"
|
||||
version = "0.30.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41ab3d1b54c32a65194ecc44bc7f7575df50ef4255b139547d7dcc1753dc883d"
|
||||
checksum = "3cdc7abec9bc2a9ca0b4831cc26ce97a6a8c39a0bde44a19281a719e861b4293"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"indexmap",
|
||||
@@ -4548,9 +4562,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-federation-api"
|
||||
version = "0.11.1"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "373bc5a30b84574dfce3e75c33d79d6ba9843bf0eee1bf351f904eef9bea001a"
|
||||
checksum = "bb2a705c3911870782e036a3a8b676d0166c6c93800b84f6b8b23c981f78ef08"
|
||||
dependencies = [
|
||||
"http",
|
||||
"js_int",
|
||||
@@ -4586,9 +4600,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-macros"
|
||||
version = "0.15.1"
|
||||
version = "0.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1182e83ee5cd10121974f163337b16af68a93eedfc7cdbdbd52307ac7e1d743"
|
||||
checksum = "5ff13fbd6045a7278533390826de316d6116d8582ed828352661337b0c422e1c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"proc-macro-crate",
|
||||
@@ -4602,9 +4616,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.33.0"
|
||||
version = "0.35.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c6d5e5acb6f6129fe3f7ba0a7fc77bca1942cb568535e18e7bc40262baf3110"
|
||||
checksum = "a22715a5d6deef63c637207afbe68d0c72c3f8d0022d7cf9714c442d6157606b"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"fallible-iterator",
|
||||
@@ -4800,6 +4814,7 @@ dependencies = [
|
||||
"sentry-backtrace",
|
||||
"sentry-contexts",
|
||||
"sentry-core",
|
||||
"sentry-debug-images",
|
||||
"sentry-panic",
|
||||
"sentry-tracing",
|
||||
"tokio",
|
||||
@@ -4846,6 +4861,17 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-debug-images"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a60bc2154e6df59beed0ac13d58f8dfaf5ad20a88548a53e29e4d92e8e835c2"
|
||||
dependencies = [
|
||||
"findshlibs",
|
||||
"once_cell",
|
||||
"sentry-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-panic"
|
||||
version = "0.36.0"
|
||||
@@ -4952,9 +4978,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_path_to_error"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6"
|
||||
checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"serde",
|
||||
|
||||
+19
-15
@@ -60,24 +60,26 @@ reqwest = { version = "0.12.12", default-features = false }
|
||||
rmp-serde = "1.3.0"
|
||||
# Be careful to use commits from the https://github.com/ruma/ruma/tree/ruma-0.12
|
||||
# branch until a proper release with breaking changes happens.
|
||||
ruma = { version = "0.12.3", features = [
|
||||
ruma = { version = "0.12.5", features = [
|
||||
"client-api-c",
|
||||
"compat-upload-signatures",
|
||||
"compat-user-id",
|
||||
"compat-arbitrary-length-ids",
|
||||
"compat-tag-info",
|
||||
"compat-encrypted-stickers",
|
||||
"compat-lax-room-create-deser",
|
||||
"compat-lax-room-topic-deser",
|
||||
"unstable-msc3401",
|
||||
"unstable-msc3266",
|
||||
"unstable-msc3488",
|
||||
"unstable-msc3489",
|
||||
"unstable-msc4075",
|
||||
"unstable-msc4140",
|
||||
"unstable-msc4143",
|
||||
"unstable-msc4171",
|
||||
"unstable-msc4278",
|
||||
"unstable-msc4286",
|
||||
] }
|
||||
ruma-common = "0.15.2"
|
||||
] }
|
||||
ruma-common = "0.15.4"
|
||||
sentry = "0.36.0"
|
||||
sentry-tracing = "0.36.0"
|
||||
serde = { version = "1.0.217", features = ["rc"] }
|
||||
@@ -105,17 +107,17 @@ web-sys = "0.3.69"
|
||||
wiremock = "0.6.2"
|
||||
zeroize = "1.8.1"
|
||||
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.12.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.12.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.12.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.12.0" }
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.13.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.13.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.13.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.13.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.12.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.12.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.12.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.12.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.12.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.12.0", default-features = false }
|
||||
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.13.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.13.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.13.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.13.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.13.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.13.0", default-features = false }
|
||||
|
||||
[workspace.lints.rust]
|
||||
rust_2018_idioms = "warn"
|
||||
@@ -137,13 +139,15 @@ cloned_instead_of_copied = "warn"
|
||||
dbg_macro = "warn"
|
||||
inefficient_to_string = "warn"
|
||||
macro_use_imports = "warn"
|
||||
manual_let_else = "warn"
|
||||
mut_mut = "warn"
|
||||
needless_borrow = "warn"
|
||||
nonstandard_macro_braces = "warn"
|
||||
redundant_clone = "warn"
|
||||
str_to_string = "warn"
|
||||
todo = "warn"
|
||||
unnecessary_semicolon = "warn"
|
||||
unused_async = "warn"
|
||||
redundant_clone = "warn"
|
||||
|
||||
# Default development profile; default for most Cargo commands, otherwise
|
||||
# selected with `--debug`
|
||||
|
||||
@@ -1,38 +1,55 @@
|
||||
<h1 align="center">Matrix Rust SDK</h1>
|
||||
|
||||
<div align="center">
|
||||
<i>Your all-in-one toolkit for creating Matrix clients with Rust, from simple bots to full-featured apps.</i>
|
||||
<br/><br/>
|
||||
<img src="contrib/logo.svg">
|
||||
<br>
|
||||
<hr>
|
||||
<a href="https://github.com/matrix-org/matrix-rust-sdk/releases">
|
||||
<img src="https://img.shields.io/github/v/release/matrix-org/matrix-rust-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=GitHub&logoColor=white"></a>
|
||||
<a href="https://crates.io/crates/matrix-sdk/">
|
||||
<img src="https://img.shields.io/crates/v/matrix-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Rust&logoColor=white"></a>
|
||||
<a href="https://codecov.io/gh/matrix-org/matrix-rust-sdk">
|
||||
<img src="https://img.shields.io/codecov/c/gh/matrix-org/matrix-rust-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Codecov&logoColor=white"></a>
|
||||
<br>
|
||||
<a href="https://docs.rs/matrix-sdk/">
|
||||
<img src="https://img.shields.io/docsrs/matrix-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Rust&logoColor=white"></a>
|
||||
<a href="https://github.com/matrix-org/matrix-rust-sdk/actions/workflows/ci.yml">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/matrix-org/matrix-rust-sdk/ci.yml?style=flat&labelColor=1C2E27&color=66845F&logo=GitHub%20Actions&logoColor=white"></a>
|
||||
<br>
|
||||
<br>
|
||||
<em>Your all-in-one toolkit for creating Matrix clients with Rust, from simple bots to full-featured apps.</em>
|
||||
<br />
|
||||
<img src="contrib/logo.svg">
|
||||
<hr />
|
||||
<a href="https://github.com/matrix-org/matrix-rust-sdk/releases">
|
||||
<img src="https://img.shields.io/github/v/release/matrix-org/matrix-rust-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=GitHub&logoColor=white"></a>
|
||||
<a href="https://crates.io/crates/matrix-sdk/">
|
||||
<img src="https://img.shields.io/crates/v/matrix-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Rust&logoColor=white"></a>
|
||||
<a href="https://codecov.io/gh/matrix-org/matrix-rust-sdk">
|
||||
<img src="https://img.shields.io/codecov/c/gh/matrix-org/matrix-rust-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Codecov&logoColor=white"></a>
|
||||
<br />
|
||||
<a href="https://docs.rs/matrix-sdk/">
|
||||
<img src="https://img.shields.io/docsrs/matrix-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Rust&logoColor=white"></a>
|
||||
<a href="https://github.com/matrix-org/matrix-rust-sdk/actions/workflows/ci.yml">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/matrix-org/matrix-rust-sdk/ci.yml?style=flat&labelColor=1C2E27&color=66845F&logo=GitHub%20Actions&logoColor=white"></a>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
The Matrix Rust SDK is a collection of libraries that make it easier to build
|
||||
[Matrix] clients in [Rust]. It takes care of the low-level details like encryption,
|
||||
The Matrix Rust SDK is a collection of libraries that make it easier to build [Matrix] clients in [Rust].
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<picture>
|
||||
<source srcset="contrib/element-logo-light.png" media="(prefers-color-scheme: dark)">
|
||||
<source srcset="contrib/element-logo-dark.png" media="(prefers-color-scheme: light)">
|
||||
<img src="contrib/element-logo-fallback.png" alt="Element logo">
|
||||
</picture>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
Development of the SDK is proudly sponsored and maintained by [Element](https://element.io). Element uses the SDK in their next-generation mobile apps Element X on [iOS](https://github.com/element-hq/element-x-ios) and [Android](https://github.com/element-hq/element-x-android) and has plans to introduce it to the web and desktop clients as well.
|
||||
|
||||
The SDK is also the basis for multiple Matrix projects and we welcome contributions from all.
|
||||
|
||||
</div>
|
||||
|
||||
## Purpose
|
||||
|
||||
The SDK takes care of the low-level details like encryption,
|
||||
syncing, and room state, so you can focus on your app's logic and UI. Whether
|
||||
you're writing a small bot, a desktop client, or something in between, the SDK
|
||||
is designed to be flexible, async-friendly, and ready to use out of the box.
|
||||
|
||||
[Matrix]: https://matrix.org/
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
|
||||
## Project structure
|
||||
|
||||
The Matrix Rust SDK is made up of several crates that build on top of each other. Here are the key ones:
|
||||
The Matrix Rust SDK is made up of several crates that build on top of each
|
||||
other. The following crates are expected to be usable as direct dependencies:
|
||||
|
||||
- [matrix-sdk-ui](https://docs.rs/matrix-sdk-ui/latest/matrix_sdk_ui/) – A high-level client library that makes it easy to build
|
||||
full-featured UI clients with minimal setup. Check out our reference client,
|
||||
@@ -45,6 +62,9 @@ The Matrix Rust SDK is made up of several crates that build on top of each other
|
||||
See the [crypto tutorial](https://docs.rs/matrix-sdk-crypto/latest/matrix_sdk_crypto/tutorial/index.html)
|
||||
for a step-by-step introduction.
|
||||
|
||||
All other crates are effectively internal-only and only structured as crates
|
||||
for organizational purposes and to improve compilation times. Direct usage of them is discouraged.
|
||||
|
||||
## Status
|
||||
|
||||
The library is considered production ready and backs multiple client
|
||||
@@ -54,9 +74,6 @@ implementations such as Element X
|
||||
[Fractal](https://gitlab.gnome.org/World/fractal) and [iamb](https://github.com/ulyssa/iamb). Client developers should feel
|
||||
confident to build upon it.
|
||||
|
||||
Development of the SDK has been primarily sponsored by Element though accepts
|
||||
contributions from all.
|
||||
|
||||
## Bindings
|
||||
|
||||
The higher-level crates of the Matrix Rust SDK can be embedded in other
|
||||
@@ -67,3 +84,7 @@ into your language of choice.
|
||||
## License
|
||||
|
||||
[Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
|
||||
[Matrix]: https://matrix.org/
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
|
||||
@@ -4,6 +4,7 @@ use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Through
|
||||
use matrix_sdk::{store::RoomLoadSettings, test_utils::mocks::MatrixMockServer};
|
||||
use matrix_sdk_base::{
|
||||
store::StoreConfig, BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore,
|
||||
ThreadingSupport,
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteStateStore;
|
||||
use matrix_sdk_test::{event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent};
|
||||
@@ -58,6 +59,7 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
|
||||
let base_client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
|
||||
.state_store(sqlite_store),
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
runtime
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{collections::HashMap, iter, ops::DerefMut, sync::Arc};
|
||||
use hmac::Hmac;
|
||||
use matrix_sdk_crypto::{
|
||||
backups::DecryptionError,
|
||||
store::{BackupDecryptionKey, CryptoStoreError as InnerStoreError},
|
||||
store::{types::BackupDecryptionKey, CryptoStoreError as InnerStoreError},
|
||||
};
|
||||
use pbkdf2::pbkdf2;
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use std::{mem::ManuallyDrop, sync::Arc};
|
||||
|
||||
use matrix_sdk_common::executor::Handle;
|
||||
use matrix_sdk_crypto::{
|
||||
dehydrated_devices::{
|
||||
DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices,
|
||||
RehydratedDevice as InnerRehydratedDevice,
|
||||
},
|
||||
store::DehydratedDeviceKey as InnerDehydratedDeviceKey,
|
||||
store::types::DehydratedDeviceKey as InnerDehydratedDeviceKey,
|
||||
};
|
||||
use ruma::{api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw, OwnedDeviceId};
|
||||
use serde_json::json;
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
use crate::{CryptoStoreError, DehydratedDeviceKey};
|
||||
|
||||
|
||||
@@ -37,8 +37,11 @@ use matrix_sdk_common::deserialized_responses::{ShieldState as RustShieldState,
|
||||
use matrix_sdk_crypto::{
|
||||
olm::{IdentityKeys, InboundGroupSession, SenderData, Session},
|
||||
store::{
|
||||
Changes, CryptoStore, DehydratedDeviceKey as InnerDehydratedDeviceKey, PendingChanges,
|
||||
RoomSettings as RustRoomSettings,
|
||||
types::{
|
||||
Changes, DehydratedDeviceKey as InnerDehydratedDeviceKey, PendingChanges,
|
||||
RoomSettings as RustRoomSettings,
|
||||
},
|
||||
CryptoStore,
|
||||
},
|
||||
types::{
|
||||
DeviceKey, DeviceKeys, EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey,
|
||||
@@ -221,7 +224,7 @@ async fn migrate_data(
|
||||
passphrase: Option<String>,
|
||||
progress_listener: Box<dyn ProgressListener>,
|
||||
) -> anyhow::Result<()> {
|
||||
use matrix_sdk_crypto::{olm::PrivateCrossSigningIdentity, store::BackupDecryptionKey};
|
||||
use matrix_sdk_crypto::{olm::PrivateCrossSigningIdentity, store::types::BackupDecryptionKey};
|
||||
use vodozemac::olm::Account;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
@@ -818,10 +821,10 @@ impl BackupKeys {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<matrix_sdk_crypto::store::BackupKeys> for BackupKeys {
|
||||
impl TryFrom<matrix_sdk_crypto::store::types::BackupKeys> for BackupKeys {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(keys: matrix_sdk_crypto::store::BackupKeys) -> Result<Self, Self::Error> {
|
||||
fn try_from(keys: matrix_sdk_crypto::store::types::BackupKeys) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
recovery_key: BackupRecoveryKey {
|
||||
inner: keys.decryption_key.ok_or(())?,
|
||||
@@ -866,8 +869,8 @@ impl From<InnerDehydratedDeviceKey> for DehydratedDeviceKey {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::store::RoomKeyCounts> for RoomKeyCounts {
|
||||
fn from(count: matrix_sdk_crypto::store::RoomKeyCounts) -> Self {
|
||||
impl From<matrix_sdk_crypto::store::types::RoomKeyCounts> for RoomKeyCounts {
|
||||
fn from(count: matrix_sdk_crypto::store::types::RoomKeyCounts) -> Self {
|
||||
Self { total: count.total as i64, backed_up: count.backed_up as i64 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use matrix_sdk_crypto::{
|
||||
},
|
||||
decrypt_room_key_export, encrypt_room_key_export,
|
||||
olm::ExportedRoomKey,
|
||||
store::{BackupDecryptionKey, Changes},
|
||||
store::types::{BackupDecryptionKey, Changes},
|
||||
types::requests::ToDeviceRequest,
|
||||
DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, UserIdentity as SdkUserIdentity,
|
||||
};
|
||||
@@ -96,8 +96,8 @@ pub struct RoomKeyInfo {
|
||||
pub session_id: String,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::store::RoomKeyInfo> for RoomKeyInfo {
|
||||
fn from(value: matrix_sdk_crypto::store::RoomKeyInfo) -> Self {
|
||||
impl From<matrix_sdk_crypto::store::types::RoomKeyInfo> for RoomKeyInfo {
|
||||
fn from(value: matrix_sdk_crypto::store::types::RoomKeyInfo) -> Self {
|
||||
Self {
|
||||
algorithm: value.algorithm.to_string(),
|
||||
room_id: value.room_id.to_string(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::{Stream, StreamExt};
|
||||
use matrix_sdk_common::executor::Handle;
|
||||
use matrix_sdk_crypto::{
|
||||
matrix_sdk_qrcode::QrVerificationData, CancelInfo as RustCancelInfo, QrVerification as InnerQr,
|
||||
QrVerificationState, Sas as InnerSas, SasState as RustSasState,
|
||||
@@ -8,7 +9,6 @@ use matrix_sdk_crypto::{
|
||||
VerificationRequestState as RustVerificationRequestState,
|
||||
};
|
||||
use ruma::events::key::verification::VerificationMethod;
|
||||
use tokio::runtime::Handle;
|
||||
use vodozemac::{base64_decode, base64_encode};
|
||||
|
||||
use crate::{CryptoStoreError, OutgoingVerificationRequest, SignatureUploadRequest};
|
||||
|
||||
@@ -6,11 +6,53 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
## [0.13.0] - 2025-07-10
|
||||
|
||||
### Features
|
||||
|
||||
- Add `NotificationRoomInfo::topic` to the `NotificationRoomInfo` struct, which
|
||||
contains the topic of the room. This is useful for displaying the room topic
|
||||
in notifications. ([#5300](https://github.com/matrix-org/matrix-rust-sdk/pull/5300))
|
||||
- Add `EmbeddedEventDetails::timestamp` and `EmbeddedEventDetails::event_or_transaction_id`
|
||||
which are already available in regular timeline items.
|
||||
([#5331](https://github.com/matrix-org/matrix-rust-sdk/pull/5331))
|
||||
- `RoomListService::subscribe_to_rooms` becomes `async` and automatically calls
|
||||
`matrix_sdk::latest_events::LatestEvents::listen_to_room`
|
||||
([#5369](https://github.com/matrix-org/matrix-rust-sdk/pull/5369))
|
||||
|
||||
### Refactor
|
||||
|
||||
- Adjust features in the `matrix-sdk-ffi` crate to expose more platform-specific knobs.
|
||||
Previously the `matrix-sdk-ffi` was configured primarily by target configs, choosing
|
||||
between the tls flavor (`rustls-tls` or `native-tls`) and features like `sentry` based
|
||||
purely on the target. As we work to add an additional Wasm target to this crate,
|
||||
the cross product of target specific features has become somewhat chaotic, and we
|
||||
have shifted to externalize these choices as feature flags.
|
||||
|
||||
To maintain existing compatibility on the major platforms, these features should be used:
|
||||
Android: `"bundled-sqlite,unstable-msc4274,rustls-tls,sentry"`
|
||||
iOS: `"bundled-sqlite,unstable-msc4274,native-tls,sentry"`
|
||||
Javascript/Wasm: `"unstable-msc4274,native-tls"`
|
||||
|
||||
In the future additional choices (such as session storage, `sqlite` and `indexeddb`)
|
||||
will likely be added as well.
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- `Client::reset_server_capabilities` has been renamed to `Client::reset_server_info`.
|
||||
([#5167](https://github.com/matrix-org/matrix-rust-sdk/pull/5167))
|
||||
- `RoomPreview::join_rule`, `NotificationItem::join_rule`, `RoomInfo::is_public`, and
|
||||
`Room::is_public()` return values are now optional. They will be set to `None` if the join rule
|
||||
state event is missing for a given room. `NotificationRoomInfo::is_public` has been removed;
|
||||
callers can inspect the value of `NotificationItem::join_rule` to determine if the room is public
|
||||
(i.e. if the join rule is `Public`).
|
||||
([#5278](https://github.com/matrix-org/matrix-rust-sdk/pull/5278))
|
||||
|
||||
## [0.12.0] - 2025-06-10
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- `Client::send_call_notification_if_needed` now returns `Result<bool>` instead of `Result<()>` so we can check if
|
||||
- `Client::send_call_notification_if_needed` now returns `Result<bool>` instead of `Result<()>` so we can check if
|
||||
the event was sent.
|
||||
- `Client::upload_avatar` and `Timeline::send_attachment` now may fail if a file too large for the homeserver media
|
||||
config is uploaded.
|
||||
@@ -25,8 +67,8 @@ Breaking changes:
|
||||
|
||||
Additions:
|
||||
|
||||
- `Client::subscribe_to_room_info` allows clients to subscribe to room info updates in rooms which may not be known yet.
|
||||
This is useful when displaying a room preview for an unknown room, so when we receive any membership change for it,
|
||||
- `Client::subscribe_to_room_info` allows clients to subscribe to room info updates in rooms which may not be known yet.
|
||||
This is useful when displaying a room preview for an unknown room, so when we receive any membership change for it,
|
||||
we can automatically update the UI.
|
||||
- `Client::get_max_media_upload_size` to get the max size of a request sent to the homeserver so we can tweak our media
|
||||
uploads by compressing/transcoding the media.
|
||||
@@ -46,7 +88,7 @@ Additions:
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- `contacts` has been removed from `OidcConfiguration` (it was unused since the switch to OAuth).
|
||||
- `contacts` has been removed from `OidcConfiguration` (it was unused since the switch to OAuth).
|
||||
|
||||
## [0.11.0] - 2025-04-11
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "matrix-sdk-ffi"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ffi"]
|
||||
@@ -14,99 +14,84 @@ publish = false
|
||||
release = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "staticlib"]
|
||||
crate-type = [
|
||||
# Needed by uniffi for Android bindings
|
||||
"cdylib",
|
||||
# Needed by uniffi for iOS bindings
|
||||
"staticlib",
|
||||
# Needed by uniffi for JS/Wasm bindings, which use rust as an intermediate language
|
||||
"lib"
|
||||
]
|
||||
|
||||
[features]
|
||||
default = ["bundled-sqlite", "unstable-msc4274"]
|
||||
bundled-sqlite = ["matrix-sdk/bundled-sqlite"]
|
||||
unstable-msc4274 = ["matrix-sdk-ui/unstable-msc4274"]
|
||||
# Required when targeting a Javascript environment, like Wasm in a browser.
|
||||
js = ["matrix-sdk-ui/js"]
|
||||
# Use the TLS implementation provided by the host system, necessary on iOS and Wasm platforms.
|
||||
native-tls = ["matrix-sdk/native-tls", "sentry?/native-tls"]
|
||||
# Use Rustls as the TLS implementation, necessary on Android platforms.
|
||||
rustls-tls = ["matrix-sdk/rustls-tls", "sentry?/rustls"]
|
||||
# Enable sentry error monitoring, not compatible with Wasm platforms.
|
||||
sentry = ["dep:sentry", "dep:sentry-tracing"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
as_variant.workspace = true
|
||||
async-compat = "0.2.4"
|
||||
extension-trait = "1.0.1"
|
||||
eyeball-im.workspace = true
|
||||
futures-util.workspace = true
|
||||
language-tags = "0.3.2"
|
||||
log-panics = { version = "2", features = ["with-backtrace"] }
|
||||
matrix-sdk = { workspace = true, features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
"experimental-widgets",
|
||||
"markdown",
|
||||
"socks",
|
||||
"sqlite",
|
||||
"uniffi",
|
||||
] }
|
||||
matrix-sdk-common.workspace = true
|
||||
matrix-sdk-ffi-macros.workspace = true
|
||||
matrix-sdk-ui = { workspace = true, features = ["uniffi"] }
|
||||
mime = "0.3.16"
|
||||
once_cell.workspace = true
|
||||
ruma = { workspace = true, features = ["html", "unstable-unspecified", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat", "unstable-msc4278"] }
|
||||
sentry-tracing = "0.36.0"
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sentry = { version = "0.36.0", optional = true, default-features = false, features = [
|
||||
# Most default features enabled otherwise.
|
||||
"backtrace",
|
||||
"contexts",
|
||||
"panic",
|
||||
"reqwest",
|
||||
"sentry-debug-images",
|
||||
] }
|
||||
sentry-tracing = { version = "0.36.0", optional = true }
|
||||
thiserror.workspace = true
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
tracing.workspace = true
|
||||
tracing-appender = { version = "0.2.2" }
|
||||
tracing-core.workspace = true
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
uniffi = { workspace = true, features = ["tokio"] }
|
||||
url.workspace = true
|
||||
uuid = { version = "1.4.1", features = ["v4"] }
|
||||
zeroize.workspace = true
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies.matrix-sdk]
|
||||
workspace = true
|
||||
features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
"experimental-widgets",
|
||||
"markdown",
|
||||
# note: differ from block below
|
||||
"native-tls",
|
||||
"socks",
|
||||
"sqlite",
|
||||
"uniffi",
|
||||
]
|
||||
[target.'cfg(target_family = "wasm")'.dependencies]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
tokio = { workspace = true, features = ["sync", "macros"] }
|
||||
uniffi.workspace = true
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies.sentry]
|
||||
version = "0.36.0"
|
||||
default-features = false
|
||||
features = [
|
||||
# TLS lib used on non-Android platforms.
|
||||
"native-tls",
|
||||
# Most default features enabled otherwise.
|
||||
"backtrace",
|
||||
"contexts",
|
||||
"panic",
|
||||
"reqwest",
|
||||
]
|
||||
[target.'cfg(not(target_family = "wasm"))'.dependencies]
|
||||
async-compat.workspace = true
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
uniffi = { workspace = true, features = ["tokio"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
paranoid-android = "0.2.1"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies.matrix-sdk]
|
||||
workspace = true
|
||||
features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
"experimental-widgets",
|
||||
"markdown",
|
||||
# note: differ from block above
|
||||
"rustls-tls",
|
||||
"socks",
|
||||
"sqlite",
|
||||
"uniffi",
|
||||
]
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies.sentry]
|
||||
version = "0.36.0"
|
||||
default-features = false
|
||||
features = [
|
||||
# TLS lib specific for Android.
|
||||
"rustls",
|
||||
# Most default features enabled otherwise.
|
||||
"backtrace",
|
||||
"contexts",
|
||||
"panic",
|
||||
"reqwest",
|
||||
]
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
vergen = { version = "8.1.3", features = ["build", "git", "gitcl"] }
|
||||
|
||||
@@ -2,8 +2,28 @@
|
||||
|
||||
This uses [`uniffi`](https://mozilla.github.io/uniffi-rs/Overview.html) to build the matrix bindings for native support and wasm-bindgen for web-browser assembly support. Please refer to the specific section to figure out how to build and use the bindings for your platform.
|
||||
|
||||
## Features
|
||||
Given the number of platforms targeted, we have broken out a number of features
|
||||
|
||||
### Platform specific
|
||||
- `rustls-tls`: Use Rustls as the TLS implementation, necessary on Android platforms.
|
||||
- `native-tls`: Use the TLS implementation provided by the host system, necessary on iOS and Wasm platforms.
|
||||
|
||||
### Functionality
|
||||
- `sentry`: Enable error monitoring using Sentry, not supports on Wasm platforms.
|
||||
- `bundled-sqlite`: Use an embedded version of sqlite instead of the system provided one.
|
||||
|
||||
### Unstable specs
|
||||
- `unstable-msc4274`: Adds support for gallery message types, which contain multiple media elements.
|
||||
|
||||
## Platforms
|
||||
|
||||
Each supported target should use features to select the relevant TLS system. Here are some suggested feature flags for the major platforms:
|
||||
|
||||
- Android: `"bundled-sqlite,unstable-msc4274,rustls-tls,sentry"`
|
||||
- iOS: `"bundled-sqlite,unstable-msc4274,native-tls,sentry"`
|
||||
- Javascript/Wasm: `"unstable-msc4274,native-tls"`
|
||||
|
||||
### Swift/iOS sync
|
||||
|
||||
|
||||
|
||||
@@ -2,24 +2,26 @@ use std::{
|
||||
collections::HashMap,
|
||||
fmt::Debug,
|
||||
path::PathBuf,
|
||||
sync::{Arc, OnceLock, RwLock},
|
||||
sync::{Arc, OnceLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use futures_util::pin_mut;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use matrix_sdk::media::MediaFileHandle as SdkMediaFileHandle;
|
||||
use matrix_sdk::{
|
||||
authentication::oauth::{
|
||||
AccountManagementActionFull, ClientId, OAuthAuthorizationData, OAuthSession,
|
||||
},
|
||||
event_cache::EventCacheError,
|
||||
media::{
|
||||
MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequestParameters,
|
||||
MediaRetentionPolicy, MediaThumbnailSettings,
|
||||
},
|
||||
media::{MediaFormat, MediaRequestParameters, MediaRetentionPolicy, MediaThumbnailSettings},
|
||||
ruma::{
|
||||
api::client::{
|
||||
discovery::get_authorization_server_metadata::msc2965::Prompt as RumaOidcPrompt,
|
||||
discovery::{
|
||||
discover_homeserver::RtcFocusInfo,
|
||||
get_authorization_server_metadata::v1::Prompt as RumaOidcPrompt,
|
||||
},
|
||||
push::{EmailPusherData, PusherIds, PusherInit, PusherKind as RumaPusherKind},
|
||||
room::{create_room, Visibility},
|
||||
session::get_login_types,
|
||||
@@ -84,7 +86,10 @@ use tokio::sync::broadcast::error::RecvError;
|
||||
use tracing::{debug, error};
|
||||
use url::Url;
|
||||
|
||||
use super::{room::Room, session_verification::SessionVerificationController};
|
||||
use super::{
|
||||
room::{room_info::RoomInfo, Room},
|
||||
session_verification::SessionVerificationController,
|
||||
};
|
||||
use crate::{
|
||||
authentication::{HomeserverLoginDetails, OidcConfiguration, OidcError, SsoError, SsoHandler},
|
||||
client,
|
||||
@@ -93,7 +98,6 @@ use crate::{
|
||||
notification_settings::NotificationSettings,
|
||||
room::{RoomHistoryVisibility, RoomInfoListener},
|
||||
room_directory_search::RoomDirectorySearch,
|
||||
room_info::RoomInfo,
|
||||
room_preview::RoomPreview,
|
||||
ruma::{
|
||||
AccountDataEvent, AccountDataEventType, AuthData, InviteAvatars, MediaPreviewConfig,
|
||||
@@ -490,32 +494,6 @@ impl Client {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_media_file(
|
||||
&self,
|
||||
media_source: Arc<MediaSource>,
|
||||
filename: Option<String>,
|
||||
mime_type: String,
|
||||
use_cache: bool,
|
||||
temp_dir: Option<String>,
|
||||
) -> Result<Arc<MediaFileHandle>, ClientError> {
|
||||
let source = (*media_source).clone();
|
||||
let mime_type: mime::Mime = mime_type.parse()?;
|
||||
|
||||
let handle = self
|
||||
.inner
|
||||
.media()
|
||||
.get_media_file(
|
||||
&MediaRequestParameters { source: source.media_source, format: MediaFormat::File },
|
||||
filename,
|
||||
&mime_type,
|
||||
use_cache,
|
||||
temp_dir,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Arc::new(MediaFileHandle::new(handle)))
|
||||
}
|
||||
|
||||
/// Restores the client from a `Session`.
|
||||
///
|
||||
/// It reloads the entire set of rooms from the previous session.
|
||||
@@ -725,11 +703,44 @@ impl Client {
|
||||
|
||||
/// Empty the server version and unstable features cache.
|
||||
///
|
||||
/// Since the SDK caches server capabilities (versions and unstable
|
||||
/// features), it's possible to have a stale entry in the cache. This
|
||||
/// functions makes it possible to force reset it.
|
||||
pub async fn reset_server_capabilities(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.reset_server_capabilities().await?)
|
||||
/// Since the SDK caches server info (versions, unstable features,
|
||||
/// well-known etc), it's possible to have a stale entry in the cache.
|
||||
/// This functions makes it possible to force reset it.
|
||||
pub async fn reset_server_info(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.reset_server_info().await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Client {
|
||||
/// Retrieves a media file from the media source
|
||||
///
|
||||
/// Not available on Wasm platforms, due to lack of accessible file system.
|
||||
pub async fn get_media_file(
|
||||
&self,
|
||||
media_source: Arc<MediaSource>,
|
||||
filename: Option<String>,
|
||||
mime_type: String,
|
||||
use_cache: bool,
|
||||
temp_dir: Option<String>,
|
||||
) -> Result<Arc<MediaFileHandle>, ClientError> {
|
||||
let source = (*media_source).clone();
|
||||
let mime_type: mime::Mime = mime_type.parse()?;
|
||||
|
||||
let handle = self
|
||||
.inner
|
||||
.media()
|
||||
.get_media_file(
|
||||
&MediaRequestParameters { source: source.media_source, format: MediaFormat::File },
|
||||
filename,
|
||||
&mime_type,
|
||||
use_cache,
|
||||
temp_dir,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Arc::new(MediaFileHandle::new(handle)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1419,9 +1430,16 @@ impl Client {
|
||||
/// Clear all the non-critical caches for this Client instance.
|
||||
///
|
||||
/// WARNING: This will clear all the caches, including the base store (state
|
||||
/// store), so callers must make sure that any sync is inactive before
|
||||
/// calling this method. In particular, the `SyncService` must not be
|
||||
/// running. After the method returns, the Client will be in an unstable
|
||||
/// store), so callers must make sure that the Client is at rest before
|
||||
/// calling it.
|
||||
///
|
||||
/// In particular, if a [`SyncService`] is running, it must be passed here
|
||||
/// as a parameter, or stopped before calling this method. Ideally, the
|
||||
/// send queues should have been disabled and must all be inactive (i.e.
|
||||
/// not sending events); this method will disable them, but it might not
|
||||
/// be enough if the queues are still processing events.
|
||||
///
|
||||
/// After the method returns, the Client will be in an unstable
|
||||
/// state, and it is required that the caller reinstantiates a new
|
||||
/// Client instance, be it via dropping the previous and re-creating it,
|
||||
/// restarting their application, or any other similar means.
|
||||
@@ -1431,8 +1449,23 @@ impl Client {
|
||||
/// will start as if they were empty.
|
||||
/// - This will empty the media cache according to the current media
|
||||
/// retention policy.
|
||||
pub async fn clear_caches(&self) -> Result<(), ClientError> {
|
||||
pub async fn clear_caches(
|
||||
&self,
|
||||
sync_service: Option<Arc<SyncService>>,
|
||||
) -> Result<(), ClientError> {
|
||||
let closure = async || -> Result<_, ClientError> {
|
||||
// First, make sure to expire sessions in the sync service.
|
||||
if let Some(sync_service) = sync_service {
|
||||
sync_service.inner.expire_sessions().await;
|
||||
}
|
||||
|
||||
// Disable the send queues, as they might read and write to the state store.
|
||||
// Events being send might still be active, and cause errors if
|
||||
// processing finishes, so this will only minimize damage. Since
|
||||
// this method should only be called in exceptional cases, this has
|
||||
// been deemed acceptable.
|
||||
self.inner.send_queue().set_enabled(false).await;
|
||||
|
||||
// Clean up the media cache according to the current media retention policy.
|
||||
self.inner
|
||||
.event_cache_store()
|
||||
@@ -1489,6 +1522,16 @@ impl Client {
|
||||
Ok(self.inner.server_versions().await?.contains(&ruma::api::MatrixVersion::V1_13))
|
||||
}
|
||||
|
||||
/// Checks if the server supports the LiveKit RTC focus for placing calls.
|
||||
pub async fn is_livekit_rtc_supported(&self) -> Result<bool, ClientError> {
|
||||
Ok(self
|
||||
.inner
|
||||
.rtc_foci()
|
||||
.await?
|
||||
.iter()
|
||||
.any(|focus| matches!(focus, RtcFocusInfo::LiveKit(_))))
|
||||
}
|
||||
|
||||
/// Subscribe to changes in the media preview configuration.
|
||||
pub async fn subscribe_to_media_preview_config(
|
||||
&self,
|
||||
@@ -2153,17 +2196,20 @@ fn gen_transaction_id() -> String {
|
||||
|
||||
/// A file handle that takes ownership of a media file on disk. When the handle
|
||||
/// is dropped, the file will be removed from the disk.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct MediaFileHandle {
|
||||
inner: RwLock<Option<SdkMediaFileHandle>>,
|
||||
inner: std::sync::RwLock<Option<SdkMediaFileHandle>>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
impl MediaFileHandle {
|
||||
fn new(handle: SdkMediaFileHandle) -> Self {
|
||||
Self { inner: RwLock::new(Some(handle)) }
|
||||
Self { inner: std::sync::RwLock::new(Some(handle)) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl MediaFileHandle {
|
||||
/// Get the media file's path.
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
use std::{fs, num::NonZeroUsize, path::Path, sync::Arc, time::Duration};
|
||||
|
||||
use futures_util::StreamExt;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use matrix_sdk::reqwest::Certificate;
|
||||
use matrix_sdk::{
|
||||
authentication::oauth::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason},
|
||||
crypto::{
|
||||
types::qr_login::{LoginQrCodeDecodeError, QrCodeModeData},
|
||||
CollectStrategy, TrustRequirement,
|
||||
types::qr_login::QrCodeModeData, CollectStrategy, DecryptionSettings, TrustRequirement,
|
||||
},
|
||||
encryption::{BackupDownloadStrategy, EncryptionSettings},
|
||||
event_cache::EventCacheError,
|
||||
reqwest::Certificate,
|
||||
ruma::{ServerName, UserId},
|
||||
sliding_sync::{
|
||||
Error as MatrixSlidingSyncError, VersionBuilder as MatrixSlidingSyncVersionBuilder,
|
||||
VersionBuilderError,
|
||||
},
|
||||
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
|
||||
RumaApiError, SqliteStoreConfig,
|
||||
RumaApiError, SqliteStoreConfig, ThreadingSupport,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::api::error::{DeserializationError, FromHttpResponseError};
|
||||
use tracing::{debug, error};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use super::client::Client;
|
||||
use crate::{
|
||||
authentication::OidcConfiguration, client::ClientSessionDelegate, error::ClientError,
|
||||
helpers::unwrap_or_clone_arc, runtime::get_runtime_handle, task_handle::TaskHandle,
|
||||
authentication::OidcConfiguration,
|
||||
client::ClientSessionDelegate,
|
||||
error::ClientError,
|
||||
helpers::unwrap_or_clone_arc,
|
||||
qr_code::{HumanQrLoginError, QrCodeData, QrLoginProgressListener},
|
||||
runtime::get_runtime_handle,
|
||||
task_handle::TaskHandle,
|
||||
};
|
||||
|
||||
/// A list of bytes containing a certificate in DER or PEM form.
|
||||
@@ -39,164 +42,6 @@ enum HomeserverConfig {
|
||||
ServerNameOrUrl(String),
|
||||
}
|
||||
|
||||
/// Data for the QR code login mechanism.
|
||||
///
|
||||
/// The [`QrCodeData`] can be serialized and encoded as a QR code or it can be
|
||||
/// decoded from a QR code.
|
||||
#[derive(Debug, uniffi::Object)]
|
||||
pub struct QrCodeData {
|
||||
inner: qrcode::QrCodeData,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl QrCodeData {
|
||||
/// Attempt to decode a slice of bytes into a [`QrCodeData`] object.
|
||||
///
|
||||
/// The slice of bytes would generally be returned by a QR code decoder.
|
||||
#[uniffi::constructor]
|
||||
pub fn from_bytes(bytes: Vec<u8>) -> Result<Arc<Self>, QrCodeDecodeError> {
|
||||
Ok(Self { inner: qrcode::QrCodeData::from_bytes(&bytes)? }.into())
|
||||
}
|
||||
|
||||
/// The server name contained within the scanned QR code data.
|
||||
///
|
||||
/// Note: This value is only present when scanning a QR code the belongs to
|
||||
/// a logged in client. The mode where the new client shows the QR code
|
||||
/// will return `None`.
|
||||
pub fn server_name(&self) -> Option<String> {
|
||||
match &self.inner.mode_data {
|
||||
QrCodeModeData::Reciprocate { server_name } => Some(server_name.to_owned()),
|
||||
QrCodeModeData::Login => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for the decoding of the [`QrCodeData`].
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum QrCodeDecodeError {
|
||||
#[error("Error decoding QR code: {error:?}")]
|
||||
Crypto {
|
||||
#[from]
|
||||
error: LoginQrCodeDecodeError,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
pub enum HumanQrLoginError {
|
||||
#[error("Linking with this device is not supported.")]
|
||||
LinkingNotSupported,
|
||||
#[error("The sign in was cancelled.")]
|
||||
Cancelled,
|
||||
#[error("The sign in was not completed in the required time.")]
|
||||
Expired,
|
||||
#[error("A secure connection could not have been established between the two devices.")]
|
||||
ConnectionInsecure,
|
||||
#[error("The sign in was declined.")]
|
||||
Declined,
|
||||
#[error("An unknown error has happened.")]
|
||||
Unknown,
|
||||
#[error("The homeserver doesn't provide sliding sync in its configuration.")]
|
||||
SlidingSyncNotAvailable,
|
||||
#[error("Unable to use OIDC as the supplied client metadata is invalid.")]
|
||||
OidcMetadataInvalid,
|
||||
#[error("The other device is not signed in and as such can't sign in other devices.")]
|
||||
OtherDeviceNotSignedIn,
|
||||
}
|
||||
|
||||
impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
|
||||
fn from(value: qrcode::QRCodeLoginError) -> Self {
|
||||
use qrcode::{QRCodeLoginError, SecureChannelError};
|
||||
|
||||
match value {
|
||||
QRCodeLoginError::LoginFailure { reason, .. } => match reason {
|
||||
LoginFailureReason::UnsupportedProtocol => HumanQrLoginError::LinkingNotSupported,
|
||||
LoginFailureReason::AuthorizationExpired => HumanQrLoginError::Expired,
|
||||
LoginFailureReason::UserCancelled => HumanQrLoginError::Cancelled,
|
||||
_ => HumanQrLoginError::Unknown,
|
||||
},
|
||||
|
||||
QRCodeLoginError::OAuth(e) => {
|
||||
if let Some(e) = e.as_request_token_error() {
|
||||
match e {
|
||||
DeviceCodeErrorResponseType::AccessDenied => HumanQrLoginError::Declined,
|
||||
DeviceCodeErrorResponseType::ExpiredToken => HumanQrLoginError::Expired,
|
||||
_ => HumanQrLoginError::Unknown,
|
||||
}
|
||||
} else {
|
||||
HumanQrLoginError::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
QRCodeLoginError::SecureChannel(e) => match e {
|
||||
SecureChannelError::Utf8(_)
|
||||
| SecureChannelError::MessageDecode(_)
|
||||
| SecureChannelError::Json(_)
|
||||
| SecureChannelError::RendezvousChannel(_) => HumanQrLoginError::Unknown,
|
||||
SecureChannelError::SecureChannelMessage { .. }
|
||||
| SecureChannelError::Ecies(_)
|
||||
| SecureChannelError::InvalidCheckCode => HumanQrLoginError::ConnectionInsecure,
|
||||
SecureChannelError::InvalidIntent => HumanQrLoginError::OtherDeviceNotSignedIn,
|
||||
},
|
||||
|
||||
QRCodeLoginError::UnexpectedMessage { .. }
|
||||
| QRCodeLoginError::CrossProcessRefreshLock(_)
|
||||
| QRCodeLoginError::DeviceKeyUpload(_)
|
||||
| QRCodeLoginError::SessionTokens(_)
|
||||
| QRCodeLoginError::UserIdDiscovery(_)
|
||||
| QRCodeLoginError::SecretImport(_) => HumanQrLoginError::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum describing the progress of the QR-code login.
|
||||
#[derive(Debug, Default, Clone, uniffi::Enum)]
|
||||
pub enum QrLoginProgress {
|
||||
/// The login process is starting.
|
||||
#[default]
|
||||
Starting,
|
||||
/// We established a secure channel with the other device.
|
||||
EstablishingSecureChannel {
|
||||
/// The check code that the device should display so the other device
|
||||
/// can confirm that the channel is secure as well.
|
||||
check_code: u8,
|
||||
/// The string representation of the check code, will be guaranteed to
|
||||
/// be 2 characters long, preserving the leading zero if the
|
||||
/// first digit is a zero.
|
||||
check_code_string: String,
|
||||
},
|
||||
/// We are waiting for the login and for the OAuth 2.0 authorization server
|
||||
/// to give us an access token.
|
||||
WaitingForToken { user_code: String },
|
||||
/// The login has successfully finished.
|
||||
Done,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait QrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, state: QrLoginProgress);
|
||||
}
|
||||
|
||||
impl From<qrcode::LoginProgress> for QrLoginProgress {
|
||||
fn from(value: qrcode::LoginProgress) -> Self {
|
||||
use qrcode::LoginProgress;
|
||||
|
||||
match value {
|
||||
LoginProgress::Starting => Self::Starting,
|
||||
LoginProgress::EstablishingSecureChannel { check_code } => {
|
||||
let check_code = check_code.to_digit();
|
||||
|
||||
Self::EstablishingSecureChannel {
|
||||
check_code,
|
||||
check_code_string: format!("{check_code:02}"),
|
||||
}
|
||||
}
|
||||
LoginProgress::WaitingForToken { user_code } => Self::WaitingForToken { user_code },
|
||||
LoginProgress::Done => Self::Done,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum ClientBuildError {
|
||||
@@ -274,21 +119,29 @@ pub struct ClientBuilder {
|
||||
system_is_memory_constrained: bool,
|
||||
username: Option<String>,
|
||||
homeserver_cfg: Option<HomeserverConfig>,
|
||||
user_agent: Option<String>,
|
||||
sliding_sync_version_builder: SlidingSyncVersionBuilder,
|
||||
proxy: Option<String>,
|
||||
disable_ssl_verification: bool,
|
||||
disable_automatic_token_refresh: bool,
|
||||
cross_process_store_locks_holder_name: Option<String>,
|
||||
enable_oidc_refresh_lock: bool,
|
||||
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
|
||||
additional_root_certificates: Vec<Vec<u8>>,
|
||||
disable_built_in_root_certificates: bool,
|
||||
encryption_settings: EncryptionSettings,
|
||||
room_key_recipient_strategy: CollectStrategy,
|
||||
decryption_trust_requirement: TrustRequirement,
|
||||
decryption_settings: DecryptionSettings,
|
||||
enable_share_history_on_invite: bool,
|
||||
request_config: Option<RequestConfig>,
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
user_agent: Option<String>,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
proxy: Option<String>,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
disable_ssl_verification: bool,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
disable_built_in_root_certificates: bool,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
additional_root_certificates: Vec<Vec<u8>>,
|
||||
|
||||
threads_enabled: bool,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
@@ -321,9 +174,12 @@ impl ClientBuilder {
|
||||
auto_enable_backups: false,
|
||||
},
|
||||
room_key_recipient_strategy: Default::default(),
|
||||
decryption_trust_requirement: TrustRequirement::Untrusted,
|
||||
decryption_settings: DecryptionSettings {
|
||||
sender_device_trust_requirement: TrustRequirement::Untrusted,
|
||||
},
|
||||
enable_share_history_on_invite: false,
|
||||
request_config: Default::default(),
|
||||
threads_enabled: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -455,12 +311,6 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn user_agent(self: Arc<Self>, user_agent: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.user_agent = Some(user_agent);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn sliding_sync_version_builder(
|
||||
self: Arc<Self>,
|
||||
version_builder: SlidingSyncVersionBuilder,
|
||||
@@ -470,43 +320,12 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn proxy(self: Arc<Self>, url: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.proxy = Some(url);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn disable_ssl_verification(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.disable_ssl_verification = true;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn disable_automatic_token_refresh(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.disable_automatic_token_refresh = true;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn add_root_certificates(
|
||||
self: Arc<Self>,
|
||||
certificates: Vec<CertificateBytes>,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.additional_root_certificates = certificates;
|
||||
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Don't trust any system root certificates, only trust the certificates
|
||||
/// provided through
|
||||
/// [`add_root_certificates`][ClientBuilder::add_root_certificates].
|
||||
pub fn disable_built_in_root_certificates(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.disable_built_in_root_certificates = true;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn auto_enable_cross_signing(
|
||||
self: Arc<Self>,
|
||||
auto_enable_cross_signing: bool,
|
||||
@@ -545,12 +364,12 @@ impl ClientBuilder {
|
||||
}
|
||||
|
||||
/// Set the trust requirement to be used when decrypting events.
|
||||
pub fn room_decryption_trust_requirement(
|
||||
pub fn decryption_settings(
|
||||
self: Arc<Self>,
|
||||
trust_requirement: TrustRequirement,
|
||||
decryption_settings: DecryptionSettings,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.decryption_trust_requirement = trust_requirement;
|
||||
builder.decryption_settings = decryption_settings;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
@@ -574,6 +393,12 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn threads_enabled(self: Arc<Self>, enabled: bool) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.threads_enabled = enabled;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub async fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientBuildError> {
|
||||
let builder = unwrap_or_clone_arc(self);
|
||||
let mut inner_builder = MatrixClient::builder();
|
||||
@@ -650,52 +475,55 @@ impl ClientBuilder {
|
||||
}
|
||||
};
|
||||
|
||||
let mut certificates = Vec::new();
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
let mut certificates = Vec::new();
|
||||
|
||||
for certificate in builder.additional_root_certificates {
|
||||
// We don't really know what type of certificate we may get here, so let's try
|
||||
// first one type, then the other.
|
||||
match Certificate::from_der(&certificate) {
|
||||
Ok(cert) => {
|
||||
certificates.push(cert);
|
||||
}
|
||||
Err(der_error) => {
|
||||
let cert = Certificate::from_pem(&certificate).map_err(|pem_error| {
|
||||
ClientBuildError::Generic {
|
||||
message: format!("Failed to add a root certificate as DER ({der_error:?}) or PEM ({pem_error:?})"),
|
||||
}
|
||||
})?;
|
||||
certificates.push(cert);
|
||||
for certificate in builder.additional_root_certificates {
|
||||
// We don't really know what type of certificate we may get here, so let's try
|
||||
// first one type, then the other.
|
||||
match Certificate::from_der(&certificate) {
|
||||
Ok(cert) => {
|
||||
certificates.push(cert);
|
||||
}
|
||||
Err(der_error) => {
|
||||
let cert = Certificate::from_pem(&certificate).map_err(|pem_error| {
|
||||
ClientBuildError::Generic {
|
||||
message: format!("Failed to add a root certificate as DER ({der_error:?}) or PEM ({pem_error:?})"),
|
||||
}
|
||||
})?;
|
||||
certificates.push(cert);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner_builder = inner_builder.add_root_certificates(certificates);
|
||||
inner_builder = inner_builder.add_root_certificates(certificates);
|
||||
|
||||
if builder.disable_built_in_root_certificates {
|
||||
inner_builder = inner_builder.disable_built_in_root_certificates();
|
||||
}
|
||||
if builder.disable_built_in_root_certificates {
|
||||
inner_builder = inner_builder.disable_built_in_root_certificates();
|
||||
}
|
||||
|
||||
if let Some(proxy) = builder.proxy {
|
||||
inner_builder = inner_builder.proxy(proxy);
|
||||
}
|
||||
if let Some(proxy) = builder.proxy {
|
||||
inner_builder = inner_builder.proxy(proxy);
|
||||
}
|
||||
|
||||
if builder.disable_ssl_verification {
|
||||
inner_builder = inner_builder.disable_ssl_verification();
|
||||
if builder.disable_ssl_verification {
|
||||
inner_builder = inner_builder.disable_ssl_verification();
|
||||
}
|
||||
|
||||
if let Some(user_agent) = builder.user_agent {
|
||||
inner_builder = inner_builder.user_agent(user_agent);
|
||||
}
|
||||
}
|
||||
|
||||
if !builder.disable_automatic_token_refresh {
|
||||
inner_builder = inner_builder.handle_refresh_tokens();
|
||||
}
|
||||
|
||||
if let Some(user_agent) = builder.user_agent {
|
||||
inner_builder = inner_builder.user_agent(user_agent);
|
||||
}
|
||||
|
||||
inner_builder = inner_builder
|
||||
.with_encryption_settings(builder.encryption_settings)
|
||||
.with_room_key_recipient_strategy(builder.room_key_recipient_strategy)
|
||||
.with_decryption_trust_requirement(builder.decryption_trust_requirement)
|
||||
.with_decryption_settings(builder.decryption_settings)
|
||||
.with_enable_share_history_on_invite(builder.enable_share_history_on_invite);
|
||||
|
||||
match builder.sliding_sync_version_builder {
|
||||
@@ -736,6 +564,12 @@ impl ClientBuilder {
|
||||
inner_builder = inner_builder.request_config(updated_config);
|
||||
}
|
||||
|
||||
inner_builder = inner_builder.with_threading_support(if builder.threads_enabled {
|
||||
ThreadingSupport::Enabled
|
||||
} else {
|
||||
ThreadingSupport::Disabled
|
||||
});
|
||||
|
||||
let sdk_client = inner_builder.build().await?;
|
||||
|
||||
Ok(Arc::new(
|
||||
@@ -804,6 +638,47 @@ impl ClientBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl ClientBuilder {
|
||||
pub fn proxy(self: Arc<Self>, url: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.proxy = Some(url);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn disable_ssl_verification(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.disable_ssl_verification = true;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn add_root_certificates(
|
||||
self: Arc<Self>,
|
||||
certificates: Vec<CertificateBytes>,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.additional_root_certificates = certificates;
|
||||
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Don't trust any system root certificates, only trust the certificates
|
||||
/// provided through
|
||||
/// [`add_root_certificates`][ClientBuilder::add_root_certificates].
|
||||
pub fn disable_built_in_root_certificates(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.disable_built_in_root_certificates = true;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn user_agent(self: Arc<Self>, user_agent: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.user_agent = Some(user_agent);
|
||||
Arc::new(builder)
|
||||
}
|
||||
}
|
||||
|
||||
/// The store paths the client will use when built.
|
||||
#[derive(Clone)]
|
||||
struct SessionPaths {
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::ClientError;
|
||||
|
||||
/// Well-known settings specific to ElementCall
|
||||
#[derive(Deserialize, uniffi::Record)]
|
||||
pub struct ElementCallWellKnown {
|
||||
widget_url: String,
|
||||
}
|
||||
|
||||
/// Element specific well-known settings
|
||||
#[derive(Deserialize, uniffi::Record)]
|
||||
pub struct ElementWellKnown {
|
||||
call: Option<ElementCallWellKnown>,
|
||||
registration_helper_url: Option<String>,
|
||||
}
|
||||
|
||||
/// Helper function to parse a string into a ElementWellKnown struct
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn make_element_well_known(string: String) -> Result<ElementWellKnown, ClientError> {
|
||||
serde_json::from_str(&string).map_err(ClientError::from_err)
|
||||
}
|
||||
@@ -442,7 +442,7 @@ impl Encryption {
|
||||
Err(error) => {
|
||||
error!("Failed fetching identity from the store: {error}");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
info!("Requesting identity from the server.");
|
||||
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
use std::{collections::HashMap, error::Error, fmt, fmt::Display, time::SystemTime};
|
||||
use std::{collections::HashMap, error::Error, fmt, fmt::Display};
|
||||
|
||||
use matrix_sdk::{
|
||||
authentication::oauth::OAuthError, encryption::CryptoStoreError, event_cache::EventCacheError,
|
||||
reqwest, room::edit::EditError, send_queue::RoomSendQueueError, HttpError, IdParseError,
|
||||
NotificationSettingsError as SdkNotificationSettingsError,
|
||||
authentication::oauth::OAuthError,
|
||||
encryption::{identities::RequestVerificationError, CryptoStoreError},
|
||||
event_cache::EventCacheError,
|
||||
reqwest,
|
||||
room::edit::EditError,
|
||||
send_queue::RoomSendQueueError,
|
||||
HttpError, IdParseError, NotificationSettingsError as SdkNotificationSettingsError,
|
||||
QueueWedgeError as SdkQueueWedgeError, StoreError,
|
||||
};
|
||||
use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline};
|
||||
use ruma::api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter};
|
||||
use ruma::{
|
||||
api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter},
|
||||
MilliSecondsSinceUnixEpoch,
|
||||
};
|
||||
use tracing::warn;
|
||||
use uniffi::UnexpectedUniFFICallbackError;
|
||||
|
||||
@@ -198,6 +205,12 @@ impl From<FocusEventError> for ClientError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RequestVerificationError> for ClientError {
|
||||
fn from(e: RequestVerificationError) -> Self {
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// Bindings version of the sdk type replacing OwnedUserId/DeviceIds with simple
|
||||
/// String.
|
||||
///
|
||||
@@ -749,7 +762,9 @@ impl TryFrom<RumaApiErrorKind> for ErrorKind {
|
||||
let retry_after_ms = match retry_after {
|
||||
Some(RetryAfter::Delay(duration)) => Some(duration.as_millis() as u64),
|
||||
Some(RetryAfter::DateTime(system_time)) => {
|
||||
let duration = system_time.duration_since(SystemTime::now()).ok();
|
||||
let duration = MilliSecondsSinceUnixEpoch::now()
|
||||
.to_system_time()
|
||||
.and_then(|now| system_time.duration_since(now).ok());
|
||||
duration.map(|duration| duration.as_millis() as u64)
|
||||
}
|
||||
None => None,
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
// 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.
|
||||
// Needed because uniffi macros contain empty lines after docs.
|
||||
#![allow(clippy::empty_line_after_doc_comments)]
|
||||
|
||||
mod authentication;
|
||||
mod chunk_iterator;
|
||||
mod client;
|
||||
mod client_builder;
|
||||
mod element;
|
||||
mod encryption;
|
||||
mod error;
|
||||
mod event;
|
||||
@@ -18,10 +15,10 @@ mod live_location_share;
|
||||
mod notification;
|
||||
mod notification_settings;
|
||||
mod platform;
|
||||
mod qr_code;
|
||||
mod room;
|
||||
mod room_alias;
|
||||
mod room_directory_search;
|
||||
mod room_info;
|
||||
mod room_list;
|
||||
mod room_member;
|
||||
mod room_preview;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use matrix_sdk_ui::notification_client::{
|
||||
NotificationClient as MatrixNotificationClient, NotificationItem as MatrixNotificationItem,
|
||||
NotificationClient as SdkNotificationClient, NotificationEvent as SdkNotificationEvent,
|
||||
NotificationItem as SdkNotificationItem, NotificationStatus as SdkNotificationStatus,
|
||||
};
|
||||
use ruma::{EventId, OwnedEventId, OwnedRoomId, RoomId};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
client::{Client, JoinRule},
|
||||
@@ -31,11 +31,11 @@ pub struct NotificationRoomInfo {
|
||||
pub display_name: String,
|
||||
pub avatar_url: Option<String>,
|
||||
pub canonical_alias: Option<String>,
|
||||
pub topic: Option<String>,
|
||||
pub join_rule: Option<JoinRule>,
|
||||
pub joined_members_count: u64,
|
||||
pub is_encrypted: Option<bool>,
|
||||
pub is_direct: bool,
|
||||
pub is_public: bool,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
@@ -54,12 +54,12 @@ pub struct NotificationItem {
|
||||
}
|
||||
|
||||
impl NotificationItem {
|
||||
fn from_inner(item: MatrixNotificationItem) -> Self {
|
||||
fn from_inner(item: SdkNotificationItem) -> Self {
|
||||
let event = match item.event {
|
||||
matrix_sdk_ui::notification_client::NotificationEvent::Timeline(event) => {
|
||||
SdkNotificationEvent::Timeline(event) => {
|
||||
NotificationEvent::Timeline { event: Arc::new(TimelineEvent(event)) }
|
||||
}
|
||||
matrix_sdk_ui::notification_client::NotificationEvent::Invite(event) => {
|
||||
SdkNotificationEvent::Invite(event) => {
|
||||
NotificationEvent::Invite { sender: event.sender.to_string() }
|
||||
}
|
||||
};
|
||||
@@ -74,11 +74,11 @@ impl NotificationItem {
|
||||
display_name: item.room_computed_display_name,
|
||||
avatar_url: item.room_avatar_url,
|
||||
canonical_alias: item.room_canonical_alias,
|
||||
join_rule: item.room_join_rule.try_into().ok(),
|
||||
topic: item.room_topic,
|
||||
join_rule: item.room_join_rule.map(TryInto::try_into).transpose().ok().flatten(),
|
||||
joined_members_count: item.joined_members_count,
|
||||
is_encrypted: item.is_room_encrypted,
|
||||
is_direct: item.is_direct_message_room,
|
||||
is_public: item.is_room_public,
|
||||
},
|
||||
is_noisy: item.is_noisy,
|
||||
has_mention: item.has_mention,
|
||||
@@ -87,9 +87,46 @@ impl NotificationItem {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum NotificationStatus {
|
||||
/// The event has been found and was not filtered out.
|
||||
Event { item: NotificationItem },
|
||||
/// The event couldn't be found in the network queries used to find it.
|
||||
EventNotFound,
|
||||
/// The event has been filtered out, either because of the user's push
|
||||
/// rules, or because the user which triggered it is ignored by the
|
||||
/// current user.
|
||||
EventFilteredOut,
|
||||
}
|
||||
|
||||
impl From<SdkNotificationStatus> for NotificationStatus {
|
||||
fn from(item: SdkNotificationStatus) -> Self {
|
||||
match item {
|
||||
SdkNotificationStatus::Event(item) => {
|
||||
NotificationStatus::Event { item: NotificationItem::from_inner(*item) }
|
||||
}
|
||||
SdkNotificationStatus::EventNotFound => NotificationStatus::EventNotFound,
|
||||
SdkNotificationStatus::EventFilteredOut => NotificationStatus::EventFilteredOut,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum BatchNotificationResult {
|
||||
/// We have more detailed information about the notification.
|
||||
Ok { status: NotificationStatus },
|
||||
/// An error occurred while trying to fetch the notification.
|
||||
Error {
|
||||
/// The error message observed while handling a specific notification.
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct NotificationClient {
|
||||
pub(crate) inner: MatrixNotificationClient,
|
||||
pub(crate) inner: SdkNotificationClient,
|
||||
|
||||
/// A reference to the FFI client.
|
||||
///
|
||||
@@ -113,55 +150,54 @@ impl NotificationClient {
|
||||
Ok(room)
|
||||
}
|
||||
|
||||
/// See also documentation of
|
||||
/// `MatrixNotificationClient::get_notification`.
|
||||
/// Fetches the content of a notification.
|
||||
///
|
||||
/// This will first try to get the notification using a short-lived sliding
|
||||
/// sync, and if the sliding-sync can't find the event, then it'll use a
|
||||
/// `/context` query to find the event with associated member information.
|
||||
///
|
||||
/// An error result means that we couldn't resolve the notification; in that
|
||||
/// case, a dummy notification may be displayed instead.
|
||||
pub async fn get_notification(
|
||||
&self,
|
||||
room_id: String,
|
||||
event_id: String,
|
||||
) -> Result<Option<NotificationItem>, ClientError> {
|
||||
) -> Result<NotificationStatus, ClientError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
|
||||
let item =
|
||||
self.inner.get_notification(&room_id, &event_id).await.map_err(ClientError::from)?;
|
||||
|
||||
if let Some(item) = item {
|
||||
Ok(Some(NotificationItem::from_inner(item)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
Ok(item.into())
|
||||
}
|
||||
|
||||
/// Get several notification items in a single batch.
|
||||
///
|
||||
/// Returns an error if the flow failed when preparing to fetch the
|
||||
/// notifications, and a [`HashMap`] containing either a
|
||||
/// [`NotificationItem`] or no entry for it if it failed to fetch a
|
||||
/// notification for the provided [`EventId`].
|
||||
/// [`BatchNotificationResult`], that indicates if the notification was
|
||||
/// successfully fetched (in which case, it's a [`NotificationStatus`]), or
|
||||
/// an error message if it couldn't be fetched.
|
||||
pub async fn get_notifications(
|
||||
&self,
|
||||
requests: Vec<NotificationItemsRequest>,
|
||||
) -> Result<HashMap<String, NotificationItem>, ClientError> {
|
||||
) -> Result<HashMap<String, BatchNotificationResult>, ClientError> {
|
||||
let requests =
|
||||
requests.into_iter().map(TryInto::try_into).collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let items = self.inner.get_notifications(&requests).await?;
|
||||
let mut result = HashMap::new();
|
||||
|
||||
let mut batch_result = HashMap::new();
|
||||
for (key, value) in items.into_iter() {
|
||||
match value {
|
||||
Ok(item) => {
|
||||
result.insert(key.to_string(), NotificationItem::from_inner(item));
|
||||
}
|
||||
Err(error) => {
|
||||
// TODO This error should actually be returned so the clients can handle the
|
||||
// error as they see fit, but it's failing when creating
|
||||
// bindings for Go, i.e.
|
||||
// (https://github.com/NordSecurity/uniffi-bindgen-go/issues/62)
|
||||
error!("Could not fetch notification {key}, an error happened: {error}");
|
||||
}
|
||||
}
|
||||
let result = match value {
|
||||
Ok(status) => BatchNotificationResult::Ok { status: status.into() },
|
||||
Err(error) => BatchNotificationResult::Error { message: error.to_string() },
|
||||
};
|
||||
batch_result.insert(key.to_string(), result);
|
||||
}
|
||||
Ok(result)
|
||||
|
||||
Ok(batch_result)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use std::sync::{atomic::AtomicBool, Arc, OnceLock};
|
||||
use std::sync::OnceLock;
|
||||
#[cfg(feature = "sentry")]
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
|
||||
#[cfg(feature = "sentry")]
|
||||
use tracing::warn;
|
||||
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
||||
use tracing_core::Subscriber;
|
||||
@@ -11,164 +14,158 @@ use tracing_subscriber::{
|
||||
time::FormatTime,
|
||||
FormatEvent, FormatFields, FormattedFields,
|
||||
},
|
||||
layer::SubscriberExt as _,
|
||||
layer::{Layered, SubscriberExt as _},
|
||||
registry::LookupSpan,
|
||||
reload::{self, Handle},
|
||||
util::SubscriberInitExt as _,
|
||||
Layer,
|
||||
EnvFilter, Layer, Registry,
|
||||
};
|
||||
|
||||
use crate::{error::ClientError, tracing::LogLevel};
|
||||
|
||||
fn text_layers<S>(config: TracingConfiguration) -> impl Layer<S>
|
||||
// Adjusted version of tracing_subscriber::fmt::Format
|
||||
struct EventFormatter {
|
||||
display_timestamp: bool,
|
||||
display_level: bool,
|
||||
}
|
||||
|
||||
impl EventFormatter {
|
||||
fn new() -> Self {
|
||||
Self { display_timestamp: true, display_level: true }
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn for_logcat() -> Self {
|
||||
// Level and time are already captured by logcat separately
|
||||
Self { display_timestamp: false, display_level: false }
|
||||
}
|
||||
|
||||
fn format_timestamp(&self, writer: &mut fmt::format::Writer<'_>) -> std::fmt::Result {
|
||||
if fmt::time::SystemTime.format_time(writer).is_err() {
|
||||
writer.write_str("<unknown time>")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_filename(
|
||||
&self,
|
||||
writer: &mut fmt::format::Writer<'_>,
|
||||
filename: &str,
|
||||
) -> std::fmt::Result {
|
||||
const CRATES_IO_PATH_MATCHER: &str = ".cargo/registry/src/index.crates.io";
|
||||
let crates_io_filename = filename
|
||||
.split_once(CRATES_IO_PATH_MATCHER)
|
||||
.and_then(|(_, rest)| rest.split_once('/').map(|(_, rest)| rest));
|
||||
|
||||
if let Some(filename) = crates_io_filename {
|
||||
writer.write_str("<crates.io>/")?;
|
||||
writer.write_str(filename)
|
||||
} else {
|
||||
writer.write_str(filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, N> FormatEvent<S, N> for EventFormatter
|
||||
where
|
||||
S: Subscriber + for<'a> LookupSpan<'a>,
|
||||
N: for<'a> FormatFields<'a> + 'static,
|
||||
{
|
||||
// Adjusted version of tracing_subscriber::fmt::Format
|
||||
struct EventFormatter {
|
||||
display_timestamp: bool,
|
||||
display_level: bool,
|
||||
}
|
||||
fn format_event(
|
||||
&self,
|
||||
ctx: &fmt::FmtContext<'_, S, N>,
|
||||
mut writer: fmt::format::Writer<'_>,
|
||||
event: &tracing_core::Event<'_>,
|
||||
) -> std::fmt::Result {
|
||||
let meta = event.metadata();
|
||||
|
||||
impl EventFormatter {
|
||||
fn new() -> Self {
|
||||
Self { display_timestamp: true, display_level: true }
|
||||
if self.display_timestamp {
|
||||
self.format_timestamp(&mut writer)?;
|
||||
writer.write_char(' ')?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn for_logcat() -> Self {
|
||||
// Level and time are already captured by logcat separately
|
||||
Self { display_timestamp: false, display_level: false }
|
||||
if self.display_level {
|
||||
// For info and warn, add a padding space to the left
|
||||
write!(writer, "{:>5} ", meta.level())?;
|
||||
}
|
||||
|
||||
fn format_timestamp(&self, writer: &mut fmt::format::Writer<'_>) -> std::fmt::Result {
|
||||
if fmt::time::SystemTime.format_time(writer).is_err() {
|
||||
writer.write_str("<unknown time>")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
write!(writer, "{}: ", meta.target())?;
|
||||
|
||||
fn write_filename(
|
||||
&self,
|
||||
writer: &mut fmt::format::Writer<'_>,
|
||||
filename: &str,
|
||||
) -> std::fmt::Result {
|
||||
const CRATES_IO_PATH_MATCHER: &str = ".cargo/registry/src/index.crates.io";
|
||||
let crates_io_filename = filename
|
||||
.split_once(CRATES_IO_PATH_MATCHER)
|
||||
.and_then(|(_, rest)| rest.split_once('/').map(|(_, rest)| rest));
|
||||
ctx.format_fields(writer.by_ref(), event)?;
|
||||
|
||||
if let Some(filename) = crates_io_filename {
|
||||
writer.write_str("<crates.io>/")?;
|
||||
writer.write_str(filename)
|
||||
} else {
|
||||
writer.write_str(filename)
|
||||
if let Some(filename) = meta.file() {
|
||||
writer.write_str(" | ")?;
|
||||
self.write_filename(&mut writer, filename)?;
|
||||
if let Some(line_number) = meta.line() {
|
||||
write!(writer, ":{line_number}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, N> FormatEvent<S, N> for EventFormatter
|
||||
where
|
||||
S: Subscriber + for<'a> LookupSpan<'a>,
|
||||
N: for<'a> FormatFields<'a> + 'static,
|
||||
{
|
||||
fn format_event(
|
||||
&self,
|
||||
ctx: &fmt::FmtContext<'_, S, N>,
|
||||
mut writer: fmt::format::Writer<'_>,
|
||||
event: &tracing_core::Event<'_>,
|
||||
) -> std::fmt::Result {
|
||||
let meta = event.metadata();
|
||||
if let Some(scope) = ctx.event_scope() {
|
||||
writer.write_str(" | spans: ")?;
|
||||
|
||||
if self.display_timestamp {
|
||||
self.format_timestamp(&mut writer)?;
|
||||
writer.write_char(' ')?;
|
||||
}
|
||||
let mut first = true;
|
||||
|
||||
if self.display_level {
|
||||
// For info and warn, add a padding space to the left
|
||||
write!(writer, "{:>5} ", meta.level())?;
|
||||
}
|
||||
|
||||
write!(writer, "{}: ", meta.target())?;
|
||||
|
||||
ctx.format_fields(writer.by_ref(), event)?;
|
||||
|
||||
if let Some(filename) = meta.file() {
|
||||
writer.write_str(" | ")?;
|
||||
self.write_filename(&mut writer, filename)?;
|
||||
if let Some(line_number) = meta.line() {
|
||||
write!(writer, ":{line_number}")?;
|
||||
for span in scope.from_root() {
|
||||
if !first {
|
||||
writer.write_str(" > ")?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(scope) = ctx.event_scope() {
|
||||
writer.write_str(" | spans: ")?;
|
||||
first = false;
|
||||
|
||||
let mut first = true;
|
||||
write!(writer, "{}", span.name())?;
|
||||
|
||||
for span in scope.from_root() {
|
||||
if !first {
|
||||
writer.write_str(" > ")?;
|
||||
}
|
||||
|
||||
first = false;
|
||||
|
||||
write!(writer, "{}", span.name())?;
|
||||
|
||||
if let Some(fields) = &span.extensions().get::<FormattedFields<N>>() {
|
||||
if !fields.is_empty() {
|
||||
write!(writer, "{{{fields}}}")?;
|
||||
}
|
||||
if let Some(fields) = &span.extensions().get::<FormattedFields<N>>() {
|
||||
if !fields.is_empty() {
|
||||
write!(writer, "{{{fields}}}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(writer)
|
||||
}
|
||||
|
||||
writeln!(writer)
|
||||
}
|
||||
}
|
||||
|
||||
let file_layer = config.write_to_files.map(|c| {
|
||||
let mut builder = RollingFileAppender::builder()
|
||||
.rotation(Rotation::HOURLY)
|
||||
.filename_prefix(&c.file_prefix);
|
||||
// Another fields formatter is necessary because of this bug
|
||||
// https://github.com/tokio-rs/tracing/issues/1372. Using a new
|
||||
// formatter for the fields forces to record them in different span
|
||||
// extensions, and thus remove the duplicated fields in the span.
|
||||
#[derive(Default)]
|
||||
struct FieldsFormatterForFiles(DefaultFields);
|
||||
|
||||
if let Some(max_files) = c.max_files {
|
||||
builder = builder.max_log_files(max_files as usize)
|
||||
};
|
||||
if let Some(file_suffix) = c.file_suffix {
|
||||
builder = builder.filename_suffix(file_suffix)
|
||||
}
|
||||
impl<'writer> FormatFields<'writer> for FieldsFormatterForFiles {
|
||||
fn format_fields<R: RecordFields>(
|
||||
&self,
|
||||
writer: Writer<'writer>,
|
||||
fields: R,
|
||||
) -> std::fmt::Result {
|
||||
self.0.format_fields(writer, fields)
|
||||
}
|
||||
}
|
||||
|
||||
let writer = builder.build(&c.path).expect("Failed to create a rolling file appender.");
|
||||
type ReloadHandle = Handle<
|
||||
tracing_subscriber::fmt::Layer<
|
||||
Layered<EnvFilter, Registry>,
|
||||
FieldsFormatterForFiles,
|
||||
EventFormatter,
|
||||
RollingFileAppender,
|
||||
>,
|
||||
Layered<EnvFilter, Registry>,
|
||||
>;
|
||||
|
||||
// Another fields formatter is necessary because of this bug
|
||||
// https://github.com/tokio-rs/tracing/issues/1372. Using a new
|
||||
// formatter for the fields forces to record them in different span
|
||||
// extensions, and thus remove the duplicated fields in the span.
|
||||
#[derive(Default)]
|
||||
struct FieldsFormatterForFiles(DefaultFields);
|
||||
fn text_layers(
|
||||
config: TracingConfiguration,
|
||||
) -> (impl Layer<Layered<EnvFilter, Registry>>, Option<ReloadHandle>) {
|
||||
let (file_layer, reload_handle) = config
|
||||
.write_to_files
|
||||
.map(|c| {
|
||||
let layer = make_file_layer(c);
|
||||
reload::Layer::new(layer)
|
||||
})
|
||||
.unzip();
|
||||
|
||||
impl<'writer> FormatFields<'writer> for FieldsFormatterForFiles {
|
||||
fn format_fields<R: RecordFields>(
|
||||
&self,
|
||||
writer: Writer<'writer>,
|
||||
fields: R,
|
||||
) -> std::fmt::Result {
|
||||
self.0.format_fields(writer, fields)
|
||||
}
|
||||
}
|
||||
|
||||
fmt::layer()
|
||||
.fmt_fields(FieldsFormatterForFiles::default())
|
||||
.event_format(EventFormatter::new())
|
||||
// EventFormatter doesn't support ANSI colors anyways, but the
|
||||
// default field formatter does, which is unhelpful for iOS +
|
||||
// Android logs, but enabled by default.
|
||||
.with_ansi(false)
|
||||
.with_writer(writer)
|
||||
});
|
||||
|
||||
Layer::and_then(
|
||||
let layers = Layer::and_then(
|
||||
file_layer,
|
||||
config.write_to_stdout_or_system.then(|| {
|
||||
// Another fields formatter is necessary because of this bug
|
||||
@@ -206,7 +203,41 @@ where
|
||||
"org.matrix.rust.sdk".to_owned(),
|
||||
));
|
||||
}),
|
||||
)
|
||||
);
|
||||
|
||||
(layers, reload_handle)
|
||||
}
|
||||
|
||||
fn make_file_layer(
|
||||
file_configuration: TracingFileConfiguration,
|
||||
) -> tracing_subscriber::fmt::Layer<
|
||||
Layered<EnvFilter, Registry, Registry>,
|
||||
FieldsFormatterForFiles,
|
||||
EventFormatter,
|
||||
RollingFileAppender,
|
||||
> {
|
||||
let mut builder = RollingFileAppender::builder()
|
||||
.rotation(Rotation::HOURLY)
|
||||
.filename_prefix(&file_configuration.file_prefix);
|
||||
|
||||
if let Some(max_files) = file_configuration.max_files {
|
||||
builder = builder.max_log_files(max_files as usize)
|
||||
}
|
||||
if let Some(file_suffix) = file_configuration.file_suffix {
|
||||
builder = builder.filename_suffix(file_suffix)
|
||||
}
|
||||
|
||||
let writer =
|
||||
builder.build(&file_configuration.path).expect("Failed to create a rolling file appender.");
|
||||
|
||||
fmt::layer()
|
||||
.fmt_fields(FieldsFormatterForFiles::default())
|
||||
.event_format(EventFormatter::new())
|
||||
// EventFormatter doesn't support ANSI colors anyways, but the
|
||||
// default field formatter does, which is unhelpful for iOS +
|
||||
// Android logs, but enabled by default.
|
||||
.with_ansi(false)
|
||||
.with_writer(writer)
|
||||
}
|
||||
|
||||
/// Configuration to save logs to (rotated) log-files.
|
||||
@@ -258,6 +289,7 @@ enum LogTarget {
|
||||
|
||||
// SDK UI modules.
|
||||
MatrixSdkUiTimeline,
|
||||
MatrixSdkUiNotificationClient,
|
||||
}
|
||||
|
||||
impl LogTarget {
|
||||
@@ -280,6 +312,7 @@ impl LogTarget {
|
||||
LogTarget::MatrixSdkSendQueue => "matrix_sdk::send_queue",
|
||||
LogTarget::MatrixSdkEventCacheStore => "matrix_sdk_sqlite::event_cache_store",
|
||||
LogTarget::MatrixSdkUiTimeline => "matrix_sdk_ui::timeline",
|
||||
LogTarget::MatrixSdkUiNotificationClient => "matrix_sdk_ui::notification_client",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -302,6 +335,7 @@ const DEFAULT_TARGET_LOG_LEVELS: &[(LogTarget, LogLevel)] = &[
|
||||
(LogTarget::MatrixSdkEventCacheStore, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkCommonStoreLocks, LogLevel::Warn),
|
||||
(LogTarget::MatrixSdkBaseStoreAmbiguityMap, LogLevel::Warn),
|
||||
(LogTarget::MatrixSdkUiNotificationClient, LogLevel::Info),
|
||||
];
|
||||
|
||||
const IMMUTABLE_LOG_TARGETS: &[LogTarget] = &[
|
||||
@@ -322,6 +356,8 @@ pub enum TraceLogPacks {
|
||||
SendQueue,
|
||||
/// Enables all the logs relevant to the timeline.
|
||||
Timeline,
|
||||
/// Enables all the logs relevant to the notification client.
|
||||
NotificationClient,
|
||||
}
|
||||
|
||||
impl TraceLogPacks {
|
||||
@@ -336,10 +372,12 @@ impl TraceLogPacks {
|
||||
],
|
||||
TraceLogPacks::SendQueue => &[LogTarget::MatrixSdkSendQueue],
|
||||
TraceLogPacks::Timeline => &[LogTarget::MatrixSdkUiTimeline],
|
||||
TraceLogPacks::NotificationClient => &[LogTarget::MatrixSdkUiNotificationClient],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sentry")]
|
||||
struct SentryLoggingCtx {
|
||||
/// The Sentry client guard, which keeps the Sentry context alive.
|
||||
_guard: sentry::ClientInitGuard,
|
||||
@@ -349,6 +387,8 @@ struct SentryLoggingCtx {
|
||||
}
|
||||
|
||||
struct LoggingCtx {
|
||||
reload_handle: Option<ReloadHandle>,
|
||||
#[cfg(feature = "sentry")]
|
||||
sentry: Option<SentryLoggingCtx>,
|
||||
}
|
||||
|
||||
@@ -376,12 +416,14 @@ pub struct TracingConfiguration {
|
||||
write_to_files: Option<TracingFileConfiguration>,
|
||||
|
||||
/// If set, the Sentry DSN to use for error reporting.
|
||||
#[cfg(feature = "sentry")]
|
||||
sentry_dsn: Option<String>,
|
||||
}
|
||||
|
||||
impl TracingConfiguration {
|
||||
/// Sets up the tracing configuration and return a [`Logger`] instance
|
||||
/// holding onto it.
|
||||
#[cfg_attr(not(feature = "sentry"), allow(unused_mut))]
|
||||
fn build(mut self) -> LoggingCtx {
|
||||
// Show full backtraces, if we run into panics.
|
||||
std::env::set_var("RUST_BACKTRACE", "1");
|
||||
@@ -389,73 +431,90 @@ impl TracingConfiguration {
|
||||
// Log panics.
|
||||
log_panics::init();
|
||||
|
||||
// Prepare the Sentry layer, if a DSN is provided.
|
||||
let (sentry_layer, sentry_logging_ctx) = if let Some(sentry_dsn) = self.sentry_dsn.take() {
|
||||
// Initialize the Sentry client with the given options.
|
||||
let sentry_guard = sentry::init((
|
||||
sentry_dsn,
|
||||
sentry::ClientOptions {
|
||||
traces_sample_rate: 0.0,
|
||||
attach_stacktrace: true,
|
||||
..sentry::ClientOptions::default()
|
||||
},
|
||||
));
|
||||
|
||||
let sentry_enabled = Arc::new(AtomicBool::new(true));
|
||||
|
||||
// Add a Sentry layer to the tracing subscriber.
|
||||
//
|
||||
// Pass custom event and span filters, which will ignore anything, if the Sentry
|
||||
// support has been globally disabled, or if the statement doesn't include a
|
||||
// `sentry` field set to `true`.
|
||||
let sentry_layer = sentry_tracing::layer()
|
||||
.event_filter({
|
||||
let enabled = sentry_enabled.clone();
|
||||
|
||||
move |metadata| {
|
||||
if enabled.load(std::sync::atomic::Ordering::SeqCst)
|
||||
&& metadata.fields().field("sentry").is_some()
|
||||
{
|
||||
sentry_tracing::default_event_filter(metadata)
|
||||
} else {
|
||||
// Ignore the event.
|
||||
sentry_tracing::EventFilter::Ignore
|
||||
}
|
||||
}
|
||||
})
|
||||
.span_filter({
|
||||
let enabled = sentry_enabled.clone();
|
||||
|
||||
move |metadata| {
|
||||
if enabled.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
sentry_tracing::default_span_filter(metadata)
|
||||
} else {
|
||||
// Ignore, if sentry is globally disabled.
|
||||
false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
(
|
||||
Some(sentry_layer),
|
||||
Some(SentryLoggingCtx { _guard: sentry_guard, enabled: sentry_enabled }),
|
||||
)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let env_filter = build_tracing_filter(&self);
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::EnvFilter::new(&env_filter))
|
||||
.with(crate::platform::text_layers(self))
|
||||
.with(sentry_layer)
|
||||
.init();
|
||||
let logging_ctx;
|
||||
#[cfg(feature = "sentry")]
|
||||
{
|
||||
// Prepare the Sentry layer, if a DSN is provided.
|
||||
let (sentry_layer, sentry_logging_ctx) =
|
||||
if let Some(sentry_dsn) = self.sentry_dsn.take() {
|
||||
// Initialize the Sentry client with the given options.
|
||||
let sentry_guard = sentry::init((
|
||||
sentry_dsn,
|
||||
sentry::ClientOptions {
|
||||
traces_sample_rate: 0.0,
|
||||
attach_stacktrace: true,
|
||||
release: Some(env!("VERGEN_GIT_SHA").into()),
|
||||
..sentry::ClientOptions::default()
|
||||
},
|
||||
));
|
||||
|
||||
let sentry_enabled = Arc::new(AtomicBool::new(true));
|
||||
|
||||
// Add a Sentry layer to the tracing subscriber.
|
||||
//
|
||||
// Pass custom event and span filters, which will ignore anything, if the Sentry
|
||||
// support has been globally disabled, or if the statement doesn't include a
|
||||
// `sentry` field set to `true`.
|
||||
let sentry_layer = sentry_tracing::layer()
|
||||
.event_filter({
|
||||
let enabled = sentry_enabled.clone();
|
||||
|
||||
move |metadata| {
|
||||
if enabled.load(std::sync::atomic::Ordering::SeqCst)
|
||||
&& metadata.fields().field("sentry").is_some()
|
||||
{
|
||||
sentry_tracing::default_event_filter(metadata)
|
||||
} else {
|
||||
// Ignore the event.
|
||||
sentry_tracing::EventFilter::Ignore
|
||||
}
|
||||
}
|
||||
})
|
||||
.span_filter({
|
||||
let enabled = sentry_enabled.clone();
|
||||
|
||||
move |metadata| {
|
||||
if enabled.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
sentry_tracing::default_span_filter(metadata)
|
||||
} else {
|
||||
// Ignore, if sentry is globally disabled.
|
||||
false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
(
|
||||
Some(sentry_layer),
|
||||
Some(SentryLoggingCtx { _guard: sentry_guard, enabled: sentry_enabled }),
|
||||
)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
let (text_layers, reload_handle) = crate::platform::text_layers(self);
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::EnvFilter::new(&env_filter))
|
||||
.with(text_layers)
|
||||
.with(sentry_layer)
|
||||
.init();
|
||||
logging_ctx = LoggingCtx { reload_handle, sentry: sentry_logging_ctx };
|
||||
}
|
||||
#[cfg(not(feature = "sentry"))]
|
||||
{
|
||||
let (text_layers, reload_handle) = crate::platform::text_layers(self);
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::EnvFilter::new(&env_filter))
|
||||
.with(text_layers)
|
||||
.init();
|
||||
logging_ctx = LoggingCtx { reload_handle };
|
||||
}
|
||||
|
||||
// Log the log levels 🧠.
|
||||
tracing::info!(env_filter, "Logging has been set up");
|
||||
|
||||
LoggingCtx { sentry: sentry_logging_ctx }
|
||||
logging_ctx
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,15 +564,22 @@ pub fn init_platform(
|
||||
config: TracingConfiguration,
|
||||
use_lightweight_tokio_runtime: bool,
|
||||
) -> Result<(), ClientError> {
|
||||
LOGGING.set(config.build()).map_err(|_| ClientError::Generic {
|
||||
msg: "logger already initialized".to_owned(),
|
||||
details: None,
|
||||
})?;
|
||||
#[cfg(all(feature = "js", target_family = "wasm"))]
|
||||
{
|
||||
console_error_panic_hook::set_once();
|
||||
}
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
LOGGING.set(config.build()).map_err(|_| ClientError::Generic {
|
||||
msg: "logger already initialized".to_owned(),
|
||||
details: None,
|
||||
})?;
|
||||
|
||||
if use_lightweight_tokio_runtime {
|
||||
setup_lightweight_tokio_runtime();
|
||||
} else {
|
||||
setup_multithreaded_tokio_runtime();
|
||||
if use_lightweight_tokio_runtime {
|
||||
setup_lightweight_tokio_runtime();
|
||||
} else {
|
||||
setup_multithreaded_tokio_runtime();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -522,6 +588,7 @@ pub fn init_platform(
|
||||
/// Set the global enablement level for the Sentry layer (after the logs have
|
||||
/// been set up).
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[cfg(feature = "sentry")]
|
||||
pub fn enable_sentry_logging(enabled: bool) {
|
||||
if let Some(ctx) = LOGGING.get() {
|
||||
if let Some(sentry_ctx) = &ctx.sentry {
|
||||
@@ -535,6 +602,37 @@ pub fn enable_sentry_logging(enabled: bool) {
|
||||
};
|
||||
}
|
||||
|
||||
/// Updates the tracing subscriber with a new file writer based on the provided
|
||||
/// configuration.
|
||||
///
|
||||
/// This method will throw if `init_platform` hasn't been called, or if it was
|
||||
/// called with `write_to_files` set to `None`.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn reload_tracing_file_writer(
|
||||
configuration: TracingFileConfiguration,
|
||||
) -> Result<(), ClientError> {
|
||||
let Some(logging_context) = LOGGING.get() else {
|
||||
return Err(ClientError::Generic {
|
||||
msg: "Logging hasn't been initialized yet".to_owned(),
|
||||
details: None,
|
||||
});
|
||||
};
|
||||
|
||||
let Some(reload_handle) = logging_context.reload_handle.as_ref() else {
|
||||
return Err(ClientError::Generic {
|
||||
msg: "Logging wasn't initialized with a file config".to_owned(),
|
||||
details: None,
|
||||
});
|
||||
};
|
||||
|
||||
let layer = make_file_layer(configuration);
|
||||
reload_handle.reload(layer).map_err(|error| ClientError::Generic {
|
||||
msg: format!("Failed to reload file config: {error}"),
|
||||
details: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn setup_multithreaded_tokio_runtime() {
|
||||
async_compat::set_runtime_builder(Box::new(|| {
|
||||
eprintln!("spawning a multithreaded tokio runtime");
|
||||
@@ -545,6 +643,7 @@ fn setup_multithreaded_tokio_runtime() {
|
||||
}));
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn setup_lightweight_tokio_runtime() {
|
||||
async_compat::set_runtime_builder(Box::new(|| {
|
||||
eprintln!("spawning a lightweight tokio runtime");
|
||||
@@ -587,6 +686,7 @@ mod tests {
|
||||
extra_targets: vec!["super_duper_app".to_owned()],
|
||||
write_to_stdout_or_system: true,
|
||||
write_to_files: None,
|
||||
#[cfg(feature = "sentry")]
|
||||
sentry_dsn: None,
|
||||
};
|
||||
|
||||
@@ -594,25 +694,30 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
filter,
|
||||
"panic=error,\
|
||||
hyper=warn,\
|
||||
matrix_sdk_ffi=info,\
|
||||
matrix_sdk=info,\
|
||||
matrix_sdk::client=trace,\
|
||||
matrix_sdk_crypto=debug,\
|
||||
matrix_sdk_crypto::olm::account=trace,\
|
||||
matrix_sdk::oidc=trace,\
|
||||
matrix_sdk::http_client=debug,\
|
||||
matrix_sdk::sliding_sync=info,\
|
||||
matrix_sdk_base::sliding_sync=info,\
|
||||
matrix_sdk_ui::timeline=info,\
|
||||
matrix_sdk::send_queue=info,\
|
||||
matrix_sdk::event_cache=info,\
|
||||
matrix_sdk_base::event_cache=info,\
|
||||
matrix_sdk_sqlite::event_cache_store=info,\
|
||||
matrix_sdk_common::store_locks=warn,\
|
||||
matrix_sdk_base::store::ambiguity_map=warn,\
|
||||
super_duper_app=error"
|
||||
r#"panic=error,
|
||||
hyper=warn,
|
||||
matrix_sdk_ffi=info,
|
||||
matrix_sdk=info,
|
||||
matrix_sdk::client=trace,
|
||||
matrix_sdk_crypto=debug,
|
||||
matrix_sdk_crypto::olm::account=trace,
|
||||
matrix_sdk::oidc=trace,
|
||||
matrix_sdk::http_client=debug,
|
||||
matrix_sdk::sliding_sync=info,
|
||||
matrix_sdk_base::sliding_sync=info,
|
||||
matrix_sdk_ui::timeline=info,
|
||||
matrix_sdk::send_queue=info,
|
||||
matrix_sdk::event_cache=info,
|
||||
matrix_sdk_base::event_cache=info,
|
||||
matrix_sdk_sqlite::event_cache_store=info,
|
||||
matrix_sdk_common::store_locks=warn,
|
||||
matrix_sdk_base::store::ambiguity_map=warn,
|
||||
matrix_sdk_ui::notification_client=info,
|
||||
super_duper_app=error"#
|
||||
.split('\n')
|
||||
.map(|s| s.trim())
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -624,6 +729,7 @@ mod tests {
|
||||
extra_targets: vec!["super_duper_app".to_owned(), "some_other_span".to_owned()],
|
||||
write_to_stdout_or_system: true,
|
||||
write_to_files: None,
|
||||
#[cfg(feature = "sentry")]
|
||||
sentry_dsn: None,
|
||||
};
|
||||
|
||||
@@ -631,26 +737,31 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
filter,
|
||||
"panic=error,\
|
||||
hyper=warn,\
|
||||
matrix_sdk_ffi=info,\
|
||||
matrix_sdk=info,\
|
||||
matrix_sdk::client=trace,\
|
||||
matrix_sdk_crypto=trace,\
|
||||
matrix_sdk_crypto::olm::account=trace,\
|
||||
matrix_sdk::oidc=trace,\
|
||||
matrix_sdk::http_client=trace,\
|
||||
matrix_sdk::sliding_sync=trace,\
|
||||
matrix_sdk_base::sliding_sync=trace,\
|
||||
matrix_sdk_ui::timeline=trace,\
|
||||
matrix_sdk::send_queue=trace,\
|
||||
matrix_sdk::event_cache=trace,\
|
||||
matrix_sdk_base::event_cache=trace,\
|
||||
matrix_sdk_sqlite::event_cache_store=trace,\
|
||||
matrix_sdk_common::store_locks=warn,\
|
||||
matrix_sdk_base::store::ambiguity_map=warn,\
|
||||
super_duper_app=trace,\
|
||||
some_other_span=trace"
|
||||
r#"panic=error,
|
||||
hyper=warn,
|
||||
matrix_sdk_ffi=info,
|
||||
matrix_sdk=info,
|
||||
matrix_sdk::client=trace,
|
||||
matrix_sdk_crypto=trace,
|
||||
matrix_sdk_crypto::olm::account=trace,
|
||||
matrix_sdk::oidc=trace,
|
||||
matrix_sdk::http_client=trace,
|
||||
matrix_sdk::sliding_sync=trace,
|
||||
matrix_sdk_base::sliding_sync=trace,
|
||||
matrix_sdk_ui::timeline=trace,
|
||||
matrix_sdk::send_queue=trace,
|
||||
matrix_sdk::event_cache=trace,
|
||||
matrix_sdk_base::event_cache=trace,
|
||||
matrix_sdk_sqlite::event_cache_store=trace,
|
||||
matrix_sdk_common::store_locks=warn,
|
||||
matrix_sdk_base::store::ambiguity_map=warn,
|
||||
matrix_sdk_ui::notification_client=trace,
|
||||
super_duper_app=trace,
|
||||
some_other_span=trace"#
|
||||
.split('\n')
|
||||
.map(|s| s.trim())
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -662,6 +773,7 @@ mod tests {
|
||||
extra_targets: vec!["super_duper_app".to_owned()],
|
||||
write_to_stdout_or_system: true,
|
||||
write_to_files: None,
|
||||
#[cfg(feature = "sentry")]
|
||||
sentry_dsn: None,
|
||||
};
|
||||
|
||||
@@ -687,6 +799,7 @@ mod tests {
|
||||
matrix_sdk_sqlite::event_cache_store=trace,
|
||||
matrix_sdk_common::store_locks=warn,
|
||||
matrix_sdk_base::store::ambiguity_map=warn,
|
||||
matrix_sdk_ui::notification_client=info,
|
||||
super_duper_app=info"#
|
||||
.split('\n')
|
||||
.map(|s| s.trim())
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk::{
|
||||
authentication::oauth::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason},
|
||||
crypto::types::qr_login::{LoginQrCodeDecodeError, QrCodeModeData},
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use tracing::error;
|
||||
|
||||
/// Data for the QR code login mechanism.
|
||||
///
|
||||
/// The [`QrCodeData`] can be serialized and encoded as a QR code or it can be
|
||||
/// decoded from a QR code.
|
||||
#[derive(Debug, uniffi::Object)]
|
||||
pub struct QrCodeData {
|
||||
pub(crate) inner: qrcode::QrCodeData,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl QrCodeData {
|
||||
/// Attempt to decode a slice of bytes into a [`QrCodeData`] object.
|
||||
///
|
||||
/// The slice of bytes would generally be returned by a QR code decoder.
|
||||
#[uniffi::constructor]
|
||||
pub fn from_bytes(bytes: Vec<u8>) -> Result<Arc<Self>, QrCodeDecodeError> {
|
||||
Ok(Self { inner: qrcode::QrCodeData::from_bytes(&bytes)? }.into())
|
||||
}
|
||||
|
||||
/// The server name contained within the scanned QR code data.
|
||||
///
|
||||
/// Note: This value is only present when scanning a QR code the belongs to
|
||||
/// a logged in client. The mode where the new client shows the QR code
|
||||
/// will return `None`.
|
||||
pub fn server_name(&self) -> Option<String> {
|
||||
match &self.inner.mode_data {
|
||||
QrCodeModeData::Reciprocate { server_name } => Some(server_name.to_owned()),
|
||||
QrCodeModeData::Login => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for the decoding of the [`QrCodeData`].
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum QrCodeDecodeError {
|
||||
#[error("Error decoding QR code: {error:?}")]
|
||||
Crypto {
|
||||
#[from]
|
||||
error: LoginQrCodeDecodeError,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
pub enum HumanQrLoginError {
|
||||
#[error("Linking with this device is not supported.")]
|
||||
LinkingNotSupported,
|
||||
#[error("The sign in was cancelled.")]
|
||||
Cancelled,
|
||||
#[error("The sign in was not completed in the required time.")]
|
||||
Expired,
|
||||
#[error("A secure connection could not have been established between the two devices.")]
|
||||
ConnectionInsecure,
|
||||
#[error("The sign in was declined.")]
|
||||
Declined,
|
||||
#[error("An unknown error has happened.")]
|
||||
Unknown,
|
||||
#[error("The homeserver doesn't provide sliding sync in its configuration.")]
|
||||
SlidingSyncNotAvailable,
|
||||
#[error("Unable to use OIDC as the supplied client metadata is invalid.")]
|
||||
OidcMetadataInvalid,
|
||||
#[error("The other device is not signed in and as such can't sign in other devices.")]
|
||||
OtherDeviceNotSignedIn,
|
||||
}
|
||||
|
||||
impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
|
||||
fn from(value: qrcode::QRCodeLoginError) -> Self {
|
||||
use qrcode::{QRCodeLoginError, SecureChannelError};
|
||||
|
||||
match value {
|
||||
QRCodeLoginError::LoginFailure { reason, .. } => match reason {
|
||||
LoginFailureReason::UnsupportedProtocol => HumanQrLoginError::LinkingNotSupported,
|
||||
LoginFailureReason::AuthorizationExpired => HumanQrLoginError::Expired,
|
||||
LoginFailureReason::UserCancelled => HumanQrLoginError::Cancelled,
|
||||
_ => HumanQrLoginError::Unknown,
|
||||
},
|
||||
|
||||
QRCodeLoginError::OAuth(e) => {
|
||||
if let Some(e) = e.as_request_token_error() {
|
||||
match e {
|
||||
DeviceCodeErrorResponseType::AccessDenied => HumanQrLoginError::Declined,
|
||||
DeviceCodeErrorResponseType::ExpiredToken => HumanQrLoginError::Expired,
|
||||
_ => HumanQrLoginError::Unknown,
|
||||
}
|
||||
} else {
|
||||
HumanQrLoginError::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
QRCodeLoginError::SecureChannel(e) => match e {
|
||||
SecureChannelError::Utf8(_)
|
||||
| SecureChannelError::MessageDecode(_)
|
||||
| SecureChannelError::Json(_)
|
||||
| SecureChannelError::RendezvousChannel(_) => HumanQrLoginError::Unknown,
|
||||
SecureChannelError::SecureChannelMessage { .. }
|
||||
| SecureChannelError::Ecies(_)
|
||||
| SecureChannelError::InvalidCheckCode => HumanQrLoginError::ConnectionInsecure,
|
||||
SecureChannelError::InvalidIntent => HumanQrLoginError::OtherDeviceNotSignedIn,
|
||||
},
|
||||
|
||||
QRCodeLoginError::UnexpectedMessage { .. }
|
||||
| QRCodeLoginError::CrossProcessRefreshLock(_)
|
||||
| QRCodeLoginError::DeviceKeyUpload(_)
|
||||
| QRCodeLoginError::SessionTokens(_)
|
||||
| QRCodeLoginError::UserIdDiscovery(_)
|
||||
| QRCodeLoginError::SecretImport(_) => HumanQrLoginError::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum describing the progress of the QR-code login.
|
||||
#[derive(Debug, Default, Clone, uniffi::Enum)]
|
||||
pub enum QrLoginProgress {
|
||||
/// The login process is starting.
|
||||
#[default]
|
||||
Starting,
|
||||
/// We established a secure channel with the other device.
|
||||
EstablishingSecureChannel {
|
||||
/// The check code that the device should display so the other device
|
||||
/// can confirm that the channel is secure as well.
|
||||
check_code: u8,
|
||||
/// The string representation of the check code, will be guaranteed to
|
||||
/// be 2 characters long, preserving the leading zero if the
|
||||
/// first digit is a zero.
|
||||
check_code_string: String,
|
||||
},
|
||||
/// We are waiting for the login and for the OAuth 2.0 authorization server
|
||||
/// to give us an access token.
|
||||
WaitingForToken { user_code: String },
|
||||
/// The login has successfully finished.
|
||||
Done,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait QrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, state: QrLoginProgress);
|
||||
}
|
||||
|
||||
impl From<qrcode::LoginProgress> for QrLoginProgress {
|
||||
fn from(value: qrcode::LoginProgress) -> Self {
|
||||
use qrcode::LoginProgress;
|
||||
|
||||
match value {
|
||||
LoginProgress::Starting => Self::Starting,
|
||||
LoginProgress::EstablishingSecureChannel { check_code } => {
|
||||
let check_code = check_code.to_digit();
|
||||
|
||||
Self::EstablishingSecureChannel {
|
||||
check_code,
|
||||
check_code_string: format!("{check_code:02}"),
|
||||
}
|
||||
}
|
||||
LoginProgress::WaitingForToken { user_code } => Self::WaitingForToken { user_code },
|
||||
LoginProgress::Done => Self::Done,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,23 +26,22 @@ use ruma::{
|
||||
avatar::ImageInfo as RumaAvatarImageInfo,
|
||||
history_visibility::HistoryVisibility as RumaHistoryVisibility,
|
||||
join_rules::JoinRule as RumaJoinRule, message::RoomMessageEventContentWithoutRelation,
|
||||
power_levels::RoomPowerLevels as RumaPowerLevels, MediaSource,
|
||||
MediaSource,
|
||||
},
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent, TimelineEventType,
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent,
|
||||
},
|
||||
EventId, Int, OwnedDeviceId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomAliasId,
|
||||
ServerName, UserId,
|
||||
};
|
||||
use tracing::{error, warn};
|
||||
|
||||
use self::{power_levels::RoomPowerLevels, room_info::RoomInfo};
|
||||
use crate::{
|
||||
chunk_iterator::ChunkIterator,
|
||||
client::{JoinRule, RoomVisibility},
|
||||
error::{ClientError, MediaInfoError, NotYetImplemented, RoomError},
|
||||
event::{MessageLikeEventType, StateEventType},
|
||||
identity_status_change::IdentityStatusChange,
|
||||
live_location_share::{LastLocation, LiveLocationShare},
|
||||
room_info::RoomInfo,
|
||||
room_member::{RoomMember, RoomMemberWithSenderInfo},
|
||||
room_preview::RoomPreview,
|
||||
ruma::{ImageInfo, LocationContent, Mentions, NotifyType},
|
||||
@@ -55,6 +54,9 @@ use crate::{
|
||||
TaskHandle,
|
||||
};
|
||||
|
||||
mod power_levels;
|
||||
pub mod room_info;
|
||||
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum Membership {
|
||||
Invited,
|
||||
@@ -114,7 +116,10 @@ impl Room {
|
||||
self.inner.is_direct().await.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn is_public(&self) -> bool {
|
||||
/// Whether the room can be publicly joined or not, based on its join rule.
|
||||
///
|
||||
/// Can return `None` if the join rule state event is missing.
|
||||
pub fn is_public(&self) -> Option<bool> {
|
||||
self.inner.is_public()
|
||||
}
|
||||
|
||||
@@ -570,21 +575,6 @@ impl Room {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn can_user_redact_own(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_redact_own(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_redact_other(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_redact_other(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_ban(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_ban(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn ban_user(
|
||||
&self,
|
||||
user_id: String,
|
||||
@@ -603,16 +593,6 @@ impl Room {
|
||||
Ok(self.inner.unban_user(&user_id, reason.as_deref()).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_invite(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_invite(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_kick(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_kick(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn kick_user(
|
||||
&self,
|
||||
user_id: String,
|
||||
@@ -622,37 +602,6 @@ impl Room {
|
||||
Ok(self.inner.kick_user(&user_id, reason.as_deref()).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_send_state(
|
||||
&self,
|
||||
user_id: String,
|
||||
state_event: StateEventType,
|
||||
) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_send_state(&user_id, state_event.into()).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_send_message(
|
||||
&self,
|
||||
user_id: String,
|
||||
message: MessageLikeEventType,
|
||||
) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_send_message(&user_id, message.into()).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_pin_unpin(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_pin_unpin(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_trigger_room_notification(
|
||||
&self,
|
||||
user_id: String,
|
||||
) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_trigger_room_notification(&user_id).await?)
|
||||
}
|
||||
|
||||
pub fn own_user_id(&self) -> String {
|
||||
self.inner.own_user_id().to_string()
|
||||
}
|
||||
@@ -717,9 +666,9 @@ impl Room {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_power_levels(&self) -> Result<RoomPowerLevels, ClientError> {
|
||||
pub async fn get_power_levels(&self) -> Result<Arc<RoomPowerLevels>, ClientError> {
|
||||
let power_levels = self.inner.power_levels().await.map_err(matrix_sdk::Error::from)?;
|
||||
Ok(RoomPowerLevels::from(power_levels))
|
||||
Ok(Arc::new(RoomPowerLevels::new(power_levels, self.inner.own_user_id().to_owned())))
|
||||
}
|
||||
|
||||
pub async fn apply_power_level_changes(
|
||||
@@ -755,8 +704,11 @@ impl Room {
|
||||
Ok(self.inner.get_suggested_user_role(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn reset_power_levels(&self) -> Result<RoomPowerLevels, ClientError> {
|
||||
Ok(RoomPowerLevels::from(self.inner.reset_power_levels().await?))
|
||||
pub async fn reset_power_levels(&self) -> Result<Arc<RoomPowerLevels>, ClientError> {
|
||||
Ok(Arc::new(RoomPowerLevels::new(
|
||||
self.inner.reset_power_levels().await?,
|
||||
self.inner.own_user_id().to_owned(),
|
||||
)))
|
||||
}
|
||||
|
||||
pub async fn matrix_to_permalink(&self) -> Result<String, ClientError> {
|
||||
@@ -828,18 +780,31 @@ impl Room {
|
||||
|
||||
/// Store the given `ComposerDraft` in the state store using the current
|
||||
/// room id, as identifier.
|
||||
pub async fn save_composer_draft(&self, draft: ComposerDraft) -> Result<(), ClientError> {
|
||||
Ok(self.inner.save_composer_draft(draft.try_into()?).await?)
|
||||
pub async fn save_composer_draft(
|
||||
&self,
|
||||
draft: ComposerDraft,
|
||||
thread_root: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let thread_root = thread_root.map(EventId::parse).transpose()?;
|
||||
Ok(self.inner.save_composer_draft(draft.try_into()?, thread_root.as_deref()).await?)
|
||||
}
|
||||
|
||||
/// Retrieve the `ComposerDraft` stored in the state store for this room.
|
||||
pub async fn load_composer_draft(&self) -> Result<Option<ComposerDraft>, ClientError> {
|
||||
Ok(self.inner.load_composer_draft().await?.map(Into::into))
|
||||
pub async fn load_composer_draft(
|
||||
&self,
|
||||
thread_root: Option<String>,
|
||||
) -> Result<Option<ComposerDraft>, ClientError> {
|
||||
let thread_root = thread_root.map(EventId::parse).transpose()?;
|
||||
Ok(self.inner.load_composer_draft(thread_root.as_deref()).await?.map(Into::into))
|
||||
}
|
||||
|
||||
/// Remove the `ComposerDraft` stored in the state store for this room.
|
||||
pub async fn clear_composer_draft(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.clear_composer_draft().await?)
|
||||
pub async fn clear_composer_draft(
|
||||
&self,
|
||||
thread_root: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let thread_root = thread_root.map(EventId::parse).transpose()?;
|
||||
Ok(self.inner.clear_composer_draft(thread_root.as_deref()).await?)
|
||||
}
|
||||
|
||||
/// Edit an event given its event id.
|
||||
@@ -1271,54 +1236,6 @@ pub fn matrix_to_room_alias_permalink(
|
||||
Ok(room_alias.matrix_to_uri().to_string())
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomPowerLevels {
|
||||
/// The level required to ban a user.
|
||||
pub ban: i64,
|
||||
/// The level required to invite a user.
|
||||
pub invite: i64,
|
||||
/// The level required to kick a user.
|
||||
pub kick: i64,
|
||||
/// The level required to redact an event.
|
||||
pub redact: i64,
|
||||
/// The default level required to send message events.
|
||||
pub events_default: i64,
|
||||
/// The default level required to send state events.
|
||||
pub state_default: i64,
|
||||
/// The default power level for every user in the room.
|
||||
pub users_default: i64,
|
||||
/// The level required to change the room's name.
|
||||
pub room_name: i64,
|
||||
/// The level required to change the room's avatar.
|
||||
pub room_avatar: i64,
|
||||
/// The level required to change the room's topic.
|
||||
pub room_topic: i64,
|
||||
}
|
||||
|
||||
impl From<RumaPowerLevels> for RoomPowerLevels {
|
||||
fn from(value: RumaPowerLevels) -> Self {
|
||||
fn state_event_level_for(
|
||||
power_levels: &RumaPowerLevels,
|
||||
event_type: &TimelineEventType,
|
||||
) -> i64 {
|
||||
let default_state: i64 = power_levels.state_default.into();
|
||||
power_levels.events.get(event_type).map_or(default_state, |&level| level.into())
|
||||
}
|
||||
Self {
|
||||
ban: value.ban.into(),
|
||||
invite: value.invite.into(),
|
||||
kick: value.kick.into(),
|
||||
redact: value.redact.into(),
|
||||
events_default: value.events_default.into(),
|
||||
state_default: value.state_default.into(),
|
||||
users_default: value.users_default.into(),
|
||||
room_name: state_event_level_for(&value, &TimelineEventType::RoomName),
|
||||
room_avatar: state_event_level_for(&value, &TimelineEventType::RoomAvatar),
|
||||
room_topic: state_event_level_for(&value, &TimelineEventType::RoomTopic),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomInfoListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn call(&self, room_info: RoomInfo);
|
||||
@@ -0,0 +1,233 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use ruma::{
|
||||
events::{room::power_levels::RoomPowerLevels as RumaPowerLevels, TimelineEventType},
|
||||
OwnedUserId, UserId,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::ClientError,
|
||||
event::{MessageLikeEventType, StateEventType},
|
||||
};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomPowerLevels {
|
||||
inner: RumaPowerLevels,
|
||||
own_user_id: OwnedUserId,
|
||||
}
|
||||
|
||||
impl RoomPowerLevels {
|
||||
pub fn new(value: RumaPowerLevels, own_user_id: OwnedUserId) -> Self {
|
||||
Self { inner: value, own_user_id }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomPowerLevels {
|
||||
fn values(&self) -> RoomPowerLevelsValues {
|
||||
self.inner.clone().into()
|
||||
}
|
||||
|
||||
/// Gets a map with the `UserId` of users with power levels other than `0`
|
||||
/// and their power level.
|
||||
pub fn user_power_levels(&self) -> HashMap<String, i64> {
|
||||
let mut user_power_levels = HashMap::<String, i64>::new();
|
||||
|
||||
for (id, level) in self.inner.users.iter() {
|
||||
user_power_levels.insert(id.to_string(), (*level).into());
|
||||
}
|
||||
|
||||
user_power_levels
|
||||
}
|
||||
|
||||
/// Returns true if the current user is able to ban in the room.
|
||||
pub fn can_own_user_ban(&self) -> bool {
|
||||
self.inner.user_can_ban(&self.own_user_id)
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to ban in the
|
||||
/// room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_ban(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_ban(&user_id))
|
||||
}
|
||||
|
||||
/// Returns true if the current user is able to redact their own messages in
|
||||
/// the room.
|
||||
pub fn can_own_user_redact_own(&self) -> bool {
|
||||
self.inner.user_can_redact_own_event(&self.own_user_id)
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to redact
|
||||
/// their own messages in the room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_redact_own(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_redact_own_event(&user_id))
|
||||
}
|
||||
|
||||
/// Returns true if the current user user is able to redact messages of
|
||||
/// other users in the room.
|
||||
pub fn can_own_user_redact_other(&self) -> bool {
|
||||
self.inner.user_can_redact_event_of_other(&self.own_user_id)
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to redact
|
||||
/// messages of other users in the room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_redact_other(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_redact_event_of_other(&user_id))
|
||||
}
|
||||
|
||||
/// Returns true if the current user is able to invite in the room.
|
||||
pub fn can_own_user_invite(&self) -> bool {
|
||||
self.inner.user_can_invite(&self.own_user_id)
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to invite in the
|
||||
/// room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_invite(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_invite(&user_id))
|
||||
}
|
||||
|
||||
/// Returns true if the current user is able to kick in the room.
|
||||
pub fn can_own_user_kick(&self) -> bool {
|
||||
self.inner.user_can_kick(&self.own_user_id)
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to kick in the
|
||||
/// room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_kick(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_kick(&user_id))
|
||||
}
|
||||
|
||||
/// Returns true if the current user is able to send a specific state event
|
||||
/// type in the room.
|
||||
pub fn can_own_user_send_state(&self, state_event: StateEventType) -> bool {
|
||||
self.inner.user_can_send_state(&self.own_user_id, state_event.into())
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to send a
|
||||
/// specific state event type in the room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_send_state(
|
||||
&self,
|
||||
user_id: String,
|
||||
state_event: StateEventType,
|
||||
) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_send_state(&user_id, state_event.into()))
|
||||
}
|
||||
|
||||
/// Returns true if the current user is able to send a specific message type
|
||||
/// in the room.
|
||||
pub fn can_own_user_send_message(&self, message: MessageLikeEventType) -> bool {
|
||||
self.inner.user_can_send_message(&self.own_user_id, message.into())
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to send a
|
||||
/// specific message type in the room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_send_message(
|
||||
&self,
|
||||
user_id: String,
|
||||
message: MessageLikeEventType,
|
||||
) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_send_message(&user_id, message.into()))
|
||||
}
|
||||
|
||||
/// Returns true if the current user is able to pin or unpin events in the
|
||||
/// room.
|
||||
pub fn can_own_user_pin_unpin(&self) -> bool {
|
||||
self.inner.user_can_send_state(&self.own_user_id, StateEventType::RoomPinnedEvents.into())
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to pin or unpin
|
||||
/// events in the room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_pin_unpin(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_send_state(&user_id, StateEventType::RoomPinnedEvents.into()))
|
||||
}
|
||||
|
||||
/// Returns true if the current user is able to trigger a notification in
|
||||
/// the room.
|
||||
pub fn can_own_user_trigger_room_notification(&self) -> bool {
|
||||
self.inner.user_can_trigger_room_notification(&self.own_user_id)
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to trigger a
|
||||
/// notification in the room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_trigger_room_notification(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_trigger_room_notification(&user_id))
|
||||
}
|
||||
}
|
||||
|
||||
/// This intermediary struct is used to expose the power levels values through
|
||||
/// FFI and work around it not exposing public exported object fields.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomPowerLevelsValues {
|
||||
/// The level required to ban a user.
|
||||
pub ban: i64,
|
||||
/// The level required to invite a user.
|
||||
pub invite: i64,
|
||||
/// The level required to kick a user.
|
||||
pub kick: i64,
|
||||
/// The level required to redact an event.
|
||||
pub redact: i64,
|
||||
/// The default level required to send message events.
|
||||
pub events_default: i64,
|
||||
/// The default level required to send state events.
|
||||
pub state_default: i64,
|
||||
/// The default power level for every user in the room.
|
||||
pub users_default: i64,
|
||||
/// The level required to change the room's name.
|
||||
pub room_name: i64,
|
||||
/// The level required to change the room's avatar.
|
||||
pub room_avatar: i64,
|
||||
/// The level required to change the room's topic.
|
||||
pub room_topic: i64,
|
||||
}
|
||||
|
||||
impl From<RumaPowerLevels> for RoomPowerLevelsValues {
|
||||
fn from(value: RumaPowerLevels) -> Self {
|
||||
fn state_event_level_for(
|
||||
power_levels: &RumaPowerLevels,
|
||||
event_type: &TimelineEventType,
|
||||
) -> i64 {
|
||||
let default_state: i64 = power_levels.state_default.into();
|
||||
power_levels.events.get(event_type).map_or(default_state, |&level| level.into())
|
||||
}
|
||||
Self {
|
||||
ban: value.ban.into(),
|
||||
invite: value.invite.into(),
|
||||
kick: value.kick.into(),
|
||||
redact: value.redact.into(),
|
||||
events_default: value.events_default.into(),
|
||||
state_default: value.state_default.into(),
|
||||
users_default: value.users_default.into(),
|
||||
room_name: state_event_level_for(&value, &TimelineEventType::RoomName),
|
||||
room_avatar: state_event_level_for(&value, &TimelineEventType::RoomAvatar),
|
||||
room_topic: state_event_level_for(&value, &TimelineEventType::RoomTopic),
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
-15
@@ -1,4 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk::{EncryptionState, RoomState};
|
||||
use tracing::warn;
|
||||
@@ -7,7 +7,9 @@ use crate::{
|
||||
client::JoinRule,
|
||||
error::ClientError,
|
||||
notification_settings::RoomNotificationMode,
|
||||
room::{Membership, RoomHero, RoomHistoryVisibility, SuccessorRoom},
|
||||
room::{
|
||||
power_levels::RoomPowerLevels, Membership, RoomHero, RoomHistoryVisibility, SuccessorRoom,
|
||||
},
|
||||
room_member::RoomMember,
|
||||
};
|
||||
|
||||
@@ -24,7 +26,11 @@ pub struct RoomInfo {
|
||||
topic: Option<String>,
|
||||
avatar_url: Option<String>,
|
||||
is_direct: bool,
|
||||
is_public: bool,
|
||||
/// Whether the room is public or not, based on the join rules.
|
||||
///
|
||||
/// Can be `None` if the join rules state event is not available for this
|
||||
/// room.
|
||||
is_public: Option<bool>,
|
||||
is_space: bool,
|
||||
/// If present, it means the room has been archived/upgraded.
|
||||
successor_room: Option<SuccessorRoom>,
|
||||
@@ -42,7 +48,6 @@ pub struct RoomInfo {
|
||||
active_members_count: u64,
|
||||
invited_members_count: u64,
|
||||
joined_members_count: u64,
|
||||
user_power_levels: HashMap<String, i64>,
|
||||
highlight_count: u64,
|
||||
notification_count: u64,
|
||||
cached_user_defined_notification_mode: Option<RoomNotificationMode>,
|
||||
@@ -65,24 +70,34 @@ pub struct RoomInfo {
|
||||
join_rule: Option<JoinRule>,
|
||||
/// The history visibility for this room, if known.
|
||||
history_visibility: RoomHistoryVisibility,
|
||||
/// This room's current power levels.
|
||||
///
|
||||
/// Can be missing if the room power levels event is missing from the store.
|
||||
power_levels: Option<Arc<RoomPowerLevels>>,
|
||||
}
|
||||
|
||||
impl RoomInfo {
|
||||
pub(crate) async fn new(room: &matrix_sdk::Room) -> Result<Self, ClientError> {
|
||||
let unread_notification_counts = room.unread_notification_counts();
|
||||
|
||||
let power_levels_map = room.users_with_power_levels().await;
|
||||
let mut user_power_levels = HashMap::<String, i64>::new();
|
||||
for (id, level) in power_levels_map.iter() {
|
||||
user_power_levels.insert(id.to_string(), *level);
|
||||
}
|
||||
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:?}");
|
||||
}
|
||||
let join_rule = room
|
||||
.join_rule()
|
||||
.map(TryInto::try_into)
|
||||
.transpose()
|
||||
.inspect_err(|err| {
|
||||
warn!("Failed to parse join rule: {err}");
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
let power_levels = room
|
||||
.power_levels()
|
||||
.await
|
||||
.ok()
|
||||
.map(|p| RoomPowerLevels::new(p, room.own_user_id().to_owned()));
|
||||
|
||||
Ok(Self {
|
||||
id: room.room_id().to_string(),
|
||||
@@ -116,7 +131,6 @@ impl RoomInfo {
|
||||
active_members_count: room.active_members_count(),
|
||||
invited_members_count: room.invited_members_count(),
|
||||
joined_members_count: room.joined_members_count(),
|
||||
user_power_levels,
|
||||
highlight_count: unread_notification_counts.highlight_count,
|
||||
notification_count: unread_notification_counts.notification_count,
|
||||
cached_user_defined_notification_mode: room
|
||||
@@ -133,8 +147,9 @@ impl RoomInfo {
|
||||
num_unread_notifications: room.num_unread_notifications(),
|
||||
num_unread_mentions: room.num_unread_mentions(),
|
||||
pinned_event_ids,
|
||||
join_rule: join_rule.ok(),
|
||||
join_rule,
|
||||
history_visibility: room.history_visibility_or_default().try_into()?,
|
||||
power_levels: power_levels.map(Arc::new),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,10 @@ pub enum RoomListError {
|
||||
InvalidRoomId { error: String },
|
||||
#[error("Event cache ran into an error: {error}")]
|
||||
EventCache { error: String },
|
||||
#[error("The requested room doesn't match the membership requirements {expected:?}, observed {actual:?}")]
|
||||
#[error(
|
||||
"The requested room doesn't match the membership requirements {expected:?}, \
|
||||
observed {actual:?}"
|
||||
)]
|
||||
IncorrectRoomMembership { expected: Vec<Membership>, actual: Membership },
|
||||
}
|
||||
|
||||
@@ -118,7 +121,7 @@ impl RoomListService {
|
||||
})))
|
||||
}
|
||||
|
||||
fn subscribe_to_rooms(&self, room_ids: Vec<String>) -> Result<(), RoomListError> {
|
||||
async fn subscribe_to_rooms(&self, room_ids: Vec<String>) -> Result<(), RoomListError> {
|
||||
let room_ids = room_ids
|
||||
.into_iter()
|
||||
.map(|room_id| {
|
||||
@@ -126,7 +129,9 @@ impl RoomListService {
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
self.inner.subscribe_to_rooms(&room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>());
|
||||
self.inner
|
||||
.subscribe_to_rooms(&room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -37,8 +37,9 @@ impl RoomPreview {
|
||||
membership: info.state.map(|state| state.into()),
|
||||
join_rule: info
|
||||
.join_rule
|
||||
.clone()
|
||||
.try_into()
|
||||
.as_ref()
|
||||
.map(TryInto::try_into)
|
||||
.transpose()
|
||||
.map_err(|_| anyhow::anyhow!("unhandled SpaceRoomJoinRule kind"))?,
|
||||
is_direct: info.is_direct,
|
||||
heroes: info
|
||||
@@ -114,17 +115,17 @@ pub struct RoomPreviewInfo {
|
||||
/// 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,
|
||||
pub join_rule: Option<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 {
|
||||
impl TryFrom<&SpaceRoomJoinRule> for JoinRule {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(join_rule: SpaceRoomJoinRule) -> Result<Self, ()> {
|
||||
fn try_from(join_rule: &SpaceRoomJoinRule) -> Result<Self, ()> {
|
||||
Ok(match join_rule {
|
||||
SpaceRoomJoinRule::Invite => JoinRule::Invite,
|
||||
SpaceRoomJoinRule::Knock => JoinRule::Knock,
|
||||
|
||||
@@ -39,7 +39,7 @@ mod sys {
|
||||
mod sys {
|
||||
use std::future::Future;
|
||||
|
||||
use crate::executor::{spawn, JoinHandle};
|
||||
use matrix_sdk_common::executor::{spawn, JoinHandle};
|
||||
|
||||
/// A dummy guard that does nothing when dropped.
|
||||
/// This is used for the Wasm implementation to match
|
||||
|
||||
@@ -116,11 +116,8 @@ impl SessionVerificationController {
|
||||
/// Request verification for the current device
|
||||
pub async fn request_device_verification(&self) -> Result<(), ClientError> {
|
||||
let methods = vec![VerificationMethod::SasV1];
|
||||
let verification_request = self
|
||||
.user_identity
|
||||
.request_verification_with_methods(methods)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?;
|
||||
let verification_request =
|
||||
self.user_identity.request_verification_with_methods(methods).await?;
|
||||
|
||||
self.set_ongoing_verification_request(verification_request)
|
||||
}
|
||||
@@ -141,10 +138,7 @@ impl SessionVerificationController {
|
||||
|
||||
let methods = vec![VerificationMethod::SasV1];
|
||||
|
||||
let verification_request = user_identity
|
||||
.request_verification_with_methods(methods)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?;
|
||||
let verification_request = user_identity.request_verification_with_methods(methods).await?;
|
||||
|
||||
self.set_ongoing_verification_request(verification_request)
|
||||
}
|
||||
@@ -241,7 +235,10 @@ impl SessionVerificationController {
|
||||
if sender != self.user_identity.user_id() {
|
||||
if let Some(status) = self.encryption.cross_signing_status().await {
|
||||
if !status.is_complete() {
|
||||
warn!("Cannot verify other users until our own device's cross-signing status is complete: {status:?}");
|
||||
warn!(
|
||||
"Cannot verify other users until our own device's cross-signing status \
|
||||
is complete: {status:?}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,12 @@ impl SyncServiceBuilder {
|
||||
Arc::new(Self { builder, ..this })
|
||||
}
|
||||
|
||||
pub fn with_share_pos(self: Arc<Self>, enable: bool) -> Arc<Self> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
let builder = this.builder.with_share_pos(enable);
|
||||
Arc::new(Self { builder, ..this })
|
||||
}
|
||||
|
||||
pub async fn finish(self: Arc<Self>) -> Result<Arc<SyncService>, ClientError> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
Ok(Arc::new(SyncService {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use tokio::task::JoinHandle;
|
||||
use matrix_sdk_common::executor::JoinHandle;
|
||||
use tracing::debug;
|
||||
|
||||
/// A task handle is a way to keep the handle a task running by itself in
|
||||
|
||||
@@ -79,7 +79,6 @@ pub enum TimelineFocus {
|
||||
Thread {
|
||||
/// The thread root event ID to focus on.
|
||||
root_event_id: String,
|
||||
num_events: u16,
|
||||
},
|
||||
PinnedEvents {
|
||||
max_events_to_load: u16,
|
||||
@@ -108,7 +107,7 @@ impl TryFrom<TimelineFocus> for matrix_sdk_ui::timeline::TimelineFocus {
|
||||
hide_threaded_events,
|
||||
})
|
||||
}
|
||||
TimelineFocus::Thread { root_event_id, num_events } => {
|
||||
TimelineFocus::Thread { root_event_id } => {
|
||||
let parsed_root_event_id = EventId::parse(&root_event_id).map_err(|err| {
|
||||
FocusEventError::InvalidEventId {
|
||||
event_id: root_event_id.clone(),
|
||||
@@ -116,7 +115,7 @@ impl TryFrom<TimelineFocus> for matrix_sdk_ui::timeline::TimelineFocus {
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(Self::Thread { root_event_id: parsed_root_event_id, num_events })
|
||||
Ok(Self::Thread { root_event_id: parsed_root_event_id })
|
||||
}
|
||||
TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests } => {
|
||||
Ok(Self::PinnedEvents { max_events_to_load, max_concurrent_requests })
|
||||
|
||||
@@ -17,7 +17,7 @@ use std::{collections::HashMap, fmt::Write as _, fs, panic, sync::Arc};
|
||||
use anyhow::{Context, Result};
|
||||
use as_variant::as_variant;
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{pin_mut, StreamExt as _};
|
||||
use futures_util::pin_mut;
|
||||
use matrix_sdk::{
|
||||
attachment::{
|
||||
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
|
||||
@@ -30,6 +30,10 @@ use matrix_sdk::{
|
||||
reply::{EnforceThread, Reply},
|
||||
},
|
||||
};
|
||||
use matrix_sdk_common::{
|
||||
executor::{AbortHandle, JoinHandle},
|
||||
stream::StreamExt,
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{
|
||||
self, AttachmentSource, EventItemOrigin, Profile, TimelineDetails,
|
||||
TimelineUniqueId as SdkTimelineUniqueId,
|
||||
@@ -47,7 +51,6 @@ use ruma::{
|
||||
UnstablePollStartContentBlock,
|
||||
},
|
||||
},
|
||||
receipt::ReceiptThread,
|
||||
room::message::{
|
||||
LocationMessageEventContent, MessageType, ReplyWithinThread,
|
||||
RoomMessageEventContentWithoutRelation,
|
||||
@@ -56,10 +59,7 @@ use ruma::{
|
||||
},
|
||||
EventId, UInt,
|
||||
};
|
||||
use tokio::{
|
||||
sync::Mutex,
|
||||
task::{AbortHandle, JoinHandle},
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{error, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -350,9 +350,7 @@ impl Timeline {
|
||||
event_id: String,
|
||||
) -> Result<(), ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
self.inner
|
||||
.send_single_receipt(receipt_type.into(), ReceiptThread::Unthreaded, event_id)
|
||||
.await?;
|
||||
self.inner.send_single_receipt(receipt_type.into(), event_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -380,7 +378,7 @@ impl Timeline {
|
||||
Ok(handle) => Ok(Arc::new(SendHandle::new(handle))),
|
||||
Err(err) => {
|
||||
error!("error when sending a message: {err}");
|
||||
Err(anyhow::anyhow!(err).into())
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -533,10 +531,7 @@ impl Timeline {
|
||||
msg: Arc<RoomMessageEventContentWithoutRelation>,
|
||||
reply_params: ReplyParameters,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner
|
||||
.send_reply((*msg).clone(), reply_params.try_into()?)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!(err))?;
|
||||
self.inner.send_reply((*msg).clone(), reply_params.try_into()?).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -566,7 +561,10 @@ impl Timeline {
|
||||
let event_id = match event_or_transaction_id {
|
||||
EventOrTransactionId::EventId { event_id } => EventId::parse(event_id)?,
|
||||
EventOrTransactionId::TransactionId { .. } => {
|
||||
warn!("trying to apply an edit to a local echo that doesn't exist in this timeline, aborting");
|
||||
warn!(
|
||||
"trying to apply an edit to a local echo that doesn't exist \
|
||||
in this timeline, aborting"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -587,7 +585,8 @@ impl Timeline {
|
||||
description: Option<String>,
|
||||
zoom_level: Option<u8>,
|
||||
asset_type: Option<AssetType>,
|
||||
) {
|
||||
reply_params: Option<ReplyParameters>,
|
||||
) -> Result<(), ClientError> {
|
||||
let mut location_event_message_content =
|
||||
LocationMessageEventContent::new(body, geo_uri.clone());
|
||||
|
||||
@@ -604,8 +603,13 @@ impl Timeline {
|
||||
let room_message_event_content = RoomMessageEventContentWithoutRelation::new(
|
||||
MessageType::Location(location_event_message_content),
|
||||
);
|
||||
// Errors are logged in `Self::send` already.
|
||||
let _ = self.send(Arc::new(room_message_event_content)).await;
|
||||
|
||||
if let Some(reply_params) = reply_params {
|
||||
self.send_reply(Arc::new(room_message_event_content), reply_params).await
|
||||
} else {
|
||||
self.send(Arc::new(room_message_event_content)).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle a reaction on an event.
|
||||
@@ -630,7 +634,10 @@ impl Timeline {
|
||||
|
||||
pub async fn fetch_details_for_event(&self, event_id: String) -> Result<(), ClientError> {
|
||||
let event_id = <&EventId>::try_from(event_id.as_str())?;
|
||||
self.inner.fetch_details_for_event(event_id).await.context("Fetching event details")?;
|
||||
self.inner
|
||||
.fetch_details_for_event(event_id)
|
||||
.await
|
||||
.map_err(|e| ClientError::from_str(e, Some("Fetching event details".to_owned())))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -694,6 +701,8 @@ impl Timeline {
|
||||
content: replied_to.content.clone().into(),
|
||||
sender: replied_to.sender.to_string(),
|
||||
sender_profile: replied_to.sender_profile.into(),
|
||||
timestamp: replied_to.timestamp.into(),
|
||||
event_or_transaction_id: replied_to.identifier.into(),
|
||||
},
|
||||
))),
|
||||
|
||||
@@ -1229,7 +1238,10 @@ impl SendAttachmentJoinHandle {
|
||||
return Ok(());
|
||||
}
|
||||
error!("task panicked! resuming panic from here.");
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
panic::resume_unwind(err.into_panic());
|
||||
#[cfg(target_family = "wasm")]
|
||||
panic!("task panicked! {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1373,24 +1385,22 @@ impl LazyTimelineItemProvider {
|
||||
mod galleries {
|
||||
use std::{panic, sync::Arc};
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use matrix_sdk::{
|
||||
attachment::{
|
||||
AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo, Thumbnail,
|
||||
},
|
||||
utils::formatted_body_from,
|
||||
};
|
||||
use matrix_sdk_common::executor::{AbortHandle, JoinHandle};
|
||||
use matrix_sdk_ui::timeline::GalleryConfig;
|
||||
use mime::Mime;
|
||||
use tokio::{
|
||||
sync::Mutex,
|
||||
task::{AbortHandle, JoinHandle},
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
error::RoomError,
|
||||
ruma::{AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, VideoInfo},
|
||||
runtime::get_runtime_handle,
|
||||
timeline::{build_thumbnail_info, ReplyParameters, Timeline},
|
||||
};
|
||||
|
||||
@@ -1560,7 +1570,10 @@ mod galleries {
|
||||
return Ok(());
|
||||
}
|
||||
error!("task panicked! resuming panic from here.");
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
panic::resume_unwind(err.into_panic());
|
||||
#[cfg(target_family = "wasm")]
|
||||
panic!("task panicked! {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +241,7 @@ pub struct PollAnswer {
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct ThreadSummary {
|
||||
pub latest_event: EmbeddedEventDetails,
|
||||
pub num_replies: usize,
|
||||
pub num_replies: u32,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
@@ -249,6 +249,10 @@ impl ThreadSummary {
|
||||
pub fn latest_event(&self) -> EmbeddedEventDetails {
|
||||
self.latest_event.clone()
|
||||
}
|
||||
|
||||
pub fn num_replies(&self) -> u64 {
|
||||
self.num_replies as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::ThreadSummary> for ThreadSummary {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
use matrix_sdk_ui::timeline::{EmbeddedEvent, TimelineDetails};
|
||||
|
||||
use super::{content::TimelineItemContent, ProfileDetails};
|
||||
use crate::{event::EventOrTransactionId, utils::Timestamp};
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct InReplyToDetails {
|
||||
@@ -50,8 +51,16 @@ impl From<matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
|
||||
pub enum EmbeddedEventDetails {
|
||||
Unavailable,
|
||||
Pending,
|
||||
Ready { content: TimelineItemContent, sender: String, sender_profile: ProfileDetails },
|
||||
Error { message: String },
|
||||
Ready {
|
||||
content: TimelineItemContent,
|
||||
sender: String,
|
||||
sender_profile: ProfileDetails,
|
||||
timestamp: Timestamp,
|
||||
event_or_transaction_id: EventOrTransactionId,
|
||||
},
|
||||
Error {
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<TimelineDetails<Box<EmbeddedEvent>>> for EmbeddedEventDetails {
|
||||
@@ -63,6 +72,8 @@ impl From<TimelineDetails<Box<EmbeddedEvent>>> for EmbeddedEventDetails {
|
||||
content: event.content.into(),
|
||||
sender: event.sender.to_string(),
|
||||
sender_profile: event.sender_profile.into(),
|
||||
timestamp: event.timestamp.into(),
|
||||
event_or_transaction_id: event.identifier.into(),
|
||||
},
|
||||
TimelineDetails::Error(err) => EmbeddedEventDetails::Error { message: err.to_string() },
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use tracing_core::{identify_callsite, metadata::Kind as MetadataKind};
|
||||
/// Log an event.
|
||||
///
|
||||
/// The target should be something like a module path, and can be referenced in
|
||||
/// the filter string given to `setup_tracing`. `level` and `target` for a
|
||||
/// the filter string given to `init_platform`. `level` and `target` for a
|
||||
/// callsite are fixed at the first `log_event` call for that callsite and can
|
||||
/// not be changed afterwards, i.e. the level and target passed for second and
|
||||
/// following `log_event`s with the same callsite will be ignored.
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use language_tags::LanguageTag;
|
||||
use matrix_sdk::{
|
||||
async_trait,
|
||||
widget::{MessageLikeEventFilter, StateEventFilter, ToDeviceEventFilter},
|
||||
};
|
||||
use matrix_sdk::widget::{MessageLikeEventFilter, StateEventFilter, ToDeviceEventFilter};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::events::MessageLikeEventType;
|
||||
use tracing::error;
|
||||
@@ -113,174 +110,6 @@ pub async fn generate_webview_url(
|
||||
.map(|url| url.to_string())?)
|
||||
}
|
||||
|
||||
/// Defines if a call is encrypted and which encryption system should be used.
|
||||
///
|
||||
/// This controls the url parameters: `perParticipantE2EE`, `password`.
|
||||
#[derive(uniffi::Enum, Clone)]
|
||||
pub enum EncryptionSystem {
|
||||
/// Equivalent to the element call url parameter: `enableE2EE=false`
|
||||
Unencrypted,
|
||||
/// Equivalent to the element call url parameter:
|
||||
/// `perParticipantE2EE=true`
|
||||
PerParticipantKeys,
|
||||
/// Equivalent to the element call url parameter:
|
||||
/// `password={secret}`
|
||||
SharedSecret {
|
||||
/// The secret/password which is used in the url.
|
||||
secret: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<EncryptionSystem> for matrix_sdk::widget::EncryptionSystem {
|
||||
fn from(value: EncryptionSystem) -> Self {
|
||||
match value {
|
||||
EncryptionSystem::Unencrypted => Self::Unencrypted,
|
||||
EncryptionSystem::PerParticipantKeys => Self::PerParticipantKeys,
|
||||
EncryptionSystem::SharedSecret { secret } => Self::SharedSecret { secret },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the intent of showing the call.
|
||||
///
|
||||
/// This controls whether to show or skip the lobby.
|
||||
#[derive(uniffi::Enum, Clone)]
|
||||
pub enum Intent {
|
||||
/// The user wants to start a call.
|
||||
StartCall,
|
||||
/// The user wants to join an existing call.
|
||||
JoinExisting,
|
||||
}
|
||||
impl From<Intent> for matrix_sdk::widget::Intent {
|
||||
fn from(value: Intent) -> Self {
|
||||
match value {
|
||||
Intent::StartCall => Self::StartCall,
|
||||
Intent::JoinExisting => Self::JoinExisting,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Properties to create a new virtual Element Call widget.
|
||||
#[derive(uniffi::Record, Clone)]
|
||||
pub struct VirtualElementCallWidgetOptions {
|
||||
/// The url to the Element Call app including any `/room` path if required.
|
||||
///
|
||||
/// E.g. <https://call.element.io>, <https://call.element.dev>, <https://call.element.dev/room>
|
||||
pub element_call_url: String,
|
||||
|
||||
/// The widget id.
|
||||
pub widget_id: String,
|
||||
|
||||
/// The url that is used as the target for the PostMessages sent
|
||||
/// by the widget (to the client).
|
||||
///
|
||||
/// For a web app client this is the client url. In case of using other
|
||||
/// platforms the client most likely is setup up to listen to
|
||||
/// postmessages in the same webview the widget is hosted. In this case
|
||||
/// the `parent_url` is set to the url of the webview with the widget. Be
|
||||
/// aware that this means that the widget will receive its own postmessage
|
||||
/// messages. The `matrix-widget-api` (js) ignores those so this works but
|
||||
/// it might break custom implementations.
|
||||
///
|
||||
/// Defaults to `element_call_url` for the non-iframe (dedicated webview)
|
||||
/// usecase.
|
||||
pub parent_url: Option<String>,
|
||||
|
||||
/// Whether the branding header of Element call should be hidden.
|
||||
///
|
||||
/// Default: `true`
|
||||
pub hide_header: Option<bool>,
|
||||
|
||||
/// If set, the lobby will be skipped and the widget will join the
|
||||
/// call on the `io.element.join` action.
|
||||
///
|
||||
/// Default: `false`
|
||||
pub preload: Option<bool>,
|
||||
|
||||
/// The font scale which will be used inside element call.
|
||||
///
|
||||
/// Default: `1`
|
||||
pub font_scale: Option<f64>,
|
||||
|
||||
/// Whether element call should prompt the user to open in the browser or
|
||||
/// the app.
|
||||
///
|
||||
/// Default: `false`
|
||||
pub app_prompt: Option<bool>,
|
||||
|
||||
/// Make it not possible to get to the calls list in the webview.
|
||||
///
|
||||
/// Default: `true`
|
||||
pub confine_to_room: Option<bool>,
|
||||
|
||||
/// The font to use, to adapt to the system font.
|
||||
pub font: Option<String>,
|
||||
|
||||
/// The encryption system to use.
|
||||
///
|
||||
/// Use `EncryptionSystem::Unencrypted` to disable encryption.
|
||||
pub encryption: EncryptionSystem,
|
||||
|
||||
/// The intent of showing the call.
|
||||
/// If the user wants to start a call or join an existing one.
|
||||
/// Controls if the lobby is skipped or not.
|
||||
pub intent: Option<Intent>,
|
||||
|
||||
/// Do not show the screenshare button.
|
||||
pub hide_screensharing: bool,
|
||||
|
||||
/// Can be used to pass a PostHog id to element call.
|
||||
pub posthog_user_id: Option<String>,
|
||||
/// The host of the posthog api.
|
||||
/// Supported since Element Call v0.9.0. Only used by the embedded package.
|
||||
pub posthog_api_host: Option<String>,
|
||||
/// The key for the posthog api.
|
||||
/// Supported since Element Call v0.9.0. Only used by the embedded package.
|
||||
pub posthog_api_key: Option<String>,
|
||||
|
||||
/// The url to use for submitting rageshakes.
|
||||
/// Supported since Element Call v0.9.0. Only used by the embedded package.
|
||||
pub rageshake_submit_url: Option<String>,
|
||||
|
||||
/// Sentry [DSN](https://docs.sentry.io/concepts/key-terms/dsn-explainer/)
|
||||
/// Supported since Element Call v0.9.0. Only used by the embedded package.
|
||||
pub sentry_dsn: Option<String>,
|
||||
/// Sentry [environment](https://docs.sentry.io/concepts/key-terms/key-terms/)
|
||||
/// Supported since Element Call v0.9.0. Only used by the embedded package.
|
||||
pub sentry_environment: Option<String>,
|
||||
//// - `true`: The webview should show the list of media devices it detects using
|
||||
//// `enumerateDevices`.
|
||||
/// - `false`: the webview shows a a list of devices injected by the
|
||||
/// client. (used on ios & android)
|
||||
pub controlled_media_devices: bool,
|
||||
}
|
||||
|
||||
impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElementCallWidgetOptions {
|
||||
fn from(value: VirtualElementCallWidgetOptions) -> Self {
|
||||
Self {
|
||||
element_call_url: value.element_call_url,
|
||||
widget_id: value.widget_id,
|
||||
parent_url: value.parent_url,
|
||||
hide_header: value.hide_header,
|
||||
preload: value.preload,
|
||||
font_scale: value.font_scale,
|
||||
app_prompt: value.app_prompt,
|
||||
confine_to_room: value.confine_to_room,
|
||||
font: value.font,
|
||||
posthog_user_id: value.posthog_user_id,
|
||||
encryption: value.encryption.into(),
|
||||
intent: value.intent.map(Into::into),
|
||||
hide_screensharing: value.hide_screensharing,
|
||||
posthog_api_host: value.posthog_api_host,
|
||||
posthog_api_key: value.posthog_api_key,
|
||||
rageshake_submit_url: value.rageshake_submit_url,
|
||||
sentry_dsn: value.sentry_dsn,
|
||||
sentry_environment: value.sentry_environment,
|
||||
controlled_media_devices: value.controlled_media_devices,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `WidgetSettings` are usually created from a state event.
|
||||
/// (currently unimplemented)
|
||||
///
|
||||
@@ -296,9 +125,9 @@ impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElemen
|
||||
/// call widget.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn new_virtual_element_call_widget(
|
||||
props: VirtualElementCallWidgetOptions,
|
||||
props: matrix_sdk::widget::VirtualElementCallWidgetOptions,
|
||||
) -> Result<WidgetSettings, ParseError> {
|
||||
Ok(matrix_sdk::widget::WidgetSettings::new_virtual_element_call_widget(props.into())
|
||||
Ok(matrix_sdk::widget::WidgetSettings::new_virtual_element_call_widget(props)
|
||||
.map(|w| w.into())?)
|
||||
}
|
||||
|
||||
@@ -352,6 +181,8 @@ pub fn get_element_call_required_permissions(
|
||||
read: vec![
|
||||
// To compute the current state of the matrixRTC session.
|
||||
WidgetEventFilter::StateWithType { event_type: StateEventType::CallMember.to_string() },
|
||||
// To display the name of the room.
|
||||
WidgetEventFilter::StateWithType { event_type: StateEventType::RoomName.to_string() },
|
||||
// To detect leaving/kicked room members during a call.
|
||||
WidgetEventFilter::StateWithType { event_type: StateEventType::RoomMember.to_string() },
|
||||
// To decide whether to encrypt the call streams based on the room encryption setting.
|
||||
@@ -553,8 +384,6 @@ pub trait WidgetCapabilitiesProvider: SendOutsideWasm + SyncOutsideWasm {
|
||||
|
||||
struct CapabilitiesProviderWrap(Arc<dyn WidgetCapabilitiesProvider>);
|
||||
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
impl matrix_sdk::widget::CapabilitiesProvider for CapabilitiesProviderWrap {
|
||||
async fn acquire_capabilities(
|
||||
&self,
|
||||
@@ -656,14 +485,21 @@ mod tests {
|
||||
cap_assert("org.matrix.msc4157.update_delayed_event");
|
||||
cap_assert("org.matrix.msc4157.send.delayed_event");
|
||||
cap_assert("org.matrix.msc2762.receive.state_event:org.matrix.msc3401.call.member");
|
||||
cap_assert("org.matrix.msc2762.receive.state_event:m.room.name");
|
||||
cap_assert("org.matrix.msc2762.receive.state_event:m.room.member");
|
||||
cap_assert("org.matrix.msc2762.receive.state_event:m.room.encryption");
|
||||
cap_assert("org.matrix.msc2762.receive.event:org.matrix.rageshake_request");
|
||||
cap_assert("org.matrix.msc2762.receive.event:io.element.call.encryption_keys");
|
||||
cap_assert("org.matrix.msc2762.receive.state_event:m.room.create");
|
||||
cap_assert("org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org");
|
||||
cap_assert("org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org_ABCDEFGHI");
|
||||
cap_assert("org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#_@my_user:my_domain.org_ABCDEFGHI");
|
||||
cap_assert(
|
||||
"org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org",
|
||||
);
|
||||
cap_assert(
|
||||
"org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org_ABCDEFGHI",
|
||||
);
|
||||
cap_assert(
|
||||
"org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#_@my_user:my_domain.org_ABCDEFGHI",
|
||||
);
|
||||
cap_assert("org.matrix.msc2762.send.event:org.matrix.rageshake_request");
|
||||
cap_assert("org.matrix.msc2762.send.event:io.element.call.encryption_keys");
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -6,6 +6,30 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
## [0.13.0] - 2025-07-10
|
||||
|
||||
### Features
|
||||
- The `RoomInfo` now remembers when an invite was explicitly accepted when the
|
||||
`BaseClient::room_joined()` method was called. A new getter for this
|
||||
timestamp exists, the `RoomInfo::invite_accepted_at()` method returns this
|
||||
timestamp.
|
||||
([#5333](https://github.com/matrix-org/matrix-rust-sdk/pull/5333))
|
||||
- [**breaking**] The `BaseClient::new()` method now takes an additional `ThreadingSupport`
|
||||
parameter controlling whether the client is supposed to do extra processing for threads. Right
|
||||
now, it controls whether to exclude in-thread events from the room unread counts, but it may be
|
||||
expanded in the future to support more threading-related features.
|
||||
([#5325](https://github.com/matrix-org/matrix-rust-sdk/pull/5325))
|
||||
|
||||
### Refactor
|
||||
|
||||
- The cached `ServerCapabilities` has been renamed to `ServerInfo` and
|
||||
additionally contains the well-known response alongside the existing server versions.
|
||||
Despite the old name, it does not contain the server capabilities.
|
||||
([#5167](https://github.com/matrix-org/matrix-rust-sdk/pull/5167))
|
||||
- `Room::join_rule` and `Room::is_public` now return an `Option` to reflect that the join rule
|
||||
state event might be missing, in which case they will return `None`.
|
||||
([#5278](https://github.com/matrix-org/matrix-rust-sdk/pull/5278))
|
||||
|
||||
## [0.12.0] - 2025-06-10
|
||||
|
||||
No notable changes in this release.
|
||||
|
||||
@@ -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.12.0"
|
||||
version = "0.13.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
@@ -86,6 +86,7 @@ unicode-normalization.workspace = true
|
||||
uniffi = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow.workspace = true
|
||||
assert_matches.workspace = true
|
||||
assert_matches2.workspace = true
|
||||
assign = "1.1.1"
|
||||
|
||||
@@ -26,8 +26,8 @@ use eyeball_im::{Vector, VectorDiff};
|
||||
use futures_util::Stream;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_crypto::{
|
||||
store::DynCryptoStore, types::requests::ToDeviceRequest, CollectStrategy, EncryptionSettings,
|
||||
OlmError, OlmMachine, TrustRequirement,
|
||||
store::DynCryptoStore, types::requests::ToDeviceRequest, CollectStrategy, DecryptionSettings,
|
||||
EncryptionSettings, OlmError, OlmMachine, TrustRequirement,
|
||||
};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::events::room::{history_visibility::HistoryVisibility, member::MembershipState};
|
||||
@@ -76,11 +76,12 @@ use crate::{
|
||||
/// rather through `matrix_sdk::Client`.
|
||||
///
|
||||
/// ```rust
|
||||
/// use matrix_sdk_base::{store::StoreConfig, BaseClient};
|
||||
/// use matrix_sdk_base::{store::StoreConfig, BaseClient, ThreadingSupport};
|
||||
///
|
||||
/// let client = BaseClient::new(StoreConfig::new(
|
||||
/// "cross-process-holder-name".to_owned(),
|
||||
/// ));
|
||||
/// let client = BaseClient::new(
|
||||
/// StoreConfig::new("cross-process-holder-name".to_owned()),
|
||||
/// ThreadingSupport::Disabled,
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct BaseClient {
|
||||
@@ -115,13 +116,16 @@ pub struct BaseClient {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub room_key_recipient_strategy: CollectStrategy,
|
||||
|
||||
/// The trust requirement to use for decrypting events.
|
||||
/// The settings to use for decrypting events.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub decryption_trust_requirement: TrustRequirement,
|
||||
pub decryption_settings: DecryptionSettings,
|
||||
|
||||
/// If the client should handle verification events received when syncing.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub handle_verification_events: bool,
|
||||
|
||||
/// Whether the client supports threads or not.
|
||||
pub threading_support: ThreadingSupport,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
@@ -134,6 +138,25 @@ impl fmt::Debug for BaseClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this client instance supports threading or not. Currently used to
|
||||
/// determine how the client handles read receipts and unread count computations
|
||||
/// on the base SDK level.
|
||||
///
|
||||
/// Timelines on the other hand have a separate `TimelineFocus`
|
||||
/// `hide_threaded_events` associated value that can be used to hide threaded
|
||||
/// events but also to enable threaded read receipt sending. This is because
|
||||
/// certain timeline instances should ignore threading no matter what's defined
|
||||
/// at the client level. One such example are media filtered timelines which
|
||||
/// should contain all the room's media no matter what thread its in (unless
|
||||
/// explicitly opted into).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ThreadingSupport {
|
||||
/// Threading enabled
|
||||
Enabled,
|
||||
/// Threading disabled
|
||||
Disabled,
|
||||
}
|
||||
|
||||
impl BaseClient {
|
||||
/// Create a new client.
|
||||
///
|
||||
@@ -141,7 +164,7 @@ impl BaseClient {
|
||||
///
|
||||
/// * `config` - the configuration for the stores (state store, event cache
|
||||
/// store and crypto store).
|
||||
pub fn new(config: StoreConfig) -> Self {
|
||||
pub fn new(config: StoreConfig, threading_support: ThreadingSupport) -> Self {
|
||||
let store = BaseStateStore::new(config.state_store);
|
||||
|
||||
// Create the channel to receive `RoomInfoNotableUpdate`.
|
||||
@@ -168,9 +191,12 @@ impl BaseClient {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
room_key_recipient_strategy: Default::default(),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
decryption_trust_requirement: TrustRequirement::Untrusted,
|
||||
decryption_settings: DecryptionSettings {
|
||||
sender_device_trust_requirement: TrustRequirement::Untrusted,
|
||||
},
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
handle_verification_events: true,
|
||||
threading_support,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,8 +226,9 @@ impl BaseClient {
|
||||
ignore_user_list_changes: Default::default(),
|
||||
room_info_notable_update_sender: self.room_info_notable_update_sender.clone(),
|
||||
room_key_recipient_strategy: self.room_key_recipient_strategy.clone(),
|
||||
decryption_trust_requirement: self.decryption_trust_requirement,
|
||||
decryption_settings: self.decryption_settings.clone(),
|
||||
handle_verification_events,
|
||||
threading_support: self.threading_support,
|
||||
};
|
||||
|
||||
copy.state_store
|
||||
@@ -222,7 +249,7 @@ impl BaseClient {
|
||||
) -> Result<Self> {
|
||||
let config = StoreConfig::new(cross_process_store_locks_holder.to_owned())
|
||||
.state_store(MemoryStore::new());
|
||||
Ok(Self::new(config))
|
||||
Ok(Self::new(config, ThreadingSupport::Disabled))
|
||||
}
|
||||
|
||||
/// Get the session meta information.
|
||||
@@ -392,9 +419,33 @@ impl BaseClient {
|
||||
Ok(room)
|
||||
}
|
||||
|
||||
/// User has joined a room.
|
||||
/// The user has joined a room using this specific client.
|
||||
///
|
||||
/// This method should be called if the user accepts an invite or if they
|
||||
/// join a public room.
|
||||
///
|
||||
/// The method will create a [`Room`] object if one does not exist yet and
|
||||
/// set the state of the [`Room`] to [`RoomState::Joined`]. The [`Room`]
|
||||
/// object will be persisted in the cache. Please note that the [`Room`]
|
||||
/// will be a stub until a sync has been received with the full room
|
||||
/// state using [`BaseClient::receive_sync_response`].
|
||||
///
|
||||
/// Update the internal and cached state accordingly. Return the final Room.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use matrix_sdk_base::{BaseClient, store::StoreConfig, RoomState, ThreadingSupport};
|
||||
/// # use ruma::OwnedRoomId;
|
||||
/// # async {
|
||||
/// # let client = BaseClient::new(StoreConfig::new("example".to_owned()), ThreadingSupport::Disabled);
|
||||
/// # async fn send_join_request() -> anyhow::Result<OwnedRoomId> { todo!() }
|
||||
/// let room_id = send_join_request().await?;
|
||||
/// let room = client.room_joined(&room_id).await?;
|
||||
///
|
||||
/// assert_eq!(room.state(), RoomState::Joined);
|
||||
/// # anyhow::Ok(()) };
|
||||
/// ```
|
||||
pub async fn room_joined(&self, room_id: &RoomId) -> Result<Room> {
|
||||
let room = self.state_store.get_or_create_room(
|
||||
room_id,
|
||||
@@ -402,16 +453,36 @@ impl BaseClient {
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
);
|
||||
|
||||
// If the state isn't `RoomState::Joined` then this means that we knew about
|
||||
// this room before. Let's modify the existing state now.
|
||||
if room.state() != RoomState::Joined {
|
||||
let _sync_lock = self.sync_lock().lock().await;
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
|
||||
// If our previous state was an invite and we're now in the joined state, this
|
||||
// means that the user has explicitly accepted the invite. Let's
|
||||
// remember when this has happened.
|
||||
//
|
||||
// This is somewhat of a workaround for our lack of cryptographic membership.
|
||||
// Later on we will decide if historic room keys should be accepted
|
||||
// based on this info. If a user has accepted an invite and we receive a room
|
||||
// key bundle shortly after, we might accept it. If we don't do
|
||||
// this, the homeserver could trick us into accepting any historic room key
|
||||
// bundle.
|
||||
if room.state() == RoomState::Invited {
|
||||
room_info.set_invite_accepted_now();
|
||||
}
|
||||
|
||||
room_info.mark_as_joined();
|
||||
room_info.mark_state_partially_synced();
|
||||
room_info.mark_members_missing(); // the own member event changed
|
||||
|
||||
let mut changes = StateChanges::default();
|
||||
changes.add_room(room_info.clone());
|
||||
|
||||
self.state_store.save_changes(&changes).await?; // Update the store
|
||||
|
||||
room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
|
||||
}
|
||||
|
||||
@@ -496,7 +567,7 @@ impl BaseClient {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
let to_device = {
|
||||
let processors::e2ee::to_device::Output {
|
||||
decrypted_to_device_events: to_device,
|
||||
processed_to_device_events: to_device,
|
||||
room_key_updates,
|
||||
} = processors::e2ee::to_device::from_sync_v2(&response, olm_machine.as_ref()).await?;
|
||||
|
||||
@@ -509,7 +580,7 @@ impl BaseClient {
|
||||
.collect(),
|
||||
processors::e2ee::E2EE::new(
|
||||
olm_machine.as_ref(),
|
||||
self.decryption_trust_requirement,
|
||||
&self.decryption_settings,
|
||||
self.handle_verification_events,
|
||||
),
|
||||
)
|
||||
@@ -519,7 +590,22 @@ impl BaseClient {
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "e2e-encryption"))]
|
||||
let to_device = response.to_device.events;
|
||||
let to_device = response
|
||||
.to_device
|
||||
.events
|
||||
.into_iter()
|
||||
.map(|raw| {
|
||||
if let Ok(Some(event_type)) = raw.get_field::<String>("type") {
|
||||
if event_type == "m.room.encrypted" {
|
||||
matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent::UnableToDecrypt(raw)
|
||||
} else {
|
||||
matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent::PlainText(raw)
|
||||
}
|
||||
} else {
|
||||
matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent::Invalid(raw) // Exclude events with no type
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut ambiguity_cache = AmbiguityCache::new(self.state_store.inner.clone());
|
||||
|
||||
@@ -553,7 +639,7 @@ impl BaseClient {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
processors::e2ee::E2EE::new(
|
||||
olm_machine.as_ref(),
|
||||
self.decryption_trust_requirement,
|
||||
&self.decryption_settings,
|
||||
self.handle_verification_events,
|
||||
),
|
||||
)
|
||||
@@ -580,7 +666,7 @@ impl BaseClient {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
processors::e2ee::E2EE::new(
|
||||
olm_machine.as_ref(),
|
||||
self.decryption_trust_requirement,
|
||||
&self.decryption_settings,
|
||||
self.handle_verification_events,
|
||||
),
|
||||
)
|
||||
@@ -1063,6 +1149,7 @@ mod tests {
|
||||
|
||||
use super::{BaseClient, RequestedRequiredStates};
|
||||
use crate::{
|
||||
client::ThreadingSupport,
|
||||
store::{RoomLoadSettings, StateStoreExt, StoreConfig},
|
||||
test_utils::logged_in_base_client,
|
||||
RoomDisplayName, RoomState, SessionMeta,
|
||||
@@ -1357,8 +1444,10 @@ mod tests {
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
|
||||
|
||||
let client =
|
||||
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
|
||||
let client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
client
|
||||
.activate(
|
||||
SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
|
||||
@@ -1417,8 +1506,10 @@ mod tests {
|
||||
let inviter_user_id = user_id!("@bob:example.org");
|
||||
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
|
||||
|
||||
let client =
|
||||
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
|
||||
let client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
client
|
||||
.activate(
|
||||
SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
|
||||
@@ -1479,8 +1570,10 @@ mod tests {
|
||||
let inviter_user_id = user_id!("@bob:example.org");
|
||||
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
|
||||
|
||||
let client =
|
||||
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
|
||||
let client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
client
|
||||
.activate(
|
||||
SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
|
||||
@@ -1551,8 +1644,10 @@ mod tests {
|
||||
#[async_test]
|
||||
async fn test_ignored_user_list_changes() {
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
let client =
|
||||
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
|
||||
let client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
client
|
||||
.activate(
|
||||
@@ -1642,4 +1737,47 @@ mod tests {
|
||||
|
||||
assert!(client.is_user_ignored(ignored_user_id).await);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_joined_at_timestamp_is_set() {
|
||||
let client = logged_in_base_client(None).await;
|
||||
let invited_room_id = room_id!("!invited:localhost");
|
||||
let unknown_room_id = room_id!("!unknown:localhost");
|
||||
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
let response = sync_builder
|
||||
.add_invited_room(InvitedRoomBuilder::new(invited_room_id))
|
||||
.build_sync_response();
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
// Let us first check the initial state, we should have a room in the invite
|
||||
// state.
|
||||
let invited_room = client
|
||||
.get_room(invited_room_id)
|
||||
.expect("The sync should have created a room in the invited state");
|
||||
|
||||
assert_eq!(invited_room.state(), RoomState::Invited);
|
||||
assert!(invited_room.inner.get().invite_accepted_at().is_none());
|
||||
|
||||
// Now we join the room.
|
||||
let joined_room = client
|
||||
.room_joined(invited_room_id)
|
||||
.await
|
||||
.expect("We should be able to mark a room as joined");
|
||||
|
||||
// Yup, there's a timestamp now.
|
||||
assert_eq!(joined_room.state(), RoomState::Joined);
|
||||
assert!(joined_room.inner.get().invite_accepted_at().is_some());
|
||||
|
||||
// If we didn't know about the room before the join, we assume that there wasn't
|
||||
// an invite and we don't record the timestamp.
|
||||
assert!(client.get_room(unknown_room_id).is_none());
|
||||
let unknown_room = client
|
||||
.room_joined(unknown_room_id)
|
||||
.await
|
||||
.expect("We should be able to mark a room as joined");
|
||||
|
||||
assert_eq!(unknown_room.state(), RoomState::Joined);
|
||||
assert!(unknown_room.inner.get().invite_accepted_at().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
use std::fmt;
|
||||
|
||||
pub use matrix_sdk_common::debug::*;
|
||||
use matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent;
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::v3::{InvitedRoom, KnockedRoom},
|
||||
serde::Raw,
|
||||
@@ -35,6 +36,19 @@ impl<T> fmt::Debug for DebugListOfRawEventsNoId<'_, T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around a slice of `ProcessedToDeviceEvent` events that implements
|
||||
/// `Debug` in a way that only prints the event type of each item.
|
||||
pub struct DebugListOfProcessedToDeviceEvents<'a>(pub &'a [ProcessedToDeviceEvent]);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for DebugListOfProcessedToDeviceEvents<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut list = f.debug_list();
|
||||
list.entries(self.0.iter().map(|e| DebugRawEventNoId(e.as_raw())));
|
||||
list.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around an invited room as found in `/sync` responses that
|
||||
/// implements `Debug` in a way that only prints the event ID and event type for
|
||||
/// the raw events contained in `invite_state`.
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
//! Trait and macro of integration tests for `EventCacheStore` implementations.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk_common::{
|
||||
@@ -133,6 +133,9 @@ pub trait EventCacheStoreIntegrationTests {
|
||||
/// anything.
|
||||
async fn test_rebuild_empty_linked_chunk(&self);
|
||||
|
||||
/// Test that loading a linked chunk's metadata works as intended.
|
||||
async fn test_load_all_chunks_metadata(&self);
|
||||
|
||||
/// Test that clear all the rooms' linked chunks works.
|
||||
async fn test_clear_all_linked_chunks(&self);
|
||||
|
||||
@@ -417,6 +420,72 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
assert!(chunks.next().is_none());
|
||||
}
|
||||
|
||||
async fn test_load_all_chunks_metadata(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_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 2
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(2), 0),
|
||||
items: vec![make_test_event(room_id, "sup")],
|
||||
},
|
||||
// and an empty items chunk to finish
|
||||
Update::NewItemsChunk { previous: Some(CId::new(2)), new: CId::new(3), next: None },
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let metas = self.load_all_chunks_metadata(linked_chunk_id).await.unwrap();
|
||||
assert_eq!(metas.len(), 4);
|
||||
|
||||
// The first chunk has two items.
|
||||
assert_eq!(metas[0].identifier, CId::new(0));
|
||||
assert_eq!(metas[0].previous, None);
|
||||
assert_eq!(metas[0].next, Some(CId::new(1)));
|
||||
assert_eq!(metas[0].num_items, 2);
|
||||
|
||||
// The second chunk is a gap, so it has 0 items.
|
||||
assert_eq!(metas[1].identifier, CId::new(1));
|
||||
assert_eq!(metas[1].previous, Some(CId::new(0)));
|
||||
assert_eq!(metas[1].next, Some(CId::new(2)));
|
||||
assert_eq!(metas[1].num_items, 0);
|
||||
|
||||
// The third event chunk has one item.
|
||||
assert_eq!(metas[2].identifier, CId::new(2));
|
||||
assert_eq!(metas[2].previous, Some(CId::new(1)));
|
||||
assert_eq!(metas[2].next, Some(CId::new(3)));
|
||||
assert_eq!(metas[2].num_items, 1);
|
||||
|
||||
// The final event chunk is empty.
|
||||
assert_eq!(metas[3].identifier, CId::new(3));
|
||||
assert_eq!(metas[3].previous, Some(CId::new(2)));
|
||||
assert_eq!(metas[3].next, None);
|
||||
assert_eq!(metas[3].num_items, 0);
|
||||
}
|
||||
|
||||
async fn test_linked_chunk_incremental_loading(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
@@ -822,8 +891,8 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let duplicated_events = self
|
||||
.filter_duplicated_events(
|
||||
let duplicated_events = BTreeMap::from_iter(
|
||||
self.filter_duplicated_events(
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
event_comte.event_id().unwrap().to_owned(),
|
||||
@@ -835,20 +904,22 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(duplicated_events.len(), 3);
|
||||
|
||||
assert_eq!(
|
||||
duplicated_events[0],
|
||||
(event_comte.event_id().unwrap(), Position::new(CId::new(0), 0))
|
||||
*duplicated_events.get(&event_comte.event_id().unwrap()).unwrap(),
|
||||
Position::new(CId::new(0), 0)
|
||||
);
|
||||
assert_eq!(
|
||||
duplicated_events[1],
|
||||
(event_morbier.event_id().unwrap(), Position::new(CId::new(2), 0))
|
||||
*duplicated_events.get(&event_morbier.event_id().unwrap()).unwrap(),
|
||||
Position::new(CId::new(2), 0)
|
||||
);
|
||||
assert_eq!(
|
||||
duplicated_events[2],
|
||||
(event_mont_dor.event_id().unwrap(), Position::new(CId::new(2), 1))
|
||||
*duplicated_events.get(&event_mont_dor.event_id().unwrap()).unwrap(),
|
||||
Position::new(CId::new(2), 1)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -949,7 +1020,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
// Save All The Things!
|
||||
self.save_event(room_id, e1).await.unwrap();
|
||||
self.save_event(room_id, edit_e1).await.unwrap();
|
||||
self.save_event(room_id, reaction_e1).await.unwrap();
|
||||
self.save_event(room_id, reaction_e1.clone()).await.unwrap();
|
||||
self.save_event(room_id, e2).await.unwrap();
|
||||
self.save_event(another_room_id, e3).await.unwrap();
|
||||
self.save_event(another_room_id, reaction_e3).await.unwrap();
|
||||
@@ -957,8 +1028,13 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
// Finding relations without a filter returns all of them.
|
||||
let relations = self.find_event_relations(room_id, eid1, None).await.unwrap();
|
||||
assert_eq!(relations.len(), 2);
|
||||
assert!(relations.iter().any(|r| r.event_id().as_deref() == Some(edit_eid1)));
|
||||
assert!(relations.iter().any(|r| r.event_id().as_deref() == Some(reaction_eid1)));
|
||||
// The position is `None` for items outside the linked chunk.
|
||||
assert!(relations
|
||||
.iter()
|
||||
.any(|(ev, pos)| ev.event_id().as_deref() == Some(edit_eid1) && pos.is_none()));
|
||||
assert!(relations
|
||||
.iter()
|
||||
.any(|(ev, pos)| ev.event_id().as_deref() == Some(reaction_eid1) && pos.is_none()));
|
||||
|
||||
// Finding relations with a filter only returns a subset.
|
||||
let relations = self
|
||||
@@ -966,7 +1042,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(relations.len(), 1);
|
||||
assert_eq!(relations[0].event_id().as_deref(), Some(edit_eid1));
|
||||
assert_eq!(relations[0].0.event_id().as_deref(), Some(edit_eid1));
|
||||
|
||||
let relations = self
|
||||
.find_event_relations(
|
||||
@@ -977,8 +1053,8 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(relations.len(), 2);
|
||||
assert!(relations.iter().any(|r| r.event_id().as_deref() == Some(edit_eid1)));
|
||||
assert!(relations.iter().any(|r| r.event_id().as_deref() == Some(reaction_eid1)));
|
||||
assert!(relations.iter().any(|r| r.0.event_id().as_deref() == Some(edit_eid1)));
|
||||
assert!(relations.iter().any(|r| r.0.event_id().as_deref() == Some(reaction_eid1)));
|
||||
|
||||
// We can't find relations using the wrong room.
|
||||
let relations = self
|
||||
@@ -986,6 +1062,35 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(relations.is_empty());
|
||||
|
||||
// But if an event exists in the linked chunk, we may have its position when
|
||||
// it's found as a relationship.
|
||||
|
||||
// Add reaction_e1 to the room's linked chunk.
|
||||
self.handle_linked_chunk_updates(
|
||||
LinkedChunkId::Room(room_id),
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems { at: Position::new(CId::new(0), 0), items: vec![reaction_e1] },
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// When looking for aggregations to e1, we should have the position for
|
||||
// reaction_e1.
|
||||
let relations = self.find_event_relations(room_id, eid1, None).await.unwrap();
|
||||
|
||||
// The position is set for `reaction_eid1` now.
|
||||
assert!(relations.iter().any(|(ev, pos)| {
|
||||
ev.event_id().as_deref() == Some(reaction_eid1)
|
||||
&& *pos == Some(Position::new(CId::new(0), 0))
|
||||
}));
|
||||
|
||||
// But it's still not set for the other related events.
|
||||
assert!(relations
|
||||
.iter()
|
||||
.any(|(ev, pos)| ev.event_id().as_deref() == Some(edit_eid1) && pos.is_none()));
|
||||
}
|
||||
|
||||
async fn test_save_event(&self) {
|
||||
@@ -1105,6 +1210,13 @@ macro_rules! event_cache_store_integration_tests {
|
||||
event_cache_store.test_rebuild_empty_linked_chunk().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_load_all_chunks_metadata() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_load_all_chunks_metadata().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_clear_all_linked_chunks() {
|
||||
let event_cache_store =
|
||||
|
||||
@@ -22,7 +22,7 @@ use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
linked_chunk::{
|
||||
relational::RelationalLinkedChunk, ChunkIdentifier, ChunkIdentifierGenerator,
|
||||
LinkedChunkId, Position, RawChunk, Update,
|
||||
ChunkMetadata, LinkedChunkId, OwnedLinkedChunkId, Position, RawChunk, Update,
|
||||
},
|
||||
ring_buffer::RingBuffer,
|
||||
store_locks::memory_store_helper::try_take_leased_lock,
|
||||
@@ -148,6 +148,17 @@ impl EventCacheStore for MemoryStore {
|
||||
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
|
||||
}
|
||||
|
||||
async fn load_all_chunks_metadata(
|
||||
&self,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<Vec<ChunkMetadata>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
inner
|
||||
.events
|
||||
.load_all_chunks_metadata(linked_chunk_id)
|
||||
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
|
||||
}
|
||||
|
||||
async fn load_last_chunk(
|
||||
&self,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
@@ -181,17 +192,17 @@ impl EventCacheStore for MemoryStore {
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
mut events: Vec<OwnedEventId>,
|
||||
) -> Result<Vec<(OwnedEventId, Position)>, Self::Error> {
|
||||
// Collect all duplicated events.
|
||||
if events.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
let mut duplicated_events = Vec::new();
|
||||
|
||||
for (event, position) in inner.events.unordered_linked_chunk_items(linked_chunk_id) {
|
||||
// If `events` is empty, we can short-circuit.
|
||||
if events.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
for (event, position) in
|
||||
inner.events.unordered_linked_chunk_items(&linked_chunk_id.to_owned())
|
||||
{
|
||||
if let Some(known_event_id) = event.event_id() {
|
||||
// This event is a duplicate!
|
||||
if let Some(index) =
|
||||
@@ -212,10 +223,12 @@ impl EventCacheStore for MemoryStore {
|
||||
) -> Result<Option<Event>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
let event = inner.events.items().find_map(|(event, this_linked_chunk_id)| {
|
||||
(room_id == this_linked_chunk_id.room_id() && event.event_id()? == event_id)
|
||||
.then_some(event.clone())
|
||||
});
|
||||
let target_linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
|
||||
|
||||
let event = inner
|
||||
.events
|
||||
.items(&target_linked_chunk_id)
|
||||
.find_map(|(event, _pos)| (event.event_id()? == event_id).then_some(event.clone()));
|
||||
|
||||
Ok(event)
|
||||
}
|
||||
@@ -225,20 +238,17 @@ impl EventCacheStore for MemoryStore {
|
||||
room_id: &RoomId,
|
||||
event_id: &EventId,
|
||||
filters: Option<&[RelationType]>,
|
||||
) -> Result<Vec<Event>, Self::Error> {
|
||||
) -> Result<Vec<(Event, Option<Position>)>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
let target_linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
|
||||
|
||||
let filters = compute_filters_string(filters);
|
||||
|
||||
let related_events = inner
|
||||
.events
|
||||
.items()
|
||||
.filter_map(|(event, this_linked_chunk_id)| {
|
||||
// Must be in the same room.
|
||||
if room_id != this_linked_chunk_id.room_id() {
|
||||
return None;
|
||||
}
|
||||
|
||||
.items(&target_linked_chunk_id)
|
||||
.filter_map(|(event, pos)| {
|
||||
// Must have a relation.
|
||||
let (related_to, rel_type) = extract_event_relation(event.raw())?;
|
||||
|
||||
@@ -249,9 +259,9 @@ impl EventCacheStore for MemoryStore {
|
||||
|
||||
// Must not be filtered out.
|
||||
if let Some(filters) = &filters {
|
||||
filters.contains(&rel_type).then_some(event.clone())
|
||||
filters.contains(&rel_type).then_some((event.clone(), pos))
|
||||
} else {
|
||||
Some(event.clone())
|
||||
Some((event.clone(), pos))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@@ -400,7 +410,7 @@ impl EventCacheStoreMedia for MemoryStore {
|
||||
if !ignore_policy && policy.exceeds_max_file_size(data.len() as u64) {
|
||||
// Do not store it.
|
||||
return Ok(());
|
||||
};
|
||||
}
|
||||
|
||||
// Now, let's add it.
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
@@ -126,14 +126,8 @@ impl Deref for EventCacheStoreLockGuard<'_> {
|
||||
pub enum EventCacheStoreError {
|
||||
/// An error happened in the underlying database backend.
|
||||
#[error(transparent)]
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
Backend(Box<dyn std::error::Error + Send + Sync>),
|
||||
|
||||
/// An error happened in the underlying database backend.
|
||||
#[error(transparent)]
|
||||
#[cfg(target_family = "wasm")]
|
||||
Backend(Box<dyn std::error::Error>),
|
||||
|
||||
/// The store is locked with a passphrase and an incorrect passphrase
|
||||
/// was given.
|
||||
#[error("The event cache store failed to be unlocked")]
|
||||
@@ -175,25 +169,12 @@ impl EventCacheStoreError {
|
||||
///
|
||||
/// Shorthand for `EventCacheStoreError::Backend(Box::new(error))`.
|
||||
#[inline]
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub fn backend<E>(error: E) -> Self
|
||||
where
|
||||
E: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
Self::Backend(Box::new(error))
|
||||
}
|
||||
|
||||
/// Create a new [`Backend`][Self::Backend] error.
|
||||
///
|
||||
/// Shorthand for `EventCacheStoreError::Backend(Box::new(error))`.
|
||||
#[inline]
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub fn backend<E>(error: E) -> Self
|
||||
where
|
||||
E: std::error::Error + 'static,
|
||||
{
|
||||
Self::Backend(Box::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
/// An `EventCacheStore` specific result type.
|
||||
|
||||
@@ -17,7 +17,8 @@ use std::{fmt, sync::Arc};
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
linked_chunk::{
|
||||
ChunkIdentifier, ChunkIdentifierGenerator, LinkedChunkId, Position, RawChunk, Update,
|
||||
ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId, Position,
|
||||
RawChunk, Update,
|
||||
},
|
||||
AsyncTraitDeps,
|
||||
};
|
||||
@@ -77,6 +78,15 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error>;
|
||||
|
||||
/// Load all of the chunks' metadata for the given [`LinkedChunkId`].
|
||||
///
|
||||
/// Chunks are unordered, and there's no guarantee that the chunks would
|
||||
/// form a valid linked chunk after reconstruction.
|
||||
async fn load_all_chunks_metadata(
|
||||
&self,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<Vec<ChunkMetadata>, Self::Error>;
|
||||
|
||||
/// Load the last chunk of the `LinkedChunk` holding all events of the room
|
||||
/// identified by `room_id`.
|
||||
///
|
||||
@@ -124,7 +134,11 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
event_id: &EventId,
|
||||
) -> Result<Option<Event>, Self::Error>;
|
||||
|
||||
/// Find all the events that relate to a given event.
|
||||
/// Find all the events (alongside their position in the room's linked
|
||||
/// chunk, if available) that relate to a given event.
|
||||
///
|
||||
/// The only events which don't have a position are those which have been
|
||||
/// saved out-of-band using [`Self::save_event`].
|
||||
///
|
||||
/// Note: it doesn't process relations recursively: for instance, if
|
||||
/// requesting only thread events, it will NOT return the aggregated
|
||||
@@ -138,7 +152,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
room_id: &RoomId,
|
||||
event_id: &EventId,
|
||||
filter: Option<&[RelationType]>,
|
||||
) -> Result<Vec<Event>, Self::Error>;
|
||||
) -> Result<Vec<(Event, Option<Position>)>, Self::Error>;
|
||||
|
||||
/// Save an event, that might or might not be part of an existing linked
|
||||
/// chunk.
|
||||
@@ -313,6 +327,13 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
|
||||
self.0.load_all_chunks(linked_chunk_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn load_all_chunks_metadata(
|
||||
&self,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<Vec<ChunkMetadata>, Self::Error> {
|
||||
self.0.load_all_chunks_metadata(linked_chunk_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn load_last_chunk(
|
||||
&self,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
@@ -356,7 +377,7 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
|
||||
room_id: &RoomId,
|
||||
event_id: &EventId,
|
||||
filter: Option<&[RelationType]>,
|
||||
) -> Result<Vec<Event>, Self::Error> {
|
||||
) -> Result<Vec<(Event, Option<Position>)>, Self::Error> {
|
||||
self.0.find_event_relations(room_id, event_id, filter).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
|
||||
@@ -227,9 +227,10 @@ impl<'de> Deserialize<'de> for LatestEvent {
|
||||
Err(err) => variant_errors.push(err),
|
||||
}
|
||||
|
||||
Err(serde::de::Error::custom(
|
||||
format!("data did not match any variant of serialized LatestEvent (using serde_json). Observed errors: {variant_errors:?}")
|
||||
))
|
||||
Err(serde::de::Error::custom(format!(
|
||||
"data did not match any variant of serialized LatestEvent (using serde_json). \
|
||||
Observed errors: {variant_errors:?}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ mod utils;
|
||||
#[cfg(feature = "uniffi")]
|
||||
uniffi::setup_scaffolding!();
|
||||
|
||||
pub use client::BaseClient;
|
||||
pub use client::{BaseClient, ThreadingSupport};
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
pub use http;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
|
||||
@@ -122,7 +122,10 @@ use std::{
|
||||
num::NonZeroUsize,
|
||||
};
|
||||
|
||||
use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer};
|
||||
use matrix_sdk_common::{
|
||||
deserialized_responses::TimelineEvent, ring_buffer::RingBuffer,
|
||||
serde_helpers::extract_thread_root,
|
||||
};
|
||||
use ruma::{
|
||||
events::{
|
||||
poll::{start::PollStartEventContent, unstable_start::UnstablePollStartEventContent},
|
||||
@@ -137,6 +140,8 @@ use ruma::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{debug, instrument, trace, warn};
|
||||
|
||||
use crate::ThreadingSupport;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
struct LatestReadReceipt {
|
||||
/// The id of the event the read receipt is referring to. (Not the read
|
||||
@@ -201,7 +206,18 @@ impl RoomReadReceipts {
|
||||
///
|
||||
/// Returns whether a new event triggered a new unread/notification/mention.
|
||||
#[inline(always)]
|
||||
fn process_event(&mut self, event: &TimelineEvent, user_id: &UserId) {
|
||||
fn process_event(
|
||||
&mut self,
|
||||
event: &TimelineEvent,
|
||||
user_id: &UserId,
|
||||
threading_support: ThreadingSupport,
|
||||
) {
|
||||
if matches!(threading_support, ThreadingSupport::Enabled)
|
||||
&& extract_thread_root(event.raw()).is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if marks_as_unread(event.raw(), user_id) {
|
||||
self.num_unread += 1;
|
||||
}
|
||||
@@ -240,6 +256,7 @@ impl RoomReadReceipts {
|
||||
receipt_event_id: &EventId,
|
||||
user_id: &UserId,
|
||||
events: impl IntoIterator<Item = &'a TimelineEvent>,
|
||||
threading_support: ThreadingSupport,
|
||||
) -> bool {
|
||||
let mut counting_receipts = false;
|
||||
|
||||
@@ -259,7 +276,7 @@ impl RoomReadReceipts {
|
||||
}
|
||||
|
||||
if counting_receipts {
|
||||
self.process_event(event, user_id);
|
||||
self.process_event(event, user_id, threading_support);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,6 +460,7 @@ pub(crate) fn compute_unread_counts(
|
||||
mut previous_events: Vec<TimelineEvent>,
|
||||
new_events: &[TimelineEvent],
|
||||
read_receipts: &mut RoomReadReceipts,
|
||||
threading_support: ThreadingSupport,
|
||||
) {
|
||||
debug!(?read_receipts, "Starting");
|
||||
|
||||
@@ -489,7 +507,12 @@ pub(crate) fn compute_unread_counts(
|
||||
|
||||
// The event for the receipt is in `all_events`, so we'll find it and can count
|
||||
// safely from here.
|
||||
read_receipts.find_and_process_events(&event_id, user_id, all_events.iter());
|
||||
read_receipts.find_and_process_events(
|
||||
&event_id,
|
||||
user_id,
|
||||
all_events.iter(),
|
||||
threading_support,
|
||||
);
|
||||
|
||||
debug!(?read_receipts, "after finding a better receipt");
|
||||
return;
|
||||
@@ -503,7 +526,7 @@ pub(crate) fn compute_unread_counts(
|
||||
// for the next receipt.
|
||||
|
||||
for event in new_events {
|
||||
read_receipts.process_event(event, user_id);
|
||||
read_receipts.process_event(event, user_id, threading_support);
|
||||
}
|
||||
|
||||
debug!(?read_receipts, "no better receipt, {} new events", new_events.len());
|
||||
@@ -619,7 +642,10 @@ mod tests {
|
||||
};
|
||||
|
||||
use super::compute_unread_counts;
|
||||
use crate::read_receipts::{marks_as_unread, ReceiptSelector, RoomReadReceipts};
|
||||
use crate::{
|
||||
read_receipts::{marks_as_unread, ReceiptSelector, RoomReadReceipts},
|
||||
ThreadingSupport,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_room_message_marks_as_unread() {
|
||||
@@ -720,7 +746,7 @@ mod tests {
|
||||
// An interesting event from oneself doesn't count as a new unread message.
|
||||
let event = make_event(user_id, Vec::new());
|
||||
let mut receipts = RoomReadReceipts::default();
|
||||
receipts.process_event(&event, user_id);
|
||||
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
|
||||
assert_eq!(receipts.num_unread, 0);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
@@ -728,7 +754,7 @@ mod tests {
|
||||
// An interesting event from someone else does count as a new unread message.
|
||||
let event = make_event(user_id!("@bob:example.org"), Vec::new());
|
||||
let mut receipts = RoomReadReceipts::default();
|
||||
receipts.process_event(&event, user_id);
|
||||
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
|
||||
assert_eq!(receipts.num_unread, 1);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
@@ -736,7 +762,7 @@ mod tests {
|
||||
// Push actions computed beforehand are respected.
|
||||
let event = make_event(user_id!("@bob:example.org"), vec![Action::Notify]);
|
||||
let mut receipts = RoomReadReceipts::default();
|
||||
receipts.process_event(&event, user_id);
|
||||
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
|
||||
assert_eq!(receipts.num_unread, 1);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
assert_eq!(receipts.num_notifications, 1);
|
||||
@@ -746,7 +772,7 @@ mod tests {
|
||||
vec![Action::SetTweak(ruma::push::Tweak::Highlight(true))],
|
||||
);
|
||||
let mut receipts = RoomReadReceipts::default();
|
||||
receipts.process_event(&event, user_id);
|
||||
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
|
||||
assert_eq!(receipts.num_unread, 1);
|
||||
assert_eq!(receipts.num_mentions, 1);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
@@ -756,7 +782,7 @@ mod tests {
|
||||
vec![Action::SetTweak(ruma::push::Tweak::Highlight(true)), Action::Notify],
|
||||
);
|
||||
let mut receipts = RoomReadReceipts::default();
|
||||
receipts.process_event(&event, user_id);
|
||||
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
|
||||
assert_eq!(receipts.num_unread, 1);
|
||||
assert_eq!(receipts.num_mentions, 1);
|
||||
assert_eq!(receipts.num_notifications, 1);
|
||||
@@ -765,7 +791,7 @@ mod tests {
|
||||
// make sure to resist against it.
|
||||
let event = make_event(user_id!("@bob:example.org"), vec![Action::Notify, Action::Notify]);
|
||||
let mut receipts = RoomReadReceipts::default();
|
||||
receipts.process_event(&event, user_id);
|
||||
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
|
||||
assert_eq!(receipts.num_unread, 1);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
assert_eq!(receipts.num_notifications, 1);
|
||||
@@ -779,7 +805,9 @@ mod tests {
|
||||
// When provided with no events, we report not finding the event to which the
|
||||
// receipt relates.
|
||||
let mut receipts = RoomReadReceipts::default();
|
||||
assert!(receipts.find_and_process_events(ev0, user_id, &[]).not());
|
||||
assert!(receipts
|
||||
.find_and_process_events(ev0, user_id, &[], ThreadingSupport::Disabled)
|
||||
.not());
|
||||
assert_eq!(receipts.num_unread, 0);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
@@ -801,7 +829,12 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
assert!(receipts
|
||||
.find_and_process_events(ev0, user_id, &[make_event(event_id!("$1"))],)
|
||||
.find_and_process_events(
|
||||
ev0,
|
||||
user_id,
|
||||
&[make_event(event_id!("$1"))],
|
||||
ThreadingSupport::Disabled
|
||||
)
|
||||
.not());
|
||||
assert_eq!(receipts.num_unread, 42);
|
||||
assert_eq!(receipts.num_notifications, 13);
|
||||
@@ -816,7 +849,12 @@ mod tests {
|
||||
num_mentions: 37,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(receipts.find_and_process_events(ev0, user_id, &[make_event(ev0)]));
|
||||
assert!(receipts.find_and_process_events(
|
||||
ev0,
|
||||
user_id,
|
||||
&[make_event(ev0)],
|
||||
ThreadingSupport::Disabled
|
||||
),);
|
||||
assert_eq!(receipts.num_unread, 0);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
@@ -838,6 +876,7 @@ mod tests {
|
||||
make_event(event_id!("$2")),
|
||||
make_event(event_id!("$3"))
|
||||
],
|
||||
ThreadingSupport::Disabled
|
||||
)
|
||||
.not());
|
||||
assert_eq!(receipts.num_unread, 42);
|
||||
@@ -861,6 +900,7 @@ mod tests {
|
||||
make_event(event_id!("$2")),
|
||||
make_event(event_id!("$3"))
|
||||
],
|
||||
ThreadingSupport::Disabled
|
||||
));
|
||||
assert_eq!(receipts.num_unread, 2);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
@@ -883,6 +923,7 @@ mod tests {
|
||||
make_event(event_id!("$2")),
|
||||
make_event(event_id!("$3"))
|
||||
],
|
||||
ThreadingSupport::Disabled
|
||||
));
|
||||
assert_eq!(receipts.num_unread, 2);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
@@ -908,7 +949,7 @@ mod tests {
|
||||
.add(receipt_event_id, user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
|
||||
.into_content();
|
||||
|
||||
let mut read_receipts = Default::default();
|
||||
let mut read_receipts = RoomReadReceipts::default();
|
||||
compute_unread_counts(
|
||||
user_id,
|
||||
room_id,
|
||||
@@ -916,6 +957,7 @@ mod tests {
|
||||
previous_events.clone(),
|
||||
&[ev1.clone(), ev2.clone()],
|
||||
&mut read_receipts,
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
// It did find the receipt event (ev1).
|
||||
@@ -934,6 +976,7 @@ mod tests {
|
||||
previous_events,
|
||||
&[new_event],
|
||||
&mut read_receipts,
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
// Only the new event should be added.
|
||||
@@ -1000,6 +1043,7 @@ mod tests {
|
||||
all_events.clone(),
|
||||
&[],
|
||||
&mut read_receipts,
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
assert!(
|
||||
@@ -1021,6 +1065,7 @@ mod tests {
|
||||
head_events.clone(),
|
||||
&tail_events,
|
||||
&mut read_receipts,
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
assert!(
|
||||
@@ -1065,6 +1110,7 @@ mod tests {
|
||||
events,
|
||||
&[], // no new events
|
||||
&mut read_receipts,
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
// Then there are no unread events,
|
||||
@@ -1102,6 +1148,7 @@ mod tests {
|
||||
events,
|
||||
&[ev0], // duplicate event!
|
||||
&mut read_receipts,
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
// All events are unread, and there's no pending receipt.
|
||||
@@ -1536,6 +1583,7 @@ mod tests {
|
||||
Vec::new(),
|
||||
&events,
|
||||
&mut read_receipts,
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
// Only the last two events sent by Bob count as unread.
|
||||
@@ -1547,4 +1595,60 @@ mod tests {
|
||||
// And the active receipt is the implicit one on my event.
|
||||
assert_eq!(read_receipts.latest_active.unwrap().event_id, event_id!("$6"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_unread_counts_with_threading_enabled() {
|
||||
fn make_event(user_id: &UserId, thread_root: &EventId) -> TimelineEvent {
|
||||
EventFactory::new()
|
||||
.text_msg("A")
|
||||
.sender(user_id)
|
||||
.event_id(event_id!("$ida"))
|
||||
.in_thread(thread_root, event_id!("$latest_event"))
|
||||
.into_event()
|
||||
}
|
||||
|
||||
let mut receipts = RoomReadReceipts::default();
|
||||
|
||||
let own_alice = user_id!("@alice:example.org");
|
||||
let bob = user_id!("@bob:example.org");
|
||||
|
||||
// Threaded messages from myself or other users shouldn't change the
|
||||
// unread counts.
|
||||
receipts.process_event(
|
||||
&make_event(own_alice, event_id!("$some_thread_root")),
|
||||
own_alice,
|
||||
ThreadingSupport::Enabled,
|
||||
);
|
||||
receipts.process_event(
|
||||
&make_event(own_alice, event_id!("$some_other_thread_root")),
|
||||
own_alice,
|
||||
ThreadingSupport::Enabled,
|
||||
);
|
||||
|
||||
receipts.process_event(
|
||||
&make_event(bob, event_id!("$some_thread_root")),
|
||||
own_alice,
|
||||
ThreadingSupport::Enabled,
|
||||
);
|
||||
receipts.process_event(
|
||||
&make_event(bob, event_id!("$some_other_thread_root")),
|
||||
own_alice,
|
||||
ThreadingSupport::Enabled,
|
||||
);
|
||||
|
||||
assert_eq!(receipts.num_unread, 0);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
|
||||
// Processing an unthreaded message should still count as unread.
|
||||
receipts.process_event(
|
||||
&EventFactory::new().text_msg("A").sender(bob).event_id(event_id!("$ida")).into_event(),
|
||||
own_alice,
|
||||
ThreadingSupport::Enabled,
|
||||
);
|
||||
|
||||
assert_eq!(receipts.num_unread, 1);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use matrix_sdk_crypto::{DecryptionSettings, RoomEventDecryptionResult};
|
||||
use matrix_sdk_crypto::RoomEventDecryptionResult;
|
||||
use ruma::{events::AnySyncTimelineEvent, serde::Raw, RoomId};
|
||||
|
||||
use super::{super::verification, E2EE};
|
||||
@@ -33,11 +33,11 @@ pub async fn sync_timeline_event(
|
||||
) -> Result<Option<TimelineEvent>> {
|
||||
let Some(olm) = e2ee.olm_machine else { return Ok(None) };
|
||||
|
||||
let decryption_settings =
|
||||
DecryptionSettings { sender_device_trust_requirement: e2ee.decryption_trust_requirement };
|
||||
|
||||
Ok(Some(
|
||||
match olm.try_decrypt_room_event(event.cast_ref(), room_id, &decryption_settings).await? {
|
||||
match olm
|
||||
.try_decrypt_room_event(event.cast_ref(), room_id, e2ee.decryption_settings)
|
||||
.await?
|
||||
{
|
||||
RoomEventDecryptionResult::Decrypted(decrypted) => {
|
||||
// Note: the push actions are set by the caller.
|
||||
let timeline_event = TimelineEvent::from_decrypted(decrypted, None);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk_crypto::{OlmMachine, TrustRequirement};
|
||||
use matrix_sdk_crypto::{DecryptionSettings, OlmMachine};
|
||||
|
||||
pub mod decrypt;
|
||||
pub mod to_device;
|
||||
@@ -22,16 +22,16 @@ pub mod tracked_users;
|
||||
#[derive(Clone)]
|
||||
pub struct E2EE<'a> {
|
||||
pub olm_machine: Option<&'a OlmMachine>,
|
||||
pub decryption_trust_requirement: TrustRequirement,
|
||||
pub decryption_settings: &'a DecryptionSettings,
|
||||
pub verification_is_allowed: bool,
|
||||
}
|
||||
|
||||
impl<'a> E2EE<'a> {
|
||||
pub fn new(
|
||||
olm_machine: Option<&'a OlmMachine>,
|
||||
decryption_trust_requirement: TrustRequirement,
|
||||
decryption_settings: &'a DecryptionSettings,
|
||||
verification_is_allowed: bool,
|
||||
) -> Self {
|
||||
Self { olm_machine, decryption_trust_requirement, verification_is_allowed }
|
||||
Self { olm_machine, decryption_settings, verification_is_allowed }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use matrix_sdk_crypto::{store::RoomKeyInfo, EncryptionSyncChanges, OlmMachine};
|
||||
use matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent;
|
||||
use matrix_sdk_crypto::{store::types::RoomKeyInfo, EncryptionSyncChanges, OlmMachine};
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::{v3, v5, DeviceLists},
|
||||
events::AnyToDeviceEvent,
|
||||
@@ -93,28 +94,35 @@ async fn process(
|
||||
let (events, room_key_updates) =
|
||||
olm_machine.receive_sync_changes(encryption_sync_changes).await?;
|
||||
|
||||
let events = events
|
||||
.iter()
|
||||
// TODO: There is loss of information here, after calling `to_raw` it is not
|
||||
// possible to make the difference between a successfully decrypted event and a plain
|
||||
// text event. This information needs to be propagated to top layer at some point if
|
||||
// clients relies on custom encrypted to device events.
|
||||
.map(|p| p.to_raw())
|
||||
.collect();
|
||||
|
||||
Output { decrypted_to_device_events: events, room_key_updates: Some(room_key_updates) }
|
||||
Output { processed_to_device_events: events, room_key_updates: Some(room_key_updates) }
|
||||
} else {
|
||||
// If we have no `OlmMachine`, just return the events that were passed in.
|
||||
// If we have no `OlmMachine`, just return the clear events that were passed in.
|
||||
// The encrypted ones are dropped as they are un-usable.
|
||||
// This should not happen unless we forget to set things up by calling
|
||||
// `Self::activate()`.
|
||||
Output {
|
||||
decrypted_to_device_events: encryption_sync_changes.to_device_events,
|
||||
processed_to_device_events: encryption_sync_changes
|
||||
.to_device_events
|
||||
.into_iter()
|
||||
.map(|raw| {
|
||||
if let Ok(Some(event_type)) = raw.get_field::<String>("type") {
|
||||
if event_type == "m.room.encrypted" {
|
||||
ProcessedToDeviceEvent::UnableToDecrypt(raw)
|
||||
} else {
|
||||
ProcessedToDeviceEvent::PlainText(raw)
|
||||
}
|
||||
} else {
|
||||
// Exclude events with no type
|
||||
ProcessedToDeviceEvent::Invalid(raw)
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
room_key_updates: None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub struct Output {
|
||||
pub decrypted_to_device_events: Vec<Raw<AnyToDeviceEvent>>,
|
||||
pub processed_to_device_events: Vec<ProcessedToDeviceEvent>,
|
||||
pub room_key_updates: Option<Vec<RoomKeyInfo>>,
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use matrix_sdk_crypto::{DecryptionSettings, RoomEventDecryptionResult};
|
||||
use matrix_sdk_crypto::RoomEventDecryptionResult;
|
||||
use ruma::{events::AnySyncTimelineEvent, serde::Raw, RoomId};
|
||||
|
||||
use super::{e2ee::E2EE, verification, Context};
|
||||
@@ -108,13 +108,10 @@ async fn decrypt_sync_room_event(
|
||||
e2ee: &E2EE<'_>,
|
||||
room_id: &RoomId,
|
||||
) -> Result<TimelineEvent> {
|
||||
let decryption_settings =
|
||||
DecryptionSettings { sender_device_trust_requirement: e2ee.decryption_trust_requirement };
|
||||
|
||||
let event = match e2ee
|
||||
.olm_machine
|
||||
.expect("An `OlmMachine` is expected")
|
||||
.try_decrypt_room_event(event.cast_ref(), room_id, &decryption_settings)
|
||||
.try_decrypt_room_event(event.cast_ref(), room_id, e2ee.decryption_settings)
|
||||
.await?
|
||||
{
|
||||
RoomEventDecryptionResult::Decrypted(decrypted) => {
|
||||
@@ -184,7 +181,7 @@ mod tests {
|
||||
vec![room.clone()],
|
||||
E2EE::new(
|
||||
client.olm_machine().await.as_ref(),
|
||||
client.decryption_trust_requirement,
|
||||
&client.decryption_settings,
|
||||
client.handle_verification_events,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -118,6 +118,7 @@ pub async fn update_any_room(
|
||||
&mut room_info,
|
||||
ambiguity_cache,
|
||||
&mut new_user_ids,
|
||||
state_store,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ pub async fn update_joined_room(
|
||||
&mut room_info,
|
||||
ambiguity_cache,
|
||||
&mut new_user_ids,
|
||||
state_store,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -89,6 +90,7 @@ pub async fn update_joined_room(
|
||||
&mut room_info,
|
||||
ambiguity_cache,
|
||||
&mut new_user_ids,
|
||||
state_store,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -185,6 +187,7 @@ pub async fn update_left_room(
|
||||
&mut room_info,
|
||||
ambiguity_cache,
|
||||
&mut (),
|
||||
state_store,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -197,6 +200,7 @@ pub async fn update_left_room(
|
||||
&mut room_info,
|
||||
ambiguity_cache,
|
||||
&mut (),
|
||||
state_store,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -12,11 +12,21 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use ruma::{events::AnySyncStateEvent, serde::Raw};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use ruma::{
|
||||
events::{
|
||||
room::{create::RoomCreateEventContent, tombstone::RoomTombstoneEventContent},
|
||||
AnySyncStateEvent, SyncStateEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
RoomId,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use tracing::warn;
|
||||
|
||||
use super::Context;
|
||||
use crate::store::BaseStateStore;
|
||||
|
||||
/// Collect [`AnySyncStateEvent`].
|
||||
pub mod sync {
|
||||
@@ -29,11 +39,11 @@ pub mod sync {
|
||||
},
|
||||
OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
use tracing::instrument;
|
||||
use tracing::{error, instrument};
|
||||
|
||||
use super::{super::profiles, AnySyncStateEvent, Context, Raw};
|
||||
use crate::{
|
||||
store::{ambiguity_map::AmbiguityCache, Result as StoreResult},
|
||||
store::{ambiguity_map::AmbiguityCache, BaseStateStore, Result as StoreResult},
|
||||
RoomInfo,
|
||||
};
|
||||
|
||||
@@ -76,22 +86,71 @@ pub mod sync {
|
||||
room_info: &mut RoomInfo,
|
||||
ambiguity_cache: &mut AmbiguityCache,
|
||||
new_users: &mut U,
|
||||
state_store: &BaseStateStore,
|
||||
) -> StoreResult<()>
|
||||
where
|
||||
U: NewUsers,
|
||||
{
|
||||
for (raw_event, event) in iter::zip(raw_events, events) {
|
||||
room_info.handle_state_event(event);
|
||||
match event {
|
||||
AnySyncStateEvent::RoomMember(member) => {
|
||||
room_info.handle_state_event(event);
|
||||
|
||||
if let AnySyncStateEvent::RoomMember(member) = event {
|
||||
dispatch_room_member(
|
||||
context,
|
||||
&room_info.room_id,
|
||||
member,
|
||||
ambiguity_cache,
|
||||
new_users,
|
||||
)
|
||||
.await?;
|
||||
dispatch_room_member(
|
||||
context,
|
||||
&room_info.room_id,
|
||||
member,
|
||||
ambiguity_cache,
|
||||
new_users,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
AnySyncStateEvent::RoomCreate(create) => {
|
||||
if super::is_create_event_valid(
|
||||
context,
|
||||
room_info.room_id(),
|
||||
create,
|
||||
state_store,
|
||||
) {
|
||||
room_info.handle_state_event(event);
|
||||
} else {
|
||||
error!(
|
||||
room_id = ?room_info.room_id(),
|
||||
?create,
|
||||
"`m.create.tombstone` event is invalid, it creates a loop"
|
||||
);
|
||||
|
||||
// Do not add the event to `room_info`.
|
||||
// Do not add the event to `context.state_changes.state`.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
AnySyncStateEvent::RoomTombstone(tombstone) => {
|
||||
if super::is_tombstone_event_valid(
|
||||
context,
|
||||
room_info.room_id(),
|
||||
tombstone,
|
||||
state_store,
|
||||
) {
|
||||
room_info.handle_state_event(event);
|
||||
} else {
|
||||
error!(
|
||||
room_id = ?room_info.room_id(),
|
||||
?tombstone,
|
||||
"`m.room.tombstone` event is invalid, it creates a loop"
|
||||
);
|
||||
|
||||
// Do not add the event to `room_info`.
|
||||
// Do not add the event to `context.state_changes.state`.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
room_info.handle_state_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
context
|
||||
@@ -248,16 +307,762 @@ where
|
||||
.unzip()
|
||||
}
|
||||
|
||||
/// Check if `m.room.create` isn't creating a loop of rooms.
|
||||
pub fn is_create_event_valid(
|
||||
context: &mut Context,
|
||||
room_id: &RoomId,
|
||||
event: &SyncStateEvent<RoomCreateEventContent>,
|
||||
state_store: &BaseStateStore,
|
||||
) -> bool {
|
||||
let mut already_seen = BTreeSet::new();
|
||||
already_seen.insert(room_id.to_owned());
|
||||
|
||||
let Some(mut predecessor_room_id) = event
|
||||
.as_original()
|
||||
.and_then(|event| Some(event.content.predecessor.as_ref()?.room_id.clone()))
|
||||
else {
|
||||
// `true` means no problem. No predecessor = no problem here.
|
||||
return true;
|
||||
};
|
||||
|
||||
loop {
|
||||
// We must check immediately if the `predecessor_room_id` is in `already_seen`
|
||||
// in case of a room is created and marks itself as its predecessor in a single
|
||||
// sync.
|
||||
if already_seen.contains(AsRef::<RoomId>::as_ref(&predecessor_room_id)) {
|
||||
// Ahhh, there is a loop with `m.room.create` events!
|
||||
return false;
|
||||
}
|
||||
|
||||
already_seen.insert(predecessor_room_id.clone());
|
||||
|
||||
// Where is the predecessor room? Check in `room_infos` and then in
|
||||
// `state_store`.
|
||||
let Some(next_predecessor_room_id) = context
|
||||
.state_changes
|
||||
.room_infos
|
||||
.get(&predecessor_room_id)
|
||||
.and_then(|room_info| Some(room_info.create()?.predecessor.as_ref()?.room_id.clone()))
|
||||
.or_else(|| {
|
||||
state_store
|
||||
.room(&predecessor_room_id)
|
||||
.and_then(|room| Some(room.predecessor_room()?.room_id))
|
||||
})
|
||||
else {
|
||||
// No more predecessor found. Everything seems alright. No loop.
|
||||
break;
|
||||
};
|
||||
|
||||
predecessor_room_id = next_predecessor_room_id;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Check if `m.room.tombstone` isn't creating a loop of rooms.
|
||||
pub fn is_tombstone_event_valid(
|
||||
context: &mut Context,
|
||||
room_id: &RoomId,
|
||||
event: &SyncStateEvent<RoomTombstoneEventContent>,
|
||||
state_store: &BaseStateStore,
|
||||
) -> bool {
|
||||
let mut already_seen = BTreeSet::new();
|
||||
already_seen.insert(room_id.to_owned());
|
||||
|
||||
let Some(mut successor_room_id) =
|
||||
event.as_original().map(|event| event.content.replacement_room.clone())
|
||||
else {
|
||||
// `true` means no problem. No successor = no problem here.
|
||||
return true;
|
||||
};
|
||||
|
||||
loop {
|
||||
// We must check immediately if the `successor_room_id` is in `already_seen` in
|
||||
// case of a room is created and tombstones itself in a single sync.
|
||||
if already_seen.contains(AsRef::<RoomId>::as_ref(&successor_room_id)) {
|
||||
// Ahhh, there is a loop with `m.room.tombstone` events!
|
||||
return false;
|
||||
}
|
||||
|
||||
already_seen.insert(successor_room_id.clone());
|
||||
|
||||
// Where is the successor room? Check in `room_infos` and then in `state_store`.
|
||||
let Some(next_successor_room_id) = context
|
||||
.state_changes
|
||||
.room_infos
|
||||
.get(&successor_room_id)
|
||||
.and_then(|room_info| Some(room_info.tombstone()?.replacement_room.clone()))
|
||||
.or_else(|| {
|
||||
state_store
|
||||
.room(&successor_room_id)
|
||||
.and_then(|room| Some(room.successor_room()?.room_id))
|
||||
})
|
||||
else {
|
||||
// No more successor found. Everything seems alright. No loop.
|
||||
break;
|
||||
};
|
||||
|
||||
successor_room_id = next_successor_room_id;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use matrix_sdk_test::{
|
||||
async_test, event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent,
|
||||
SyncResponseBuilder, DEFAULT_TEST_ROOM_ID,
|
||||
};
|
||||
use ruma::{event_id, user_id};
|
||||
use ruma::{event_id, room_id, user_id, RoomVersionId};
|
||||
|
||||
use crate::test_utils::logged_in_base_client;
|
||||
|
||||
#[async_test]
|
||||
async fn test_not_possible_to_overwrite_m_room_create() {
|
||||
let sender = user_id!("@mnt_io:matrix.org");
|
||||
let event_factory = EventFactory::new().sender(sender);
|
||||
let mut response_builder = SyncResponseBuilder::new();
|
||||
let room_id_0 = room_id!("!r0");
|
||||
let room_id_1 = room_id!("!r1");
|
||||
let room_id_2 = room_id!("!r2");
|
||||
|
||||
let client = logged_in_base_client(None).await;
|
||||
|
||||
// Create room 0 with 2 `m.room.create` events.
|
||||
// Create room 1 with 1 `m.room.create` event.
|
||||
// Create room 2 with 0 `m.room.create` event.
|
||||
{
|
||||
let response = response_builder
|
||||
.add_joined_room(
|
||||
JoinedRoomBuilder::new(room_id_0)
|
||||
.add_timeline_event(
|
||||
event_factory.create(sender, RoomVersionId::try_from("42").unwrap()),
|
||||
)
|
||||
.add_timeline_event(
|
||||
event_factory.create(sender, RoomVersionId::try_from("43").unwrap()),
|
||||
),
|
||||
)
|
||||
.add_joined_room(JoinedRoomBuilder::new(room_id_1).add_timeline_event(
|
||||
event_factory.create(sender, RoomVersionId::try_from("44").unwrap()),
|
||||
))
|
||||
.add_joined_room(JoinedRoomBuilder::new(room_id_2))
|
||||
.build_sync_response();
|
||||
|
||||
assert!(client.receive_sync_response(response).await.is_ok());
|
||||
|
||||
// Room 0
|
||||
// the second `m.room.create` has been ignored!
|
||||
assert_eq!(
|
||||
client.get_room(room_id_0).unwrap().create_content().unwrap().room_version.as_str(),
|
||||
"42"
|
||||
);
|
||||
// Room 1
|
||||
assert_eq!(
|
||||
client.get_room(room_id_1).unwrap().create_content().unwrap().room_version.as_str(),
|
||||
"44"
|
||||
);
|
||||
// Room 2
|
||||
assert!(client.get_room(room_id_2).unwrap().create_content().is_none());
|
||||
}
|
||||
|
||||
// Room 0 receives a new `m.room.create` event.
|
||||
// Room 1 receives a new `m.room.create` event.
|
||||
// Room 2 receives its first `m.room.create` event.
|
||||
{
|
||||
let response = response_builder
|
||||
.add_joined_room(JoinedRoomBuilder::new(room_id_0).add_timeline_event(
|
||||
event_factory.create(sender, RoomVersionId::try_from("45").unwrap()),
|
||||
))
|
||||
.add_joined_room(JoinedRoomBuilder::new(room_id_1).add_timeline_event(
|
||||
event_factory.create(sender, RoomVersionId::try_from("46").unwrap()),
|
||||
))
|
||||
.add_joined_room(JoinedRoomBuilder::new(room_id_2).add_timeline_event(
|
||||
event_factory.create(sender, RoomVersionId::try_from("47").unwrap()),
|
||||
))
|
||||
.build_sync_response();
|
||||
|
||||
assert!(client.receive_sync_response(response).await.is_ok());
|
||||
|
||||
// Room 0
|
||||
// the third `m.room.create` has been ignored!
|
||||
assert_eq!(
|
||||
client.get_room(room_id_0).unwrap().create_content().unwrap().room_version.as_str(),
|
||||
"42"
|
||||
);
|
||||
// Room 1
|
||||
// the second `m.room.create` has been ignored!
|
||||
assert_eq!(
|
||||
client.get_room(room_id_1).unwrap().create_content().unwrap().room_version.as_str(),
|
||||
"44"
|
||||
);
|
||||
// Room 2
|
||||
assert_eq!(
|
||||
client.get_room(room_id_2).unwrap().create_content().unwrap().room_version.as_str(),
|
||||
"47"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_check_room_upgrades_no_newly_tombstoned_rooms() {
|
||||
let client = logged_in_base_client(None).await;
|
||||
|
||||
// Create a new room, no tombstone, no anything.
|
||||
{
|
||||
let response = SyncResponseBuilder::new()
|
||||
.add_joined_room(JoinedRoomBuilder::new(room_id!("!r0")))
|
||||
.build_sync_response();
|
||||
|
||||
assert!(client.receive_sync_response(response).await.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_check_room_upgrades_no_error() {
|
||||
let sender = user_id!("@mnt_io:matrix.org");
|
||||
let event_factory = EventFactory::new().sender(sender);
|
||||
let mut response_builder = SyncResponseBuilder::new();
|
||||
let room_id_0 = room_id!("!r0");
|
||||
let room_id_1 = room_id!("!r1");
|
||||
let room_id_2 = room_id!("!r2");
|
||||
|
||||
let client = logged_in_base_client(None).await;
|
||||
|
||||
// Room 0.
|
||||
{
|
||||
let response = response_builder
|
||||
.add_joined_room(JoinedRoomBuilder::new(room_id_0).add_timeline_event(
|
||||
// Room 0 has no predecessor.
|
||||
event_factory.create(sender, RoomVersionId::try_from("41").unwrap()),
|
||||
))
|
||||
.build_sync_response();
|
||||
|
||||
assert!(client.receive_sync_response(response).await.is_ok());
|
||||
|
||||
let room_0 = client.get_room(room_id_0).unwrap();
|
||||
|
||||
assert!(room_0.predecessor_room().is_none());
|
||||
assert!(room_0.successor_room().is_none());
|
||||
}
|
||||
|
||||
// Room 0 and room 1.
|
||||
{
|
||||
let tombstone_event_id = event_id!("$ev0");
|
||||
let response = response_builder
|
||||
.add_joined_room(JoinedRoomBuilder::new(room_id_0).add_timeline_event(
|
||||
// Successor of room 0 is room 1.
|
||||
event_factory.room_tombstone("hello", room_id_1).event_id(tombstone_event_id),
|
||||
))
|
||||
.add_joined_room(
|
||||
JoinedRoomBuilder::new(room_id_1).add_timeline_event(
|
||||
// Predecessor of room 1 is room 0.
|
||||
event_factory
|
||||
.create(sender, RoomVersionId::try_from("42").unwrap())
|
||||
.predecessor(room_id_0, tombstone_event_id),
|
||||
),
|
||||
)
|
||||
.build_sync_response();
|
||||
|
||||
assert!(client.receive_sync_response(response).await.is_ok());
|
||||
|
||||
let room_0 = client.get_room(room_id_0).unwrap();
|
||||
|
||||
assert!(room_0.predecessor_room().is_none(), "room 0 must not have a predecessor");
|
||||
assert_eq!(
|
||||
room_0.successor_room().expect("room 0 must have a successor").room_id,
|
||||
room_id_1,
|
||||
"room 0 does not have the expected successor",
|
||||
);
|
||||
|
||||
let room_1 = client.get_room(room_id_1).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
room_1.predecessor_room().expect("room 1 must have a predecessor").room_id,
|
||||
room_id_0,
|
||||
"room 1 does not have the expected predecessor",
|
||||
);
|
||||
assert!(room_1.successor_room().is_none(), "room 1 must not have a successor");
|
||||
}
|
||||
|
||||
// Room 1 and room 2.
|
||||
{
|
||||
let tombstone_event_id = event_id!("$ev1");
|
||||
let response = response_builder
|
||||
.add_joined_room(JoinedRoomBuilder::new(room_id_1).add_timeline_event(
|
||||
// Successor of room 1 is room 2.
|
||||
event_factory.room_tombstone("hello", room_id_2).event_id(tombstone_event_id),
|
||||
))
|
||||
.add_joined_room(
|
||||
JoinedRoomBuilder::new(room_id_2).add_timeline_event(
|
||||
// Predecessor of room 2 is room 1.
|
||||
event_factory
|
||||
.create(sender, RoomVersionId::try_from("43").unwrap())
|
||||
.predecessor(room_id_1, tombstone_event_id),
|
||||
),
|
||||
)
|
||||
.build_sync_response();
|
||||
|
||||
assert!(client.receive_sync_response(response).await.is_ok());
|
||||
|
||||
let room_1 = client.get_room(room_id_1).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
room_1.predecessor_room().expect("room 1 must have a predecessor").room_id,
|
||||
room_id_0,
|
||||
"room 1 does not have the expected predecessor",
|
||||
);
|
||||
assert_eq!(
|
||||
room_1.successor_room().expect("room 1 must have a successor").room_id,
|
||||
room_id_2,
|
||||
"room 1 does not have the expected successor",
|
||||
);
|
||||
|
||||
let room_2 = client.get_room(room_id_2).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
room_2.predecessor_room().expect("room 2 must have a predecessor").room_id,
|
||||
room_id_1,
|
||||
"room 2 does not have the expected predecessor",
|
||||
);
|
||||
assert!(room_2.successor_room().is_none(), "room 2 must not have a successor");
|
||||
}
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_check_room_upgrades_no_loop_within_misordered_rooms() {
|
||||
let sender = user_id!("@mnt_io:matrix.org");
|
||||
let event_factory = EventFactory::new().sender(sender);
|
||||
let mut response_builder = SyncResponseBuilder::new();
|
||||
// The room IDs are important because `SyncResponseBuilder` stores them in a
|
||||
// `HashMap`, so they are going to be “shuffled”.
|
||||
let room_id_0 = room_id!("!r1");
|
||||
let room_id_1 = room_id!("!r0");
|
||||
let room_id_2 = room_id!("!r2");
|
||||
|
||||
let client = logged_in_base_client(None).await;
|
||||
|
||||
// Create all rooms in a misordered way to see if `check_tombstone` will
|
||||
// re-order them appropriately.
|
||||
{
|
||||
let response = response_builder
|
||||
// Room 0
|
||||
.add_joined_room(
|
||||
JoinedRoomBuilder::new(room_id_0)
|
||||
.add_timeline_event(
|
||||
// No predecessor for room 0.
|
||||
event_factory.create(sender, RoomVersionId::try_from("41").unwrap()),
|
||||
)
|
||||
.add_timeline_event(
|
||||
// Successor of room 0 is room 1.
|
||||
event_factory
|
||||
.room_tombstone("hello", room_id_1)
|
||||
.event_id(event_id!("$ev0")),
|
||||
),
|
||||
)
|
||||
// Room 1
|
||||
.add_joined_room(
|
||||
JoinedRoomBuilder::new(room_id_1)
|
||||
.add_timeline_event(
|
||||
// Predecessor of room 1 is room 0.
|
||||
event_factory
|
||||
.create(sender, RoomVersionId::try_from("42").unwrap())
|
||||
.predecessor(room_id_0, event_id!("$ev0")),
|
||||
)
|
||||
.add_timeline_event(
|
||||
// Successor of room 1 is room 2.
|
||||
event_factory
|
||||
.room_tombstone("hello", room_id_2)
|
||||
.event_id(event_id!("$ev1")),
|
||||
),
|
||||
)
|
||||
// Room 2
|
||||
.add_joined_room(
|
||||
JoinedRoomBuilder::new(room_id_2).add_timeline_event(
|
||||
// Predecessor of room 2 is room 1.
|
||||
event_factory
|
||||
.create(sender, RoomVersionId::try_from("43").unwrap())
|
||||
.predecessor(room_id_1, event_id!("$ev1")),
|
||||
),
|
||||
)
|
||||
.build_sync_response();
|
||||
|
||||
// At this point, we can check that `response` contains misordered room updates.
|
||||
{
|
||||
let mut rooms = response.rooms.join.keys();
|
||||
|
||||
// Room 1 is before room 0!
|
||||
assert_eq!(rooms.next().unwrap(), room_id_1);
|
||||
assert_eq!(rooms.next().unwrap(), room_id_0);
|
||||
assert_eq!(rooms.next().unwrap(), room_id_2);
|
||||
assert!(rooms.next().is_none());
|
||||
}
|
||||
|
||||
// But the algorithm to detect invalid states works nicely.
|
||||
assert!(client.receive_sync_response(response).await.is_ok());
|
||||
|
||||
let room_0 = client.get_room(room_id_0).unwrap();
|
||||
|
||||
assert!(room_0.predecessor_room().is_none(), "room 0 must not have a predecessor");
|
||||
assert_eq!(
|
||||
room_0.successor_room().expect("room 0 must have a successor").room_id,
|
||||
room_id_1,
|
||||
"room 0 does not have the expected successor",
|
||||
);
|
||||
|
||||
let room_1 = client.get_room(room_id_1).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
room_1.predecessor_room().expect("room 1 must have a predecessor").room_id,
|
||||
room_id_0,
|
||||
"room 1 does not have the expected predecessor",
|
||||
);
|
||||
assert_eq!(
|
||||
room_1.successor_room().expect("room 1 must have a successor").room_id,
|
||||
room_id_2,
|
||||
"room 1 does not have the expected successor",
|
||||
);
|
||||
|
||||
let room_2 = client.get_room(room_id_2).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
room_2.predecessor_room().expect("room 2 must have a predecessor").room_id,
|
||||
room_id_1,
|
||||
"room 2 does not have the expected predecessor",
|
||||
);
|
||||
assert!(room_2.successor_room().is_none(), "room 2 must not have a successor");
|
||||
}
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_check_room_upgrades_shortest_invalid_successor() {
|
||||
let sender = user_id!("@mnt_io:matrix.org");
|
||||
let event_factory = EventFactory::new().sender(sender);
|
||||
let mut response_builder = SyncResponseBuilder::new();
|
||||
let room_id_0 = room_id!("!r0");
|
||||
|
||||
let client = logged_in_base_client(None).await;
|
||||
|
||||
// Room 0.
|
||||
{
|
||||
let tombstone_event_id = event_id!("$ev0");
|
||||
let response = response_builder
|
||||
.add_joined_room(
|
||||
// Successor of room 0 is room 0.
|
||||
// No predecessor.
|
||||
JoinedRoomBuilder::new(room_id_0).add_timeline_event(
|
||||
event_factory
|
||||
.room_tombstone("hello", room_id_0)
|
||||
.event_id(tombstone_event_id),
|
||||
),
|
||||
)
|
||||
.build_sync_response();
|
||||
|
||||
// The sync doesn't fail but…
|
||||
assert!(client.receive_sync_response(response).await.is_ok());
|
||||
|
||||
// … the state event has not been saved.
|
||||
let room_0 = client.get_room(room_id_0).unwrap();
|
||||
|
||||
assert!(room_0.predecessor_room().is_none(), "room 0 must not have a predecessor");
|
||||
assert!(room_0.successor_room().is_none(), "room 0 must not have a successor");
|
||||
}
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_check_room_upgrades_invalid_successor() {
|
||||
let sender = user_id!("@mnt_io:matrix.org");
|
||||
let event_factory = EventFactory::new().sender(sender);
|
||||
let mut response_builder = SyncResponseBuilder::new();
|
||||
let room_id_0 = room_id!("!r0");
|
||||
let room_id_1 = room_id!("!r1");
|
||||
let room_id_2 = room_id!("!r2");
|
||||
|
||||
let client = logged_in_base_client(None).await;
|
||||
|
||||
// Room 0 and room 1.
|
||||
{
|
||||
let tombstone_event_id = event_id!("$ev0");
|
||||
let response = response_builder
|
||||
.add_joined_room(JoinedRoomBuilder::new(room_id_0).add_timeline_event(
|
||||
// Successor of room 0 is room 1.
|
||||
event_factory.room_tombstone("hello", room_id_1).event_id(tombstone_event_id),
|
||||
))
|
||||
.add_joined_room(
|
||||
JoinedRoomBuilder::new(room_id_1).add_timeline_event(
|
||||
// Predecessor of room 1 is room 0.
|
||||
event_factory
|
||||
.create(sender, RoomVersionId::try_from("42").unwrap())
|
||||
.predecessor(room_id_0, tombstone_event_id),
|
||||
),
|
||||
)
|
||||
.build_sync_response();
|
||||
|
||||
assert!(client.receive_sync_response(response).await.is_ok());
|
||||
|
||||
let room_0 = client.get_room(room_id_0).unwrap();
|
||||
|
||||
assert!(room_0.predecessor_room().is_none(), "room 0 must not have a predecessor");
|
||||
assert_eq!(
|
||||
room_0.successor_room().expect("room 0 must have a successor").room_id,
|
||||
room_id_1,
|
||||
"room 0 does not have the expected successor",
|
||||
);
|
||||
|
||||
let room_1 = client.get_room(room_id_1).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
room_1.predecessor_room().expect("room 1 must have a predecessor").room_id,
|
||||
room_id_0,
|
||||
"room 1 does not have the expected predecessor",
|
||||
);
|
||||
assert!(room_1.successor_room().is_none(), "room 1 must not have a successor");
|
||||
}
|
||||
|
||||
// Room 1, room 2 and room 0.
|
||||
{
|
||||
let tombstone_event_id = event_id!("$ev1");
|
||||
let response = response_builder
|
||||
.add_joined_room(JoinedRoomBuilder::new(room_id_1).add_timeline_event(
|
||||
// Successor of room 1 is room 2.
|
||||
event_factory.room_tombstone("hello", room_id_2).event_id(tombstone_event_id),
|
||||
))
|
||||
.add_joined_room(
|
||||
JoinedRoomBuilder::new(room_id_2)
|
||||
.add_timeline_event(
|
||||
// Predecessor of room 2 is room 1.
|
||||
event_factory
|
||||
.create(sender, RoomVersionId::try_from("43").unwrap())
|
||||
.predecessor(room_id_1, tombstone_event_id),
|
||||
)
|
||||
.add_timeline_event(
|
||||
// Successor of room 2 is room 0.
|
||||
event_factory
|
||||
.room_tombstone("hehe", room_id_0)
|
||||
.event_id(event_id!("$ev_foo")),
|
||||
),
|
||||
)
|
||||
.build_sync_response();
|
||||
|
||||
// The sync doesn't fail but…
|
||||
assert!(client.receive_sync_response(response).await.is_ok());
|
||||
|
||||
// … the state event for `room_id_2` has not been saved.
|
||||
let room_0 = client.get_room(room_id_0).unwrap();
|
||||
|
||||
assert!(room_0.predecessor_room().is_none(), "room 0 must not have a predecessor");
|
||||
assert_eq!(
|
||||
room_0.successor_room().expect("room 0 must have a successor").room_id,
|
||||
room_id_1,
|
||||
"room 0 does not have the expected successor",
|
||||
);
|
||||
|
||||
let room_1 = client.get_room(room_id_1).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
room_1.predecessor_room().expect("room 1 must have a predecessor").room_id,
|
||||
room_id_0,
|
||||
"room 1 does not have the expected predecessor",
|
||||
);
|
||||
assert_eq!(
|
||||
room_1.successor_room().expect("room 1 must have a successor").room_id,
|
||||
room_id_2,
|
||||
"room 1 does not have the expected successor",
|
||||
);
|
||||
|
||||
let room_2 = client.get_room(room_id_2).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
room_2.predecessor_room().expect("room 2 must have a predecessor").room_id,
|
||||
room_id_1,
|
||||
"room 2 does not have the expected predecessor",
|
||||
);
|
||||
// this state event is missing because it creates a loop
|
||||
assert!(room_2.successor_room().is_none(), "room 2 must not have a successor",);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_check_room_upgrades_shortest_invalid_predecessor() {
|
||||
let sender = user_id!("@mnt_io:matrix.org");
|
||||
let event_factory = EventFactory::new().sender(sender);
|
||||
let mut response_builder = SyncResponseBuilder::new();
|
||||
let room_id_0 = room_id!("!r0");
|
||||
|
||||
let client = logged_in_base_client(None).await;
|
||||
|
||||
// Room 0.
|
||||
{
|
||||
let tombstone_event_id = event_id!("$ev0");
|
||||
let response = response_builder
|
||||
.add_joined_room(
|
||||
// Predecessor of room 0 is room 0.
|
||||
// No successor.
|
||||
JoinedRoomBuilder::new(room_id_0).add_timeline_event(
|
||||
event_factory
|
||||
.create(sender, RoomVersionId::try_from("42").unwrap())
|
||||
.predecessor(room_id_0, tombstone_event_id)
|
||||
.event_id(tombstone_event_id),
|
||||
),
|
||||
)
|
||||
.build_sync_response();
|
||||
|
||||
// The sync doesn't fail but…
|
||||
assert!(client.receive_sync_response(response).await.is_ok());
|
||||
|
||||
// … the state event has not been saved.
|
||||
let room_0 = client.get_room(room_id_0).unwrap();
|
||||
|
||||
assert!(room_0.predecessor_room().is_none(), "room 0 must not have a predecessor");
|
||||
assert!(room_0.successor_room().is_none(), "room 0 must not have a successor");
|
||||
}
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_check_room_upgrades_shortest_loop() {
|
||||
let sender = user_id!("@mnt_io:matrix.org");
|
||||
let event_factory = EventFactory::new().sender(sender);
|
||||
let mut response_builder = SyncResponseBuilder::new();
|
||||
let room_id_0 = room_id!("!r0");
|
||||
|
||||
let client = logged_in_base_client(None).await;
|
||||
|
||||
// Room 0.
|
||||
{
|
||||
let tombstone_event_id = event_id!("$ev0");
|
||||
let response = response_builder
|
||||
.add_joined_room(
|
||||
JoinedRoomBuilder::new(room_id_0)
|
||||
.add_timeline_event(
|
||||
// Successor of room 0 is room 0
|
||||
event_factory
|
||||
.room_tombstone("hello", room_id_0)
|
||||
.event_id(tombstone_event_id),
|
||||
)
|
||||
.add_timeline_event(
|
||||
// Predecessor of room 0 is room 0
|
||||
event_factory
|
||||
.create(sender, RoomVersionId::try_from("42").unwrap())
|
||||
.predecessor(room_id_0, tombstone_event_id),
|
||||
),
|
||||
)
|
||||
.build_sync_response();
|
||||
|
||||
// The sync doesn't fail but…
|
||||
assert!(client.receive_sync_response(response).await.is_ok());
|
||||
|
||||
// … the state event has not been saved.
|
||||
let room_0 = client.get_room(room_id_0).unwrap();
|
||||
|
||||
assert!(room_0.predecessor_room().is_none(), "room 0 must not have a predecessor");
|
||||
assert!(room_0.successor_room().is_none(), "room 0 must not have a successor");
|
||||
}
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_check_room_upgrades_loop() {
|
||||
let sender = user_id!("@mnt_io:matrix.org");
|
||||
let event_factory = EventFactory::new().sender(sender);
|
||||
let mut response_builder = SyncResponseBuilder::new();
|
||||
let room_id_0 = room_id!("!r0");
|
||||
let room_id_1 = room_id!("!r1");
|
||||
let room_id_2 = room_id!("!r2");
|
||||
|
||||
let client = logged_in_base_client(None).await;
|
||||
|
||||
// Room 0, room 1 and room 2.
|
||||
//
|
||||
// Doing that in one sync, it's the only way to create such loop (otherwise it
|
||||
// implies overwriting the `m.room.create` event, or not setting it first, then
|
||||
// setting it later… anyway, it works in one sync)
|
||||
{
|
||||
let response = response_builder
|
||||
.add_joined_room(
|
||||
JoinedRoomBuilder::new(room_id_0)
|
||||
.add_timeline_event(
|
||||
// Predecessor of room 0 is room 2
|
||||
event_factory
|
||||
.create(sender, RoomVersionId::try_from("42").unwrap())
|
||||
.predecessor(room_id_2, event_id!("$ev2")),
|
||||
)
|
||||
.add_timeline_event(
|
||||
// Successor of room 0 is room 1
|
||||
event_factory
|
||||
.room_tombstone("hello", room_id_1)
|
||||
.event_id(event_id!("$ev0")),
|
||||
),
|
||||
)
|
||||
.add_joined_room(
|
||||
JoinedRoomBuilder::new(room_id_1)
|
||||
.add_timeline_event(
|
||||
// Predecessor of room 1 is room 0
|
||||
event_factory
|
||||
.create(sender, RoomVersionId::try_from("43").unwrap())
|
||||
.predecessor(room_id_0, event_id!("$ev0")),
|
||||
)
|
||||
.add_timeline_event(
|
||||
// Successor of room 1 is room 2
|
||||
event_factory
|
||||
.room_tombstone("hello", room_id_2)
|
||||
.event_id(event_id!("$ev1")),
|
||||
),
|
||||
)
|
||||
.add_joined_room(
|
||||
JoinedRoomBuilder::new(room_id_2)
|
||||
.add_timeline_event(
|
||||
// Predecessor of room 2 is room 1
|
||||
event_factory
|
||||
.create(sender, RoomVersionId::try_from("44").unwrap())
|
||||
.predecessor(room_id_1, event_id!("$ev1")),
|
||||
)
|
||||
.add_timeline_event(
|
||||
// Successor of room 2 is room 0
|
||||
event_factory
|
||||
.room_tombstone("hello", room_id_0)
|
||||
.event_id(event_id!("$ev2")),
|
||||
),
|
||||
)
|
||||
.build_sync_response();
|
||||
|
||||
// The sync doesn't fail but…
|
||||
assert!(client.receive_sync_response(response).await.is_ok());
|
||||
|
||||
// … the state event for room 2 -> room 0 has not been saved, but room 0 <- room
|
||||
// 2 has been saved.
|
||||
let room_0 = client.get_room(room_id_0).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
room_0.predecessor_room().expect("room 0 must have a predecessor").room_id,
|
||||
room_id_2,
|
||||
"room 0 does not have the expected predecessor"
|
||||
);
|
||||
assert_eq!(
|
||||
room_0.successor_room().expect("room 0 must have a successor").room_id,
|
||||
room_id_1,
|
||||
"room 0 does not have the expected successor",
|
||||
);
|
||||
|
||||
let room_1 = client.get_room(room_id_1).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
room_1.predecessor_room().expect("room 1 must have a predecessor").room_id,
|
||||
room_id_0,
|
||||
"room 1 does not have the expected predecessor",
|
||||
);
|
||||
assert_eq!(
|
||||
room_1.successor_room().expect("room 1 must have a successor").room_id,
|
||||
room_id_2,
|
||||
"room 1 does not have the expected successor",
|
||||
);
|
||||
|
||||
let room_2 = client.get_room(room_id_2).unwrap();
|
||||
|
||||
// this state event is missing because it creates a loop
|
||||
assert!(room_2.predecessor_room().is_none(), "room 2 must not have a predecessor");
|
||||
assert!(room_2.successor_room().is_none(), "room 2 must not have a successor",);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_state_events_after_sync() {
|
||||
// Given a room
|
||||
|
||||
@@ -24,7 +24,7 @@ use ruma::{
|
||||
StateEventType,
|
||||
},
|
||||
push::{Action, PushConditionRoomCtx},
|
||||
RoomVersionId, UInt, UserId,
|
||||
UInt, UserId,
|
||||
};
|
||||
use tracing::{instrument, trace, warn};
|
||||
|
||||
@@ -76,9 +76,9 @@ pub async fn build<'notification, 'e2ee>(
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomRedaction(
|
||||
redaction_event,
|
||||
)) => {
|
||||
let room_version = room_info.room_version().unwrap_or(&RoomVersionId::V1);
|
||||
let room_version = room_info.room_version_or_default();
|
||||
|
||||
if let Some(redacts) = redaction_event.redacts(room_version) {
|
||||
if let Some(redacts) = redaction_event.redacts(&room_version) {
|
||||
room_info
|
||||
.handle_redaction(redaction_event, timeline_event.raw().cast_ref());
|
||||
|
||||
|
||||
@@ -51,7 +51,11 @@ impl Room {
|
||||
if event.content.membership == MembershipState::Knock {
|
||||
event_to_user_ids.push((event.event_id, event.state_key))
|
||||
} else {
|
||||
warn!("Could not mark knock event as seen: event {} for user {} is not in Knock membership state.", event.event_id, event.state_key);
|
||||
warn!(
|
||||
"Could not mark knock event as seen: event {} for user {} \
|
||||
is not in Knock membership state.",
|
||||
event.event_id, event.state_key
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => warn!(
|
||||
|
||||
@@ -88,6 +88,7 @@ mod tests_with_e2e_encryption {
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
client::ThreadingSupport,
|
||||
latest_event::LatestEvent,
|
||||
response_processors as processors,
|
||||
store::{MemoryStore, RoomLoadSettings, StoreConfig},
|
||||
@@ -107,8 +108,10 @@ mod tests_with_e2e_encryption {
|
||||
#[async_test]
|
||||
async fn test_setting_the_latest_event_doesnt_cause_a_room_info_notable_update() {
|
||||
// Given a room,
|
||||
let client =
|
||||
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
|
||||
let client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
client
|
||||
.activate(
|
||||
|
||||
@@ -53,7 +53,7 @@ use ruma::{
|
||||
direct::OwnedDirectUserIdentifier,
|
||||
receipt::{Receipt, ReceiptThread, ReceiptType},
|
||||
room::{
|
||||
avatar::{self},
|
||||
avatar,
|
||||
guest_access::GuestAccess,
|
||||
history_visibility::HistoryVisibility,
|
||||
join_rules::JoinRule,
|
||||
@@ -340,13 +340,15 @@ impl Room {
|
||||
}
|
||||
|
||||
/// Is the room considered to be public.
|
||||
pub fn is_public(&self) -> bool {
|
||||
matches!(self.join_rule(), JoinRule::Public)
|
||||
///
|
||||
/// May return `None` if the join rule event is not available.
|
||||
pub fn is_public(&self) -> Option<bool> {
|
||||
self.inner.read().join_rule().map(|join_rule| matches!(join_rule, JoinRule::Public))
|
||||
}
|
||||
|
||||
/// Get the join rule policy of this room.
|
||||
pub fn join_rule(&self) -> JoinRule {
|
||||
self.inner.read().join_rule().clone()
|
||||
/// Get the join rule policy of this room, if available.
|
||||
pub fn join_rule(&self) -> Option<JoinRule> {
|
||||
self.inner.read().join_rule().cloned()
|
||||
}
|
||||
|
||||
/// Get the maximum power level that this room contains.
|
||||
|
||||
@@ -19,7 +19,7 @@ use std::{
|
||||
|
||||
use bitflags::bitflags;
|
||||
use eyeball::Subscriber;
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEventKind;
|
||||
use matrix_sdk_common::{deserialized_responses::TimelineEventKind, ROOM_VERSION_FALLBACK};
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::v3::RoomSummary as RumaSummary,
|
||||
assign,
|
||||
@@ -46,8 +46,8 @@ use ruma::{
|
||||
},
|
||||
room::RoomType,
|
||||
serde::Raw,
|
||||
EventId, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId,
|
||||
RoomAliasId, RoomId, RoomVersionId, UserId,
|
||||
EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId,
|
||||
OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomVersionId, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{debug, field::debug, info, instrument, warn};
|
||||
@@ -192,6 +192,7 @@ impl BaseRoomInfo {
|
||||
AnySyncStateEvent::RoomName(n) => {
|
||||
self.name = Some(n.into());
|
||||
}
|
||||
// `m.room.create` can NOT be overwritten.
|
||||
AnySyncStateEvent::RoomCreate(c) if self.create.is_none() => {
|
||||
self.create = Some(c.into());
|
||||
}
|
||||
@@ -309,7 +310,7 @@ impl BaseRoomInfo {
|
||||
}
|
||||
|
||||
pub(super) fn handle_redaction(&mut self, redacts: &EventId) {
|
||||
let room_version = self.room_version().unwrap_or(&RoomVersionId::V1).to_owned();
|
||||
let room_version = self.room_version().unwrap_or(&ROOM_VERSION_FALLBACK).to_owned();
|
||||
|
||||
// FIXME: Use let chains once available to get rid of unwrap()s
|
||||
if self.avatar.has_event_id(redacts) {
|
||||
@@ -463,6 +464,14 @@ pub struct RoomInfo {
|
||||
/// more accurate than relying on the latest event.
|
||||
#[serde(default)]
|
||||
pub(crate) recency_stamp: Option<u64>,
|
||||
|
||||
/// A timestamp remembering when we observed the user accepting an invite on
|
||||
/// this current device.
|
||||
///
|
||||
/// This is useful to remember if the user accepted this a join on this
|
||||
/// specific client.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) invite_accepted_at: Option<MilliSecondsSinceUnixEpoch>,
|
||||
}
|
||||
|
||||
impl RoomInfo {
|
||||
@@ -485,6 +494,7 @@ impl RoomInfo {
|
||||
cached_display_name: None,
|
||||
cached_user_defined_notification_mode: None,
|
||||
recency_stamp: None,
|
||||
invite_accepted_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -642,9 +652,9 @@ impl RoomInfo {
|
||||
event: &SyncRoomRedactionEvent,
|
||||
_raw: &Raw<SyncRoomRedactionEvent>,
|
||||
) {
|
||||
let room_version = self.base_info.room_version().unwrap_or(&RoomVersionId::V1);
|
||||
let room_version = self.room_version_or_default();
|
||||
|
||||
let Some(redacts) = event.redacts(room_version) else {
|
||||
let Some(redacts) = event.redacts(&room_version) else {
|
||||
info!("Can't apply redaction, redacts field is missing");
|
||||
return;
|
||||
};
|
||||
@@ -653,7 +663,7 @@ impl RoomInfo {
|
||||
if let Some(latest_event) = &mut self.latest_event {
|
||||
tracing::trace!("Checking if redaction applies to latest event");
|
||||
if latest_event.event_id().as_deref() == Some(redacts) {
|
||||
match apply_redaction(latest_event.event().raw(), _raw, room_version) {
|
||||
match apply_redaction(latest_event.event().raw(), _raw, &room_version) {
|
||||
Some(redacted) => {
|
||||
// Even if the original event was encrypted, redaction removes all its
|
||||
// fields so it cannot possibly be successfully decrypted after redaction.
|
||||
@@ -748,6 +758,22 @@ impl RoomInfo {
|
||||
self.summary.invited_member_count = count;
|
||||
}
|
||||
|
||||
/// Mark that the user has accepted an invite and remember when this has
|
||||
/// happened using a timestamp set to [`MilliSecondsSinceUnixEpoch::now()`].
|
||||
pub(crate) fn set_invite_accepted_now(&mut self) {
|
||||
self.invite_accepted_at = Some(MilliSecondsSinceUnixEpoch::now());
|
||||
}
|
||||
|
||||
/// Returns the timestamp when an invite to this room has been accepted by
|
||||
/// this specific client.
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Some` if the invite has been accepted by this specific client.
|
||||
/// - `None` if the invite has not been accepted
|
||||
pub fn invite_accepted_at(&self) -> Option<MilliSecondsSinceUnixEpoch> {
|
||||
self.invite_accepted_at
|
||||
}
|
||||
|
||||
/// Updates the room heroes.
|
||||
pub(crate) fn update_heroes(&mut self, heroes: Vec<RoomHero>) {
|
||||
self.summary.room_heroes = heroes;
|
||||
@@ -813,10 +839,10 @@ impl RoomInfo {
|
||||
.compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
|
||||
.is_ok()
|
||||
{
|
||||
warn!("Unknown room version, falling back to v10");
|
||||
warn!("Unknown room version, falling back to {ROOM_VERSION_FALLBACK}");
|
||||
}
|
||||
|
||||
RoomVersionId::V10
|
||||
ROOM_VERSION_FALLBACK
|
||||
})
|
||||
}
|
||||
|
||||
@@ -866,13 +892,12 @@ impl RoomInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the join rule for this room.
|
||||
///
|
||||
/// Defaults to `Public`, if missing.
|
||||
pub fn join_rule(&self) -> &JoinRule {
|
||||
/// Return the join rule for this room, if the `m.room.join_rules` event is
|
||||
/// available.
|
||||
pub fn join_rule(&self) -> Option<&JoinRule> {
|
||||
match &self.base_info.join_rules {
|
||||
Some(MinimalStateEvent::Original(ev)) => &ev.content.join_rule,
|
||||
_ => &JoinRule::Public,
|
||||
Some(MinimalStateEvent::Original(ev)) => Some(&ev.content.join_rule),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -882,7 +907,13 @@ impl RoomInfo {
|
||||
(!name.is_empty()).then_some(name)
|
||||
}
|
||||
|
||||
pub(super) fn tombstone(&self) -> Option<&RoomTombstoneEventContent> {
|
||||
/// Get the content of the `m.room.create` event if any.
|
||||
pub fn create(&self) -> Option<&RoomCreateWithCreatorEventContent> {
|
||||
Some(&self.base_info.create.as_ref()?.as_original()?.content)
|
||||
}
|
||||
|
||||
/// Get the content of the `m.room.tombstone` event if any.
|
||||
pub fn tombstone(&self) -> Option<&RoomTombstoneEventContent> {
|
||||
Some(&self.base_info.tombstone.as_ref()?.as_original()?.content)
|
||||
}
|
||||
|
||||
@@ -1167,6 +1198,7 @@ mod tests {
|
||||
owned_mxc_uri, owned_user_id, room_id, serde::Raw,
|
||||
};
|
||||
use serde_json::json;
|
||||
use similar_asserts::assert_eq;
|
||||
|
||||
use super::{BaseRoomInfo, RoomInfo, SyncInfo};
|
||||
use crate::{
|
||||
@@ -1215,6 +1247,7 @@ mod tests {
|
||||
cached_display_name: None,
|
||||
cached_user_defined_notification_mode: None,
|
||||
recency_stamp: Some(42),
|
||||
invite_accepted_at: None,
|
||||
};
|
||||
|
||||
let info_json = json!({
|
||||
@@ -1268,7 +1301,7 @@ mod tests {
|
||||
"num_mentions": 0,
|
||||
"num_notifications": 0,
|
||||
"latest_active": null,
|
||||
"pending": []
|
||||
"pending": [],
|
||||
},
|
||||
"recency_stamp": 42,
|
||||
});
|
||||
|
||||
@@ -82,6 +82,7 @@ mod tests {
|
||||
|
||||
use super::{super::BaseRoomInfo, RoomNotableTags};
|
||||
use crate::{
|
||||
client::ThreadingSupport,
|
||||
response_processors as processors,
|
||||
store::{RoomLoadSettings, StoreConfig},
|
||||
BaseClient, RoomState, SessionMeta,
|
||||
@@ -90,8 +91,10 @@ mod tests {
|
||||
#[async_test]
|
||||
async fn test_is_favourite() {
|
||||
// Given a room,
|
||||
let client =
|
||||
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
|
||||
let client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
client
|
||||
.activate(
|
||||
@@ -186,8 +189,10 @@ mod tests {
|
||||
#[async_test]
|
||||
async fn test_is_low_priority() {
|
||||
// Given a room,
|
||||
let client =
|
||||
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
|
||||
let client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
client
|
||||
.activate(
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
|
||||
//! Extend `BaseClient` with capabilities to handle MSC4186.
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent;
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use ruma::{api::client::sync::sync_events::v5 as http, OwnedRoomId};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::{events::AnyToDeviceEvent, serde::Raw};
|
||||
use tracing::{instrument, trace};
|
||||
|
||||
use super::BaseClient;
|
||||
@@ -44,7 +44,7 @@ impl BaseClient {
|
||||
&self,
|
||||
to_device: Option<&http::response::ToDevice>,
|
||||
e2ee: &http::response::E2EE,
|
||||
) -> Result<Option<Vec<Raw<AnyToDeviceEvent>>>> {
|
||||
) -> Result<Option<Vec<ProcessedToDeviceEvent>>> {
|
||||
if to_device.is_none() && e2ee.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -62,7 +62,7 @@ impl BaseClient {
|
||||
|
||||
let mut context = processors::Context::default();
|
||||
|
||||
let processors::e2ee::to_device::Output { decrypted_to_device_events, room_key_updates } =
|
||||
let processors::e2ee::to_device::Output { processed_to_device_events, room_key_updates } =
|
||||
processors::e2ee::to_device::from_msc4186(to_device, e2ee, olm_machine.as_ref())
|
||||
.await?;
|
||||
|
||||
@@ -75,7 +75,7 @@ impl BaseClient {
|
||||
.collect(),
|
||||
processors::e2ee::E2EE::new(
|
||||
olm_machine.as_ref(),
|
||||
self.decryption_trust_requirement,
|
||||
&self.decryption_settings,
|
||||
self.handle_verification_events,
|
||||
),
|
||||
)
|
||||
@@ -89,7 +89,7 @@ impl BaseClient {
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Some(decrypted_to_device_events))
|
||||
Ok(Some(processed_to_device_events))
|
||||
}
|
||||
|
||||
/// Process a response from a sliding sync call.
|
||||
@@ -117,7 +117,7 @@ impl BaseClient {
|
||||
// we received a room reshuffling event only, there won't be anything for us to
|
||||
// process. stop early
|
||||
return Ok(SyncResponse::default());
|
||||
};
|
||||
}
|
||||
|
||||
let mut context = processors::Context::default();
|
||||
|
||||
@@ -152,7 +152,7 @@ impl BaseClient {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
processors::e2ee::E2EE::new(
|
||||
self.olm_machine().await.as_ref(),
|
||||
self.decryption_trust_requirement,
|
||||
&self.decryption_settings,
|
||||
self.handle_verification_events,
|
||||
),
|
||||
processors::notification::Notification::new(
|
||||
@@ -284,6 +284,7 @@ impl BaseClient {
|
||||
room_previous_events,
|
||||
&joined_room_update.timeline.events,
|
||||
&mut room_info.read_receipts,
|
||||
self.threading_support,
|
||||
);
|
||||
|
||||
if prev_read_receipts != room_info.read_receipts {
|
||||
@@ -348,6 +349,7 @@ mod tests {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use super::processors::room::msc4186::cache_latest_events;
|
||||
use crate::{
|
||||
client::ThreadingSupport,
|
||||
room::{RoomHero, RoomInfoNotableUpdateReasons},
|
||||
store::{RoomLoadSettings, StoreConfig},
|
||||
test_utils::logged_in_base_client,
|
||||
@@ -1157,7 +1159,7 @@ mod tests {
|
||||
let store = StoreConfig::new("cross-process-foo".to_owned());
|
||||
state_store = store.state_store.clone();
|
||||
|
||||
let client = BaseClient::new(store);
|
||||
let client = BaseClient::new(store, ThreadingSupport::Disabled);
|
||||
client
|
||||
.activate(
|
||||
session_meta.clone(),
|
||||
@@ -1188,7 +1190,7 @@ mod tests {
|
||||
let client = {
|
||||
let mut store = StoreConfig::new("cross-process-foo".to_owned());
|
||||
store.state_store = state_store;
|
||||
let client = BaseClient::new(store);
|
||||
let client = BaseClient::new(store, ThreadingSupport::Disabled);
|
||||
client
|
||||
.activate(
|
||||
session_meta,
|
||||
@@ -1486,7 +1488,7 @@ mod tests {
|
||||
#[async_test]
|
||||
async fn test_when_only_one_event_we_cache_it() {
|
||||
let event1 = make_event("m.room.message", "$1");
|
||||
let events = &[event1.clone()];
|
||||
let events = std::slice::from_ref(&event1);
|
||||
let chosen = choose_event_to_cache(events).await;
|
||||
assert_eq!(ev_id(chosen), rawev_id(event1));
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@ use assert_matches2::assert_let;
|
||||
use growable_bloom_filter::GrowableBloomBuilder;
|
||||
use matrix_sdk_test::{event_factory::EventFactory, test_json};
|
||||
use ruma::{
|
||||
api::MatrixVersion,
|
||||
api::{
|
||||
client::discovery::discover_homeserver::{HomeserverInfo, RtcFocusInfo},
|
||||
FeatureFlag, MatrixVersion,
|
||||
},
|
||||
event_id,
|
||||
events::{
|
||||
presence::PresenceEvent,
|
||||
@@ -34,7 +37,7 @@ use serde_json::{json, value::Value as JsonValue};
|
||||
|
||||
use super::{
|
||||
send_queue::SentRequestKey, DependentQueuedRequestKind, DisplayName, DynStateStore,
|
||||
RoomLoadSettings, ServerCapabilities,
|
||||
RoomLoadSettings, ServerInfo, WellKnownResponse,
|
||||
};
|
||||
use crate::{
|
||||
deserialized_responses::MemberEvent,
|
||||
@@ -90,8 +93,8 @@ pub trait StateStoreIntegrationTests {
|
||||
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);
|
||||
/// Test saving/restoring server info.
|
||||
async fn test_server_info_saving(&self);
|
||||
/// Test fetching room infos based on [`RoomLoadSettings`].
|
||||
async fn test_get_room_infos(&self);
|
||||
}
|
||||
@@ -472,34 +475,41 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_server_capabilities_saving(&self) {
|
||||
async fn test_server_info_saving(&self) {
|
||||
let versions = &[MatrixVersion::V1_1, MatrixVersion::V1_2, MatrixVersion::V1_11];
|
||||
let server_caps = ServerCapabilities::new(
|
||||
versions,
|
||||
let server_info = ServerInfo::new(
|
||||
versions.iter().map(|version| version.to_string()).collect(),
|
||||
[("org.matrix.experimental".to_owned(), true)].into(),
|
||||
Some(WellKnownResponse {
|
||||
homeserver: HomeserverInfo::new("matrix.example.com".to_owned()),
|
||||
identity_server: None,
|
||||
tile_server: None,
|
||||
rtc_foci: vec![RtcFocusInfo::livekit("livekit.example.com".to_owned())],
|
||||
}),
|
||||
);
|
||||
|
||||
self.set_kv_data(
|
||||
StateStoreDataKey::ServerCapabilities,
|
||||
StateStoreDataValue::ServerCapabilities(server_caps.clone()),
|
||||
StateStoreDataKey::ServerInfo,
|
||||
StateStoreDataValue::ServerInfo(server_info.clone()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_let!(
|
||||
Ok(Some(StateStoreDataValue::ServerCapabilities(stored_caps))) =
|
||||
self.get_kv_data(StateStoreDataKey::ServerCapabilities).await
|
||||
Ok(Some(StateStoreDataValue::ServerInfo(stored_info))) =
|
||||
self.get_kv_data(StateStoreDataKey::ServerInfo).await
|
||||
);
|
||||
assert_eq!(stored_caps, server_caps);
|
||||
assert_eq!(stored_info, server_info);
|
||||
|
||||
let (stored_versions, stored_features) = stored_caps.maybe_decode().unwrap();
|
||||
let decoded_server_info = stored_info.maybe_decode().unwrap();
|
||||
let stored_supported = decoded_server_info.supported_versions();
|
||||
|
||||
assert_eq!(stored_versions, versions);
|
||||
assert_eq!(stored_features.len(), 1);
|
||||
assert_eq!(stored_features.get("org.matrix.experimental"), Some(&true));
|
||||
assert_eq!(stored_supported.versions.as_ref(), versions);
|
||||
assert_eq!(stored_supported.features.len(), 1);
|
||||
assert!(stored_supported.features.contains(&FeatureFlag::from("org.matrix.experimental")));
|
||||
|
||||
self.remove_kv_data(StateStoreDataKey::ServerCapabilities).await.unwrap();
|
||||
assert_matches!(self.get_kv_data(StateStoreDataKey::ServerCapabilities).await, Ok(None));
|
||||
self.remove_kv_data(StateStoreDataKey::ServerInfo).await.unwrap();
|
||||
assert_matches!(self.get_kv_data(StateStoreDataKey::ServerInfo).await, Ok(None));
|
||||
}
|
||||
|
||||
async fn test_sync_token_saving(&self) {
|
||||
@@ -1807,9 +1817,9 @@ macro_rules! statestore_integration_tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_server_capabilities_saving() {
|
||||
async fn test_server_info_saving() {
|
||||
let store = get_store().await.unwrap().into_state_store();
|
||||
store.test_server_capabilities_saving().await
|
||||
store.test_server_info_saving().await
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
|
||||
@@ -19,6 +19,7 @@ use std::{
|
||||
|
||||
use async_trait::async_trait;
|
||||
use growable_bloom_filter::GrowableBloom;
|
||||
use matrix_sdk_common::ROOM_VERSION_FALLBACK;
|
||||
use ruma::{
|
||||
canonical_json::{redact, RedactedBecause},
|
||||
events::{
|
||||
@@ -31,13 +32,13 @@ use ruma::{
|
||||
serde::Raw,
|
||||
time::Instant,
|
||||
CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri,
|
||||
OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, RoomVersionId, TransactionId, UserId,
|
||||
OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId,
|
||||
};
|
||||
use tracing::{debug, instrument, warn};
|
||||
|
||||
use super::{
|
||||
send_queue::{ChildTransactionId, QueuedRequest, SentRequestKey},
|
||||
traits::{ComposerDraft, ServerCapabilities},
|
||||
traits::{ComposerDraft, ServerInfo},
|
||||
DependentQueuedRequest, DependentQueuedRequestKind, QueuedRequestKind, Result, RoomInfo,
|
||||
RoomLoadSettings, StateChanges, StateStore, StoreError,
|
||||
};
|
||||
@@ -51,10 +52,10 @@ use crate::{
|
||||
#[allow(clippy::type_complexity)]
|
||||
struct MemoryStoreInner {
|
||||
recently_visited_rooms: HashMap<OwnedUserId, Vec<OwnedRoomId>>,
|
||||
composer_drafts: HashMap<OwnedRoomId, ComposerDraft>,
|
||||
composer_drafts: HashMap<(OwnedRoomId, Option<OwnedEventId>), ComposerDraft>,
|
||||
user_avatar_url: HashMap<OwnedUserId, OwnedMxcUri>,
|
||||
sync_token: Option<String>,
|
||||
server_capabilities: Option<ServerCapabilities>,
|
||||
server_info: Option<ServerInfo>,
|
||||
filters: HashMap<String, String>,
|
||||
utd_hook_manager_data: Option<GrowableBloom>,
|
||||
account_data: HashMap<GlobalAccountDataEventType, Raw<AnyGlobalAccountDataEvent>>,
|
||||
@@ -149,8 +150,8 @@ impl StateStore for MemoryStore {
|
||||
StateStoreDataKey::SyncToken => {
|
||||
inner.sync_token.clone().map(StateStoreDataValue::SyncToken)
|
||||
}
|
||||
StateStoreDataKey::ServerCapabilities => {
|
||||
inner.server_capabilities.clone().map(StateStoreDataValue::ServerCapabilities)
|
||||
StateStoreDataKey::ServerInfo => {
|
||||
inner.server_info.clone().map(StateStoreDataValue::ServerInfo)
|
||||
}
|
||||
StateStoreDataKey::Filter(filter_name) => {
|
||||
inner.filters.get(filter_name).cloned().map(StateStoreDataValue::Filter)
|
||||
@@ -166,8 +167,9 @@ impl StateStore for MemoryStore {
|
||||
StateStoreDataKey::UtdHookManagerData => {
|
||||
inner.utd_hook_manager_data.clone().map(StateStoreDataValue::UtdHookManagerData)
|
||||
}
|
||||
StateStoreDataKey::ComposerDraft(room_id) => {
|
||||
inner.composer_drafts.get(room_id).cloned().map(StateStoreDataValue::ComposerDraft)
|
||||
StateStoreDataKey::ComposerDraft(room_id, thread_root) => {
|
||||
let key = (room_id.to_owned(), thread_root.map(ToOwned::to_owned));
|
||||
inner.composer_drafts.get(&key).cloned().map(StateStoreDataValue::ComposerDraft)
|
||||
}
|
||||
StateStoreDataKey::SeenKnockRequests(room_id) => inner
|
||||
.seen_knock_requests
|
||||
@@ -215,17 +217,15 @@ impl StateStore for MemoryStore {
|
||||
.expect("Session data not the hook manager data"),
|
||||
);
|
||||
}
|
||||
StateStoreDataKey::ComposerDraft(room_id) => {
|
||||
StateStoreDataKey::ComposerDraft(room_id, thread_root) => {
|
||||
inner.composer_drafts.insert(
|
||||
room_id.to_owned(),
|
||||
(room_id.to_owned(), thread_root.map(ToOwned::to_owned)),
|
||||
value.into_composer_draft().expect("Session data not a composer draft"),
|
||||
);
|
||||
}
|
||||
StateStoreDataKey::ServerCapabilities => {
|
||||
inner.server_capabilities = Some(
|
||||
value
|
||||
.into_server_capabilities()
|
||||
.expect("Session data not containing server capabilities"),
|
||||
StateStoreDataKey::ServerInfo => {
|
||||
inner.server_info = Some(
|
||||
value.into_server_info().expect("Session data not containing server info"),
|
||||
);
|
||||
}
|
||||
StateStoreDataKey::SeenKnockRequests(room_id) => {
|
||||
@@ -245,7 +245,7 @@ impl StateStore for MemoryStore {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
match key {
|
||||
StateStoreDataKey::SyncToken => inner.sync_token = None,
|
||||
StateStoreDataKey::ServerCapabilities => inner.server_capabilities = None,
|
||||
StateStoreDataKey::ServerInfo => inner.server_info = None,
|
||||
StateStoreDataKey::Filter(filter_name) => {
|
||||
inner.filters.remove(filter_name);
|
||||
}
|
||||
@@ -256,8 +256,9 @@ impl StateStore for MemoryStore {
|
||||
inner.recently_visited_rooms.remove(user_id);
|
||||
}
|
||||
StateStoreDataKey::UtdHookManagerData => inner.utd_hook_manager_data = None,
|
||||
StateStoreDataKey::ComposerDraft(room_id) => {
|
||||
inner.composer_drafts.remove(room_id);
|
||||
StateStoreDataKey::ComposerDraft(room_id, thread_root) => {
|
||||
let key = (room_id.to_owned(), thread_root.map(ToOwned::to_owned));
|
||||
inner.composer_drafts.remove(&key);
|
||||
}
|
||||
StateStoreDataKey::SeenKnockRequests(room_id) => {
|
||||
inner.seen_knock_requests.remove(room_id);
|
||||
@@ -439,12 +440,13 @@ impl StateStore for MemoryStore {
|
||||
}
|
||||
|
||||
let make_room_version = |room_info: &HashMap<OwnedRoomId, RoomInfo>, room_id| {
|
||||
room_info.get(room_id).and_then(|info| info.room_version().cloned()).unwrap_or_else(
|
||||
|| {
|
||||
warn!(?room_id, "Unable to find the room version, assuming version 9");
|
||||
RoomVersionId::V9
|
||||
},
|
||||
)
|
||||
room_info.get(room_id).map(|info| info.room_version_or_default()).unwrap_or_else(|| {
|
||||
warn!(
|
||||
?room_id,
|
||||
"Unable to find the room version, assuming {ROOM_VERSION_FALLBACK}"
|
||||
);
|
||||
ROOM_VERSION_FALLBACK
|
||||
})
|
||||
};
|
||||
|
||||
let inner = &mut *inner;
|
||||
|
||||
@@ -121,6 +121,7 @@ impl RoomInfoV1 {
|
||||
cached_display_name: None,
|
||||
cached_user_defined_notification_mode: None,
|
||||
recency_stamp: None,
|
||||
invite_accepted_at: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ use crate::{
|
||||
deserialized_responses::DisplayName,
|
||||
event_cache::store as event_cache_store,
|
||||
room::{RoomInfo, RoomInfoNotableUpdate, RoomState},
|
||||
MinimalRoomMemberEvent, Room, RoomStateFilter, SendOutsideWasm, SessionMeta, SyncOutsideWasm,
|
||||
MinimalRoomMemberEvent, Room, RoomStateFilter, SessionMeta,
|
||||
};
|
||||
|
||||
pub(crate) mod ambiguity_map;
|
||||
@@ -81,8 +81,8 @@ pub use self::{
|
||||
SentMediaInfo, SentRequestKey, SerializableEventContent,
|
||||
},
|
||||
traits::{
|
||||
ComposerDraft, ComposerDraftType, DynStateStore, IntoStateStore, ServerCapabilities,
|
||||
StateStore, StateStoreDataKey, StateStoreDataValue, StateStoreExt,
|
||||
ComposerDraft, ComposerDraftType, DynStateStore, IntoStateStore, ServerInfo, StateStore,
|
||||
StateStoreDataKey, StateStoreDataValue, StateStoreExt, WellKnownResponse,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -90,14 +90,8 @@ pub use self::{
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum StoreError {
|
||||
/// An error happened in the underlying database backend.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[error(transparent)]
|
||||
Backend(Box<dyn std::error::Error + Send + Sync>),
|
||||
|
||||
/// An error happened in the underlying database backend.
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[error(transparent)]
|
||||
Backend(Box<dyn std::error::Error>),
|
||||
/// An error happened while serializing or deserializing some data.
|
||||
#[error(transparent)]
|
||||
Json(#[from] serde_json::Error),
|
||||
@@ -140,7 +134,7 @@ impl StoreError {
|
||||
#[inline]
|
||||
pub fn backend<E>(error: E) -> Self
|
||||
where
|
||||
E: std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static,
|
||||
E: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
Self::Backend(Box::new(error))
|
||||
}
|
||||
|
||||
@@ -206,8 +206,7 @@ pub enum QueueWedgeError {
|
||||
},
|
||||
}
|
||||
|
||||
/// The specific user intent that characterizes a
|
||||
/// [`DependentQueuedRequestKind`].
|
||||
/// The specific user intent that characterizes a [`DependentQueuedRequest`].
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum DependentQueuedRequestKind {
|
||||
/// The event should be edited.
|
||||
|
||||
@@ -24,7 +24,12 @@ use async_trait::async_trait;
|
||||
use growable_bloom_filter::GrowableBloom;
|
||||
use matrix_sdk_common::AsyncTraitDeps;
|
||||
use ruma::{
|
||||
api::MatrixVersion,
|
||||
api::{
|
||||
client::discovery::discover_homeserver::{
|
||||
self, HomeserverInfo, IdentityServerInfo, RtcFocusInfo, TileServerInfo,
|
||||
},
|
||||
SupportedVersions,
|
||||
},
|
||||
events::{
|
||||
presence::PresenceEvent,
|
||||
receipt::{Receipt, ReceiptThread, ReceiptType},
|
||||
@@ -950,48 +955,84 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Server capabilities returned by the /client/versions endpoint.
|
||||
/// Useful server info such as data returned by the /client/versions and
|
||||
/// .well-known/client/matrix endpoints.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ServerCapabilities {
|
||||
pub struct ServerInfo {
|
||||
/// Versions supported by the remote server.
|
||||
///
|
||||
/// This contains [`MatrixVersion`]s converted to strings.
|
||||
pub versions: Vec<String>,
|
||||
|
||||
/// List of unstable features and their enablement status.
|
||||
pub unstable_features: BTreeMap<String, bool>,
|
||||
|
||||
/// Information about the server found in the client well-known file.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub well_known: Option<WellKnownResponse>,
|
||||
|
||||
/// Last time we fetched this data from the server, in milliseconds since
|
||||
/// epoch.
|
||||
last_fetch_ts: f64,
|
||||
}
|
||||
|
||||
impl ServerCapabilities {
|
||||
impl ServerInfo {
|
||||
/// The number of milliseconds after which the data is considered stale.
|
||||
pub const STALE_THRESHOLD: f64 = (1000 * 60 * 60 * 24 * 7) as _; // seven days
|
||||
|
||||
/// Encode server capabilities into this serializable struct.
|
||||
pub fn new(versions: &[MatrixVersion], unstable_features: BTreeMap<String, bool>) -> Self {
|
||||
Self {
|
||||
versions: versions.iter().map(|item| item.to_string()).collect(),
|
||||
unstable_features,
|
||||
last_fetch_ts: now_timestamp_ms(),
|
||||
}
|
||||
/// Encode server info into this serializable struct.
|
||||
pub fn new(
|
||||
versions: Vec<String>,
|
||||
unstable_features: BTreeMap<String, bool>,
|
||||
well_known: Option<WellKnownResponse>,
|
||||
) -> Self {
|
||||
Self { versions, unstable_features, well_known, last_fetch_ts: now_timestamp_ms() }
|
||||
}
|
||||
|
||||
/// Decode server capabilities from this serializable struct.
|
||||
/// Decode server info from this serializable struct.
|
||||
///
|
||||
/// May return `None` if the data is considered stale, after
|
||||
/// [`Self::STALE_THRESHOLD`] milliseconds since the last time we stored
|
||||
/// it.
|
||||
pub fn maybe_decode(&self) -> Option<(Vec<MatrixVersion>, BTreeMap<String, bool>)> {
|
||||
pub fn maybe_decode(&self) -> Option<Self> {
|
||||
if now_timestamp_ms() - self.last_fetch_ts >= Self::STALE_THRESHOLD {
|
||||
None
|
||||
} else {
|
||||
Some((
|
||||
self.versions.iter().filter_map(|item| item.parse().ok()).collect(),
|
||||
self.unstable_features.clone(),
|
||||
))
|
||||
Some(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts known Matrix versions and features from the un-typed lists of
|
||||
/// strings.
|
||||
///
|
||||
/// Note: Matrix versions that Ruma cannot parse, or does not know about,
|
||||
/// are discarded.
|
||||
pub fn supported_versions(&self) -> SupportedVersions {
|
||||
SupportedVersions::from_parts(&self.versions, &self.unstable_features)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
/// A serialisable representation of discover_homeserver::Response.
|
||||
pub struct WellKnownResponse {
|
||||
/// Information about the homeserver to connect to.
|
||||
pub homeserver: HomeserverInfo,
|
||||
|
||||
/// Information about the identity server to connect to.
|
||||
pub identity_server: Option<IdentityServerInfo>,
|
||||
|
||||
/// Information about the tile server to use to display location data.
|
||||
pub tile_server: Option<TileServerInfo>,
|
||||
|
||||
/// A list of the available MatrixRTC foci, ordered by priority.
|
||||
pub rtc_foci: Vec<RtcFocusInfo>,
|
||||
}
|
||||
|
||||
impl From<discover_homeserver::Response> for WellKnownResponse {
|
||||
fn from(response: discover_homeserver::Response) -> Self {
|
||||
Self {
|
||||
homeserver: response.homeserver,
|
||||
identity_server: response.identity_server,
|
||||
tile_server: response.tile_server,
|
||||
rtc_foci: response.rtc_foci,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1011,8 +1052,8 @@ pub enum StateStoreDataValue {
|
||||
/// The sync token.
|
||||
SyncToken(String),
|
||||
|
||||
/// The server capabilities.
|
||||
ServerCapabilities(ServerCapabilities),
|
||||
/// The server info (versions, well-known etc).
|
||||
ServerInfo(ServerInfo),
|
||||
|
||||
/// A filter with the given ID.
|
||||
Filter(String),
|
||||
@@ -1097,9 +1138,9 @@ impl StateStoreDataValue {
|
||||
as_variant!(self, Self::ComposerDraft)
|
||||
}
|
||||
|
||||
/// Get this value if it is the server capabilities metadata.
|
||||
pub fn into_server_capabilities(self) -> Option<ServerCapabilities> {
|
||||
as_variant!(self, Self::ServerCapabilities)
|
||||
/// Get this value if it is the server info metadata.
|
||||
pub fn into_server_info(self) -> Option<ServerInfo> {
|
||||
as_variant!(self, Self::ServerInfo)
|
||||
}
|
||||
|
||||
/// Get this value if it is the data for the ignored join requests.
|
||||
@@ -1114,8 +1155,8 @@ pub enum StateStoreDataKey<'a> {
|
||||
/// The sync token.
|
||||
SyncToken,
|
||||
|
||||
/// The server capabilities,
|
||||
ServerCapabilities,
|
||||
/// The server info,
|
||||
ServerInfo,
|
||||
|
||||
/// A filter with the given name.
|
||||
Filter(&'a str),
|
||||
@@ -1134,7 +1175,7 @@ pub enum StateStoreDataKey<'a> {
|
||||
/// To learn more, see [`ComposerDraft`].
|
||||
///
|
||||
/// [`ComposerDraft`]: Self::ComposerDraft
|
||||
ComposerDraft(&'a RoomId),
|
||||
ComposerDraft(&'a RoomId, Option<&'a EventId>),
|
||||
|
||||
/// A list of knock request ids marked as seen in a room.
|
||||
SeenKnockRequests(&'a RoomId),
|
||||
@@ -1143,9 +1184,9 @@ pub enum StateStoreDataKey<'a> {
|
||||
impl StateStoreDataKey<'_> {
|
||||
/// Key to use for the [`SyncToken`][Self::SyncToken] variant.
|
||||
pub const SYNC_TOKEN: &'static str = "sync_token";
|
||||
/// Key to use for the [`ServerCapabilities`][Self::ServerCapabilities]
|
||||
/// Key to use for the [`ServerInfo`][Self::ServerInfo]
|
||||
/// variant.
|
||||
pub const SERVER_CAPABILITIES: &'static str = "server_capabilities";
|
||||
pub const SERVER_INFO: &'static str = "server_capabilities"; // Note: this is the old name, kept for backwards compatibility.
|
||||
/// Key prefix to use for the [`Filter`][Self::Filter] variant.
|
||||
pub const FILTER: &'static str = "filter";
|
||||
/// Key prefix to use for the [`UserAvatarUrl`][Self::UserAvatarUrl]
|
||||
@@ -1171,21 +1212,22 @@ impl StateStoreDataKey<'_> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{now_timestamp_ms, ServerCapabilities};
|
||||
use super::{now_timestamp_ms, ServerInfo};
|
||||
|
||||
#[test]
|
||||
fn test_stale_server_capabilities() {
|
||||
let mut caps = ServerCapabilities {
|
||||
fn test_stale_server_info() {
|
||||
let mut server_info = ServerInfo {
|
||||
versions: Default::default(),
|
||||
unstable_features: Default::default(),
|
||||
last_fetch_ts: now_timestamp_ms() - ServerCapabilities::STALE_THRESHOLD - 1.0,
|
||||
well_known: Default::default(),
|
||||
last_fetch_ts: now_timestamp_ms() - ServerInfo::STALE_THRESHOLD - 1.0,
|
||||
};
|
||||
|
||||
// Definitely stale.
|
||||
assert!(caps.maybe_decode().is_none());
|
||||
assert!(server_info.maybe_decode().is_none());
|
||||
|
||||
// Definitely not stale.
|
||||
caps.last_fetch_ts = now_timestamp_ms() - 1.0;
|
||||
assert!(caps.maybe_decode().is_some());
|
||||
server_info.last_fetch_ts = now_timestamp_ms() - 1.0;
|
||||
assert!(server_info.maybe_decode().is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
|
||||
use std::{collections::BTreeMap, fmt};
|
||||
|
||||
use matrix_sdk_common::{debug::DebugRawEvent, deserialized_responses::TimelineEvent};
|
||||
use matrix_sdk_common::{
|
||||
debug::DebugRawEvent,
|
||||
deserialized_responses::{ProcessedToDeviceEvent, TimelineEvent},
|
||||
};
|
||||
pub use ruma::api::client::sync::sync_events::v3::{
|
||||
InvitedRoom as InvitedRoomUpdate, KnockedRoom as KnockedRoomUpdate,
|
||||
};
|
||||
@@ -24,7 +27,7 @@ use ruma::{
|
||||
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
events::{
|
||||
presence::PresenceEvent, AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent,
|
||||
AnySyncEphemeralRoomEvent, AnySyncStateEvent, AnyToDeviceEvent,
|
||||
AnySyncEphemeralRoomEvent, AnySyncStateEvent,
|
||||
},
|
||||
push::Action,
|
||||
serde::Raw,
|
||||
@@ -33,7 +36,10 @@ use ruma::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
debug::{DebugInvitedRoom, DebugKnockedRoom, DebugListOfRawEvents, DebugListOfRawEventsNoId},
|
||||
debug::{
|
||||
DebugInvitedRoom, DebugKnockedRoom, DebugListOfProcessedToDeviceEvents,
|
||||
DebugListOfRawEvents, DebugListOfRawEventsNoId,
|
||||
},
|
||||
deserialized_responses::{AmbiguityChange, RawAnySyncOrStrippedTimelineEvent},
|
||||
};
|
||||
|
||||
@@ -50,7 +56,7 @@ pub struct SyncResponse {
|
||||
/// The global private data created by this user.
|
||||
pub account_data: Vec<Raw<AnyGlobalAccountDataEvent>>,
|
||||
/// Messages sent directly between devices.
|
||||
pub to_device: Vec<Raw<AnyToDeviceEvent>>,
|
||||
pub to_device: Vec<ProcessedToDeviceEvent>,
|
||||
/// New notifications per room.
|
||||
pub notifications: BTreeMap<OwnedRoomId, Vec<Notification>>,
|
||||
}
|
||||
@@ -61,7 +67,7 @@ impl fmt::Debug for SyncResponse {
|
||||
f.debug_struct("SyncResponse")
|
||||
.field("rooms", &self.rooms)
|
||||
.field("account_data", &DebugListOfRawEventsNoId(&self.account_data))
|
||||
.field("to_device", &DebugListOfRawEventsNoId(&self.to_device))
|
||||
.field("to_device", &DebugListOfProcessedToDeviceEvents(&self.to_device))
|
||||
.field("notifications", &self.notifications)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
use ruma::{owned_user_id, UserId};
|
||||
|
||||
use crate::{
|
||||
client::ThreadingSupport,
|
||||
store::{RoomLoadSettings, StoreConfig},
|
||||
BaseClient, SessionMeta,
|
||||
};
|
||||
@@ -26,8 +27,10 @@ use crate::{
|
||||
/// Create a [`BaseClient`] with the given user id, if provided, or an hardcoded
|
||||
/// one otherwise.
|
||||
pub(crate) async fn logged_in_base_client(user_id: Option<&UserId>) -> BaseClient {
|
||||
let client =
|
||||
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
|
||||
let client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
let user_id =
|
||||
user_id.map(|user_id| user_id.to_owned()).unwrap_or_else(|| owned_user_id!("@u:e.uk"));
|
||||
client
|
||||
|
||||
@@ -6,6 +6,14 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
## [0.13.0] - 2025-07-10
|
||||
|
||||
### Features
|
||||
|
||||
- Expose the `ROOM_VERSION_FALLBACK` that should be used when the version of a
|
||||
room is unknown.
|
||||
([#5306](https://github.com/matrix-org/matrix-rust-sdk/pull/5306))
|
||||
|
||||
## [0.12.0] - 2025-06-10
|
||||
|
||||
No notable changes in this release.
|
||||
@@ -53,5 +61,3 @@ No notable changes in this release.
|
||||
### Refactor
|
||||
|
||||
- Move `linked_chunk` from `matrix-sdk` to `matrix-sdk-common`.
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ name = "matrix-sdk-common"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version.workspace = true
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
default-target = "x86_64-unknown-linux-gnu"
|
||||
|
||||
@@ -15,5 +15,5 @@ fn main() {
|
||||
",
|
||||
);
|
||||
process::exit(1);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use std::{collections::BTreeMap, fmt, sync::Arc};
|
||||
#[cfg(doc)]
|
||||
use ruma::events::AnyTimelineEvent;
|
||||
use ruma::{
|
||||
events::{AnyMessageLikeEvent, AnySyncTimelineEvent},
|
||||
events::{AnyMessageLikeEvent, AnySyncTimelineEvent, AnyToDeviceEvent, MessageLikeEventType},
|
||||
push::Action,
|
||||
serde::{
|
||||
AsRefStr, AsStrAsRefStr, DebugAsRefStr, DeserializeFromCowStr, FromString, JsonObject, Raw,
|
||||
@@ -42,6 +42,9 @@ const VERIFICATION_VIOLATION: &str =
|
||||
"Encrypted by a previously-verified user who is no longer verified.";
|
||||
const UNSIGNED_DEVICE: &str = "Encrypted by a device not verified by its owner.";
|
||||
const UNKNOWN_DEVICE: &str = "Encrypted by an unknown or deleted device.";
|
||||
const MISMATCHED_SENDER: &str = "\
|
||||
The sender of the event does not match the owner of the device \
|
||||
that created the Megolm session.";
|
||||
pub const SENT_IN_CLEAR: &str = "Not encrypted.";
|
||||
|
||||
/// Represents the state of verification for a decrypted message sent by a
|
||||
@@ -117,6 +120,10 @@ impl VerificationState {
|
||||
message: AUTHENTICITY_NOT_GUARANTEED,
|
||||
},
|
||||
},
|
||||
VerificationLevel::MismatchedSender => ShieldState::Red {
|
||||
code: ShieldStateCode::MismatchedSender,
|
||||
message: MISMATCHED_SENDER,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -171,6 +178,10 @@ impl VerificationState {
|
||||
}
|
||||
}
|
||||
},
|
||||
VerificationLevel::MismatchedSender => ShieldState::Red {
|
||||
code: ShieldStateCode::MismatchedSender,
|
||||
message: MISMATCHED_SENDER,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -198,6 +209,10 @@ pub enum VerificationLevel {
|
||||
/// deleted) or because the key to decrypt the message was obtained from
|
||||
/// an insecure source.
|
||||
None(DeviceLinkProblem),
|
||||
|
||||
/// The `sender` field on the event does not match the owner of the device
|
||||
/// that established the Megolm session.
|
||||
MismatchedSender,
|
||||
}
|
||||
|
||||
impl fmt::Display for VerificationLevel {
|
||||
@@ -211,6 +226,7 @@ impl fmt::Display for VerificationLevel {
|
||||
"The sending device was not signed by the user's identity"
|
||||
}
|
||||
VerificationLevel::None(..) => "The sending device is not known",
|
||||
VerificationLevel::MismatchedSender => MISMATCHED_SENDER,
|
||||
};
|
||||
write!(f, "{display}")
|
||||
}
|
||||
@@ -271,6 +287,9 @@ pub enum ShieldStateCode {
|
||||
/// The sender was previously verified but changed their identity.
|
||||
#[serde(alias = "PreviouslyVerified")]
|
||||
VerificationViolation,
|
||||
/// The `sender` field on the event does not match the owner of the device
|
||||
/// that established the Megolm session.
|
||||
MismatchedSender,
|
||||
}
|
||||
|
||||
/// The algorithm specific information of a decrypted event.
|
||||
@@ -387,7 +406,7 @@ pub struct ThreadSummary {
|
||||
/// This doesn't include the thread root event itself. It can be zero if no
|
||||
/// events in the thread are considered to be meaningful (or they've all
|
||||
/// been redacted).
|
||||
pub num_replies: usize,
|
||||
pub num_replies: u32,
|
||||
}
|
||||
|
||||
/// The status of a thread summary.
|
||||
@@ -567,16 +586,18 @@ impl TimelineEvent {
|
||||
}
|
||||
}
|
||||
|
||||
let deserialized = match latest_event.deserialize() {
|
||||
Ok(ev) => ev,
|
||||
Err(err) => {
|
||||
warn!("couldn't deserialize bundled latest thread event: {err}");
|
||||
return None;
|
||||
match latest_event.get_field::<MessageLikeEventType>("type") {
|
||||
Ok(None) => {
|
||||
let event_id = latest_event.get_field::<OwnedEventId>("event_id").ok().flatten();
|
||||
warn!(
|
||||
?event_id,
|
||||
"couldn't deserialize bundled latest thread event: missing `type` field \
|
||||
in bundled latest thread event"
|
||||
);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
match deserialized {
|
||||
AnyMessageLikeEvent::RoomEncrypted(_) => {
|
||||
Ok(Some(MessageLikeEventType::RoomEncrypted)) => {
|
||||
// The bundled latest thread event is encrypted, but we didn't have any
|
||||
// information about it in the unsigned map. Provide some dummy
|
||||
// UTD info, since we can't really do much better.
|
||||
@@ -589,7 +610,13 @@ impl TimelineEvent {
|
||||
)))
|
||||
}
|
||||
|
||||
_ => Some(Box::new(TimelineEvent::from_plaintext(latest_event.cast()))),
|
||||
Ok(_) => Some(Box::new(TimelineEvent::from_plaintext(latest_event.cast()))),
|
||||
|
||||
Err(err) => {
|
||||
let event_id = latest_event.get_field::<OwnedEventId>("event_id").ok().flatten();
|
||||
warn!(?event_id, "couldn't deserialize bundled latest thread event's type: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1147,6 +1174,53 @@ impl From<SyncTimelineEventDeserializationHelperV0> for TimelineEvent {
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a to-device event after it has been processed by the Olm machine.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ProcessedToDeviceEvent {
|
||||
/// A successfully-decrypted encrypted event.
|
||||
/// Contains the raw decrypted event and encryption info
|
||||
Decrypted {
|
||||
/// The raw decrypted event
|
||||
raw: Raw<AnyToDeviceEvent>,
|
||||
/// The Olm encryption info
|
||||
encryption_info: EncryptionInfo,
|
||||
},
|
||||
|
||||
/// An encrypted event which could not be decrypted.
|
||||
UnableToDecrypt(Raw<AnyToDeviceEvent>),
|
||||
|
||||
/// An unencrypted event.
|
||||
PlainText(Raw<AnyToDeviceEvent>),
|
||||
|
||||
/// An invalid to device event that was ignored because it is missing some
|
||||
/// required information to be processed (like no event `type` for
|
||||
/// example)
|
||||
Invalid(Raw<AnyToDeviceEvent>),
|
||||
}
|
||||
|
||||
impl ProcessedToDeviceEvent {
|
||||
/// Converts a ProcessedToDeviceEvent to the `Raw<AnyToDeviceEvent>` it
|
||||
/// encapsulates
|
||||
pub fn to_raw(&self) -> Raw<AnyToDeviceEvent> {
|
||||
match self {
|
||||
ProcessedToDeviceEvent::Decrypted { raw, .. } => raw.clone(),
|
||||
ProcessedToDeviceEvent::UnableToDecrypt(event) => event.clone(),
|
||||
ProcessedToDeviceEvent::PlainText(event) => event.clone(),
|
||||
ProcessedToDeviceEvent::Invalid(event) => event.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the raw to-device event.
|
||||
pub fn as_raw(&self) -> &Raw<AnyToDeviceEvent> {
|
||||
match self {
|
||||
ProcessedToDeviceEvent::Decrypted { raw, .. } => raw,
|
||||
ProcessedToDeviceEvent::UnableToDecrypt(event) => event,
|
||||
ProcessedToDeviceEvent::PlainText(event) => event,
|
||||
ProcessedToDeviceEvent::Invalid(event) => event,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
@@ -1467,6 +1541,7 @@ mod tests {
|
||||
"origin_server_ts": 42,
|
||||
"content": {
|
||||
"body": "Hello to you too!",
|
||||
"msgtype": "m.text",
|
||||
}
|
||||
},
|
||||
"count": 2,
|
||||
@@ -1486,6 +1561,8 @@ mod tests {
|
||||
assert_eq!(latest_reply.as_deref(), Some(event_id!("$latest_event:example.com")));
|
||||
});
|
||||
|
||||
assert!(timeline_event.bundled_latest_thread_event.is_some());
|
||||
|
||||
// When deserializing an old serialized timeline event, the thread summary is
|
||||
// also extracted, if it wasn't serialized.
|
||||
let serialized_timeline_item = json!({
|
||||
@@ -1499,6 +1576,10 @@ mod tests {
|
||||
let timeline_event: TimelineEvent =
|
||||
serde_json::from_value(serialized_timeline_item).unwrap();
|
||||
assert_matches!(timeline_event.thread_summary, ThreadSummaryStatus::Unknown);
|
||||
|
||||
// The bundled latest thread event is not persisted, so it should be `None` when
|
||||
// deserialized from a previously serialized `TimelineEvent`.
|
||||
assert!(timeline_event.bundled_latest_thread_event.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -14,11 +14,17 @@
|
||||
|
||||
//! Abstraction over an executor so we can spawn tasks under Wasm the same way
|
||||
//! we do usually.
|
||||
|
||||
//!
|
||||
//! On non Wasm platforms, this re-exports parts of tokio directly. For Wasm,
|
||||
//! we provide a single-threaded solution that matches the interface that tokio
|
||||
//! provides as a drop in replacement.
|
||||
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
mod sys {
|
||||
pub use tokio::{
|
||||
@@ -141,6 +147,41 @@ mod sys {
|
||||
|
||||
pub use sys::*;
|
||||
|
||||
/// A type ensuring a task is aborted on drop.
|
||||
#[derive(Debug)]
|
||||
pub struct AbortOnDrop<T>(JoinHandle<T>);
|
||||
|
||||
impl<T> AbortOnDrop<T> {
|
||||
pub fn new(join_handle: JoinHandle<T>) -> Self {
|
||||
Self(join_handle)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for AbortOnDrop<T> {
|
||||
fn drop(&mut self) {
|
||||
self.0.abort();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> Future for AbortOnDrop<T> {
|
||||
type Output = Result<T, JoinError>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
Pin::new(&mut self.0).poll(context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait to create an [`AbortOnDrop`] from a [`JoinHandle`].
|
||||
pub trait JoinHandleExt<T> {
|
||||
fn abort_on_drop(self) -> AbortOnDrop<T>;
|
||||
}
|
||||
|
||||
impl<T> JoinHandleExt<T> for JoinHandle<T> {
|
||||
fn abort_on_drop(self) -> AbortOnDrop<T> {
|
||||
AbortOnDrop::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
@@ -190,7 +190,7 @@ fn write_message_to_logger(level: Level, message: &JsValue, logger: &JsLogger) {
|
||||
Level::INFO => logger.info(message),
|
||||
Level::WARN => logger.warn(message),
|
||||
Level::ERROR => logger.error(message),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn write_message_to_console(level: Level, message: &JsValue) {
|
||||
@@ -199,7 +199,7 @@ fn write_message_to_console(level: Level, message: &JsValue) {
|
||||
Level::INFO => web_sys::console::info_1(message),
|
||||
Level::WARN => web_sys::console::warn_1(message),
|
||||
Level::ERROR => web_sys::console::error_1(message),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// An implementation of [`FormatEvent`] which formats events in a sensible way
|
||||
|
||||
@@ -42,6 +42,7 @@ pub mod ttl_cache;
|
||||
#[cfg(all(target_family = "wasm", not(tarpaulin_include)))]
|
||||
pub mod js_tracing;
|
||||
|
||||
use ruma::RoomVersionId;
|
||||
pub use store_locks::LEASE_DURATION_MS;
|
||||
|
||||
/// Alias for `Send` on non-wasm, empty trait (implemented by everything) on
|
||||
@@ -104,3 +105,6 @@ pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
|
||||
|
||||
#[cfg(feature = "uniffi")]
|
||||
uniffi::setup_scaffolding!();
|
||||
|
||||
/// The room version to use as a fallback when the version of a room is unknown.
|
||||
pub const ROOM_VERSION_FALLBACK: RoomVersionId = RoomVersionId::V11;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
iter::repeat_n,
|
||||
ops::{ControlFlow, Not},
|
||||
ops::ControlFlow,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ use super::{
|
||||
updates::{ReaderToken, Update, UpdatesInner},
|
||||
ChunkContent, ChunkIdentifier, Iter, Position,
|
||||
};
|
||||
use crate::linked_chunk::ChunkMetadata;
|
||||
|
||||
/// A type alias to represent a chunk's length. This is purely for commodity.
|
||||
type ChunkLength = usize;
|
||||
@@ -43,7 +44,7 @@ pub struct AsVector<Item, Gap> {
|
||||
token: ReaderToken,
|
||||
|
||||
/// Mapper from `Update` to `VectorDiff`.
|
||||
mapper: UpdateToVectorDiff,
|
||||
mapper: UpdateToVectorDiff<Item, Vec<VectorDiff<Item>>>,
|
||||
}
|
||||
|
||||
impl<Item, Gap> AsVector<Item, Gap> {
|
||||
@@ -83,20 +84,38 @@ impl<Item, Gap> AsVector<Item, Gap> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal type that converts [`Update`] into [`VectorDiff`].
|
||||
#[derive(Debug)]
|
||||
struct UpdateToVectorDiff {
|
||||
/// Pairs of all known chunks and their respective length. This is the only
|
||||
/// required data for this algorithm.
|
||||
chunks: VecDeque<(ChunkIdentifier, ChunkLength)>,
|
||||
/// Interface for a type accumulating updates from [`UpdateToVectorDiff::map`],
|
||||
/// and being returned as a result of this.
|
||||
pub(super) trait UpdatesAccumulator<Item>: Extend<VectorDiff<Item>> {
|
||||
/// Create a new accumulator with a rough estimation of the number of
|
||||
/// updates this accumulator is going to receive.
|
||||
fn new(num_updates_hint: usize) -> Self;
|
||||
}
|
||||
|
||||
impl UpdateToVectorDiff {
|
||||
// Simple implementation for a `Vec<VectorDiff<Item>>` collection for
|
||||
// `AsVector<Item, Gap>`.
|
||||
impl<Item> UpdatesAccumulator<Item> for Vec<VectorDiff<Item>> {
|
||||
fn new(num_updates_hint: usize) -> Vec<VectorDiff<Item>> {
|
||||
Vec::with_capacity(num_updates_hint)
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal type that converts [`Update`] into [`VectorDiff`].
|
||||
#[derive(Debug)]
|
||||
pub(super) struct UpdateToVectorDiff<Item, Acc: UpdatesAccumulator<Item>> {
|
||||
/// Pairs of all known chunks and their respective length. This is the only
|
||||
/// required data for this algorithm.
|
||||
pub chunks: VecDeque<(ChunkIdentifier, ChunkLength)>,
|
||||
|
||||
_phantom: std::marker::PhantomData<(Item, Acc)>,
|
||||
}
|
||||
|
||||
impl<Item, Acc: UpdatesAccumulator<Item>> UpdateToVectorDiff<Item, Acc> {
|
||||
/// Construct [`UpdateToVectorDiff`], based on an iterator of
|
||||
/// [`Chunk`](super::Chunk)s, used to set up its own internal state.
|
||||
///
|
||||
/// See [`Self::map`] to learn more about the algorithm.
|
||||
fn new<const CAP: usize, Item, Gap>(chunk_iterator: Iter<'_, CAP, Item, Gap>) -> Self {
|
||||
pub fn new<const CAP: usize, Gap>(chunk_iterator: Iter<'_, CAP, Item, Gap>) -> Self {
|
||||
let mut initial_chunk_lengths = VecDeque::new();
|
||||
|
||||
for chunk in chunk_iterator {
|
||||
@@ -109,7 +128,22 @@ impl UpdateToVectorDiff {
|
||||
))
|
||||
}
|
||||
|
||||
Self { chunks: initial_chunk_lengths }
|
||||
Self { chunks: initial_chunk_lengths, _phantom: std::marker::PhantomData }
|
||||
}
|
||||
|
||||
/// Construct [`UpdateToVectorDiff`], based on a linked chunk's full
|
||||
/// metadata, used to set up its own internal state.
|
||||
///
|
||||
/// The vector of [`ChunkMetadata`] must be ordered by their links in the
|
||||
/// linked chunk. If that precondition doesn't hold, then the mapping will
|
||||
/// be incorrect over time, and may cause assertions/panics.
|
||||
///
|
||||
/// See [`Self::map`] to learn more about the algorithm.
|
||||
pub fn from_metadata(metas: Vec<ChunkMetadata>) -> Self {
|
||||
let initial_chunk_lengths =
|
||||
metas.into_iter().map(|meta| (meta.identifier, meta.num_items)).collect();
|
||||
|
||||
Self { chunks: initial_chunk_lengths, _phantom: std::marker::PhantomData }
|
||||
}
|
||||
|
||||
/// Map several [`Update`] into [`VectorDiff`].
|
||||
@@ -172,13 +206,18 @@ impl UpdateToVectorDiff {
|
||||
/// [`LinkedChunk`]: super::LinkedChunk
|
||||
/// [`ChunkContent::Gap`]: super::ChunkContent::Gap
|
||||
/// [`ChunkContent::Content`]: super::ChunkContent::Content
|
||||
fn map<Item, Gap>(&mut self, updates: &[Update<Item, Gap>]) -> Vec<VectorDiff<Item>>
|
||||
pub fn map<Gap>(&mut self, updates: &[Update<Item, Gap>]) -> Acc
|
||||
where
|
||||
Item: Clone,
|
||||
{
|
||||
let mut diffs = Vec::with_capacity(updates.len());
|
||||
let mut acc = Acc::new(updates.len());
|
||||
|
||||
// A flag specifying when updates are reattaching detached items.
|
||||
// Flags specifying when updates are reattaching detached items.
|
||||
//
|
||||
// TL;DR: This is an optimization to avoid that insertions in the middle of a
|
||||
// chunk cause a large series of `VectorDiff::Remove` and
|
||||
// `VectorDiff::Insert` updates for the elements placed after the
|
||||
// inserted item.
|
||||
//
|
||||
// Why is it useful?
|
||||
//
|
||||
@@ -329,7 +368,7 @@ impl UpdateToVectorDiff {
|
||||
.expect("Removing an index out of the bounds");
|
||||
|
||||
// Removing at the same index because each `Remove` shifts items to the left.
|
||||
diffs.extend(repeat_n(VectorDiff::Remove { index: offset }, number_of_items));
|
||||
acc.extend(repeat_n(VectorDiff::Remove { index: offset }, number_of_items));
|
||||
}
|
||||
|
||||
Update::PushItems { at: position, items } => {
|
||||
@@ -348,12 +387,12 @@ impl UpdateToVectorDiff {
|
||||
}
|
||||
|
||||
// Optimisation: we can emit a `VectorDiff::Append` in this particular case.
|
||||
if is_pushing_back && detaching.not() {
|
||||
diffs.push(VectorDiff::Append { values: items.into() });
|
||||
if is_pushing_back && !detaching {
|
||||
acc.extend([VectorDiff::Append { values: items.into() }]);
|
||||
}
|
||||
// No optimisation: let's emit `VectorDiff::Insert`.
|
||||
else {
|
||||
diffs.extend(items.iter().enumerate().map(|(nth, item)| {
|
||||
acc.extend(items.iter().enumerate().map(|(nth, item)| {
|
||||
VectorDiff::Insert { index: offset + nth, value: item.clone() }
|
||||
}));
|
||||
}
|
||||
@@ -364,7 +403,7 @@ impl UpdateToVectorDiff {
|
||||
|
||||
// The chunk length doesn't change.
|
||||
|
||||
diffs.push(VectorDiff::Set { index: offset, value: item.clone() });
|
||||
acc.extend([VectorDiff::Set { index: offset, value: item.clone() }]);
|
||||
}
|
||||
|
||||
Update::RemoveItem { at: position } => {
|
||||
@@ -379,7 +418,7 @@ impl UpdateToVectorDiff {
|
||||
}
|
||||
|
||||
// Let's emit a `VectorDiff::Remove`.
|
||||
diffs.push(VectorDiff::Remove { index: offset });
|
||||
acc.extend([VectorDiff::Remove { index: offset }]);
|
||||
}
|
||||
|
||||
Update::DetachLastItems { at: position } => {
|
||||
@@ -422,12 +461,12 @@ impl UpdateToVectorDiff {
|
||||
self.chunks.clear();
|
||||
|
||||
// Let's straightforwardly emit a `VectorDiff::Clear`.
|
||||
diffs.push(VectorDiff::Clear);
|
||||
acc.extend([VectorDiff::Clear]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
diffs
|
||||
acc
|
||||
}
|
||||
|
||||
fn map_to_offset(&mut self, position: &Position) -> (usize, (usize, &mut usize)) {
|
||||
@@ -524,8 +563,8 @@ mod tests {
|
||||
|
||||
linked_chunk
|
||||
.insert_items_at(
|
||||
['w', 'x', 'y', 'z'],
|
||||
linked_chunk.item_position(|item| *item == 'b').unwrap(),
|
||||
['w', 'x', 'y', 'z'],
|
||||
)
|
||||
.unwrap();
|
||||
assert_items_eq!(linked_chunk, ['a', 'w', 'x'] ['y', 'z', 'b'] ['c'] ['d']);
|
||||
@@ -607,7 +646,7 @@ mod tests {
|
||||
);
|
||||
|
||||
linked_chunk
|
||||
.insert_items_at(['m'], linked_chunk.item_position(|item| *item == 'a').unwrap())
|
||||
.insert_items_at(linked_chunk.item_position(|item| *item == 'a').unwrap(), ['m'])
|
||||
.unwrap();
|
||||
assert_items_eq!(
|
||||
linked_chunk,
|
||||
@@ -670,7 +709,7 @@ mod tests {
|
||||
apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Remove { index: 5 }]);
|
||||
|
||||
linked_chunk
|
||||
.insert_items_at(['z'], linked_chunk.item_position(|item| *item == 'h').unwrap())
|
||||
.insert_items_at(linked_chunk.item_position(|item| *item == 'h').unwrap(), ['z'])
|
||||
.unwrap();
|
||||
|
||||
assert_items_eq!(
|
||||
|
||||
@@ -93,6 +93,7 @@ macro_rules! assert_items_eq {
|
||||
|
||||
mod as_vector;
|
||||
pub mod lazy_loader;
|
||||
mod order_tracker;
|
||||
pub mod relational;
|
||||
mod updates;
|
||||
|
||||
@@ -104,6 +105,7 @@ use std::{
|
||||
};
|
||||
|
||||
pub use as_vector::*;
|
||||
pub use order_tracker::OrderTracker;
|
||||
use ruma::{OwnedRoomId, RoomId};
|
||||
pub use updates::*;
|
||||
|
||||
@@ -127,12 +129,6 @@ impl LinkedChunkId<'_> {
|
||||
LinkedChunkId::Room(room_id) => OwnedLinkedChunkId::Room((*room_id).to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn room_id(&self) -> &RoomId {
|
||||
match self {
|
||||
LinkedChunkId::Room(room_id) => room_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<&OwnedLinkedChunkId> for LinkedChunkId<'_> {
|
||||
@@ -166,6 +162,7 @@ impl Display for OwnedLinkedChunkId {
|
||||
}
|
||||
|
||||
impl OwnedLinkedChunkId {
|
||||
#[cfg(test)]
|
||||
fn as_ref(&self) -> LinkedChunkId<'_> {
|
||||
match self {
|
||||
OwnedLinkedChunkId::Room(room_id) => LinkedChunkId::Room(room_id.as_ref()),
|
||||
@@ -297,7 +294,7 @@ impl<const CAP: usize, Item, Gap> Ends<CAP, Item, Gap> {
|
||||
// Fetch the previous chunk pointer.
|
||||
let previous_ptr = unsafe { chunk_ptr.as_ref() }.previous;
|
||||
|
||||
// Re-box the chunk, and let Rust does its job.
|
||||
// Re-box the chunk, and let Rust do its job.
|
||||
let _chunk_boxed = unsafe { Box::from_raw(chunk_ptr.as_ptr()) };
|
||||
|
||||
// Update the `current_chunk_ptr`.
|
||||
@@ -469,7 +466,7 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
|
||||
///
|
||||
/// Because the `position` can be invalid, this method returns a
|
||||
/// `Result`.
|
||||
pub fn insert_items_at<I>(&mut self, items: I, position: Position) -> Result<(), Error>
|
||||
pub fn insert_items_at<I>(&mut self, position: Position, items: I) -> Result<(), Error>
|
||||
where
|
||||
Item: Clone,
|
||||
Gap: Clone,
|
||||
@@ -486,7 +483,7 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
|
||||
|
||||
let chunk = match &mut chunk.content {
|
||||
ChunkContent::Gap(..) => {
|
||||
return Err(Error::ChunkIsAGap { identifier: chunk_identifier })
|
||||
return Err(Error::ChunkIsAGap { identifier: chunk_identifier });
|
||||
}
|
||||
|
||||
ChunkContent::Items(current_items) => {
|
||||
@@ -572,32 +569,24 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
|
||||
.chunk_mut(chunk_identifier)
|
||||
.ok_or(Error::InvalidChunkIdentifier { identifier: chunk_identifier })?;
|
||||
|
||||
let can_unlink_chunk = match &mut chunk.content {
|
||||
let current_items = match &mut chunk.content {
|
||||
ChunkContent::Gap(..) => {
|
||||
return Err(Error::ChunkIsAGap { identifier: chunk_identifier })
|
||||
}
|
||||
|
||||
ChunkContent::Items(current_items) => {
|
||||
let current_items_length = current_items.len();
|
||||
|
||||
if item_index > current_items_length {
|
||||
return Err(Error::InvalidItemIndex { index: item_index });
|
||||
}
|
||||
|
||||
removed_item = current_items.remove(item_index);
|
||||
|
||||
if let Some(updates) = self.updates.as_mut() {
|
||||
updates
|
||||
.push(Update::RemoveItem { at: Position(chunk_identifier, item_index) })
|
||||
}
|
||||
|
||||
current_items.is_empty()
|
||||
return Err(Error::ChunkIsAGap { identifier: chunk_identifier });
|
||||
}
|
||||
ChunkContent::Items(current_items) => current_items,
|
||||
};
|
||||
|
||||
// If removing empty chunk is desired, and if the `chunk` can be unlinked, and
|
||||
// if the `chunk` is not the first one, we can remove it.
|
||||
if can_unlink_chunk && !chunk.is_first_chunk() {
|
||||
if item_index > current_items.len() {
|
||||
return Err(Error::InvalidItemIndex { index: item_index });
|
||||
}
|
||||
|
||||
removed_item = current_items.remove(item_index);
|
||||
if let Some(updates) = self.updates.as_mut() {
|
||||
updates.push(Update::RemoveItem { at: Position(chunk_identifier, item_index) })
|
||||
}
|
||||
|
||||
// If the chunk is empty and not the first one, we can remove it.
|
||||
if current_items.is_empty() && !chunk.is_first_chunk() {
|
||||
// Unlink `chunk`.
|
||||
chunk.unlink(self.updates.as_mut());
|
||||
|
||||
@@ -616,7 +605,7 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
|
||||
if let Some(chunk_ptr) = chunk_ptr {
|
||||
// `chunk` has been unlinked.
|
||||
|
||||
// Re-box the chunk, and let Rust does its job.
|
||||
// Re-box the chunk, and let Rust do its job.
|
||||
//
|
||||
// SAFETY: `chunk` is unlinked and not borrowed anymore. `LinkedChunk` doesn't
|
||||
// use it anymore, it's a leak. It is time to re-`Box` it and drop it.
|
||||
@@ -644,7 +633,7 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
|
||||
|
||||
match &mut chunk.content {
|
||||
ChunkContent::Gap(..) => {
|
||||
return Err(Error::ChunkIsAGap { identifier: chunk_identifier })
|
||||
return Err(Error::ChunkIsAGap { identifier: chunk_identifier });
|
||||
}
|
||||
|
||||
ChunkContent::Items(current_items) => {
|
||||
@@ -864,7 +853,7 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
|
||||
|
||||
if chunk.is_items() {
|
||||
return Err(Error::ChunkIsItems { identifier: chunk_identifier });
|
||||
};
|
||||
}
|
||||
|
||||
let chunk_was_first = chunk.is_first_chunk();
|
||||
|
||||
@@ -908,7 +897,7 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
|
||||
// Stop borrowing `chunk`.
|
||||
}
|
||||
|
||||
// Re-box the chunk, and let Rust does its job.
|
||||
// Re-box the chunk, and let Rust do its job.
|
||||
//
|
||||
// SAFETY: `chunk` is unlinked and not borrowed anymore. `LinkedChunk` doesn't
|
||||
// use it anymore, it's a leak. It is time to re-`Box` it and drop it.
|
||||
@@ -1085,6 +1074,42 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
|
||||
Some(AsVector::new(updates, token, chunk_iterator))
|
||||
}
|
||||
|
||||
/// Get an [`OrderTracker`] for the linked chunk, which can be used to
|
||||
/// compare the relative position of two events in this linked chunk.
|
||||
///
|
||||
/// A pre-requisite is that the linked chunk has been constructed with
|
||||
/// [`Self::new_with_update_history`], and that if the linked chunk is
|
||||
/// lazily-loaded, an iterator over the fully-loaded linked chunk is
|
||||
/// passed at construction time here.
|
||||
pub fn order_tracker(
|
||||
&mut self,
|
||||
all_chunks: Option<Vec<ChunkMetadata>>,
|
||||
) -> Option<OrderTracker<Item, Gap>>
|
||||
where
|
||||
Item: Clone,
|
||||
{
|
||||
let (updates, token) = self
|
||||
.updates
|
||||
.as_mut()
|
||||
.map(|updates| (updates.inner.clone(), updates.new_reader_token()))?;
|
||||
|
||||
Some(OrderTracker::new(
|
||||
updates,
|
||||
token,
|
||||
all_chunks.unwrap_or_else(|| {
|
||||
// Consider the linked chunk as fully loaded.
|
||||
self.chunks()
|
||||
.map(|chunk| ChunkMetadata {
|
||||
identifier: chunk.identifier(),
|
||||
num_items: chunk.num_items(),
|
||||
previous: chunk.previous().map(|prev| prev.identifier()),
|
||||
next: chunk.next().map(|next| next.identifier()),
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
||||
/// Returns the number of items of the linked chunk.
|
||||
pub fn num_items(&self) -> usize {
|
||||
self.items().count()
|
||||
@@ -1158,7 +1183,10 @@ impl ChunkIdentifierGenerator {
|
||||
// Check for overflows.
|
||||
// unlikely — TODO: call `std::intrinsics::unlikely` once it's stable.
|
||||
if previous == u64::MAX {
|
||||
panic!("No more chunk identifiers available. Congrats, you did it. 2^64 identifiers have been consumed.")
|
||||
panic!(
|
||||
"No more chunk identifiers available. Congrats, you did it. \
|
||||
2^64 identifiers have been consumed."
|
||||
)
|
||||
}
|
||||
|
||||
ChunkIdentifier(previous + 1)
|
||||
@@ -1721,6 +1749,25 @@ pub struct RawChunk<Item, Gap> {
|
||||
pub next: Option<ChunkIdentifier>,
|
||||
}
|
||||
|
||||
/// A simplified [`RawChunk`] that only contains the number of items in a chunk,
|
||||
/// instead of its type.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ChunkMetadata {
|
||||
/// The number of items in this chunk.
|
||||
///
|
||||
/// By convention, a gap chunk contains 0 items.
|
||||
pub num_items: usize,
|
||||
|
||||
/// Link to the previous chunk, via its identifier.
|
||||
pub previous: Option<ChunkIdentifier>,
|
||||
|
||||
/// Current chunk's identifier.
|
||||
pub identifier: ChunkIdentifier,
|
||||
|
||||
/// Link to the next chunk, via its identifier.
|
||||
pub next: Option<ChunkIdentifier>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
@@ -2210,11 +2257,11 @@ mod tests {
|
||||
|
||||
// Insert inside the last chunk.
|
||||
{
|
||||
let position_of_e = linked_chunk.item_position(|item| *item == 'e').unwrap();
|
||||
let pos_e = linked_chunk.item_position(|item| *item == 'e').unwrap();
|
||||
|
||||
// Insert 4 elements, so that it overflows the chunk capacity. It's important to
|
||||
// see whether chunks are correctly updated and linked.
|
||||
linked_chunk.insert_items_at(['w', 'x', 'y', 'z'], position_of_e)?;
|
||||
linked_chunk.insert_items_at(pos_e, ['w', 'x', 'y', 'z'])?;
|
||||
|
||||
assert_items_eq!(
|
||||
linked_chunk,
|
||||
@@ -2247,8 +2294,8 @@ mod tests {
|
||||
|
||||
// Insert inside the first chunk.
|
||||
{
|
||||
let position_of_a = linked_chunk.item_position(|item| *item == 'a').unwrap();
|
||||
linked_chunk.insert_items_at(['l', 'm', 'n', 'o'], position_of_a)?;
|
||||
let pos_a = linked_chunk.item_position(|item| *item == 'a').unwrap();
|
||||
linked_chunk.insert_items_at(pos_a, ['l', 'm', 'n', 'o'])?;
|
||||
|
||||
assert_items_eq!(
|
||||
linked_chunk,
|
||||
@@ -2281,8 +2328,8 @@ mod tests {
|
||||
|
||||
// Insert inside a middle chunk.
|
||||
{
|
||||
let position_of_c = linked_chunk.item_position(|item| *item == 'c').unwrap();
|
||||
linked_chunk.insert_items_at(['r', 's'], position_of_c)?;
|
||||
let pos_c = linked_chunk.item_position(|item| *item == 'c').unwrap();
|
||||
linked_chunk.insert_items_at(pos_c, ['r', 's'])?;
|
||||
|
||||
assert_items_eq!(
|
||||
linked_chunk,
|
||||
@@ -2303,11 +2350,10 @@ mod tests {
|
||||
|
||||
// Insert at the end of a chunk.
|
||||
{
|
||||
let position_of_f = linked_chunk.item_position(|item| *item == 'f').unwrap();
|
||||
let position_after_f =
|
||||
Position(position_of_f.chunk_identifier(), position_of_f.index() + 1);
|
||||
let pos_f = linked_chunk.item_position(|item| *item == 'f').unwrap();
|
||||
let pos_f = Position(pos_f.chunk_identifier(), pos_f.index() + 1);
|
||||
|
||||
linked_chunk.insert_items_at(['p', 'q'], position_after_f)?;
|
||||
linked_chunk.insert_items_at(pos_f, ['p', 'q'])?;
|
||||
assert_items_eq!(
|
||||
linked_chunk,
|
||||
['l', 'm', 'n'] ['o', 'a', 'b'] ['r', 's', 'c'] ['d', 'w', 'x'] ['y', 'z', 'e'] ['f', 'p', 'q']
|
||||
@@ -2322,7 +2368,7 @@ mod tests {
|
||||
// Insert in a chunk that does not exist.
|
||||
{
|
||||
assert_matches!(
|
||||
linked_chunk.insert_items_at(['u', 'v'], Position(ChunkIdentifier(128), 0)),
|
||||
linked_chunk.insert_items_at(Position(ChunkIdentifier(128), 0), ['u', 'v'],),
|
||||
Err(Error::InvalidChunkIdentifier { identifier: ChunkIdentifier(128) })
|
||||
);
|
||||
assert!(linked_chunk.updates().unwrap().take().is_empty());
|
||||
@@ -2331,7 +2377,7 @@ mod tests {
|
||||
// Insert in a chunk that exists, but at an item that does not exist.
|
||||
{
|
||||
assert_matches!(
|
||||
linked_chunk.insert_items_at(['u', 'v'], Position(ChunkIdentifier(0), 128)),
|
||||
linked_chunk.insert_items_at(Position(ChunkIdentifier(0), 128), ['u', 'v'],),
|
||||
Err(Error::InvalidItemIndex { index: 128 })
|
||||
);
|
||||
assert!(linked_chunk.updates().unwrap().take().is_empty());
|
||||
@@ -2356,7 +2402,7 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_matches!(
|
||||
linked_chunk.insert_items_at(['u', 'v'], Position(ChunkIdentifier(6), 0)),
|
||||
linked_chunk.insert_items_at(Position(ChunkIdentifier(6), 0), ['u', 'v'],),
|
||||
Err(Error::ChunkIsAGap { identifier: ChunkIdentifier(6) })
|
||||
);
|
||||
}
|
||||
@@ -2391,11 +2437,11 @@ mod tests {
|
||||
);
|
||||
|
||||
// Insert inside the last chunk.
|
||||
let position_of_e = linked_chunk.item_position(|item| *item == 'e').unwrap();
|
||||
let pos_e = linked_chunk.item_position(|item| *item == 'e').unwrap();
|
||||
|
||||
// Insert 4 elements, so that it overflows the chunk capacity. It's important to
|
||||
// see whether chunks are correctly updated and linked.
|
||||
linked_chunk.insert_items_at(['w', 'x', 'y', 'z'], position_of_e)?;
|
||||
linked_chunk.insert_items_at(pos_e, ['w', 'x', 'y', 'z'])?;
|
||||
|
||||
assert_items_eq!(
|
||||
linked_chunk,
|
||||
@@ -2453,8 +2499,8 @@ mod tests {
|
||||
);
|
||||
|
||||
// Insert inside the first chunk.
|
||||
let position_of_a = linked_chunk.item_position(|item| *item == 'a').unwrap();
|
||||
linked_chunk.insert_items_at(['l', 'm', 'n', 'o'], position_of_a)?;
|
||||
let pos_a = linked_chunk.item_position(|item| *item == 'a').unwrap();
|
||||
linked_chunk.insert_items_at(pos_a, ['l', 'm', 'n', 'o'])?;
|
||||
|
||||
assert_items_eq!(
|
||||
linked_chunk,
|
||||
@@ -2517,8 +2563,8 @@ mod tests {
|
||||
]
|
||||
);
|
||||
|
||||
let position_of_d = linked_chunk.item_position(|item| *item == 'd').unwrap();
|
||||
linked_chunk.insert_items_at(['r', 's'], position_of_d)?;
|
||||
let pos_d = linked_chunk.item_position(|item| *item == 'd').unwrap();
|
||||
linked_chunk.insert_items_at(pos_d, ['r', 's'])?;
|
||||
|
||||
assert_items_eq!(
|
||||
linked_chunk,
|
||||
@@ -2570,11 +2616,10 @@ mod tests {
|
||||
);
|
||||
|
||||
// Insert at the end of a chunk.
|
||||
let position_of_e = linked_chunk.item_position(|item| *item == 'e').unwrap();
|
||||
let position_after_e =
|
||||
Position(position_of_e.chunk_identifier(), position_of_e.index() + 1);
|
||||
let pos_e = linked_chunk.item_position(|item| *item == 'e').unwrap();
|
||||
let pos_after_e = Position(pos_e.chunk_identifier(), pos_e.index() + 1);
|
||||
|
||||
linked_chunk.insert_items_at(['p', 'q'], position_after_e)?;
|
||||
linked_chunk.insert_items_at(pos_after_e, ['p', 'q'])?;
|
||||
assert_items_eq!(
|
||||
linked_chunk,
|
||||
['a', 'b', 'c'] ['d', 'e', 'p'] ['q']
|
||||
@@ -2624,7 +2669,7 @@ mod tests {
|
||||
// Insert in a chunk that does not exist.
|
||||
{
|
||||
assert_matches!(
|
||||
linked_chunk.insert_items_at(['u', 'v'], Position(ChunkIdentifier(128), 0)),
|
||||
linked_chunk.insert_items_at(Position(ChunkIdentifier(128), 0), ['u', 'v'],),
|
||||
Err(Error::InvalidChunkIdentifier { identifier: ChunkIdentifier(128) })
|
||||
);
|
||||
assert!(linked_chunk.updates().unwrap().take().is_empty());
|
||||
@@ -2633,7 +2678,7 @@ mod tests {
|
||||
// Insert in a chunk that exists, but at an item that does not exist.
|
||||
{
|
||||
assert_matches!(
|
||||
linked_chunk.insert_items_at(['u', 'v'], Position(ChunkIdentifier(0), 128)),
|
||||
linked_chunk.insert_items_at(Position(ChunkIdentifier(0), 128), ['u', 'v'],),
|
||||
Err(Error::InvalidItemIndex { index: 128 })
|
||||
);
|
||||
assert!(linked_chunk.updates().unwrap().take().is_empty());
|
||||
@@ -2642,7 +2687,7 @@ mod tests {
|
||||
// Insert in a gap.
|
||||
{
|
||||
assert_matches!(
|
||||
linked_chunk.insert_items_at(['u', 'v'], Position(ChunkIdentifier(1), 0)),
|
||||
linked_chunk.insert_items_at(Position(ChunkIdentifier(1), 0), ['u', 'v'],),
|
||||
Err(Error::ChunkIsAGap { identifier: ChunkIdentifier(1) })
|
||||
);
|
||||
}
|
||||
@@ -2984,7 +3029,7 @@ mod tests {
|
||||
// Insert in a chunk that does not exist.
|
||||
{
|
||||
assert_matches!(
|
||||
linked_chunk.insert_items_at(['u', 'v'], Position(ChunkIdentifier(128), 0)),
|
||||
linked_chunk.insert_items_at(Position(ChunkIdentifier(128), 0), ['u', 'v'],),
|
||||
Err(Error::InvalidChunkIdentifier { identifier: ChunkIdentifier(128) })
|
||||
);
|
||||
assert!(linked_chunk.updates().unwrap().take().is_empty());
|
||||
@@ -2993,7 +3038,7 @@ mod tests {
|
||||
// Insert in a chunk that exists, but at an item that does not exist.
|
||||
{
|
||||
assert_matches!(
|
||||
linked_chunk.insert_items_at(['u', 'v'], Position(ChunkIdentifier(0), 128)),
|
||||
linked_chunk.insert_items_at(Position(ChunkIdentifier(0), 128), ['u', 'v'],),
|
||||
Err(Error::InvalidItemIndex { index: 128 })
|
||||
);
|
||||
assert!(linked_chunk.updates().unwrap().take().is_empty());
|
||||
|
||||
@@ -0,0 +1,569 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use eyeball_im::VectorDiff;
|
||||
|
||||
use super::{
|
||||
updates::{ReaderToken, Update, UpdatesInner},
|
||||
Position,
|
||||
};
|
||||
use crate::linked_chunk::{ChunkMetadata, UpdateToVectorDiff};
|
||||
|
||||
/// A tracker for the order of items in a linked chunk.
|
||||
///
|
||||
/// This can be used to determine the absolute ordering of an item, and thus the
|
||||
/// relative ordering of two items in a linked chunk, in an
|
||||
/// efficient manner, thanks to [`OrderTracker::ordering`]. Internally, it
|
||||
/// keeps track of the relative ordering of the chunks themselves; given a
|
||||
/// [`Position`] in a linked chunk, the item ordering is the lexicographic
|
||||
/// ordering of the chunk in the linked chunk, and the internal position within
|
||||
/// the chunk. For the sake of ease, we return the absolute vector index of the
|
||||
/// item in the linked chunk.
|
||||
///
|
||||
/// It requires the full links' metadata to be provided at creation time, so
|
||||
/// that it can also give an order for an item that's not loaded yet, in the
|
||||
/// context of lazy-loading.
|
||||
#[derive(Debug)]
|
||||
pub struct OrderTracker<Item, Gap> {
|
||||
/// Strong reference to [`UpdatesInner`].
|
||||
updates: Arc<RwLock<UpdatesInner<Item, Gap>>>,
|
||||
|
||||
/// The token to read the updates.
|
||||
token: ReaderToken,
|
||||
|
||||
/// Mapper from `Update` to `VectorDiff`.
|
||||
mapper: UpdateToVectorDiff<Item, NullAccumulator<Item>>,
|
||||
}
|
||||
|
||||
struct NullAccumulator<Item> {
|
||||
_phantom: std::marker::PhantomData<Item>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<Item> std::fmt::Debug for NullAccumulator<Item> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("NullAccumulator")
|
||||
}
|
||||
}
|
||||
|
||||
impl<Item> super::UpdatesAccumulator<Item> for NullAccumulator<Item> {
|
||||
fn new(_num_updates_hint: usize) -> Self {
|
||||
Self { _phantom: std::marker::PhantomData }
|
||||
}
|
||||
}
|
||||
|
||||
impl<Item> Extend<VectorDiff<Item>> for NullAccumulator<Item> {
|
||||
fn extend<T: IntoIterator<Item = VectorDiff<Item>>>(&mut self, _iter: T) {
|
||||
// This is a no-op, as we don't want to accumulate anything.
|
||||
}
|
||||
}
|
||||
|
||||
impl<Item, Gap> OrderTracker<Item, Gap>
|
||||
where
|
||||
Item: Clone,
|
||||
{
|
||||
/// Create a new [`OrderTracker`].
|
||||
///
|
||||
/// The `all_chunks_metadata` parameter must include the metadata for *all*
|
||||
/// chunks (the full collection, even if the linked chunk is
|
||||
/// lazy-loaded).
|
||||
///
|
||||
/// They must be ordered by their links in the linked chunk, i.e. the first
|
||||
/// chunk in the vector is the first chunk in the linked chunk, the
|
||||
/// second in the vector is the first's next chunk, and so on. If that
|
||||
/// precondition doesn't hold, then the ordering of items will be undefined.
|
||||
pub(super) fn new(
|
||||
updates: Arc<RwLock<UpdatesInner<Item, Gap>>>,
|
||||
token: ReaderToken,
|
||||
all_chunks_metadata: Vec<ChunkMetadata>,
|
||||
) -> Self {
|
||||
// Drain previous updates so that this type is synced with `Updates`.
|
||||
{
|
||||
let mut updates = updates.write().unwrap();
|
||||
let _ = updates.take_with_token(token);
|
||||
}
|
||||
|
||||
Self { updates, token, mapper: UpdateToVectorDiff::from_metadata(all_chunks_metadata) }
|
||||
}
|
||||
|
||||
/// Force flushing of the updates manually.
|
||||
///
|
||||
/// If `inhibit` is `true` (which is useful in the case of lazy-loading
|
||||
/// related updates, which shouldn't affect the canonical, persisted
|
||||
/// linked chunk), the updates are ignored; otherwise, they are consumed
|
||||
/// normally.
|
||||
pub fn flush_updates(&mut self, inhibit: bool) {
|
||||
if inhibit {
|
||||
// Ignore the updates.
|
||||
let _ = self.updates.write().unwrap().take_with_token(self.token);
|
||||
} else {
|
||||
// Consume the updates.
|
||||
let mut updater = self.updates.write().unwrap();
|
||||
let updates = updater.take_with_token(self.token);
|
||||
let _ = self.mapper.map(updates);
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply some out-of-band updates to the ordering tracker.
|
||||
///
|
||||
/// This must only be used when the updates do not affect the observed
|
||||
/// linked chunk, but would affect the fully-loaded collection.
|
||||
pub fn map_updates(&mut self, updates: &[Update<Item, Gap>]) {
|
||||
let _ = self.mapper.map(updates);
|
||||
}
|
||||
|
||||
/// Given an event's position, returns its final ordering in the current
|
||||
/// state of the linked chunk as a vector.
|
||||
///
|
||||
/// Useful to compare the ordering of multiple events.
|
||||
///
|
||||
/// Precondition: the reader must be up to date, i.e.
|
||||
/// [`Self::flush_updates`] must have been called before this method.
|
||||
///
|
||||
/// Will return `None` if the position doesn't match a known chunk in the
|
||||
/// linked chunk, or if the chunk is a gap.
|
||||
pub fn ordering(&self, event_pos: Position) -> Option<usize> {
|
||||
// Check the precondition: there must not be any pending updates for this
|
||||
// reader.
|
||||
debug_assert!(self.updates.read().unwrap().is_reader_up_to_date(self.token));
|
||||
|
||||
// Find the chunk that contained the event.
|
||||
let mut ordering = 0;
|
||||
for (chunk_id, chunk_length) in &self.mapper.chunks {
|
||||
if *chunk_id == event_pos.chunk_identifier() {
|
||||
let offset_within_chunk = event_pos.index();
|
||||
if offset_within_chunk >= *chunk_length {
|
||||
// The event is out of bounds for this chunk, return None.
|
||||
return None;
|
||||
}
|
||||
// The final ordering is the number of items before the event, plus its own
|
||||
// index within the chunk.
|
||||
return Some(ordering + offset_within_chunk);
|
||||
}
|
||||
// This is not the target chunk yet, so add the size of the current chunk to the
|
||||
// number of seen items, and continue.
|
||||
ordering += *chunk_length;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk_test_macros::async_test;
|
||||
|
||||
use crate::linked_chunk::{
|
||||
lazy_loader::from_last_chunk, ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator,
|
||||
ChunkMetadata, LinkedChunk, OrderTracker, Position, RawChunk, Update,
|
||||
};
|
||||
|
||||
#[async_test]
|
||||
async fn test_linked_chunk_without_update_history_no_tracking() {
|
||||
let mut linked_chunk = LinkedChunk::<10, char, ()>::new();
|
||||
assert_matches!(linked_chunk.order_tracker(None), None);
|
||||
}
|
||||
|
||||
/// Given a fully-loaded linked chunk, checks that the ordering of an item
|
||||
/// is effectively the same as its index in an iteration of items.
|
||||
fn assert_order_fully_loaded(
|
||||
linked_chunk: &LinkedChunk<3, char, ()>,
|
||||
tracker: &OrderTracker<char, ()>,
|
||||
) {
|
||||
assert_order(linked_chunk, tracker, 0);
|
||||
}
|
||||
|
||||
/// Given a linked chunk with an offset representing the number of items not
|
||||
/// loaded yet, checks that the ordering of an item is effectively the
|
||||
/// same as its index+offset in an iteration of items.
|
||||
fn assert_order(
|
||||
linked_chunk: &LinkedChunk<3, char, ()>,
|
||||
tracker: &OrderTracker<char, ()>,
|
||||
offset: usize,
|
||||
) {
|
||||
for (i, (item_pos, _value)) in linked_chunk.items().enumerate() {
|
||||
assert_eq!(tracker.ordering(item_pos), Some(i + offset));
|
||||
}
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_non_lazy_updates() {
|
||||
// Assume the linked chunk is fully loaded, so we have all the chunks at
|
||||
// our disposal.
|
||||
let mut linked_chunk = LinkedChunk::<3, _, _>::new_with_update_history();
|
||||
|
||||
let mut tracker = linked_chunk.order_tracker(None).unwrap();
|
||||
|
||||
// Let's apply some updates to the live linked chunk.
|
||||
|
||||
// Pushing new items.
|
||||
{
|
||||
linked_chunk.push_items_back(['a', 'b', 'c']);
|
||||
tracker.flush_updates(false);
|
||||
assert_order_fully_loaded(&linked_chunk, &tracker);
|
||||
}
|
||||
|
||||
// Pushing a gap.
|
||||
{
|
||||
linked_chunk.push_gap_back(());
|
||||
tracker.flush_updates(false);
|
||||
assert_order_fully_loaded(&linked_chunk, &tracker);
|
||||
}
|
||||
|
||||
// Inserting items in the middle.
|
||||
{
|
||||
let pos_b = linked_chunk.item_position(|c| *c == 'b').unwrap();
|
||||
linked_chunk.insert_items_at(pos_b, ['d', 'e']).unwrap();
|
||||
tracker.flush_updates(false);
|
||||
assert_order_fully_loaded(&linked_chunk, &tracker);
|
||||
}
|
||||
|
||||
// Inserting a gap in the middle.
|
||||
{
|
||||
let c_pos = linked_chunk.item_position(|c| *c == 'c').unwrap();
|
||||
linked_chunk.insert_gap_at((), c_pos).unwrap();
|
||||
tracker.flush_updates(false);
|
||||
assert_order_fully_loaded(&linked_chunk, &tracker);
|
||||
}
|
||||
|
||||
// Replacing a gap with items.
|
||||
{
|
||||
let last_gap =
|
||||
linked_chunk.rchunks().filter(|c| c.is_gap()).last().unwrap().identifier();
|
||||
linked_chunk.replace_gap_at(['f', 'g'], last_gap).unwrap();
|
||||
tracker.flush_updates(false);
|
||||
assert_order_fully_loaded(&linked_chunk, &tracker);
|
||||
}
|
||||
|
||||
// Removing an item.
|
||||
{
|
||||
let a_pos = linked_chunk.item_position(|c| *c == 'd').unwrap();
|
||||
linked_chunk.remove_item_at(a_pos).unwrap();
|
||||
tracker.flush_updates(false);
|
||||
assert_order_fully_loaded(&linked_chunk, &tracker);
|
||||
}
|
||||
|
||||
// Replacing an item.
|
||||
{
|
||||
let b_pos = linked_chunk.item_position(|c| *c == 'e').unwrap();
|
||||
linked_chunk.replace_item_at(b_pos, 'E').unwrap();
|
||||
tracker.flush_updates(false);
|
||||
assert_order_fully_loaded(&linked_chunk, &tracker);
|
||||
}
|
||||
|
||||
// Clearing all items.
|
||||
{
|
||||
linked_chunk.clear();
|
||||
tracker.flush_updates(false);
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 0)), None);
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_lazy_loading() {
|
||||
// Assume that all the chunks haven't been loaded yet, so we have a few of them
|
||||
// in some memory, and some of them are still in an hypothetical
|
||||
// database.
|
||||
let db_metadata = vec![
|
||||
// Hypothetical non-empty items chunk with items 'a', 'b', 'c'.
|
||||
ChunkMetadata {
|
||||
previous: None,
|
||||
identifier: ChunkIdentifier(0),
|
||||
next: Some(ChunkIdentifier(1)),
|
||||
num_items: 3,
|
||||
},
|
||||
// Hypothetical gap chunk.
|
||||
ChunkMetadata {
|
||||
previous: Some(ChunkIdentifier(0)),
|
||||
identifier: ChunkIdentifier(1),
|
||||
next: Some(ChunkIdentifier(2)),
|
||||
num_items: 0,
|
||||
},
|
||||
// Hypothetical non-empty items chunk with items 'd', 'e', 'f'.
|
||||
ChunkMetadata {
|
||||
previous: Some(ChunkIdentifier(1)),
|
||||
identifier: ChunkIdentifier(2),
|
||||
next: Some(ChunkIdentifier(3)),
|
||||
num_items: 3,
|
||||
},
|
||||
// Hypothetical non-empty items chunk with items 'g'.
|
||||
ChunkMetadata {
|
||||
previous: Some(ChunkIdentifier(2)),
|
||||
identifier: ChunkIdentifier(3),
|
||||
next: None,
|
||||
num_items: 1,
|
||||
},
|
||||
];
|
||||
|
||||
// The in-memory linked chunk contains the latest chunk only.
|
||||
let mut linked_chunk = from_last_chunk::<3, _, ()>(
|
||||
Some(RawChunk {
|
||||
content: ChunkContent::Items(vec!['g']),
|
||||
previous: Some(ChunkIdentifier(2)),
|
||||
identifier: ChunkIdentifier(3),
|
||||
next: None,
|
||||
}),
|
||||
ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier(3)),
|
||||
)
|
||||
.expect("could recreate the linked chunk")
|
||||
.expect("the linked chunk isn't empty");
|
||||
|
||||
let tracker = linked_chunk.order_tracker(Some(db_metadata)).unwrap();
|
||||
|
||||
// At first, even if the main linked chunk is empty, the order tracker can
|
||||
// compute the position for unloaded items.
|
||||
|
||||
// Order of 'a':
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 0)), Some(0));
|
||||
// Order of 'b':
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
|
||||
// Order of 'c':
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 2)), Some(2));
|
||||
// An invalid position in a known chunk returns no ordering.
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 42)), None);
|
||||
|
||||
// A gap chunk doesn't have an ordering.
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(1), 0)), None);
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(1), 42)), None);
|
||||
|
||||
// Order of 'd':
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(2), 0)), Some(3));
|
||||
// Order of 'e':
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(2), 1)), Some(4));
|
||||
// Order of 'f':
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(2), 2)), Some(5));
|
||||
// No subsequent entry in the same chunk, it's been split when inserting g.
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(2), 3)), None);
|
||||
|
||||
// Order of 'g':
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(6));
|
||||
// This was the final entry so far.
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 1)), None);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_lazy_updates() {
|
||||
// Assume that all the chunks haven't been loaded yet, so we have a few of them
|
||||
// in some memory, and some of them are still in an hypothetical
|
||||
// database.
|
||||
let db_metadata = vec![
|
||||
// Hypothetical non-empty items chunk with items 'a', 'b'.
|
||||
ChunkMetadata {
|
||||
previous: None,
|
||||
identifier: ChunkIdentifier(0),
|
||||
next: Some(ChunkIdentifier(1)),
|
||||
num_items: 2,
|
||||
},
|
||||
// Hypothetical gap chunk.
|
||||
ChunkMetadata {
|
||||
previous: Some(ChunkIdentifier(0)),
|
||||
identifier: ChunkIdentifier(1),
|
||||
next: Some(ChunkIdentifier(2)),
|
||||
num_items: 0,
|
||||
},
|
||||
// Hypothetical non-empty items chunk with items 'd', 'e', 'f'.
|
||||
ChunkMetadata {
|
||||
previous: Some(ChunkIdentifier(1)),
|
||||
identifier: ChunkIdentifier(2),
|
||||
next: Some(ChunkIdentifier(3)),
|
||||
num_items: 3,
|
||||
},
|
||||
// Hypothetical non-empty items chunk with items 'g'.
|
||||
ChunkMetadata {
|
||||
previous: Some(ChunkIdentifier(2)),
|
||||
identifier: ChunkIdentifier(3),
|
||||
next: None,
|
||||
num_items: 1,
|
||||
},
|
||||
];
|
||||
|
||||
// The in-memory linked chunk contains the latest chunk only.
|
||||
let mut linked_chunk = from_last_chunk(
|
||||
Some(RawChunk {
|
||||
content: ChunkContent::Items(vec!['g']),
|
||||
previous: Some(ChunkIdentifier(2)),
|
||||
identifier: ChunkIdentifier(3),
|
||||
next: None,
|
||||
}),
|
||||
ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier(3)),
|
||||
)
|
||||
.expect("could recreate the linked chunk")
|
||||
.expect("the linked chunk isn't empty");
|
||||
|
||||
let mut tracker = linked_chunk.order_tracker(Some(db_metadata)).unwrap();
|
||||
|
||||
// Sanity checks on the initial state.
|
||||
{
|
||||
// Order of 'b':
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
|
||||
// Order of 'g':
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
|
||||
}
|
||||
|
||||
// Let's apply some updates to the live linked chunk.
|
||||
|
||||
// Pushing new items.
|
||||
{
|
||||
linked_chunk.push_items_back(['h', 'i']);
|
||||
tracker.flush_updates(false);
|
||||
|
||||
// Order of items not loaded:
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
|
||||
// The loaded items are off by 5 (the absolute order of g).
|
||||
assert_order(&linked_chunk, &tracker, 5);
|
||||
}
|
||||
|
||||
// Pushing a gap.
|
||||
let gap_id = {
|
||||
linked_chunk.push_gap_back(());
|
||||
tracker.flush_updates(false);
|
||||
|
||||
// The gap doesn't have an ordering.
|
||||
let last_chunk = linked_chunk.rchunks().next().unwrap();
|
||||
assert!(last_chunk.is_gap());
|
||||
assert_eq!(tracker.ordering(Position::new(last_chunk.identifier(), 0)), None);
|
||||
assert_eq!(tracker.ordering(Position::new(last_chunk.identifier(), 42)), None);
|
||||
|
||||
// The previous items are still ordered.
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
|
||||
// The loaded items are off by 5 (the absolute order of g).
|
||||
assert_order(&linked_chunk, &tracker, 5);
|
||||
|
||||
last_chunk.identifier()
|
||||
};
|
||||
|
||||
// Inserting items in the middle.
|
||||
{
|
||||
let pos_h = linked_chunk.item_position(|c| *c == 'h').unwrap();
|
||||
linked_chunk.insert_items_at(pos_h, ['j', 'k']).unwrap();
|
||||
tracker.flush_updates(false);
|
||||
|
||||
// The previous items are still ordered.
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
|
||||
// The loaded items are off by 5 (the absolute order of g).
|
||||
assert_order(&linked_chunk, &tracker, 5);
|
||||
}
|
||||
|
||||
// Replacing a gap with items.
|
||||
{
|
||||
linked_chunk.replace_gap_at(['l', 'm'], gap_id).unwrap();
|
||||
tracker.flush_updates(false);
|
||||
|
||||
// The previous items are still ordered.
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
|
||||
// The loaded items are off by 5 (the absolute order of g).
|
||||
assert_order(&linked_chunk, &tracker, 5);
|
||||
}
|
||||
|
||||
// Removing an item.
|
||||
{
|
||||
let j_pos = linked_chunk.item_position(|c| *c == 'j').unwrap();
|
||||
linked_chunk.remove_item_at(j_pos).unwrap();
|
||||
tracker.flush_updates(false);
|
||||
|
||||
// The previous items are still ordered.
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
|
||||
// The loaded items are off by 5 (the absolute order of g).
|
||||
assert_order(&linked_chunk, &tracker, 5);
|
||||
}
|
||||
|
||||
// Replacing an item.
|
||||
{
|
||||
let k_pos = linked_chunk.item_position(|c| *c == 'k').unwrap();
|
||||
linked_chunk.replace_item_at(k_pos, 'K').unwrap();
|
||||
tracker.flush_updates(false);
|
||||
|
||||
// The previous items are still ordered.
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
|
||||
// The loaded items are off by 5 (the absolute order of g).
|
||||
assert_order(&linked_chunk, &tracker, 5);
|
||||
}
|
||||
|
||||
// Clearing all items.
|
||||
{
|
||||
linked_chunk.clear();
|
||||
tracker.flush_updates(false);
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 0)), None);
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_out_of_band_updates() {
|
||||
// Assume that all the chunks haven't been loaded yet, so we have a few of them
|
||||
// in some memory, and some of them are still in an hypothetical
|
||||
// database.
|
||||
let db_metadata = vec![
|
||||
// Hypothetical non-empty items chunk with items 'a', 'b'.
|
||||
ChunkMetadata {
|
||||
previous: None,
|
||||
identifier: ChunkIdentifier(0),
|
||||
next: Some(ChunkIdentifier(1)),
|
||||
num_items: 2,
|
||||
},
|
||||
// Hypothetical gap chunk.
|
||||
ChunkMetadata {
|
||||
previous: Some(ChunkIdentifier(0)),
|
||||
identifier: ChunkIdentifier(1),
|
||||
next: Some(ChunkIdentifier(2)),
|
||||
num_items: 0,
|
||||
},
|
||||
// Hypothetical non-empty items chunk with items 'd', 'e', 'f'.
|
||||
ChunkMetadata {
|
||||
previous: Some(ChunkIdentifier(1)),
|
||||
identifier: ChunkIdentifier(2),
|
||||
next: Some(ChunkIdentifier(3)),
|
||||
num_items: 3,
|
||||
},
|
||||
// Hypothetical non-empty items chunk with items 'g'.
|
||||
ChunkMetadata {
|
||||
previous: Some(ChunkIdentifier(2)),
|
||||
identifier: ChunkIdentifier(3),
|
||||
next: None,
|
||||
num_items: 1,
|
||||
},
|
||||
];
|
||||
|
||||
let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
|
||||
|
||||
let mut tracker = linked_chunk.order_tracker(Some(db_metadata)).unwrap();
|
||||
|
||||
// Sanity checks.
|
||||
// Order of 'b':
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
|
||||
// Order of 'e':
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(2), 1)), Some(3));
|
||||
|
||||
// It's possible to apply updates out of band, i.e. without affecting the
|
||||
// observed linked chunk. This can be useful when an update only applies
|
||||
// to a database, but not to the in-memory linked chunk.
|
||||
tracker.map_updates(&[Update::RemoveChunk(ChunkIdentifier::new(0))]);
|
||||
|
||||
// 'b' doesn't exist anymore, so its ordering is now undefined.
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), None);
|
||||
// 'e' has been shifted back by 2 places, aka the number of items in the first
|
||||
// chunk.
|
||||
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(2), 1)), Some(1));
|
||||
}
|
||||
}
|
||||
@@ -15,14 +15,19 @@
|
||||
//! Implementation for a _relational linked chunk_, see
|
||||
//! [`RelationalLinkedChunk`].
|
||||
|
||||
use std::{collections::HashMap, hash::Hash};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
hash::Hash,
|
||||
};
|
||||
|
||||
use ruma::{OwnedEventId, OwnedRoomId};
|
||||
|
||||
use super::{ChunkContent, ChunkIdentifierGenerator, RawChunk};
|
||||
use crate::{
|
||||
deserialized_responses::TimelineEvent,
|
||||
linked_chunk::{ChunkIdentifier, LinkedChunkId, OwnedLinkedChunkId, Position, Update},
|
||||
linked_chunk::{
|
||||
ChunkIdentifier, ChunkMetadata, LinkedChunkId, OwnedLinkedChunkId, Position, Update,
|
||||
},
|
||||
};
|
||||
|
||||
/// A row of the [`RelationalLinkedChunk::chunks`].
|
||||
@@ -79,7 +84,7 @@ pub struct RelationalLinkedChunk<ItemId, Item, Gap> {
|
||||
items_chunks: Vec<ItemRow<ItemId, Gap>>,
|
||||
|
||||
/// The items' content themselves.
|
||||
items: HashMap<OwnedLinkedChunkId, HashMap<ItemId, Item>>,
|
||||
items: HashMap<OwnedLinkedChunkId, BTreeMap<ItemId, (Item, Option<Position>)>>,
|
||||
}
|
||||
|
||||
/// The [`IndexableItem`] trait is used to mark items that can be indexed into a
|
||||
@@ -103,7 +108,7 @@ impl IndexableItem for TimelineEvent {
|
||||
impl<ItemId, Item, Gap> RelationalLinkedChunk<ItemId, Item, Gap>
|
||||
where
|
||||
Item: IndexableItem<ItemId = ItemId>,
|
||||
ItemId: Hash + PartialEq + Eq + Clone,
|
||||
ItemId: Hash + PartialEq + Eq + Clone + Ord,
|
||||
{
|
||||
/// Create a new relational linked chunk.
|
||||
pub fn new() -> Self {
|
||||
@@ -127,11 +132,11 @@ where
|
||||
for update in updates {
|
||||
match update {
|
||||
Update::NewItemsChunk { previous, new, next } => {
|
||||
insert_chunk(&mut self.chunks, linked_chunk_id, previous, new, next);
|
||||
Self::insert_chunk(&mut self.chunks, linked_chunk_id, previous, new, next);
|
||||
}
|
||||
|
||||
Update::NewGapChunk { previous, new, next, gap } => {
|
||||
insert_chunk(&mut self.chunks, linked_chunk_id, previous, new, next);
|
||||
Self::insert_chunk(&mut self.chunks, linked_chunk_id, previous, new, next);
|
||||
self.items_chunks.push(ItemRow {
|
||||
linked_chunk_id: linked_chunk_id.to_owned(),
|
||||
position: Position::new(new, 0),
|
||||
@@ -140,7 +145,7 @@ where
|
||||
}
|
||||
|
||||
Update::RemoveChunk(chunk_identifier) => {
|
||||
remove_chunk(&mut self.chunks, linked_chunk_id, chunk_identifier);
|
||||
Self::remove_chunk(&mut self.chunks, linked_chunk_id, chunk_identifier);
|
||||
|
||||
let indices_to_remove = self
|
||||
.items_chunks
|
||||
@@ -173,7 +178,7 @@ where
|
||||
self.items
|
||||
.entry(linked_chunk_id.to_owned())
|
||||
.or_default()
|
||||
.insert(item_id.clone(), item);
|
||||
.insert(item_id.clone(), (item, Some(at)));
|
||||
self.items_chunks.push(ItemRow {
|
||||
linked_chunk_id: linked_chunk_id.to_owned(),
|
||||
position: at,
|
||||
@@ -197,7 +202,7 @@ where
|
||||
self.items
|
||||
.entry(linked_chunk_id.to_owned())
|
||||
.or_default()
|
||||
.insert(item_id.clone(), item);
|
||||
.insert(item_id.clone(), (item, Some(at)));
|
||||
existing.item = Either::Item(item_id);
|
||||
}
|
||||
|
||||
@@ -269,101 +274,93 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_chunk(
|
||||
chunks: &mut Vec<ChunkRow>,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
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 { linked_chunk_id: linked_chunk_id_candidate, chunk, .. }| {
|
||||
linked_chunk_id == linked_chunk_id_candidate && *chunk == previous
|
||||
},
|
||||
)
|
||||
.expect("Previous chunk should be present");
|
||||
fn insert_chunk(
|
||||
chunks: &mut Vec<ChunkRow>,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
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 { linked_chunk_id: linked_chunk_id_candidate, chunk, .. }| {
|
||||
linked_chunk_id == linked_chunk_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 { linked_chunk_id: linked_chunk_id_candidate, chunk, .. }| {
|
||||
linked_chunk_id == linked_chunk_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 {
|
||||
linked_chunk_id: linked_chunk_id.to_owned(),
|
||||
previous_chunk: previous,
|
||||
chunk: new,
|
||||
next_chunk: next,
|
||||
});
|
||||
// Link the chunk.
|
||||
entry_for_previous_chunk.next_chunk = Some(new);
|
||||
}
|
||||
|
||||
fn remove_chunk(
|
||||
chunks: &mut Vec<ChunkRow>,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
chunk_to_remove: ChunkIdentifier,
|
||||
) {
|
||||
let entry_nth_to_remove = chunks
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(
|
||||
|(nth, ChunkRow { linked_chunk_id: linked_chunk_id_candidate, chunk, .. })| {
|
||||
(linked_chunk_id == linked_chunk_id_candidate && *chunk == chunk_to_remove)
|
||||
.then_some(nth)
|
||||
},
|
||||
)
|
||||
.expect("Remove an unknown chunk");
|
||||
// Find the next chunk, and update its previous chunk.
|
||||
if let Some(next) = next {
|
||||
let entry_for_next_chunk = chunks
|
||||
.iter_mut()
|
||||
.find(|ChunkRow { linked_chunk_id: linked_chunk_id_candidate, chunk, .. }| {
|
||||
linked_chunk_id == linked_chunk_id_candidate && *chunk == next
|
||||
})
|
||||
.expect("Next chunk should be present");
|
||||
|
||||
let ChunkRow { linked_chunk_id, previous_chunk: previous, next_chunk: next, .. } =
|
||||
chunks.remove(entry_nth_to_remove);
|
||||
// Link the chunk.
|
||||
entry_for_next_chunk.previous_chunk = Some(new);
|
||||
}
|
||||
|
||||
// Find the previous chunk, and update its next chunk.
|
||||
if let Some(previous) = previous {
|
||||
let entry_for_previous_chunk = chunks
|
||||
.iter_mut()
|
||||
.find(
|
||||
|ChunkRow { linked_chunk_id: linked_chunk_id_candidate, chunk, .. }| {
|
||||
&linked_chunk_id == linked_chunk_id_candidate && *chunk == previous
|
||||
},
|
||||
)
|
||||
.expect("Previous chunk should be present");
|
||||
// Insert the chunk.
|
||||
chunks.push(ChunkRow {
|
||||
linked_chunk_id: linked_chunk_id.to_owned(),
|
||||
previous_chunk: previous,
|
||||
chunk: new,
|
||||
next_chunk: next,
|
||||
});
|
||||
}
|
||||
|
||||
// Insert the chunk.
|
||||
entry_for_previous_chunk.next_chunk = next;
|
||||
}
|
||||
fn remove_chunk(
|
||||
chunks: &mut Vec<ChunkRow>,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
chunk_to_remove: ChunkIdentifier,
|
||||
) {
|
||||
let entry_nth_to_remove = chunks
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(
|
||||
|(nth, ChunkRow { linked_chunk_id: linked_chunk_id_candidate, chunk, .. })| {
|
||||
(linked_chunk_id == linked_chunk_id_candidate && *chunk == chunk_to_remove)
|
||||
.then_some(nth)
|
||||
},
|
||||
)
|
||||
.expect("Remove an unknown chunk");
|
||||
|
||||
// Find the next chunk, and update its previous chunk.
|
||||
if let Some(next) = next {
|
||||
let entry_for_next_chunk = chunks
|
||||
.iter_mut()
|
||||
.find(
|
||||
|ChunkRow { linked_chunk_id: linked_chunk_id_candidate, chunk, .. }| {
|
||||
&linked_chunk_id == linked_chunk_id_candidate && *chunk == next
|
||||
},
|
||||
)
|
||||
.expect("Next chunk should be present");
|
||||
let ChunkRow { linked_chunk_id, previous_chunk: previous, next_chunk: next, .. } =
|
||||
chunks.remove(entry_nth_to_remove);
|
||||
|
||||
// Insert the chunk.
|
||||
entry_for_next_chunk.previous_chunk = previous;
|
||||
}
|
||||
// Find the previous chunk, and update its next chunk.
|
||||
if let Some(previous) = previous {
|
||||
let entry_for_previous_chunk = chunks
|
||||
.iter_mut()
|
||||
.find(|ChunkRow { linked_chunk_id: linked_chunk_id_candidate, chunk, .. }| {
|
||||
&linked_chunk_id == linked_chunk_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 { linked_chunk_id: linked_chunk_id_candidate, chunk, .. }| {
|
||||
&linked_chunk_id == linked_chunk_id_candidate && *chunk == next
|
||||
})
|
||||
.expect("Next chunk should be present");
|
||||
|
||||
// Insert the chunk.
|
||||
entry_for_next_chunk.previous_chunk = previous;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,37 +368,43 @@ where
|
||||
/// particular order.
|
||||
pub fn unordered_linked_chunk_items<'a>(
|
||||
&'a self,
|
||||
target: LinkedChunkId<'a>,
|
||||
) -> impl Iterator<Item = (&'a Item, Position)> {
|
||||
self.items_chunks.iter().filter_map(move |item_row| {
|
||||
if item_row.linked_chunk_id == target {
|
||||
match &item_row.item {
|
||||
Either::Item(item_id) => {
|
||||
Some((self.items.get(&target.to_owned())?.get(item_id)?, item_row.position))
|
||||
}
|
||||
Either::Gap(..) => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
target: &OwnedLinkedChunkId,
|
||||
) -> impl 'a + Iterator<Item = (&'a Item, Position)> {
|
||||
self.items.get(target).into_iter().flat_map(|items| {
|
||||
// Only keep items which have a position.
|
||||
items.values().filter_map(|(item, pos)| pos.map(|pos| (item, pos)))
|
||||
})
|
||||
}
|
||||
|
||||
/// Return an iterator over all items of all room linked chunks, without
|
||||
/// their actual positions.
|
||||
/// Return an iterator over all items of a given linked chunk, along with
|
||||
/// their positions, if available.
|
||||
///
|
||||
/// The only items which will NOT have a position are those saved with
|
||||
/// [`Self::save_item`].
|
||||
///
|
||||
/// This will include out-of-band items.
|
||||
pub fn items(&self) -> impl Iterator<Item = (&Item, LinkedChunkId<'_>)> {
|
||||
self.items.iter().flat_map(|(linked_chunk_id, items)| {
|
||||
items.values().map(|item| (item, linked_chunk_id.as_ref()))
|
||||
})
|
||||
pub fn items(
|
||||
&self,
|
||||
target: &OwnedLinkedChunkId,
|
||||
) -> impl Iterator<Item = (&Item, Option<Position>)> {
|
||||
self.items
|
||||
.get(target)
|
||||
.into_iter()
|
||||
.flat_map(|items| items.values().map(|(item, pos)| (item, *pos)))
|
||||
}
|
||||
|
||||
/// Save a single item "out-of-band" in the relational linked chunk.
|
||||
pub fn save_item(&mut self, room_id: OwnedRoomId, item: Item) {
|
||||
let id = item.id();
|
||||
let linked_chunk_id = OwnedLinkedChunkId::Room(room_id);
|
||||
self.items.entry(linked_chunk_id).or_default().insert(id, item);
|
||||
|
||||
let map = self.items.entry(linked_chunk_id).or_default();
|
||||
if let Some(prev_value) = map.get_mut(&id) {
|
||||
// If the item already exists, we keep the position.
|
||||
prev_value.0 = item;
|
||||
} else {
|
||||
map.insert(id, (item, None));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,7 +412,7 @@ impl<ItemId, Item, Gap> RelationalLinkedChunk<ItemId, Item, Gap>
|
||||
where
|
||||
Gap: Clone,
|
||||
Item: Clone,
|
||||
ItemId: Hash + PartialEq + Eq,
|
||||
ItemId: Hash + PartialEq + Eq + Ord,
|
||||
{
|
||||
/// Loads all the chunks.
|
||||
///
|
||||
@@ -427,6 +430,22 @@ where
|
||||
.collect::<Result<Vec<_>, String>>()
|
||||
}
|
||||
|
||||
/// Loads all the chunks' metadata.
|
||||
///
|
||||
/// Return an error result if the data was malformed in the struct, with a
|
||||
/// string message explaining details about the error.
|
||||
#[doc(hidden)]
|
||||
pub fn load_all_chunks_metadata(
|
||||
&self,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<Vec<ChunkMetadata>, String> {
|
||||
self.chunks
|
||||
.iter()
|
||||
.filter(|chunk| chunk.linked_chunk_id == linked_chunk_id)
|
||||
.map(|chunk_row| load_raw_chunk_metadata(self, chunk_row, linked_chunk_id))
|
||||
.collect::<Result<Vec<_>, String>>()
|
||||
}
|
||||
|
||||
pub fn load_last_chunk(
|
||||
&self,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
@@ -511,13 +530,17 @@ where
|
||||
impl<ItemId, Item, Gap> Default for RelationalLinkedChunk<ItemId, Item, Gap>
|
||||
where
|
||||
Item: IndexableItem<ItemId = ItemId>,
|
||||
ItemId: Hash + PartialEq + Eq + Clone,
|
||||
ItemId: Hash + PartialEq + Eq + Clone + Ord,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads a single chunk along all its items.
|
||||
///
|
||||
/// The code of this method must be kept in sync with that of
|
||||
/// [`load_raw_chunk_metadata`] below.
|
||||
fn load_raw_chunk<ItemId, Item, Gap>(
|
||||
relational_linked_chunk: &RelationalLinkedChunk<ItemId, Item, Gap>,
|
||||
chunk_row: &ChunkRow,
|
||||
@@ -526,7 +549,7 @@ fn load_raw_chunk<ItemId, Item, Gap>(
|
||||
where
|
||||
Item: Clone,
|
||||
Gap: Clone,
|
||||
ItemId: Hash + PartialEq + Eq,
|
||||
ItemId: Hash + PartialEq + Eq + Ord,
|
||||
{
|
||||
// Find all items that correspond to the chunk.
|
||||
let mut items = relational_linked_chunk
|
||||
@@ -577,11 +600,14 @@ where
|
||||
collected_items
|
||||
.into_iter()
|
||||
.filter_map(|(item_id, _index)| {
|
||||
relational_linked_chunk
|
||||
.items
|
||||
.get(&linked_chunk_id.to_owned())?
|
||||
.get(item_id)
|
||||
.cloned()
|
||||
Some(
|
||||
relational_linked_chunk
|
||||
.items
|
||||
.get(&linked_chunk_id.to_owned())?
|
||||
.get(item_id)?
|
||||
.0
|
||||
.clone(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
@@ -612,8 +638,93 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
/// Loads the metadata for a single chunk.
|
||||
///
|
||||
/// The code of this method must be kept in sync with that of [`load_raw_chunk`]
|
||||
/// above.
|
||||
fn load_raw_chunk_metadata<ItemId, Item, Gap>(
|
||||
relational_linked_chunk: &RelationalLinkedChunk<ItemId, Item, Gap>,
|
||||
chunk_row: &ChunkRow,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<ChunkMetadata, String>
|
||||
where
|
||||
Item: Clone,
|
||||
Gap: Clone,
|
||||
ItemId: Hash + PartialEq + Eq,
|
||||
{
|
||||
// Find all items that correspond to the chunk.
|
||||
let mut items = relational_linked_chunk
|
||||
.items_chunks
|
||||
.iter()
|
||||
.filter(|item_row| {
|
||||
item_row.linked_chunk_id == linked_chunk_id
|
||||
&& item_row.position.chunk_identifier() == chunk_row.chunk
|
||||
})
|
||||
.peekable();
|
||||
|
||||
let Some(first_item) = items.peek() else {
|
||||
// No item. It means it is a chunk of kind `Items` and that it is empty!
|
||||
return Ok(ChunkMetadata {
|
||||
num_items: 0,
|
||||
previous: chunk_row.previous_chunk,
|
||||
identifier: chunk_row.chunk,
|
||||
next: chunk_row.next_chunk,
|
||||
});
|
||||
};
|
||||
|
||||
Ok(match first_item.item {
|
||||
// This is a chunk of kind `Items`.
|
||||
Either::Item(_) => {
|
||||
// Count all the items. We add an additional filter that will exclude gaps, in
|
||||
// case the chunk is malformed, but we should not have to, in theory.
|
||||
|
||||
let mut num_items = 0;
|
||||
for item in items {
|
||||
match &item.item {
|
||||
Either::Item(_) => num_items += 1,
|
||||
Either::Gap(_) => {
|
||||
return Err(format!(
|
||||
"unexpected gap in items chunk {}",
|
||||
chunk_row.chunk.index()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChunkMetadata {
|
||||
num_items,
|
||||
previous: chunk_row.previous_chunk,
|
||||
identifier: chunk_row.chunk,
|
||||
next: chunk_row.next_chunk,
|
||||
}
|
||||
}
|
||||
|
||||
Either::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()
|
||||
));
|
||||
}
|
||||
|
||||
ChunkMetadata {
|
||||
// By convention, a gap has 0 items.
|
||||
num_items: 0,
|
||||
previous: chunk_row.previous_chunk,
|
||||
identifier: chunk_row.chunk,
|
||||
next: chunk_row.next_chunk,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use ruma::room_id;
|
||||
|
||||
@@ -1288,16 +1399,17 @@ mod tests {
|
||||
],
|
||||
);
|
||||
|
||||
let mut events =
|
||||
relational_linked_chunk.unordered_linked_chunk_items(linked_chunk_id.as_ref());
|
||||
let events = BTreeMap::from_iter(
|
||||
relational_linked_chunk.unordered_linked_chunk_items(&linked_chunk_id),
|
||||
);
|
||||
|
||||
assert_eq!(events.next().unwrap(), (&'a', Position::new(CId::new(0), 0)));
|
||||
assert_eq!(events.next().unwrap(), (&'b', Position::new(CId::new(0), 1)));
|
||||
assert_eq!(events.next().unwrap(), (&'c', Position::new(CId::new(0), 2)));
|
||||
assert_eq!(events.next().unwrap(), (&'d', Position::new(CId::new(1), 0)));
|
||||
assert_eq!(events.next().unwrap(), (&'e', Position::new(CId::new(1), 1)));
|
||||
assert_eq!(events.next().unwrap(), (&'f', Position::new(CId::new(1), 2)));
|
||||
assert!(events.next().is_none());
|
||||
assert_eq!(events.len(), 6);
|
||||
assert_eq!(*events.get(&'a').unwrap(), Position::new(CId::new(0), 0));
|
||||
assert_eq!(*events.get(&'b').unwrap(), Position::new(CId::new(0), 1));
|
||||
assert_eq!(*events.get(&'c').unwrap(), Position::new(CId::new(0), 2));
|
||||
assert_eq!(*events.get(&'d').unwrap(), Position::new(CId::new(1), 0));
|
||||
assert_eq!(*events.get(&'e').unwrap(), Position::new(CId::new(1), 1));
|
||||
assert_eq!(*events.get(&'f').unwrap(), Position::new(CId::new(1), 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -14,13 +14,10 @@
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
pin::Pin,
|
||||
sync::{Arc, RwLock, Weak},
|
||||
task::{Context, Poll, Waker},
|
||||
sync::{Arc, RwLock},
|
||||
task::Waker,
|
||||
};
|
||||
|
||||
use futures_core::Stream;
|
||||
|
||||
use super::{ChunkIdentifier, Position};
|
||||
|
||||
/// Represent the updates that have happened inside a [`LinkedChunk`].
|
||||
@@ -294,6 +291,12 @@ impl<Item, Gap> UpdatesInner<Item, Gap> {
|
||||
slice
|
||||
}
|
||||
|
||||
/// Has the given reader, identified by its [`ReaderToken`], some pending
|
||||
/// updates, or has it consumed all the pending updates?
|
||||
pub(super) fn is_reader_up_to_date(&self, token: ReaderToken) -> bool {
|
||||
*self.last_index_per_reader.get(&token).expect("unknown reader token") == self.updates.len()
|
||||
}
|
||||
|
||||
/// Return the number of updates in the buffer.
|
||||
#[cfg(test)]
|
||||
fn len(&self) -> usize {
|
||||
@@ -321,36 +324,42 @@ impl<Item, Gap> UpdatesInner<Item, Gap> {
|
||||
|
||||
/// A subscriber to [`ObservableUpdates`]. It is helpful to receive updates via
|
||||
/// a [`Stream`].
|
||||
#[cfg(test)]
|
||||
pub(super) struct UpdatesSubscriber<Item, Gap> {
|
||||
/// Weak reference to [`UpdatesInner`].
|
||||
///
|
||||
/// Using a weak reference allows [`ObservableUpdates`] to be dropped
|
||||
/// freely even if a subscriber exists.
|
||||
updates: Weak<RwLock<UpdatesInner<Item, Gap>>>,
|
||||
updates: std::sync::Weak<RwLock<UpdatesInner<Item, Gap>>>,
|
||||
|
||||
/// The token to read the updates.
|
||||
token: ReaderToken,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl<Item, Gap> UpdatesSubscriber<Item, Gap> {
|
||||
/// Create a new [`Self`].
|
||||
#[cfg(test)]
|
||||
fn new(updates: Weak<RwLock<UpdatesInner<Item, Gap>>>, token: ReaderToken) -> Self {
|
||||
fn new(updates: std::sync::Weak<RwLock<UpdatesInner<Item, Gap>>>, token: ReaderToken) -> Self {
|
||||
Self { updates, token }
|
||||
}
|
||||
}
|
||||
|
||||
impl<Item, Gap> Stream for UpdatesSubscriber<Item, Gap>
|
||||
#[cfg(test)]
|
||||
impl<Item, Gap> futures_core::Stream for UpdatesSubscriber<Item, Gap>
|
||||
where
|
||||
Item: Clone,
|
||||
Gap: Clone,
|
||||
{
|
||||
type Item = Vec<Update<Item, Gap>>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
fn poll_next(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
context: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Option<Self::Item>> {
|
||||
let Some(updates) = self.updates.upgrade() else {
|
||||
// The `ObservableUpdates` has been dropped. It's time to close this stream.
|
||||
return Poll::Ready(None);
|
||||
return std::task::Poll::Ready(None);
|
||||
};
|
||||
|
||||
let mut updates = updates.write().unwrap();
|
||||
@@ -362,14 +371,15 @@ where
|
||||
updates.wakers.push(context.waker().clone());
|
||||
|
||||
// The stream is pending.
|
||||
return Poll::Pending;
|
||||
return std::task::Poll::Pending;
|
||||
}
|
||||
|
||||
// There is updates! Let's forward them in this stream.
|
||||
Poll::Ready(Some(the_updates.to_owned()))
|
||||
std::task::Poll::Ready(Some(the_updates.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl<Item, Gap> Drop for UpdatesSubscriber<Item, Gap> {
|
||||
fn drop(&mut self) {
|
||||
// Remove `Self::token` from `UpdatesInner::last_index_per_reader`.
|
||||
@@ -394,9 +404,10 @@ mod tests {
|
||||
};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use futures_core::Stream;
|
||||
use futures_util::pin_mut;
|
||||
|
||||
use super::{super::LinkedChunk, ChunkIdentifier, Position, Stream, UpdatesInner};
|
||||
use super::{super::LinkedChunk, ChunkIdentifier, Position, UpdatesInner};
|
||||
|
||||
#[test]
|
||||
fn test_updates_take_and_garbage_collector() {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
use ruma::{
|
||||
events::{relation::BundledThread, AnyMessageLikeEvent, AnySyncTimelineEvent},
|
||||
serde::Raw,
|
||||
OwnedEventId, UInt,
|
||||
OwnedEventId,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
@@ -80,9 +80,9 @@ pub fn extract_bundled_thread_summary(
|
||||
match event.get_field::<Unsigned>("unsigned") {
|
||||
Ok(Some(Unsigned { relations: Some(Relations { thread: Some(bundled_thread) }) })) => {
|
||||
// Take the count from the bundled thread summary, if available. If it can't be
|
||||
// converted to a `u64`, we use `UInt::MAX` as a fallback, as this is unlikely
|
||||
// converted to a `u32`, we use `u32::MAX` as a fallback, as this is unlikely
|
||||
// to happen to have that many events in real-world threads.
|
||||
let count = bundled_thread.count.try_into().unwrap_or(UInt::MAX.try_into().unwrap());
|
||||
let count = bundled_thread.count.try_into().unwrap_or(u32::MAX);
|
||||
|
||||
let latest_reply =
|
||||
bundled_thread.latest_event.get_field::<OwnedEventId>("event_id").ok().flatten();
|
||||
|
||||
@@ -6,17 +6,34 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
## [0.13.0] - 2025-07-10
|
||||
|
||||
### Features
|
||||
|
||||
- [**breaking**] Add a new `VerificationLevel::MismatchedSender` to indicate that the sender of an event appears to have been tampered with.
|
||||
([#5219](https://github.com/matrix-org/matrix-rust-sdk/pull/5219))
|
||||
|
||||
### Refactor
|
||||
|
||||
- [**breaking**] The `PendingChanges`, `Changes`, `StoredRoomKeyBundleData`,
|
||||
`TrackedUser`, `IdentityChanges`, `DeviceChanges`, `DeviceUpdates`,
|
||||
`IdentityUpdates`, `BackupDecryptionKey`, `DehydratedDeviceKey`,
|
||||
`RoomKeyCounts`, `BackupKeys`, `CrossSigningKeyExport`, `UserKeyQueryResult`,
|
||||
`RoomSettings`, `RoomKeyInfo`, and `RoomKeyWithheldInfo` types have been moved
|
||||
from the `store` module into a new `store/types` module.
|
||||
([#5177](https://github.com/matrix-org/matrix-rust-sdk/pull/5177))
|
||||
|
||||
## [0.12.0] - 2025-06-10
|
||||
|
||||
### Features
|
||||
|
||||
- [**breaking**] The `ProcessedToDeviceEvent::Decrypted` variant now also have an `EncryptionInfo` field.
|
||||
Format changed from `Decrypted(Raw<AnyToDeviceEvent>)` to `Decrypted { raw: Raw<AnyToDeviceEvent>, encryption_info: EncryptionInfo) }`
|
||||
([5074](https://github.com/matrix-org/matrix-rust-sdk/pull/5074))
|
||||
([#5074](https://github.com/matrix-org/matrix-rust-sdk/pull/5074))
|
||||
|
||||
- [**breaking**] Move `session_id` from `EncryptionInfo` to `AlgorithmInfo` as it is megolm specific.
|
||||
Use `EncryptionInfo::session_id()` helper for quick access.
|
||||
([4981](https://github.com/matrix-org/matrix-rust-sdk/pull/4981))
|
||||
([#4981](https://github.com/matrix-org/matrix-rust-sdk/pull/4981))
|
||||
|
||||
- Send stable identifier `sender_device_keys` for MSC4147 (Including device
|
||||
keys with Olm-encrypted events).
|
||||
@@ -49,7 +66,7 @@ All notable changes to this project will be documented in this file.
|
||||
### Security Fixes
|
||||
- Check the sender of an event matches owner of session, preventing sender
|
||||
spoofing by homeserver owners.
|
||||
[13c1d20](https://github.com/matrix-org/matrix-rust-sdk/commit/13c1d2048286bbabf5e7bc6b015aafee98f04d55) (High, [GHSA-x958-rvg6-956w](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-x958-rvg6-956w)).
|
||||
[13c1d20](https://github.com/matrix-org/matrix-rust-sdk/commit/13c1d2048286bbabf5e7bc6b015aafee98f04d55) (High, [CVE-2025-48937](https://www.cve.org/CVERecord?id=CVE-2025-48937), [GHSA-x958-rvg6-956w](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-x958-rvg6-956w)).
|
||||
|
||||
### Bug Fixes
|
||||
- Remove a wildcard enum variant import which breaks compilation if used with
|
||||
|
||||
@@ -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.12.0"
|
||||
version = "0.13.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--cfg", "docsrs", "--generate-link-to-definition"]
|
||||
|
||||
@@ -28,7 +28,7 @@ use zeroize::{Zeroize, Zeroizing};
|
||||
use super::MegolmV1BackupKey;
|
||||
use crate::{
|
||||
olm::BackedUpRoomKey,
|
||||
store::BackupDecryptionKey,
|
||||
store::types::BackupDecryptionKey,
|
||||
types::{MegolmV1AuthData, RoomKeyBackupInfo},
|
||||
};
|
||||
|
||||
|
||||
@@ -37,7 +37,10 @@ use tracing::{debug, info, instrument, trace, warn};
|
||||
|
||||
use crate::{
|
||||
olm::{BackedUpRoomKey, ExportedRoomKey, InboundGroupSession, SignedJsonObject},
|
||||
store::{BackupDecryptionKey, BackupKeys, Changes, RoomKeyCounts, Store},
|
||||
store::{
|
||||
types::{BackupDecryptionKey, BackupKeys, Changes, RoomKeyCounts},
|
||||
Store,
|
||||
},
|
||||
types::{requests::KeysBackupRequest, MegolmV1AuthData, RoomKeyBackupInfo, Signatures},
|
||||
CryptoStoreError, Device, RoomKeyImportResult, SignatureError,
|
||||
};
|
||||
@@ -510,7 +513,7 @@ impl BackupMachine {
|
||||
?request_id,
|
||||
"Tried to mark a pending backup as sent but there isn't a backup pending"
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -644,7 +647,10 @@ mod tests {
|
||||
use super::BackupMachine;
|
||||
use crate::{
|
||||
olm::BackedUpRoomKey,
|
||||
store::{BackupDecryptionKey, Changes, CryptoStore, MemoryStore},
|
||||
store::{
|
||||
types::{BackupDecryptionKey, Changes},
|
||||
CryptoStore, MemoryStore,
|
||||
},
|
||||
types::RoomKeyBackupInfo,
|
||||
OlmError, OlmMachine,
|
||||
};
|
||||
|
||||
@@ -55,7 +55,10 @@ use tracing::{instrument, trace};
|
||||
use vodozemac::{DehydratedDeviceError, LibolmPickleError};
|
||||
|
||||
use crate::{
|
||||
store::{Changes, CryptoStoreWrapper, DehydratedDeviceKey, MemoryStore, RoomKeyInfo, Store},
|
||||
store::{
|
||||
types::{Changes, DehydratedDeviceKey, RoomKeyInfo},
|
||||
CryptoStoreWrapper, MemoryStore, Store,
|
||||
},
|
||||
verification::VerificationMachine,
|
||||
Account, CryptoStoreError, EncryptionSyncChanges, OlmError, OlmMachine, SignatureError,
|
||||
};
|
||||
@@ -113,7 +116,9 @@ impl DehydratedDevices {
|
||||
|
||||
let store =
|
||||
Store::new(account.static_data().clone(), user_identity, store, verification_machine);
|
||||
store.save_pending_changes(crate::store::PendingChanges { account: Some(account) }).await?;
|
||||
store
|
||||
.save_pending_changes(crate::store::types::PendingChanges { account: Some(account) })
|
||||
.await?;
|
||||
|
||||
Ok(DehydratedDevice { store })
|
||||
}
|
||||
@@ -210,7 +215,7 @@ impl RehydratedDevice {
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use anyhow::Result;
|
||||
/// # use matrix_sdk_crypto::{ OlmMachine, store::DehydratedDeviceKey };
|
||||
/// # use matrix_sdk_crypto::{ OlmMachine, store::types::DehydratedDeviceKey };
|
||||
/// # use ruma::{api::client::dehydrated_device, DeviceId};
|
||||
/// # async fn example() -> Result<()> {
|
||||
/// # let machine: OlmMachine = unimplemented!();
|
||||
@@ -326,7 +331,7 @@ impl DehydratedDevice {
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use matrix_sdk_crypto::OlmMachine; /// #
|
||||
/// use matrix_sdk_crypto::store::DehydratedDeviceKey;
|
||||
/// use matrix_sdk_crypto::store::types::DehydratedDeviceKey;
|
||||
///
|
||||
/// async fn example() -> anyhow::Result<()> {
|
||||
/// # let machine: OlmMachine = unimplemented!();
|
||||
@@ -415,7 +420,7 @@ mod tests {
|
||||
tests::to_device_requests_to_content,
|
||||
},
|
||||
olm::OutboundGroupSession,
|
||||
store::DehydratedDeviceKey,
|
||||
store::types::DehydratedDeviceKey,
|
||||
types::{events::ToDeviceEvent, DeviceKeys as DeviceKeysType},
|
||||
utilities::json_convert,
|
||||
EncryptionSettings, OlmMachine,
|
||||
|
||||
@@ -47,7 +47,7 @@ use crate::{
|
||||
identities::IdentityManager,
|
||||
olm::{InboundGroupSession, Session},
|
||||
session_manager::GroupSessionCache,
|
||||
store::{Changes, CryptoStoreError, SecretImportError, Store, StoreCache},
|
||||
store::{caches::StoreCache, types::Changes, CryptoStoreError, SecretImportError, Store},
|
||||
types::{
|
||||
events::{
|
||||
forwarded_room_key::ForwardedRoomKeyContent,
|
||||
@@ -1119,7 +1119,7 @@ mod tests {
|
||||
use crate::{
|
||||
gossiping::KeyForwardDecision,
|
||||
olm::OutboundGroupSession,
|
||||
store::{CryptoStore, DeviceChanges},
|
||||
store::{types::DeviceChanges, CryptoStore},
|
||||
types::requests::AnyOutgoingRequest,
|
||||
types::{
|
||||
events::{
|
||||
@@ -1134,7 +1134,10 @@ mod tests {
|
||||
identities::{DeviceData, IdentityManager, LocalTrust},
|
||||
olm::{Account, PrivateCrossSigningIdentity},
|
||||
session_manager::GroupSessionCache,
|
||||
store::{Changes, CryptoStoreWrapper, MemoryStore, PendingChanges, Store},
|
||||
store::{
|
||||
types::{Changes, PendingChanges},
|
||||
CryptoStoreWrapper, MemoryStore, Store,
|
||||
},
|
||||
types::events::room::encrypted::{
|
||||
EncryptedEvent, EncryptedToDeviceEvent, RoomEncryptedEventContent,
|
||||
},
|
||||
@@ -2018,7 +2021,7 @@ mod tests {
|
||||
alice_machine.store().save_device_data(&[bob_device.inner]).await.unwrap();
|
||||
bob_machine.store().save_device_data(&[alice_device.inner]).await.unwrap();
|
||||
|
||||
let decryption_key = crate::store::BackupDecryptionKey::new().unwrap();
|
||||
let decryption_key = crate::store::types::BackupDecryptionKey::new().unwrap();
|
||||
alice_machine
|
||||
.backup_machine()
|
||||
.save_decryption_key(Some(decryption_key), None)
|
||||
|
||||
@@ -44,7 +44,9 @@ use crate::{
|
||||
InboundGroupSession, OutboundGroupSession, Session, ShareInfo, SignedJsonObject, VerifyJson,
|
||||
},
|
||||
store::{
|
||||
caches::SequenceNumber, Changes, CryptoStoreWrapper, DeviceChanges, Result as StoreResult,
|
||||
caches::SequenceNumber,
|
||||
types::{Changes, DeviceChanges},
|
||||
CryptoStoreWrapper, Result as StoreResult,
|
||||
},
|
||||
types::{
|
||||
events::{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user