Merge branch 'main' into valere/msc_2399

This commit is contained in:
valere
2023-03-09 17:04:21 +01:00
166 changed files with 11238 additions and 9060 deletions

View File

@@ -1,5 +1,7 @@
<!-- description of the changes in this PR -->
- [ ] Public API changes documented in changelogs (optional)
<!-- Sign-off, if not part of the commits -->
<!-- See CONTRIBUTING.md if you don't know what this is -->
Signed-off-by:

View File

@@ -12,48 +12,28 @@ on:
- synchronize
- ready_for_review
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
MATRIX_SDK_CRYPTO_NODEJS_PATH: bindings/matrix-sdk-crypto-nodejs
MATRIX_SDK_CRYPTO_JS_PATH: bindings/matrix-sdk-crypto-js
jobs:
xtask-linux:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v2
- name: Install Protoc
uses: arduino/setup-protoc@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Check xtask cache
uses: actions/cache@v3
id: xtask-cache
with:
path: target/debug/xtask
key: xtask-linux-${{ hashFiles('Cargo.toml', 'xtask/**') }}
- name: Install rust stable toolchain
if: steps.xtask-cache.outputs.cache-hit != 'true'
uses: dtolnay/rust-toolchain@stable
- name: Build
if: steps.xtask-cache.outputs.cache-hit != 'true'
run: |
cargo build -p xtask
xtask:
uses: ./.github/workflows/xtask.yml
test-uniffi-codegen:
name: Test UniFFI bindings generation
needs: xtask-linux
needs: xtask
if: github.event_name == 'push' || !github.event.pull_request.draft
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v1
uses: actions/checkout@v3
- name: Install Protoc
uses: arduino/setup-protoc@v1
@@ -67,10 +47,11 @@ jobs:
uses: Swatinem/rust-cache@v2
- name: Get xtask
uses: actions/cache@v3
uses: actions/cache/restore@v3
with:
path: target/debug/xtask
key: xtask-linux-${{ hashFiles('Cargo.toml', 'xtask/**') }}
key: "${{ needs.xtask.outputs.cachekey-linux }}"
fail-on-cache-miss: true
- name: Build library & generate bindings
run: target/debug/xtask ci bindings
@@ -211,43 +192,17 @@ jobs:
working-directory: ${{ env.MATRIX_SDK_CRYPTO_JS_PATH }}
run: npm run doc
xtask-macos:
runs-on: macos-12
steps:
- name: Checkout repo
uses: actions/checkout@v2
- name: Install Protoc
uses: arduino/setup-protoc@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Check xtask cache
uses: actions/cache@v3
id: xtask-cache
with:
path: target/debug/xtask
key: xtask-macos-${{ hashFiles('Cargo.toml', 'xtask/**') }}
- name: Install rust stable toolchain
if: steps.xtask-cache.outputs.cache-hit != 'true'
uses: dtolnay/rust-toolchain@stable
- name: Build
if: steps.xtask-cache.outputs.cache-hit != 'true'
run: |
cargo build -p xtask
test-apple:
name: matrix-rust-components-swift
needs: xtask-macos
needs: xtask
runs-on: macos-12
if: github.event_name == 'push' || !github.event.pull_request.draft
steps:
- name: Checkout
uses: actions/checkout@v1
uses: actions/checkout@v3
# install protoc in case we end up rebuilding opentelemetry-proto
- name: Install Protoc
uses: arduino/setup-protoc@v1
with:
@@ -263,10 +218,11 @@ jobs:
uses: Swatinem/rust-cache@v2
- name: Get xtask
uses: actions/cache@v3
uses: actions/cache/restore@v3
with:
path: target/debug/xtask
key: xtask-macos-${{ hashFiles('Cargo.toml', 'xtask/**') }}
key: "${{ needs.xtask.outputs.cachekey-macos }}"
fail-on-cache-miss: true
- name: Build library & bindings
run: target/debug/xtask swift build-library
@@ -276,4 +232,4 @@ jobs:
run: swift test
- name: Build Framework
run: cargo xtask swift build-framework --only-target=aarch64-apple-ios
run: target/debug/xtask swift build-framework --only-target=aarch64-apple-ios

View File

@@ -1,13 +0,0 @@
on:
pull_request:
branches: [main]
jobs:
cancel-others:
runs-on: ubuntu-latest
steps:
- name: Cancel workflows for older commits
uses: styfle/cancel-workflow-action@0.11.0
with:
workflow_id: all
all_but_latest: true

View File

@@ -12,36 +12,16 @@ on:
- synchronize
- ready_for_review
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
jobs:
xtask:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v2
- name: Install Protoc
uses: arduino/setup-protoc@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Check xtask cache
uses: actions/cache@v3
id: xtask-cache
with:
path: target/debug/xtask
key: xtask-${{ hashFiles('xtask/**') }}
- name: Install rust stable toolchain
if: steps.xtask-cache.outputs.cache-hit != 'true'
uses: dtolnay/rust-toolchain@stable
- name: Build
if: steps.xtask-cache.outputs.cache-hit != 'true'
run: |
cargo build -p xtask
uses: ./.github/workflows/xtask.yml
test-matrix-sdk-features:
name: 🐧 [m], ${{ matrix.name }}
@@ -64,26 +44,35 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v1
uses: actions/checkout@v3
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Load cache
uses: Swatinem/rust-cache@v2
with:
# use a separate cache for each job to work around
# https://github.com/Swatinem/rust-cache/issues/124
key: "${{ matrix.name }}"
# ... but only save the cache on the main branch
# cf https://github.com/Swatinem/rust-cache/issues/95
save-if: ${{ github.ref == 'refs/head/main' }}
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Get xtask
uses: actions/cache@v3
uses: actions/cache/restore@v3
with:
path: target/debug/xtask
key: xtask-${{ hashFiles('xtask/**') }}
key: "${{ needs.xtask.outputs.cachekey-linux }}"
fail-on-cache-miss: true
- name: Test
run: |
cargo run -p xtask -- ci test-features ${{ matrix.name }}
target/debug/xtask ci test-features ${{ matrix.name }}
test-matrix-sdk-examples:
name: 🐧 [m]-examples
@@ -105,14 +94,15 @@ jobs:
uses: taiki-e/install-action@nextest
- name: Get xtask
uses: actions/cache@v3
uses: actions/cache/restore@v3
with:
path: target/debug/xtask
key: xtask-${{ hashFiles('xtask/**') }}
key: "${{ needs.xtask.outputs.cachekey-linux }}"
fail-on-cache-miss: true
- name: Test
run: |
cargo run -p xtask -- ci examples
target/debug/xtask ci examples
test-matrix-sdk-crypto:
name: 🐧 [m]-crypto
@@ -134,14 +124,15 @@ jobs:
uses: taiki-e/install-action@nextest
- name: Get xtask
uses: actions/cache@v3
uses: actions/cache/restore@v3
with:
path: target/debug/xtask
key: xtask-${{ hashFiles('xtask/**') }}
key: "${{ needs.xtask.outputs.cachekey-linux }}"
fail-on-cache-miss: true
- name: Test
run: |
cargo run -p xtask -- ci test-crypto
target/debug/xtask ci test-crypto
test-all-crates:
name: ${{ matrix.name }}
@@ -166,7 +157,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v1
uses: actions/checkout@v3
- name: Install Protoc
uses: arduino/setup-protoc@v1
@@ -174,15 +165,15 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# Can't use `${{ matrix.* }}` inside uses
- name: Install Rust
- name: Install Rust stable
if: matrix.rust == 'stable'
uses: dtolnay/rust-toolchain@stable
- name: Install Rust
- name: Install Rust beta
if: matrix.rust == 'beta'
uses: dtolnay/rust-toolchain@beta
- name: Install Rust
- name: Install Rust nightly
if: matrix.rust == 'nightly'
uses: dtolnay/rust-toolchain@nightly
@@ -256,23 +247,32 @@ jobs:
- name: Load cache
uses: Swatinem/rust-cache@v2
with:
# use a separate cache for each job to work around
# https://github.com/Swatinem/rust-cache/issues/124
key: "${{ matrix.cmd }}"
# ... but only save the cache on the main branch
# cf https://github.com/Swatinem/rust-cache/issues/95
save-if: ${{ github.ref == 'refs/head/main' }}
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Get xtask
uses: actions/cache@v3
uses: actions/cache/restore@v3
with:
path: target/debug/xtask
key: xtask-${{ hashFiles('xtask/**') }}
key: "${{ needs.xtask.outputs.cachekey-linux }}"
fail-on-cache-miss: true
- name: Rust Check
run: |
cargo run -p xtask -- ci wasm ${{ matrix.cmd }}
target/debug/xtask ci wasm ${{ matrix.cmd }}
- name: Wasm-Pack test
run: |
cargo run -p xtask -- ci wasm-pack ${{ matrix.cmd }}
target/debug/xtask ci wasm-pack ${{ matrix.cmd }}
test-appservice:
name: ${{ matrix.os-name }} [m]-appservice
@@ -286,13 +286,15 @@ jobs:
include:
- os: ubuntu-latest
os-name: 🐧
xtask-cachekey: "${{ needs.xtask.outputs.cachekey-linux }}"
- os: macos-latest
os-name: 🍏
xtask-cachekey: "${{ needs.xtask.outputs.cachekey-macos }}"
steps:
- name: Checkout
uses: actions/checkout@v1
uses: actions/checkout@v3
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -304,14 +306,15 @@ jobs:
uses: taiki-e/install-action@nextest
- name: Get xtask
uses: actions/cache@v3
uses: actions/cache/restore@v3
with:
path: target/debug/xtask
key: xtask-${{ hashFiles('xtask/**') }}
key: "${{ matrix.xtask-cachekey }}"
fail-on-cache-miss: true
- name: Run checks
run: |
cargo run -p xtask -- ci test-appservice
target/debug/xtask ci test-appservice
formatting:
name: Check Formatting
@@ -367,14 +370,15 @@ jobs:
uses: Swatinem/rust-cache@v2
- name: Get xtask
uses: actions/cache@v3
uses: actions/cache/restore@v3
with:
path: target/debug/xtask
key: xtask-${{ hashFiles('xtask/**') }}
key: "${{ needs.xtask.outputs.cachekey-linux }}"
fail-on-cache-miss: true
- name: Clippy
run: |
cargo run -p xtask -- ci clippy
target/debug/xtask ci clippy
integration-tests:
name: Integration test

View File

@@ -6,6 +6,10 @@ on:
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
@@ -51,6 +55,9 @@ jobs:
- name: Upload to codecov.io
uses: codecov/codecov-action@v3
with:
# Work around frequent upload errors, for runs inside the main repo (not PRs from forks).
# Otherwise not required for public repos.
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
# The upload sometimes fails due to https://github.com/codecov/codecov-action/issues/837.
# To make sure that the failure gets flagged clearly in the UI, fail the action.
fail_ci_if_error: true

View File

@@ -5,6 +5,10 @@ on:
branches: [main]
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
docs:
name: All crates

76
.github/workflows/xtask.yml vendored Normal file
View File

@@ -0,0 +1,76 @@
# A reusable github actions workflow that will build xtask, if it is not
# already cached.
#
# It will create a pair of GHA cache entries, if they do not already exist.
# The cache keys take the form `xtask-{os}-{hash}`, where "{os}" is "linux"
# or "macos", and "{hash}" is the hash of the xtask# directory.
#
# The cache keys are written to output variables named "cachekey-{os}".
#
name: Build xtask if necessary
on:
workflow_call:
outputs:
cachekey-linux:
description: "The cache key for the linux build artifact"
value: "${{ jobs.xtask.outputs.cachekey-linux }}"
cachekey-macos:
description: "The cache key for the macos build artifact"
value: "${{ jobs.xtask.outputs.cachekey-macos }}"
env:
CARGO_TERM_COLOR: always
jobs:
xtask:
name: "xtask-${{ matrix.os-name }}"
strategy:
fail-fast: true
matrix:
include:
- os: ubuntu-latest
os-name: 🐧
cachekey-id: linux
- os: macos-12
os-name: 🍏
cachekey-id: macos
runs-on: "${{ matrix.os }}"
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Calculate cache key
id: cachekey
# set a step output variable "cachekey-{os}" that can be referenced in
# the job outputs below.
run: |
echo "cachekey-${{ matrix.cachekey-id }}=xtask-${{ matrix.cachekey-id }}-${{ hashFiles('Cargo.toml', 'xtask/**') }}" >> $GITHUB_OUTPUT
- name: Check xtask cache
uses: actions/cache@v3
id: xtask-cache
with:
path: target/debug/xtask
# use the cache key calculated in the step above. Bit of an awkard
# syntax
key: |
${{ steps.cachekey.outputs[format('cachekey-{0}', matrix.cachekey-id)] }}
- name: Install rust stable toolchain
if: steps.xtask-cache.outputs.cache-hit != 'true'
uses: dtolnay/rust-toolchain@stable
- name: Build
if: steps.xtask-cache.outputs.cache-hit != 'true'
run: |
cargo build -p xtask
outputs:
"cachekey-linux": "${{ steps.cachekey.outputs.cachekey-linux }}"
"cachekey-macos": "${{ steps.cachekey.outputs.cachekey-macos }}"

View File

@@ -5,6 +5,7 @@ Fo = "Fo"
BA = "BA"
UE = "UE"
Ure = "Ure"
OFO = "OFO"
Ot = "Ot"
# This is the thead html tag, remove this once typos is updated in the github
# action. 1.3.1 seems to work correctly, while 1.11.0 on the CI seems to get

View File

@@ -1,124 +0,0 @@
# Conventional Commits
This project uses [Conventional
Commits](https://www.conventionalcommits.org/). Read the
[Summary](https://www.conventionalcommits.org/en/v1.0.0/#summary) or
the [Full
Specification](https://www.conventionalcommits.org/en/v1.0.0/#specification)
to learn more.
## Types
Conventional Commits defines _type_ (as in `type(scope):
message`). This section aims at listing the types used inside this
project:
| Type | Definition |
|-|-|
| `feat` | About a new feature. |
| `fix` | About a bug fix. |
| `test` | About a test (suite, case, runner…). |
| `docs` | About a documentation modification. |
| `refactor` | About a refactoring. |
| `ci` | About a Continuous Integration modification. |
| `chore` | About some cleanup, or regular tasks. |
## Scopes
Conventional Commits defines _scope_ (as in `type(scope): message`). This
section aims at listing all the scopes used inside this project:
<table>
<thead>
<tr>
<th>Group</th>
<th>Scope</th>
<th>Definition</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan="10">Crates</td>
<td><code>sdk</code></td>
<td>About the <code>matrix-sdk</code> crate.</td>
</tr>
<tr>
<td><code>appservice</code></td>
<td>About the <code>matrix-sdk-appservice</code> crate.</td>
</tr>
<tr>
<td><code>base</code></td>
<td>About the <code>matrix-sdk-base</code> crate.</td>
</tr>
<tr>
<td><code>common</code></td>
<td>About the <code>matrix-sdk-common</code> crate.</td>
</tr>
<tr>
<td><code>crypto</code></td>
<td>About the <code>matrix-sdk-crypto</code> crate.</td>
</tr>
<tr>
<td><code>indexeddb</code></td>
<td>About the <code>matrix-sdk-indexeddb</code> crate.</td>
</tr>
<tr>
<td><code>qrcode</code></td>
<td>About the <code>matrix-sdk-qrcode</code> crate.</td>
</tr>
<tr>
<td><code>sled</code></td>
<td>About the <code>matrix-sdk-sled</code> crate.</td>
</tr>
<tr>
<td><code>store-encryption</code></td>
<td>About the <code>matrix-sdk-store-encryption</code> crate.</td>
</tr>
<tr>
<td><code>test</code></td>
<td>About the <code>matrix-sdk-test</code> and <code>matrix-sdk-test-macros</code> crate.</td>
</tr>
<tr>
<td rowspan="5">Bindings</td>
<td><code>apple</code></td>
<td>About the <code>matrix-rust-components-swift</code> binding.</td>
</tr>
<tr>
<td><code>crypto-nodejs</code></td>
<td>About the <code>matrix-sdk-crypto-nodejs</code> binding.</td>
</tr>
<tr>
<td><code>crypto-js</code></td>
<td>About the <code>matrix-sdk-crypto-js</code> binding.</td>
</tr>
<tr>
<td><code>crypto-ffi</code></td>
<td>About the <code>matrix-sdk-crypto-ffi</code> binding.</td>
</tr>
<tr>
<td><code>ffi</code></td>
<td>About the <code>matrix-sdk-ffi</code> binding.</td>
</tr>
<tr>
<td>Labs</td>
<td><code>sled-state-inspector</code></td>
<td>About the <code>sled-state-inspector</code> project.</td>
</tr>
<tr>
<td>Continuous Integration</td>
<td><code>xtask</code></td>
<td>About the <code>xtask</code> project.</td>
</tr>
</tbody>
</table>
## Generating `CHANGELOG.md`
The [`git-cliff`](https://github.com/orhun/git-cliff) project is used
to generate `CHANGELOG.md` automatically. Hence the various
`cliff.toml` files that are present in this project, or the
`package.metadata.git-cliff` sections in various `Cargo.toml` files.
Its companion,
[`git-cliff-action`](https://github.com/orhun/git-cliff-action)
project, is used inside Github Action workflows.

494
Cargo.lock generated
View File

@@ -426,21 +426,6 @@ dependencies = [
"serde",
]
[[package]]
name = "bit-set"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -772,17 +757,6 @@ dependencies = [
"os_str_bytes",
]
[[package]]
name = "clipboard-win"
version = "4.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362"
dependencies = [
"error-code",
"str-buf",
"winapi",
]
[[package]]
name = "cmake"
version = "0.1.49"
@@ -1262,16 +1236,6 @@ dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if",
"dirs-sys-next",
]
[[package]]
name = "dirs-sys"
version = "0.3.7"
@@ -1283,23 +1247,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "discard"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
[[package]]
name = "displaydoc"
version = "0.2.3"
@@ -1357,12 +1304,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "endian-type"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
[[package]]
name = "errno"
version = "0.2.8"
@@ -1384,16 +1325,6 @@ dependencies = [
"libc",
]
[[package]]
name = "error-code"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21"
dependencies = [
"libc",
"str-buf",
]
[[package]]
name = "event-listener"
version = "2.5.3"
@@ -1513,6 +1444,20 @@ dependencies = [
"url",
]
[[package]]
name = "example-persist-session"
version = "0.1.0"
dependencies = [
"anyhow",
"dirs",
"matrix-sdk",
"rand 0.8.5",
"serde",
"serde_json",
"tokio",
"tracing-subscriber",
]
[[package]]
name = "example-timeline"
version = "0.1.0"
@@ -1520,7 +1465,6 @@ dependencies = [
"anyhow",
"clap 4.1.6",
"futures",
"futures-signals",
"matrix-sdk",
"tokio",
"tracing-subscriber",
@@ -1538,6 +1482,18 @@ dependencies = [
"syn",
]
[[package]]
name = "eyeball"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3609348664c9c1b07d9ff3933a466beaa4d197fb393b5d41ffda349458867626"
dependencies = [
"futures-core",
"readlock",
"tokio",
"tokio-stream",
]
[[package]]
name = "eyeball-im"
version = "0.1.0"
@@ -1572,16 +1528,6 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fancy-regex"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d6b8560a05112eb52f04b00e5d3790c0dd75d9d980eb8a122fb23b92a623ccf"
dependencies = [
"bit-set",
"regex",
]
[[package]]
name = "fastrand"
version = "1.9.0"
@@ -1591,17 +1537,6 @@ dependencies = [
"instant",
]
[[package]]
name = "fd-lock"
version = "3.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ef1a30ae415c3a691a4f41afddc2dbcd6d70baf338368d85ebc1e8ed92cedb9"
dependencies = [
"cfg-if",
"rustix",
"windows-sys 0.45.0",
]
[[package]]
name = "findshlibs"
version = "0.10.2"
@@ -1682,6 +1617,16 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "futf"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
dependencies = [
"mac",
"new_debug_unreachable",
]
[[package]]
name = "futures"
version = "0.3.26"
@@ -1756,21 +1701,6 @@ dependencies = [
"syn",
]
[[package]]
name = "futures-signals"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3acc659ba666cff13fdf65242d16428f2f11935b688f82e4024ad39667a5132"
dependencies = [
"discard",
"futures-channel",
"futures-core",
"futures-util",
"log",
"pin-project",
"serde",
]
[[package]]
name = "futures-sink"
version = "0.3.26"
@@ -2001,6 +1931,20 @@ dependencies = [
"digest 0.10.6",
]
[[package]]
name = "html5ever"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7"
dependencies = [
"log",
"mac",
"markup5ever",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "http"
version = "0.2.8"
@@ -2167,6 +2111,7 @@ dependencies = [
"bitmaps",
"rand_core 0.6.4",
"rand_xoshiro",
"serde",
"sized-chunks",
"typenum",
"version_check",
@@ -2343,10 +2288,10 @@ dependencies = [
"chrono",
"clap 4.1.6",
"dialoguer",
"eyeball",
"eyeball-im",
"eyre",
"futures",
"futures-signals",
"log4rs",
"matrix-sdk",
"matrix-sdk-common",
@@ -2581,12 +2526,32 @@ dependencies = [
"thread-id",
]
[[package]]
name = "mac"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "maplit"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
[[package]]
name = "markup5ever"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016"
dependencies = [
"log",
"phf 0.10.1",
"phf_codegen",
"string_cache",
"string_cache_codegen",
"tendril",
]
[[package]]
name = "matchers"
version = "0.1.0"
@@ -2643,11 +2608,11 @@ dependencies = [
"dashmap",
"dirs",
"event-listener",
"eyeball",
"eyeball-im",
"eyre",
"futures",
"futures-core",
"futures-signals",
"futures-util",
"getrandom 0.2.8",
"gloo-timers",
@@ -2673,11 +2638,11 @@ dependencies = [
"tempfile",
"thiserror",
"tokio",
"tokio-stream",
"tower",
"tracing",
"tracing-subscriber",
"url",
"uuid",
"wasm-bindgen-test",
"wiremock",
"zeroize",
@@ -2713,14 +2678,15 @@ dependencies = [
name = "matrix-sdk-base"
version = "0.6.1"
dependencies = [
"assert_matches",
"assign",
"async-stream",
"async-trait",
"ctor",
"dashmap",
"eyeball",
"futures",
"futures-core",
"futures-signals",
"futures-util",
"http",
"matrix-sdk-common",
@@ -2770,12 +2736,13 @@ dependencies = [
"bs58",
"byteorder",
"cfg-if",
"ctor",
"ctr",
"dashmap",
"event-listener",
"eyeball",
"futures",
"futures-core",
"futures-signals",
"futures-util",
"hmac",
"http",
@@ -2795,6 +2762,7 @@ dependencies = [
"thiserror",
"tokio",
"tracing",
"tracing-subscriber",
"vodozemac",
"zeroize",
]
@@ -2858,6 +2826,7 @@ dependencies = [
"matrix-sdk-common",
"matrix-sdk-crypto",
"matrix-sdk-sled",
"matrix-sdk-sqlite",
"napi",
"napi-build",
"napi-derive",
@@ -2875,9 +2844,9 @@ dependencies = [
"anyhow",
"base64 0.21.0",
"extension-trait",
"eyeball",
"eyeball-im",
"futures-core",
"futures-signals",
"futures-util",
"log-panics",
"matrix-sdk",
@@ -2885,6 +2854,7 @@ dependencies = [
"once_cell",
"opentelemetry",
"opentelemetry-otlp",
"ruma",
"sanitize-filename-reader-friendly",
"serde_json",
"thiserror",
@@ -2904,6 +2874,7 @@ name = "matrix-sdk-indexeddb"
version = "0.2.0"
dependencies = [
"anyhow",
"assert_matches",
"async-trait",
"base64 0.21.0",
"dashmap",
@@ -3229,13 +3200,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
[[package]]
name = "nibble_vec"
version = "0.1.0"
name = "new_debug_unreachable"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43"
dependencies = [
"smallvec",
]
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
[[package]]
name = "nix"
@@ -3248,18 +3216,6 @@ dependencies = [
"libc",
]
[[package]]
name = "nix"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4"
dependencies = [
"autocfg",
"bitflags",
"cfg-if",
"libc",
]
[[package]]
name = "nom"
version = "7.1.3"
@@ -3663,6 +3619,86 @@ dependencies = [
"indexmap",
]
[[package]]
name = "phf"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
dependencies = [
"phf_shared 0.10.0",
]
[[package]]
name = "phf"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c"
dependencies = [
"phf_macros",
"phf_shared 0.11.1",
]
[[package]]
name = "phf_codegen"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
dependencies = [
"phf_generator 0.10.0",
"phf_shared 0.10.0",
]
[[package]]
name = "phf_generator"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
dependencies = [
"phf_shared 0.10.0",
"rand 0.8.5",
]
[[package]]
name = "phf_generator"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf"
dependencies = [
"phf_shared 0.11.1",
"rand 0.8.5",
]
[[package]]
name = "phf_macros"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92aacdc5f16768709a569e913f7451034034178b05bdc8acda226659a3dccc66"
dependencies = [
"phf_generator 0.11.1",
"phf_shared 0.11.1",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "phf_shared"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
dependencies = [
"siphasher",
]
[[package]]
name = "phf_shared"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676"
dependencies = [
"siphasher",
]
[[package]]
name = "pin-project"
version = "1.0.12"
@@ -3793,7 +3829,7 @@ dependencies = [
"inferno",
"libc",
"log",
"nix 0.24.3",
"nix",
"once_cell",
"parking_lot 0.12.1",
"smallvec",
@@ -3808,6 +3844,12 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "precomputed-hash"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "prettyplease"
version = "0.1.23"
@@ -3979,16 +4021,6 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "radix_trie"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd"
dependencies = [
"endian-type",
"nibble_vec",
]
[[package]]
name = "rand"
version = "0.7.3"
@@ -4100,6 +4132,12 @@ dependencies = [
"num_cpus",
]
[[package]]
name = "readlock"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a8f0cb425ba44d6bde0d063097aae68a2ce31e1d5359e96427704f33f4f73d9"
[[package]]
name = "redox_syscall"
version = "0.2.16"
@@ -4255,8 +4293,7 @@ dependencies = [
[[package]]
name = "ruma"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6429e3fae5d6ab07742bcf9a1705f68f97d082801cc5afe9290579bf7abcf053"
source = "git+https://github.com/ruma/ruma?rev=543c03f8f2d0f5a357b6be41cc0e166aa58cce63#543c03f8f2d0f5a357b6be41cc0e166aa58cce63"
dependencies = [
"assign",
"js_int",
@@ -4270,8 +4307,7 @@ dependencies = [
[[package]]
name = "ruma-appservice-api"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed16a943a44ba290481f2284f71cac31757fabaee8ca2d059afd20ff8b33c31"
source = "git+https://github.com/ruma/ruma?rev=543c03f8f2d0f5a357b6be41cc0e166aa58cce63#543c03f8f2d0f5a357b6be41cc0e166aa58cce63"
dependencies = [
"js_int",
"ruma-common",
@@ -4282,8 +4318,7 @@ dependencies = [
[[package]]
name = "ruma-client-api"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67b35700529224d167697ce575c71ca26c489af5774756843e335af767f6fdf"
source = "git+https://github.com/ruma/ruma?rev=543c03f8f2d0f5a357b6be41cc0e166aa58cce63#543c03f8f2d0f5a357b6be41cc0e166aa58cce63"
dependencies = [
"assign",
"bytes",
@@ -4300,13 +4335,13 @@ dependencies = [
[[package]]
name = "ruma-common"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3b4ec3f70ea9afeae96a6c1e5eb86ed02760d5c28a167b5d9a433cefaaf815c"
source = "git+https://github.com/ruma/ruma?rev=543c03f8f2d0f5a357b6be41cc0e166aa58cce63#543c03f8f2d0f5a357b6be41cc0e166aa58cce63"
dependencies = [
"base64 0.21.0",
"bytes",
"form_urlencoded",
"getrandom 0.2.8",
"html5ever",
"http",
"indexmap",
"js-sys",
@@ -4314,6 +4349,7 @@ dependencies = [
"js_option",
"konst",
"percent-encoding",
"phf 0.11.1",
"pulldown-cmark",
"rand 0.8.5",
"regex",
@@ -4332,8 +4368,7 @@ dependencies = [
[[package]]
name = "ruma-federation-api"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d05ebbed580138816c3d564f9191e576acf96441e1faca9dcefe7092db6979"
source = "git+https://github.com/ruma/ruma?rev=543c03f8f2d0f5a357b6be41cc0e166aa58cce63#543c03f8f2d0f5a357b6be41cc0e166aa58cce63"
dependencies = [
"js_int",
"ruma-common",
@@ -4344,8 +4379,7 @@ dependencies = [
[[package]]
name = "ruma-identifiers-validation"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebefdab34311af44d07cd2cd91c36cfe6a8c770647c6b00f6ab47f1186b2bb72"
source = "git+https://github.com/ruma/ruma?rev=543c03f8f2d0f5a357b6be41cc0e166aa58cce63#543c03f8f2d0f5a357b6be41cc0e166aa58cce63"
dependencies = [
"js_int",
"thiserror",
@@ -4354,8 +4388,7 @@ dependencies = [
[[package]]
name = "ruma-macros"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23e883799456b6213da90fe065d4234f282b89afe161af3e5fcc854e44e8f582"
source = "git+https://github.com/ruma/ruma?rev=543c03f8f2d0f5a357b6be41cc0e166aa58cce63#543c03f8f2d0f5a357b6be41cc0e166aa58cce63"
dependencies = [
"once_cell",
"proc-macro-crate",
@@ -4428,40 +4461,6 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70"
[[package]]
name = "rustyline"
version = "10.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1e83c32c3f3c33b08496e0d1df9ea8c64d39adb8eb36a1ebb1440c690697aef"
dependencies = [
"bitflags",
"cfg-if",
"clipboard-win",
"dirs-next",
"fd-lock",
"libc",
"log",
"memchr",
"nix 0.25.1",
"radix_trie",
"scopeguard",
"unicode-segmentation",
"unicode-width",
"utf8parse",
"winapi",
]
[[package]]
name = "rustyline-derive"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "107c3d5d7f370ac09efa62a78375f94d94b8a33c61d8c278b96683fb4dbf2d8d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "ryu"
version = "1.0.12"
@@ -4804,29 +4803,13 @@ dependencies = [
"parking_lot 0.11.2",
]
[[package]]
name = "sled-state-inspector"
version = "0.1.0"
dependencies = [
"atty",
"clap 3.2.23",
"futures",
"matrix-sdk-base",
"matrix-sdk-sled",
"ruma",
"rustyline",
"rustyline-derive",
"serde",
"serde_json",
"syntect",
]
[[package]]
name = "sliding-sync-integration-test"
version = "0.1.0"
dependencies = [
"anyhow",
"assert_matches",
"eyeball",
"eyeball-im",
"futures",
"matrix-sdk",
@@ -4890,18 +4873,38 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "str-buf"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0"
[[package]]
name = "str_stack"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb"
[[package]]
name = "string_cache"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d69e88b23f23030bf4d0e9ca7b07434f70e1c1f4d3ca7e93ce958b373654d9f"
dependencies = [
"new_debug_unreachable",
"once_cell",
"parking_lot 0.12.1",
"phf_shared 0.10.0",
"precomputed-hash",
"serde",
]
[[package]]
name = "string_cache_codegen"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988"
dependencies = [
"phf_generator 0.10.0",
"phf_shared 0.10.0",
"proc-macro2",
"quote",
]
[[package]]
name = "strsim"
version = "0.10.0"
@@ -4966,27 +4969,6 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "syntect"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6c454c27d9d7d9a84c7803aaa3c50cd088d2906fe3c6e42da3209aa623576a8"
dependencies = [
"bincode",
"bitflags",
"fancy-regex",
"flate2",
"fnv",
"lazy_static",
"once_cell",
"regex-syntax",
"serde",
"serde_derive",
"serde_json",
"thiserror",
"walkdir",
]
[[package]]
name = "tempfile"
version = "3.3.0"
@@ -5001,6 +4983,17 @@ dependencies = [
"winapi",
]
[[package]]
name = "tendril"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
dependencies = [
"futf",
"mac",
"utf-8",
]
[[package]]
name = "termcolor"
version = "1.2.0"
@@ -5660,8 +5653,7 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
[[package]]
name = "uniffi"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f71cc01459bc34cfe43fabf32b39f1228709bc6db1b3a664a92940af3d062376"
source = "git+https://github.com/mozilla/uniffi-rs?rev=58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4#58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4"
dependencies = [
"anyhow",
"camino",
@@ -5682,8 +5674,7 @@ dependencies = [
[[package]]
name = "uniffi_bindgen"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbbba5103051c18f10b22f80a74439ddf7100273f217a547005d2735b2498994"
source = "git+https://github.com/mozilla/uniffi-rs?rev=58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4#58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4"
dependencies = [
"anyhow",
"askama",
@@ -5706,8 +5697,7 @@ dependencies = [
[[package]]
name = "uniffi_build"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee1a28368ff3d83717e3d3e2e15a66269c43488c3f036914131bb68892f29fb"
source = "git+https://github.com/mozilla/uniffi-rs?rev=58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4#58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4"
dependencies = [
"anyhow",
"camino",
@@ -5717,8 +5707,7 @@ dependencies = [
[[package]]
name = "uniffi_checksum_derive"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03de61393a42b4ad4984a3763c0600594ac3e57e5aaa1d05cede933958987c03"
source = "git+https://github.com/mozilla/uniffi-rs?rev=58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4#58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4"
dependencies = [
"quote",
"syn",
@@ -5727,8 +5716,7 @@ dependencies = [
[[package]]
name = "uniffi_core"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2b4852d638d74ca2d70e450475efb6d91fe6d54a7cd8d6bd80ad2ee6cd7daa"
source = "git+https://github.com/mozilla/uniffi-rs?rev=58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4#58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4"
dependencies = [
"anyhow",
"bytes",
@@ -5743,8 +5731,7 @@ dependencies = [
[[package]]
name = "uniffi_macros"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa03394de21e759e0022f1ea8d992d2e39290d735b9ed52b1f74b20a684f794e"
source = "git+https://github.com/mozilla/uniffi-rs?rev=58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4#58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4"
dependencies = [
"bincode",
"camino",
@@ -5762,8 +5749,7 @@ dependencies = [
[[package]]
name = "uniffi_meta"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66fdab2c436aed7a6391bec64204ec33948bfed9b11b303235740771f85c4ea6"
source = "git+https://github.com/mozilla/uniffi-rs?rev=58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4#58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4"
dependencies = [
"serde",
"siphasher",
@@ -5773,8 +5759,7 @@ dependencies = [
[[package]]
name = "uniffi_testing"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92b0570953ec41d97ce23e3b92161ac18231670a1f97523258a6d2ab76d7f76c"
source = "git+https://github.com/mozilla/uniffi-rs?rev=58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4#58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4"
dependencies = [
"anyhow",
"camino",
@@ -5820,10 +5805,10 @@ dependencies = [
]
[[package]]
name = "utf8parse"
version = "0.2.0"
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "uuid"
@@ -5856,7 +5841,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "vodozemac"
version = "0.3.0"
source = "git+https://github.com/matrix-org/vodozemac?rev=12b24e909107c1fac23245376f294eaf48ba186a#12b24e909107c1fac23245376f294eaf48ba186a"
source = "git+https://github.com/matrix-org/vodozemac?rev=fb609ca1e4df5a7a818490ae86ac694119e41e71#fb609ca1e4df5a7a818490ae86ac694119e41e71"
dependencies = [
"aes",
"arrayvec",
@@ -6058,8 +6043,7 @@ dependencies = [
[[package]]
name = "weedle2"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e79c5206e1f43a2306fd64bdb95025ee4228960f2e6c5a8b173f3caaf807741"
source = "git+https://github.com/mozilla/uniffi-rs?rev=58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4#58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4"
dependencies = [
"nom",
]

View File

@@ -27,19 +27,21 @@ base64 = "0.21.0"
byteorder = "1.4.3"
ctor = "0.1.26"
dashmap = "5.2.0"
eyeball = "0.1.4"
eyeball-im = "0.1.0"
futures-util = { version = "0.3.26", default-features = false, features = ["alloc"] }
http = "0.2.6"
ruma = { version = "0.8.0", features = ["client-api-c"] }
ruma-common = "0.11.2"
ruma = { git = "https://github.com/ruma/ruma", rev = "543c03f8f2d0f5a357b6be41cc0e166aa58cce63", features = ["client-api-c"] }
ruma-common = { git = "https://github.com/ruma/ruma", rev = "543c03f8f2d0f5a357b6be41cc0e166aa58cce63" }
once_cell = "1.16.0"
serde = "1.0.151"
serde_html_form = "0.2.0"
serde_json = "1.0.91"
thiserror = "1.0.38"
tracing = { version = "0.1.36", default-features = false, features = ["std"] }
uniffi = "0.23.0"
uniffi_bindgen = "0.23.0"
vodozemac = { git = "https://github.com/matrix-org/vodozemac", rev = "12b24e909107c1fac23245376f294eaf48ba186a" }
uniffi = { git = "https://github.com/mozilla/uniffi-rs", rev = "58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4" }
uniffi_bindgen = { git = "https://github.com/mozilla/uniffi-rs", rev = "58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4" }
vodozemac = { git = "https://github.com/matrix-org/vodozemac", rev = "fb609ca1e4df5a7a818490ae86ac694119e41e71" }
zeroize = "1.3.0"
# Default release profile, select with `--release`

View File

@@ -5,6 +5,10 @@ import PackageDescription
let package = Package(
name: "MatrixRustSDK",
platforms: [
.iOS(.v15),
.macOS(.v12)
],
products: [
.library(name: "MatrixRustSDK",
targets: ["MatrixRustSDK"]),

View File

@@ -1,23 +0,0 @@
# Matrix rust components kotlin
This project and build scripts demonstrate how to create an aar and how to import it in your android projects.
## Prerequisites
* the Rust toolchain
* cargo-ndk < 2.12.0 `cargo install cargo-ndk --version 2.11.0`
* android targets (e.g. `rustup target add \
aarch64-linux-android \
armv7-linux-androideabi \
x86_64-linux-android \
i686-linux-android`)
## Building the SDK
To build the full sdk and get an aar you can call :
`./bindings/kotlin/scripts/build_sdk.sh /matrix-rust_sdk/bindings/kotlin/sample/libs`
where the parameter is the path for the aar to go
## License
[Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)

View File

@@ -1,5 +0,0 @@
# Reporting a Vulnerability
**If you've found a security vulnerability, please report it to security@matrix.org**
For more information on our security disclosure policy, visit https://www.matrix.org/security-disclosure-policy/

View File

@@ -1,23 +0,0 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
apply plugin: 'io.github.gradle-nexus.publish-plugin'
apply from: "${rootDir}/scripts/publish-root.gradle"
buildscript {
repositories {
maven { url "https://plugins.gradle.org/m2/" }
google()
mavenCentral()
}
dependencies {
classpath BuildPlugins.android
classpath BuildPlugins.kotlin
classpath BuildPlugins.nexusPublish
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@@ -1,9 +0,0 @@
import org.gradle.kotlin.dsl.`kotlin-dsl`
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
}

View File

@@ -1,11 +0,0 @@
object ConfigurationData {
const val compileSdk = 31
const val targetSdk = 31
const val minSdk = 21
const val majorVersion = 0
const val minorVersion = 2
const val patchVersion = 0
const val versionName = "$majorVersion.$minorVersion.$patchVersion"
const val snapshotVersionName = "$majorVersion.$minorVersion.${patchVersion + 1}-SNAPSHOT"
const val publishGroupId = "org.matrix.rustcomponents"
}

View File

@@ -1,22 +0,0 @@
internal object Versions {
const val androidGradlePlugin = "7.1.2"
const val kotlin = "1.6.10"
const val jUnit = "4.12"
const val nexusPublishGradlePlugin = "1.1.0"
const val jna = "5.10.0"
}
internal object BuildPlugins {
const val android = "com.android.tools.build:gradle:${Versions.androidGradlePlugin}"
const val kotlin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}"
const val nexusPublish = "io.github.gradle-nexus:publish-plugin:${Versions.nexusPublishGradlePlugin}"
}
/**
* To define dependencies
*/
internal object Dependencies {
const val kotlin = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.kotlin}"
const val junit = "junit:junit:${Versions.jUnit}"
const val jna = "net.java.dev.jna:jna:${Versions.jna}@aar"
}

View File

@@ -1 +0,0 @@
/build

View File

@@ -1,51 +0,0 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
}
ext {
PUBLISH_GROUP_ID = ConfigurationData.publishGroupId
PUBLISH_ARTIFACT_ID = 'crypto-android'
PUBLISH_VERSION = rootVersionName
PUBLISH_DESCRIPTION = 'Android Bindings to the Matrix Rust Crypto SDK'
}
apply from: "${rootDir}/scripts/publish-module.gradle"
android {
compileSdk ConfigurationData.compileSdk
defaultConfig {
minSdk ConfigurationData.minSdk
targetSdk ConfigurationData.targetSdk
versionName ConfigurationData.versionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
android.libraryVariants.all { variant ->
def sourceSet = variant.sourceSets.find { it.name == variant.name }
sourceSet.java.srcDir new File(buildDir, "generated/source/${variant.name}")
}
dependencies {
implementation Dependencies.jna
testImplementation Dependencies.junit
}

View File

@@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="org.matrix.rustcomponents.sdk.crypto">
</manifest>

View File

@@ -1 +0,0 @@
/build

View File

@@ -1,13 +0,0 @@
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
}
java {
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
}
dependencies {
implementation 'net.java.dev.jna:jna:5.10.0@aar'
}

View File

@@ -1,25 +0,0 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

View File

Binary file not shown.

View File

@@ -1,6 +0,0 @@
#Mon Feb 28 18:48:31 CET 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

View File

@@ -1,185 +0,0 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# 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
#
# https://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.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

View File

@@ -1,89 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -1,66 +0,0 @@
#!/usr/bin/env bash
set -eEu
cd "$(dirname "$0")"
CURRENT_DIR=$(pwd)
# FOR DEBUG
#RELEASE_FLAG=""
#RELEASE_TYPE_DIR="debug"
#RELEASE_AAR_NAME="crypto-android-debug"
# FOR RELEASE
RELEASE_FLAG="--release"
RELEASE_TYPE_DIR="release"
RELEASE_AAR_NAME="crypto-android-release"
SRC_ROOT=../../..
# Path to the kotlin root project
KOTLIN_ROOT=..
BASE_TARGET_DIR="${SRC_ROOT}/target"
SDK_ROOT="${KOTLIN_ROOT}/crypto/crypto-android"
SDK_TARGET_DIR="${SDK_ROOT}/src/main/jniLibs"
BUILD_DIR="${SDK_ROOT}/build"
GENERATED_DIR="${BUILD_DIR}/generated/source/${RELEASE_TYPE_DIR}"
mkdir -p ${GENERATED_DIR}
TARGET_CRATE=matrix-sdk-crypto-ffi
AAR_DESTINATION=$1
# Build libs for all the different architectures
echo -e "Building for x86_64-linux-android[1/4]"
cargo ndk --target x86_64-linux-android -o ${SDK_TARGET_DIR}/ build "${RELEASE_FLAG}" -p ${TARGET_CRATE}
echo -e "Building for aarch64-linux-android[2/4]"
cargo ndk --target aarch64-linux-android -o ${SDK_TARGET_DIR}/ build "${RELEASE_FLAG}" -p ${TARGET_CRATE}
echo -e "Building for armv7-linux-androideabi[3/4]"
cargo ndk --target armv7-linux-androideabi -o ${SDK_TARGET_DIR}/ build "${RELEASE_FLAG}" -p ${TARGET_CRATE}
echo -e "Building for i686-linux-android[4/4]"
cargo ndk --target i686-linux-android -o ${SDK_TARGET_DIR}/ build "${RELEASE_FLAG}" -p ${TARGET_CRATE}
# Generate uniffi files
echo -e "Generate uniffi kotlin file"
cargo uniffi-bindgen generate "${SRC_ROOT}/bindings/${TARGET_CRATE}/src/olm.udl" \
--language kotlin \
--config "${SRC_ROOT}/bindings/${TARGET_CRATE}/uniffi.toml" \
--out-dir ${GENERATED_DIR} \
--lib-file "${BASE_TARGET_DIR}/x86_64-linux-android/${RELEASE_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a"
# Create android library
cd "${KOTLIN_ROOT}"
./gradlew :crypto:crypto-android:assemble
cd "${CURRENT_DIR}"
echo -e "Moving the generated aar file to ${AAR_DESTINATION}/matrix-rust-sdk-crypto.aar"
mv "${BUILD_DIR}/outputs/aar/${RELEASE_AAR_NAME}.aar" "${AAR_DESTINATION}/matrix-rust-sdk-crypto.aar"
# Clean-up
echo -e "Cleaning up temporary files"
rm -r "${BUILD_DIR}"
rm -r "${SDK_TARGET_DIR}"

View File

@@ -1,63 +0,0 @@
#!/usr/bin/env bash
set -eEu
cd "$(dirname "$0")"
CURRENT_DIR=$(pwd)
# FOR DEBUG
#RELEASE_FLAG=""
#RELEASE_TYPE_DIR="debug"
#RELEASE_AAR_NAME="sdk-android-debug"
# FOR RELEASE
RELEASE_FLAG="--release"
RELEASE_TYPE_DIR="release"
RELEASE_AAR_NAME="sdk-android-release"
SRC_ROOT=../../..
# Path to the kotlin root project
KOTLIN_ROOT=..
BASE_TARGET_DIR="${SRC_ROOT}/target"
SDK_ROOT="${KOTLIN_ROOT}/sdk/sdk-android"
SDK_TARGET_DIR="${SDK_ROOT}/src/main/jniLibs"
BUILD_DIR="${SDK_ROOT}/build"
GENERATED_DIR="${BUILD_DIR}/generated/source/${RELEASE_TYPE_DIR}"
mkdir -p ${GENERATED_DIR}
AAR_DESTINATION=$1
# Build libs for all the different architectures
echo -e "Building for x86_64-linux-android[1/4]"
cargo ndk --target x86_64-linux-android -o ${SDK_TARGET_DIR}/ build "${RELEASE_FLAG}" -p matrix-sdk-ffi
echo -e "Building for aarch64-linux-android[2/4]"
cargo ndk --target aarch64-linux-android -o ${SDK_TARGET_DIR}/ build "${RELEASE_FLAG}" -p matrix-sdk-ffi
echo -e "Building for armv7-linux-androideabi[3/4]"
cargo ndk --target armv7-linux-androideabi -o ${SDK_TARGET_DIR}/ build "${RELEASE_FLAG}" -p matrix-sdk-ffi
echo -e "Building for i686-linux-android[4/4]"
cargo ndk --target i686-linux-android -o ${SDK_TARGET_DIR}/ build "${RELEASE_FLAG}" -p matrix-sdk-ffi
# Generate uniffi files
echo -e "Generate uniffi kotlin file"
cargo uniffi-bindgen generate "${SRC_ROOT}/bindings/matrix-sdk-ffi/src/api.udl" \
--language kotlin \
--out-dir ${GENERATED_DIR} \
--lib-file "${BASE_TARGET_DIR}/x86_64-linux-android/${RELEASE_TYPE_DIR}/libmatrix_sdk_ffi.a"
# Create android library
cd "${KOTLIN_ROOT}"
./gradlew :sdk:sdk-android:assemble
cd "${CURRENT_DIR}"
echo -e "Moving the generated aar file to ${AAR_DESTINATION}/matrix-rust-sdk.aar"
mv "${BUILD_DIR}/outputs/aar/${RELEASE_AAR_NAME}.aar" "${AAR_DESTINATION}/matrix-rust-sdk.aar"
# Clean-up
echo -e "Cleaning up temporary files"
rm -r "${BUILD_DIR}"
rm -r "${SDK_TARGET_DIR}"

View File

@@ -1,77 +0,0 @@
apply plugin: 'maven-publish'
apply plugin: 'signing'
task androidSourcesJar(type: Jar) {
archiveClassifier.set('sources')
if (project.plugins.findPlugin("com.android.library")) {
// For Android libraries
from android.sourceSets.main.java.srcDirs
from android.sourceSets.main.kotlin.srcDirs
} else {
// For pure Kotlin libraries, in case you have them
from sourceSets.main.java.srcDirs
from sourceSets.main.kotlin.srcDirs
}
}
artifacts {
archives androidSourcesJar
}
group = PUBLISH_GROUP_ID
version = rootVersionName
afterEvaluate {
publishing {
publications {
release(MavenPublication) {
groupId PUBLISH_GROUP_ID
artifactId PUBLISH_ARTIFACT_ID
version PUBLISH_VERSION
if (project.plugins.findPlugin("com.android.library")) {
from components.release
} else {
from components.java
}
artifact androidSourcesJar
pom {
name = PUBLISH_ARTIFACT_ID
description = PUBLISH_DESCRIPTION
url = 'https://github.com/matrix-org/matrix-rust-components-kotlin'
licenses {
license {
name = 'The Apache Software License, Version 2.0'
url = 'https://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
developers {
developer {
id = 'matrixdev'
name = 'matrixdev'
email = 'android@element.io'
}
}
scm {
connection = 'scm:git:git://github.com/matrix-org/matrix-rust-components-kotlin.git'
developerConnection = 'scm:git:ssh://git@github.com/matrix-org/matrix-rust-components-kotlin.git'
url = 'https://github.com/matrix-org/matrix-rust-components-kotlin'
}
}
}
}
}
}
signing {
useInMemoryPgpKeys(
rootProject.ext["signing.keyId"],
rootProject.ext["signing.key"],
rootProject.ext["signing.password"],
)
sign publishing.publications
}

View File

@@ -1,43 +0,0 @@
ext["signing.keyId"] = ''
ext["signing.password"] = ''
ext["signing.key"] = ''
ext["ossrhUsername"] = ''
ext["ossrhPassword"] = ''
ext["sonatypeStagingProfileId"] = ''
ext["snapshot"] = ''
File secretPropsFile = project.rootProject.file('local.properties')
if (secretPropsFile.exists()) {
// Read local.properties file first if it exists
Properties p = new Properties()
new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) }
p.each { name, value -> ext[name] = value }
} else {
// Use system environment variables
ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME')
ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD')
ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')
ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID')
ext["signing.password"] = System.getenv('SIGNING_PASSWORD')
ext["signing.key"] = System.getenv('SIGNING_KEY')
ext["snapshot"] = System.getenv('SNAPSHOT')
}
if (snapshot.toBoolean()) {
ext["rootVersionName"] = ConfigurationData.snapshotVersionName
} else {
ext["rootVersionName"] = ConfigurationData.versionName
}
nexusPublishing {
repositories {
sonatype {
stagingProfileId = sonatypeStagingProfileId
username = ossrhUsername
password = ossrhPassword
version = rootVersionName
nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
}
}
}

View File

@@ -1 +0,0 @@
/build

View File

@@ -1,51 +0,0 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
}
ext {
PUBLISH_GROUP_ID = ConfigurationData.publishGroupId
PUBLISH_ARTIFACT_ID = 'sdk-android'
PUBLISH_VERSION = rootVersionName
PUBLISH_DESCRIPTION = 'Android Bindings to the Matrix Rust SDK'
}
apply from: "${rootDir}/scripts/publish-module.gradle"
android {
compileSdk ConfigurationData.compileSdk
defaultConfig {
minSdk ConfigurationData.minSdk
targetSdk ConfigurationData.targetSdk
versionName ConfigurationData.versionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
android.libraryVariants.all { variant ->
def sourceSet = variant.sourceSets.find { it.name == variant.name }
sourceSet.java.srcDir new File(buildDir, "generated/source/${variant.name}")
}
dependencies {
implementation Dependencies.jna
testImplementation Dependencies.junit
}

View File

@@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="org.matrix.rustcomponents.sdk.android" />

View File

@@ -1 +0,0 @@
/build

View File

@@ -1,13 +0,0 @@
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
}
java {
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
}
dependencies {
implementation 'net.java.dev.jna:jna:5.10.0@aar'
}

View File

@@ -1,27 +0,0 @@
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
plugins {
id 'com.android.application' version '7.1.0-beta01'
id 'com.android.library' version '7.1.0-beta01'
id 'org.jetbrains.kotlin.android' version '1.5.30'
id 'org.jetbrains.kotlin.jvm' version '1.5.30'
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
flatDir {
dirs 'libs'
}
}
}
rootProject.name = "MatrixKotlinRustSDK"
include ':crypto:crypto-android'
include ':crypto:crypto-jvm'
include ':sdk:sdk-jvm'
include ':sdk:sdk-android'

View File

@@ -27,8 +27,7 @@ pub enum PkDecryptionError {
}
/// Error type for the decoding and storing of the backup key.
#[derive(Debug, Error, uniffi::Error)]
#[uniffi(flat_error)]
#[derive(Debug, Error)]
pub enum DecodeError {
/// An error happened while decoding the recovery key.
#[error(transparent)]
@@ -41,7 +40,7 @@ pub enum DecodeError {
/// Struct containing info about the way the backup key got derived from a
/// passphrase.
#[derive(Debug, Clone, uniffi::Record)]
#[derive(Debug, Clone)]
pub struct PassphraseInfo {
/// The salt that was used during key derivation.
pub private_key_salt: String,

View File

@@ -41,8 +41,7 @@ pub enum SignatureError {
UnknownUserIdentity(String),
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
#[derive(Debug, thiserror::Error)]
pub enum CryptoStoreError {
#[error("Failed to open the store")]
OpenStore(#[from] OpenStoreError),

View File

@@ -5,9 +5,6 @@
#![warn(missing_docs)]
#![allow(unused_qualifications)]
// Triggers false positives.
// See <https://github.com/rust-lang/rust-clippy/issues/10319>.
#![allow(clippy::extra_unused_type_parameters)]
mod backup_recovery_key;
mod device;
@@ -34,6 +31,8 @@ pub use machine::{KeyRequestPair, OlmMachine, SignatureVerification};
use matrix_sdk_common::deserialized_responses::VerificationState;
use matrix_sdk_crypto::{
backups::SignatureState,
olm::{IdentityKeys, InboundGroupSession, Session},
store::{Changes, CryptoStore},
types::{EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey},
EncryptionSettings as RustEncryptionSettings, LocalTrust,
};
@@ -44,15 +43,18 @@ pub use responses::{
};
use ruma::{
events::room::history_visibility::HistoryVisibility as RustHistoryVisibility, DeviceId,
DeviceKeyAlgorithm, OwnedUserId, RoomId, SecondsSinceUnixEpoch, UserId,
DeviceKeyAlgorithm, OwnedDeviceId, OwnedUserId, RoomId, SecondsSinceUnixEpoch, UserId,
};
use serde::{Deserialize, Serialize};
use tokio::runtime::Runtime;
use uniffi_api::*;
pub use users::UserIdentity;
pub use verification::{
CancelInfo, ConfirmVerificationResult, QrCode, QrCodeListener, QrCodeState,
RequestVerificationResult, Sas, SasListener, SasState, ScanResult, StartSasResult,
Verification, VerificationRequest, VerificationRequestListener, VerificationRequestState,
};
use vodozemac::{Curve25519PublicKey, Ed25519PublicKey};
/// Struct collecting data that is important to migrate to the rust-sdk
#[derive(Deserialize, Serialize)]
@@ -75,6 +77,24 @@ pub struct MigrationData {
tracked_users: Vec<String>,
}
/// Struct collecting data that is important to migrate sessions to the rust-sdk
pub struct SessionMigrationData {
/// The user id that the data belongs to.
user_id: String,
/// The device id that the data belongs to.
device_id: String,
/// The Curve25519 public key of the Account that owns this data.
curve25519_key: String,
/// The Ed25519 public key of the Account that owns this data.
ed25519_key: String,
/// The list of pickleds Olm Sessions.
sessions: Vec<PickledSession>,
/// The list of pickled Megolm inbound group sessions.
inbound_group_sessions: Vec<PickledInboundGroupSession>,
/// The Olm pickle key that was used to pickle all the Olm objects.
pickle_key: Vec<u8>,
}
/// A pickled version of an `Account`.
///
/// Holds all the information that needs to be stored in a database to restore
@@ -152,17 +172,17 @@ impl From<anyhow::Error> for MigrationError {
}
}
/// Migrate a libolm based setup to a vodozemac based setup stored in a Sled
/// Migrate a libolm based setup to a vodozemac based setup stored in a SQLite
/// store.
///
/// # Arguments
///
/// * `data` - The data that should be migrated over to the Sled store.
/// * `data` - The data that should be migrated over to the SQLite store.
///
/// * `path` - The path where the Sled store should be created.
/// * `path` - The path where the SQLite store should be created.
///
/// * `passphrase` - The passphrase that should be used to encrypt the data at
/// rest in the Sled store. **Warning**, if no passphrase is given, the store
/// rest in the SQLite store. **Warning**, if no passphrase is given, the store
/// and all its data will remain unencrypted.
///
/// * `progress_listener` - A callback that can be used to introspect the
@@ -173,7 +193,6 @@ pub fn migrate(
passphrase: Option<String>,
progress_listener: Box<dyn ProgressListener>,
) -> anyhow::Result<()> {
use tokio::runtime::Runtime;
let runtime = Runtime::new()?;
runtime.block_on(async move {
migrate_data(data, path, passphrase, progress_listener).await?;
@@ -187,15 +206,8 @@ async fn migrate_data(
passphrase: Option<String>,
progress_listener: Box<dyn ProgressListener>,
) -> anyhow::Result<()> {
use matrix_sdk_crypto::{
olm::PrivateCrossSigningIdentity,
store::{Changes as RustChanges, CryptoStore, RecoveryKey},
};
use vodozemac::{
megolm::InboundGroupSession,
olm::{Account, Session},
Curve25519PublicKey,
};
use matrix_sdk_crypto::{olm::PrivateCrossSigningIdentity, store::RecoveryKey};
use vodozemac::olm::Account;
use zeroize::Zeroize;
// The total steps here include all the sessions/inbound group sessions and
@@ -241,69 +253,17 @@ async fn migrate_data(
processed_steps += 1;
listener(processed_steps, total_steps);
let mut sessions = Vec::new();
for session_pickle in data.sessions {
let pickle =
Session::from_libolm_pickle(&session_pickle.pickle, &data.pickle_key)?.pickle();
let creation_time = SecondsSinceUnixEpoch(UInt::from_str(&session_pickle.creation_time)?);
let last_use_time = SecondsSinceUnixEpoch(UInt::from_str(&session_pickle.last_use_time)?);
let pickle = matrix_sdk_crypto::olm::PickledSession {
pickle,
sender_key: Curve25519PublicKey::from_base64(&session_pickle.sender_key)?,
created_using_fallback_key: session_pickle.created_using_fallback_key,
creation_time,
last_use_time,
};
let session = matrix_sdk_crypto::olm::Session::from_pickle(
user_id.clone(),
device_id.clone(),
identity_keys.clone(),
pickle,
);
sessions.push(session);
processed_steps += 1;
listener(processed_steps, total_steps);
}
let mut inbound_group_sessions = Vec::new();
for session in data.inbound_group_sessions {
let pickle =
InboundGroupSession::from_libolm_pickle(&session.pickle, &data.pickle_key)?.pickle();
let sender_key = Curve25519PublicKey::from_base64(&session.sender_key)?;
let pickle = matrix_sdk_crypto::olm::PickledInboundGroupSession {
pickle,
sender_key,
signing_key: session
.signing_key
.into_iter()
.map(|(k, v)| {
let algorithm = DeviceKeyAlgorithm::try_from(k)?;
let key = SigningKey::from_parts(&algorithm, v)?;
Ok((algorithm, key))
})
.collect::<anyhow::Result<_>>()?,
room_id: RoomId::parse(session.room_id)?,
imported: session.imported,
backed_up: session.backed_up,
history_visibility: None,
algorithm: RustEventEncryptionAlgorithm::MegolmV1AesSha2,
};
let session = matrix_sdk_crypto::olm::InboundGroupSession::from_pickle(pickle)?;
inbound_group_sessions.push(session);
processed_steps += 1;
listener(processed_steps, total_steps);
}
let (sessions, inbound_group_sessions) = collect_sessions(
processed_steps,
total_steps,
&listener,
&data.pickle_key,
user_id.clone(),
device_id,
identity_keys,
data.sessions,
data.inbound_group_sessions,
)?;
let recovery_key =
data.backup_recovery_key.map(|k| RecoveryKey::from_base58(k.as_str())).transpose()?;
@@ -336,7 +296,7 @@ async fn migrate_data(
processed_steps += 1;
listener(processed_steps, total_steps);
let changes = RustChanges {
let changes = Changes {
account: Some(account),
private_identity: Some(cross_signing),
sessions,
@@ -345,6 +305,17 @@ async fn migrate_data(
backup_version: data.backup_version,
..Default::default()
};
save_changes(processed_steps, total_steps, &listener, changes, &store).await
}
async fn save_changes(
mut processed_steps: usize,
total_steps: usize,
listener: &dyn Fn(usize, usize),
changes: Changes,
store: &SqliteCryptoStore,
) -> anyhow::Result<()> {
store.save_changes(changes).await?;
processed_steps += 1;
@@ -353,6 +324,153 @@ async fn migrate_data(
Ok(())
}
/// Migrate sessions and group sessions of a libolm based setup to a vodozemac
/// based setup stored in a SQLite store.
///
/// This method allows you to migrate a subset of the data, it should only be
/// used after the [`migrate()`] method has been already used.
///
/// # Arguments
///
/// * `data` - The data that should be migrated over to the SQLite store.
///
/// * `path` - The path where the SQLite store should be created.
///
/// * `passphrase` - The passphrase that should be used to encrypt the data at
/// rest in the SQLite store. **Warning**, if no passphrase is given, the store
/// and all its data will remain unencrypted.
///
/// * `progress_listener` - A callback that can be used to introspect the
/// progress of the migration.
pub fn migrate_sessions(
data: SessionMigrationData,
path: &str,
passphrase: Option<String>,
progress_listener: Box<dyn ProgressListener>,
) -> anyhow::Result<()> {
let runtime = Runtime::new()?;
runtime.block_on(migrate_session_data(data, path, passphrase, progress_listener))
}
async fn migrate_session_data(
data: SessionMigrationData,
path: &str,
passphrase: Option<String>,
progress_listener: Box<dyn ProgressListener>,
) -> anyhow::Result<()> {
let store = SqliteCryptoStore::open(path, passphrase.as_deref()).await?;
let listener = |progress: usize, total: usize| {
progress_listener.on_progress(progress as i32, total as i32)
};
let total_steps = 1 + data.sessions.len() + data.inbound_group_sessions.len();
let processed_steps = 0;
let user_id = UserId::parse(data.user_id)?.into();
let device_id: OwnedDeviceId = data.device_id.into();
let identity_keys = IdentityKeys {
ed25519: Ed25519PublicKey::from_base64(&data.ed25519_key)?,
curve25519: Curve25519PublicKey::from_base64(&data.curve25519_key)?,
}
.into();
let (sessions, inbound_group_sessions) = collect_sessions(
processed_steps,
total_steps,
&listener,
&data.pickle_key,
user_id,
device_id.into(),
identity_keys,
data.sessions,
data.inbound_group_sessions,
)?;
let changes = Changes { sessions, inbound_group_sessions, ..Default::default() };
save_changes(processed_steps, total_steps, &listener, changes, &store).await
}
#[allow(clippy::too_many_arguments)]
fn collect_sessions(
mut processed_steps: usize,
total_steps: usize,
listener: &dyn Fn(usize, usize),
pickle_key: &[u8],
user_id: Arc<UserId>,
device_id: Arc<DeviceId>,
identity_keys: Arc<IdentityKeys>,
session_pickles: Vec<PickledSession>,
group_session_pickles: Vec<PickledInboundGroupSession>,
) -> anyhow::Result<(Vec<Session>, Vec<InboundGroupSession>)> {
let mut sessions = Vec::new();
for session_pickle in session_pickles {
let pickle =
vodozemac::olm::Session::from_libolm_pickle(&session_pickle.pickle, pickle_key)?
.pickle();
let creation_time = SecondsSinceUnixEpoch(UInt::from_str(&session_pickle.creation_time)?);
let last_use_time = SecondsSinceUnixEpoch(UInt::from_str(&session_pickle.last_use_time)?);
let pickle = matrix_sdk_crypto::olm::PickledSession {
pickle,
sender_key: Curve25519PublicKey::from_base64(&session_pickle.sender_key)?,
created_using_fallback_key: session_pickle.created_using_fallback_key,
creation_time,
last_use_time,
};
let session =
Session::from_pickle(user_id.clone(), device_id.clone(), identity_keys.clone(), pickle);
sessions.push(session);
processed_steps += 1;
listener(processed_steps, total_steps);
}
let mut inbound_group_sessions = Vec::new();
for session in group_session_pickles {
let pickle = vodozemac::megolm::InboundGroupSession::from_libolm_pickle(
&session.pickle,
pickle_key,
)?
.pickle();
let sender_key = Curve25519PublicKey::from_base64(&session.sender_key)?;
let pickle = matrix_sdk_crypto::olm::PickledInboundGroupSession {
pickle,
sender_key,
signing_key: session
.signing_key
.into_iter()
.map(|(k, v)| {
let algorithm = DeviceKeyAlgorithm::try_from(k)?;
let key = SigningKey::from_parts(&algorithm, v)?;
Ok((algorithm, key))
})
.collect::<anyhow::Result<_>>()?,
room_id: RoomId::parse(session.room_id)?,
imported: session.imported,
backed_up: session.backed_up,
history_visibility: None,
algorithm: RustEventEncryptionAlgorithm::MegolmV1AesSha2,
};
let session = matrix_sdk_crypto::olm::InboundGroupSession::from_pickle(pickle)?;
inbound_group_sessions.push(session);
processed_steps += 1;
listener(processed_steps, total_steps);
}
Ok((sessions, inbound_group_sessions))
}
/// Callback that will be passed over the FFI to report progress
pub trait ProgressListener {
/// The callback that should be called on the Rust side
@@ -435,6 +553,7 @@ impl From<HistoryVisibility> for RustHistoryVisibility {
/// These settings control which algorithm the room key should use, how long a
/// room key should be used and some other important information that determines
/// the lifetime of a room key.
#[derive(uniffi::Record)]
pub struct EncryptionSettings {
/// The encryption algorithm that should be used in the room.
pub algorithm: EventEncryptionAlgorithm,
@@ -484,7 +603,7 @@ pub struct DecryptedEvent {
/// Struct representing the state of our private cross signing keys, it shows
/// which private cross signing keys we have locally stored.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, uniffi::Record)]
pub struct CrossSigningStatus {
/// Do we have the master key.
pub has_master: bool,
@@ -599,10 +718,14 @@ mod uniffi_types {
backup_recovery_key::{
BackupRecoveryKey, DecodeError, MegolmV1BackupKey, PassphraseInfo, PkDecryptionError,
},
error::CryptoStoreError,
machine::OlmMachine,
responses::Request,
BackupKeys, RoomKeyCounts,
error::{CryptoStoreError, DecryptionError, SecretImportError},
machine::{KeyRequestPair, OlmMachine},
responses::{BootstrapCrossSigningResult, DeviceLists, Request},
verification::{
RequestVerificationResult, StartSasResult, Verification, VerificationRequest,
},
BackupKeys, CrossSigningKeyExport, CrossSigningStatus, DecryptedEvent, EncryptionSettings,
RoomKeyCounts,
};
}

View File

@@ -149,6 +149,17 @@ impl OlmMachine {
}
}
#[uniffi::export]
impl OlmMachine {
/// Get the status of the private cross signing keys.
///
/// This can be used to check which private cross signing keys we have
/// stored locally.
pub fn cross_signing_status(&self) -> CrossSigningStatus {
self.runtime.block_on(self.inner.cross_signing_status()).into()
}
}
impl OlmMachine {
/// Create a new `OlmMachine`
///
@@ -439,7 +450,10 @@ impl OlmMachine {
Ok(())
}
}
#[uniffi::export]
impl OlmMachine {
/// Let the state machine know about E2EE related sync changes that we
/// received from the server.
///
@@ -457,12 +471,12 @@ impl OlmMachine {
/// * `key_counts` - The map of uploaded one-time key types and counts.
pub fn receive_sync_changes(
&self,
events: &str,
events: String,
device_changes: DeviceLists,
key_counts: HashMap<String, i32>,
unused_fallback_keys: Option<Vec<String>>,
) -> Result<String, CryptoStoreError> {
let to_device: ToDevice = serde_json::from_str(events)?;
let to_device: ToDevice = serde_json::from_str(&events)?;
let device_changes: RumaDeviceLists = device_changes.into();
let key_counts: BTreeMap<DeviceKeyAlgorithm, UInt> = key_counts
.into_iter()
@@ -517,8 +531,8 @@ impl OlmMachine {
///
/// A user can be marked for tracking using the
/// [`OlmMachine::update_tracked_users()`] method.
pub fn is_user_tracked(&self, user_id: &str) -> Result<bool, CryptoStoreError> {
let user_id = parse_user_id(user_id)?;
pub fn is_user_tracked(&self, user_id: String) -> Result<bool, CryptoStoreError> {
let user_id = parse_user_id(&user_id)?;
Ok(self.runtime.block_on(self.inner.tracked_users())?.contains(&user_id))
}
@@ -570,7 +584,7 @@ impl OlmMachine {
/// * `settings` - The settings that should be used for the room key.
pub fn share_room_key(
&self,
room_id: &str,
room_id: String,
users: Vec<String>,
settings: EncryptionSettings,
) -> Result<Vec<Request>, CryptoStoreError> {
@@ -622,16 +636,16 @@ impl OlmMachine {
/// * `content` - The serialized content of the event.
pub fn encrypt(
&self,
room_id: &str,
event_type: &str,
content: &str,
room_id: String,
event_type: String,
content: String,
) -> Result<String, CryptoStoreError> {
let room_id = RoomId::parse(room_id)?;
let content: Value = serde_json::from_str(content)?;
let content: Value = serde_json::from_str(&content)?;
let encrypted_content = self
.runtime
.block_on(self.inner.encrypt_room_event_raw(&room_id, content, event_type))
.block_on(self.inner.encrypt_room_event_raw(&room_id, content, &event_type))
.expect("Encrypting an event produced an error");
Ok(serde_json::to_string(&encrypted_content)?)
@@ -646,8 +660,8 @@ impl OlmMachine {
/// * `room_id` - The unique id of the room where the event was sent to.
pub fn decrypt_room_event(
&self,
event: &str,
room_id: &str,
event: String,
room_id: String,
handle_verification_events: bool,
) -> Result<DecryptedEvent, DecryptionError> {
// Element Android wants only the content and the type and will create a
@@ -661,7 +675,7 @@ impl OlmMachine {
content: &'a RawValue,
}
let event: Raw<_> = serde_json::from_str(event)?;
let event: Raw<_> = serde_json::from_str(&event)?;
let room_id = RoomId::parse(room_id)?;
let decrypted = self.runtime.block_on(self.inner.decrypt_room_event(&event, &room_id))?;
@@ -716,10 +730,10 @@ impl OlmMachine {
/// * `room_id` - The id of the room the event was sent to.
pub fn request_room_key(
&self,
event: &str,
room_id: &str,
event: String,
room_id: String,
) -> Result<KeyRequestPair, DecryptionError> {
let event: Raw<_> = serde_json::from_str(event)?;
let event: Raw<_> = serde_json::from_str(&event)?;
let room_id = RoomId::parse(room_id)?;
let (cancel, request) =
@@ -742,17 +756,19 @@ impl OlmMachine {
/// passphrase into an key.
pub fn export_room_keys(
&self,
passphrase: &str,
passphrase: String,
rounds: i32,
) -> Result<String, CryptoStoreError> {
let keys = self.runtime.block_on(self.inner.export_room_keys(|_| true))?;
let encrypted = encrypt_room_key_export(&keys, passphrase, rounds as u32)
let encrypted = encrypt_room_key_export(&keys, &passphrase, rounds as u32)
.map_err(CryptoStoreError::Serialization)?;
Ok(encrypted)
}
}
impl OlmMachine {
fn import_room_keys_helper(
&self,
keys: Vec<ExportedRoomKey>,
@@ -827,10 +843,13 @@ impl OlmMachine {
self.import_room_keys_helper(keys, true, progress_listener)
}
}
#[uniffi::export]
impl OlmMachine {
/// Discard the currently active room key for the given room if there is
/// one.
pub fn discard_room_key(&self, room_id: &str) -> Result<(), CryptoStoreError> {
pub fn discard_room_key(&self, room_id: String) -> Result<(), CryptoStoreError> {
let room_id = RoomId::parse(room_id)?;
self.runtime.block_on(self.inner.invalidate_group_session(&room_id))?;
@@ -846,8 +865,8 @@ impl OlmMachine {
/// **Note**: This has been deprecated.
pub fn receive_unencrypted_verification_event(
&self,
event: &str,
room_id: &str,
event: String,
room_id: String,
) -> Result<(), CryptoStoreError> {
self.receive_verification_event(event, room_id)
}
@@ -858,11 +877,11 @@ impl OlmMachine {
/// in rooms to the `OlmMachine`. The event should be in the decrypted form.
pub fn receive_verification_event(
&self,
event: &str,
room_id: &str,
event: String,
room_id: String,
) -> Result<(), CryptoStoreError> {
let room_id = RoomId::parse(room_id)?;
let event: AnySyncMessageLikeEvent = serde_json::from_str(event)?;
let event: AnySyncMessageLikeEvent = serde_json::from_str(&event)?;
let event = event.into_full_event(room_id);
@@ -877,7 +896,7 @@ impl OlmMachine {
///
/// * `user_id` - The ID of the user for which we would like to fetch the
/// verification requests.
pub fn get_verification_requests(&self, user_id: &str) -> Vec<Arc<VerificationRequest>> {
pub fn get_verification_requests(&self, user_id: String) -> Vec<Arc<VerificationRequest>> {
let Ok(user_id) = UserId::parse(user_id) else {
return vec![];
};
@@ -902,8 +921,8 @@ impl OlmMachine {
/// * `flow_id` - The ID that uniquely identifies the verification flow.
pub fn get_verification_request(
&self,
user_id: &str,
flow_id: &str,
user_id: String,
flow_id: String,
) -> Option<Arc<VerificationRequest>> {
let user_id = UserId::parse(user_id).ok()?;
@@ -923,10 +942,10 @@ impl OlmMachine {
/// support.
pub fn verification_request_content(
&self,
user_id: &str,
user_id: String,
methods: Vec<String>,
) -> Result<Option<String>, CryptoStoreError> {
let user_id = parse_user_id(user_id)?;
let user_id = parse_user_id(&user_id)?;
let identity = self.runtime.block_on(self.inner.get_identity(&user_id, None))?;
@@ -963,12 +982,12 @@ impl OlmMachine {
/// [verification_request_content()]: #method.verification_request_content
pub fn request_verification(
&self,
user_id: &str,
room_id: &str,
event_id: &str,
user_id: String,
room_id: String,
event_id: String,
methods: Vec<String>,
) -> Result<Option<Arc<VerificationRequest>>, CryptoStoreError> {
let user_id = parse_user_id(user_id)?;
let user_id = parse_user_id(&user_id)?;
let event_id = EventId::parse(event_id)?;
let room_id = RoomId::parse(room_id)?;
@@ -1005,17 +1024,18 @@ impl OlmMachine {
/// supported in the `m.key.verification.request` event.
pub fn request_verification_with_device(
&self,
user_id: &str,
device_id: &str,
user_id: String,
device_id: String,
methods: Vec<String>,
) -> Result<Option<RequestVerificationResult>, CryptoStoreError> {
let user_id = parse_user_id(user_id)?;
let user_id = parse_user_id(&user_id)?;
let device_id = device_id.as_str().into();
let methods = methods.into_iter().map(VerificationMethod::from).collect();
Ok(
if let Some(device) =
self.runtime.block_on(self.inner.get_device(&user_id, device_id.into(), None))?
self.runtime.block_on(self.inner.get_device(&user_id, device_id, None))?
{
let (verification, request) =
self.runtime.block_on(device.request_verification_with_methods(methods));
@@ -1074,11 +1094,11 @@ impl OlmMachine {
/// verification.
///
/// * `flow_id` - The ID that uniquely identifies the verification flow.
pub fn get_verification(&self, user_id: &str, flow_id: &str) -> Option<Arc<Verification>> {
pub fn get_verification(&self, user_id: String, flow_id: String) -> Option<Arc<Verification>> {
let user_id = UserId::parse(user_id).ok()?;
self.inner
.get_verification(&user_id, flow_id)
.get_verification(&user_id, &flow_id)
.map(|v| Verification { inner: v, runtime: self.runtime.handle().to_owned() }.into())
}
@@ -1098,14 +1118,15 @@ impl OlmMachine {
/// [request_verification_with_device()]: #method.request_verification_with_device
pub fn start_sas_with_device(
&self,
user_id: &str,
device_id: &str,
user_id: String,
device_id: String,
) -> Result<Option<StartSasResult>, CryptoStoreError> {
let user_id = parse_user_id(user_id)?;
let user_id = parse_user_id(&user_id)?;
let device_id = device_id.as_str().into();
Ok(
if let Some(device) =
self.runtime.block_on(self.inner.get_device(&user_id, device_id.into(), None))?
self.runtime.block_on(self.inner.get_device(&user_id, device_id, None))?
{
let (sas, request) = self.runtime.block_on(device.start_verification())?;
@@ -1125,14 +1146,6 @@ impl OlmMachine {
Ok(self.runtime.block_on(self.inner.bootstrap_cross_signing(true))?.into())
}
/// Get the status of the private cross signing keys.
///
/// This can be used to check which private cross signing keys we have
/// stored locally.
pub fn cross_signing_status(&self) -> CrossSigningStatus {
self.runtime.block_on(self.inner.cross_signing_status()).into()
}
/// Export all our private cross signing keys.
///
/// The export will contain the seed for the ed25519 keys as a base64
@@ -1156,10 +1169,7 @@ impl OlmMachine {
Ok(())
}
}
#[uniffi::export]
impl OlmMachine {
/// Activate the given backup key to be used with the given backup version.
///
/// **Warning**: The caller needs to make sure that the given `BackupKey` is

View File

@@ -7,11 +7,18 @@ namespace matrix_sdk_crypto_ffi {
string? passphrase,
ProgressListener progress_listener
);
[Throws=MigrationError]
void migrate_sessions(
SessionMigrationData data,
[ByRef] string path,
string? passphrase,
ProgressListener progress_listener
);
};
[Error]
interface MigrationError {
Generic(string error_message);
Generic(string error_message);
};
callback interface Logger {
@@ -64,11 +71,6 @@ interface DecryptionError {
Store(string error);
};
dictionary DeviceLists {
sequence<string> changed;
sequence<string> left;
};
dictionary KeysImportResult {
i64 imported;
i64 total;
@@ -110,12 +112,6 @@ interface UserIdentity {
);
};
dictionary CrossSigningStatus {
boolean has_master;
boolean has_self_signing;
boolean has_user_signing;
};
dictionary CrossSigningKeyExport {
string? master_key;
string? self_signing_key;
@@ -166,12 +162,12 @@ interface Sas {
[Enum]
interface SasState {
Started();
Accepted();
KeysExchanged(sequence<i32>? emojis, sequence<i32> decimals);
Confirmed();
Done();
Cancelled(CancelInfo cancel_info);
Started();
Accepted();
KeysExchanged(sequence<i32>? emojis, sequence<i32> decimals);
Confirmed();
Done();
Cancelled(CancelInfo cancel_info);
};
callback interface SasListener {
@@ -206,12 +202,12 @@ interface QrCode {
[Enum]
interface QrCodeState {
Started();
Scanned();
Confirmed();
Reciprocated();
Done();
Cancelled(CancelInfo cancel_info);
Started();
Scanned();
Confirmed();
Reciprocated();
Done();
Cancelled(CancelInfo cancel_info);
};
callback interface QrCodeListener {
@@ -250,10 +246,10 @@ interface VerificationRequest {
[Enum]
interface VerificationRequestState {
Requested();
Ready(sequence<string> their_methods, sequence<string> our_methods);
Done();
Cancelled(CancelInfo cancel_info);
Requested();
Ready(sequence<string> their_methods, sequence<string> our_methods);
Done();
Cancelled(CancelInfo cancel_info);
};
callback interface VerificationRequestListener {
@@ -336,14 +332,6 @@ enum HistoryVisibility {
"WorldReadable",
};
dictionary EncryptionSettings {
EventEncryptionAlgorithm algorithm;
u64 rotation_period;
u64 rotation_period_msgs;
HistoryVisibility history_visibility;
boolean only_allow_trusted_devices;
};
interface OlmMachine {
[Throws=CryptoStoreError]
constructor(
@@ -353,11 +341,6 @@ interface OlmMachine {
string? passphrase
);
[Throws=CryptoStoreError]
string receive_sync_changes([ByRef] string events,
DeviceLists device_changes,
record<DOMString, i32> key_counts,
sequence<string>? unused_fallback_keys);
[Throws=CryptoStoreError]
sequence<Request> outgoing_requests();
[Throws=CryptoStoreError]
@@ -367,11 +350,6 @@ interface OlmMachine {
[ByRef] string response
);
[Throws=DecryptionError]
DecryptedEvent decrypt_room_event([ByRef] string event, [ByRef] string room_id, boolean handle_verificaton_events);
[Throws=CryptoStoreError]
string encrypt([ByRef] string room_id, [ByRef] string event_type, [ByRef] string content);
[Throws=CryptoStoreError]
UserIdentity? get_identity([ByRef] string user_id, u32 timeout);
[Throws=SignatureError]
@@ -385,56 +363,6 @@ interface OlmMachine {
[Throws=CryptoStoreError]
sequence<Device> get_user_devices([ByRef] string user_id, u32 timeout);
[Throws=CryptoStoreError]
boolean is_user_tracked([ByRef] string user_id);
[Throws=CryptoStoreError]
void update_tracked_users(sequence<string> users);
[Throws=CryptoStoreError]
Request? get_missing_sessions(sequence<string> users);
[Throws=CryptoStoreError]
sequence<Request> share_room_key(
[ByRef] string room_id,
sequence<string> users,
EncryptionSettings settings
);
[Throws=CryptoStoreError]
void receive_unencrypted_verification_event([ByRef] string event, [ByRef] string room_id);
[Throws=CryptoStoreError]
void receive_verification_event([ByRef] string event, [ByRef] string room_id);
sequence<VerificationRequest> get_verification_requests([ByRef] string user_id);
VerificationRequest? get_verification_request([ByRef] string user_id, [ByRef] string flow_id);
Verification? get_verification([ByRef] string user_id, [ByRef] string flow_id);
[Throws=CryptoStoreError]
VerificationRequest? request_verification(
[ByRef] string user_id,
[ByRef] string room_id,
[ByRef] string event_id,
sequence<string> methods
);
[Throws=CryptoStoreError]
string? verification_request_content(
[ByRef] string user_id,
sequence<string> methods
);
[Throws=CryptoStoreError]
RequestVerificationResult? request_self_verification(sequence<string> methods);
[Throws=CryptoStoreError]
RequestVerificationResult? request_verification_with_device(
[ByRef] string user_id,
[ByRef] string device_id,
sequence<string> methods
);
[Throws=CryptoStoreError]
StartSasResult? start_sas_with_device([ByRef] string user_id, [ByRef] string device_id);
[Throws=DecryptionError]
KeyRequestPair request_room_key([ByRef] string event, [ByRef] string room_id);
[Throws=CryptoStoreError]
string export_room_keys([ByRef] string passphrase, i32 rounds);
[Throws=KeyImportError]
KeysImportResult import_room_keys(
[ByRef] string keys,
@@ -446,15 +374,7 @@ interface OlmMachine {
[ByRef] string keys,
ProgressListener progress_listener
);
[Throws=CryptoStoreError]
void discard_room_key([ByRef] string room_id);
CrossSigningStatus cross_signing_status();
[Throws=CryptoStoreError]
BootstrapCrossSigningResult bootstrap_cross_signing();
CrossSigningKeyExport? export_cross_signing_keys();
[Throws=SecretImportError]
void import_cross_signing_keys(CrossSigningKeyExport export);
[Throws=CryptoStoreError]
boolean is_identity_verified([ByRef] string user_id);
@@ -511,6 +431,16 @@ dictionary MigrationData {
sequence<string> tracked_users;
};
dictionary SessionMigrationData {
string user_id;
string device_id;
string curve25519_key;
string ed25519_key;
sequence<PickledSession> sessions;
sequence<PickledInboundGroupSession> inbound_group_sessions;
sequence<u8> pickle_key;
};
dictionary PickledAccount {
string user_id;
string device_id;

View File

@@ -112,7 +112,7 @@ impl From<ToDeviceRequest> for OutgoingVerificationRequest {
}
}
#[derive(Debug, uniffi::Enum)]
#[derive(Debug)]
pub enum Request {
ToDevice { request_id: String, event_type: String, body: String },
KeysUpload { request_id: String, body: String },
@@ -231,6 +231,7 @@ pub enum RequestType {
RoomMessage,
}
#[derive(uniffi::Record)]
pub struct DeviceLists {
pub changed: Vec<String>,
pub left: Vec<String>,

View File

@@ -1,61 +0,0 @@
# configuration file for git-cliff (0.1.0)
[changelog]
# changelog header
header = """
# Matrix SDK Crypto JavaScript Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://tera.netlify.app/docs/#introduction
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | filter(attribute="scope", value="crypto-js") | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
"""
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/issues/${2}))"},
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "Features"},
{ message = "^fix", group = "Bug Fixes"},
{ message = "^test", group = "Testing"},
{ message = "^doc", group = "Documentation"},
{ message = "^refactor", group = "Refactoring"},
{ message = "^ci", group = "Continuous Integration"},
{ message = "^chore", group = "Miscellaneous Tasks"},
{ body = ".*security", group = "Security"},
]
# filter out the commits that are not matched by commit parsers
filter_commits = false
# glob pattern for matching git tags
tag_pattern = "v[0-9]*"
# regex for skipping tags
skip_tags = ""
# regex for ignoring tags
ignore_tags = ""
# sort the tags chronologically
date_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"

View File

@@ -17,9 +17,6 @@
#![warn(missing_docs, missing_debug_implementations)]
// triggered by wasm_bindgen code
#![allow(clippy::drop_non_drop)]
// Triggers false positives.
// See <https://github.com/rust-lang/rust-clippy/issues/10319>.
#![allow(clippy::extra_unused_type_parameters)]
pub mod attachment;
pub mod device;

View File

@@ -5,4 +5,3 @@ build.rs
*.node
*.tgz
tsconfig.json
cliff.toml

View File

@@ -26,6 +26,7 @@ tracing = ["dep:tracing-subscriber"]
matrix-sdk-crypto = { version = "0.6.0", path = "../../crates/matrix-sdk-crypto", features = ["js"] }
matrix-sdk-common = { version = "0.6.0", path = "../../crates/matrix-sdk-common", features = ["js"] }
matrix-sdk-sled = { version = "0.2.0", path = "../../crates/matrix-sdk-sled", default-features = false, features = ["crypto-store"] }
matrix-sdk-sqlite = { version = "0.1.0", path = "../../crates/matrix-sdk-sqlite", features = ["crypto-store"] }
ruma = { workspace = true, features = ["rand", "unstable-msc2677"] }
napi = { version = "2.9.1", default-features = false, features = ["napi6", "tokio_rt"] }
napi-derive = "2.9.1"

View File

@@ -1,61 +0,0 @@
# configuration file for git-cliff (0.1.0)
[changelog]
# changelog header
header = """
# Matrix SDK Crypto Node.js Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://tera.netlify.app/docs/#introduction
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | filter(attribute="scope", value="crypto-nodejs") | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {{ commit.id | truncate(length=7, end="") }}{% if commit.breaking %} [**breaking**] {% endif %}: {{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
"""
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/issues/${2}))"},
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "Features"},
{ message = "^fix", group = "Bug Fixes"},
{ message = "^test", group = "Testing"},
{ message = "^doc", group = "Documentation"},
{ message = "^refactor", group = "Refactoring"},
{ message = "^ci", group = "Continuous Integration"},
{ message = "^chore", group = "Miscellaneous Tasks"},
{ body = ".*security", group = "Security"},
]
# filter out the commits that are not matched by commit parsers
filter_commits = true
# glob pattern for matching git tags
tag_pattern = "v[0-9]*"
# regex for skipping tags
skip_tags = "v0.1.0-beta.1"
# regex for ignoring tags
ignore_tags = ""
# sort the tags chronologically
date_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"

View File

@@ -2,11 +2,12 @@
use std::{
collections::{BTreeMap, HashMap},
mem::ManuallyDrop,
ops::Deref,
sync::Arc,
};
use napi::bindgen_prelude::Either7;
use napi::bindgen_prelude::{within_runtime_if_available, Either7, ToNapiValue};
use napi_derive::*;
use ruma::{serde::Raw, DeviceKeyAlgorithm, OwnedTransactionId, UInt};
use serde_json::{value::RawValue, Value as JsonValue};
@@ -27,10 +28,21 @@ use crate::{
///
/// Using the `OlmMachine` when its state is `Closed` will panic.
enum OlmMachineInner {
Opened(matrix_sdk_crypto::OlmMachine),
Opened(ManuallyDrop<matrix_sdk_crypto::OlmMachine>),
Closed,
}
impl Drop for OlmMachineInner {
fn drop(&mut self) {
if let Self::Opened(machine) = self {
// SAFETY: `self` won't be used anymore after this `take`, so it's safe to do it
// here.
let machine = unsafe { ManuallyDrop::take(machine) };
within_runtime_if_available(move || drop(machine));
}
}
}
impl Deref for OlmMachineInner {
type Target = matrix_sdk_crypto::OlmMachine;
@@ -43,6 +55,18 @@ impl Deref for OlmMachineInner {
}
}
/// Represents the type of store an `OlmMachine` can use.
#[derive(Default)]
#[napi]
pub enum StoreType {
/// Use `matrix-sdk-sled`.
#[default]
Sled,
/// Use `matrix-sdk-sqlite`.
Sqlite,
}
/// State machine implementation of the Olm/Megolm encryption protocol
/// used for Matrix end to end encryption.
// #[napi(custom_finalize)]
@@ -87,40 +111,56 @@ impl OlmMachine {
device_id: &identifiers::DeviceId,
store_path: Option<String>,
mut store_passphrase: Option<String>,
store_type: Option<StoreType>,
) -> napi::Result<OlmMachine> {
let user_id = user_id.clone();
let device_id = device_id.clone();
let user_id = user_id.clone().inner;
let device_id = device_id.clone().inner;
let store = if let Some(store_path) = store_path {
Some(
matrix_sdk_sled::SledCryptoStore::open(store_path, store_passphrase.as_deref())
.await
.map(Arc::new)
.map_err(into_err)?,
)
} else {
None
};
store_passphrase.zeroize();
let user_id = user_id.as_ref();
let device_id = device_id.as_ref();
Ok(OlmMachine {
inner: OlmMachineInner::Opened(match store {
Some(store) => matrix_sdk_crypto::OlmMachine::with_store(
user_id.inner.as_ref(),
device_id.inner.as_ref(),
store,
)
.await
.map_err(into_err)?,
None => {
matrix_sdk_crypto::OlmMachine::new(
user_id.inner.as_ref(),
device_id.inner.as_ref(),
)
.await
inner: OlmMachineInner::Opened(ManuallyDrop::new(match store_path {
Some(store_path) => {
let machine = match store_type.unwrap_or_default() {
StoreType::Sled => {
matrix_sdk_crypto::OlmMachine::with_store(
user_id,
device_id,
matrix_sdk_sled::SledCryptoStore::open(
store_path,
store_passphrase.as_deref(),
)
.await
.map(Arc::new)
.map_err(into_err)?,
)
.await
}
StoreType::Sqlite => {
matrix_sdk_crypto::OlmMachine::with_store(
user_id,
device_id,
matrix_sdk_sqlite::SqliteCryptoStore::open(
store_path,
store_passphrase.as_deref(),
)
.await
.map(Arc::new)
.map_err(into_err)?,
)
.await
}
};
store_passphrase.zeroize();
machine.map_err(into_err)?
}
}),
None => matrix_sdk_crypto::OlmMachine::new(user_id, device_id).await,
})),
})
}

View File

@@ -14,11 +14,19 @@ const {
VerificationState,
CrossSigningStatus,
MaybeSignature,
StoreType,
} = require("../");
const path = require("path");
const os = require("os");
const fs = require("fs/promises");
describe("StoreType", () => {
test("has the correct variant values", () => {
expect(StoreType.Sled).toStrictEqual(0);
expect(StoreType.Sqlite).toStrictEqual(1);
});
});
describe(OlmMachine.name, () => {
test("cannot be instantiated with the constructor", () => {
expect(() => {
@@ -31,21 +39,39 @@ describe(OlmMachine.name, () => {
});
describe("can be instantiated with a store", () => {
test("with no passphrase", async () => {
const temp_directory = await fs.mkdtemp(path.join(os.tmpdir(), "matrix-sdk-crypto--"));
for (const [store_type, store_name] of [
[StoreType.Sled, "sled"],
[StoreType.Sqlite, "sqlite"],
[null, "default"],
]) {
test(`with no passphrase (store: ${store_name})`, async () => {
const temp_directory = await fs.mkdtemp(path.join(os.tmpdir(), "matrix-sdk-crypto--"));
expect(
await OlmMachine.initialize(new UserId("@foo:bar.org"), new DeviceId("baz"), temp_directory),
).toBeInstanceOf(OlmMachine);
});
expect(
await OlmMachine.initialize(
new UserId("@foo:bar.org"),
new DeviceId("baz"),
temp_directory,
null,
store_type,
),
).toBeInstanceOf(OlmMachine);
});
test("with a passphrase", async () => {
const temp_directory = await fs.mkdtemp(path.join(os.tmpdir(), "matrix-sdk-crypto--"));
test(`with a passphrase (store: ${store_name})`, async () => {
const temp_directory = await fs.mkdtemp(path.join(os.tmpdir(), "matrix-sdk-crypto--"));
expect(
await OlmMachine.initialize(new UserId("@foo:bar.org"), new DeviceId("baz"), temp_directory, "hello"),
).toBeInstanceOf(OlmMachine);
});
expect(
await OlmMachine.initialize(
new UserId("@foo:bar.org"),
new DeviceId("baz"),
temp_directory,
"hello",
store_type,
),
).toBeInstanceOf(OlmMachine);
});
}
});
const user = new UserId("@alice:example.org");

View File

@@ -18,10 +18,10 @@ uniffi = { workspace = true, features = ["build"] }
[dependencies]
anyhow = { workspace = true }
base64 = "0.21"
eyeball = { workspace = true }
eyeball-im = { workspace = true }
extension-trait = "1.0.1"
futures-core = "0.3.17"
futures-signals = { version = "0.3.30", default-features = false }
futures-util = { version = "0.3.17", default-features = false }
mime = "0.3.16"
# FIXME: we currently can't feature flag anything in the api.udl, therefore we must enforce experimental-sliding-sync being exposed here..
@@ -29,6 +29,7 @@ mime = "0.3.16"
once_cell = { workspace = true }
opentelemetry = { version = "0.18.0", features = ["rt-tokio"] }
opentelemetry-otlp = { version = "0.11.0", features = ["tokio", "reqwest-client", "http-proto"] }
ruma = { workspace = true, features = ["unstable-sanitize"] }
sanitize-filename-reader-friendly = "2.2.1"
serde_json = { workspace = true }
thiserror = { workspace = true }

View File

@@ -1,8 +1,7 @@
namespace matrix_sdk_ffi {};
/// Cancels on drop
interface StoppableSpawn {};
interface TaskHandle {};
[Error]
interface ClientError {
@@ -10,9 +9,7 @@ interface ClientError {
};
callback interface ClientDelegate {
void did_receive_sync_update();
void did_receive_auth_error(boolean is_soft_logout);
void did_update_restore_token();
};
dictionary RequiredState {
@@ -26,7 +23,7 @@ dictionary RoomSubscription {
};
dictionary UpdateSummary {
sequence<string> views;
sequence<string> lists;
sequence<string> rooms;
};
@@ -54,7 +51,7 @@ enum SlidingSyncMode {
"Selective",
};
callback interface SlidingSyncViewStateObserver {
callback interface SlidingSyncListStateObserver {
void did_receive_update(SlidingSyncState new_state);
};
@@ -66,56 +63,49 @@ interface RoomListEntry {
};
[Enum]
interface SlidingSyncViewRoomsListDiff {
Replace(sequence<RoomListEntry> values);
InsertAt(
u32 index,
RoomListEntry value
);
UpdateAt(
u32 index,
RoomListEntry value
);
RemoveAt(u32 index);
Move(
u32 old_index,
u32 new_index
);
Push(RoomListEntry value);
Pop();
interface SlidingSyncListRoomsListDiff {
Append(sequence<RoomListEntry> values);
Insert(u32 index, RoomListEntry value);
Set(u32 index, RoomListEntry value);
Remove(u32 index);
PushBack(RoomListEntry value);
PushFront(RoomListEntry value);
PopBack();
PopFront();
Clear();
Reset(sequence<RoomListEntry> values);
};
callback interface SlidingSyncViewRoomListObserver {
void did_receive_update(SlidingSyncViewRoomsListDiff diff);
callback interface SlidingSyncListRoomListObserver {
void did_receive_update(SlidingSyncListRoomsListDiff diff);
};
callback interface SlidingSyncViewRoomsCountObserver {
callback interface SlidingSyncListRoomsCountObserver {
void did_receive_update(u32 count);
};
callback interface SlidingSyncViewRoomItemsObserver {
callback interface SlidingSyncListRoomItemsObserver {
void did_receive_update();
};
interface SlidingSyncViewBuilder {
interface SlidingSyncListBuilder {
constructor();
[Self=ByArc]
SlidingSyncViewBuilder sync_mode(SlidingSyncMode mode);
SlidingSyncListBuilder sync_mode(SlidingSyncMode mode);
[Self=ByArc]
SlidingSyncViewBuilder send_updates_for_items(boolean enable);
SlidingSyncListBuilder send_updates_for_items(boolean enable);
[Throws=ClientError, Self=ByArc]
SlidingSyncView build();
SlidingSyncList build();
};
interface SlidingSyncView {
StoppableSpawn observe_room_list(SlidingSyncViewRoomListObserver observer);
StoppableSpawn observe_rooms_count(SlidingSyncViewRoomsCountObserver observer);
StoppableSpawn observe_state(SlidingSyncViewStateObserver observer);
StoppableSpawn observe_room_items(SlidingSyncViewRoomItemsObserver observer);
interface SlidingSyncList {
TaskHandle observe_room_list(SlidingSyncListRoomListObserver observer);
TaskHandle observe_rooms_count(SlidingSyncListRoomsCountObserver observer);
TaskHandle observe_state(SlidingSyncListStateObserver observer);
TaskHandle observe_room_items(SlidingSyncListRoomItemsObserver observer);
};
interface SlidingSyncRoom {
@@ -127,7 +117,7 @@ interface SlidingSyncRoom {
dictionary SlidingSyncSubscribeResult {
sequence<TimelineItem> items;
StoppableSpawn task_handle;
TaskHandle task_handle;
};
interface SlidingSync {
@@ -181,7 +171,10 @@ interface Client {
void set_display_name(string name);
[Throws=ClientError]
string avatar_url();
string? avatar_url();
[Throws=ClientError]
string? cached_avatar_url();
[Throws=ClientError]
string device_id();
@@ -217,7 +210,6 @@ dictionary Session {
string user_id;
string device_id;
string homeserver_url;
boolean is_soft_logout;
string? sliding_sync_proxy;
};

View File

@@ -2,7 +2,7 @@ use std::sync::{Arc, RwLock};
use futures_util::future::join3;
use matrix_sdk::{
ruma::{OwnedDeviceId, UserId},
ruma::{IdParseError, OwnedDeviceId, UserId},
Session,
};
use zeroize::Zeroize;
@@ -28,6 +28,8 @@ impl Drop for AuthenticationService {
pub enum AuthenticationError {
#[error("A successful call to configure_homeserver must be made first.")]
ClientMissing,
#[error("{message}")]
InvalidServerName { message: String },
#[error("The homeserver doesn't provide a trusted a sliding sync proxy in its well-known configuration.")]
SlidingSyncNotAvailable,
#[error("Login was successful but is missing a valid Session to configure the file store.")]
@@ -42,6 +44,12 @@ impl From<anyhow::Error> for AuthenticationError {
}
}
impl From<IdParseError> for AuthenticationError {
fn from(e: IdParseError) -> AuthenticationError {
AuthenticationError::InvalidServerName { message: e.to_string() }
}
}
#[derive(uniffi::Object)]
pub struct HomeserverLoginDetails {
url: String,
@@ -112,32 +120,47 @@ impl AuthenticationService {
/// Updates the service to authenticate with the homeserver for the
/// specified address.
pub fn configure_homeserver(&self, server_name: String) -> Result<(), AuthenticationError> {
pub fn configure_homeserver(
&self,
server_name_or_homeserver_url: String,
) -> Result<(), AuthenticationError> {
let mut builder = Arc::new(ClientBuilder::new()).base_path(self.base_path.clone());
if server_name.starts_with("http://") || server_name.starts_with("https://") {
builder = builder.homeserver_url(server_name)
} else {
builder = builder.server_name(server_name);
}
// Remove any URL scheme from the name to attempt discovery first.
let server_name = matrix_sdk::sanitize_server_name(&server_name_or_homeserver_url)
.map_err(AuthenticationError::from)?;
let client = builder.build().map_err(AuthenticationError::from)?;
builder = builder.server_name(server_name.to_string());
// Make sure there's a sliding sync proxy available one way or another.
let client = builder
.build()
.or_else(|e| {
if !server_name_or_homeserver_url.starts_with("http://")
&& !server_name_or_homeserver_url.starts_with("http://")
{
return Err(e);
}
// When discovery fails, fallback to the homeserver URL if supplied.
let mut builder = Arc::new(ClientBuilder::new()).base_path(self.base_path.clone());
builder = builder.homeserver_url(server_name_or_homeserver_url);
builder.build()
})
.map_err(AuthenticationError::from)?;
let details = RUNTIME.block_on(async { self.details_from_client(&client).await })?;
// Now we've verified that it's a valid homeserver, make sure
// there's a sliding sync proxy available one way or another.
if self.custom_sliding_sync_proxy.read().unwrap().is_none()
&& client.discovered_sliding_sync_proxy().is_none()
{
return Err(AuthenticationError::SlidingSyncNotAvailable);
}
RUNTIME.block_on(async move {
let details = Arc::new(self.details_from_client(&client).await?);
*self.client.write().unwrap() = Some(client);
*self.homeserver_details.write().unwrap() = Some(Arc::new(details));
*self.client.write().unwrap() = Some(client);
*self.homeserver_details.write().unwrap() = Some(details);
Ok(())
})
Ok(())
}
/// Performs a password login using the current homeserver.

View File

@@ -2,16 +2,11 @@ use std::sync::{Arc, RwLock};
use anyhow::{anyhow, Context};
use matrix_sdk::{
config::SyncSettings,
media::{MediaFormat, MediaRequest, MediaThumbnailSize},
ruma::{
api::client::{
account::whoami,
error::ErrorKind,
filter::{FilterDefinition, LazyLoadOptions, RoomEventFilter, RoomFilter},
media::get_content_thumbnail::v3::Method,
account::whoami, error::ErrorKind, media::get_content_thumbnail::v3::Method,
session::get_login_types,
sync::sync_events::v3::Filter,
},
events::{room::MediaSource, AnyToDeviceEvent},
serde::Raw,
@@ -19,12 +14,10 @@ use matrix_sdk::{
},
Client as MatrixClient, Error, LoopCtrl,
};
use tokio::sync::broadcast;
use tokio::sync::broadcast::{self, error::RecvError};
use tracing::{debug, warn};
use super::{
room::Room, session_verification::SessionVerificationController, ClientState, RUNTIME,
};
use super::{room::Room, session_verification::SessionVerificationController, RUNTIME};
impl std::ops::Deref for Client {
type Target = MatrixClient;
@@ -34,15 +27,12 @@ impl std::ops::Deref for Client {
}
pub trait ClientDelegate: Sync + Send {
fn did_receive_sync_update(&self);
fn did_receive_auth_error(&self, is_soft_logout: bool);
fn did_update_restore_token(&self);
}
#[derive(Clone)]
pub struct Client {
pub(crate) client: MatrixClient,
state: Arc<RwLock<ClientState>>,
delegate: Arc<RwLock<Option<Box<dyn ClientDelegate>>>>,
session_verification_controller:
Arc<matrix_sdk::locks::RwLock<Option<SessionVerificationController>>>,
@@ -54,7 +44,7 @@ pub struct Client {
}
impl Client {
pub fn new(client: MatrixClient, state: ClientState) -> Self {
pub fn new(client: MatrixClient) -> Self {
let session_verification_controller: Arc<
matrix_sdk::locks::RwLock<Option<SessionVerificationController>>,
> = Default::default();
@@ -73,14 +63,30 @@ impl Client {
let (sliding_sync_reset_broadcast_tx, _) = broadcast::channel(1);
Client {
let client = Client {
client,
state: Arc::new(RwLock::new(state)),
delegate: Arc::new(RwLock::new(None)),
session_verification_controller,
sliding_sync_proxy: Arc::new(RwLock::new(None)),
sliding_sync_reset_broadcast_tx,
}
};
let mut unknown_token_error_receiver = client.subscribe_to_unknown_token_errors();
let client_clone = client.clone();
RUNTIME.spawn(async move {
loop {
match unknown_token_error_receiver.recv().await {
Ok(unknown_token) => client_clone.process_unknown_token_error(unknown_token),
Err(receive_error) => {
if let RecvError::Closed = receive_error {
break;
}
}
}
}
});
client
}
/// Login using a username and password.
@@ -112,12 +118,9 @@ impl Client {
user_id,
device_id,
homeserver_url: _,
is_soft_logout,
sliding_sync_proxy,
} = session;
// update the client state
self.state.write().unwrap().is_soft_logout = is_soft_logout;
*self.sliding_sync_proxy.write().unwrap() = sliding_sync_proxy;
let session = matrix_sdk::Session {
@@ -184,8 +187,6 @@ impl Client {
let matrix_sdk::Session { access_token, refresh_token, user_id, device_id } =
self.client.session().context("Missing session")?;
let homeserver_url = self.client.homeserver().await.into();
let state = self.state.read().unwrap();
let is_soft_logout = state.is_soft_logout;
let sliding_sync_proxy = self.sliding_sync_proxy.read().unwrap().clone();
Ok(Session {
@@ -194,7 +195,6 @@ impl Client {
user_id: user_id.to_string(),
device_id: device_id.to_string(),
homeserver_url,
is_soft_logout,
sliding_sync_proxy,
})
})
@@ -224,11 +224,19 @@ impl Client {
})
}
pub fn avatar_url(&self) -> anyhow::Result<String> {
pub fn avatar_url(&self) -> anyhow::Result<Option<String>> {
let l = self.client.clone();
RUNTIME.block_on(async move {
let avatar_url = l.account().get_avatar_url().await?.context("No User ID found")?;
Ok(avatar_url.to_string())
let avatar_url = l.account().get_avatar_url().await?;
Ok(avatar_url.map(|u| u.to_string()))
})
}
pub fn cached_avatar_url(&self) -> anyhow::Result<Option<String>> {
let l = self.client.clone();
RUNTIME.block_on(async move {
let url = l.account().get_cached_avatar_url().await?;
Ok(url)
})
}
@@ -347,14 +355,6 @@ impl Client {
pub(crate) fn process_sync_error(&self, sync_error: Error) -> LoopCtrl {
let client_api_error_kind = sync_error.client_api_error_kind();
match client_api_error_kind {
Some(ErrorKind::UnknownToken { soft_logout }) => {
self.state.write().unwrap().is_soft_logout = *soft_logout;
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_update_restore_token();
delegate.did_receive_auth_error(*soft_logout);
}
LoopCtrl::Break
}
Some(ErrorKind::UnknownPos) => {
let _ = self.sliding_sync_reset_broadcast_tx.send(());
LoopCtrl::Continue
@@ -365,6 +365,12 @@ impl Client {
}
}
}
fn process_unknown_token_error(&self, unknown_token: matrix_sdk::UnknownToken) {
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_receive_auth_error(unknown_token.soft_logout);
}
}
}
#[uniffi::export]
@@ -374,76 +380,9 @@ impl Client {
RUNTIME.block_on(async move { self.async_homeserver().await })
}
/// Indication whether we've received a first sync response since
/// establishing the client (in memory)
pub fn has_first_synced(&self) -> bool {
self.state.read().unwrap().has_first_synced
}
/// Indication whether we are currently syncing
pub fn is_syncing(&self) -> bool {
self.state.read().unwrap().is_syncing
}
/// Flag indicating whether the session is in soft logout mode
pub fn is_soft_logout(&self) -> bool {
self.state.read().unwrap().is_soft_logout
}
pub fn rooms(&self) -> Vec<Arc<Room>> {
self.client.rooms().into_iter().map(|room| Arc::new(Room::new(room))).collect()
}
pub fn start_sync(&self, timeline_limit: Option<u16>) {
let client = self.client.clone();
let state = self.state.clone();
let delegate = self.delegate.clone();
let local_self = self.clone();
RUNTIME.spawn(async move {
let mut filter = FilterDefinition::default();
let mut room_filter = RoomFilter::default();
let mut event_filter = RoomEventFilter::default();
let mut timeline_filter = RoomEventFilter::default();
event_filter.lazy_load_options =
LazyLoadOptions::Enabled { include_redundant_members: false };
room_filter.state = event_filter;
filter.room = room_filter;
timeline_filter.limit = timeline_limit.map(|limit| limit.into());
filter.room.timeline = timeline_filter;
let filter_id = client.get_or_upload_filter("sync", filter).await.unwrap();
let sync_settings = SyncSettings::new().filter(Filter::FilterId(filter_id));
client
.sync_with_result_callback(sync_settings, |result| async {
Ok(if result.is_ok() {
if !state.read().unwrap().has_first_synced {
state.write().unwrap().has_first_synced = true;
}
if state.read().unwrap().should_stop_syncing {
state.write().unwrap().is_syncing = false;
return Ok(LoopCtrl::Break);
} else if !state.read().unwrap().is_syncing {
state.write().unwrap().is_syncing = true;
}
if let Some(delegate) = &*delegate.read().unwrap() {
delegate.did_receive_sync_update()
}
LoopCtrl::Continue
} else {
local_self.process_sync_error(result.err().unwrap())
})
})
.await
.unwrap();
});
}
}
pub struct Session {
@@ -461,7 +400,6 @@ pub struct Session {
// FFI-only fields (for now)
pub homeserver_url: String,
pub is_soft_logout: bool,
pub sliding_sync_proxy: Option<String>,
}

View File

@@ -11,7 +11,7 @@ use matrix_sdk::{
use sanitize_filename_reader_friendly::sanitize;
use zeroize::Zeroizing;
use super::{client::Client, ClientState, RUNTIME};
use super::{client::Client, RUNTIME};
use crate::helpers::unwrap_or_clone_arc;
#[derive(Clone)]
@@ -135,7 +135,7 @@ impl ClientBuilder {
RUNTIME.block_on(async move {
let client = inner_builder.build().await?;
let c = Client::new(client, ClientState::default());
let c = Client::new(client);
c.set_sliding_sync_proxy(builder.sliding_sync_proxy);
Ok(Arc::new(c))
})

View File

@@ -1,9 +1,6 @@
// TODO: target-os conditional would be good.
#![allow(unused_qualifications, clippy::new_without_default)]
// Triggers false positives.
// See <https://github.com/rust-lang/rust-clippy/issues/10319>.
#![allow(clippy::extra_unused_type_parameters)]
macro_rules! unwrap_or_clone_arc_into_variant {
(
@@ -54,14 +51,6 @@ pub use self::{
timeline::*,
};
#[derive(Default, Debug)]
pub struct ClientState {
has_first_synced: bool,
is_syncing: bool,
should_stop_syncing: bool,
is_soft_logout: bool,
}
#[derive(thiserror::Error, Debug)]
pub enum ClientError {
#[error("client error: {msg}")]
@@ -88,9 +77,9 @@ mod uniffi_types {
room::{Membership, MembershipState, Room, RoomMember},
session_verification::{SessionVerificationController, SessionVerificationEmoji},
sliding_sync::{
RequiredState, RoomListEntry, SlidingSync, SlidingSyncBuilder,
SlidingSyncRequestListFilters, SlidingSyncRoom, SlidingSyncView,
SlidingSyncViewBuilder, StoppableSpawn, UnreadNotificationsCount,
RequiredState, RoomListEntry, SlidingSync, SlidingSyncBuilder, SlidingSyncList,
SlidingSyncListBuilder, SlidingSyncRequestListFilters, SlidingSyncRoom, TaskHandle,
UnreadNotificationsCount,
},
timeline::{
AudioInfo, AudioMessageContent, EmoteMessageContent, EncryptedMessage, EventSendState,

View File

@@ -39,7 +39,7 @@ pub struct Room {
timeline: TimelineLock,
}
#[derive(Clone, uniffi::Enum)]
#[derive(Clone)]
pub enum MembershipState {
/// The user is banned.
Ban,

View File

@@ -1,10 +1,8 @@
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc, RwLock,
};
use std::sync::{Arc, RwLock};
use anyhow::Context;
use futures_signals::{signal::SignalExt, signal_vec::VecDiff};
use eyeball::Observable;
use eyeball_im::VectorDiff;
use futures_util::{future::join, pin_mut, StreamExt};
use matrix_sdk::ruma::{
api::client::sync::sync_events::{
@@ -19,7 +17,7 @@ pub use matrix_sdk::{
SlidingSyncBuilder as MatrixSlidingSyncBuilder, SlidingSyncMode, SlidingSyncState,
};
use tokio::{sync::broadcast::error::RecvError, task::JoinHandle};
use tracing::{debug, error, trace, warn};
use tracing::{debug, error, warn};
use url::Url;
use super::{Client, Room, RUNTIME};
@@ -28,49 +26,44 @@ use crate::{
TimelineItem, TimelineListener,
};
type StoppableSpawnCallback = Box<dyn FnOnce() + Send + Sync>;
type TaskHandleFinalizer = Box<dyn FnOnce() + Send + Sync>;
pub struct StoppableSpawn {
handle: Option<JoinHandle<()>>,
callback: RwLock<Option<StoppableSpawnCallback>>,
pub struct TaskHandle {
handle: JoinHandle<()>,
finalizer: RwLock<Option<TaskHandleFinalizer>>,
}
impl StoppableSpawn {
fn with_handle(handle: JoinHandle<()>) -> StoppableSpawn {
StoppableSpawn { handle: Some(handle), callback: Default::default() }
}
fn with_callback(callback: StoppableSpawnCallback) -> StoppableSpawn {
StoppableSpawn { handle: Default::default(), callback: RwLock::new(Some(callback)) }
impl TaskHandle {
// Create a new task handle.
fn new(handle: JoinHandle<()>) -> Self {
Self { handle, finalizer: RwLock::new(None) }
}
fn set_callback(&mut self, f: StoppableSpawnCallback) {
*self.callback.write().unwrap() = Some(f)
}
}
impl From<JoinHandle<()>> for StoppableSpawn {
fn from(value: JoinHandle<()>) -> Self {
StoppableSpawn::with_handle(value)
/// Define a function that will run after the handle has been aborted.
fn set_finalizer(&mut self, finalizer: TaskHandleFinalizer) {
*self.finalizer.write().unwrap() = Some(finalizer);
}
}
#[uniffi::export]
impl StoppableSpawn {
impl TaskHandle {
pub fn cancel(&self) {
debug!("stoppable.cancel() called");
if let Some(handle) = &self.handle {
handle.abort();
}
if let Some(callback) = self.callback.write().unwrap().take() {
callback();
self.handle.abort();
if let Some(finalizer) = self.finalizer.write().unwrap().take() {
finalizer();
}
}
/// Check whether the handle is finished.
pub fn is_finished(&self) -> bool {
self.handle.as_ref().map(|h| h.is_finished()).unwrap_or_default()
self.handle.is_finished()
}
}
impl Drop for StoppableSpawn {
impl Drop for TaskHandle {
fn drop(&mut self) {
self.cancel();
}
@@ -166,6 +159,7 @@ impl SlidingSyncRoom {
listener: Box<dyn TimelineListener>,
) -> anyhow::Result<SlidingSyncSubscribeResult> {
let (items, stoppable_spawn) = self.add_timeline_listener_inner(listener)?;
Ok(SlidingSyncSubscribeResult { items, task_handle: Arc::new(stoppable_spawn) })
}
@@ -176,16 +170,19 @@ impl SlidingSyncRoom {
) -> anyhow::Result<SlidingSyncSubscribeResult> {
let (items, mut stoppable_spawn) = self.add_timeline_listener_inner(listener)?;
let room_id = self.inner.room_id().clone();
self.runner.subscribe(room_id.clone(), settings.map(Into::into));
let runner = self.runner.clone();
stoppable_spawn.set_callback(Box::new(move || runner.unsubscribe(room_id)));
stoppable_spawn.set_finalizer(Box::new(move || runner.unsubscribe(room_id)));
Ok(SlidingSyncSubscribeResult { items, task_handle: Arc::new(stoppable_spawn) })
}
fn add_timeline_listener_inner(
&self,
listener: Box<dyn TimelineListener>,
) -> anyhow::Result<(Vec<Arc<TimelineItem>>, StoppableSpawn)> {
) -> anyhow::Result<(Vec<Arc<TimelineItem>>, TaskHandle)> {
let mut timeline_lock = self.timeline.write().unwrap();
let timeline = match &*timeline_lock {
Some(timeline) => timeline,
@@ -230,25 +227,25 @@ impl SlidingSyncRoom {
};
let items = timeline_items.into_iter().map(TimelineItem::from_arc).collect();
let task_handle = StoppableSpawn::with_handle(RUNTIME.spawn(async move {
let task_handle = TaskHandle::new(RUNTIME.spawn(async move {
join(handle_events, handle_sliding_sync_reset).await;
}));
Ok((items, task_handle))
}
}
pub struct SlidingSyncSubscribeResult {
pub items: Vec<Arc<TimelineItem>>,
pub task_handle: Arc<StoppableSpawn>,
pub task_handle: Arc<TaskHandle>,
}
pub struct UpdateSummary {
/// The views (according to their name), which have seen an update
pub views: Vec<String>,
/// The lists (according to their name), which have seen an update
pub lists: Vec<String>,
pub rooms: Vec<String>,
}
#[derive(uniffi::Record)]
pub struct RequiredState {
pub key: String,
pub value: String,
@@ -271,56 +268,54 @@ impl From<RoomSubscription> for RumaRoomSubscription {
}
impl From<matrix_sdk::UpdateSummary> for UpdateSummary {
fn from(other: matrix_sdk::UpdateSummary) -> UpdateSummary {
UpdateSummary {
views: other.views,
fn from(other: matrix_sdk::UpdateSummary) -> Self {
Self {
lists: other.lists,
rooms: other.rooms.into_iter().map(|r| r.as_str().to_owned()).collect(),
}
}
}
pub enum SlidingSyncViewRoomsListDiff {
Replace { values: Vec<RoomListEntry> },
InsertAt { index: u32, value: RoomListEntry },
UpdateAt { index: u32, value: RoomListEntry },
RemoveAt { index: u32 },
Move { old_index: u32, new_index: u32 },
Push { value: RoomListEntry },
Pop, // removes the last item
Clear, // clears the list
pub enum SlidingSyncListRoomsListDiff {
Append { values: Vec<RoomListEntry> },
Insert { index: u32, value: RoomListEntry },
Set { index: u32, value: RoomListEntry },
Remove { index: u32 },
PushBack { value: RoomListEntry },
PushFront { value: RoomListEntry },
PopBack,
PopFront,
Clear,
Reset { values: Vec<RoomListEntry> },
}
impl From<VecDiff<MatrixRoomEntry>> for SlidingSyncViewRoomsListDiff {
fn from(other: VecDiff<MatrixRoomEntry>) -> Self {
impl From<VectorDiff<MatrixRoomEntry>> for SlidingSyncListRoomsListDiff {
fn from(other: VectorDiff<MatrixRoomEntry>) -> Self {
match other {
VecDiff::Replace { values } => SlidingSyncViewRoomsListDiff::Replace {
values: values.into_iter().map(|e| (&e).into()).collect(),
},
VecDiff::InsertAt { index, value } => SlidingSyncViewRoomsListDiff::InsertAt {
index: index as u32,
value: (&value).into(),
},
VecDiff::UpdateAt { index, value } => SlidingSyncViewRoomsListDiff::UpdateAt {
index: index as u32,
value: (&value).into(),
},
VecDiff::RemoveAt { index } => {
SlidingSyncViewRoomsListDiff::RemoveAt { index: index as u32 }
VectorDiff::Append { values } => {
Self::Append { values: values.into_iter().map(|e| (&e).into()).collect() }
}
VecDiff::Move { old_index, new_index } => SlidingSyncViewRoomsListDiff::Move {
old_index: old_index as u32,
new_index: new_index as u32,
},
VecDiff::Push { value } => {
SlidingSyncViewRoomsListDiff::Push { value: (&value).into() }
VectorDiff::Insert { index, value } => {
Self::Insert { index: index as u32, value: (&value).into() }
}
VectorDiff::Set { index, value } => {
Self::Set { index: index as u32, value: (&value).into() }
}
VectorDiff::Remove { index } => Self::Remove { index: index as u32 },
VectorDiff::PushBack { value } => Self::PushBack { value: (&value).into() },
VectorDiff::PushFront { value } => Self::PushFront { value: (&value).into() },
VectorDiff::PopBack => Self::PopBack,
VectorDiff::PopFront => Self::PopFront,
VectorDiff::Clear => Self::Clear,
VectorDiff::Reset { values } => {
warn!("Room list subscriber lagged behind and was reset");
Self::Reset { values: values.into_iter().map(|e| (&e).into()).collect() }
}
VecDiff::Pop {} => SlidingSyncViewRoomsListDiff::Pop,
VecDiff::Clear {} => SlidingSyncViewRoomsListDiff::Clear,
}
}
}
#[derive(Clone, Debug, uniffi::Enum)]
#[derive(Clone, Debug)]
pub enum RoomListEntry {
Empty,
Invalidated { room_id: String },
@@ -330,34 +325,32 @@ pub enum RoomListEntry {
impl From<&MatrixRoomEntry> for RoomListEntry {
fn from(other: &MatrixRoomEntry) -> Self {
match other {
MatrixRoomEntry::Empty => RoomListEntry::Empty,
MatrixRoomEntry::Filled(b) => RoomListEntry::Filled { room_id: b.to_string() },
MatrixRoomEntry::Invalidated(b) => {
RoomListEntry::Invalidated { room_id: b.to_string() }
}
MatrixRoomEntry::Empty => Self::Empty,
MatrixRoomEntry::Filled(b) => Self::Filled { room_id: b.to_string() },
MatrixRoomEntry::Invalidated(b) => Self::Invalidated { room_id: b.to_string() },
}
}
}
pub trait SlidingSyncViewRoomItemsObserver: Sync + Send {
pub trait SlidingSyncListRoomItemsObserver: Sync + Send {
fn did_receive_update(&self);
}
pub trait SlidingSyncViewRoomListObserver: Sync + Send {
fn did_receive_update(&self, diff: SlidingSyncViewRoomsListDiff);
pub trait SlidingSyncListRoomListObserver: Sync + Send {
fn did_receive_update(&self, diff: SlidingSyncListRoomsListDiff);
}
pub trait SlidingSyncViewRoomsCountObserver: Sync + Send {
pub trait SlidingSyncListRoomsCountObserver: Sync + Send {
fn did_receive_update(&self, new_count: u32);
}
pub trait SlidingSyncViewStateObserver: Sync + Send {
pub trait SlidingSyncListStateObserver: Sync + Send {
fn did_receive_update(&self, new_state: SlidingSyncState);
}
#[derive(Clone)]
pub struct SlidingSyncViewBuilder {
inner: matrix_sdk::SlidingSyncViewBuilder,
pub struct SlidingSyncListBuilder {
inner: matrix_sdk::SlidingSyncListBuilder,
}
#[derive(uniffi::Record)]
@@ -389,15 +382,16 @@ impl From<SlidingSyncRequestListFilters> for SyncRequestListFilters {
tags,
not_tags,
} = value;
assign!(SyncRequestListFilters::default(), {
is_dm, spaces, is_encrypted, is_invite, is_tombstoned, room_types, not_room_types, room_name_like, tags, not_tags,
})
}
}
impl SlidingSyncViewBuilder {
impl SlidingSyncListBuilder {
pub fn new() -> Self {
Self { inner: matrix_sdk::SlidingSyncView::builder() }
Self { inner: matrix_sdk::SlidingSyncList::builder() }
}
pub fn sync_mode(self: Arc<Self>, mode: SlidingSyncMode) -> Arc<Self> {
@@ -418,14 +412,14 @@ impl SlidingSyncViewBuilder {
Arc::new(builder)
}
pub fn build(self: Arc<Self>) -> anyhow::Result<Arc<SlidingSyncView>> {
pub fn build(self: Arc<Self>) -> anyhow::Result<Arc<SlidingSyncList>> {
let builder = unwrap_or_clone_arc(self);
Ok(Arc::new(builder.inner.build()?.into()))
}
}
#[uniffi::export]
impl SlidingSyncViewBuilder {
impl SlidingSyncListBuilder {
pub fn sort(self: Arc<Self>, sort: Vec<String>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.inner = builder.inner.sort(sort);
@@ -502,24 +496,24 @@ impl SlidingSyncViewBuilder {
}
#[derive(Clone)]
pub struct SlidingSyncView {
inner: matrix_sdk::SlidingSyncView,
pub struct SlidingSyncList {
inner: matrix_sdk::SlidingSyncList,
}
impl From<matrix_sdk::SlidingSyncView> for SlidingSyncView {
fn from(inner: matrix_sdk::SlidingSyncView) -> Self {
SlidingSyncView { inner }
impl From<matrix_sdk::SlidingSyncList> for SlidingSyncList {
fn from(inner: matrix_sdk::SlidingSyncList) -> Self {
SlidingSyncList { inner }
}
}
impl SlidingSyncView {
impl SlidingSyncList {
pub fn observe_state(
&self,
observer: Box<dyn SlidingSyncViewStateObserver>,
) -> Arc<StoppableSpawn> {
observer: Box<dyn SlidingSyncListStateObserver>,
) -> Arc<TaskHandle> {
let mut state_stream = self.inner.state_stream();
Arc::new(StoppableSpawn::with_handle(RUNTIME.spawn(async move {
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
loop {
if let Some(new_state) = state_stream.next().await {
observer.did_receive_update(new_state);
@@ -530,11 +524,11 @@ impl SlidingSyncView {
pub fn observe_room_list(
&self,
observer: Box<dyn SlidingSyncViewRoomListObserver>,
) -> Arc<StoppableSpawn> {
observer: Box<dyn SlidingSyncListRoomListObserver>,
) -> Arc<TaskHandle> {
let mut rooms_list_stream = self.inner.rooms_list_stream();
Arc::new(StoppableSpawn::with_handle(RUNTIME.spawn(async move {
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
loop {
if let Some(diff) = rooms_list_stream.next().await {
observer.did_receive_update(diff.into());
@@ -545,10 +539,11 @@ impl SlidingSyncView {
pub fn observe_room_items(
&self,
observer: Box<dyn SlidingSyncViewRoomItemsObserver>,
) -> Arc<StoppableSpawn> {
let mut rooms_updated = self.inner.rooms_updated_broadcaster.signal_cloned().to_stream();
Arc::new(StoppableSpawn::with_handle(RUNTIME.spawn(async move {
observer: Box<dyn SlidingSyncListRoomItemsObserver>,
) -> Arc<TaskHandle> {
let mut rooms_updated =
Observable::subscribe(&self.inner.rooms_updated_broadcast.read().unwrap());
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
loop {
if rooms_updated.next().await.is_some() {
observer.did_receive_update();
@@ -559,11 +554,11 @@ impl SlidingSyncView {
pub fn observe_rooms_count(
&self,
observer: Box<dyn SlidingSyncViewRoomsCountObserver>,
) -> Arc<StoppableSpawn> {
observer: Box<dyn SlidingSyncListRoomsCountObserver>,
) -> Arc<TaskHandle> {
let mut rooms_count_stream = self.inner.rooms_count_stream();
Arc::new(StoppableSpawn::with_handle(RUNTIME.spawn(async move {
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
loop {
if let Some(Some(new)) = rooms_count_stream.next().await {
observer.did_receive_update(new);
@@ -574,7 +569,7 @@ impl SlidingSyncView {
}
#[uniffi::export]
impl SlidingSyncView {
impl SlidingSyncList {
/// Get the current list of rooms
pub fn current_rooms_list(&self) -> Vec<RoomListEntry> {
self.inner.rooms_list()
@@ -608,17 +603,19 @@ impl SlidingSyncView {
/// The current timeline limit
pub fn get_timeline_limit(&self) -> Option<u32> {
self.inner.timeline_limit.get_cloned().map(|limit| u32::try_from(limit).unwrap_or_default())
(**self.inner.timeline_limit.read().unwrap())
.map(|limit| u32::try_from(limit).unwrap_or_default())
}
/// The current timeline limit
pub fn set_timeline_limit(&self, value: u32) {
self.inner.timeline_limit.set(Some(UInt::try_from(value).unwrap()))
let value = Some(UInt::try_from(value).unwrap());
Observable::set(&mut self.inner.timeline_limit.write().unwrap(), value);
}
/// Unset the current timeline limit
pub fn unset_timeline_limit(&self) {
self.inner.timeline_limit.set(None)
Observable::set(&mut self.inner.timeline_limit.write().unwrap(), None);
}
}
@@ -634,7 +631,7 @@ pub struct SlidingSync {
impl SlidingSync {
fn new(inner: matrix_sdk::SlidingSync, client: Client) -> Self {
SlidingSync { inner, client, observer: Default::default() }
Self { inner, client, observer: Default::default() }
}
pub fn set_observer(&self, observer: Option<Box<dyn SlidingSyncObserver>>) {
@@ -697,63 +694,55 @@ impl SlidingSync {
#[uniffi::export]
impl SlidingSync {
#[allow(clippy::significant_drop_in_scrutinee)]
pub fn get_view(&self, name: String) -> Option<Arc<SlidingSyncView>> {
self.inner.view(&name).map(|inner| Arc::new(SlidingSyncView { inner }))
pub fn get_list(&self, name: String) -> Option<Arc<SlidingSyncList>> {
self.inner.list(&name).map(|inner| Arc::new(SlidingSyncList { inner }))
}
pub fn add_view(&self, view: Arc<SlidingSyncView>) -> Option<Arc<SlidingSyncView>> {
self.inner.add_view(view.inner.clone()).map(|inner| Arc::new(SlidingSyncView { inner }))
pub fn add_list(&self, list: Arc<SlidingSyncList>) -> Option<Arc<SlidingSyncList>> {
self.inner.add_list(list.inner.clone()).map(|inner| Arc::new(SlidingSyncList { inner }))
}
pub fn pop_view(&self, name: String) -> Option<Arc<SlidingSyncView>> {
self.inner.pop_view(&name).map(|inner| Arc::new(SlidingSyncView { inner }))
pub fn pop_list(&self, name: String) -> Option<Arc<SlidingSyncList>> {
self.inner.pop_list(&name).map(|inner| Arc::new(SlidingSyncList { inner }))
}
pub fn add_common_extensions(&self) {
self.inner.add_common_extensions();
}
pub fn sync(&self) -> Arc<StoppableSpawn> {
pub fn sync(&self) -> Arc<TaskHandle> {
let inner = self.inner.clone();
let client = self.client.clone();
let observer = self.observer.clone();
let stop_loop = Arc::new(AtomicBool::new(false));
let remote_stopper = stop_loop.clone();
let stoppable = Arc::new(StoppableSpawn::with_callback(Box::new(move || {
remote_stopper.store(true, Ordering::Relaxed);
})));
RUNTIME.spawn(async move {
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
let stream = inner.stream();
pin_mut!(stream);
loop {
let update = match stream.next().await {
Some(Ok(u)) => u,
Some(Err(e)) => {
if client.process_sync_error(e) == LoopCtrl::Break {
let update_summary = match stream.next().await {
Some(Ok(update_summary)) => update_summary,
Some(Err(error)) => {
if client.process_sync_error(error) == LoopCtrl::Break {
warn!("loop was stopped by client error processing");
break;
} else {
continue;
}
}
None => {
warn!("Inner streaming loop ended unexpectedly");
break;
}
};
if let Some(ref observer) = *observer.read().unwrap() {
observer.did_receive_sync_update(update.into());
}
if stop_loop.load(Ordering::Relaxed) {
trace!("stopped sync loop after cancellation");
break;
observer.did_receive_sync_update(update_summary.into());
}
}
});
stoppable
})))
}
}
@@ -780,15 +769,15 @@ impl SlidingSyncBuilder {
#[uniffi::export]
impl SlidingSyncBuilder {
pub fn add_fullsync_view(self: Arc<Self>) -> Arc<Self> {
pub fn add_fullsync_list(self: Arc<Self>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.inner = builder.inner.add_fullsync_view();
builder.inner = builder.inner.add_fullsync_list();
Arc::new(builder)
}
pub fn no_views(self: Arc<Self>) -> Arc<Self> {
pub fn no_lists(self: Arc<Self>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.inner = builder.inner.no_views();
builder.inner = builder.inner.no_lists();
Arc::new(builder)
}
@@ -798,10 +787,10 @@ impl SlidingSyncBuilder {
Arc::new(builder)
}
pub fn add_view(self: Arc<Self>, v: Arc<SlidingSyncView>) -> Arc<Self> {
pub fn add_list(self: Arc<Self>, v: Arc<SlidingSyncList>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
let view = unwrap_or_clone_arc(v);
builder.inner = builder.inner.add_view(view.inner);
let list = unwrap_or_clone_arc(v);
builder.inner = builder.inner.add_list(list.inner);
Arc::new(builder)
}
@@ -852,7 +841,7 @@ impl Client {
pub fn full_sliding_sync(&self) -> anyhow::Result<Arc<SlidingSync>> {
RUNTIME.block_on(async move {
let builder = self.client.sliding_sync().await;
let inner = builder.add_fullsync_view().build().await?;
let inner = builder.add_fullsync_list().build().await?;
Ok(Arc::new(SlidingSync::new(inner, self.clone())))
})
}

View File

@@ -312,7 +312,7 @@ impl EventTimelineItem {
use matrix_sdk::room::timeline::EventTimelineItem::*;
match &self.0 {
Local(local_event) => Some((&local_event.send_state).into()),
Local(local_event) => Some(local_event.send_state().into()),
Remote(_) => None,
}
}

View File

@@ -23,19 +23,21 @@ qrcode = ["matrix-sdk-crypto?/qrcode"]
experimental-sliding-sync = ["ruma/unstable-msc3575"]
# helpers for testing features build upon this
testing = ["dep:http"]
testing = ["dep:http", "dep:matrix-sdk-test", "dep:assert_matches"]
[dependencies]
assert_matches = { version = "1.5.0", optional = true }
async-stream = { workspace = true }
async-trait = { workspace = true }
dashmap = { workspace = true }
eyeball = { workspace = true }
futures-core = "0.3.21"
futures-signals = { version = "0.3.30", default-features = false }
futures-util = { version = "0.3.21", default-features = false }
futures-util = { workspace = true }
http = { workspace = true, optional = true }
matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" }
matrix-sdk-crypto = { version = "0.6.0", path = "../matrix-sdk-crypto", optional = true }
matrix-sdk-store-encryption = { version = "0.2.0", path = "../matrix-sdk-store-encryption" }
matrix-sdk-test = { version = "0.6.0", path = "../../testing/matrix-sdk-test", optional = true }
once_cell = { workspace = true }
ruma = { workspace = true, features = ["canonical-json"] }
serde = { workspace = true, features = ["rc"] }
@@ -45,6 +47,7 @@ tracing = { workspace = true }
zeroize = { workspace = true, features = ["zeroize_derive"] }
[dev-dependencies]
assert_matches = "1.5.0"
assign = "1.1.1"
ctor = { workspace = true }
futures = { version = "0.3.21", default-features = false, features = ["executor"] }

View File

@@ -17,11 +17,12 @@ use std::{
borrow::Borrow,
collections::{BTreeMap, BTreeSet},
fmt,
sync::RwLockReadGuard as StdRwLockReadGuard,
};
#[cfg(feature = "e2e-encryption")]
use std::{ops::Deref, sync::Arc};
use futures_signals::signal::ReadOnlyMutable;
use eyeball::Observable;
use matrix_sdk_common::{instant::Instant, locks::RwLock};
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_crypto::{
@@ -62,11 +63,11 @@ use crate::{
error::Result,
rooms::{Room, RoomInfo, RoomType},
store::{
ambiguity_map::AmbiguityCache, Result as StoreResult, StateChanges, StateStoreExt, Store,
StoreConfig,
ambiguity_map::AmbiguityCache, DynStateStore, Result as StoreResult, StateChanges,
StateStoreDataKey, StateStoreDataValue, StateStoreExt, Store, StoreConfig,
},
sync::{JoinedRoom, LeftRoom, Rooms, SyncResponse, Timeline},
Session, SessionMeta, SessionTokens, StateStore,
Session, SessionMeta, SessionTokens,
};
/// A no IO Client implementation.
@@ -136,7 +137,7 @@ impl BaseClient {
/// If the client is currently logged in, this will return a
/// [`SessionTokens`] object which contains the access token and optional
/// refresh token. Otherwise it returns `None`.
pub fn session_tokens(&self) -> ReadOnlyMutable<Option<SessionTokens>> {
pub fn session_tokens(&self) -> StdRwLockReadGuard<'_, Observable<Option<SessionTokens>>> {
self.store.session_tokens()
}
@@ -174,7 +175,7 @@ impl BaseClient {
/// Get a reference to the store.
#[allow(unknown_lints, clippy::explicit_auto_deref)]
pub fn store(&self) -> &dyn StateStore {
pub fn store(&self) -> &DynStateStore {
&*self.store
}
@@ -1024,7 +1025,13 @@ impl BaseClient {
filter_name: &str,
response: &api::filter::create_filter::v3::Response,
) -> Result<()> {
Ok(self.store.save_filter(filter_name, &response.filter_id).await?)
Ok(self
.store
.set_kv_data(
StateStoreDataKey::Filter(filter_name),
StateStoreDataValue::Filter(response.filter_id.clone()),
)
.await?)
}
/// Get the filter id of a previously uploaded filter.
@@ -1039,7 +1046,13 @@ impl BaseClient {
///
/// [`receive_filter_upload`]: #method.receive_filter_upload
pub async fn get_filter(&self, filter_name: &str) -> StoreResult<Option<String>> {
self.store.get_filter(filter_name).await
let filter = self
.store
.get_kv_data(StateStoreDataKey::Filter(filter_name))
.await?
.map(|d| d.into_filter().expect("State store data not a filter"));
Ok(filter)
}
/// Get a to-device request that will share a room key with users in a room.

View File

@@ -25,7 +25,7 @@ use ruma::{
serde::Raw,
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UserId,
};
use serde::{Deserialize, Serialize};
use serde::Serialize;
/// A change in ambiguity of room members that an `m.room.member` event
/// triggers.
@@ -82,8 +82,7 @@ impl RawMemberEvent {
}
/// Wrapper around both MemberEvent-Types
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Debug)]
pub enum MemberEvent {
/// A member event from a room in joined or left state.
Sync(SyncRoomMemberEvent),

View File

@@ -43,7 +43,7 @@ pub use http;
pub use matrix_sdk_crypto as crypto;
pub use once_cell;
pub use rooms::{DisplayName, Room, RoomInfo, RoomMember, RoomType};
pub use store::{StateChanges, StateStore, StoreError};
pub use store::{StateChanges, StateStore, StateStoreDataKey, StateStoreDataValue, StoreError};
pub use utils::{
MinimalRoomMemberEvent, MinimalStateEvent, OriginalMinimalStateEvent, RedactedMinimalStateEvent,
};

View File

@@ -17,7 +17,10 @@ use std::sync::Arc;
use ruma::{
events::{
presence::PresenceEvent,
room::{member::MembershipState, power_levels::SyncRoomPowerLevelsEvent},
room::{
member::MembershipState,
power_levels::{PowerLevelAction, SyncRoomPowerLevelsEvent},
},
},
MxcUri, UserId,
};
@@ -101,6 +104,15 @@ impl RoomMember {
.unwrap_or_else(|| if self.is_room_creator { 100 } else { 0 })
}
/// Whether the given user can do the given action based on the power
/// levels.
pub fn can_do(&self, action: PowerLevelAction) -> bool {
(*self.power_levels)
.as_ref()
.map(|e| e.power_levels().user_can_do(self.user_id(), action))
.unwrap_or_else(|| self.is_room_creator)
}
/// Is the name that the member uses ambiguous in the room.
///
/// A name is considered to be ambiguous if at least one other member shares

View File

@@ -40,7 +40,7 @@ use tracing::debug;
use super::{BaseRoomInfo, DisplayName, RoomMember};
use crate::{
store::{Result as StoreResult, StateStore, StateStoreExt},
store::{DynStateStore, Result as StoreResult, StateStoreExt},
sync::UnreadNotificationsCount,
MinimalStateEvent,
};
@@ -52,7 +52,7 @@ pub struct Room {
room_id: Arc<RoomId>,
own_user_id: Arc<UserId>,
inner: Arc<SyncRwLock<RoomInfo>>,
store: Arc<dyn StateStore>,
store: Arc<DynStateStore>,
}
/// The room summary containing member counts and members that should be used to
@@ -83,7 +83,7 @@ pub enum RoomType {
impl Room {
pub(crate) fn new(
own_user_id: &UserId,
store: Arc<dyn StateStore>,
store: Arc<DynStateStore>,
room_id: &RoomId,
room_type: RoomType,
) -> Self {
@@ -93,7 +93,7 @@ impl Room {
pub(crate) fn restore(
own_user_id: &UserId,
store: Arc<dyn StateStore>,
store: Arc<DynStateStore>,
room_info: RoomInfo,
) -> Self {
Self {
@@ -515,21 +515,21 @@ pub struct RoomInfo {
/// The unique room id of the room.
pub(crate) room_id: Arc<RoomId>,
/// The type of the room.
pub(crate) room_type: RoomType,
room_type: RoomType,
/// The unread notifications counts.
pub(crate) notification_counts: UnreadNotificationsCount,
notification_counts: UnreadNotificationsCount,
/// The summary of this room.
pub(crate) summary: RoomSummary,
summary: RoomSummary,
/// Flag remembering if the room members are synced.
pub(crate) members_synced: bool,
members_synced: bool,
/// The prev batch of this room we received during the last sync.
pub(crate) last_prev_batch: Option<String>,
/// How much we know about this room.
#[serde(default = "SyncInfo::complete")] // see fn docs for why we use this default
pub(crate) sync_info: SyncInfo,
sync_info: SyncInfo,
/// Whether or not the encryption info was been synced.
#[serde(default = "encryption_state_default")] // see fn docs for why we use this default
pub(crate) encryption_state_synced: bool,
encryption_state_synced: bool,
/// Base room info which holds some basic event contents important for the
/// room state.
pub(crate) base_info: BaseRoomInfo,
@@ -811,7 +811,7 @@ mod test {
use super::*;
use crate::{
store::{MemoryStore, StateChanges},
store::{MemoryStore, StateChanges, StateStore},
MinimalStateEvent, OriginalMinimalStateEvent,
};

View File

@@ -1,12 +1,16 @@
use std::collections::BTreeMap;
#[cfg(feature = "e2e-encryption")]
use std::ops::Deref;
use ruma::api::client::sync::sync_events::{
v3::{self, Ephemeral},
v4,
};
#[cfg(feature = "e2e-encryption")]
use ruma::UserId;
use ruma::{
api::client::sync::sync_events::{
v3::{self, Ephemeral},
v4, DeviceLists,
},
DeviceKeyAlgorithm, UInt,
};
use tracing::{debug, info, instrument};
use super::BaseClient;
@@ -26,7 +30,7 @@ impl BaseClient {
/// * `response` - The response that we received after a successful sliding
/// sync.
#[instrument(skip_all, level = "trace")]
pub async fn process_sliding_sync(&self, response: v4::Response) -> Result<SyncResponse> {
pub async fn process_sliding_sync(&self, response: &v4::Response) -> Result<SyncResponse> {
#[allow(unused_variables)]
let v4::Response {
// FIXME not yet supported by sliding sync. see
@@ -39,6 +43,7 @@ impl BaseClient {
//presence,
..
} = response;
info!(rooms = rooms.len(), lists = lists.len(), extensions = !extensions.is_empty());
if rooms.is_empty() && extensions.is_empty() {
@@ -47,23 +52,37 @@ impl BaseClient {
return Ok(SyncResponse::default());
};
let v4::Extensions { to_device, e2ee, account_data, .. } = extensions;
let v4::Extensions { to_device, e2ee, account_data, receipts, .. } = extensions;
let to_device_events = to_device.map(|v4| v4.events).unwrap_or_default();
let to_device_events = to_device.as_ref().map(|v4| v4.events.clone()).unwrap_or_default();
// Destructure the single `None` of the E2EE extension into separate objects
// since that's what the OlmMachine API expects. Passing in the default
// empty maps and vecs for this is completely fine, since the OlmMachine
// since that's what the `OlmMachine` API expects. Passing in the default
// empty maps and vecs for this is completely fine, since the `OlmMachine`
// assumes empty maps/vecs mean no change in the one-time key counts.
// We declare default values that can be referenced hereinbelow. When we try to
// extract values from `e2ee`, that would be unfortunate to clone the
// value just to pass them (to remove them `e2ee`) as a reference later.
let device_one_time_keys_count = BTreeMap::<DeviceKeyAlgorithm, UInt>::default();
let device_unused_fallback_key_types = None;
let (device_lists, device_one_time_keys_count, device_unused_fallback_key_types) = e2ee
.as_ref()
.map(|e2ee| {
(
e2ee.device_lists,
e2ee.device_one_time_keys_count,
e2ee.device_unused_fallback_key_types,
e2ee.device_lists.clone(),
&e2ee.device_one_time_keys_count,
&e2ee.device_unused_fallback_key_types,
)
})
.unwrap_or_default();
.unwrap_or_else(|| {
(
DeviceLists::default(),
&device_one_time_keys_count,
&device_unused_fallback_key_types,
)
});
info!(
to_device_events = to_device_events.len(),
@@ -80,7 +99,7 @@ impl BaseClient {
self.preprocess_to_device_events(
to_device_events,
&device_lists,
&device_one_time_keys_count,
device_one_time_keys_count,
device_unused_fallback_key_types.as_deref(),
)
.await?
@@ -98,14 +117,14 @@ impl BaseClient {
let mut new_rooms = Rooms::default();
for (room_id, room_data) in rooms.into_iter() {
for (room_id, room_data) in rooms {
if !room_data.invite_state.is_empty() {
let invite_states = &room_data.invite_state;
let room = store.get_or_create_stripped_room(&room_id).await;
let room = store.get_or_create_stripped_room(room_id).await;
let mut room_info = room.clone_info();
room_info.mark_state_partially_synced();
if let Some(r) = store.get_room(&room_id) {
if let Some(r) = store.get_room(room_id) {
let mut room_info = r.clone_info();
room_info.mark_as_invited(); // FIXME: this might not be accurate
room_info.mark_state_partially_synced();
@@ -119,7 +138,7 @@ impl BaseClient {
v3::InvitedRoom::from(v3::InviteState::from(invite_states.clone())),
);
} else {
let room = store.get_or_create_room(&room_id, RoomType::Joined).await;
let room = store.get_or_create_room(room_id, RoomType::Joined).await;
let mut room_info = room.clone_info();
room_info.mark_as_joined(); // FIXME: this might not be accurate
room_info.mark_state_partially_synced();
@@ -141,20 +160,9 @@ impl BaseClient {
Default::default()
};
// FIXME not yet supported by sliding sync. see
// https://github.com/matrix-org/matrix-rust-sdk/issues/1014
// if let Some(event) =
// room_data.ephemeral.events.iter().find_map(|e| match e.deserialize() {
// Ok(AnySyncEphemeralRoomEvent::Receipt(event)) => Some(event.content),
// _ => None,
// })
// {
// changes.add_receipts(&room_id, event);
// }
let room_account_data = if let Some(inner_account_data) = &account_data {
if let Some(events) = inner_account_data.rooms.get(&room_id) {
self.handle_room_account_data(&room_id, events, &mut changes).await;
if let Some(events) = inner_account_data.rooms.get(room_id) {
self.handle_room_account_data(room_id, events, &mut changes).await;
Some(events.to_vec())
} else {
None
@@ -171,8 +179,8 @@ impl BaseClient {
.handle_timeline(
&room,
room_data.limited,
room_data.timeline,
room_data.prev_batch,
room_data.timeline.clone(),
room_data.prev_batch.clone(),
&push_rules,
&mut user_ids,
&mut room_info,
@@ -188,8 +196,8 @@ impl BaseClient {
// The room turned on encryption in this sync, we need
// to also get all the existing users and mark them for
// tracking.
let joined = store.get_joined_user_ids(&room_id).await?;
let invited = store.get_invited_user_ids(&room_id).await?;
let joined = store.get_joined_user_ids(room_id).await?;
let invited = store.get_invited_user_ids(room_id).await?;
let user_ids: Vec<&UserId> =
joined.iter().chain(&invited).map(Deref::deref).collect();
@@ -219,6 +227,15 @@ impl BaseClient {
}
}
// Process receipts now we have rooms
if let Some(receipts) = &receipts {
for (room_id, receipt_edu) in &receipts.rooms {
if let Ok(receipt_edu) = receipt_edu.deserialize() {
changes.add_receipts(room_id, receipt_edu.content);
}
}
}
// TODO remove this, we're processing account data events here again
// because we want to have the push rules in place before we process
// rooms and their events, but we want to create the rooms before we
@@ -246,7 +263,7 @@ impl BaseClient {
debug!("applied changes");
let device_one_time_keys_count =
device_one_time_keys_count.into_iter().map(|(k, v)| (k, v.into())).collect();
device_one_time_keys_count.iter().map(|(k, v)| (k.clone(), (*v).into())).collect();
Ok(SyncResponse {
rooms: new_rooms,
@@ -254,7 +271,7 @@ impl BaseClient {
notifications: changes.notifications,
// FIXME not yet supported by sliding sync.
presence: Default::default(),
account_data: account_data.map(|a| a.global).unwrap_or_default(),
account_data: account_data.as_ref().map(|a| a.global.clone()).unwrap_or_default(),
to_device_events,
device_lists,
device_one_time_keys_count,

View File

@@ -23,15 +23,12 @@ use ruma::{
};
use tracing::trace;
use super::{Result, StateChanges};
use crate::{
deserialized_responses::{AmbiguityChange, RawMemberEvent},
StateStore,
};
use super::{DynStateStore, Result, StateChanges};
use crate::deserialized_responses::{AmbiguityChange, RawMemberEvent};
#[derive(Debug)]
pub(crate) struct AmbiguityCache {
pub store: Arc<dyn StateStore>,
pub store: Arc<DynStateStore>,
pub cache: BTreeMap<OwnedRoomId, BTreeMap<String, BTreeSet<OwnedUserId>>>,
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
}
@@ -72,7 +69,7 @@ impl AmbiguityMap {
}
impl AmbiguityCache {
pub fn new(store: Arc<dyn StateStore>) -> Self {
pub fn new(store: Arc<DynStateStore>) -> Self {
Self { store, cache: BTreeMap::new(), changes: BTreeMap::new() }
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -37,7 +37,10 @@ use ruma::{
use tracing::{debug, info, warn};
use super::{Result, RoomInfo, StateChanges, StateStore, StoreError};
use crate::{deserialized_responses::RawMemberEvent, media::MediaRequest, MinimalRoomMemberEvent};
use crate::{
deserialized_responses::RawMemberEvent, media::MediaRequest, MinimalRoomMemberEvent,
StateStoreDataKey, StateStoreDataValue,
};
/// In-Memory, non-persistent implementation of the `StateStore`
///
@@ -45,6 +48,7 @@ use crate::{deserialized_responses::RawMemberEvent, media::MediaRequest, Minimal
#[allow(clippy::type_complexity)]
#[derive(Debug, Clone)]
pub struct MemoryStore {
user_avatar_url: Arc<DashMap<String, String>>,
sync_token: Arc<RwLock<Option<String>>>,
filters: Arc<DashMap<String, String>>,
account_data: Arc<DashMap<GlobalAccountDataEventType, Raw<AnyGlobalAccountDataEvent>>>,
@@ -92,6 +96,7 @@ impl MemoryStore {
/// Create a new empty MemoryStore
pub fn new() -> Self {
Self {
user_avatar_url: Default::default(),
sync_token: Default::default(),
filters: Default::default(),
account_data: Default::default(),
@@ -119,18 +124,61 @@ impl MemoryStore {
}
}
async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<()> {
self.filters.insert(filter_name.to_owned(), filter_id.to_owned());
async fn get_kv_data(&self, key: StateStoreDataKey<'_>) -> Result<Option<StateStoreDataValue>> {
match key {
StateStoreDataKey::SyncToken => {
Ok(self.sync_token.read().unwrap().clone().map(StateStoreDataValue::SyncToken))
}
StateStoreDataKey::Filter(filter_name) => Ok(self
.filters
.get(filter_name)
.map(|f| StateStoreDataValue::Filter(f.value().clone()))),
StateStoreDataKey::UserAvatarUrl(user_id) => Ok(self
.user_avatar_url
.get(user_id.as_str())
.map(|u| StateStoreDataValue::UserAvatarUrl(u.value().clone()))),
}
}
async fn set_kv_data(
&self,
key: StateStoreDataKey<'_>,
value: StateStoreDataValue,
) -> Result<()> {
match key {
StateStoreDataKey::SyncToken => {
*self.sync_token.write().unwrap() =
Some(value.into_sync_token().expect("Session data not a sync token"))
}
StateStoreDataKey::Filter(filter_name) => {
self.filters.insert(
filter_name.to_owned(),
value.into_filter().expect("Session data not a filter"),
);
}
StateStoreDataKey::UserAvatarUrl(user_id) => {
self.filters.insert(
user_id.to_string(),
value.into_user_avatar_url().expect("Session data not a user avatar url"),
);
}
}
Ok(())
}
async fn get_filter(&self, filter_name: &str) -> Result<Option<String>> {
Ok(self.filters.get(filter_name).map(|f| f.to_string()))
}
async fn remove_kv_data(&self, key: StateStoreDataKey<'_>) -> Result<()> {
match key {
StateStoreDataKey::SyncToken => *self.sync_token.write().unwrap() = None,
StateStoreDataKey::Filter(filter_name) => {
self.filters.remove(filter_name);
}
StateStoreDataKey::UserAvatarUrl(user_id) => {
self.filters.remove(user_id.as_str());
}
}
async fn get_sync_token(&self) -> Result<Option<String>> {
Ok(self.sync_token.read().unwrap().clone())
Ok(())
}
async fn save_changes(&self, changes: &StateChanges) -> Result<()> {
@@ -558,6 +606,10 @@ impl MemoryStore {
Ok(self.custom.insert(key.to_vec(), value))
}
async fn remove_custom_value(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {
Ok(self.custom.remove(key).map(|entry| entry.1))
}
// The in-memory store doesn't cache media
async fn add_media_content(&self, _request: &MediaRequest, _data: Vec<u8>) -> Result<()> {
Ok(())
@@ -594,22 +646,28 @@ impl MemoryStore {
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl StateStore for MemoryStore {
async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<()> {
self.save_filter(filter_name, filter_id).await
type Error = StoreError;
async fn get_kv_data(&self, key: StateStoreDataKey<'_>) -> Result<Option<StateStoreDataValue>> {
self.get_kv_data(key).await
}
async fn set_kv_data(
&self,
key: StateStoreDataKey<'_>,
value: StateStoreDataValue,
) -> Result<()> {
self.set_kv_data(key, value).await
}
async fn remove_kv_data(&self, key: StateStoreDataKey<'_>) -> Result<()> {
self.remove_kv_data(key).await
}
async fn save_changes(&self, changes: &StateChanges) -> Result<()> {
self.save_changes(changes).await
}
async fn get_filter(&self, filter_id: &str) -> Result<Option<String>> {
self.get_filter(filter_id).await
}
async fn get_sync_token(&self) -> Result<Option<String>> {
self.get_sync_token().await
}
async fn get_presence_event(&self, user_id: &UserId) -> Result<Option<Raw<PresenceEvent>>> {
self.get_presence_event(user_id).await
}
@@ -730,6 +788,10 @@ impl StateStore for MemoryStore {
self.set_custom_value(key, value).await
}
async fn remove_custom_value(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {
self.remove_custom_value(key).await
}
async fn add_media_content(&self, request: &MediaRequest, data: Vec<u8>) -> Result<()> {
self.add_media_content(request, data).await
}

View File

@@ -21,26 +21,25 @@
//! store.
use std::{
borrow::Borrow,
collections::{BTreeMap, BTreeSet},
fmt,
ops::Deref,
pin::Pin,
result::Result as StdResult,
str::Utf8Error,
sync::Arc,
sync::{Arc, RwLockReadGuard as StdRwLockReadGuard},
};
use futures_signals::signal::{Mutable, ReadOnlyMutable};
use eyeball::{Observable, SharedObservable};
use once_cell::sync::OnceCell;
#[cfg(any(test, feature = "testing"))]
#[macro_use]
pub mod integration_tests;
mod traits;
use async_trait::async_trait;
use dashmap::DashMap;
use matrix_sdk_common::{locks::RwLock, AsyncTraitDeps};
use matrix_sdk_common::locks::RwLock;
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_crypto::store::{DynCryptoStore, IntoCryptoStore};
pub use matrix_sdk_store_encryption::Error as StoreEncryptionError;
@@ -48,27 +47,22 @@ use ruma::{
api::client::push::get_notifications::v3::Notification,
events::{
presence::PresenceEvent,
receipt::{Receipt, ReceiptEventContent, ReceiptThread, ReceiptType},
receipt::ReceiptEventContent,
room::{
member::{StrippedRoomMemberEvent, SyncRoomMemberEvent},
redaction::OriginalSyncRoomRedactionEvent,
},
AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent,
AnySyncStateEvent, EmptyStateKey, GlobalAccountDataEvent, GlobalAccountDataEventContent,
GlobalAccountDataEventType, RedactContent, RedactedStateEventContent, RoomAccountDataEvent,
RoomAccountDataEventContent, RoomAccountDataEventType, StateEventType, StaticEventContent,
StaticStateEventContent, SyncStateEvent,
AnySyncStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType,
},
serde::Raw,
EventId, MxcUri, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
};
/// BoxStream of owned Types
pub type BoxStream<T> = Pin<Box<dyn futures_util::Stream<Item = T> + Send>>;
use crate::{
deserialized_responses::RawMemberEvent,
media::MediaRequest,
rooms::{RoomInfo, RoomType},
MinimalRoomMemberEvent, Room, Session, SessionMeta, SessionTokens,
};
@@ -76,7 +70,15 @@ use crate::{
pub(crate) mod ambiguity_map;
mod memory_store;
pub use self::memory_store::MemoryStore;
#[cfg(any(test, feature = "testing"))]
pub use self::integration_tests::StateStoreIntegrationTests;
pub use self::{
memory_store::MemoryStore,
traits::{
DynStateStore, IntoStateStore, StateStore, StateStoreDataKey, StateStoreDataValue,
StateStoreExt,
},
};
/// State store specific error type.
#[derive(Debug, thiserror::Error)]
@@ -135,377 +137,15 @@ impl StoreError {
/// A `StateStore` specific result type.
pub type Result<T, E = StoreError> = std::result::Result<T, E>;
/// An abstract state store trait that can be used to implement different stores
/// for the SDK.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait StateStore: AsyncTraitDeps {
/// Save the given filter id under the given name.
///
/// # Arguments
///
/// * `filter_name` - The name that should be used to store the filter id.
///
/// * `filter_id` - The filter id that should be stored in the state store.
async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<()>;
/// Save the set of state changes in the store.
async fn save_changes(&self, changes: &StateChanges) -> Result<()>;
/// Get the filter id that was stored under the given filter name.
///
/// # Arguments
///
/// * `filter_name` - The name that was used to store the filter id.
async fn get_filter(&self, filter_name: &str) -> Result<Option<String>>;
/// Get the last stored sync token.
async fn get_sync_token(&self) -> Result<Option<String>>;
/// Get the stored presence event for the given user.
///
/// # Arguments
///
/// * `user_id` - The id of the user for which we wish to fetch the presence
/// event for.
async fn get_presence_event(&self, user_id: &UserId) -> Result<Option<Raw<PresenceEvent>>>;
/// Get a state event out of the state store.
///
/// # Arguments
///
/// * `room_id` - The id of the room the state event was received for.
///
/// * `event_type` - The event type of the state event.
async fn get_state_event(
&self,
room_id: &RoomId,
event_type: StateEventType,
state_key: &str,
) -> Result<Option<Raw<AnySyncStateEvent>>>;
/// Get a list of state events for a given room and `StateEventType`.
///
/// # Arguments
///
/// * `room_id` - The id of the room to find events for.
///
/// * `event_type` - The event type.
async fn get_state_events(
&self,
room_id: &RoomId,
event_type: StateEventType,
) -> Result<Vec<Raw<AnySyncStateEvent>>>;
/// Get the current profile for the given user in the given room.
///
/// # Arguments
///
/// * `room_id` - The room id the profile is used in.
///
/// * `user_id` - The id of the user the profile belongs to.
async fn get_profile(
&self,
room_id: &RoomId,
user_id: &UserId,
) -> Result<Option<MinimalRoomMemberEvent>>;
/// Get the `MemberEvent` for the given state key in the given room id.
///
/// # Arguments
///
/// * `room_id` - The room id the member event belongs to.
///
/// * `state_key` - The user id that the member event defines the state for.
async fn get_member_event(
&self,
room_id: &RoomId,
state_key: &UserId,
) -> Result<Option<RawMemberEvent>>;
/// Get all the user ids of members for a given room, for stripped and
/// regular rooms alike.
async fn get_user_ids(&self, room_id: &RoomId) -> Result<Vec<OwnedUserId>>;
/// Get all the user ids of members that are in the invited state for a
/// given room, for stripped and regular rooms alike.
async fn get_invited_user_ids(&self, room_id: &RoomId) -> Result<Vec<OwnedUserId>>;
/// Get all the user ids of members that are in the joined state for a
/// given room, for stripped and regular rooms alike.
async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result<Vec<OwnedUserId>>;
/// Get all the pure `RoomInfo`s the store knows about.
async fn get_room_infos(&self) -> Result<Vec<RoomInfo>>;
/// Get all the pure `RoomInfo`s the store knows about.
async fn get_stripped_room_infos(&self) -> Result<Vec<RoomInfo>>;
/// Get all the users that use the given display name in the given room.
///
/// # Arguments
///
/// * `room_id` - The id of the room for which the display name users should
/// be fetched for.
///
/// * `display_name` - The display name that the users use.
async fn get_users_with_display_name(
&self,
room_id: &RoomId,
display_name: &str,
) -> Result<BTreeSet<OwnedUserId>>;
/// Get an event out of the account data store.
///
/// # Arguments
///
/// * `event_type` - The event type of the account data event.
async fn get_account_data_event(
&self,
event_type: GlobalAccountDataEventType,
) -> Result<Option<Raw<AnyGlobalAccountDataEvent>>>;
/// Get an event out of the room account data store.
///
/// # Arguments
///
/// * `room_id` - The id of the room for which the room account data event
/// should
/// be fetched.
///
/// * `event_type` - The event type of the room account data event.
async fn get_room_account_data_event(
&self,
room_id: &RoomId,
event_type: RoomAccountDataEventType,
) -> Result<Option<Raw<AnyRoomAccountDataEvent>>>;
/// Get an event out of the user room receipt store.
///
/// # Arguments
///
/// * `room_id` - The id of the room for which the receipt should be
/// fetched.
///
/// * `receipt_type` - The type of the receipt.
///
/// * `thread` - The thread containing this receipt.
///
/// * `user_id` - The id of the user for who the receipt should be fetched.
async fn get_user_room_receipt_event(
&self,
room_id: &RoomId,
receipt_type: ReceiptType,
thread: ReceiptThread,
user_id: &UserId,
) -> Result<Option<(OwnedEventId, Receipt)>>;
/// Get events out of the event room receipt store.
///
/// # Arguments
///
/// * `room_id` - The id of the room for which the receipts should be
/// fetched.
///
/// * `receipt_type` - The type of the receipts.
///
/// * `thread` - The thread containing this receipt.
///
/// * `event_id` - The id of the event for which the receipts should be
/// fetched.
async fn get_event_room_receipt_events(
&self,
room_id: &RoomId,
receipt_type: ReceiptType,
thread: ReceiptThread,
event_id: &EventId,
) -> Result<Vec<(OwnedUserId, Receipt)>>;
/// Get arbitrary data from the custom store
///
/// # Arguments
///
/// * `key` - The key to fetch data for
async fn get_custom_value(&self, key: &[u8]) -> Result<Option<Vec<u8>>>;
/// Put arbitrary data into the custom store
///
/// # Arguments
///
/// * `key` - The key to insert data into
///
/// * `value` - The value to insert
async fn set_custom_value(&self, key: &[u8], value: Vec<u8>) -> Result<Option<Vec<u8>>>;
/// Add a media file's content in the media store.
///
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
///
/// * `content` - The content of the file.
async fn add_media_content(&self, request: &MediaRequest, content: Vec<u8>) -> Result<()>;
/// Get a media file's content out of the media store.
///
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
async fn get_media_content(&self, request: &MediaRequest) -> Result<Option<Vec<u8>>>;
/// Removes a media file's content from the media store.
///
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
async fn remove_media_content(&self, request: &MediaRequest) -> Result<()>;
/// Removes all the media files' content associated to an `MxcUri` from the
/// media store.
///
/// # Arguments
///
/// * `uri` - The `MxcUri` of the media files.
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()>;
/// Removes a room and all elements associated from the state store.
///
/// # Arguments
///
/// * `room_id` - The `RoomId` of the room to delete.
async fn remove_room(&self, room_id: &RoomId) -> Result<()>;
}
/// Convenience functionality for state stores.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait StateStoreExt: StateStore {
/// Get a specific state event of statically-known type.
///
/// # Arguments
///
/// * `room_id` - The id of the room the state event was received for.
async fn get_state_event_static<C>(
&self,
room_id: &RoomId,
) -> Result<Option<Raw<SyncStateEvent<C>>>>
where
C: StaticEventContent + StaticStateEventContent<StateKey = EmptyStateKey> + RedactContent,
C::Redacted: RedactedStateEventContent,
{
Ok(self.get_state_event(room_id, C::TYPE.into(), "").await?.map(Raw::cast))
}
/// Get a specific state event of statically-known type.
///
/// # Arguments
///
/// * `room_id` - The id of the room the state event was received for.
async fn get_state_event_static_for_key<C, K>(
&self,
room_id: &RoomId,
state_key: &K,
) -> Result<Option<Raw<SyncStateEvent<C>>>>
where
C: StaticEventContent + StaticStateEventContent + RedactContent,
C::StateKey: Borrow<K>,
C::Redacted: RedactedStateEventContent,
K: AsRef<str> + ?Sized + Sync,
{
Ok(self.get_state_event(room_id, C::TYPE.into(), state_key.as_ref()).await?.map(Raw::cast))
}
/// Get a list of state events of a statically-known type for a given room.
///
/// # Arguments
///
/// * `room_id` - The id of the room to find events for.
async fn get_state_events_static<C>(
&self,
room_id: &RoomId,
) -> Result<Vec<Raw<SyncStateEvent<C>>>>
where
C: StaticEventContent + StaticStateEventContent + RedactContent,
C::Redacted: RedactedStateEventContent,
{
// FIXME: Could be more efficient, if we had streaming store accessor functions
Ok(self
.get_state_events(room_id, C::TYPE.into())
.await?
.into_iter()
.map(Raw::cast)
.collect())
}
/// Get an event of a statically-known type from the account data store.
async fn get_account_data_event_static<C>(
&self,
) -> Result<Option<Raw<GlobalAccountDataEvent<C>>>>
where
C: StaticEventContent + GlobalAccountDataEventContent,
{
Ok(self.get_account_data_event(C::TYPE.into()).await?.map(Raw::cast))
}
/// Get an event of a statically-known type from the room account data
/// store.
///
/// # Arguments
///
/// * `room_id` - The id of the room for which the room account data event
/// should be fetched.
async fn get_room_account_data_event_static<C>(
&self,
room_id: &RoomId,
) -> Result<Option<Raw<RoomAccountDataEvent<C>>>>
where
C: StaticEventContent + RoomAccountDataEventContent,
{
Ok(self.get_room_account_data_event(room_id, C::TYPE.into()).await?.map(Raw::cast))
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<T: StateStore + ?Sized> StateStoreExt for T {}
/// A type that can be type-erased into `Arc<dyn StateStore>`.
///
/// This trait is not meant to be implemented directly outside
/// `matrix-sdk-crypto`, but it is automatically implemented for everything that
/// implements `StateStore`.
pub trait IntoStateStore {
#[doc(hidden)]
fn into_state_store(self) -> Arc<dyn StateStore>;
}
impl<T> IntoStateStore for T
where
T: StateStore + Sized + 'static,
{
fn into_state_store(self) -> Arc<dyn StateStore> {
Arc::new(self)
}
}
impl<T> IntoStateStore for Arc<T>
where
T: StateStore + 'static,
{
fn into_state_store(self) -> Arc<dyn StateStore> {
self
}
}
/// A state store wrapper for the SDK.
///
/// This adds additional higher level store functionality on top of a
/// `StateStore` implementation.
#[derive(Clone)]
pub(crate) struct Store {
pub(super) inner: Arc<dyn StateStore>,
pub(super) inner: Arc<DynStateStore>,
session_meta: Arc<OnceCell<SessionMeta>>,
pub(super) session_tokens: Mutable<Option<SessionTokens>>,
pub(super) session_tokens: SharedObservable<Option<SessionTokens>>,
/// The current sync token that should be used for the next sync call.
pub(super) sync_token: Arc<RwLock<Option<String>>>,
rooms: Arc<DashMap<OwnedRoomId, Room>>,
@@ -520,7 +160,7 @@ pub(crate) struct Store {
impl Store {
/// Create a new store, wrapping the given `StateStore`
pub fn new(inner: Arc<dyn StateStore>) -> Self {
pub fn new(inner: Arc<DynStateStore>) -> Self {
Self {
inner,
session_meta: Default::default(),
@@ -554,7 +194,8 @@ impl Store {
self.stripped_rooms.insert(room.room_id().to_owned(), room);
}
let token = self.get_sync_token().await?;
let token =
self.get_kv_data(StateStoreDataKey::SyncToken).await?.and_then(|s| s.into_sync_token());
*self.sync_token.write().await = token;
self.session_meta.set(session_meta).expect("Session Meta was already set");
@@ -567,10 +208,10 @@ impl Store {
self.session_meta.get()
}
/// The current [`SessionTokens`] containing our access token and optional
/// refresh token.
pub fn session_tokens(&self) -> ReadOnlyMutable<Option<SessionTokens>> {
self.session_tokens.read_only()
/// The [`SessionTokens`] containing our access token and optional refresh
/// token.
pub fn session_tokens(&self) -> StdRwLockReadGuard<'_, Observable<Option<SessionTokens>>> {
self.session_tokens.read()
}
/// Set the current [`SessionTokens`].
@@ -582,7 +223,7 @@ impl Store {
/// token and optional refresh token.
pub fn session(&self) -> Option<Session> {
let meta = self.session_meta.get()?;
let tokens = self.session_tokens.get_cloned()?;
let tokens = self.session_tokens().clone()?;
Some(Session::from_parts(meta.to_owned(), tokens))
}
@@ -662,7 +303,7 @@ impl fmt::Debug for Store {
}
impl Deref for Store {
type Target = dyn StateStore;
type Target = DynStateStore;
fn deref(&self) -> &Self::Target {
self.inner.deref()
@@ -840,7 +481,7 @@ impl StateChanges {
pub struct StoreConfig {
#[cfg(feature = "e2e-encryption")]
pub(crate) crypto_store: Arc<DynCryptoStore>,
pub(crate) state_store: Arc<dyn StateStore>,
pub(crate) state_store: Arc<DynStateStore>,
}
#[cfg(not(tarpaulin_include))]

View File

@@ -0,0 +1,700 @@
// Copyright 2023 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::{borrow::Borrow, collections::BTreeSet, fmt, sync::Arc};
use async_trait::async_trait;
use matrix_sdk_common::AsyncTraitDeps;
use ruma::{
events::{
presence::PresenceEvent,
receipt::{Receipt, ReceiptThread, ReceiptType},
AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnySyncStateEvent, EmptyStateKey,
GlobalAccountDataEvent, GlobalAccountDataEventContent, GlobalAccountDataEventType,
RedactContent, RedactedStateEventContent, RoomAccountDataEvent,
RoomAccountDataEventContent, RoomAccountDataEventType, StateEventType, StaticEventContent,
StaticStateEventContent, SyncStateEvent,
},
serde::Raw,
EventId, MxcUri, OwnedEventId, OwnedUserId, RoomId, UserId,
};
use super::{StateChanges, StoreError};
use crate::{
deserialized_responses::RawMemberEvent, media::MediaRequest, MinimalRoomMemberEvent, RoomInfo,
};
/// An abstract state store trait that can be used to implement different stores
/// for the SDK.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait StateStore: AsyncTraitDeps {
/// The error type used by this state store.
type Error: fmt::Debug + Into<StoreError> + From<serde_json::Error>;
/// Get key-value data from the store.
///
/// # Arguments
///
/// * `key` - The key to fetch data for.
async fn get_kv_data(
&self,
key: StateStoreDataKey<'_>,
) -> Result<Option<StateStoreDataValue>, Self::Error>;
/// Put key-value data into the store.
///
/// # Arguments
///
/// * `key` - The key to identify the data in the store.
///
/// * `value` - The data to insert.
///
/// Panics if the key and value variants do not match.
async fn set_kv_data(
&self,
key: StateStoreDataKey<'_>,
value: StateStoreDataValue,
) -> Result<(), Self::Error>;
/// Remove key-value data from the store.
///
/// # Arguments
///
/// * `key` - The key to remove the data for.
async fn remove_kv_data(&self, key: StateStoreDataKey<'_>) -> Result<(), Self::Error>;
/// Save the set of state changes in the store.
async fn save_changes(&self, changes: &StateChanges) -> Result<(), Self::Error>;
/// Get the stored presence event for the given user.
///
/// # Arguments
///
/// * `user_id` - The id of the user for which we wish to fetch the presence
/// event for.
async fn get_presence_event(
&self,
user_id: &UserId,
) -> Result<Option<Raw<PresenceEvent>>, Self::Error>;
/// Get a state event out of the state store.
///
/// # Arguments
///
/// * `room_id` - The id of the room the state event was received for.
///
/// * `event_type` - The event type of the state event.
async fn get_state_event(
&self,
room_id: &RoomId,
event_type: StateEventType,
state_key: &str,
) -> Result<Option<Raw<AnySyncStateEvent>>, Self::Error>;
/// Get a list of state events for a given room and `StateEventType`.
///
/// # Arguments
///
/// * `room_id` - The id of the room to find events for.
///
/// * `event_type` - The event type.
async fn get_state_events(
&self,
room_id: &RoomId,
event_type: StateEventType,
) -> Result<Vec<Raw<AnySyncStateEvent>>, Self::Error>;
/// Get the current profile for the given user in the given room.
///
/// # Arguments
///
/// * `room_id` - The room id the profile is used in.
///
/// * `user_id` - The id of the user the profile belongs to.
async fn get_profile(
&self,
room_id: &RoomId,
user_id: &UserId,
) -> Result<Option<MinimalRoomMemberEvent>, Self::Error>;
/// Get the `MemberEvent` for the given state key in the given room id.
///
/// # Arguments
///
/// * `room_id` - The room id the member event belongs to.
///
/// * `state_key` - The user id that the member event defines the state for.
async fn get_member_event(
&self,
room_id: &RoomId,
state_key: &UserId,
) -> Result<Option<RawMemberEvent>, Self::Error>;
/// Get all the user ids of members for a given room, for stripped and
/// regular rooms alike.
async fn get_user_ids(&self, room_id: &RoomId) -> Result<Vec<OwnedUserId>, Self::Error>;
/// Get all the user ids of members that are in the invited state for a
/// given room, for stripped and regular rooms alike.
async fn get_invited_user_ids(&self, room_id: &RoomId)
-> Result<Vec<OwnedUserId>, Self::Error>;
/// Get all the user ids of members that are in the joined state for a
/// given room, for stripped and regular rooms alike.
async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result<Vec<OwnedUserId>, Self::Error>;
/// Get all the pure `RoomInfo`s the store knows about.
async fn get_room_infos(&self) -> Result<Vec<RoomInfo>, Self::Error>;
/// Get all the pure `RoomInfo`s the store knows about.
async fn get_stripped_room_infos(&self) -> Result<Vec<RoomInfo>, Self::Error>;
/// Get all the users that use the given display name in the given room.
///
/// # Arguments
///
/// * `room_id` - The id of the room for which the display name users should
/// be fetched for.
///
/// * `display_name` - The display name that the users use.
async fn get_users_with_display_name(
&self,
room_id: &RoomId,
display_name: &str,
) -> Result<BTreeSet<OwnedUserId>, Self::Error>;
/// Get an event out of the account data store.
///
/// # Arguments
///
/// * `event_type` - The event type of the account data event.
async fn get_account_data_event(
&self,
event_type: GlobalAccountDataEventType,
) -> Result<Option<Raw<AnyGlobalAccountDataEvent>>, Self::Error>;
/// Get an event out of the room account data store.
///
/// # Arguments
///
/// * `room_id` - The id of the room for which the room account data event
/// should
/// be fetched.
///
/// * `event_type` - The event type of the room account data event.
async fn get_room_account_data_event(
&self,
room_id: &RoomId,
event_type: RoomAccountDataEventType,
) -> Result<Option<Raw<AnyRoomAccountDataEvent>>, Self::Error>;
/// Get an event out of the user room receipt store.
///
/// # Arguments
///
/// * `room_id` - The id of the room for which the receipt should be
/// fetched.
///
/// * `receipt_type` - The type of the receipt.
///
/// * `thread` - The thread containing this receipt.
///
/// * `user_id` - The id of the user for who the receipt should be fetched.
async fn get_user_room_receipt_event(
&self,
room_id: &RoomId,
receipt_type: ReceiptType,
thread: ReceiptThread,
user_id: &UserId,
) -> Result<Option<(OwnedEventId, Receipt)>, Self::Error>;
/// Get events out of the event room receipt store.
///
/// # Arguments
///
/// * `room_id` - The id of the room for which the receipts should be
/// fetched.
///
/// * `receipt_type` - The type of the receipts.
///
/// * `thread` - The thread containing this receipt.
///
/// * `event_id` - The id of the event for which the receipts should be
/// fetched.
async fn get_event_room_receipt_events(
&self,
room_id: &RoomId,
receipt_type: ReceiptType,
thread: ReceiptThread,
event_id: &EventId,
) -> Result<Vec<(OwnedUserId, Receipt)>, Self::Error>;
/// Get arbitrary data from the custom store
///
/// # Arguments
///
/// * `key` - The key to fetch data for
async fn get_custom_value(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error>;
/// Put arbitrary data into the custom store
///
/// # Arguments
///
/// * `key` - The key to insert data into
///
/// * `value` - The value to insert
async fn set_custom_value(
&self,
key: &[u8],
value: Vec<u8>,
) -> Result<Option<Vec<u8>>, Self::Error>;
/// Remove arbitrary data from the custom store and return it if existed
///
/// # Arguments
///
/// * `key` - The key to remove data from
async fn remove_custom_value(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error>;
/// Add a media file's content in the media store.
///
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
///
/// * `content` - The content of the file.
async fn add_media_content(
&self,
request: &MediaRequest,
content: Vec<u8>,
) -> Result<(), Self::Error>;
/// Get a media file's content out of the media store.
///
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
async fn get_media_content(
&self,
request: &MediaRequest,
) -> Result<Option<Vec<u8>>, Self::Error>;
/// Removes a media file's content from the media store.
///
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
async fn remove_media_content(&self, request: &MediaRequest) -> Result<(), Self::Error>;
/// Removes all the media files' content associated to an `MxcUri` from the
/// media store.
///
/// # Arguments
///
/// * `uri` - The `MxcUri` of the media files.
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error>;
/// Removes a room and all elements associated from the state store.
///
/// # Arguments
///
/// * `room_id` - The `RoomId` of the room to delete.
async fn remove_room(&self, room_id: &RoomId) -> Result<(), Self::Error>;
}
#[repr(transparent)]
struct EraseStateStoreError<T>(T);
impl<T: fmt::Debug> fmt::Debug for EraseStateStoreError<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<T: StateStore> StateStore for EraseStateStoreError<T> {
type Error = StoreError;
async fn get_kv_data(
&self,
key: StateStoreDataKey<'_>,
) -> Result<Option<StateStoreDataValue>, Self::Error> {
self.0.get_kv_data(key).await.map_err(Into::into)
}
async fn set_kv_data(
&self,
key: StateStoreDataKey<'_>,
value: StateStoreDataValue,
) -> Result<(), Self::Error> {
self.0.set_kv_data(key, value).await.map_err(Into::into)
}
async fn remove_kv_data(&self, key: StateStoreDataKey<'_>) -> Result<(), Self::Error> {
self.0.remove_kv_data(key).await.map_err(Into::into)
}
async fn save_changes(&self, changes: &StateChanges) -> Result<(), Self::Error> {
self.0.save_changes(changes).await.map_err(Into::into)
}
async fn get_presence_event(
&self,
user_id: &UserId,
) -> Result<Option<Raw<PresenceEvent>>, Self::Error> {
self.0.get_presence_event(user_id).await.map_err(Into::into)
}
async fn get_state_event(
&self,
room_id: &RoomId,
event_type: StateEventType,
state_key: &str,
) -> Result<Option<Raw<AnySyncStateEvent>>, Self::Error> {
self.0.get_state_event(room_id, event_type, state_key).await.map_err(Into::into)
}
async fn get_state_events(
&self,
room_id: &RoomId,
event_type: StateEventType,
) -> Result<Vec<Raw<AnySyncStateEvent>>, Self::Error> {
self.0.get_state_events(room_id, event_type).await.map_err(Into::into)
}
async fn get_profile(
&self,
room_id: &RoomId,
user_id: &UserId,
) -> Result<Option<MinimalRoomMemberEvent>, Self::Error> {
self.0.get_profile(room_id, user_id).await.map_err(Into::into)
}
async fn get_member_event(
&self,
room_id: &RoomId,
state_key: &UserId,
) -> Result<Option<RawMemberEvent>, Self::Error> {
self.0.get_member_event(room_id, state_key).await.map_err(Into::into)
}
async fn get_user_ids(&self, room_id: &RoomId) -> Result<Vec<OwnedUserId>, Self::Error> {
self.0.get_user_ids(room_id).await.map_err(Into::into)
}
async fn get_invited_user_ids(
&self,
room_id: &RoomId,
) -> Result<Vec<OwnedUserId>, Self::Error> {
self.0.get_invited_user_ids(room_id).await.map_err(Into::into)
}
async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result<Vec<OwnedUserId>, Self::Error> {
self.0.get_joined_user_ids(room_id).await.map_err(Into::into)
}
async fn get_room_infos(&self) -> Result<Vec<RoomInfo>, Self::Error> {
self.0.get_room_infos().await.map_err(Into::into)
}
async fn get_stripped_room_infos(&self) -> Result<Vec<RoomInfo>, Self::Error> {
self.0.get_stripped_room_infos().await.map_err(Into::into)
}
async fn get_users_with_display_name(
&self,
room_id: &RoomId,
display_name: &str,
) -> Result<BTreeSet<OwnedUserId>, Self::Error> {
self.0.get_users_with_display_name(room_id, display_name).await.map_err(Into::into)
}
async fn get_account_data_event(
&self,
event_type: GlobalAccountDataEventType,
) -> Result<Option<Raw<AnyGlobalAccountDataEvent>>, Self::Error> {
self.0.get_account_data_event(event_type).await.map_err(Into::into)
}
async fn get_room_account_data_event(
&self,
room_id: &RoomId,
event_type: RoomAccountDataEventType,
) -> Result<Option<Raw<AnyRoomAccountDataEvent>>, Self::Error> {
self.0.get_room_account_data_event(room_id, event_type).await.map_err(Into::into)
}
async fn get_user_room_receipt_event(
&self,
room_id: &RoomId,
receipt_type: ReceiptType,
thread: ReceiptThread,
user_id: &UserId,
) -> Result<Option<(OwnedEventId, Receipt)>, Self::Error> {
self.0
.get_user_room_receipt_event(room_id, receipt_type, thread, user_id)
.await
.map_err(Into::into)
}
async fn get_event_room_receipt_events(
&self,
room_id: &RoomId,
receipt_type: ReceiptType,
thread: ReceiptThread,
event_id: &EventId,
) -> Result<Vec<(OwnedUserId, Receipt)>, Self::Error> {
self.0
.get_event_room_receipt_events(room_id, receipt_type, thread, event_id)
.await
.map_err(Into::into)
}
async fn get_custom_value(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
self.0.get_custom_value(key).await.map_err(Into::into)
}
async fn set_custom_value(
&self,
key: &[u8],
value: Vec<u8>,
) -> Result<Option<Vec<u8>>, Self::Error> {
self.0.set_custom_value(key, value).await.map_err(Into::into)
}
async fn remove_custom_value(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
self.0.remove_custom_value(key).await.map_err(Into::into)
}
async fn add_media_content(
&self,
request: &MediaRequest,
content: Vec<u8>,
) -> Result<(), Self::Error> {
self.0.add_media_content(request, content).await.map_err(Into::into)
}
async fn get_media_content(
&self,
request: &MediaRequest,
) -> Result<Option<Vec<u8>>, Self::Error> {
self.0.get_media_content(request).await.map_err(Into::into)
}
async fn remove_media_content(&self, request: &MediaRequest) -> Result<(), Self::Error> {
self.0.remove_media_content(request).await.map_err(Into::into)
}
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error> {
self.0.remove_media_content_for_uri(uri).await.map_err(Into::into)
}
async fn remove_room(&self, room_id: &RoomId) -> Result<(), Self::Error> {
self.0.remove_room(room_id).await.map_err(Into::into)
}
}
/// Convenience functionality for state stores.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait StateStoreExt: StateStore {
/// Get a specific state event of statically-known type.
///
/// # Arguments
///
/// * `room_id` - The id of the room the state event was received for.
async fn get_state_event_static<C>(
&self,
room_id: &RoomId,
) -> Result<Option<Raw<SyncStateEvent<C>>>, Self::Error>
where
C: StaticEventContent + StaticStateEventContent<StateKey = EmptyStateKey> + RedactContent,
C::Redacted: RedactedStateEventContent,
{
Ok(self.get_state_event(room_id, C::TYPE.into(), "").await?.map(Raw::cast))
}
/// Get a specific state event of statically-known type.
///
/// # Arguments
///
/// * `room_id` - The id of the room the state event was received for.
async fn get_state_event_static_for_key<C, K>(
&self,
room_id: &RoomId,
state_key: &K,
) -> Result<Option<Raw<SyncStateEvent<C>>>, Self::Error>
where
C: StaticEventContent + StaticStateEventContent + RedactContent,
C::StateKey: Borrow<K>,
C::Redacted: RedactedStateEventContent,
K: AsRef<str> + ?Sized + Sync,
{
Ok(self.get_state_event(room_id, C::TYPE.into(), state_key.as_ref()).await?.map(Raw::cast))
}
/// Get a list of state events of a statically-known type for a given room.
///
/// # Arguments
///
/// * `room_id` - The id of the room to find events for.
async fn get_state_events_static<C>(
&self,
room_id: &RoomId,
) -> Result<Vec<Raw<SyncStateEvent<C>>>, Self::Error>
where
C: StaticEventContent + StaticStateEventContent + RedactContent,
C::Redacted: RedactedStateEventContent,
{
// FIXME: Could be more efficient, if we had streaming store accessor functions
Ok(self
.get_state_events(room_id, C::TYPE.into())
.await?
.into_iter()
.map(Raw::cast)
.collect())
}
/// Get an event of a statically-known type from the account data store.
async fn get_account_data_event_static<C>(
&self,
) -> Result<Option<Raw<GlobalAccountDataEvent<C>>>, Self::Error>
where
C: StaticEventContent + GlobalAccountDataEventContent,
{
Ok(self.get_account_data_event(C::TYPE.into()).await?.map(Raw::cast))
}
/// Get an event of a statically-known type from the room account data
/// store.
///
/// # Arguments
///
/// * `room_id` - The id of the room for which the room account data event
/// should be fetched.
async fn get_room_account_data_event_static<C>(
&self,
room_id: &RoomId,
) -> Result<Option<Raw<RoomAccountDataEvent<C>>>, Self::Error>
where
C: StaticEventContent + RoomAccountDataEventContent,
{
Ok(self.get_room_account_data_event(room_id, C::TYPE.into()).await?.map(Raw::cast))
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<T: StateStore + ?Sized> StateStoreExt for T {}
/// A type-erased [`StateStore`].
pub type DynStateStore = dyn StateStore<Error = StoreError>;
/// A type that can be type-erased into `Arc<dyn StateStore>`.
///
/// This trait is not meant to be implemented directly outside
/// `matrix-sdk-crypto`, but it is automatically implemented for everything that
/// implements `StateStore`.
pub trait IntoStateStore {
#[doc(hidden)]
fn into_state_store(self) -> Arc<DynStateStore>;
}
impl<T> IntoStateStore for T
where
T: StateStore + Sized + 'static,
{
fn into_state_store(self) -> Arc<DynStateStore> {
Arc::new(EraseStateStoreError(self))
}
}
// Turns a given `Arc<T>` into `Arc<DynStateStore>` by attaching the
// StateStore impl vtable of `EraseStateStoreError<T>`.
impl<T> IntoStateStore for Arc<T>
where
T: StateStore + 'static,
{
fn into_state_store(self) -> Arc<DynStateStore> {
let ptr: *const T = Arc::into_raw(self);
let ptr_erased = ptr as *const EraseStateStoreError<T>;
// SAFETY: EraseStateStoreError is repr(transparent) so T and
// EraseStateStoreError<T> have the same layout and ABI
unsafe { Arc::from_raw(ptr_erased) }
}
}
/// A value for key-value data that should be persisted into the store.
#[derive(Debug, Clone)]
pub enum StateStoreDataValue {
/// The sync token.
SyncToken(String),
/// A filter with the given ID.
Filter(String),
/// The user avatar url
UserAvatarUrl(String),
}
impl StateStoreDataValue {
/// Get this value if it is a sync token.
pub fn into_sync_token(self) -> Option<String> {
match self {
Self::SyncToken(token) => Some(token),
_ => None,
}
}
/// Get this value if it is a filter.
pub fn into_filter(self) -> Option<String> {
match self {
Self::Filter(filter) => Some(filter),
_ => None,
}
}
/// Get this value if it is a user avatar url.
pub fn into_user_avatar_url(self) -> Option<String> {
match self {
Self::UserAvatarUrl(user_avatar_url) => Some(user_avatar_url),
_ => None,
}
}
}
/// A key for key-value data.
#[derive(Debug, Clone, Copy)]
pub enum StateStoreDataKey<'a> {
/// The sync token.
SyncToken,
/// A filter with the given name.
Filter(&'a str),
/// Avatar URL
UserAvatarUrl(&'a UserId),
}
impl StateStoreDataKey<'_> {
/// Key to use for the [`SyncToken`][Self::SyncToken] variant.
pub const SYNC_TOKEN: &str = "sync_token";
/// Key prefix to use for the [`Filter`][Self::Filter] variant.
pub const FILTER: &str = "filter";
/// Key prefix to use for the [`UserAvatarUrl`][Self::UserAvatarUrl]
/// variant.
pub const USER_AVATAR_URL: &str = "user_avatar_url";
}

View File

@@ -27,7 +27,7 @@ serde_json = { workspace = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
async-lock = "2.5.0"
futures-util = { version = "0.3.21", default-features = false, features = ["channel"] }
futures-util = { workspace = true, features = ["channel"] }
wasm-bindgen-futures = { version = "0.4.33", optional = true }
gloo-timers = { version = "0.2.6", features = ["futures"] }

View File

@@ -44,7 +44,7 @@ where
}
}
#[cfg(all(test))]
#[cfg(test)]
pub(crate) mod tests {
use std::{future, time::Duration};

View File

@@ -34,9 +34,9 @@ byteorder = { workspace = true }
ctr = "0.9.1"
dashmap = { workspace = true }
event-listener = "2.5.2"
eyeball = { workspace = true }
futures-core = "0.3.24"
futures-util = { version = "0.3.21", default-features = false, features = ["alloc"] }
futures-signals = { version = "0.3.31", default-features = false }
futures-util = { workspace = true }
hmac = "0.12.1"
http = { workspace = true, optional = true } # feature = testing only
itertools = "0.10.5"
@@ -61,6 +61,7 @@ tokio = { version = "1.24", default-features = false, features = ["time"] }
[dev-dependencies]
anyhow = { workspace = true }
assert_matches = "1.5.0"
ctor.workspace = true
futures = { version = "0.3.21", default-features = false, features = ["executor"] }
http = { workspace = true }
indoc = "1.0.4"
@@ -68,3 +69,4 @@ matrix-sdk-test = { version = "0.6.0", path = "../../testing/matrix-sdk-test" }
proptest = { version = "1.0.0", default-features = false, features = ["std"] }
# required for async_test macro
tokio = { version = "1.24.2", default-features = false, features = ["macros", "rt-multi-thread"] }
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }

View File

@@ -203,7 +203,7 @@ impl<'a, R: Read + ?Sized + 'a> AttachmentEncryptor<'a, R> {
///
/// # Arguments
///
/// * `reader` - The `Reader` that should be wrapped and enrypted.
/// * `reader` - The `Reader` that should be wrapped and encrypted.
///
/// # Panics
///

View File

@@ -31,7 +31,7 @@ use ruma::{
};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
use tracing::warn;
use tracing::{trace, warn};
use vodozemac::{olm::SessionConfig, Curve25519PublicKey, Ed25519PublicKey};
use super::{atomic_bool_deserializer, atomic_bool_serializer};
@@ -691,6 +691,13 @@ impl ReadOnlyDevice {
let message = session.encrypt(self, event_type, content).await?;
trace!(
user_id = ?self.user_id(),
device_id = ?self.device_id(),
session_id = session.session_id(),
"Successfully encrypted a Megolm session",
);
Ok((session, message))
}

View File

@@ -22,11 +22,13 @@ use std::{
use futures_util::future::join_all;
use matrix_sdk_common::{
executor::spawn,
locks::Mutex,
timeout::{timeout, ElapsedError},
};
use ruma::{
api::client::keys::get_keys::v3::Response as KeysQueryResponse, serde::Raw, DeviceId,
OwnedDeviceId, OwnedServerName, OwnedUserId, ServerName, UserId,
OwnedDeviceId, OwnedServerName, OwnedTransactionId, OwnedUserId, ServerName, TransactionId,
UserId,
};
use tracing::{debug, info, instrument, trace, warn};
@@ -89,7 +91,7 @@ impl KeysQueryListener {
timeout: Duration,
user: &UserId,
) -> Result<UserKeyQueryResult, ElapsedError> {
let users_for_key_query = self.store.users_for_key_query().await.unwrap_or_default();
let (users_for_key_query, _) = self.store.users_for_key_query().await.unwrap_or_default();
if users_for_key_query.contains(user) {
if let Err(e) = self.wait(timeout).await {
@@ -124,6 +126,22 @@ pub(crate) struct IdentityManager {
keys_query_listener: KeysQueryListener,
failures: FailuresCache<OwnedServerName>,
store: Store,
/// Details of the current "in-flight" key query request, if any
keys_query_request_details: Arc<Mutex<KeysQueryRequestDetails>>,
}
/// Details of an in-flight key query request
#[derive(Debug, Clone, Default)]
struct KeysQueryRequestDetails {
/// The sequence number, to be passed to
/// `Store.mark_tracked_users_as_up_to_date`.
sequence_number: i64,
/// A single batch of queries returned by the Store is broken up into one or
/// more actual KeysQueryRequests, each with their own request id. We
/// record the outstanding request ids here.
request_ids: HashSet<OwnedTransactionId>,
}
impl IdentityManager {
@@ -131,6 +149,7 @@ impl IdentityManager {
pub fn new(user_id: Arc<UserId>, device_id: Arc<DeviceId>, store: Store) -> Self {
let keys_query_listener = KeysQueryListener::new(store.clone());
let keys_query_request_details = Mutex::new(KeysQueryRequestDetails::default());
IdentityManager {
user_id,
@@ -138,6 +157,7 @@ impl IdentityManager {
store,
keys_query_listener,
failures: Default::default(),
keys_query_request_details: keys_query_request_details.into(),
}
}
@@ -156,13 +176,16 @@ impl IdentityManager {
///
/// # Arguments
///
/// * `request_id` - The request_id returned by users_for_key_query
/// * `response` - The keys query response of the request that the client
/// performed.
pub async fn receive_keys_query_response(
&self,
request_id: &TransactionId,
response: &KeysQueryResponse,
) -> OlmResult<(DeviceChanges, IdentityChanges)> {
debug!(
?request_id,
users = ?response.device_keys.keys().collect::<BTreeSet<_>>(),
failures = ?response.failures,
"Handling a keys query response"
@@ -196,8 +219,22 @@ impl IdentityManager {
};
self.store.save_changes(changes).await?;
self.mark_tracked_users_as_up_to_date(response.device_keys.keys().map(Deref::deref))
.await?;
// if this request is one of those we expected to be in flight, pass the
// sequence number back to the store so that it can mark devices up to
// date
let (sequence_number, removed) = {
let mut request_details = self.keys_query_request_details.lock().await;
(request_details.sequence_number, request_details.request_ids.remove(request_id))
};
if removed {
self.store
.mark_tracked_users_as_up_to_date(
response.device_keys.keys().map(Deref::deref),
sequence_number,
)
.await?;
}
let changed_devices = devices.changed.iter().fold(BTreeMap::new(), |mut acc, d| {
acc.entry(d.user_id()).or_insert_with(BTreeSet::new).insert(d.device_id());
@@ -219,6 +256,7 @@ impl IdentityManager {
identities.changed.iter().map(|i| i.user_id()).collect::<BTreeSet<_>>();
debug!(
?request_id,
?new_devices,
?changed_devices,
?deleted_devices,
@@ -644,44 +682,58 @@ impl IdentityManager {
Ok((changes, changed_identity))
}
/// Get a key query request if one is needed.
/// Get a list of key query requests needed.
///
/// Returns a key query request if the client should query E2E keys,
/// otherwise None.
/// # Returns
///
/// A list of pairs of `(request_id, request)`
///
/// The response of a successful key query requests needs to be passed to
/// the [`OlmMachine`] with the [`receive_keys_query_response`].
///
/// [`OlmMachine`]: struct.OlmMachine.html
/// [`receive_keys_query_response`]: #method.receive_keys_query_response
pub async fn users_for_key_query(&self) -> StoreResult<Vec<KeysQueryRequest>> {
let users = self.store.users_for_key_query().await?;
pub async fn users_for_key_query(
&self,
) -> StoreResult<Vec<(OwnedTransactionId, KeysQueryRequest)>> {
// forget about any previous key queries in flight
*(self.keys_query_request_details.lock().await) = KeysQueryRequestDetails::default();
let (users, sequence_number) = self.store.users_for_key_query().await?;
// We always want to track our own user, but in case we aren't in an encrypted
// room yet, we won't be tracking ourselves yet. This ensures we are always
// tracking ourselves.
//
// The check for emptiness is done first for performance.
let users =
let (users, sequence_number) =
if users.is_empty() && !self.store.tracked_users().await?.contains(self.user_id()) {
self.store.mark_user_as_changed(self.user_id()).await?;
self.store.users_for_key_query().await?
} else {
users
(users, sequence_number)
};
if users.is_empty() {
Ok(Vec::new())
} else {
let users: Vec<OwnedUserId> =
users.into_iter().filter(|u| !self.failures.contains(u.server_name())).collect();
Ok(users
.chunks(Self::MAX_KEY_QUERY_USERS)
.map(|u| u.iter().map(|u| (u.clone(), Vec::new())).collect())
.map(KeysQueryRequest::new)
.collect())
return Ok(Vec::new());
}
let mut result: Vec<(OwnedTransactionId, KeysQueryRequest)> = Vec::new();
let mut request_details =
KeysQueryRequestDetails { sequence_number, request_ids: HashSet::new() };
let filtered_users: Vec<OwnedUserId> =
users.into_iter().filter(|u| !self.failures.contains(u.server_name())).collect();
for c in filtered_users.chunks(Self::MAX_KEY_QUERY_USERS) {
let request_id = TransactionId::new();
let req = KeysQueryRequest::new(c.iter().map(|u| (u.clone(), Vec::new())).collect());
debug!(?request_id, users = ?c, "Created keys query request");
result.push((request_id.clone(), req));
request_details.request_ids.insert(request_id);
}
*(self.keys_query_request_details.lock().await) = request_details;
Ok(result)
}
/// Receive the list of users that contained changed devices from the
@@ -695,15 +747,7 @@ impl IdentityManager {
&self,
users: impl Iterator<Item = &UserId>,
) -> StoreResult<()> {
let mut changed_user: Vec<(&UserId, bool)> = Vec::new();
for user_id in users {
if self.store.is_user_tracked(user_id).await? {
changed_user.push((user_id, true))
}
}
self.store.save_tracked_users(&changed_user).await
self.store.mark_tracked_users_as_changed(users).await
}
/// See the docs for [`OlmMachine::update_tracked_users()`].
@@ -711,23 +755,7 @@ impl IdentityManager {
&self,
users: impl IntoIterator<Item = &UserId>,
) -> StoreResult<()> {
let mut tracked_users = Vec::new();
for user_id in users {
if !self.store.is_user_tracked(user_id).await? {
tracked_users.push((user_id, true));
}
}
self.store.save_tracked_users(&tracked_users).await
}
pub async fn mark_tracked_users_as_up_to_date(
&self,
users: impl Iterator<Item = &UserId>,
) -> StoreResult<()> {
let updated_users: Vec<(&UserId, bool)> = users.map(|u| (u, false)).collect();
self.store.save_tracked_users(&updated_users).await
self.store.update_tracked_users(users.into_iter()).await
}
}
@@ -980,24 +1008,12 @@ pub(crate) mod tests {
use matrix_sdk_test::{async_test, response_from_file};
use ruma::{
api::{client::keys::get_keys::v3::Response as KeysQueryResponse, IncomingResponse},
device_id, user_id,
device_id, user_id, TransactionId,
};
use serde_json::json;
use super::testing::{device_id, key_query, manager, other_key_query, other_user_id, user_id};
fn key_query_without_failures() -> KeysQueryResponse {
let response = json!({
"device_keys": {
"@alice:example.org": {
},
}
});
let response = response_from_file(&response);
KeysQueryResponse::try_from_http_response(response).unwrap()
}
fn key_query_with_failures() -> KeysQueryResponse {
let response = json!({
"device_keys": {
@@ -1026,11 +1042,11 @@ pub(crate) mod tests {
);
manager.receive_device_changes([alice].iter().map(Deref::deref)).await.unwrap();
assert!(
!manager.store.is_user_tracked(alice).await.unwrap(),
!manager.store.tracked_users().await.unwrap().contains(alice),
"Receiving a device changes update for a user we don't track does nothing"
);
assert!(
!manager.store.users_for_key_query().await.unwrap().contains(alice),
!manager.store.users_for_key_query().await.unwrap().0.contains(alice),
"The user we don't track doesn't end up in the `/keys/query` request"
);
}
@@ -1052,7 +1068,10 @@ pub(crate) mod tests {
let task = tokio::task::spawn(async move { listener.wait(Duration::from_secs(10)).await });
manager.receive_keys_query_response(&other_key_query()).await.unwrap();
manager
.receive_keys_query_response(&TransactionId::new(), &other_key_query())
.await
.unwrap();
task.await.unwrap().unwrap();
@@ -1085,7 +1104,10 @@ pub(crate) mod tests {
let device_keys = manager.store.account().device_keys().await;
manager
.receive_keys_query_response(&key_query(identity_request, device_keys))
.receive_keys_query_response(
&TransactionId::new(),
&key_query(identity_request, device_keys),
)
.await
.unwrap();
@@ -1120,6 +1142,37 @@ pub(crate) mod tests {
);
}
/// If a user is invalidated while a /keys/query request is in flight, that
/// user is not removed from the list of outdated users when the
/// response is received
#[async_test]
async fn invalidation_race_handling() {
let manager = manager().await;
let alice = other_user_id();
manager.update_tracked_users([alice]).await.unwrap();
// alice should be in the list of key queries
let (reqid, req) = manager.users_for_key_query().await.unwrap().pop().unwrap();
assert!(req.device_keys.contains_key(alice));
// another invalidation turns up
manager.receive_device_changes([alice].into_iter()).await.unwrap();
// the response from the query arrives
manager.receive_keys_query_response(&reqid, &other_key_query()).await.unwrap();
// alice should *still* be in the list of key queries
let (reqid, req) = manager.users_for_key_query().await.unwrap().pop().unwrap();
assert!(req.device_keys.contains_key(alice));
// another key query response
manager.receive_keys_query_response(&reqid, &other_key_query()).await.unwrap();
// finally alice should not be in the list
let queries = manager.users_for_key_query().await.unwrap();
assert!(!queries.iter().any(|(_, r)| r.device_keys.contains_key(alice)));
}
#[async_test]
async fn failure_handling() {
let manager = manager().await;
@@ -1135,40 +1188,27 @@ pub(crate) mod tests {
manager.store.tracked_users().await.unwrap().contains(alice),
"Alice is tracked after being marked as tracked"
);
assert!(manager
.users_for_key_query()
.await
.unwrap()
.iter()
.any(|r| r.device_keys.contains_key(alice)));
let (reqid, req) = manager.users_for_key_query().await.unwrap().pop().unwrap();
assert!(req.device_keys.contains_key(alice));
// a failure should stop us querying for the user's keys.
let response = key_query_with_failures();
manager.receive_keys_query_response(&response).await.unwrap();
manager.receive_keys_query_response(&reqid, &response).await.unwrap();
assert!(manager.failures.contains(alice.server_name()));
assert!(!manager
.users_for_key_query()
.await
.unwrap()
.iter()
.any(|r| r.device_keys.contains_key(alice)));
.any(|(_, r)| r.device_keys.contains_key(alice)));
let response = key_query_without_failures();
manager.receive_keys_query_response(&response).await.unwrap();
assert!(!manager.failures.contains(alice.server_name()));
assert!(!manager
.users_for_key_query()
.await
.unwrap()
.iter()
.any(|r| r.device_keys.contains_key(alice)));
manager.store.mark_user_as_changed(alice).await.unwrap();
// clearing the failure flag should make the user reappear in the query list.
manager.failures.remove([alice.server_name().to_owned()].iter());
assert!(manager
.users_for_key_query()
.await
.unwrap()
.iter()
.any(|r| r.device_keys.contains_key(alice)));
.any(|(_, r)| r.device_keys.contains_key(alice)));
}
}

View File

@@ -103,6 +103,20 @@ pub mod vodozemac {
olm::{
DecryptionError as OlmDecryptionError, SessionCreationError as OlmSessionCreationError,
},
DecodeError, KeyError, PickleError, SignatureError,
DecodeError, KeyError, PickleError, SignatureError, VERSION,
};
}
/// The version of the matrix-sdk-cypto crate being used
pub static VERSION: &str = env!("CARGO_PKG_VERSION");
// Enable tracing for tests in this crate
#[cfg(all(test, not(target_arch = "wasm32")))]
#[ctor::ctor]
fn init_logging() {
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::from_default_env())
.with(tracing_subscriber::fmt::layer().with_test_writer())
.init();
}

View File

@@ -346,9 +346,13 @@ impl OlmMachine {
requests.push(r);
}
for request in self.identity_manager.users_for_key_query().await?.into_iter().map(|r| {
OutgoingRequest { request_id: TransactionId::new(), request: Arc::new(r.into()) }
}) {
for request in self
.identity_manager
.users_for_key_query()
.await?
.into_iter()
.map(|(request_id, r)| OutgoingRequest { request_id, request: Arc::new(r.into()) })
{
requests.push(request);
}
@@ -377,7 +381,7 @@ impl OlmMachine {
self.receive_keys_upload_response(response).await?;
}
IncomingResponse::KeysQuery(response) => {
self.receive_keys_query_response(response).await?;
self.receive_keys_query_response(request_id, response).await?;
}
IncomingResponse::KeysClaim(response) => {
self.receive_keys_claim_response(response).await?;
@@ -531,9 +535,10 @@ impl OlmMachine {
/// performed.
async fn receive_keys_query_response(
&self,
request_id: &TransactionId,
response: &KeysQueryResponse,
) -> OlmResult<(DeviceChanges, IdentityChanges)> {
self.identity_manager.receive_keys_query_response(response).await
self.identity_manager.receive_keys_query_response(request_id, response).await
}
/// Get a request to upload E2EE keys to the server.
@@ -1722,7 +1727,7 @@ pub(crate) mod tests {
room_id,
serde::Raw,
uint, user_id, DeviceId, DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch,
OwnedDeviceKeyId, UserId,
OwnedDeviceKeyId, TransactionId, UserId,
};
use serde_json::json;
use vodozemac::{
@@ -1805,8 +1810,9 @@ pub(crate) mod tests {
async fn get_machine_after_query() -> (OlmMachine, OneTimeKeys) {
let (machine, otk) = get_prepared_machine().await;
let response = keys_query_response();
let req_id = TransactionId::new();
machine.receive_keys_query_response(&response).await.unwrap();
machine.receive_keys_query_response(&req_id, &response).await.unwrap();
(machine, otk)
}
@@ -2026,7 +2032,8 @@ pub(crate) mod tests {
let alice_devices = machine.store.get_user_devices(alice_id).await.unwrap();
assert!(alice_devices.devices().peekable().peek().is_none());
machine.receive_keys_query_response(&response).await.unwrap();
let req_id = TransactionId::new();
machine.receive_keys_query_response(&req_id, &response).await.unwrap();
let device = machine.store.get_device(alice_id, alice_device_id).await.unwrap().unwrap();
assert_eq!(device.user_id(), alice_id);

View File

@@ -36,7 +36,7 @@ use ruma::{
use serde::{Deserialize, Serialize};
use serde_json::{value::RawValue as RawJsonValue, Value};
use sha2::{Digest, Sha256};
use tracing::{debug, info, trace, warn};
use tracing::{debug, info, instrument, trace, warn, Span};
use vodozemac::{
olm::{
Account as InnerAccount, AccountPickle, IdentityKeys, OlmMessage, PreKeyMessage,
@@ -286,6 +286,7 @@ impl Account {
}
/// Decrypt an Olm message, creating a new Olm session if possible.
#[instrument(skip(self, message), fields(session_id))]
async fn decrypt_olm_message(
&self,
sender: &UserId,
@@ -293,59 +294,72 @@ impl Account {
message: &OlmMessage,
) -> OlmResult<(SessionType, DecryptionResult)> {
// First try to decrypt using an existing session.
let (session, plaintext) =
if let Some(d) = self.decrypt_with_existing_sessions(sender_key, message).await? {
// Decryption succeeded, de-structure the session/plaintext out of
// the Option.
(SessionType::Existing(d.0), d.1)
} else {
// Decryption failed with every known session, let's try to create a
// new session.
match message {
// A new session can only be created using a pre-key message,
// return with an error if it isn't one.
OlmMessage::Normal(_) => {
warn!(
?sender_key,
"Failed to decrypt a non-pre-key message with all \
available sessions",
);
let (session, plaintext) = if let Some(d) =
self.decrypt_with_existing_sessions(sender_key, message).await?
{
// Decryption succeeded, de-structure the session/plaintext out of
// the Option.
(SessionType::Existing(d.0), d.1)
} else {
// Decryption failed with every known session, let's try to create a
// new session.
match message {
// A new session can only be created using a pre-key message,
// return with an error if it isn't one.
OlmMessage::Normal(_) => {
let session_ids = if let Some(sessions) =
self.store.get_sessions(&sender_key.to_base64()).await?
{
sessions.lock().await.iter().map(|s| s.session_id().to_owned()).collect()
} else {
vec![]
};
return Err(OlmError::SessionWedged(sender.to_owned(), sender_key));
}
warn!(
?session_ids,
"Failed to decrypt a non-pre-key message with all available sessions",
);
OlmMessage::PreKey(m) => {
// Create the new session.
let result = match self.inner.create_inbound_session(sender_key, m).await {
Ok(r) => r,
Err(e) => {
warn!(
?sender_key,
session_keys = ?m.session_keys(),
"Failed to create a new Olm session from a \
pre-key message: {e:?}",
);
return Err(OlmError::SessionWedged(sender.to_owned(), sender_key));
}
};
// We need to add the new session to the session cache, otherwise
// we might try to create the same session again.
// TODO separate the session cache from the storage so we only add
// it to the cache but don't store it.
let changes = Changes {
account: Some(self.inner.clone()),
sessions: vec![result.session.clone()],
..Default::default()
};
self.store.save_changes(changes).await?;
(SessionType::New(result.session), result.plaintext)
}
return Err(OlmError::SessionWedged(sender.to_owned(), sender_key));
}
};
trace!(?sender_key, "Successfully decrypted an Olm message");
OlmMessage::PreKey(m) => {
// Create the new session.
let result = match self.inner.create_inbound_session(sender_key, m).await {
Ok(r) => r,
Err(e) => {
warn!(
session_keys = ?m.session_keys(),
"Failed to create a new Olm session from a pre-key message: {e:?}",
);
return Err(OlmError::SessionWedged(sender.to_owned(), sender_key));
}
};
// We need to add the new session to the session cache, otherwise
// we might try to create the same session again.
// TODO: separate the session cache from the storage so we only add
// it to the cache but don't store it.
let changes = Changes {
account: Some(self.inner.clone()),
sessions: vec![result.session.clone()],
..Default::default()
};
self.store.save_changes(changes).await?;
(SessionType::New(result.session), result.plaintext)
}
}
};
let session_id = match &session {
SessionType::New(s) => s.session_id(),
SessionType::Existing(s) => s.session_id(),
};
Span::current().record("session_id", session_id);
trace!("Successfully decrypted an Olm message");
match self.parse_decrypted_to_device_event(sender, sender_key, plaintext).await {
Ok(result) => Ok((session, result)),
@@ -368,7 +382,6 @@ impl Account {
}
warn!(
sender_key = sender_key.to_base64(),
error = ?e,
"A to-device message was successfully decrypted but \
parsing and checking the event fields failed"
@@ -1045,22 +1058,28 @@ impl ReadOnlyAccount {
///
/// * `message` - A pre-key Olm message that was sent to us by the other
/// account.
#[instrument(
skip_all,
fields(
sender_key = ?their_identity_key,
session_id = message.session_id(),
session_keys = ?message.session_keys(),
)
)]
pub async fn create_inbound_session(
&self,
their_identity_key: Curve25519PublicKey,
message: &PreKeyMessage,
) -> Result<InboundCreationResult, SessionCreationError> {
debug!(
sender_key = ?their_identity_key,
session_keys = ?message.session_keys(),
"Creating a new Olm session from a pre-key message"
);
debug!("Creating a new Olm session from a pre-key message");
let result = self.inner.lock().await.create_inbound_session(their_identity_key, message)?;
let now = SecondsSinceUnixEpoch::now();
let session_id = result.session.session_id();
trace!(?session_id, "Olm session created successfully");
let session = Session {
user_id: self.user_id.clone(),
device_id: self.device_id.clone(),

View File

@@ -51,7 +51,7 @@ use matrix_sdk_common::locks::Mutex;
use ruma::{events::secret::request::SecretName, DeviceId, OwnedDeviceId, OwnedUserId, UserId};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::{info, warn};
use tracing::{info, instrument, trace, warn, Span};
use vodozemac::{megolm::SessionOrdering, Curve25519PublicKey};
use zeroize::Zeroize;
@@ -100,11 +100,92 @@ pub(crate) struct Store {
inner: Arc<DynCryptoStore>,
verification_machine: VerificationMachine,
tracked_users_cache: Arc<DashSet<OwnedUserId>>,
users_for_key_query_cache: Arc<DashSet<OwnedUserId>>,
users_for_key_query: Arc<Mutex<UsersForKeyQuery>>,
tracked_user_loading_lock: Arc<Mutex<()>>,
tracked_users_loaded: Arc<AtomicBool>,
}
/// Record of the users that are waiting for a /keys/query.
///
/// To avoid races, we maintain a sequence number which is updated each time we
/// receive an invalidation notification. We also record the sequence number at
/// which each user was last invalidated. Then, we attach the current sequence
/// number to each `/keys/query` request, and when we get the response we can
/// tell if any users have been invalidated more recently than that request.
#[derive(Debug)]
struct UsersForKeyQuery {
/// The sequence number we will assign to the next addition to user_map
next_sequence_number: InvalidationSequenceNumber,
/// The users pending a lookup, together with the sequence number at which
/// they were added to the list
user_map: HashMap<OwnedUserId, InvalidationSequenceNumber>,
}
// We use wrapping arithmetic for the sequence numbers, to make sure we never
// run out of numbers. (2**64 should be enough for anyone, but it's easy enough
// just to make it wrap.)
//
// We use a *signed* counter so that we can compare values via a subtraction.
// For example, suppose we've just overflowed from i64::MAX to i64::MIN.
// (i64::MAX.wrapping_sub(i64::MIN)) is -1, which tells us that i64::MAX comes
// before i64::MIN in the sequence.
type InvalidationSequenceNumber = i64;
impl UsersForKeyQuery {
/// Create a new, empty, `UsersForKeyQueryCache`
fn new() -> Self {
UsersForKeyQuery { next_sequence_number: 0, user_map: HashMap::new() }
}
/// Record a new user that requires a key query
fn insert_user(&mut self, user: &UserId) {
let seq = self.next_sequence_number;
trace!(?user, sequence_number = seq, "Flagging user for key query");
self.user_map.insert(user.to_owned(), seq);
self.next_sequence_number = self.next_sequence_number.wrapping_add(1);
}
/// Record that a user has received an update with the given sequence
/// number.
///
/// If the sequence number is newer than the oldest invalidation for this
/// user, it is removed from the list of those needing an update.
///
/// Returns true if the user is now up-to-date, else false
#[instrument(level = "trace", skip(self), fields(invalidation_sequence))]
fn maybe_remove_user(
&mut self,
user: &UserId,
query_sequence: InvalidationSequenceNumber,
) -> bool {
let last_invalidation = self.user_map.get(user);
if let Some(invalidation_sequence) = last_invalidation {
Span::current().record("invalidation_sequence", invalidation_sequence);
if invalidation_sequence.wrapping_sub(query_sequence) > 0 {
trace!("User invalidated since this query started: still not up-to-date");
false
} else {
trace!("User now up-to-date");
self.user_map.remove(user);
true
}
} else {
trace!("User already up-to-date, nothing to do");
true
}
}
/// Fetch the list of users waiting for a key query, and the current
/// sequence number
fn users_for_key_query(&self) -> (HashSet<OwnedUserId>, InvalidationSequenceNumber) {
// we return the sequence number of the last invalidation
let sequence_number = self.next_sequence_number.wrapping_sub(1);
(self.user_map.keys().cloned().collect(), sequence_number)
}
}
#[derive(Default, Debug)]
#[allow(missing_docs)]
pub struct Changes {
@@ -294,7 +375,7 @@ impl Store {
inner: store,
verification_machine,
tracked_users_cache: DashSet::new().into(),
users_for_key_query_cache: DashSet::new().into(),
users_for_key_query: Mutex::new(UsersForKeyQuery::new()).into(),
tracked_users_loaded: AtomicBool::new(false).into(),
tracked_user_loading_lock: Mutex::new(()).into(),
}
@@ -617,30 +698,82 @@ impl Store {
Ok(())
}
/// Mark that the given user has an outdated device list.
/// Mark the given user as being tracked for device lists, and mark that it
/// has an outdated device list.
///
/// This means that the user will be considered for a `/keys/query` request
/// next time [`Store::users_for_key_query()`] is called.
pub async fn mark_user_as_changed(&self, user: &UserId) -> Result<()> {
self.save_tracked_users(&[(user, true)]).await
self.users_for_key_query.lock().await.insert_user(user);
self.tracked_users_cache.insert(user.to_owned());
self.inner.save_tracked_users(&[(user, true)]).await
}
/// Save the list of users and their outdated/dirty flags to the store.
/// Add entries to the list of users being tracked for device changes
///
/// This method will fill up the store-internal caches, unlike the method on
/// the various [`CryptoStore`] implementations.
pub async fn save_tracked_users(&self, users: &[(&UserId, bool)]) -> Result<()> {
for &(user, dirty) in users {
if dirty {
self.users_for_key_query_cache.insert(user.to_owned());
} else {
self.users_for_key_query_cache.remove(user);
}
/// Any users not already on the list are flagged as awaiting a key query.
/// Users that were already in the list are unaffected.
pub async fn update_tracked_users(&self, users: impl Iterator<Item = &UserId>) -> Result<()> {
self.load_tracked_users().await?;
self.tracked_users_cache.insert(user.to_owned());
let mut store_updates = Vec::new();
let mut key_query_lock = self.users_for_key_query.lock().await;
for user_id in users {
if !self.tracked_users_cache.contains(user_id) {
self.tracked_users_cache.insert(user_id.to_owned());
key_query_lock.insert_user(user_id);
store_updates.push((user_id, true))
}
}
self.inner.save_tracked_users(users).await?;
self.inner.save_tracked_users(&store_updates).await
}
/// Process notifications that users have changed devices.
///
/// This is used to handle the list of device-list updates that is received
/// from the `/sync` response. Any users *whose device lists we are
/// tracking* are flagged as needing a key query. Users whose devices we
/// are not tracking are ignored.
pub async fn mark_tracked_users_as_changed(
&self,
users: impl Iterator<Item = &UserId>,
) -> Result<()> {
self.load_tracked_users().await?;
let mut store_updates: Vec<(&UserId, bool)> = Vec::new();
let mut key_query_lock = self.users_for_key_query.lock().await;
for user_id in users {
if self.tracked_users_cache.contains(user_id) {
key_query_lock.insert_user(user_id);
store_updates.push((user_id, true));
}
}
self.inner.save_tracked_users(&store_updates).await?;
Ok(())
}
/// Flag that the given users devices are now up-to-date.
///
/// This is called after processing the response to a /keys/query request.
/// Any users whose device lists we are tracking are removed from the
/// list of those pending a /keys/query.
pub async fn mark_tracked_users_as_up_to_date(
&self,
users: impl Iterator<Item = &UserId>,
sequence_number: InvalidationSequenceNumber,
) -> Result<()> {
let mut store_updates: Vec<(&UserId, bool)> = Vec::new();
let mut key_query_lock = self.users_for_key_query.lock().await;
for user_id in users {
if self.tracked_users_cache.contains(user_id) {
let clean = key_query_lock.maybe_remove_user(user_id, sequence_number);
store_updates.push((user_id, !clean));
}
}
self.inner.save_tracked_users(&store_updates).await?;
Ok(())
}
@@ -662,11 +795,12 @@ impl Store {
if !self.tracked_users_loaded.load(Ordering::SeqCst) {
let tracked_users = self.inner.load_tracked_users().await?;
let mut query_users_lock = self.users_for_key_query.lock().await;
for user in tracked_users {
self.tracked_users_cache.insert(user.user_id.to_owned());
if user.dirty {
self.users_for_key_query_cache.insert(user.user_id);
query_users_lock.insert_user(&user.user_id);
}
}
@@ -677,22 +811,23 @@ impl Store {
Ok(())
}
/// Are we tracking the list of devices this user has?
pub async fn is_user_tracked(&self, user_id: &UserId) -> Result<bool> {
self.load_tracked_users().await?;
Ok(self.tracked_users_cache.contains(user_id))
}
/// Get the set of users that has the outdate/dirty flag set for their list
/// of devices.
///
/// This set should be included in a `/keys/query` request which will update
/// the device list.
pub async fn users_for_key_query(&self) -> Result<HashSet<OwnedUserId>> {
///
/// # Returns
///
/// A pair `(users, sequence_number)`, where `users` is the list of users to
/// be queried, and `sequence_number` is the current sequence number,
/// which should be returned in `mark_tracked_users_as_up_to_date`.
pub async fn users_for_key_query(
&self,
) -> Result<(HashSet<OwnedUserId>, InvalidationSequenceNumber)> {
self.load_tracked_users().await?;
Ok(self.users_for_key_query_cache.iter().map(|u| u.clone()).collect())
Ok(self.users_for_key_query.lock().await.users_for_key_query())
}
/// See the docs for [`crate::OlmMachine::tracked_users()`].

View File

@@ -14,8 +14,8 @@
use std::sync::Arc;
use eyeball::{Observable, SharedObservable};
use futures_core::Stream;
use futures_signals::signal::{Mutable, SignalExt};
use futures_util::StreamExt;
use matrix_sdk_qrcode::{
qrcode::QrCode, EncodingError, QrVerificationData, SelfVerificationData,
@@ -135,7 +135,7 @@ impl From<&InnerState> for QrVerificationState {
pub struct QrVerification {
flow_id: FlowId,
inner: Arc<QrVerificationData>,
state: Arc<Mutable<InnerState>>,
state: SharedObservable<InnerState>,
identities: IdentitiesBeingVerified,
request_handle: Option<RequestHandle>,
we_started: bool,
@@ -145,8 +145,8 @@ impl std::fmt::Debug for QrVerification {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("QrVerification")
.field("flow_id", &self.flow_id)
.field("inner", self.inner.as_ref())
.field("state", &self.state.lock_ref())
.field("inner", &self.inner)
.field("state", &self.state)
.finish()
}
}
@@ -157,12 +157,12 @@ impl QrVerification {
/// When the verification object is in this state it's required that the
/// user confirms that the other side has scanned the QR code.
pub fn has_been_scanned(&self) -> bool {
matches!(&*self.state.lock_ref(), InnerState::Scanned(_))
matches!(**self.state.read(), InnerState::Scanned(_))
}
/// Has the scanning of the QR code been confirmed by us.
pub fn has_been_confirmed(&self) -> bool {
matches!(&*self.state.lock_ref(), InnerState::Confirmed(_))
matches!(**self.state.read(), InnerState::Confirmed(_))
}
/// Get our own user id.
@@ -189,7 +189,7 @@ impl QrVerification {
/// Get info about the cancellation if the verification flow has been
/// cancelled.
pub fn cancel_info(&self) -> Option<CancelInfo> {
if let InnerState::Cancelled(c) = &*self.state.lock_ref() {
if let InnerState::Cancelled(c) = &**self.state.read() {
Some(c.state.clone().into())
} else {
None
@@ -198,12 +198,12 @@ impl QrVerification {
/// Has the verification flow completed.
pub fn is_done(&self) -> bool {
matches!(&*self.state.lock_ref(), InnerState::Done(_))
matches!(**self.state.read(), InnerState::Done(_))
}
/// Has the verification flow been cancelled.
pub fn is_cancelled(&self) -> bool {
matches!(&*self.state.lock_ref(), InnerState::Cancelled(_))
matches!(**self.state.read(), InnerState::Cancelled(_))
}
/// Is this a verification that is veryfying one of our own devices
@@ -214,7 +214,7 @@ impl QrVerification {
/// Have we successfully scanned the QR code and are able to send a
/// reciprocation event.
pub fn reciprocated(&self) -> bool {
matches!(&*self.state.lock_ref(), InnerState::Reciprocated(_))
matches!(**self.state.read(), InnerState::Reciprocated(_))
}
/// Get the unique ID that identifies this QR code verification flow.
@@ -268,7 +268,7 @@ impl QrVerification {
///
/// [`cancel()`]: #method.cancel
pub fn cancel_with_code(&self, code: CancelCode) -> Option<OutgoingVerificationRequest> {
let mut state = self.state.lock_mut();
let mut state = self.state.write();
if let Some(request) = &self.request_handle {
request.cancel_with_code(&code);
@@ -277,13 +277,13 @@ impl QrVerification {
let new_state = QrState::<Cancelled>::new(true, code);
let content = new_state.as_content(self.flow_id());
match &*state {
match &**state {
InnerState::Confirmed(_)
| InnerState::Created(_)
| InnerState::Scanned(_)
| InnerState::Reciprocated(_)
| InnerState::Done(_) => {
*state = InnerState::Cancelled(new_state);
Observable::set(&mut state, InnerState::Cancelled(new_state));
Some(self.content_to_request(content))
}
InnerState::Cancelled(_) => None,
@@ -296,7 +296,7 @@ impl QrVerification {
/// This will return some `OutgoingContent` if the object is in the correct
/// state to start the verification flow, otherwise `None`.
pub fn reciprocate(&self) -> Option<OutgoingVerificationRequest> {
match &*self.state.lock_ref() {
match &**self.state.read() {
InnerState::Reciprocated(s) => {
Some(self.content_to_request(s.as_content(self.flow_id())))
}
@@ -310,13 +310,13 @@ impl QrVerification {
/// Confirm that the other side has scanned our QR code.
pub fn confirm_scanning(&self) -> Option<OutgoingVerificationRequest> {
let mut state = self.state.lock_mut();
let mut state = self.state.write();
match &*state {
match &**state {
InnerState::Scanned(s) => {
let new_state = s.clone().confirm_scanning();
let content = new_state.as_content(&self.flow_id);
*state = InnerState::Confirmed(new_state);
Observable::set(&mut state, InnerState::Confirmed(new_state));
Some(self.content_to_request(content))
}
@@ -366,9 +366,7 @@ impl QrVerification {
VerificationResult::SignatureUpload(s) => (None, Some(s)),
};
let mut guard = self.state.lock_mut();
*guard = new_state;
self.state.set(new_state);
Ok((content.map(|c| self.content_to_request(c)), request))
}
@@ -379,7 +377,7 @@ impl QrVerification {
(Option<OutgoingVerificationRequest>, Option<SignatureUploadRequest>),
CryptoStoreError,
> {
let state = (*self.state.lock_ref()).clone();
let state = self.state.get();
Ok(match state {
InnerState::Confirmed(s) => {
@@ -432,17 +430,17 @@ impl QrVerification {
&self,
content: &StartContent<'_>,
) -> Option<OutgoingVerificationRequest> {
let mut state = self.state.lock_mut();
let mut state = self.state.write();
match &*state {
match &**state {
InnerState::Created(s) => match s.clone().receive_reciprocate(content) {
Ok(s) => {
*state = InnerState::Scanned(s);
Observable::set(&mut state, InnerState::Scanned(s));
None
}
Err(s) => {
let content = s.as_content(self.flow_id());
*state = InnerState::Cancelled(s);
Observable::set(&mut state, InnerState::Cancelled(s));
Some(self.content_to_request(content))
}
},
@@ -456,9 +454,9 @@ impl QrVerification {
pub(crate) fn receive_cancel(&self, sender: &UserId, content: &CancelContent<'_>) {
if sender == self.other_user_id() {
let mut state = self.state.lock_mut();
let mut state = self.state.write();
let new_state = match &*state {
let new_state = match &**state {
InnerState::Created(s) => s.clone().into_cancelled(content),
InnerState::Scanned(s) => s.clone().into_cancelled(content),
InnerState::Confirmed(s) => s.clone().into_cancelled(content),
@@ -472,7 +470,7 @@ impl QrVerification {
"Cancelling a QR verification, other user has cancelled"
);
*state = InnerState::Cancelled(new_state);
Observable::set(&mut state, InnerState::Cancelled(new_state));
}
}
@@ -630,10 +628,9 @@ impl QrVerification {
Ok(Self {
flow_id,
inner: qr_code.into(),
state: Mutable::new(InnerState::Reciprocated(QrState {
state: SharedObservable::new(InnerState::Reciprocated(QrState {
state: Reciprocated { secret, own_device_id },
}))
.into(),
})),
identities,
we_started,
request_handle,
@@ -652,7 +649,9 @@ impl QrVerification {
Self {
flow_id,
inner: inner.into(),
state: Mutable::new(InnerState::Created(QrState { state: Created { secret } })).into(),
state: SharedObservable::new(InnerState::Created(QrState {
state: Created { secret },
})),
identities,
we_started,
request_handle,
@@ -663,7 +662,7 @@ impl QrVerification {
///
/// The changes are presented as a stream of [`QrVerificationState`] values.
pub fn changes(&self) -> impl Stream<Item = QrVerificationState> {
self.state.signal_cloned().to_stream().map(|s| (&s).into())
self.state.subscribe().map(|s| (&s).into())
}
/// Get the current state the verification process is in.
@@ -671,7 +670,7 @@ impl QrVerification {
/// To listen to changes to the [`QrVerificationState`] use the
/// [`QrVerification::changes`] method.
pub fn state(&self) -> QrVerificationState {
(&*self.state.lock_ref()).into()
(&**self.state.read()).into()
}
}

View File

@@ -14,8 +14,8 @@
use std::{sync::Arc, time::Duration};
use eyeball::{Observable, SharedObservable};
use futures_core::Stream;
use futures_signals::signal::{Mutable, SignalExt};
use futures_util::StreamExt;
use matrix_sdk_common::instant::Instant;
#[cfg(feature = "qrcode")]
@@ -136,7 +136,7 @@ pub struct VerificationRequest {
account: ReadOnlyAccount,
flow_id: Arc<FlowId>,
other_user_id: Arc<UserId>,
inner: Arc<Mutable<InnerRequest>>,
inner: SharedObservable<InnerRequest>,
creation_time: Arc<Instant>,
we_started: bool,
recipient_devices: Arc<Vec<OwnedDeviceId>>,
@@ -152,20 +152,20 @@ pub struct VerificationRequest {
/// `VerificationRequest` object.
#[derive(Clone, Debug)]
pub(crate) struct RequestHandle {
inner: Arc<Mutable<InnerRequest>>,
inner: SharedObservable<InnerRequest>,
}
impl RequestHandle {
pub fn cancel_with_code(&self, cancel_code: &CancelCode) {
let mut guard = self.inner.lock_mut();
let mut guard = self.inner.write();
if let Some(updated) = guard.cancel(true, cancel_code) {
*guard = updated;
Observable::set(&mut guard, updated);
}
}
}
impl From<Arc<Mutable<InnerRequest>>> for RequestHandle {
fn from(inner: Arc<Mutable<InnerRequest>>) -> Self {
impl From<SharedObservable<InnerRequest>> for RequestHandle {
fn from(inner: SharedObservable<InnerRequest>) -> Self {
Self { inner }
}
}
@@ -180,14 +180,13 @@ impl VerificationRequest {
methods: Option<Vec<VerificationMethod>>,
) -> Self {
let account = store.account.clone();
let inner = Mutable::new(InnerRequest::Created(RequestState::new(
let inner = SharedObservable::new(InnerRequest::Created(RequestState::new(
cache.clone(),
store,
other_user,
&flow_id,
methods,
)))
.into();
)));
Self {
account,
@@ -206,9 +205,9 @@ impl VerificationRequest {
/// self-verifications and it should be sent to the specific device that we
/// want to verify.
pub(crate) fn request_to_device(&self) -> ToDeviceRequest {
let inner = self.inner.lock_ref();
let inner = self.inner.read();
let methods = if let InnerRequest::Created(c) = &*inner {
let methods = if let InnerRequest::Created(c) = &**inner {
c.state.our_methods.clone()
} else {
SUPPORTED_METHODS.to_vec()
@@ -264,7 +263,7 @@ impl VerificationRequest {
/// The id of the other device that is participating in this verification.
pub fn other_device_id(&self) -> Option<OwnedDeviceId> {
match &*self.inner.lock_ref() {
match &**self.inner.read() {
InnerRequest::Requested(r) => Some(r.state.other_device_id.clone()),
InnerRequest::Ready(r) => Some(r.state.other_device_id.clone()),
InnerRequest::Created(_)
@@ -285,7 +284,7 @@ impl VerificationRequest {
/// Get info about the cancellation if the verification request has been
/// cancelled.
pub fn cancel_info(&self) -> Option<CancelInfo> {
if let InnerRequest::Cancelled(c) = &*self.inner.lock_ref() {
if let InnerRequest::Cancelled(c) = &**self.inner.read() {
Some(c.state.clone().into())
} else {
None
@@ -294,12 +293,12 @@ impl VerificationRequest {
/// Has the verification request been answered by another device.
pub fn is_passive(&self) -> bool {
matches!(&*self.inner.lock_ref(), InnerRequest::Passive(_))
matches!(**self.inner.read(), InnerRequest::Passive(_))
}
/// Is the verification request ready to start a verification flow.
pub fn is_ready(&self) -> bool {
matches!(&*self.inner.lock_ref(), InnerRequest::Ready(_))
matches!(**self.inner.read(), InnerRequest::Ready(_))
}
/// Has the verification flow timed out.
@@ -312,7 +311,7 @@ impl VerificationRequest {
/// Will be present only if the other side requested the verification or if
/// we're in the ready state.
pub fn their_supported_methods(&self) -> Option<Vec<VerificationMethod>> {
match &*self.inner.lock_ref() {
match &**self.inner.read() {
InnerRequest::Requested(r) => Some(r.state.their_methods.clone()),
InnerRequest::Ready(r) => Some(r.state.their_methods.clone()),
InnerRequest::Created(_)
@@ -327,7 +326,7 @@ impl VerificationRequest {
/// Will be present only we requested the verification or if we're in the
/// ready state.
pub fn our_supported_methods(&self) -> Option<Vec<VerificationMethod>> {
match &*self.inner.lock_ref() {
match &**self.inner.read() {
InnerRequest::Created(r) => Some(r.state.our_methods.clone()),
InnerRequest::Ready(r) => Some(r.state.our_methods.clone()),
InnerRequest::Requested(_)
@@ -354,20 +353,20 @@ impl VerificationRequest {
/// Has the verification flow that was started with this request finished.
pub fn is_done(&self) -> bool {
matches!(&*self.inner.lock_ref(), InnerRequest::Done(_))
matches!(**self.inner.read(), InnerRequest::Done(_))
}
/// Has the verification flow that was started with this request been
/// cancelled.
pub fn is_cancelled(&self) -> bool {
matches!(&*self.inner.lock_ref(), InnerRequest::Cancelled(_))
matches!(**self.inner.read(), InnerRequest::Cancelled(_))
}
/// Generate a QR code that can be used by another client to start a QR code
/// based verification.
#[cfg(feature = "qrcode")]
pub async fn generate_qr_code(&self) -> Result<Option<QrVerification>, CryptoStoreError> {
let inner = self.inner.lock_ref().clone();
let inner = self.inner.get();
inner.generate_qr_code(self.we_started, self.inner.clone().into()).await
}
@@ -384,7 +383,7 @@ impl VerificationRequest {
&self,
data: QrVerificationData,
) -> Result<Option<QrVerification>, ScanError> {
let future = if let InnerRequest::Ready(r) = &*self.inner.lock_ref() {
let future = if let InnerRequest::Ready(r) = &**self.inner.read() {
QrVerification::from_scan(
r.store.clone(),
r.other_user_id.clone(),
@@ -398,6 +397,7 @@ impl VerificationRequest {
return Ok(None);
};
// await future after self.inner read guard is released
let qr_verification = future.await?;
// We may have previously started our own QR verification (e.g. two devices
@@ -437,9 +437,9 @@ impl VerificationRequest {
Self {
verification_cache: cache.clone(),
inner: Arc::new(Mutable::new(InnerRequest::Requested(
inner: SharedObservable::new(InnerRequest::Requested(
RequestState::from_request_event(cache, store, sender, &flow_id, content),
))),
)),
account,
other_user_id: sender.into(),
flow_id: flow_id.into(),
@@ -459,13 +459,13 @@ impl VerificationRequest {
&self,
methods: Vec<VerificationMethod>,
) -> Option<OutgoingVerificationRequest> {
let mut guard = self.inner.lock_mut();
let mut guard = self.inner.write();
let Some((updated, content)) = guard.accept(methods) else {
return None;
};
*guard = updated;
Observable::set(&mut guard, updated);
let request = match content {
OutgoingContent::ToDevice(content) => ToDeviceRequest::with_id(
@@ -505,16 +505,16 @@ impl VerificationRequest {
}
fn cancel_with_code(&self, cancel_code: CancelCode) -> Option<OutgoingVerificationRequest> {
let mut guard = self.inner.lock_mut();
let mut guard = self.inner.write();
let send_to_everyone = self.we_started() && matches!(&*guard, InnerRequest::Created(_));
let send_to_everyone = self.we_started() && matches!(**guard, InnerRequest::Created(_));
let other_device = guard.other_device_id();
if let Some(updated) = guard.cancel(true, &cancel_code) {
*guard = updated;
Observable::set(&mut guard, updated);
}
let content = if let InnerRequest::Cancelled(c) = &*guard {
let content = if let InnerRequest::Cancelled(c) = &**guard {
Some(c.state.as_content(self.flow_id()))
} else {
None
@@ -630,11 +630,12 @@ impl VerificationRequest {
}
pub(crate) fn receive_ready(&self, sender: &UserId, content: &ReadyContent<'_>) {
let mut guard = self.inner.lock_mut();
let mut guard = self.inner.write();
match &*guard {
match &**guard {
InnerRequest::Created(s) => {
*guard = InnerRequest::Ready(s.clone().into_ready(sender, content));
let new_value = InnerRequest::Ready(s.clone().into_ready(sender, content));
Observable::set(&mut guard, new_value);
if let Some(request) =
self.cancel_for_other_devices(CancelCode::Accepted, Some(content.from_device()))
@@ -645,7 +646,8 @@ impl VerificationRequest {
InnerRequest::Requested(s) => {
if sender == self.own_user_id() && content.from_device() != self.account.device_id()
{
*guard = InnerRequest::Passive(s.clone().into_passive(content))
let new_value = InnerRequest::Passive(s.clone().into_passive(content));
Observable::set(&mut guard, new_value);
}
}
InnerRequest::Ready(_)
@@ -660,7 +662,7 @@ impl VerificationRequest {
sender: &UserId,
content: &StartContent<'_>,
) -> Result<(), CryptoStoreError> {
let inner = self.inner.lock_ref().clone();
let inner = self.inner.get();
let InnerRequest::Ready(s) = inner else {
warn!(
@@ -682,9 +684,9 @@ impl VerificationRequest {
"Marking a verification request as done"
);
let mut guard = self.inner.lock_mut();
let mut guard = self.inner.write();
if let Some(updated) = guard.receive_done(content) {
*guard = updated;
Observable::set(&mut guard, updated);
}
}
}
@@ -699,9 +701,9 @@ impl VerificationRequest {
code = content.cancel_code().as_str(),
"Cancelling a verification request, other user has cancelled"
);
let mut guard = self.inner.lock_mut();
let mut guard = self.inner.write();
if let Some(updated) = guard.cancel(false, content.cancel_code()) {
*guard = updated;
Observable::set(&mut guard, updated);
}
if self.we_started() {
@@ -717,7 +719,7 @@ impl VerificationRequest {
pub async fn start_sas(
&self,
) -> Result<Option<(Sas, OutgoingVerificationRequest)>, CryptoStoreError> {
let inner = self.inner.lock_ref().clone();
let inner = self.inner.get();
Ok(match &inner {
InnerRequest::Ready(s) => {
@@ -771,7 +773,7 @@ impl VerificationRequest {
/// The changes are presented as a stream of [`VerificationRequestState`]
/// values.
pub fn changes(&self) -> impl Stream<Item = VerificationRequestState> {
self.inner.signal_cloned().to_stream().map(|s| (&s).into())
self.inner.subscribe().map(|s| (&s).into())
}
/// Get the current state the verification request is in.
@@ -779,7 +781,7 @@ impl VerificationRequest {
/// To listen to changes to the [`VerificationRequestState`] use the
/// [`VerificationRequest::changes`] method.
pub fn state(&self) -> VerificationRequestState {
(&*self.inner.lock_ref()).into()
(&**self.inner.read()).into()
}
}

View File

@@ -18,8 +18,8 @@ mod sas_state;
use std::sync::Arc;
use eyeball::{Observable, SharedObservable};
use futures_core::Stream;
use futures_signals::signal::{Mutable, SignalExt};
use futures_util::StreamExt;
use inner_sas::InnerSas;
use ruma::{
@@ -49,7 +49,7 @@ use crate::{
/// Short authentication string object.
#[derive(Clone, Debug)]
pub struct Sas {
inner: Arc<Mutable<InnerSas>>,
inner: SharedObservable<InnerSas>,
account: ReadOnlyAccount,
identities_being_verified: IdentitiesBeingVerified,
flow_id: Arc<FlowId>,
@@ -268,12 +268,12 @@ impl Sas {
/// Does this verification flow support displaying emoji for the short
/// authentication string.
pub fn supports_emoji(&self) -> bool {
self.inner.lock_ref().supports_emoji()
self.inner.read().supports_emoji()
}
/// Did this verification flow start from a verification request.
pub fn started_from_request(&self) -> bool {
self.inner.lock_ref().started_from_request()
self.inner.read().started_from_request()
}
/// Is this a verification that is veryfying one of our own devices.
@@ -283,18 +283,18 @@ impl Sas {
/// Have we confirmed that the short auth string matches.
pub fn have_we_confirmed(&self) -> bool {
self.inner.lock_ref().have_we_confirmed()
self.inner.read().have_we_confirmed()
}
/// Has the verification been accepted by both parties.
pub fn has_been_accepted(&self) -> bool {
self.inner.lock_ref().has_been_accepted()
self.inner.read().has_been_accepted()
}
/// Get info about the cancellation if the verification flow has been
/// cancelled.
pub fn cancel_info(&self) -> Option<CancelInfo> {
if let InnerSas::Cancelled(c) = &*self.inner.lock_ref() {
if let InnerSas::Cancelled(c) = &**self.inner.read() {
Some(c.state.as_ref().clone().into())
} else {
None
@@ -309,7 +309,9 @@ impl Sas {
#[cfg(test)]
#[allow(dead_code)]
pub(crate) fn set_creation_time(&self, time: matrix_sdk_common::instant::Instant) {
self.inner.lock_mut().set_creation_time(time)
self.inner.update(|inner| {
inner.set_creation_time(time);
});
}
fn start_helper(
@@ -331,7 +333,7 @@ impl Sas {
(
Sas {
inner: Arc::new(Mutable::new(inner)),
inner: SharedObservable::new(inner),
account,
identities_being_verified: identities,
flow_id: flow_id.into(),
@@ -415,7 +417,7 @@ impl Sas {
let account = identities.store.account.clone();
Ok(Sas {
inner: Arc::new(Mutable::new(inner)),
inner: SharedObservable::new(inner),
account,
identities_being_verified: identities,
flow_id: flow_id.into(),
@@ -445,12 +447,12 @@ impl Sas {
let old_state = self.state_debug();
let request = {
let mut guard = self.inner.lock_mut();
let mut guard = self.inner.write();
let sas: InnerSas = (*guard).clone();
let methods = settings.allowed_methods;
if let Some((sas, content)) = sas.accept(methods) {
*guard = sas;
Observable::set(&mut guard, sas);
Some(match content {
OwnedAcceptContent::ToDevice(c) => {
@@ -493,12 +495,12 @@ impl Sas {
) -> Result<(Vec<OutgoingVerificationRequest>, Option<SignatureUploadRequest>), CryptoStoreError>
{
let (contents, done) = {
let mut guard = self.inner.lock_mut();
let mut guard = self.inner.write();
let sas: InnerSas = (*guard).clone();
let (sas, contents) = sas.confirm();
*guard = sas;
Observable::set(&mut guard, sas);
(contents, guard.is_done())
};
@@ -565,7 +567,7 @@ impl Sas {
/// [`cancel()`]: #method.cancel
pub fn cancel_with_code(&self, code: CancelCode) -> Option<OutgoingVerificationRequest> {
let content = {
let mut guard = self.inner.lock_mut();
let mut guard = self.inner.write();
if let Some(request) = &self.request_handle {
request.cancel_with_code(&code);
@@ -573,7 +575,7 @@ impl Sas {
let sas: InnerSas = (*guard).clone();
let (sas, content) = sas.cancel(true, code);
*guard = sas;
Observable::set(&mut guard, sas);
content.map(|c| match c {
OutgoingContent::Room(room_id, content) => {
@@ -598,22 +600,22 @@ impl Sas {
/// Has the SAS verification flow timed out.
pub fn timed_out(&self) -> bool {
self.inner.lock_ref().timed_out()
self.inner.read().timed_out()
}
/// Are we in a state where we can show the short auth string.
pub fn can_be_presented(&self) -> bool {
self.inner.lock_ref().can_be_presented()
self.inner.read().can_be_presented()
}
/// Is the SAS flow done.
pub fn is_done(&self) -> bool {
self.inner.lock_ref().is_done()
self.inner.read().is_done()
}
/// Is the SAS flow canceled.
pub fn is_cancelled(&self) -> bool {
self.inner.lock_ref().is_cancelled()
self.inner.read().is_cancelled()
}
/// Get the emoji version of the short auth string.
@@ -621,7 +623,7 @@ impl Sas {
/// Returns None if we can't yet present the short auth string, otherwise
/// seven tuples containing the emoji and description.
pub fn emoji(&self) -> Option<[Emoji; 7]> {
self.inner.lock_ref().emoji()
self.inner.read().emoji()
}
/// Get the index of the emoji representing the short auth string
@@ -631,7 +633,7 @@ impl Sas {
/// converted to an emoji using the
/// [relevant spec entry](https://spec.matrix.org/unstable/client-server-api/#sas-method-emoji).
pub fn emoji_index(&self) -> Option<[u8; 7]> {
self.inner.lock_ref().emoji_index()
self.inner.read().emoji_index()
}
/// Get the decimal version of the short auth string.
@@ -640,7 +642,7 @@ impl Sas {
/// tuple containing three 4-digit integers that represent the short auth
/// string.
pub fn decimals(&self) -> Option<(u16, u16, u16)> {
self.inner.lock_ref().decimals()
self.inner.read().decimals()
}
/// Listen for changes in the SAS verification process.
@@ -732,16 +734,16 @@ impl Sas {
/// # anyhow::Ok(()) });
/// ```
pub fn changes(&self) -> impl Stream<Item = SasState> {
self.inner.signal_cloned().to_stream().map(|s| (&s).into())
self.inner.subscribe().map(|s| (&s).into())
}
/// Get the current state of the verification process.
pub fn state(&self) -> SasState {
(&*self.inner.lock_ref()).into()
(&**self.inner.read()).into()
}
fn state_debug(&self) -> State {
(&*self.inner.lock_ref()).into()
(&**self.inner.read()).into()
}
pub(crate) fn receive_any_event(
@@ -752,11 +754,11 @@ impl Sas {
let old_state = self.state_debug();
let content = {
let mut guard = self.inner.lock_mut();
let mut guard = self.inner.write();
let sas: InnerSas = (*guard).clone();
let (sas, content) = sas.receive_any_event(sender, content);
*guard = sas;
Observable::set(&mut guard, sas);
content
};
@@ -776,12 +778,12 @@ impl Sas {
let old_state = self.state_debug();
{
let mut guard = self.inner.lock_mut();
let mut guard = self.inner.write();
let sas: InnerSas = (*guard).clone();
if let Some(sas) = sas.mark_request_as_sent(request_id) {
*guard = sas;
Observable::set(&mut guard, sas);
} else {
error!(
flow_id = self.flow_id().as_str(),
@@ -803,11 +805,11 @@ impl Sas {
}
pub(crate) fn verified_devices(&self) -> Option<Arc<[ReadOnlyDevice]>> {
self.inner.lock_ref().verified_devices()
self.inner.read().verified_devices()
}
pub(crate) fn verified_identities(&self) -> Option<Arc<[ReadOnlyUserIdentities]>> {
self.inner.lock_ref().verified_identities()
self.inner.read().verified_identities()
}
pub(crate) fn content_to_request(&self, content: AnyToDeviceEventContent) -> ToDeviceRequest {

View File

@@ -41,9 +41,10 @@ web-sys = { version = "0.3.57", features = ["IdbKeyRange"] }
getrandom = { version = "0.2.6", features = ["js"] }
[dev-dependencies]
assert_matches = "1.5.0"
matrix-sdk-base = { path = "../matrix-sdk-base", features = ["testing"] }
matrix-sdk-common = { path = "../matrix-sdk-common", features = ["js"] }
matrix-sdk-crypto = { path = "../matrix-sdk-crypto", features = ["js", "testing"] }
matrix-sdk-test = { path = "../../testing/matrix-sdk-test" }
uuid = "1.0.0"
uuid = "1.3.0"
wasm-bindgen-test = "0.3.33"

View File

@@ -41,9 +41,8 @@ use web_sys::IdbKeyRange;
use crate::safe_encode::SafeEncode;
#[allow(non_snake_case)]
mod KEYS {
// STORES
mod keys {
// stores
pub const CORE: &str = "core";
pub const SESSION: &str = "session";
@@ -64,12 +63,12 @@ mod KEYS {
pub const DIRECT_WITHHELD_INFO: &str = "direct_withheld_info";
// KEYS
// keys
pub const STORE_CIPHER: &str = "store_cipher";
pub const ACCOUNT: &str = "account";
pub const PRIVATE_IDENTITY: &str = "private_identity";
// BACKUP v1
// backup v1
pub const BACKUP_KEYS: &str = "backup_keys";
pub const BACKUP_KEY_V1: &str = "backup_key_v1";
pub const RECOVERY_KEY_V1: &str = "recovery_key_v1";
@@ -155,21 +154,21 @@ impl IndexeddbCryptoStore {
// migrating to version 1
let db = evt.db();
db.create_object_store(KEYS::CORE)?;
db.create_object_store(KEYS::SESSION)?;
db.create_object_store(keys::CORE)?;
db.create_object_store(keys::SESSION)?;
db.create_object_store(KEYS::INBOUND_GROUP_SESSIONS)?;
db.create_object_store(KEYS::OUTBOUND_GROUP_SESSIONS)?;
db.create_object_store(KEYS::TRACKED_USERS)?;
db.create_object_store(KEYS::OLM_HASHES)?;
db.create_object_store(KEYS::DEVICES)?;
db.create_object_store(keys::INBOUND_GROUP_SESSIONS)?;
db.create_object_store(keys::OUTBOUND_GROUP_SESSIONS)?;
db.create_object_store(keys::TRACKED_USERS)?;
db.create_object_store(keys::OLM_HASHES)?;
db.create_object_store(keys::DEVICES)?;
db.create_object_store(KEYS::IDENTITIES)?;
db.create_object_store(KEYS::OUTGOING_SECRET_REQUESTS)?;
db.create_object_store(KEYS::UNSENT_SECRET_REQUESTS)?;
db.create_object_store(KEYS::SECRET_REQUESTS_BY_INFO)?;
db.create_object_store(keys::IDENTITIES)?;
db.create_object_store(keys::OUTGOING_SECRET_REQUESTS)?;
db.create_object_store(keys::UNSENT_SECRET_REQUESTS)?;
db.create_object_store(keys::SECRET_REQUESTS_BY_INFO)?;
db.create_object_store(KEYS::BACKUP_KEYS)?;
db.create_object_store(keys::BACKUP_KEYS)?;
} else if old_version < 1.1 {
// We changed how we store inbound group sessions, the key used to
// be a trippled of `(room_id, sender_key, session_id)` now it's a
@@ -179,8 +178,8 @@ impl IndexeddbCryptoStore {
let db = evt.db();
db.delete_object_store(KEYS::INBOUND_GROUP_SESSIONS)?;
db.create_object_store(KEYS::INBOUND_GROUP_SESSIONS)?;
db.delete_object_store(keys::INBOUND_GROUP_SESSIONS)?;
db.create_object_store(keys::INBOUND_GROUP_SESSIONS)?;
}
if old_version < 2.0 {
@@ -267,7 +266,7 @@ impl IndexeddbCryptoStore {
let ob = tx.object_store("matrix-sdk-crypto")?;
let store_cipher: Option<Vec<u8>> = ob
.get(&JsValue::from_str(KEYS::STORE_CIPHER))?
.get(&JsValue::from_str(keys::STORE_CIPHER))?
.await?
.map(|k| k.into_serde())
.transpose()?;
@@ -289,7 +288,7 @@ impl IndexeddbCryptoStore {
let ob = tx.object_store("matrix-sdk-crypto")?;
ob.put_key_val(
&JsValue::from_str(KEYS::STORE_CIPHER),
&JsValue::from_str(keys::STORE_CIPHER),
&JsValue::from_serde(&export.map_err(CryptoStoreError::backend)?)?,
)?;
tx.await.into_result()?;
@@ -368,23 +367,23 @@ macro_rules! impl_crypto_store {
impl_crypto_store! {
async fn save_changes(&self, changes: Changes) -> Result<()> {
let mut stores: Vec<&str> = [
(changes.account.is_some() || changes.private_identity.is_some(), KEYS::CORE),
(changes.recovery_key.is_some() || changes.backup_version.is_some(), KEYS::BACKUP_KEYS),
(!changes.sessions.is_empty(), KEYS::SESSION),
(changes.account.is_some() || changes.private_identity.is_some(), keys::CORE),
(changes.recovery_key.is_some() || changes.backup_version.is_some(), keys::BACKUP_KEYS),
(!changes.sessions.is_empty(), keys::SESSION),
(
!changes.devices.new.is_empty()
|| !changes.devices.changed.is_empty()
|| !changes.devices.deleted.is_empty(),
KEYS::DEVICES,
keys::DEVICES,
),
(
!changes.identities.new.is_empty() || !changes.identities.changed.is_empty(),
KEYS::IDENTITIES,
keys::IDENTITIES,
),
(!changes.inbound_group_sessions.is_empty(), KEYS::INBOUND_GROUP_SESSIONS),
(!changes.outbound_group_sessions.is_empty(), KEYS::OUTBOUND_GROUP_SESSIONS),
(!changes.message_hashes.is_empty(), KEYS::OLM_HASHES),
(!changes.withheld_session_info.is_empty(), KEYS::DIRECT_WITHHELD_INFO),
(!changes.inbound_group_sessions.is_empty(), keys::INBOUND_GROUP_SESSIONS),
(!changes.outbound_group_sessions.is_empty(), keys::OUTBOUND_GROUP_SESSIONS),
(!changes.message_hashes.is_empty(), keys::OLM_HASHES),
(!changes.withheld_session_info.is_empty(), keys::DIRECT_WITHHELD_INFO),
]
.iter()
.filter_map(|(id, key)| if *id { Some(*key) } else { None })
@@ -392,9 +391,9 @@ impl_crypto_store! {
if !changes.key_requests.is_empty() {
stores.extend([
KEYS::SECRET_REQUESTS_BY_INFO,
KEYS::UNSENT_SECRET_REQUESTS,
KEYS::OUTGOING_SECRET_REQUESTS,
keys::SECRET_REQUESTS_BY_INFO,
keys::UNSENT_SECRET_REQUESTS,
keys::OUTGOING_SECRET_REQUESTS,
])
}
@@ -426,39 +425,39 @@ impl_crypto_store! {
let backup_version = changes.backup_version;
if let Some(a) = &account_pickle {
tx.object_store(KEYS::CORE)?
.put_key_val(&JsValue::from_str(KEYS::ACCOUNT), &self.serialize_value(&a)?)?;
tx.object_store(keys::CORE)?
.put_key_val(&JsValue::from_str(keys::ACCOUNT), &self.serialize_value(&a)?)?;
}
if let Some(i) = &private_identity_pickle {
tx.object_store(KEYS::CORE)?.put_key_val(
&JsValue::from_str(KEYS::PRIVATE_IDENTITY),
tx.object_store(keys::CORE)?.put_key_val(
&JsValue::from_str(keys::PRIVATE_IDENTITY),
&self.serialize_value(i)?,
)?;
}
if let Some(a) = &recovery_key_pickle {
tx.object_store(KEYS::BACKUP_KEYS)?.put_key_val(
&JsValue::from_str(KEYS::RECOVERY_KEY_V1),
tx.object_store(keys::BACKUP_KEYS)?.put_key_val(
&JsValue::from_str(keys::RECOVERY_KEY_V1),
&self.serialize_value(&a)?,
)?;
}
if let Some(a) = &backup_version {
tx.object_store(KEYS::BACKUP_KEYS)?
.put_key_val(&JsValue::from_str(KEYS::BACKUP_KEY_V1), &self.serialize_value(&a)?)?;
tx.object_store(keys::BACKUP_KEYS)?
.put_key_val(&JsValue::from_str(keys::BACKUP_KEY_V1), &self.serialize_value(&a)?)?;
}
if !changes.sessions.is_empty() {
let sessions = tx.object_store(KEYS::SESSION)?;
let sessions = tx.object_store(keys::SESSION)?;
for session in &changes.sessions {
let sender_key = session.sender_key().to_base64();
let session_id = session.session_id();
let pickle = session.pickle().await;
let key = self.encode_key(KEYS::SESSION, (&sender_key, session_id));
let key = self.encode_key(keys::SESSION, (&sender_key, session_id));
sessions.put_key_val(&key, &self.serialize_value(&pickle)?)?;
@@ -466,12 +465,12 @@ impl_crypto_store! {
}
if !changes.inbound_group_sessions.is_empty() {
let sessions = tx.object_store(KEYS::INBOUND_GROUP_SESSIONS)?;
let sessions = tx.object_store(keys::INBOUND_GROUP_SESSIONS)?;
for session in changes.inbound_group_sessions {
let room_id = session.room_id();
let session_id = session.session_id();
let key = self.encode_key(KEYS::INBOUND_GROUP_SESSIONS, (room_id, session_id));
let key = self.encode_key(keys::INBOUND_GROUP_SESSIONS, (room_id, session_id));
let pickle = session.pickle().await;
sessions.put_key_val(&key, &self.serialize_value(&pickle)?)?;
@@ -479,13 +478,13 @@ impl_crypto_store! {
}
if !changes.outbound_group_sessions.is_empty() {
let sessions = tx.object_store(KEYS::OUTBOUND_GROUP_SESSIONS)?;
let sessions = tx.object_store(keys::OUTBOUND_GROUP_SESSIONS)?;
for session in changes.outbound_group_sessions {
let room_id = session.room_id();
let pickle = session.pickle().await;
sessions.put_key_val(
&self.encode_key(KEYS::OUTBOUND_GROUP_SESSIONS, room_id),
&self.encode_key(keys::OUTBOUND_GROUP_SESSIONS, room_id),
&self.serialize_value(&pickle)?,
)?;
}
@@ -498,9 +497,9 @@ impl_crypto_store! {
let withheld_session_info = changes.withheld_session_info;
if !device_changes.new.is_empty() || !device_changes.changed.is_empty() {
let device_store = tx.object_store(KEYS::DEVICES)?;
let device_store = tx.object_store(keys::DEVICES)?;
for device in device_changes.new.iter().chain(&device_changes.changed) {
let key = self.encode_key(KEYS::DEVICES, (device.user_id(), device.device_id()));
let key = self.encode_key(keys::DEVICES, (device.user_id(), device.device_id()));
let device = self.serialize_value(&device)?;
device_store.put_key_val(&key, &device)?;
@@ -508,43 +507,43 @@ impl_crypto_store! {
}
if !device_changes.deleted.is_empty() {
let device_store = tx.object_store(KEYS::DEVICES)?;
let device_store = tx.object_store(keys::DEVICES)?;
for device in &device_changes.deleted {
let key = self.encode_key(KEYS::DEVICES, (device.user_id(), device.device_id()));
let key = self.encode_key(keys::DEVICES, (device.user_id(), device.device_id()));
device_store.delete(&key)?;
}
}
if !identity_changes.changed.is_empty() || !identity_changes.new.is_empty() {
let identities = tx.object_store(KEYS::IDENTITIES)?;
let identities = tx.object_store(keys::IDENTITIES)?;
for identity in identity_changes.changed.iter().chain(&identity_changes.new) {
identities.put_key_val(
&self.encode_key(KEYS::IDENTITIES, identity.user_id()),
&self.encode_key(keys::IDENTITIES, identity.user_id()),
&self.serialize_value(&identity)?,
)?;
}
}
if !olm_hashes.is_empty() {
let hashes = tx.object_store(KEYS::OLM_HASHES)?;
let hashes = tx.object_store(keys::OLM_HASHES)?;
for hash in &olm_hashes {
hashes.put_key_val(
&self.encode_key(KEYS::OLM_HASHES, (&hash.sender_key, &hash.hash)),
&self.encode_key(keys::OLM_HASHES, (&hash.sender_key, &hash.hash)),
&JsValue::TRUE,
)?;
}
}
if !key_requests.is_empty() {
let secret_requests_by_info = tx.object_store(KEYS::SECRET_REQUESTS_BY_INFO)?;
let unsent_secret_requests = tx.object_store(KEYS::UNSENT_SECRET_REQUESTS)?;
let outgoing_secret_requests = tx.object_store(KEYS::OUTGOING_SECRET_REQUESTS)?;
let secret_requests_by_info = tx.object_store(keys::SECRET_REQUESTS_BY_INFO)?;
let unsent_secret_requests = tx.object_store(keys::UNSENT_SECRET_REQUESTS)?;
let outgoing_secret_requests = tx.object_store(keys::OUTGOING_SECRET_REQUESTS)?;
for key_request in &key_requests {
let key_request_id =
self.encode_key(KEYS::KEY_REQUEST, key_request.request_id.as_str());
self.encode_key(keys::KEY_REQUEST, key_request.request_id.as_str());
secret_requests_by_info.put_key_val(
&self.encode_key(KEYS::KEY_REQUEST, key_request.info.as_key()),
&self.encode_key(keys::KEY_REQUEST, key_request.info.as_key()),
&key_request_id,
)?;
@@ -584,8 +583,8 @@ impl_crypto_store! {
async fn load_tracked_users(&self) -> Result<Vec<TrackedUser>> {
let tx = self
.inner
.transaction_on_one_with_mode(KEYS::TRACKED_USERS, IdbTransactionMode::Readonly)?;
let os = tx.object_store(KEYS::TRACKED_USERS)?;
.transaction_on_one_with_mode(keys::TRACKED_USERS, IdbTransactionMode::Readonly)?;
let os = tx.object_store(keys::TRACKED_USERS)?;
let user_ids = os.get_all_keys()?.await?;
let mut users = Vec::new();
@@ -609,11 +608,11 @@ impl_crypto_store! {
if let Some(value) = self
.inner
.transaction_on_one_with_mode(
KEYS::OUTBOUND_GROUP_SESSIONS,
keys::OUTBOUND_GROUP_SESSIONS,
IdbTransactionMode::Readonly,
)?
.object_store(KEYS::OUTBOUND_GROUP_SESSIONS)?
.get(&self.encode_key(KEYS::OUTBOUND_GROUP_SESSIONS, room_id))?
.object_store(keys::OUTBOUND_GROUP_SESSIONS)?
.get(&self.encode_key(keys::OUTBOUND_GROUP_SESSIONS, room_id))?
.await?
{
Ok(Some(
@@ -635,11 +634,11 @@ impl_crypto_store! {
) -> Result<Option<GossipRequest>> {
// in this internal we expect key to already be escaped or encrypted
let jskey = JsValue::from_str(request_id.as_str());
let dbs = [KEYS::OUTGOING_SECRET_REQUESTS, KEYS::UNSENT_SECRET_REQUESTS];
let dbs = [keys::OUTGOING_SECRET_REQUESTS, keys::UNSENT_SECRET_REQUESTS];
let tx = self.inner.transaction_on_multi_with_mode(&dbs, IdbTransactionMode::Readonly)?;
let request = tx
.object_store(KEYS::OUTGOING_SECRET_REQUESTS)?
.object_store(keys::OUTGOING_SECRET_REQUESTS)?
.get(&jskey)?
.await?
.map(|i| self.deserialize_value(i))
@@ -647,7 +646,7 @@ impl_crypto_store! {
Ok(match request {
None => tx
.object_store(KEYS::UNSENT_SECRET_REQUESTS)?
.object_store(keys::UNSENT_SECRET_REQUESTS)?
.get(&jskey)?
.await?
.map(|i| self.deserialize_value(i))
@@ -659,9 +658,9 @@ impl_crypto_store! {
async fn load_account(&self) -> Result<Option<ReadOnlyAccount>> {
if let Some(pickle) = self
.inner
.transaction_on_one_with_mode(KEYS::CORE, IdbTransactionMode::Readonly)?
.object_store(KEYS::CORE)?
.get(&JsValue::from_str(KEYS::ACCOUNT))?
.transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readonly)?
.object_store(keys::CORE)?
.get(&JsValue::from_str(keys::ACCOUNT))?
.await?
{
let pickle = self.deserialize_value(pickle)?;
@@ -690,9 +689,9 @@ impl_crypto_store! {
async fn load_identity(&self) -> Result<Option<PrivateCrossSigningIdentity>> {
if let Some(pickle) = self
.inner
.transaction_on_one_with_mode(KEYS::CORE, IdbTransactionMode::Readonly)?
.object_store(KEYS::CORE)?
.get(&JsValue::from_str(KEYS::PRIVATE_IDENTITY))?
.transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readonly)?
.object_store(keys::CORE)?
.get(&JsValue::from_str(keys::PRIVATE_IDENTITY))?
.await?
{
let pickle = self.deserialize_value(pickle)?;
@@ -711,11 +710,11 @@ impl_crypto_store! {
let account_info = self.get_account_info().ok_or(CryptoStoreError::AccountUnset)?;
if self.session_cache.get(sender_key).is_none() {
let range = self.encode_to_range(KEYS::SESSION, sender_key)?;
let range = self.encode_to_range(keys::SESSION, sender_key)?;
let sessions: Vec<Session> = self
.inner
.transaction_on_one_with_mode(KEYS::SESSION, IdbTransactionMode::Readonly)?
.object_store(KEYS::SESSION)?
.transaction_on_one_with_mode(keys::SESSION, IdbTransactionMode::Readonly)?
.object_store(keys::SESSION)?
.get_all_with_key(&range)?
.await?
.iter()
@@ -741,14 +740,14 @@ impl_crypto_store! {
room_id: &RoomId,
session_id: &str,
) -> Result<Option<InboundGroupSession>> {
let key = self.encode_key(KEYS::INBOUND_GROUP_SESSIONS, (room_id, session_id));
let key = self.encode_key(keys::INBOUND_GROUP_SESSIONS, (room_id, session_id));
if let Some(pickle) = self
.inner
.transaction_on_one_with_mode(
KEYS::INBOUND_GROUP_SESSIONS,
keys::INBOUND_GROUP_SESSIONS,
IdbTransactionMode::Readonly,
)?
.object_store(KEYS::INBOUND_GROUP_SESSIONS)?
.object_store(keys::INBOUND_GROUP_SESSIONS)?
.get(&key)?
.await?
{
@@ -763,10 +762,10 @@ impl_crypto_store! {
Ok(self
.inner
.transaction_on_one_with_mode(
KEYS::INBOUND_GROUP_SESSIONS,
keys::INBOUND_GROUP_SESSIONS,
IdbTransactionMode::Readonly,
)?
.object_store(KEYS::INBOUND_GROUP_SESSIONS)?
.object_store(keys::INBOUND_GROUP_SESSIONS)?
.get_all()?
.await?
.iter()
@@ -819,8 +818,8 @@ impl_crypto_store! {
async fn save_tracked_users(&self, users: &[(&UserId, bool)]) -> Result<()> {
let tx = self
.inner
.transaction_on_one_with_mode(KEYS::TRACKED_USERS, IdbTransactionMode::Readwrite)?;
let os = tx.object_store(KEYS::TRACKED_USERS)?;
.transaction_on_one_with_mode(keys::TRACKED_USERS, IdbTransactionMode::Readwrite)?;
let os = tx.object_store(keys::TRACKED_USERS)?;
for (user, dirty) in users {
os.put_key_val(&JsValue::from_str(user.as_str()), &JsValue::from(*dirty))?;
@@ -835,11 +834,11 @@ impl_crypto_store! {
user_id: &UserId,
device_id: &DeviceId,
) -> Result<Option<ReadOnlyDevice>> {
let key = self.encode_key(KEYS::DEVICES, (user_id, device_id));
let key = self.encode_key(keys::DEVICES, (user_id, device_id));
Ok(self
.inner
.transaction_on_one_with_mode(KEYS::DEVICES, IdbTransactionMode::Readonly)?
.object_store(KEYS::DEVICES)?
.transaction_on_one_with_mode(keys::DEVICES, IdbTransactionMode::Readonly)?
.object_store(keys::DEVICES)?
.get(&key)?
.await?
.map(|i| self.deserialize_value(i))
@@ -850,11 +849,11 @@ impl_crypto_store! {
&self,
user_id: &UserId,
) -> Result<HashMap<OwnedDeviceId, ReadOnlyDevice>> {
let range = self.encode_to_range(KEYS::DEVICES, user_id)?;
let range = self.encode_to_range(keys::DEVICES, user_id)?;
Ok(self
.inner
.transaction_on_one_with_mode(KEYS::DEVICES, IdbTransactionMode::Readonly)?
.object_store(KEYS::DEVICES)?
.transaction_on_one_with_mode(keys::DEVICES, IdbTransactionMode::Readonly)?
.object_store(keys::DEVICES)?
.get_all_with_key(&range)?
.await?
.iter()
@@ -868,9 +867,9 @@ impl_crypto_store! {
async fn get_user_identity(&self, user_id: &UserId) -> Result<Option<ReadOnlyUserIdentities>> {
Ok(self
.inner
.transaction_on_one_with_mode(KEYS::IDENTITIES, IdbTransactionMode::Readonly)?
.object_store(KEYS::IDENTITIES)?
.get(&self.encode_key(KEYS::IDENTITIES, user_id))?
.transaction_on_one_with_mode(keys::IDENTITIES, IdbTransactionMode::Readonly)?
.object_store(keys::IDENTITIES)?
.get(&self.encode_key(keys::IDENTITIES, user_id))?
.await?
.map(|i| self.deserialize_value(i))
.transpose()?)
@@ -879,9 +878,9 @@ impl_crypto_store! {
async fn is_message_known(&self, hash: &OlmMessageHash) -> Result<bool> {
Ok(self
.inner
.transaction_on_one_with_mode(KEYS::OLM_HASHES, IdbTransactionMode::Readonly)?
.object_store(KEYS::OLM_HASHES)?
.get(&self.encode_key(KEYS::OLM_HASHES, (&hash.sender_key, &hash.hash)))?
.transaction_on_one_with_mode(keys::OLM_HASHES, IdbTransactionMode::Readonly)?
.object_store(keys::OLM_HASHES)?
.get(&self.encode_key(keys::OLM_HASHES, (&hash.sender_key, &hash.hash)))?
.await?
.is_some())
}
@@ -893,11 +892,11 @@ impl_crypto_store! {
let id = self
.inner
.transaction_on_one_with_mode(
KEYS::SECRET_REQUESTS_BY_INFO,
keys::SECRET_REQUESTS_BY_INFO,
IdbTransactionMode::Readonly,
)?
.object_store(KEYS::SECRET_REQUESTS_BY_INFO)?
.get(&self.encode_key(KEYS::KEY_REQUEST, key_info.as_key()))?
.object_store(keys::SECRET_REQUESTS_BY_INFO)?
.get(&self.encode_key(keys::KEY_REQUEST, key_info.as_key()))?
.await?
.and_then(|i| i.as_string());
if let Some(id) = id {
@@ -911,10 +910,10 @@ impl_crypto_store! {
Ok(self
.inner
.transaction_on_one_with_mode(
KEYS::UNSENT_SECRET_REQUESTS,
keys::UNSENT_SECRET_REQUESTS,
IdbTransactionMode::Readonly,
)?
.object_store(KEYS::UNSENT_SECRET_REQUESTS)?
.object_store(keys::UNSENT_SECRET_REQUESTS)?
.get_all()?
.await?
.iter()
@@ -923,16 +922,16 @@ impl_crypto_store! {
}
async fn delete_outgoing_secret_requests(&self, request_id: &TransactionId) -> Result<()> {
let jskey = self.encode_key(KEYS::KEY_REQUEST, request_id); //.as_str());
let jskey = self.encode_key(keys::KEY_REQUEST, request_id); //.as_str());
let dbs = [
KEYS::OUTGOING_SECRET_REQUESTS,
KEYS::UNSENT_SECRET_REQUESTS,
KEYS::SECRET_REQUESTS_BY_INFO,
keys::OUTGOING_SECRET_REQUESTS,
keys::UNSENT_SECRET_REQUESTS,
keys::SECRET_REQUESTS_BY_INFO,
];
let tx = self.inner.transaction_on_multi_with_mode(&dbs, IdbTransactionMode::Readwrite)?;
let request: Option<GossipRequest> = tx
.object_store(KEYS::OUTGOING_SECRET_REQUESTS)?
.object_store(keys::OUTGOING_SECRET_REQUESTS)?
.get(&jskey)?
.await?
.map(|i| self.deserialize_value(i))
@@ -940,7 +939,7 @@ impl_crypto_store! {
let request = match request {
None => tx
.object_store(KEYS::UNSENT_SECRET_REQUESTS)?
.object_store(keys::UNSENT_SECRET_REQUESTS)?
.get(&jskey)?
.await?
.map(|i| self.deserialize_value(i))
@@ -949,12 +948,12 @@ impl_crypto_store! {
};
if let Some(inner) = request {
tx.object_store(KEYS::SECRET_REQUESTS_BY_INFO)?
.delete(&self.encode_key(KEYS::KEY_REQUEST, inner.info.as_key()))?;
tx.object_store(keys::SECRET_REQUESTS_BY_INFO)?
.delete(&self.encode_key(keys::KEY_REQUEST, inner.info.as_key()))?;
}
tx.object_store(KEYS::UNSENT_SECRET_REQUESTS)?.delete(&jskey)?;
tx.object_store(KEYS::OUTGOING_SECRET_REQUESTS)?.delete(&jskey)?;
tx.object_store(keys::UNSENT_SECRET_REQUESTS)?.delete(&jskey)?;
tx.object_store(keys::OUTGOING_SECRET_REQUESTS)?.delete(&jskey)?;
tx.await.into_result().map_err(|e| e.into())
}
@@ -963,17 +962,17 @@ impl_crypto_store! {
let key = {
let tx = self
.inner
.transaction_on_one_with_mode(KEYS::BACKUP_KEYS, IdbTransactionMode::Readonly)?;
let store = tx.object_store(KEYS::BACKUP_KEYS)?;
.transaction_on_one_with_mode(keys::BACKUP_KEYS, IdbTransactionMode::Readonly)?;
let store = tx.object_store(keys::BACKUP_KEYS)?;
let backup_version = store
.get(&JsValue::from_str(KEYS::BACKUP_KEY_V1))?
.get(&JsValue::from_str(keys::BACKUP_KEY_V1))?
.await?
.map(|i| self.deserialize_value(i))
.transpose()?;
let recovery_key = store
.get(&JsValue::from_str(KEYS::RECOVERY_KEY_V1))?
.get(&JsValue::from_str(keys::RECOVERY_KEY_V1))?
.await?
.map(|i| self.deserialize_value(i))
.transpose()?;

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,748 @@
// Copyright 2021 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use gloo_utils::format::JsValueSerdeExt;
use indexed_db_futures::{prelude::*, request::OpenDbRequest, IdbDatabase, IdbVersionChangeEvent};
use js_sys::Date as JsDate;
use matrix_sdk_base::StateStoreDataKey;
use matrix_sdk_store_encryption::StoreCipher;
use serde::{Deserialize, Serialize};
use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue};
use wasm_bindgen::JsValue;
use web_sys::IdbTransactionMode;
use super::{
deserialize_event, encode_key, encode_to_range, keys, serialize_event, Result, ALL_STORES,
};
use crate::IndexeddbStateStoreError;
const CURRENT_DB_VERSION: u32 = 4;
const CURRENT_META_DB_VERSION: u32 = 2;
/// Sometimes Migrations can't proceed without having to drop existing
/// data. This allows you to configure, how these cases should be handled.
#[allow(dead_code)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MigrationConflictStrategy {
/// Just drop the data, we don't care that we have to sync again
Drop,
/// Raise a [`IndexeddbStateStoreError::MigrationConflict`] error with the
/// path to the DB in question. The caller then has to take care about
/// what they want to do and try again after.
Raise,
/// Default.
BackupAndDrop,
}
#[derive(Clone, Serialize, Deserialize)]
struct StoreKeyWrapper(Vec<u8>);
mod old_keys {
pub const SESSION: &str = "session";
pub const SYNC_TOKEN: &str = "sync_token";
}
pub async fn upgrade_meta_db(
meta_name: &str,
passphrase: Option<&str>,
) -> Result<(IdbDatabase, Option<Arc<StoreCipher>>)> {
// Meta database.
let mut db_req: OpenDbRequest = IdbDatabase::open_u32(meta_name, CURRENT_META_DB_VERSION)?;
db_req.set_on_upgrade_needed(Some(|evt: &IdbVersionChangeEvent| -> Result<(), JsValue> {
let db = evt.db();
let old_version = evt.old_version() as u32;
if old_version < 1 {
db.create_object_store(keys::INTERNAL_STATE)?;
}
if old_version < 2 {
db.create_object_store(keys::BACKUPS_META)?;
}
Ok(())
}));
let meta_db: IdbDatabase = db_req.into_future().await?;
let store_cipher = if let Some(passphrase) = passphrase {
let tx: IdbTransaction<'_> = meta_db
.transaction_on_one_with_mode(keys::INTERNAL_STATE, IdbTransactionMode::Readwrite)?;
let ob = tx.object_store(keys::INTERNAL_STATE)?;
let cipher = if let Some(StoreKeyWrapper(inner)) = ob
.get(&JsValue::from_str(keys::STORE_KEY))?
.await?
.map(|v| v.into_serde())
.transpose()?
{
StoreCipher::import(passphrase, &inner)?
} else {
let cipher = StoreCipher::new()?;
#[cfg(not(test))]
let export = cipher.export(passphrase)?;
#[cfg(test)]
let export = cipher._insecure_export_fast_for_testing(passphrase)?;
ob.put_key_val(
&JsValue::from_str(keys::STORE_KEY),
&JsValue::from_serde(&StoreKeyWrapper(export))?,
)?;
cipher
};
tx.await.into_result()?;
Some(Arc::new(cipher))
} else {
None
};
Ok((meta_db, store_cipher))
}
// Helper struct for upgrading the inner DB.
#[derive(Debug, Clone, Default)]
pub struct OngoingMigration {
// Names of stores to drop.
drop_stores: HashSet<&'static str>,
// Names of stores to create.
create_stores: HashSet<&'static str>,
// Store name => key-value data to add.
data: HashMap<&'static str, Vec<(JsValue, JsValue)>>,
}
impl OngoingMigration {
/// Merge this migration with the given one.
fn merge(&mut self, other: OngoingMigration) {
self.drop_stores.extend(other.drop_stores);
self.create_stores.extend(other.create_stores);
for (store, data) in other.data {
let entry = self.data.entry(store).or_default();
entry.extend(data);
}
}
}
pub async fn upgrade_inner_db(
name: &str,
store_cipher: Option<&StoreCipher>,
migration_strategy: MigrationConflictStrategy,
meta_db: &IdbDatabase,
) -> Result<IdbDatabase> {
let mut migration = OngoingMigration::default();
{
// This is a hack, we need to open the database a first time to get the current
// version.
// The indexed_db_futures crate doesn't let us access the transaction so we
// can't migrate data inside the `onupgradeneeded` callback. Instead we see if
// we need to migrate some data before the upgrade, then let the store process
// the upgrade.
// See <https://github.com/Alorel/rust-indexed-db/issues/20>
let has_store_cipher = store_cipher.is_some();
let pre_db = IdbDatabase::open(name)?.into_future().await?;
// Even if the web-sys bindings expose the version as a f64, the IndexedDB API
// works with an unsigned integer.
// See <https://github.com/rustwasm/wasm-bindgen/issues/1149>
let mut old_version = pre_db.version() as u32;
// Inside the `onupgradeneeded` callback we would know whether it's a new DB
// because the old version would be set to 0, here it is already set to 1 so we
// check if the stores exist.
if old_version == 1 && pre_db.object_store_names().next().is_none() {
old_version = 0;
}
// Upgrades to v1 and v2 (re)create empty stores, while the other upgrades
// change data that is already in the stores, so we use exclusive branches here.
if old_version == 0 {
migration.create_stores.extend(ALL_STORES);
} else if old_version < 2 && has_store_cipher {
match migration_strategy {
MigrationConflictStrategy::BackupAndDrop => {
backup_v1(&pre_db, meta_db).await?;
migration.drop_stores.extend(V1_STORES);
migration.create_stores.extend(ALL_STORES);
}
MigrationConflictStrategy::Drop => {
migration.drop_stores.extend(V1_STORES);
migration.create_stores.extend(ALL_STORES);
}
MigrationConflictStrategy::Raise => {
return Err(IndexeddbStateStoreError::MigrationConflict {
name: name.to_owned(),
old_version,
new_version: CURRENT_DB_VERSION,
});
}
}
} else {
if old_version < 3 {
migrate_to_v3(&pre_db, store_cipher).await?;
}
if old_version < 4 {
migration.merge(migrate_to_v4(&pre_db, store_cipher).await?);
}
}
pre_db.close();
}
let mut db_req: OpenDbRequest = IdbDatabase::open_u32(name, CURRENT_DB_VERSION)?;
db_req.set_on_upgrade_needed(Some(move |evt: &IdbVersionChangeEvent| -> Result<(), JsValue> {
// Changing the format can only happen in the upgrade procedure
for store in &migration.drop_stores {
evt.db().delete_object_store(store)?;
}
for store in &migration.create_stores {
evt.db().create_object_store(store)?;
}
Ok(())
}));
let db = db_req.into_future().await?;
// Finally, we can add data to the newly created tables if needed.
if !migration.data.is_empty() {
let stores: Vec<_> = migration.data.keys().copied().collect();
let tx = db.transaction_on_multi_with_mode(&stores, IdbTransactionMode::Readwrite)?;
for (name, data) in migration.data {
let store = tx.object_store(name)?;
for (key, value) in data {
store.put_key_val(&key, &value)?;
}
}
tx.await.into_result()?;
}
Ok(db)
}
pub const V1_STORES: &[&str] = &[
old_keys::SESSION,
keys::ACCOUNT_DATA,
keys::MEMBERS,
keys::PROFILES,
keys::DISPLAY_NAMES,
keys::JOINED_USER_IDS,
keys::INVITED_USER_IDS,
keys::ROOM_STATE,
keys::ROOM_INFOS,
keys::PRESENCE,
keys::ROOM_ACCOUNT_DATA,
keys::STRIPPED_ROOM_INFOS,
keys::STRIPPED_MEMBERS,
keys::STRIPPED_ROOM_STATE,
keys::STRIPPED_JOINED_USER_IDS,
keys::STRIPPED_INVITED_USER_IDS,
keys::ROOM_USER_RECEIPTS,
keys::ROOM_EVENT_RECEIPTS,
keys::MEDIA,
keys::CUSTOM,
old_keys::SYNC_TOKEN,
];
async fn backup_v1(source: &IdbDatabase, meta: &IdbDatabase) -> Result<()> {
let now = JsDate::now();
let backup_name = format!("backup-{}-{now}", source.name());
let mut db_req: OpenDbRequest = IdbDatabase::open_f64(&backup_name, source.version())?;
db_req.set_on_upgrade_needed(Some(move |evt: &IdbVersionChangeEvent| -> Result<(), JsValue> {
// migrating to version 1
let db = evt.db();
for name in V1_STORES {
db.create_object_store(name)?;
}
Ok(())
}));
let target = db_req.into_future().await?;
for name in V1_STORES {
let source_tx = source.transaction_on_one_with_mode(name, IdbTransactionMode::Readonly)?;
let source_obj = source_tx.object_store(name)?;
let Some(curs) = source_obj
.open_cursor()?
.await? else {
continue;
};
let data = curs.into_vec(0).await?;
let target_tx = target.transaction_on_one_with_mode(name, IdbTransactionMode::Readwrite)?;
let target_obj = target_tx.object_store(name)?;
for kv in data {
target_obj.put_key_val(kv.key(), kv.value())?;
}
target_tx.await.into_result()?;
}
let tx =
meta.transaction_on_one_with_mode(keys::BACKUPS_META, IdbTransactionMode::Readwrite)?;
let backup_store = tx.object_store(keys::BACKUPS_META)?;
backup_store.put_key_val(&JsValue::from_f64(now), &JsValue::from_str(&backup_name))?;
tx.await;
Ok(())
}
async fn v3_fix_store(
store: &IdbObjectStore<'_>,
store_cipher: Option<&StoreCipher>,
) -> Result<()> {
fn maybe_fix_json(raw_json: &RawJsonValue) -> Result<Option<JsonValue>> {
let json = raw_json.get();
if json.contains(r#""content":null"#) {
let mut value: JsonValue = serde_json::from_str(json)?;
if let Some(content) = value.get_mut("content") {
if matches!(content, JsonValue::Null) {
*content = JsonValue::Object(Default::default());
return Ok(Some(value));
}
}
}
Ok(None)
}
let cursor = store.open_cursor()?.await?;
if let Some(cursor) = cursor {
loop {
let raw_json: Box<RawJsonValue> = deserialize_event(store_cipher, cursor.value())?;
if let Some(fixed_json) = maybe_fix_json(&raw_json)? {
cursor.update(&serialize_event(store_cipher, &fixed_json)?)?.await?;
}
if !cursor.continue_cursor()?.await? {
break;
}
}
}
Ok(())
}
/// Fix serialized redacted state events.
async fn migrate_to_v3(db: &IdbDatabase, store_cipher: Option<&StoreCipher>) -> Result<()> {
let tx = db.transaction_on_multi_with_mode(
&[keys::ROOM_STATE, keys::ROOM_INFOS],
IdbTransactionMode::Readwrite,
)?;
v3_fix_store(&tx.object_store(keys::ROOM_STATE)?, store_cipher).await?;
v3_fix_store(&tx.object_store(keys::ROOM_INFOS)?, store_cipher).await?;
tx.await.into_result().map_err(|e| e.into())
}
/// Move the content of the SYNC_TOKEN and SESSION stores to the new KV store.
async fn migrate_to_v4(
db: &IdbDatabase,
store_cipher: Option<&StoreCipher>,
) -> Result<OngoingMigration> {
let tx = db.transaction_on_multi_with_mode(
&[old_keys::SYNC_TOKEN, old_keys::SESSION],
IdbTransactionMode::Readonly,
)?;
let mut values = Vec::new();
// Sync token
let sync_token_store = tx.object_store(old_keys::SYNC_TOKEN)?;
let sync_token = sync_token_store.get(&JsValue::from_str(old_keys::SYNC_TOKEN))?.await?;
if let Some(sync_token) = sync_token {
values.push((
encode_key(store_cipher, StateStoreDataKey::SYNC_TOKEN, StateStoreDataKey::SYNC_TOKEN),
sync_token,
));
}
// Filters
let session_store = tx.object_store(old_keys::SESSION)?;
let range =
encode_to_range(store_cipher, StateStoreDataKey::FILTER, StateStoreDataKey::FILTER)?;
if let Some(cursor) = session_store.open_cursor_with_range(&range)?.await? {
while let Some(key) = cursor.key() {
let value = cursor.value();
values.push((key, value));
cursor.continue_cursor()?.await?;
}
}
tx.await.into_result()?;
let mut data = HashMap::new();
if !values.is_empty() {
data.insert(keys::KV, values);
}
Ok(OngoingMigration {
drop_stores: [old_keys::SYNC_TOKEN, old_keys::SESSION].into_iter().collect(),
create_stores: [keys::KV].into_iter().collect(),
data,
})
}
#[cfg(all(test, target_arch = "wasm32"))]
mod tests {
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
use assert_matches::assert_matches;
use indexed_db_futures::prelude::*;
use matrix_sdk_base::{StateStore, StateStoreDataKey, StoreError};
use matrix_sdk_test::async_test;
use ruma::{
events::{AnySyncStateEvent, StateEventType},
room_id,
};
use serde_json::json;
use uuid::Uuid;
use wasm_bindgen::JsValue;
use super::{
old_keys, MigrationConflictStrategy, CURRENT_DB_VERSION, CURRENT_META_DB_VERSION, V1_STORES,
};
use crate::{
safe_encode::SafeEncode,
state_store::{encode_key, keys, serialize_event, Result, ALL_STORES},
IndexeddbStateStore, IndexeddbStateStoreError,
};
const CUSTOM_DATA_KEY: &[u8] = b"custom_data_key";
const CUSTOM_DATA: &[u8] = b"some_custom_data";
pub async fn create_fake_db(name: &str, version: u32) -> Result<IdbDatabase> {
let mut db_req: OpenDbRequest = IdbDatabase::open_u32(name, version)?;
db_req.set_on_upgrade_needed(Some(
move |evt: &IdbVersionChangeEvent| -> Result<(), JsValue> {
let db = evt.db();
// Initialize stores.
if version < 4 {
for name in V1_STORES {
db.create_object_store(name)?;
}
} else {
for name in ALL_STORES {
db.create_object_store(name)?;
}
}
Ok(())
},
));
db_req.into_future().await.map_err(Into::into)
}
#[async_test]
pub async fn test_new_store() -> Result<()> {
let name = format!("new-store-no-cipher-{}", Uuid::new_v4().as_hyphenated().to_string());
// this transparently migrates to the latest version
let store = IndexeddbStateStore::builder().name(name).build().await?;
// this didn't create any backup
assert_eq!(store.has_backups().await?, false);
// simple check that the layout exists.
assert_eq!(store.get_custom_value(CUSTOM_DATA_KEY).await?, None);
// Check versions.
assert_eq!(store.version(), CURRENT_DB_VERSION);
assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION);
Ok(())
}
#[async_test]
pub async fn test_migrating_v1_to_v2_plain() -> Result<()> {
let name = format!("migrating-v2-no-cipher-{}", Uuid::new_v4().as_hyphenated().to_string());
// Create and populate db.
{
let db = create_fake_db(&name, 1).await?;
let tx =
db.transaction_on_one_with_mode(keys::CUSTOM, IdbTransactionMode::Readwrite)?;
let custom = tx.object_store(keys::CUSTOM)?;
let jskey = JsValue::from_str(
core::str::from_utf8(CUSTOM_DATA_KEY).map_err(StoreError::Codec)?,
);
custom.put_key_val(&jskey, &serialize_event(None, &CUSTOM_DATA)?)?;
tx.await.into_result()?;
db.close();
}
// this transparently migrates to the latest version
let store = IndexeddbStateStore::builder().name(name).build().await?;
// this didn't create any backup
assert_eq!(store.has_backups().await?, false);
// Custom data is still there.
let stored_data = assert_matches!(
store.get_custom_value(CUSTOM_DATA_KEY).await?,
Some(d) => d
);
assert_eq!(stored_data, CUSTOM_DATA);
// Check versions.
assert_eq!(store.version(), CURRENT_DB_VERSION);
assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION);
Ok(())
}
#[async_test]
pub async fn test_migrating_v1_to_v2_with_pw() -> Result<()> {
let name =
format!("migrating-v2-with-cipher-{}", Uuid::new_v4().as_hyphenated().to_string());
let passphrase = "somepassphrase".to_owned();
// Create and populate db.
{
let db = create_fake_db(&name, 1).await?;
let tx =
db.transaction_on_one_with_mode(keys::CUSTOM, IdbTransactionMode::Readwrite)?;
let custom = tx.object_store(keys::CUSTOM)?;
let jskey = JsValue::from_str(
core::str::from_utf8(CUSTOM_DATA_KEY).map_err(StoreError::Codec)?,
);
custom.put_key_val(&jskey, &serialize_event(None, &CUSTOM_DATA)?)?;
tx.await.into_result()?;
db.close();
}
// this transparently migrates to the latest version
let store =
IndexeddbStateStore::builder().name(name).passphrase(passphrase).build().await?;
// this creates a backup by default
assert_eq!(store.has_backups().await?, true);
assert!(store.latest_backup().await?.is_some(), "No backup_found");
// the data is gone
assert_eq!(store.get_custom_value(CUSTOM_DATA_KEY).await?, None);
// Check versions.
assert_eq!(store.version(), CURRENT_DB_VERSION);
assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION);
Ok(())
}
#[async_test]
pub async fn test_migrating_v1_to_v2_with_pw_drops() -> Result<()> {
let name = format!(
"migrating-v2-with-cipher-drops-{}",
Uuid::new_v4().as_hyphenated().to_string()
);
let passphrase = "some-other-passphrase".to_owned();
// Create and populate db.
{
let db = create_fake_db(&name, 1).await?;
let tx =
db.transaction_on_one_with_mode(keys::CUSTOM, IdbTransactionMode::Readwrite)?;
let custom = tx.object_store(keys::CUSTOM)?;
let jskey = JsValue::from_str(
core::str::from_utf8(CUSTOM_DATA_KEY).map_err(StoreError::Codec)?,
);
custom.put_key_val(&jskey, &serialize_event(None, &CUSTOM_DATA)?)?;
tx.await.into_result()?;
db.close();
}
// this transparently migrates to the latest version
let store = IndexeddbStateStore::builder()
.name(name)
.passphrase(passphrase)
.migration_conflict_strategy(MigrationConflictStrategy::Drop)
.build()
.await?;
// this doesn't create a backup
assert_eq!(store.has_backups().await?, false);
// the data is gone
assert_eq!(store.get_custom_value(CUSTOM_DATA_KEY).await?, None);
// Check versions.
assert_eq!(store.version(), CURRENT_DB_VERSION);
assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION);
Ok(())
}
#[async_test]
pub async fn test_migrating_v1_to_v2_with_pw_raise() -> Result<()> {
let name = format!(
"migrating-v2-with-cipher-raises-{}",
Uuid::new_v4().as_hyphenated().to_string()
);
let passphrase = "some-other-passphrase".to_owned();
// Create and populate db.
{
let db = create_fake_db(&name, 1).await?;
let tx =
db.transaction_on_one_with_mode(keys::CUSTOM, IdbTransactionMode::Readwrite)?;
let custom = tx.object_store(keys::CUSTOM)?;
let jskey = JsValue::from_str(
core::str::from_utf8(CUSTOM_DATA_KEY).map_err(StoreError::Codec)?,
);
custom.put_key_val(&jskey, &serialize_event(None, &CUSTOM_DATA)?)?;
tx.await.into_result()?;
db.close();
}
// this transparently migrates to the latest version
let store_res = IndexeddbStateStore::builder()
.name(name)
.passphrase(passphrase)
.migration_conflict_strategy(MigrationConflictStrategy::Raise)
.build()
.await;
assert_matches!(store_res, Err(IndexeddbStateStoreError::MigrationConflict { .. }));
Ok(())
}
#[async_test]
pub async fn test_migrating_to_v3() -> Result<()> {
let name = format!("migrating-v3-{}", Uuid::new_v4().as_hyphenated().to_string());
// An event that fails to deserialize.
let wrong_redacted_state_event = json!({
"content": null,
"event_id": "$wrongevent",
"origin_server_ts": 1673887516047_u64,
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.topic",
"unsigned": {
"redacted_because": {
"type": "m.room.redaction",
"sender": "@example:localhost",
"content": {},
"redacts": "$wrongevent",
"origin_server_ts": 1673893816047_u64,
"unsigned": {},
"event_id": "$redactionevent",
},
},
});
serde_json::from_value::<AnySyncStateEvent>(wrong_redacted_state_event.clone())
.unwrap_err();
let room_id = room_id!("!some_room:localhost");
// Populate DB with wrong event.
{
let db = create_fake_db(&name, 2).await?;
let tx =
db.transaction_on_one_with_mode(keys::ROOM_STATE, IdbTransactionMode::Readwrite)?;
let state = tx.object_store(keys::ROOM_STATE)?;
let key = (room_id, StateEventType::RoomTopic, "").encode();
state.put_key_val(&key, &serialize_event(None, &wrong_redacted_state_event)?)?;
tx.await.into_result()?;
db.close();
}
// this transparently migrates to the latest version
let store = IndexeddbStateStore::builder().name(name).build().await?;
let event =
store.get_state_event(room_id, StateEventType::RoomTopic, "").await.unwrap().unwrap();
event.deserialize().unwrap();
// Check versions.
assert_eq!(store.version(), CURRENT_DB_VERSION);
assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION);
Ok(())
}
#[async_test]
pub async fn test_migrating_to_v4() -> Result<()> {
let name = format!("migrating-v4-{}", Uuid::new_v4().as_hyphenated().to_string());
let sync_token = "a_very_unique_string";
let filter_1 = "filter_1";
let filter_1_id = "filter_1_id";
let filter_2 = "filter_2";
let filter_2_id = "filter_2_id";
// Populate DB with old table.
{
let db = create_fake_db(&name, 3).await?;
let tx = db.transaction_on_multi_with_mode(
&[old_keys::SYNC_TOKEN, old_keys::SESSION],
IdbTransactionMode::Readwrite,
)?;
let sync_token_store = tx.object_store(old_keys::SYNC_TOKEN)?;
sync_token_store.put_key_val(
&JsValue::from_str(old_keys::SYNC_TOKEN),
&serialize_event(None, &sync_token)?,
)?;
let session_store = tx.object_store(old_keys::SESSION)?;
session_store.put_key_val(
&encode_key(None, StateStoreDataKey::FILTER, (StateStoreDataKey::FILTER, filter_1)),
&serialize_event(None, &filter_1_id)?,
)?;
session_store.put_key_val(
&encode_key(None, StateStoreDataKey::FILTER, (StateStoreDataKey::FILTER, filter_2)),
&serialize_event(None, &filter_2_id)?,
)?;
tx.await.into_result()?;
db.close();
}
// this transparently migrates to the latest version
let store = IndexeddbStateStore::builder().name(name).build().await?;
let stored_sync_token = store
.get_kv_data(StateStoreDataKey::SyncToken)
.await?
.unwrap()
.into_sync_token()
.unwrap();
assert_eq!(stored_sync_token, sync_token);
let stored_filter_1_id = store
.get_kv_data(StateStoreDataKey::Filter(filter_1))
.await?
.unwrap()
.into_filter()
.unwrap();
assert_eq!(stored_filter_1_id, filter_1_id);
let stored_filter_2_id = store
.get_kv_data(StateStoreDataKey::Filter(filter_2))
.await?
.unwrap()
.into_filter()
.unwrap();
assert_eq!(stored_filter_2_id, filter_2_id);
Ok(())
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -29,7 +29,7 @@ async-trait = { workspace = true }
dashmap = { workspace = true }
fs_extra = "1.2.0"
futures-core = "0.3.21"
futures-util = { version = "0.3.21", default-features = false }
futures-util = { workspace = true }
matrix-sdk-base = { version = "0.6.0", path = "../matrix-sdk-base", optional = true }
matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" }
matrix-sdk-crypto = { version = "0.6.0", path = "../matrix-sdk-crypto", optional = true }

View File

@@ -0,0 +1,449 @@
// Copyright 2021 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 matrix_sdk_base::{
store::{Result as StoreResult, StoreError},
StateStoreDataKey,
};
use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue};
use sled::{transaction::TransactionError, Batch, Transactional, Tree};
use tracing::debug;
use super::{keys, Result, SledStateStore, SledStoreError};
use crate::encode_key::EncodeKey;
const DATABASE_VERSION: u8 = 4;
const VERSION_KEY: &str = "state-store-version";
/// Sometimes Migrations can't proceed without having to drop existing
/// data. This allows you to configure, how these cases should be handled.
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum MigrationConflictStrategy {
/// Just drop the data, we don't care that we have to sync again
Drop,
/// Raise a `SledStoreError::MigrationConflict` error with the path to the
/// DB in question. The caller then has to take care about what they want
/// to do and try again after.
Raise,
/// _Default_: The _entire_ database is backed up under
/// `$path.$timestamp.backup` (this includes the crypto store if they
/// are linked), before the state tables are dropped.
BackupAndDrop,
}
impl SledStateStore {
pub(super) fn upgrade(&mut self) -> Result<()> {
let old_version = self.db_version()?;
if old_version == 0 {
// we are fresh, let's write the current version
return self.set_db_version(DATABASE_VERSION);
}
if old_version == DATABASE_VERSION {
// current, we don't have to do anything
return Ok(());
};
debug!(old_version, new_version = DATABASE_VERSION, "Upgrading the Sled state store");
if old_version == 1 && self.store_cipher.is_some() {
// we stored some fields un-encrypted. Drop them to force re-creation
return Err(SledStoreError::MigrationConflict {
path: self.path.take().expect("Path must exist for a migration to fail"),
old_version: old_version.into(),
new_version: DATABASE_VERSION.into(),
});
}
if old_version < 3 {
self.migrate_to_v3()?;
}
if old_version < 4 {
self.migrate_to_v4()?;
return Ok(());
}
// FUTURE UPGRADE CODE GOES HERE
// can't upgrade from that version to the new one
Err(SledStoreError::MigrationConflict {
path: self.path.take().expect("Path must exist for a migration to fail"),
old_version: old_version.into(),
new_version: DATABASE_VERSION.into(),
})
}
/// Get the version of the database.
///
/// Returns `0` for a new database.
fn db_version(&self) -> Result<u8> {
Ok(self
.inner
.get(VERSION_KEY)?
.map(|v| {
let (version_bytes, _) = v.split_at(std::mem::size_of::<u8>());
u8::from_be_bytes(version_bytes.try_into().unwrap_or_default())
})
.unwrap_or_default())
}
fn set_db_version(&self, version: u8) -> Result<()> {
self.inner.insert(VERSION_KEY, version.to_be_bytes().as_ref())?;
self.inner.flush()?;
Ok(())
}
pub fn drop_v1_tables(self) -> StoreResult<()> {
for name in V1_DB_STORES {
self.inner.drop_tree(name).map_err(StoreError::backend)?;
}
self.inner.remove(VERSION_KEY).map_err(StoreError::backend)?;
Ok(())
}
fn v3_fix_tree(&self, tree: &Tree, batch: &mut Batch) -> Result<()> {
fn maybe_fix_json(raw_json: &RawJsonValue) -> Result<Option<JsonValue>> {
let json = raw_json.get();
if json.contains(r#""content":null"#) {
let mut value: JsonValue = serde_json::from_str(json)?;
if let Some(content) = value.get_mut("content") {
if matches!(content, JsonValue::Null) {
*content = JsonValue::Object(Default::default());
return Ok(Some(value));
}
}
}
Ok(None)
}
for entry in tree.iter() {
let (key, value) = entry?;
let raw_json: Box<RawJsonValue> = self.deserialize_value(&value)?;
if let Some(fixed_json) = maybe_fix_json(&raw_json)? {
batch.insert(key, self.serialize_value(&fixed_json)?);
}
}
Ok(())
}
fn migrate_to_v3(&self) -> Result<()> {
let mut room_info_batch = sled::Batch::default();
self.v3_fix_tree(&self.room_info, &mut room_info_batch)?;
let mut room_state_batch = sled::Batch::default();
self.v3_fix_tree(&self.room_state, &mut room_state_batch)?;
let ret: Result<(), TransactionError<SledStoreError>> = (&self.room_info, &self.room_state)
.transaction(|(room_info, room_state)| {
room_info.apply_batch(&room_info_batch)?;
room_state.apply_batch(&room_state_batch)?;
Ok(())
});
ret?;
self.set_db_version(3u8)
}
/// Replace the SYNC_TOKEN and SESSION trees by KV.
fn migrate_to_v4(&self) -> Result<()> {
{
let session = &self.inner.open_tree(old_keys::SESSION)?;
let mut batch = sled::Batch::default();
// Sync token
let sync_token = session.get(StateStoreDataKey::SYNC_TOKEN.encode())?;
if let Some(sync_token) = sync_token {
batch.insert(StateStoreDataKey::SYNC_TOKEN.encode(), sync_token);
}
// Filters
let key = self.encode_key(keys::SESSION, StateStoreDataKey::FILTER);
for res in session.scan_prefix(key) {
let (key, value) = res?;
batch.insert(key, value);
}
self.kv.apply_batch(batch)?;
}
// This was unused so we can just drop it.
self.inner.drop_tree(old_keys::SYNC_TOKEN)?;
self.inner.drop_tree(old_keys::SESSION)?;
self.set_db_version(4)
}
}
mod old_keys {
/// Old stores.
pub const SYNC_TOKEN: &str = "sync_token";
pub const SESSION: &str = "session";
}
pub const V1_DB_STORES: &[&str] = &[
keys::ACCOUNT_DATA,
old_keys::SYNC_TOKEN,
keys::DISPLAY_NAME,
keys::INVITED_USER_ID,
keys::JOINED_USER_ID,
keys::MEDIA,
keys::MEMBER,
keys::PRESENCE,
keys::PROFILE,
keys::ROOM_ACCOUNT_DATA,
keys::ROOM_EVENT_RECEIPT,
keys::ROOM_INFO,
keys::ROOM_STATE,
keys::ROOM_USER_RECEIPT,
keys::ROOM,
old_keys::SESSION,
keys::STRIPPED_INVITED_USER_ID,
keys::STRIPPED_JOINED_USER_ID,
keys::STRIPPED_ROOM_INFO,
keys::STRIPPED_ROOM_MEMBER,
keys::STRIPPED_ROOM_STATE,
keys::CUSTOM,
];
#[cfg(test)]
mod test {
use matrix_sdk_base::StateStoreDataKey;
use matrix_sdk_test::async_test;
use ruma::{
events::{AnySyncStateEvent, StateEventType},
room_id,
};
use serde_json::json;
use tempfile::TempDir;
use super::{old_keys, MigrationConflictStrategy};
use crate::{
encode_key::EncodeKey,
state_store::{keys, Result, SledStateStore, SledStoreError},
};
#[async_test]
pub async fn migrating_v1_to_2_plain() -> Result<()> {
let folder = TempDir::new()?;
let store = SledStateStore::builder().path(folder.path().to_path_buf()).build()?;
store.set_db_version(1u8)?;
drop(store);
// this transparently migrates to the latest version
let _store = SledStateStore::builder().path(folder.path().to_path_buf()).build()?;
Ok(())
}
#[async_test]
pub async fn migrating_v1_to_2_with_pw_backed_up() -> Result<()> {
let folder = TempDir::new()?;
let store = SledStateStore::builder()
.path(folder.path().to_path_buf())
.passphrase("something".to_owned())
.build()?;
store.set_db_version(1u8)?;
drop(store);
// this transparently creates a backup and a fresh db
let _store = SledStateStore::builder()
.path(folder.path().to_path_buf())
.passphrase("something".to_owned())
.build()?;
assert_eq!(std::fs::read_dir(folder.path())?.count(), 2);
Ok(())
}
#[async_test]
pub async fn migrating_v1_to_2_with_pw_drop() -> Result<()> {
let folder = TempDir::new()?;
let store = SledStateStore::builder()
.path(folder.path().to_path_buf())
.passphrase("other thing".to_owned())
.build()?;
store.set_db_version(1u8)?;
drop(store);
// this transparently creates a backup and a fresh db
let _store = SledStateStore::builder()
.path(folder.path().to_path_buf())
.passphrase("other thing".to_owned())
.migration_conflict_strategy(MigrationConflictStrategy::Drop)
.build()?;
assert_eq!(std::fs::read_dir(folder.path())?.count(), 1);
Ok(())
}
#[async_test]
pub async fn migrating_v1_to_2_with_pw_raises() -> Result<()> {
let folder = TempDir::new()?;
let store = SledStateStore::builder()
.path(folder.path().to_path_buf())
.passphrase("secret".to_owned())
.build()?;
store.set_db_version(1u8)?;
drop(store);
// this transparently creates a backup and a fresh db
let res = SledStateStore::builder()
.path(folder.path().to_path_buf())
.passphrase("secret".to_owned())
.migration_conflict_strategy(MigrationConflictStrategy::Raise)
.build();
if let Err(SledStoreError::MigrationConflict { .. }) = res {
// all good
} else {
panic!("Didn't raise the expected error: {res:?}");
}
assert_eq!(std::fs::read_dir(folder.path())?.count(), 1);
Ok(())
}
#[async_test]
pub async fn migrating_v2_to_v3() {
// An event that fails to deserialize.
let wrong_redacted_state_event = json!({
"content": null,
"event_id": "$wrongevent",
"origin_server_ts": 1673887516047_u64,
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.topic",
"unsigned": {
"redacted_because": {
"type": "m.room.redaction",
"sender": "@example:localhost",
"content": {},
"redacts": "$wrongevent",
"origin_server_ts": 1673893816047_u64,
"unsigned": {},
"event_id": "$redactionevent",
},
},
});
serde_json::from_value::<AnySyncStateEvent>(wrong_redacted_state_event.clone())
.unwrap_err();
let room_id = room_id!("!some_room:localhost");
let folder = TempDir::new().unwrap();
let store = SledStateStore::builder()
.path(folder.path().to_path_buf())
.passphrase("secret".to_owned())
.build()
.unwrap();
store
.room_state
.insert(
store.encode_key(keys::ROOM_STATE, (room_id, StateEventType::RoomTopic, "")),
store.serialize_value(&wrong_redacted_state_event).unwrap(),
)
.unwrap();
store.set_db_version(2u8).unwrap();
drop(store);
let store = SledStateStore::builder()
.path(folder.path().to_path_buf())
.passphrase("secret".to_owned())
.build()
.unwrap();
let event =
store.get_state_event(room_id, StateEventType::RoomTopic, "").await.unwrap().unwrap();
event.deserialize().unwrap();
}
#[async_test]
pub async fn migrating_v3_to_v4() {
let sync_token = "a_very_unique_string";
let filter_1 = "filter_1";
let filter_1_id = "filter_1_id";
let filter_2 = "filter_2";
let filter_2_id = "filter_2_id";
let folder = TempDir::new().unwrap();
let store = SledStateStore::builder()
.path(folder.path().to_path_buf())
.passphrase("secret".to_owned())
.build()
.unwrap();
let session = store.inner.open_tree(old_keys::SESSION).unwrap();
let mut batch = sled::Batch::default();
batch.insert(
StateStoreDataKey::SYNC_TOKEN.encode(),
store.serialize_value(&sync_token).unwrap(),
);
batch.insert(
store.encode_key(keys::SESSION, (StateStoreDataKey::FILTER, filter_1)),
store.serialize_value(&filter_1_id).unwrap(),
);
batch.insert(
store.encode_key(keys::SESSION, (StateStoreDataKey::FILTER, filter_2)),
store.serialize_value(&filter_2_id).unwrap(),
);
session.apply_batch(batch).unwrap();
store.set_db_version(3).unwrap();
drop(session);
drop(store);
let store = SledStateStore::builder()
.path(folder.path().to_path_buf())
.passphrase("secret".to_owned())
.build()
.unwrap();
let stored_sync_token = store
.get_kv_data(StateStoreDataKey::SyncToken)
.await
.unwrap()
.unwrap()
.into_sync_token()
.unwrap();
assert_eq!(stored_sync_token, sync_token);
let stored_filter_1_id = store
.get_kv_data(StateStoreDataKey::Filter(filter_1))
.await
.unwrap()
.unwrap()
.into_filter()
.unwrap();
assert_eq!(stored_filter_1_id, filter_1_id);
let stored_filter_2_id = store
.get_kv_data(StateStoreDataKey::Filter(filter_2))
.await
.unwrap()
.unwrap()
.into_filter()
.unwrap();
assert_eq!(stored_filter_2_id, filter_2_id);
}
}

View File

@@ -23,7 +23,7 @@ dashmap = { workspace = true }
deadpool-sqlite = "0.5.0"
fs_extra = "1.2.0"
futures-core = "0.3.21"
futures-util = { version = "0.3.21", default-features = false }
futures-util = { workspace = true }
matrix-sdk-base = { version = "0.6.0", path = "../matrix-sdk-base", optional = true }
matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" }
matrix-sdk-crypto = { version = "0.6.0", path = "../matrix-sdk-crypto", optional = true }

View File

@@ -0,0 +1,7 @@
# 0.6.2
- Fix the access token being printed in tracing span fields.
# 0.6.1
- Fixes a bug where the access token used for Matrix requests was added as a field to a tracing span.

View File

@@ -38,7 +38,7 @@ markdown = ["ruma/markdown"]
native-tls = ["reqwest/native-tls"]
rustls-tls = ["reqwest/rustls-tls"]
socks = ["reqwest/socks"]
sso-login = ["dep:hyper", "dep:rand", "dep:tokio-stream", "dep:tower"]
sso-login = ["dep:hyper", "dep:rand", "dep:tower"]
appservice = ["ruma/appservice-api-s"]
image-proc = ["dep:image"]
image-rayon = ["image-proc", "image?/jpeg_rayon"]
@@ -49,6 +49,7 @@ experimental-sliding-sync = [
"matrix-sdk-base/experimental-sliding-sync",
"experimental-timeline",
"reqwest/gzip",
"dep:uuid",
]
docsrs = [
@@ -69,13 +70,13 @@ bytesize = "1.1"
chrono = { version = "0.4.23", optional = true }
dashmap = { workspace = true }
event-listener = "2.5.2"
eyeball = { workspace = true }
eyeball-im = { workspace = true }
eyre = { version = "0.6.8", optional = true }
futures-core = "0.3.21"
futures-signals = { version = "0.3.30", default-features = false }
futures-util = { version = "0.3.21", default-features = false }
futures-util = { workspace = true }
http = { workspace = true }
im = "15.1.0"
im = { version = "15.1.0", features = ["serde"] }
indexmap = "1.9.1"
hyper = { version = "0.14.20", features = ["http1", "http2", "server"], optional = true }
matrix-sdk-base = { version = "0.6.0", path = "../matrix-sdk-base", default_features = false }
@@ -86,15 +87,15 @@ mime = "0.3.16"
pin-project-lite = "0.2.9"
rand = { version = "0.8.5", optional = true }
reqwest = { version = "0.11.10", default_features = false }
ruma = { workspace = true, features = ["compat", "rand", "unstable-msc2448", "unstable-msc2965"] }
ruma = { workspace = true, features = ["rand", "unstable-msc2448", "unstable-msc2965"] }
serde = { workspace = true }
serde_html_form = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio-stream = { version = "0.1.8", features = ["net"], optional = true }
tower = { version = "0.4.13", features = ["make"], optional = true }
tracing = { workspace = true, features = ["attributes"] }
url = "2.2.2"
uuid = { version = "1.3.0", optional = true }
zeroize = { workspace = true }
[dependencies.image]
@@ -120,6 +121,7 @@ optional = true
[target.'cfg(target_arch = "wasm32")'.dependencies]
async-once-cell = "0.4.2"
gloo-timers = { version = "0.2.6", features = ["futures"] }
tokio = { version = "1.24.2", default-features = false, features = ["sync"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
backoff = { version = "0.4.0", features = ["tokio"] }

Some files were not shown because too many files have changed in this diff Show More