Compare commits
2 Commits
main
..
0.7.1-crypto
| Author | SHA1 | Date | |
|---|---|---|---|
| c99f665766 | |||
| 8a975c8985 |
+46
-7
@@ -1,3 +1,13 @@
|
||||
# Pass the rustflags specified to host dependencies (build scripts, proc-macros)
|
||||
# when a `--target` is passed to Cargo. Historically this was not the case, and
|
||||
# because of that, cross-compilation would not set the rustflags configured
|
||||
# below in `target.'cfg(...)'` for them, resulting in cache invalidation.
|
||||
#
|
||||
# Since this is an unstable feature (enabled at the bottom of the file), this
|
||||
# setting is unfortunately ignored on stable toolchains, but it's still better
|
||||
# to have it apply on nightly than using the old behavior for all toolchains.
|
||||
target-applies-to-host = false
|
||||
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
uniffi-bindgen = "run --package uniffi-bindgen --"
|
||||
@@ -5,12 +15,41 @@ uniffi-bindgen = "run --package uniffi-bindgen --"
|
||||
[doc.extern-map.registries]
|
||||
crates-io = "https://docs.rs/"
|
||||
|
||||
# Exclude tarpaulin, android and ios from extra lints since on stable, without
|
||||
# the nightly-only target-applies-to-host setting at the top, cross compilation
|
||||
# and otherwise changing cfg's can be very bad for caching. These should never
|
||||
# be the default either and don't have much target-specific code that would
|
||||
# benefit from the extra lints.
|
||||
[target.'cfg(not(any(tarpaulin, target_os = "android", target_os = "ios")))']
|
||||
rustflags = [
|
||||
"-Wrust_2018_idioms",
|
||||
"-Wsemicolon_in_expressions_from_macros",
|
||||
"-Wunused_extern_crates",
|
||||
"-Wunused_import_braces",
|
||||
"-Wunused_qualifications",
|
||||
"-Wtrivial_casts",
|
||||
"-Wtrivial_numeric_casts",
|
||||
"-Wclippy::cloned_instead_of_copied",
|
||||
"-Wclippy::dbg_macro",
|
||||
"-Wclippy::inefficient_to_string",
|
||||
"-Wclippy::macro_use_imports",
|
||||
"-Wclippy::mut_mut",
|
||||
"-Wclippy::needless_borrow",
|
||||
"-Wclippy::nonstandard_macro_braces",
|
||||
"-Wclippy::str_to_string",
|
||||
"-Wclippy::todo",
|
||||
]
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")']
|
||||
rustflags = [
|
||||
# We have some types that are !Send and/or !Sync only on wasm, it would be
|
||||
# slightly more efficient, but also pretty annoying, to wrap them in Rc
|
||||
# where we would use Arc on other platforms.
|
||||
"-Aclippy::arc_with_non_send_sync",
|
||||
]
|
||||
|
||||
# activate the target-applies-to-host feature.
|
||||
# Required for `target-applies-to-host` at the top to take effect.
|
||||
[unstable]
|
||||
rustdoc-map = true
|
||||
|
||||
[target.aarch64-linux-android]
|
||||
# These rust flags improve the performance on Android on arm64
|
||||
rustflags = ["-C", "target-feature=+neon,+aes,+sha2,+sha3,+pmuv3"]
|
||||
|
||||
[env]
|
||||
IPHONEOS_DEPLOYMENT_TARGET = "16.0"
|
||||
target-applies-to-host = true
|
||||
|
||||
@@ -2,12 +2,3 @@
|
||||
retries = { backoff = "exponential", count = 3, delay = "1s", jitter = true }
|
||||
# kill the slow tests if they still aren't up after 180s
|
||||
slow-timeout = { period = "60s", terminate-after = 3 }
|
||||
|
||||
[profile.ci]
|
||||
retries = { backoff = "exponential", count = 4, delay = "1s", jitter = true }
|
||||
# kill the slow tests if they still aren't up after 180s
|
||||
slow-timeout = { period = "60s", terminate-after = 3 }
|
||||
|
||||
[profile.ci.junit]
|
||||
path = "junit.xml"
|
||||
store-success-output = true
|
||||
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/cfg.html
|
||||
[graph]
|
||||
all-features = true
|
||||
exclude = [
|
||||
# dev only dependency
|
||||
"criterion"
|
||||
]
|
||||
|
||||
[advisories]
|
||||
version = 2
|
||||
ignore = [
|
||||
{ id = "RUSTSEC-2024-0436", reason = "Unmaintained paste crate, not critical." },
|
||||
{ id = "RUSTSEC-2024-0388", reason = "Unmaintained derivative crate, not a direct dependency" },
|
||||
]
|
||||
|
||||
[licenses]
|
||||
version = 2
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"CDLA-Permissive-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"BSL-1.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
]
|
||||
|
||||
[bans]
|
||||
# We should disallow this, but it's currently a PITA.
|
||||
multiple-versions = "allow"
|
||||
wildcards = "allow"
|
||||
|
||||
[sources]
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
||||
|
||||
allow-git = [
|
||||
# A patch override for the bindings fixing a bug for Android before upstream
|
||||
# releases a new version.
|
||||
"https://github.com/tokio-rs/tracing.git",
|
||||
# Well, it's Ruma.
|
||||
"https://github.com/ruma/ruma",
|
||||
# A patch override for the bindings: https://github.com/rodrimati1992/const_panic/pull/10
|
||||
"https://github.com/jplatte/const_panic",
|
||||
# A patch override for the bindings: https://github.com/smol-rs/async-compat/pull/22
|
||||
"https://github.com/element-hq/async-compat",
|
||||
# We can release vodozemac whenever we need but let's not block development
|
||||
# on releases.
|
||||
"https://github.com/matrix-org/vodozemac",
|
||||
# A patch override for the bindings: https://github.com/Alorel/rust-indexed-db/pull/72
|
||||
"https://github.com/matrix-org/rust-indexed-db"
|
||||
]
|
||||
@@ -1,3 +1 @@
|
||||
* @matrix-org/rust
|
||||
/crates/matrix-sdk-crypto @matrix-org/rust @matrix-org/rust-crypto-reviewers
|
||||
/crates/matrix-sdk-indexeddb/src/crypto_store @matrix-org/rust @matrix-org/rust-crypto-reviewers
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
# Check for updates to GitHub Actions every week
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
@@ -1,7 +1,6 @@
|
||||
<!-- description of the changes in this PR -->
|
||||
|
||||
- [ ] I've documented the public API Changes in the appropriate `CHANGELOG.md` files.
|
||||
- [ ] This PR was made with the help of AI.
|
||||
- [ ] 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 -->
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
name: Security audit
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/audit-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,103 +1,54 @@
|
||||
name: Benchmarks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
benchmarks:
|
||||
name: Run Benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
strategy:
|
||||
matrix:
|
||||
benchmark:
|
||||
- crypto_bench
|
||||
- event_cache
|
||||
- linked_chunk
|
||||
- store_bench
|
||||
- timeline
|
||||
- room_list
|
||||
environment: matrix-rust-bot
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
# This CI workflow can run into space issue, so we're cleaning up some
|
||||
# space here.
|
||||
- name: Create some more space
|
||||
run: |
|
||||
echo "Disk space before cleanup"
|
||||
df -h
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
cd /opt
|
||||
find . -maxdepth 1 -mindepth 1 '!' -path ./containerd '!' -path ./actionarchivecache '!' -path ./runner '!' -path ./runner-cache -exec rm -rf '{}' ';'
|
||||
rm -rf /opt/hostedtoolcache
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2023-11-08
|
||||
components: rustfmt
|
||||
|
||||
# Get rid of binaries and libs we're not interested in.
|
||||
sudo rm -rf \
|
||||
/usr/local/julia* \
|
||||
/usr/local/aws*
|
||||
- name: Run Benchmarks
|
||||
run: cargo bench | tee benchmark-output.txt
|
||||
|
||||
sudo rm -rf \
|
||||
/usr/local/bin/minikube \
|
||||
/usr/local/bin/node \
|
||||
/usr/local/bin/stack \
|
||||
/usr/local/bin/bicep \
|
||||
/usr/local/bin/pulumi* \
|
||||
/usr/local/bin/helm \
|
||||
/usr/local/bin/azcopy \
|
||||
/usr/local/bin/packer \
|
||||
/usr/local/bin/cmake-gui \
|
||||
/usr/local/bin/cpack
|
||||
- name: Check benchmark result for PR
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: benchmark-action/github-action-benchmark@v1
|
||||
with:
|
||||
name: Rust Benchmark
|
||||
tool: 'cargo'
|
||||
output-file-path: benchmark-output.txt
|
||||
auto-push: false
|
||||
# comment to alert the user this has gone bad
|
||||
github-token: ${{ secrets.MRB_ACCESS_TOKEN }}
|
||||
alert-threshold: '120%'
|
||||
comment-on-alert: true
|
||||
fail-threshold: '150%'
|
||||
fail-on-alert: true
|
||||
|
||||
sudo rm -rf \
|
||||
/usr/local/share/powershell \
|
||||
/usr/local/share/chromium
|
||||
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
|
||||
echo "::group::/usr/local/bin/*"
|
||||
du -hsc /usr/local/bin/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "::group::/usr/local/share/*"
|
||||
du -hsc /usr/local/share/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "::group::/usr/local/*"
|
||||
du -hsc /usr/local/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "::group::/usr/local/lib/*"
|
||||
du -hsc /usr/local/lib/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "::group::/opt/*"
|
||||
du -hsc /opt/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "Disk space after cleanup"
|
||||
df -h
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup rust toolchain, cache and cargo-codspeed binary
|
||||
uses: moonrepo/setup-rust@abb2d32350334249b178c401e5ec5836e0cd88d3
|
||||
with:
|
||||
channel: stable
|
||||
cache-target: release
|
||||
bins: cargo-codspeed
|
||||
|
||||
- name: Build the benchmark target(s)
|
||||
run: cargo codspeed build -p benchmarks --bench ${{ matrix.benchmark }} --features codspeed
|
||||
|
||||
- name: Run the benchmarks
|
||||
uses: CodSpeedHQ/action@db35df748deb45fdef0960669f57d627c1956c30
|
||||
with:
|
||||
run: cargo codspeed run
|
||||
mode: simulation
|
||||
- name: Store benchmark result
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: benchmark-action/github-action-benchmark@v1
|
||||
with:
|
||||
name: Rust Benchmark
|
||||
tool: 'cargo'
|
||||
output-file-path: benchmark-output.txt
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
auto-push: true
|
||||
# Show alert with commit comment on detecting possible performance regression
|
||||
alert-threshold: '150%'
|
||||
comment-on-alert: true
|
||||
fail-on-alert: true
|
||||
alert-comment-cc-users: '@gnunicornBen,@jplatte,@poljar'
|
||||
|
||||
@@ -12,8 +12,6 @@ on:
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
@@ -33,19 +31,15 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
# Cargo config can screw with caching and is only used for alias config
|
||||
# and extra lints, which we don't care about here
|
||||
@@ -53,12 +47,12 @@ jobs:
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -67,100 +61,24 @@ jobs:
|
||||
- name: Build library & generate bindings
|
||||
run: target/debug/xtask ci bindings
|
||||
|
||||
test-android:
|
||||
name: matrix-rust-components-kotlin
|
||||
needs: xtask
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout Rust SDK
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Checkout Kotlin Rust Components project
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: matrix-org/matrix-rust-components-kotlin
|
||||
path: rust-components-kotlin
|
||||
ref: main
|
||||
persist-credentials: false
|
||||
|
||||
- name: Use JDK 17
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
|
||||
- name: Install android sdk
|
||||
uses: malinskiy/action-android/install-sdk@fa103ef30331e95f266418a6a97e98f61f626887 # release/0.1.7
|
||||
|
||||
- name: Install android ndk
|
||||
uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1
|
||||
id: install-ndk
|
||||
with:
|
||||
ndk-version: r27
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
# Cargo config can screw with caching and is only used for alias config
|
||||
# and extra lints, which we don't care about here
|
||||
- name: Delete cargo config
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Install Rust dependencies
|
||||
run: |
|
||||
rustup target add x86_64-linux-android
|
||||
cargo install cargo-ndk
|
||||
|
||||
- name: Build SDK bindings for Android
|
||||
# Building for x86_64-linux-android as it's the most prone to breaking and building for every arch is too much
|
||||
run: |
|
||||
echo "Building SDK for x86_64-linux-android and creating bindings"
|
||||
target/debug/xtask kotlin build-android-library --package full-sdk --only-target x86_64-linux-android --src-dir rust-components-kotlin/sdk/sdk-android/src/main
|
||||
echo "Copying the result binary to the Android project"
|
||||
cd rust-components-kotlin
|
||||
echo "Building the Kotlin bindings"
|
||||
./gradlew :sdk:sdk-android:assembleDebug
|
||||
|
||||
test-apple:
|
||||
name: matrix-rust-components-swift
|
||||
needs: xtask
|
||||
runs-on: macos-15
|
||||
runs-on: macos-12
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# install protoc in case we end up rebuilding opentelemetry-proto
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install aarch64-apple-ios target
|
||||
run: rustup target install aarch64-apple-ios
|
||||
@@ -171,12 +89,12 @@ jobs:
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-macos }}"
|
||||
@@ -190,54 +108,4 @@ jobs:
|
||||
run: swift test
|
||||
|
||||
- name: Build Framework
|
||||
run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=dev --ios-deployment-target=18.0
|
||||
|
||||
complement-crypto:
|
||||
name: "Run Complement Crypto tests"
|
||||
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@399a1deeab0d7e4fa9604cbe83b1df6058c40193 # main
|
||||
with:
|
||||
use_rust_sdk: "." # use local checkout
|
||||
use_complement_crypto: "MATCHING_BRANCH"
|
||||
|
||||
test-crypto-apple-framework-generation:
|
||||
name: Generate Crypto FFI Apple XCFramework
|
||||
runs-on: macos-15
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# install protoc in case we end up rebuilding opentelemetry-proto
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Add rust targets
|
||||
run: |
|
||||
rustup target add aarch64-apple-ios
|
||||
|
||||
# Cargo config can screw with caching and is only used for alias config
|
||||
# and extra lints, which we don't care about here
|
||||
- name: Delete cargo config
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Run the Build Framework script
|
||||
run: ./bindings/apple/build_crypto_xcframework.sh -i
|
||||
|
||||
- name: Is XCFramework generated?
|
||||
if: ${{ hashFiles('generated/MatrixSDKCryptoFFI.zip') != '' }}
|
||||
run: echo "XCFramework exists"
|
||||
run: target/debug/xtask swift build-framework --only-target=aarch64-apple-ios
|
||||
|
||||
+119
-142
@@ -6,8 +6,11 @@ on:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions: {}
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -15,9 +18,6 @@ concurrency:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
# Insta.rs is run directly via cargo test. We don't want insta.rs to create new snapshots files.
|
||||
# Just want it to run the tests (option `no` instead of `auto`).
|
||||
INSTA_UPDATE: no
|
||||
|
||||
jobs:
|
||||
xtask:
|
||||
@@ -26,6 +26,7 @@ jobs:
|
||||
test-matrix-sdk-features:
|
||||
name: 🐧 [m], ${{ matrix.name }}
|
||||
needs: xtask
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
@@ -36,30 +37,20 @@ jobs:
|
||||
- no-sqlite
|
||||
- no-encryption-and-sqlite
|
||||
- sqlite-cryptostore
|
||||
- experimental-encrypted-state-events
|
||||
- rustls-tls
|
||||
- markdown
|
||||
- socks
|
||||
- sso-login
|
||||
- search
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsqlite3-dev
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
# use a separate cache for each job to work around
|
||||
# https://github.com/Swatinem/rust-cache/issues/124
|
||||
@@ -70,12 +61,10 @@ jobs:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
|
||||
with:
|
||||
tool: nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -89,30 +78,25 @@ jobs:
|
||||
name: 🐧 [m]-examples
|
||||
needs: xtask
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
|
||||
with:
|
||||
tool: nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -126,36 +110,25 @@ jobs:
|
||||
name: 🐧 [m]-crypto
|
||||
needs: xtask
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsqlite3-dev
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
|
||||
with:
|
||||
tool: nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -167,6 +140,7 @@ jobs:
|
||||
|
||||
test-all-crates:
|
||||
name: ${{ matrix.name }}
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
@@ -187,35 +161,25 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install libsqlite
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
|
||||
with:
|
||||
tool: nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
@@ -229,6 +193,7 @@ jobs:
|
||||
test-wasm:
|
||||
name: 🕸️ ${{ matrix.name }}
|
||||
needs: xtask
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -245,16 +210,15 @@ jobs:
|
||||
- name: '[m]-common'
|
||||
cmd: matrix-sdk-common
|
||||
|
||||
- name: '[m], no-default'
|
||||
- name: '[m]-indexeddb, no crypto'
|
||||
cmd: indexeddb-no-crypto
|
||||
|
||||
- name: '[m]-indexeddb, with crypto'
|
||||
cmd: indexeddb-with-crypto
|
||||
|
||||
- name: '[m], no-default, wasm-flags'
|
||||
cmd: matrix-sdk-no-default
|
||||
|
||||
- name: '[m]-ui'
|
||||
cmd: matrix-sdk-ui
|
||||
check_only: true
|
||||
|
||||
- name: '[m]-indexeddb'
|
||||
cmd: indexeddb
|
||||
|
||||
- name: '[m], indexeddb stores'
|
||||
cmd: matrix-sdk-indexeddb-stores
|
||||
|
||||
@@ -263,25 +227,21 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: wasm32-unknown-unknown
|
||||
components: clippy
|
||||
|
||||
- name: Install wasm-pack
|
||||
uses: qmaru/wasm-pack-action@785fe709cd17eb6a97607eda9b6f5dbebed2b89c # v0.5.3
|
||||
if: '!matrix.check_only'
|
||||
uses: jetli/wasm-pack-action@v0.4.0
|
||||
with:
|
||||
version: v0.13.1
|
||||
version: v0.10.3
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
# use a separate cache for each job to work around
|
||||
# https://github.com/Swatinem/rust-cache/issues/124
|
||||
@@ -292,12 +252,10 @@ jobs:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
|
||||
with:
|
||||
tool: nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -308,126 +266,145 @@ jobs:
|
||||
target/debug/xtask ci wasm ${{ matrix.cmd }}
|
||||
|
||||
- name: Wasm-Pack test
|
||||
if: '!matrix.check_only'
|
||||
run: |
|
||||
target/debug/xtask ci wasm-pack ${{ matrix.cmd }}
|
||||
|
||||
formatting:
|
||||
name: Check Formatting
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2023-11-08
|
||||
components: rustfmt
|
||||
|
||||
- name: Cargo fmt
|
||||
run: |
|
||||
cargo fmt -- --check
|
||||
|
||||
typos:
|
||||
name: Spell Check with Typos
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout Actions Repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Check the spelling of the files in our repo
|
||||
uses: crate-ci/typos@cf5f1c29a8ac336af8568821ec41919923b05a83 # v1.45.1
|
||||
uses: crate-ci/typos@v1.17.0
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
clippy:
|
||||
name: Run clippy
|
||||
needs: xtask
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2026-02-26
|
||||
components: clippy, rustfmt
|
||||
toolchain: nightly-2023-11-08
|
||||
components: clippy
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Check Formatting
|
||||
run: |
|
||||
target/debug/xtask ci style
|
||||
|
||||
- name: Clippy
|
||||
run: |
|
||||
target/debug/xtask ci clippy
|
||||
|
||||
integration-tests:
|
||||
name: 'Integration test (features: ${{ matrix.feature }})'
|
||||
name: Integration test
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# run several docker containers with the same networking stack so the hostname 'synapse'
|
||||
# maps to the synapse container, etc.
|
||||
# run several docker containers with the same networking stack so the hostname 'postgres'
|
||||
# maps to the postgres container, etc.
|
||||
services:
|
||||
# sliding sync needs a postgres container
|
||||
postgres:
|
||||
# Docker Hub image
|
||||
image: postgres
|
||||
# Provide the password for postgres
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: syncv3
|
||||
# Set health checks to wait until postgres has started
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
# Maps tcp port 5432 on service container to the host
|
||||
- 5432:5432
|
||||
# run sliding sync and point it at the postgres container and synapse container.
|
||||
# the postgres container needs to be above this to make sure it has started prior to this service.
|
||||
slidingsync:
|
||||
image: "ghcr.io/matrix-org/sliding-sync:v0.99.11" # keep in sync with ./coverage.yml
|
||||
env:
|
||||
SYNCV3_SERVER: "http://synapse:8008"
|
||||
SYNCV3_SECRET: "SUPER_CI_SECRET"
|
||||
SYNCV3_BINDADDR: ":8118"
|
||||
SYNCV3_DB: "user=postgres password=postgres dbname=syncv3 sslmode=disable host=postgres"
|
||||
ports:
|
||||
- 8118:8118
|
||||
# tests need a synapse: this is a service and not michaelkaye/setup-matrix-synapse@main as the
|
||||
# latter does not provide networking for services to communicate with it.
|
||||
synapse:
|
||||
image: ghcr.io/matrix-org/synapse-service:v1.117.0 # keep in sync with ./coverage.yml
|
||||
image: ghcr.io/matrix-org/synapse-service:v1.94.0 # keep in sync with ./coverage.yml
|
||||
env:
|
||||
SYNAPSE_COMPLEMENT_DATABASE: sqlite
|
||||
SERVER_NAME: synapse
|
||||
ports:
|
||||
- 8008:8008
|
||||
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
feature:
|
||||
- "default"
|
||||
- "experimental-encrypted-state-events"
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsqlite3-dev
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
|
||||
with:
|
||||
tool: nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Test
|
||||
env:
|
||||
RUST_LOG: "info,matrix_sdk=trace"
|
||||
RUST_LOG: "hyper=trace"
|
||||
HOMESERVER_URL: "http://localhost:8008"
|
||||
HOMESERVER_DOMAIN: "synapse"
|
||||
SLIDING_SYNC_PROXY_URL: "http://localhost:8118"
|
||||
run: |
|
||||
cargo nextest run --profile ci -p matrix-sdk-integration-testing --features "${{ matrix.feature }}"
|
||||
|
||||
- name: Upload test results to Codecov
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3
|
||||
with:
|
||||
files: ./target/nextest/ci/junit.xml
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
cargo nextest run -p matrix-sdk-integration-testing
|
||||
|
||||
+62
-109
@@ -1,12 +1,15 @@
|
||||
name: Code Coverage
|
||||
name: Code coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions: {}
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -19,24 +22,47 @@ env:
|
||||
RUST_LOG: info,matrix_sdk=trace
|
||||
|
||||
jobs:
|
||||
xtask:
|
||||
uses: ./.github/workflows/xtask.yml
|
||||
|
||||
code_coverage:
|
||||
name: Code Coverage
|
||||
needs: xtask
|
||||
runs-on: "ubuntu-latest"
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
# run several docker containers with the same networking stack so the hostname 'synapse'
|
||||
# maps to the synapse container, etc.
|
||||
# run several docker containers with the same networking stack so the hostname 'postgres'
|
||||
# maps to the postgres container, etc.
|
||||
services:
|
||||
# sliding sync needs a postgres container
|
||||
postgres:
|
||||
# Docker Hub image
|
||||
image: postgres
|
||||
# Provide the password for postgres
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: syncv3
|
||||
# Set health checks to wait until postgres has started
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
# Maps tcp port 5432 on service container to the host
|
||||
- 5432:5432
|
||||
# run sliding sync and point it at the postgres container and synapse container.
|
||||
# the postgres container needs to be above this to make sure it has started prior to this service.
|
||||
slidingsync:
|
||||
image: "ghcr.io/matrix-org/sliding-sync:v0.99.11" # keep in sync with ./ci.yml
|
||||
env:
|
||||
SYNCV3_SERVER: "http://synapse:8008"
|
||||
SYNCV3_SECRET: "SUPER_CI_SECRET"
|
||||
SYNCV3_BINDADDR: ":8118"
|
||||
SYNCV3_DB: "user=postgres password=postgres dbname=syncv3 sslmode=disable host=postgres"
|
||||
ports:
|
||||
- 8118:8118
|
||||
# tests need a synapse: this is a service and not michaelkaye/setup-matrix-synapse@main as the
|
||||
# latter does not provide networking for services to communicate with it.
|
||||
synapse:
|
||||
image: ghcr.io/matrix-org/synapse-service:v1.117.0 # keep in sync with ./ci.yml
|
||||
image: ghcr.io/matrix-org/synapse-service:v1.94.0 # keep in sync with ./ci.yml
|
||||
env:
|
||||
SYNAPSE_COMPLEMENT_DATABASE: sqlite
|
||||
SERVER_NAME: synapse
|
||||
@@ -44,80 +70,13 @@ jobs:
|
||||
- 8008:8008
|
||||
|
||||
steps:
|
||||
# This CI workflow can run into space issue, so we're cleaning up some
|
||||
# space here.
|
||||
- name: Create some more space
|
||||
run: |
|
||||
echo "Disk space before cleanup"
|
||||
df -h
|
||||
|
||||
cd /opt
|
||||
find . -maxdepth 1 -mindepth 1 '!' -path ./containerd '!' -path ./actionarchivecache '!' -path ./runner '!' -path ./runner-cache -exec rm -rf '{}' ';'
|
||||
rm -rf /opt/hostedtoolcache
|
||||
|
||||
# Get rid of binaries and libs we're not interested in.
|
||||
sudo rm -rf \
|
||||
/usr/local/julia* \
|
||||
/usr/local/aws*
|
||||
|
||||
sudo rm -rf \
|
||||
/usr/local/bin/minikube \
|
||||
/usr/local/bin/node \
|
||||
/usr/local/bin/stack \
|
||||
/usr/local/bin/bicep \
|
||||
/usr/local/bin/pulumi* \
|
||||
/usr/local/bin/helm \
|
||||
/usr/local/bin/azcopy \
|
||||
/usr/local/bin/packer \
|
||||
/usr/local/bin/cmake-gui \
|
||||
/usr/local/bin/cpack
|
||||
|
||||
sudo rm -rf \
|
||||
/usr/local/share/powershell \
|
||||
/usr/local/share/chromium
|
||||
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
|
||||
echo "::group::/usr/local/bin/*"
|
||||
du -hsc /usr/local/bin/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "::group::/usr/local/share/*"
|
||||
du -hsc /usr/local/share/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "::group::/usr/local/*"
|
||||
du -hsc /usr/local/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "::group::/usr/local/lib/*"
|
||||
du -hsc /usr/local/lib/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "::group::/opt/*"
|
||||
du -hsc /opt/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "Disk space after cleanup"
|
||||
df -h
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsqlite3-dev
|
||||
sudo apt-get clean
|
||||
sudo rm -rf /var/lib/apt/lists/*
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
# Cargo config can screw with caching and is only used for alias config
|
||||
# and extra lints, which we don't care about here
|
||||
@@ -125,44 +84,38 @@ jobs:
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: "coverage"
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest and llvm-cov
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
|
||||
- name: Install tarpaulin
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: nextest,cargo-llvm-cov
|
||||
tool: cargo-tarpaulin
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
# set up backend for integration tests
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
fail-on-cache-miss: true
|
||||
python-version: 3.8
|
||||
|
||||
- name: Check total disk space before running
|
||||
- name: Run tarpaulin
|
||||
run: |
|
||||
df -h
|
||||
|
||||
- name: Create the coverage report
|
||||
run: |
|
||||
target/debug/xtask ci coverage -o codecov
|
||||
rustup run stable cargo tarpaulin \
|
||||
--skip-clean --profile cov --out xml \
|
||||
--features experimental-widgets,testing
|
||||
env:
|
||||
CARGO_PROFILE_COV_INHERITS: 'dev'
|
||||
CARGO_PROFILE_COV_DEBUG: 1
|
||||
CARGO_PROFILE_COV_DEBUG: 'false'
|
||||
HOMESERVER_URL: "http://localhost:8008"
|
||||
HOMESERVER_DOMAIN: "synapse"
|
||||
SLIDING_SYNC_PROXY_URL: "http://localhost:8118"
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
use_oidc: true
|
||||
|
||||
- name: Upload test results to Codecov
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2
|
||||
with:
|
||||
use_oidc: true
|
||||
report_type: "test_results"
|
||||
# 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
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
name: Lint dependencies (for licences, allowed sources, banned dependencies, vulnerabilities)
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '**/Cargo.toml'
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
cargo-deny:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2.0.15
|
||||
@@ -1,39 +0,0 @@
|
||||
# Check if the path of changed file is longer than 260 characters
|
||||
# that windows filesystem allows
|
||||
|
||||
name: Detect long path among changed files
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request: # focus on the changed files in current PR
|
||||
branches: [main]
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
long-path:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
- name: Detect long path
|
||||
env:
|
||||
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} # ignore the deleted files
|
||||
MAX_LENGTH: 120 # set max length to 120, considering the base path of app project that uses matrix-sdk
|
||||
run: |
|
||||
for file in ${ALL_CHANGED_FILES}; do
|
||||
if [ ${#file} -gt $MAX_LENGTH ]; then
|
||||
echo "File path is too long. Length: ${#file}, Path: $file"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
exit 0
|
||||
@@ -1,16 +0,0 @@
|
||||
name: Detects unused dependencies
|
||||
on:
|
||||
pull_request: { branches: "*" }
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Machete
|
||||
uses: bnjbvr/cargo-machete@ac30a525c0a8d163a92d727b3ff079ee3f6ecb08
|
||||
@@ -4,6 +4,11 @@ on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -18,47 +23,48 @@ jobs:
|
||||
docs:
|
||||
name: All crates
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2026-02-26
|
||||
toolchain: nightly-2023-11-08
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
# Keep in sync with xtask docs
|
||||
- name: Build documentation
|
||||
env:
|
||||
# Work around https://github.com/rust-lang/cargo/issues/10744
|
||||
CARGO_TARGET_APPLIES_TO_HOST: "true"
|
||||
RUSTDOCFLAGS: "--enable-index-page -Zunstable-options --cfg docsrs -Dwarnings"
|
||||
run:
|
||||
cargo doc --no-deps --workspace --features docsrs --exclude=xtask
|
||||
cargo doc --no-deps --workspace --features docsrs
|
||||
|
||||
- name: Upload artifact
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
with:
|
||||
path: './target/doc/'
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
|
||||
uses: actions/deploy-pages@v2
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
name: Git Checks
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
block-fixup:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Block Fixup Commit Merge
|
||||
uses: 13rac1/block-fixup-merge-action@bd5504fb9ca0253e109d98eb86b7debc01970cdc # v2.0.0
|
||||
@@ -1,22 +0,0 @@
|
||||
name: Rust version
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
msrv:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # cargo-hack
|
||||
with:
|
||||
tool: cargo-hack
|
||||
- run: cargo hack check --rust-version --workspace --all-targets
|
||||
@@ -20,8 +20,6 @@ on:
|
||||
description: "The cache key for the macos build artifact"
|
||||
value: "${{ jobs.xtask.outputs.cachekey-macos }}"
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
@@ -37,7 +35,7 @@ jobs:
|
||||
os-name: 🐧
|
||||
cachekey-id: linux
|
||||
|
||||
- os: macos-15
|
||||
- os: macos-12
|
||||
os-name: 🍏
|
||||
cachekey-id: macos
|
||||
|
||||
@@ -45,9 +43,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Calculate cache key
|
||||
id: cachekey
|
||||
@@ -57,20 +53,18 @@ jobs:
|
||||
echo "cachekey-${{ matrix.cachekey-id }}=xtask-${{ matrix.cachekey-id }}-${{ hashFiles('Cargo.toml', 'xtask/**') }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check xtask cache
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache@v3
|
||||
id: xtask-cache
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
# use the cache key calculated in the step above. Bit of an awkward
|
||||
# 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@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Build
|
||||
if: steps.xtask-cache.outputs.cache-hit != 'true'
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
name: Lint GHA workflows with zizmor
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["**"]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
zizmor:
|
||||
name: Run zizmor
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
|
||||
@@ -4,12 +4,9 @@ master.zip
|
||||
emsdk-*
|
||||
.idea/
|
||||
.env
|
||||
.envrc
|
||||
.build
|
||||
.swiftpm
|
||||
/Package.swift
|
||||
# code coverage report
|
||||
cobertura.xml
|
||||
|
||||
## User settings
|
||||
xcuserdata/
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 49 KiB |
+1
-1
@@ -7,4 +7,4 @@ group_imports = "StdExternalCrate"
|
||||
format_code_in_doc_comments = true
|
||||
doc_comment_code_block_width = 80
|
||||
# Workaround for https://github.com/rust-lang/rust.vim/issues/464
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
|
||||
+4
-15
@@ -16,29 +16,18 @@ extend-ignore-re = [
|
||||
[default.extend-identifiers]
|
||||
WeeChat = "WeeChat"
|
||||
|
||||
[default.extend-words]
|
||||
# all of these are valid words, but should never appear in this repo
|
||||
bellow = "below"
|
||||
stat = "state"
|
||||
[default.extend-words]
|
||||
sing = "sign"
|
||||
singed = "signed"
|
||||
singing = "signing"
|
||||
# crate name
|
||||
ratatui = "ratatui"
|
||||
# path name
|
||||
consts = "consts"
|
||||
# base64 false positives
|
||||
Nd = "Nd"
|
||||
Abl = "Abl"
|
||||
Som = "Som"
|
||||
Ba = "Ba"
|
||||
Yur = "Yur" # as found in crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs
|
||||
TYE = "TYE" # as found in testing/matrix-sdk-test/src/test_json/keys_query_sets.rs
|
||||
|
||||
[files]
|
||||
# Our json files contain a bunch of base64 encoded ed25519 keys which aren't
|
||||
# automatically ignored, we ignore them here.
|
||||
extend-exclude = [
|
||||
# Our json files contain a bunch of base64 encoded ed25519 keys.
|
||||
"*.json",
|
||||
# Fuzzy match patterns that can be understood as typos confusingly.
|
||||
# We are using some fuzzy match patterns that can be understood as typos confusingly.
|
||||
"crates/matrix-sdk-ui/tests/integration/room_list_service.rs",
|
||||
]
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
rules:
|
||||
unpinned-uses:
|
||||
severity: high
|
||||
-119
@@ -1,119 +0,0 @@
|
||||
# Architecture
|
||||
|
||||
The SDK is split into multiple layers:
|
||||
|
||||
```
|
||||
WASM (external crate matrix-rust-sdk-crypto-wasm)
|
||||
/
|
||||
/ uniffi
|
||||
/ /
|
||||
/ bindings (matrix-sdk-ffi)
|
||||
crypto |
|
||||
bindings |
|
||||
| |
|
||||
| UI (matrix-sdk-ui)
|
||||
| \
|
||||
| \
|
||||
| main (matrix-sdk)
|
||||
| / /
|
||||
crypto /
|
||||
\ /
|
||||
store (matrix-sdk-base, + all the store impls)
|
||||
|
|
||||
common (matrix-sdk-common)
|
||||
```
|
||||
|
||||
Where the store implementations are `matrix-sdk-sqlite` and `matrix-sdk-indexeddb` as well as
|
||||
`MemoryStore` which is defined in `matrix-sdk-base`.
|
||||
|
||||
## `crates/matrix-sdk`
|
||||
|
||||
This is the main crate, and one that is expected to be used by most consumers. Notable data types
|
||||
include:
|
||||
|
||||
- the `Client`, which can run room-independent requests: logging in/out, creating rooms, running
|
||||
sync, etc.
|
||||
- the `Room`, which represents a room and its state (notably via the observable `RoomInfo`), and
|
||||
allows running queries that are room-specific, notably sending events.
|
||||
|
||||
## `crates/matrix-sdk-base`
|
||||
|
||||
A *sans I/O* crate to represent the base data types persisted in the SDK. No network or storage I/O
|
||||
happens in this crate, although it defines traits (`StateStore` and `EventCacheStore`) representing
|
||||
storage backends, as well as dummy in-memory implementations of these traits.
|
||||
|
||||
## `crates/matrix-sdk-common`
|
||||
|
||||
Common helpers used by most of the other crates; almost a leaf in the dependency tree of our own
|
||||
crates (the only crate it's using is test helpers).
|
||||
|
||||
## `crates/matrix-sdk-crypto`
|
||||
|
||||
A *sans I/O* implementation of a state machine that handles end-to-end encryption for Matrix
|
||||
clients. It defines a `CryptoStore` trait representing storage backends that will perform the
|
||||
actual storage I/O later, as well as a dummy in-memory implementation of this trait.
|
||||
|
||||
## `crates/matrix-sdk-indexeddb`
|
||||
|
||||
Implementations of `EventCacheStore`, `StateStore` and `CryptoStore` for a
|
||||
indexeddb backend (for use in Web browsers, via WebAssembly).
|
||||
|
||||
## `crates/matrix-sdk-qrcode`
|
||||
|
||||
Implementation of QR codes for interactive verifications, used in the crypto crate.
|
||||
|
||||
## `crates/matrix-sdk-sqlite`
|
||||
|
||||
Implementations of `EventCacheStore`, `StateStore` and `CryptoStore` for a
|
||||
SQLite backend.
|
||||
|
||||
## `crates/matrix-sdk-store-encryption`
|
||||
|
||||
Low-level primitives for encrypting/decrypting/hashing values. Store implementations that
|
||||
implement encryption at rest can use those primitives.
|
||||
|
||||
## `crates/matrix-sdk-ui`
|
||||
|
||||
Very high-level primitives implementing the best practices and cutting-edge Matrix tech:
|
||||
|
||||
- `EncryptionSyncService`: a specialized service running simplified sliding sync (MSC4186) for
|
||||
everything related to crypto and E2EE for the current `Client`.
|
||||
- `RoomListService`: a specialized service running simplified sliding sync (MSC4186) for
|
||||
retrieving the list of current rooms, and exposing its entries.
|
||||
- `SyncService`: a wrapper for the two previous services, coordinating their running and shutting
|
||||
down.
|
||||
- `Timeline`: a high-level view for a `Room`'s timeline of events, grouping related events
|
||||
(aggregations) into single timeline items.
|
||||
|
||||
## `bindings/matrix-sdk-crypto-ffi/`
|
||||
|
||||
FFI bindings for the crypto crate, used in a Web browser context via WebAssembly. These use
|
||||
`wasm-bindgen` to generate the bindings. These bindings are used in Element Web and the legacy
|
||||
Element apps, as of 2024-11-07.
|
||||
|
||||
## `bindings/matrix-sdk-ffi/`
|
||||
|
||||
FFI bindings for important concepts in `matrix-sdk-ui` and `matrix-sdk`, generated with
|
||||
[UniFFI](https://github.com/mozilla/uniffi-rs) and to be used from other languages like
|
||||
Swift/Go/Kotlin. These bindings are used in the ElementX apps, as of 2024-11-07.
|
||||
|
||||
## `bindings/matrix-sdk-ffi-macros/`
|
||||
|
||||
Macros used in `bindings/matrix-sdk-ffi`.
|
||||
|
||||
## `testing/matrix-sdk-test/`
|
||||
|
||||
Common test helpers, used by all the other crates.
|
||||
|
||||
## `testing/matrix-sdk-test-macros/`
|
||||
|
||||
Implementation of the `#[async_test]` test macro.
|
||||
|
||||
## `testing/matrix-sdk-integration-testing/`
|
||||
|
||||
Fully-fledged integration tests that require spawning a Synapse instance to run. A docker-compose
|
||||
setup is provided to ease running the tests, and it is compatible for running with Podman too.
|
||||
|
||||
# Inspiration
|
||||
|
||||
This document has been inspired by the reading of this [blog post](https://matklad.github.io/2021/02/06/ARCHITECTURE.md.html).
|
||||
+16
-296
@@ -1,4 +1,4 @@
|
||||
# Contributing to `matrix-rust-sdk`
|
||||
# Contributing to matrix-rust-sdk
|
||||
|
||||
## Chat rooms
|
||||
|
||||
@@ -29,256 +29,18 @@ integration tests that need a running synapse instance. These tests reside in
|
||||
[README](./testing/matrix-sdk-integration-testing/README.md) to easily set up a
|
||||
synapse for testing purposes.
|
||||
|
||||
### Snapshot Testing
|
||||
|
||||
You can add/review snapshot tests using [insta.rs](https://insta.rs)
|
||||
|
||||
Every new struct/enum that derives `Serialize` `Deserialise` should have a
|
||||
snapshot test for it. Any code change that breaks serialisation will then break
|
||||
a test, the author will then have to decide how to handle migration and test it
|
||||
if needed.
|
||||
|
||||
And for an improved review experience it's recommended (but not necessary) to
|
||||
install the `cargo-insta` tool:
|
||||
|
||||
Unix:
|
||||
|
||||
```shell
|
||||
curl -LsSf https://insta.rs/install.sh | sh
|
||||
```
|
||||
|
||||
Windows:
|
||||
|
||||
```shell
|
||||
powershell -c "irm https://insta.rs/install.ps1 | iex"
|
||||
```
|
||||
|
||||
Usual flow is to first run the test, then review them.
|
||||
|
||||
```shell
|
||||
cargo insta test
|
||||
cargo insta review
|
||||
```
|
||||
|
||||
### Intermittent failure policy
|
||||
|
||||
While we strive to add test coverage for as many features as we can, it
|
||||
sometimes happens that the tests will be intermittently failing in CI (such
|
||||
tests are sometimes called "flaky"). This can be caused by race conditions
|
||||
of all sorts, either in the test code itself, but sometimes in the underlying
|
||||
feature being tested too, and as such, it requires some investigation, usually
|
||||
from the original author of the test.
|
||||
|
||||
Whenever such an intermittent failure happens, we try to open an issue to track
|
||||
the failures, adding the
|
||||
[`intermittent-failure`](https://github.com/matrix-org/matrix-rust-sdk/issues?q=is%3Aissue%20state%3Aopen%20label%3Aintermittent-failure)
|
||||
label to it, and commenting with links to CI runs where the failure happened.
|
||||
|
||||
If a test has been intermittently failing for **two weeks** or more, and no one
|
||||
is actively working on fixing it, then we might decide to mark the test as
|
||||
`ignored` until it is fixed, to not cause unrelated failures in other
|
||||
contributors' pull requests and pushes.
|
||||
|
||||
## Pull requests
|
||||
|
||||
Ideally, a PR should have a _proper title_, with _atomic logical commits_, and
|
||||
each commit should have a _good commit message_.
|
||||
|
||||
A _proper PR title_ would be a one-liner summary of the changes in the PR,
|
||||
following the same guidelines of a good commit message, including the
|
||||
area/feature prefix. Something like `FFI: Allow logs files to be pruned.` would
|
||||
be a good PR title.
|
||||
|
||||
(An additional bad example of a bad PR title would be `mynickname/branch name`,
|
||||
that is, just the branch name.)
|
||||
|
||||
## Writing changelog entries
|
||||
|
||||
Our goal is to maintain clear, concise, and informative changelogs that
|
||||
accurately document changes in the project. Changelog entries should be written
|
||||
manually for each crate in the `/crates/$CRATE_NAME/Changelog.md` file.
|
||||
|
||||
Be sure to include a link to the pull request for additional context. A
|
||||
well-written changelog entry should be understandable even to those who may not
|
||||
be deeply familiar with the project. Provide enough context to ensure clarity
|
||||
and ease of understanding.
|
||||
|
||||
A couple of examples of bad changelog entry would look like:
|
||||
|
||||
```markdown
|
||||
- Fixed a panic.
|
||||
```
|
||||
|
||||
```markdown
|
||||
- Added the Bar function to Foo.
|
||||
```
|
||||
|
||||
A good example of a changelog entry could look like the following:
|
||||
|
||||
```markdown
|
||||
- Use the inviter's server name and the server name from the room alias as
|
||||
fallback values for the via parameter when requesting the room summary from
|
||||
the homeserver. This ensures requests succeed even when the room being
|
||||
previewed is hosted on a federated server.
|
||||
([#4357](https://github.com/matrix-org/matrix-rust-sdk/pull/4357))
|
||||
```
|
||||
|
||||
For security-related changelog entries, please include the following additional
|
||||
details alongside the pull request number:
|
||||
|
||||
- Impact: Clearly describe the issue's potential impact on users or systems.
|
||||
- CVE Number: If available, include the CVE (Common Vulnerabilities and
|
||||
Exposures) identifier.
|
||||
- GitHub Advisory Link: Provide a link to the corresponding GitHub security
|
||||
advisory for further context.
|
||||
|
||||
```markdown
|
||||
- Use a constant-time Base64 encoder for secret key material to mitigate
|
||||
side-channel attacks leaking secret key material
|
||||
([#156](https://github.com/matrix-org/vodozemac/pull/156)) (Low,
|
||||
[CVE-2024-40640](https://www.cve.org/CVERecord?id=CVE-2024-40640),
|
||||
[GHSA-j8cm-g7r6-hfpq](https://github.com/matrix-org/vodozemac/security/advisories/GHSA-j8cm-g7r6-hfpq)).
|
||||
```
|
||||
|
||||
## Commit message format
|
||||
|
||||
Commit messages should be formatted as Conventional Commits. In addition, some
|
||||
git trailers are supported and have special meaning (see below).
|
||||
|
||||
### Conventional commits
|
||||
|
||||
Conventional Commits are structured as follows:
|
||||
|
||||
```text
|
||||
<type>(<scope>): <short summary>
|
||||
```
|
||||
|
||||
The type of changes which will be included in changelogs is one of the
|
||||
following:
|
||||
|
||||
- `feat`: A new feature
|
||||
- `fix`: A bugfix
|
||||
- `doc`: Documentation changes
|
||||
- `refactor`: Code refactoring
|
||||
- `perf`: Performance improvements
|
||||
- `ci`: Changes to CI configuration files and scripts
|
||||
|
||||
The scope is optional and can specify the area of the codebase affected (e.g.,
|
||||
olm, cipher).
|
||||
|
||||
### Security fixes
|
||||
|
||||
Commits addressing security vulnerabilities must include specific trailers for
|
||||
vulnerability metadata, which should also be reflected in the corresponding
|
||||
changelog entry.
|
||||
|
||||
The metadata must be included in the following git-trailers:
|
||||
|
||||
- `Security-Impact`: The magnitude of harm that can be expected, i.e.
|
||||
low/moderate/high/critical.
|
||||
- `CVE`: The CVE that was assigned to this issue.
|
||||
- `GitHub-Advisory`: The GitHub advisory identifier.
|
||||
|
||||
Please include all the fields that are available.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
fix(crypto): Use a constant-time Base64 encoder for secret key material
|
||||
|
||||
This patch fixes a security issue around a side-channel vulnerability[1]
|
||||
when decoding secret key material using Base64.
|
||||
|
||||
In some circumstances an attacker can obtain information about secret
|
||||
secret key material via a controlled-channel and side-channel attack.
|
||||
|
||||
This patch avoids the side-channel by switching to the base64ct crate
|
||||
for the encoding, and more importantly, the decoding of secret key
|
||||
material.
|
||||
|
||||
Security-Impact: Low
|
||||
CVE: CVE-2024-40640
|
||||
GitHub-Advisory: GHSA-j8cm-g7r6-hfpq
|
||||
```
|
||||
|
||||
## Review process
|
||||
|
||||
To streamline the review process and make it easier for maintainers to review
|
||||
your contributions, follow these basic rules:
|
||||
|
||||
1. Do not force push after a review has started. This helps maintainers track
|
||||
incremental changes without confusion and makes it easier to follow the
|
||||
evolution of the code.
|
||||
|
||||
2. Do not mix moves and refactoring with functional changes. Keep these in
|
||||
separate commits for clarity. This ensures that the purpose of each commit is
|
||||
clear and easy to review.
|
||||
|
||||
3. Each commit must compile. If commits don’t compile, git bisect becomes
|
||||
unusable, which hampers the debugging process and makes it harder to identify
|
||||
the source of issues.
|
||||
|
||||
4. Commits should only introduce test failures if they are proving that a bug
|
||||
exists. New features should never introduce test failures. Test failures
|
||||
should only be used to demonstrate existing bugs, not as part of adding new
|
||||
functionality.
|
||||
|
||||
5. Keep PRs on topic and small. Large PRs are harder to review and more prone to
|
||||
delays. Create small, focused commits that address a single topic. Use a
|
||||
combination of [git add] -p or [git checkout] -p to split changes into
|
||||
logical units. This makes your work easier to review and reduces the chance
|
||||
of introducing unrelated changes.
|
||||
|
||||
[git add]: https://git-scm.com/docs/git-add#Documentation/git-add.txt---patch
|
||||
[git checkout]: https://git-scm.com/docs/git-checkout#Documentation/git-checkout.txt---patch
|
||||
|
||||
### Addressing review comments using fixup commits
|
||||
|
||||
So you posted a PR and the maintainers aren't quite happy with it. Here are some
|
||||
guidelines to make the maintainers life easier and increase the chances that
|
||||
your PR will be reviewed swiftly.
|
||||
|
||||
1. Use [fixup] commits. When addressing reviewer feedback, you can create fixup
|
||||
commits. These commits mark your changes as corrections of specific previous
|
||||
commits in the PR.
|
||||
|
||||
Example:
|
||||
|
||||
```shell
|
||||
git commit --fixup=<commit-hash>
|
||||
```
|
||||
|
||||
This command creates a new commit that refers to an existing one, making it
|
||||
easier to rebase and squash later while showing reviewers the history of fixes.
|
||||
For extra points, link to the fixup commit in the thread where the change was
|
||||
requested.
|
||||
|
||||
2. After all requested changes were addressed, feel free to re-request a review.
|
||||
People might not notice that all changes were addressed.
|
||||
|
||||
3. Once the PR has been approved, rebase your PR to squash all the fixup
|
||||
commits, the [autosquash] option can help with this.
|
||||
|
||||
```shell
|
||||
git rebase main --interactive --autosquash
|
||||
```
|
||||
|
||||
[fixup]: https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---fixupamendrewordltcommitgt
|
||||
[autosquash]: https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---autosquash
|
||||
|
||||
## Sign off
|
||||
|
||||
In order to have a concrete record that your contribution is intentional
|
||||
and you agree to license it under the same terms as the project's
|
||||
license, we've adopted the same lightweight approach that the [Linux
|
||||
Kernel](https://www.kernel.org/doc/Documentation/SubmittingPatches),
|
||||
[Docker](https://github.com/docker/docker/blob/master/CONTRIBUTING.md),
|
||||
and many other projects use: the DCO ([Developer Certificate of
|
||||
Origin](http://developercertificate.org/)). This is a simple declaration that
|
||||
you wrote the contribution or otherwise have the right to contribute it to
|
||||
Matrix:
|
||||
and you agree to license it under the same terms as the project's license, we've
|
||||
adopted the same lightweight approach that the Linux Kernel
|
||||
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
|
||||
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
|
||||
projects use: the DCO (Developer Certificate of Origin:
|
||||
http://developercertificate.org/). This is a simple declaration that you wrote
|
||||
the contribution or otherwise have the right to contribute it to Matrix:
|
||||
|
||||
```text
|
||||
```
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
@@ -319,10 +81,15 @@ By making a contribution to this project, I certify that:
|
||||
If you agree to this for your contribution, then all that's needed is to
|
||||
include the line in your commit or pull request comment:
|
||||
|
||||
```text
|
||||
```
|
||||
Signed-off-by: Your Name <your@email.example.org>
|
||||
```
|
||||
|
||||
We accept contributions under a legally identifiable name, such as your name on
|
||||
government documentation or common-law names (names claimed by legitimate usage
|
||||
or repute). Unfortunately, we cannot accept anonymous contributions at this
|
||||
time.
|
||||
|
||||
Git allows you to add this signoff automatically when using the `-s` flag to
|
||||
`git commit`, which uses the name and email set in your `user.name` and
|
||||
`user.email` git configs.
|
||||
@@ -330,53 +97,6 @@ Git allows you to add this signoff automatically when using the `-s` flag to
|
||||
If you forgot to sign off your commits before making your pull request and are
|
||||
on Git 2.17+ you can mass signoff using rebase:
|
||||
|
||||
```text
|
||||
```
|
||||
git rebase --signoff origin/main
|
||||
```
|
||||
|
||||
## AI policy
|
||||
|
||||
This policy is a copy of the [Forgejo's AI agreement][Forgejo].
|
||||
|
||||
### Terminology
|
||||
|
||||
This does not necessarily reflect the official or commonly used terminology.
|
||||
|
||||
Software and services that heavily rely on large language model technology to
|
||||
generate their outcomes are referred to as _Artificial Intelligence_ (AI).
|
||||
Examples of products that fit this definition: GitHub Copilot, ChatGPT, Claude
|
||||
Sonnet, DeepSeek, Llama and Gemini.
|
||||
|
||||
There is a distinction between _general_ and _narrow_ AI, all the aforementioned
|
||||
examples fall under general AI as they were not trained to execute a specific
|
||||
well-defined task. Narrow AI is trained to be used for specific well-defined
|
||||
tasks where the problem space is known in advance.
|
||||
|
||||
_Vibe coding_ is the practice where AI creates a code change (feature, bugfix,
|
||||
tests, refactor) with a human that describes what needs to be implemented.
|
||||
|
||||
_AI agents_ are AIs that are configured to perform interactions or make changes
|
||||
with little to no human supervision.
|
||||
|
||||
### Agreement
|
||||
|
||||
1. If content was made with the help of AI, you **must** convey that this is
|
||||
the case. This includes content that you authored but was motivated by a
|
||||
suggestion of AI.
|
||||
2. If at any point you used AI's work in your contribution you should make
|
||||
an effort to **verify** that you can submit this under the license of the
|
||||
repository.
|
||||
3. The **accountability** of using AI in a contribution lies with the person
|
||||
that makes that contribution.
|
||||
4. All communication, that includes: commit messages, pull request messages,
|
||||
documentation, code comments and issues (and comments on issues/pull
|
||||
requests), that is intended to be read by people to understand your thoughts
|
||||
and work **must not** have been generated with AI. We exclude machine
|
||||
translation and tooling that helps with grammar and spelling check.
|
||||
5. Using general AI for review is **forbidden**. If the change contains changes
|
||||
to the user experience it has to be approved by a human reviewer.
|
||||
6. It is **not allowed** to use AI in an autonomous-looking way to contribute to
|
||||
the Matrix Rust SDK. This also applies when someone engages in _vibe coding_
|
||||
or uses so-called _agent mode_.
|
||||
|
||||
[Forgejo]: https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md
|
||||
|
||||
Generated
+2654
-3697
File diff suppressed because it is too large
Load Diff
+55
-198
@@ -4,171 +4,70 @@ members = [
|
||||
"bindings/matrix-sdk-crypto-ffi",
|
||||
"bindings/matrix-sdk-ffi",
|
||||
"crates/*",
|
||||
"examples/*",
|
||||
"labs/*",
|
||||
"testing/*",
|
||||
"examples/*",
|
||||
"uniffi-bindgen",
|
||||
"xtask",
|
||||
]
|
||||
exclude = ["testing/data"]
|
||||
# xtask, multiverse, testing and the bindings should only be built when invoked explicitly.
|
||||
# xtask, testing and the bindings should only be built when invoked explicitly.
|
||||
default-members = ["benchmarks", "crates/*"]
|
||||
resolver = "3"
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
rust-version = "1.93"
|
||||
rust-version = "1.70"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = { version = "1.0.100", default-features = false }
|
||||
aquamarine = { version = "0.6.0", default-features = false }
|
||||
as_variant = { version = "1.3.0", default-features = false }
|
||||
assert-json-diff = { version = "2.0.2", default-features = false }
|
||||
assert_matches = { version = "1.5.0", default-features = false }
|
||||
assert_matches2 = { version = "0.1.2", default-features = false }
|
||||
async_cell = { version = "0.2.3", default-features = false }
|
||||
async-compat = { version = "0.2.5", default-features = false }
|
||||
async-once-cell = { version = "0.5.4", default-features = false }
|
||||
async-rx = { version = "0.2.0", default-features = false }
|
||||
# Bumping this to 0.3.6 produces a test failure because the semantic between the
|
||||
# versions changed subtly: https://github.com/matrix-org/matrix-rust-sdk/issues/4599
|
||||
async-stream = { version = "0.3.6", default-features = false }
|
||||
async-trait = { version = "0.1.89", default-features = false }
|
||||
base64 = { version = "0.22.1", default-features = false, features = ["std"] }
|
||||
bitflags = { version = "2.10.0", default-features = false }
|
||||
byteorder = { version = "1.5.0", default-features = false, features = ["std"] }
|
||||
cfg-if = { version = "1.0.4", default-features = false }
|
||||
clap = { version = "4.5.53", default-features = false, features = ["std", "help", "usage"] }
|
||||
chrono = { version = "0.4.42", default-features = false, features = ["clock", "std", "oldtime", "wasmbind"] }
|
||||
dirs = { version = "6.0.0", default-features = false }
|
||||
eyeball = { version = "0.8.8", default-features = false, features = ["tracing"] }
|
||||
eyeball-im = { version = "0.8.0", default-features = false, features = ["tracing"] }
|
||||
eyeball-im-util = { version = "0.10.0", default-features = false }
|
||||
futures-core = { version = "0.3.31", default-features = false, features = ["std"] }
|
||||
futures-executor = { version = "0.3.31", default-features = false, features = ["std"] }
|
||||
futures-util = { version = "0.3.31", default-features = false, features = ["std"] }
|
||||
getrandom = { version = "0.4.2", default-features = false }
|
||||
gloo-timers = { version = "0.3.0", default-features = false }
|
||||
gloo-utils = { version = "0.2.0", default-features = false, features = ["serde"] }
|
||||
growable-bloom-filter = { version = "2.1.1", default-features = false }
|
||||
hkdf = { version = "0.12.4", default-features = false }
|
||||
hmac = { version = "0.12.1", default-features = false }
|
||||
http = { version = "1.3.1", default-features = false }
|
||||
imbl = { version = "6.1.0", default-features = false }
|
||||
indexed_db_futures = { version = "0.7.0", package = "matrix_indexed_db_futures", default-features = false }
|
||||
indexmap = { version = "2.12.1", default-features = false }
|
||||
insta = { version = "1.44.1", features = ["json", "redactions"] }
|
||||
itertools = { version = "0.14.0", default-features = false, features = ["use_std"] }
|
||||
js-sys = { version = "0.3.82", default-features = false, features = ["std"] }
|
||||
mime = { version = "0.3.17", default-features = false }
|
||||
oauth2 = { version = "5.0.0", default-features = false, features = ["timing-resistant-secret-traits"] }
|
||||
oauth2-reqwest = { version = "0.1.0-alpha.3", default-features = false }
|
||||
pbkdf2 = { version = "0.12.2", default-features = false }
|
||||
pin-project-lite = { version = "0.2.16", default-features = false }
|
||||
proc-macro2 = { version = "1.0.106", default-features = false }
|
||||
proptest = { version = "1.9.0", default-features = false, features = ["std"] }
|
||||
quote = { version = "1.0.37", default-features = false }
|
||||
rand = { version = "0.10.1", default-features = false, features = ["std", "std_rng", "thread_rng"] }
|
||||
regex = { version = "1.12.2", default-features = false }
|
||||
reqwest = { version = "0.13.1", default-features = false }
|
||||
rmp-serde = { version = "1.3.0", default-features = false }
|
||||
ruma = { git = "https://github.com/ruma/ruma", rev = "7680eebd9586669e1a4e5b1fd1c2c691221369d4", features = [
|
||||
"client-api-c",
|
||||
"compat-unset-avatar",
|
||||
"compat-upload-signatures",
|
||||
"compat-arbitrary-length-ids",
|
||||
"compat-tag-info",
|
||||
"compat-encrypted-stickers",
|
||||
"compat-lax-room-create-deser",
|
||||
"compat-lax-room-topic-deser",
|
||||
"unstable-msc3230",
|
||||
"unstable-msc3401",
|
||||
"unstable-msc3417",
|
||||
"unstable-msc3488",
|
||||
"unstable-msc3489",
|
||||
"unstable-msc4075",
|
||||
"unstable-msc4140",
|
||||
"unstable-msc4143",
|
||||
"unstable-msc4171",
|
||||
"unstable-msc4278",
|
||||
"unstable-msc4286",
|
||||
"unstable-msc4306",
|
||||
"unstable-msc4308",
|
||||
"unstable-msc4310",
|
||||
] }
|
||||
sentry = { version = "0.47.0", default-features = false }
|
||||
sentry-tracing = { version = "0.47.0", default-features = false }
|
||||
serde = { version = "1.0.228", default-features = false, features = ["std", "rc", "derive"] }
|
||||
serde_html_form = { version = "0.2.8", default-features = false }
|
||||
serde_json = { version = "1.0.145", default-features = false, features = ["std"] }
|
||||
sha2 = { version = "0.10.9", default-features = false }
|
||||
similar-asserts = { version = "1.7.0", default-features = false }
|
||||
stream_assert = { version = "0.1.1", default-features = false }
|
||||
syn = { version = "2.0.43", default-features = false, features = ["derive", "parsing", "printing", "clone-impls"] }
|
||||
tempfile = { version = "3.23.0", default-features = false }
|
||||
thiserror = { version = "2.0.17", default-features = false }
|
||||
tokio = { version = "1.48.0", default-features = false, features = ["sync"] }
|
||||
tokio-stream = { version = "0.1.17", default-features = false }
|
||||
tracing = { version = "0.1.41", default-features = false, features = ["std"] }
|
||||
tracing-appender = { version = "0.2.3", default-features = false }
|
||||
tracing-core = { version = "0.1.34", default-features = false }
|
||||
tracing-subscriber = { version = "0.3.20", default-features = false, features = ["std", "smallvec", "fmt"] }
|
||||
unicode-normalization = { version = "0.1.25", default-features = false }
|
||||
unicode-segmentation = { version = "1.12.0", default-features = false }
|
||||
uniffi = { version = "0.31.0", default-features = false, features = ["cargo-metadata"] }
|
||||
uniffi_bindgen = { version = "0.31.0", default-features = false, features = ["cargo-metadata"] }
|
||||
url = { version = "2.5.7", default-features = false }
|
||||
uuid = { version = "1.18.1", default-features = false }
|
||||
vergen-gitcl = { version = "1.0.8", default-features = false }
|
||||
vodozemac = { version = "0.10.0", default-features = false, features = ["libolm-compat", "insecure-pk-encryption", "experimental-session-config"] }
|
||||
wasm-bindgen = { version = "0.2.105", default-features = false }
|
||||
wasm-bindgen-test = { version = "0.3.55", default-features = false, features = ["std"] }
|
||||
web-sys = { version = "0.3.82", default-features = false }
|
||||
wiremock = { version = "0.6.5", default-features = false }
|
||||
zeroize = { version = "1.8.2", default-features = false }
|
||||
anyhow = "1.0.68"
|
||||
assert-json-diff = "2"
|
||||
assert_matches = "1.5.0"
|
||||
assert_matches2 = "0.1.1"
|
||||
async-rx = "0.1.3"
|
||||
async-stream = "0.3.3"
|
||||
async-trait = "0.1.60"
|
||||
as_variant = "1.2.0"
|
||||
base64 = "0.21.0"
|
||||
byteorder = "1.4.3"
|
||||
eyeball = { version = "0.8.7", features = ["tracing"] }
|
||||
eyeball-im = { version = "0.4.1", features = ["tracing"] }
|
||||
eyeball-im-util = "0.5.1"
|
||||
futures-core = "0.3.28"
|
||||
futures-executor = "0.3.21"
|
||||
futures-util = { version = "0.3.26", default-features = false, features = ["alloc"] }
|
||||
http = "0.2.6"
|
||||
itertools = "0.12.0"
|
||||
ruma = { version = "0.9.3", features = ["client-api-c", "compat-upload-signatures", "compat-user-id", "compat-arbitrary-length-ids", "unstable-msc3401"] }
|
||||
ruma-common = "0.12.0"
|
||||
once_cell = "1.16.0"
|
||||
rand = "0.8.5"
|
||||
serde = "1.0.151"
|
||||
serde_html_form = "0.2.0"
|
||||
serde_json = "1.0.91"
|
||||
sha2 = "0.10.8"
|
||||
stream_assert = "0.1.1"
|
||||
thiserror = "1.0.38"
|
||||
tokio = { version = "1.30.0", default-features = false, features = ["sync"] }
|
||||
tokio-stream = "0.1.14"
|
||||
tracing = { version = "0.1.40", default-features = false, features = ["std"] }
|
||||
tracing-core = "0.1.32"
|
||||
uniffi = { version = "0.25.3", git = "https://github.com/mozilla/uniffi-rs", rev = "0d58c94cbd2ef63554f3388d03d55984be76bb1f" }
|
||||
uniffi_bindgen = { version = "0.25.3", git = "https://github.com/mozilla/uniffi-rs", rev = "0d58c94cbd2ef63554f3388d03d55984be76bb1f" }
|
||||
vodozemac = "0.5.0"
|
||||
zeroize = "1.6.0"
|
||||
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.16.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.16.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.16.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.16.0" }
|
||||
matrix-sdk-ffi-macros = { path = "bindings/matrix-sdk-ffi-macros", version = "0.7.0" }
|
||||
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.16.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.16.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.16.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.16.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.16.0" }
|
||||
matrix-sdk-test-utils = { path = "testing/matrix-sdk-test-utils", version = "0.16.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.16.0", default-features = false }
|
||||
matrix-sdk-search = { path = "crates/matrix-sdk-search", version = "0.16.0" }
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.7.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.7.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.7.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.7.0" }
|
||||
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.7.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.7.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.7.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.7.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.7.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.7.0", default-features = false }
|
||||
|
||||
[workspace.lints.rust]
|
||||
rust_2018_idioms = "warn"
|
||||
semicolon_in_expressions_from_macros = "warn"
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(tarpaulin_include)', # Used by tarpaulin (code coverage)
|
||||
'cfg(ruma_unstable_exhaustive_types)', # Used by Ruma's EventContent derive macro
|
||||
] }
|
||||
unused_extern_crates = "warn"
|
||||
unused_import_braces = "warn"
|
||||
unused_qualifications = "warn"
|
||||
trivial_casts = "warn"
|
||||
trivial_numeric_casts = "warn"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
assigning_clones = "allow"
|
||||
box_default = "allow"
|
||||
cloned_instead_of_copied = "warn"
|
||||
dbg_macro = "warn"
|
||||
inefficient_to_string = "warn"
|
||||
macro_use_imports = "warn"
|
||||
manual_let_else = "warn"
|
||||
mut_mut = "warn"
|
||||
needless_borrow = "warn"
|
||||
nonstandard_macro_braces = "warn"
|
||||
redundant_clone = "warn"
|
||||
str_to_string = "warn"
|
||||
todo = "warn"
|
||||
unnecessary_semicolon = "warn"
|
||||
unused_async = "warn"
|
||||
# Default release profile, select with `--release`
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
# Default development profile; default for most Cargo commands, otherwise
|
||||
# selected with `--debug`
|
||||
@@ -176,27 +75,11 @@ unused_async = "warn"
|
||||
# Saves a lot of disk space. If symbols are needed, use the dbg profile.
|
||||
debug = 0
|
||||
|
||||
# Profile for debug builds with full optimization and minimal debug symbols.
|
||||
# This should be just enough to have proper backtraces, having way smaller binaries
|
||||
# (10% of the size with full debug symbols profile, like `reldbg`).
|
||||
# This profile differs from `reldbg` in not containing the debug symbols needed for
|
||||
# debugging with LLDB/GDB, trading that for binary size, allowing quick iterations
|
||||
# of building the bindings, installing in a real device, testing your changes, repeat.
|
||||
# It's also different from `dev` in having enough debug symbols to display backtraces.
|
||||
[profile.reldev]
|
||||
inherits = "dev"
|
||||
opt-level = 3
|
||||
debug = "line-tables-only"
|
||||
strip = "debuginfo"
|
||||
|
||||
[profile.dev.package]
|
||||
# Optimize quote even in debug mode. Speeds up proc-macros enough to account
|
||||
# for the extra time of optimizing it for a clean build of matrix-sdk-ffi.
|
||||
quote = { opt-level = 2 }
|
||||
sha2 = { opt-level = 2 }
|
||||
# faster runs for insta.rs snapshot testing
|
||||
insta.opt-level = 3
|
||||
similar.opt-level = 3
|
||||
|
||||
# Custom profile with full debugging info, use `--profile dbg` to select
|
||||
[profile.dbg]
|
||||
@@ -209,32 +92,6 @@ debug = 2
|
||||
inherits = "dbg"
|
||||
opt-level = 3
|
||||
|
||||
[profile.dist]
|
||||
# Use release profile as a base
|
||||
inherits = "release"
|
||||
# Strip the minimal debug info, while still allowing us to have proper backtraces, but it will affect debuggers
|
||||
strip = "debuginfo"
|
||||
# Use link time optimizations
|
||||
lto = true
|
||||
# Use binary size optimization, since this is intended for distributed copies of the SDK
|
||||
opt-level = "s"
|
||||
|
||||
[profile.profiling]
|
||||
inherits = "release"
|
||||
# LTO is too slow to compile.
|
||||
lto = false
|
||||
# Get symbol names for profiling purposes.
|
||||
debug = true
|
||||
|
||||
[profile.bench]
|
||||
inherits = "release"
|
||||
lto = false
|
||||
|
||||
[patch.crates-io]
|
||||
async-compat = { git = "https://github.com/element-hq/async-compat", rev = "5a27c8b290f1f1dcfc0c4ec22c464e38528aa591" }
|
||||
const_panic = { git = "https://github.com/jplatte/const_panic", rev = "e0b317a9a7bde2d48a7d15b6a60d70e4a41d3b5f" }
|
||||
# Needed to fix rotation log issue on Android (https://github.com/tokio-rs/tracing/issues/2937)
|
||||
tracing = { git = "https://github.com/tokio-rs/tracing.git", rev = "20f5b3d8ba057ca9c4ae00ad30dda3dce8a71c05" }
|
||||
tracing-core = { git = "https://github.com/tokio-rs/tracing.git", rev = "20f5b3d8ba057ca9c4ae00ad30dda3dce8a71c05" }
|
||||
tracing-subscriber = { git = "https://github.com/tokio-rs/tracing.git", rev = "20f5b3d8ba057ca9c4ae00ad30dda3dce8a71c05" }
|
||||
tracing-appender = { git = "https://github.com/tokio-rs/tracing.git", rev = "20f5b3d8ba057ca9c4ae00ad30dda3dce8a71c05" }
|
||||
async-compat = { git = "https://github.com/jplatte/async-compat", rev = "16dc8597ec09a6102d58d4e7b67714a35dd0ecb8" }
|
||||
const_panic = { git = "https://github.com/jplatte/const_panic", rev = "9024a4cb3eac45c1d2d980f17aaee287b17be498" }
|
||||
|
||||
@@ -1,90 +1,47 @@
|
||||
<h1 align="center">Matrix Rust SDK</h1>
|
||||

|
||||
[](https://codecov.io/gh/matrix-org/matrix-rust-sdk)
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://matrix.to/#/#matrix-rust-sdk:matrix.org)
|
||||
[](https://matrix-org.github.io/matrix-rust-sdk/matrix_sdk/)
|
||||
[](https://docs.rs/matrix-sdk)
|
||||
|
||||
<div align="center">
|
||||
<em>Your all-in-one toolkit for creating Matrix clients with Rust, from simple bots to full-featured apps.</em>
|
||||
<br />
|
||||
<img src="contrib/logo.svg">
|
||||
<hr />
|
||||
<a href="https://github.com/matrix-org/matrix-rust-sdk/releases">
|
||||
<img src="https://img.shields.io/github/v/release/matrix-org/matrix-rust-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=GitHub&logoColor=white"></a>
|
||||
<a href="https://crates.io/crates/matrix-sdk/">
|
||||
<img src="https://img.shields.io/crates/v/matrix-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Rust&logoColor=white"></a>
|
||||
<a href="https://codecov.io/gh/matrix-org/matrix-rust-sdk">
|
||||
<img src="https://img.shields.io/codecov/c/gh/matrix-org/matrix-rust-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Codecov&logoColor=white"></a>
|
||||
<br />
|
||||
<a href="https://docs.rs/matrix-sdk/">
|
||||
<img src="https://img.shields.io/docsrs/matrix-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Rust&logoColor=white"></a>
|
||||
<a href="https://github.com/matrix-org/matrix-rust-sdk/actions/workflows/ci.yml">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/matrix-org/matrix-rust-sdk/ci.yml?style=flat&labelColor=1C2E27&color=66845F&logo=GitHub%20Actions&logoColor=white"></a>
|
||||
</div>
|
||||
# matrix-rust-sdk
|
||||
|
||||
<div align="center">
|
||||
**matrix-rust-sdk** is an implementation of a [Matrix][] client-server library in [Rust][].
|
||||
|
||||
The Matrix Rust SDK is a collection of libraries that make it easier to build [Matrix] clients in [Rust].
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<picture>
|
||||
<source srcset="contrib/element-logo-light.png" media="(prefers-color-scheme: dark)">
|
||||
<source srcset="contrib/element-logo-dark.png" media="(prefers-color-scheme: light)">
|
||||
<img src="contrib/element-logo-fallback.png" alt="Element logo">
|
||||
</picture>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
Development of the SDK is proudly sponsored and maintained by [Element](https://element.io). Element uses the SDK in their next-generation mobile apps Element X on [iOS](https://github.com/element-hq/element-x-ios) and [Android](https://github.com/element-hq/element-x-android) and has plans to introduce it to the web and desktop clients as well.
|
||||
|
||||
The SDK is also the basis for multiple Matrix projects and we welcome contributions from all.
|
||||
|
||||
</div>
|
||||
|
||||
## Purpose
|
||||
|
||||
The SDK takes care of the low-level details like encryption,
|
||||
syncing, and room state, so you can focus on your app's logic and UI. Whether
|
||||
you're writing a small bot, a desktop client, or something in between, the SDK
|
||||
is designed to be flexible, async-friendly, and ready to use out of the box.
|
||||
[Matrix]: https://matrix.org/
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
|
||||
## Project structure
|
||||
|
||||
The Matrix Rust SDK is made up of several crates that build on top of each
|
||||
other. The following crates are expected to be usable as direct dependencies:
|
||||
The rust-sdk consists of multiple crates that can be picked at your convenience:
|
||||
|
||||
- [matrix-sdk-ui](https://docs.rs/matrix-sdk-ui/latest/matrix_sdk_ui/) – A high-level client library that makes it easy to build
|
||||
full-featured UI clients with minimal setup. Check out our reference client,
|
||||
[multiverse](https://github.com/matrix-org/matrix-rust-sdk/tree/main/labs/multiverse), for an example.
|
||||
- [matrix-sdk](https://docs.rs/matrix-sdk/latest/matrix_sdk/) – A mid-level client library, ideal for building bots, custom
|
||||
clients, or higher-level abstractions. You can find example usage in the
|
||||
[examples directory](https://github.com/matrix-org/matrix-rust-sdk/tree/main/examples).
|
||||
- [matrix-sdk-crypto](https://docs.rs/matrix-sdk-crypto/latest/matrix_sdk_crypto/) – A standalone encryption state machine with no network I/O,
|
||||
providing end-to-end encryption support for Matrix clients and libraries.
|
||||
See the [crypto tutorial](https://docs.rs/matrix-sdk-crypto/latest/matrix_sdk_crypto/tutorial/index.html)
|
||||
for a step-by-step introduction.
|
||||
- **matrix-sdk** - High level client library, with batteries included, you're most likely
|
||||
interested in this.
|
||||
- **matrix-sdk-base** - No (network) IO client state machine that can be used to embed a
|
||||
Matrix client in your project or build a full fledged network enabled client
|
||||
lib on top of it.
|
||||
- **matrix-sdk-crypto** - No (network) IO encryption state machine that can be
|
||||
used to add Matrix E2EE support to your client or client library.
|
||||
|
||||
All other crates are effectively internal-only and only structured as crates
|
||||
for organizational purposes and to improve compilation times. Direct usage of them is discouraged.
|
||||
## Minimum Supported Rust Version (MSRV)
|
||||
|
||||
These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.70`.
|
||||
|
||||
## Status
|
||||
|
||||
The library is considered production ready and backs multiple client
|
||||
implementations such as Element X
|
||||
[[1]](https://github.com/element-hq/element-x-ios)
|
||||
[[2]](https://github.com/element-hq/element-x-android),
|
||||
[Fractal](https://gitlab.gnome.org/World/fractal) and [iamb](https://github.com/ulyssa/iamb). Client developers should feel
|
||||
confident to build upon it.
|
||||
The library is in an alpha state, things that are implemented generally work but
|
||||
the API will change in breaking ways.
|
||||
|
||||
If you are interested in using the matrix-sdk now is the time to try it out and
|
||||
provide feedback.
|
||||
|
||||
## Bindings
|
||||
|
||||
The higher-level crates of the Matrix Rust SDK can be embedded in other
|
||||
environments such as Swift, Kotlin, JavaScript, and Node.js. Check out the
|
||||
[bindings/](./bindings/) directory to learn more about how to integrate the SDK
|
||||
into your language of choice.
|
||||
Some crates of the **matrix-rust-sdk** can be embedded inside other
|
||||
environments, like Swift, Kotlin, JavaScript, Node.js etc. Please,
|
||||
explore the [`bindings/`](./bindings/) directory to learn more.
|
||||
|
||||
## License
|
||||
|
||||
[Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
|
||||
[Matrix]: https://matrix.org/
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# Releasing and publishing the SDK
|
||||
|
||||
While the release process can be handled manually, `cargo-release` has been
|
||||
configured to make it more convenient.
|
||||
|
||||
By default, [`cargo-release`](https://github.com/crate-ci/cargo-release) assumes
|
||||
that no pull request is required to cut a release. However, since the SDK
|
||||
repo is set up so that each push requires a pull request, we need to slightly
|
||||
deviate from the default workflow. A `cargo-xtask` has been created to make the
|
||||
process as smooth as possible.
|
||||
|
||||
The procedure is as follows:
|
||||
|
||||
1. Switch to a release branch:
|
||||
|
||||
```bash
|
||||
git switch -c release-x.y.z
|
||||
```
|
||||
|
||||
2. Prepare the release. This will update the `README.md`, set the versions in
|
||||
the `CHANGELOG.md` file, and bump the version in the `Cargo.toml` file.
|
||||
|
||||
```bash
|
||||
cargo xtask release prepare --execute minor|patch|rc
|
||||
```
|
||||
|
||||
3. Double-check and edit the `CHANGELOG.md` and `README.md` if necessary. Once you are
|
||||
satisfied, push the branch and open a PR.
|
||||
|
||||
```bash
|
||||
git push --set-upstream origin/release-x.y.z
|
||||
```
|
||||
|
||||
4. Pass the review and merge the branch as you would with any other branch.
|
||||
|
||||
5. Create tags for your new release, publish the release on crates.io and push
|
||||
the tags:
|
||||
|
||||
```bash
|
||||
# Switch to main first.
|
||||
git switch main
|
||||
# Pull in the now-merged release commit(s).
|
||||
git pull
|
||||
# Create tags, publish the release on crates.io, and push the tags.
|
||||
cargo xtask release publish --execute
|
||||
```
|
||||
|
||||
For more information on cargo-release: https://github.com/crate-ci/cargo-release
|
||||
@@ -0,0 +1,121 @@
|
||||
# Upgrades 0.5 ➜ 0.6
|
||||
|
||||
This is a rough migration guide to help you upgrade your code using matrix-sdk 0.5 to the newly released matrix-sdk 0.6 . While it won't cover all edge cases and problems, we are trying to get the most common issues covered. If you experience any other difficulties in upgrade or need support with using the matrix-sdk in general, please approach us in our [matrix-sdk channel on matrix.org][matrix-channel].
|
||||
|
||||
## Minimum Supported Rust Version Update: `1.60`
|
||||
|
||||
We have updated the minimal rust version you need in order to build `matrix-sdk`, as we require some new dependency resolving features from it:
|
||||
|
||||
> These crates are built with the Rust language version 2021 and require a minimum compiler version of 1.60
|
||||
|
||||
## Dependencies
|
||||
|
||||
Many dependencies have been upgraded. Most notably, we are using `ruma` at version `0.7.0` now. It has seen some renamings and restructurings since our last release, so you might find that some Types have new names now.
|
||||
|
||||
## Repo Structure Updates
|
||||
|
||||
If you are looking at the repository itself, you will find we've rearranged the code quite a bit: we have split out any bindings-specific and testing related crates (and other things) into respective folders, and we've moved all `examples` into its own top-level-folder with each example as their own crate (rendering them easier to find and copy as starting points), all in all slimming down the `crates` folder to the core aspects.
|
||||
|
||||
|
||||
## Architecture Changes / API overall
|
||||
|
||||
### Builder Pattern
|
||||
|
||||
We are moving to the [builder pattern][] (familiar from e.g. `std::io:process:Command`) as the main configurable path for many aspects of the API, including to construct Matrix-Requests and workflows. This has been and is an on-going effort, and this release sees a lot of APIs transitioning to this pattern, you should already be familiar with from the `matrix_sdk::Client::builder()` in `0.5`. This pattern been extended onto:
|
||||
- the [login configuration][login builder] and [login with sso][ssologin builder],
|
||||
- [`SledStore` configuratiion][sled-store builder]
|
||||
- [`Indexeddb` configuration][indexeddb builder]
|
||||
|
||||
Most have fallback (though maybe with deprecation warning) support for an existing code path, but these are likely to be removed in upcoming releases.
|
||||
|
||||
### Splitting of concerns: Media
|
||||
|
||||
In an effort to declutter the `Client` API dedicated types have been created dealing with specific concerns in one place. In `0.5` we introduced `client.account()`, and `client.encryption()`, we are doing the same with `client.media()` to manage media and attachments in one place with the [`media::Media` type][media typ] now.
|
||||
|
||||
The signatures of media uploads, have also changed slightly: rather than expecting a reader `R: Read + Seek`, it now is a simple `&[u8]`. Which also means no more unnecessary `seek(0)` to reset the cursor, as we are just taking an immutable reference now.
|
||||
|
||||
### Event Handling & sync updaes
|
||||
|
||||
If you are using the `client.register_event_handler` function to receive updates on incoming sync events, you'll find yourself with a deprecation warning now. That is because we've refactored and redesigned the event handler logic to allowing `removing` of event handlers on the fly, too. For that the new `add_event_handler()` (and `add_room_event_handler`) will hand you an `EventHandlerHandle` (pardon the pun), which you can pass to `remove_event_handler`, or by using the convenient `client.event_handler_drop_guard` to create a `DropGuard` that will remove the handler when the guard is dropped. While the code still works, we recommend you switch to the new one, as we will be removing the `register_event_handler` and `register_event_handler_context` in a coming release.
|
||||
|
||||
Secondly, you will find a new [`sync_with_result_callback` sync function][sync with result]. Other than the previous sync functions, this will pass the entire `Result` to your callback, allowing you to handle errors or even raise some yourself to stop the loop. Further more, it will propagate any unhandled errors (it still handles retries as before) to the outer caller, allowing the higher level to decide how to handle that (e.g. in case of a network failure). This result-returning-behavior also punshes through the existing `sync` and `sync_with_callback`-API, allowing you to handle them on a higher level now (rather than the futures just resolving). If you find that warning, just adding a `?` to the `.await` of the call is probably the quickest way to move forward.
|
||||
|
||||
### Refresh Tokens
|
||||
|
||||
This release now [supports `refresh_token`s][refresh tokens PR] as part of the [`Session`][session]. It is implemented with a default-flag in serde so deserializing a previously serialized Session (e.g. in a store) will work as before. As part of `refresh_token` support, you can now configure the client via `ClientBuilder.request_refresh_token()` to refresh the access token automagically on certain failures or do it manually by calling `client.refresh_access_token()` yourself. Auto-refresh is _off_ by default.
|
||||
|
||||
You can stay informed about updates on the access token by listening to `client.session_tokens_signal()`.
|
||||
|
||||
### Further changes
|
||||
|
||||
- [`MessageOptions`][message options] has been updated to Matrix 1.3 by making the `from` parameter optional (and function signatures have been updated, too). You can now request the server sends you messages from the first one you are allowed to have received.
|
||||
- `client.user_id()` is not a `future` anymore. Remove any `.await` you had behind it.
|
||||
- `verified()`, `blacklisted()` and `deleted()` on `matrix_sdk::encryption::identities::Device` have been renamed with a `is_` prefix.
|
||||
- `verified()` on `matrix_sdk::encryption::identities::UserIdentity`, too has been prefixed with `is_` and thus is now called `is_verified()`.
|
||||
- The top-level crypto and state-store types of Indexeddb and Sled have been renamed to unique types>
|
||||
- `state_store` and `crypto_store` do not need to be boxed anymore when passed to the [`StoreConfig`][store config]
|
||||
- Indexeddb's `SerializationError` is now `IndexedDBStoreError`
|
||||
- Javascript specific features are now behind the `js` feature-gate
|
||||
- The new experimental next generation of sync ("sliding sync"), with a totally revamped api, can be found behind the optional `sliding-sync`-feature-gate
|
||||
|
||||
|
||||
## Quick Troubleshooting
|
||||
|
||||
You find yourself focused with any of these, here are the steps to follow to upgrade your code accordingly:
|
||||
|
||||
### warning: use of deprecated associated function `matrix_sdk::Client::register_event_handler`: Use [`Client::add_event_handler`](#method.add_event_handler) instead
|
||||
|
||||
As it says on the tin: we have deprecated this function in favor of the newer removable handler approach (see above). You can still continue to use this `fn` for now, but it will be removed in a future release.
|
||||
|
||||
### warning: use of deprecated associated function `matrix_sdk::Client::login`: Replaced by [`Client::login_username`](#method.login_username)
|
||||
|
||||
We have replaced the login facilities with a `LoginBuilder` and recommend you use that from now on. This isn't an error yet, but the function will be removed in a future release.
|
||||
|
||||
### expected slice `[u8]`, found struct ...
|
||||
|
||||
We've updated the `send_attachment` and `Media` signatures to use `&[u8]` rather than `reader: Read + Seek` as it is more convenient and common place for most architectures anyways. If you are using `File::open(path)?` to get that handler, you can just replace that with `std::fs::read(path)?`
|
||||
|
||||
### no method named `verified` found for struct `matrix_sdk::encryption::identities::Device` in the current scope
|
||||
|
||||
Boolean flags like `verified`, `deleted`, `blacklisted`, etc have been renamed with a `is_` prefix. So, just follow the cargo suggestion:
|
||||
```
|
||||
|
|
||||
69 | device.verified()
|
||||
| ^^^^^^^^ help: there is an associated function with a similar name: `is_verified`
|
||||
```
|
||||
|
||||
### unresolved import `matrix_sdk::ruma::events::AnySyncRoomEvent`
|
||||
|
||||
Ruma has been updated to `0.7.0`, you will find some ruma Events names have changed, most notably, the `AnySyncRoomEvent` is now named `AnySyncTimelineEvent` (and not `AnySyncStateEvent`, which cargo wrongly suggests). Just rename the import and usage of it.
|
||||
|
||||
### `std::option::Option<&matrix_sdk::ruma::UserId>` is not a future
|
||||
|
||||
You are seeing something along the lines of:
|
||||
```
|
||||
19 | if room_member.state_key != client.user_id().await.unwrap() {
|
||||
| ^^^^^^ `std::option::Option<&matrix_sdk::ruma::UserId>` is not a future
|
||||
|
|
||||
= help: the trait `Future` is not implemented for `std::option::Option<&matrix_sdk::ruma::UserId>`
|
||||
= note: std::option::Option<&matrix_sdk::ruma::UserId> must be a future or must implement `IntoFuture` to be awaited
|
||||
= note: required because of the requirements on the impl of `IntoFuture` for `std::option::Option<&matrix_sdk::ruma::UserId>`
|
||||
help: remove the `.await`
|
||||
|
|
||||
19 - if room_member.state_key != client.user_id().await.unwrap() {
|
||||
19 + if room_member.state_key != client.user_id().unwrap() {
|
||||
```
|
||||
|
||||
You are using `client.user_id().await` but `user_id()` is no longer `async`. Just follow the cargo suggestion and remove the `.await`, it is not necessary any longer.
|
||||
|
||||
|
||||
[matrix-channel]: https://matrix.to/#/#matrix-rust-sdk:matrix.org
|
||||
[builder pattern]: https://doc.rust-lang.org/1.0.0/style/ownership/builders.html
|
||||
[login builder]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.LoginBuilder.html
|
||||
[ssologin builder]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.SsoLoginBuilder.html
|
||||
[sled-store builder]: https://docs.rs/matrix-sdk-sled/latest/matrix_sdk_sled/struct.SledStateStoreBuilder.html
|
||||
[indexeddb builder]: https://docs.rs/matrix-sdk-indexeddb/latest/matrix_sdk_indexeddb/struct.IndexeddbStateStoreBuilder.html
|
||||
[media type]: https://docs.rs/matrix-sdk/latest/matrix_sdk//media/struct.Media.html
|
||||
[sync with result]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.Client.html#method.sync_with_result_callback
|
||||
[session]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.Session.html
|
||||
[refresh tokens PR]: https://github.com/matrix-org/matrix-rust-sdk/pull/892
|
||||
[store config]: https://docs.rs/matrix-sdk-base/latest/matrix_sdk_base/store/struct.StoreConfig.html
|
||||
[message options]: https://docs.rs/matrix-sdk/latest/matrix_sdk/room/struct.MessagesOptions.html
|
||||
+14
-43
@@ -1,60 +1,31 @@
|
||||
[package]
|
||||
name = "benchmarks"
|
||||
description = "Matrix SDK benchmarks"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
rust-version.workspace = true
|
||||
rust-version = { workspace = true }
|
||||
version = "1.0.0"
|
||||
publish = false
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
[features]
|
||||
codspeed = []
|
||||
|
||||
[dependencies]
|
||||
assert_matches.workspace = true
|
||||
criterion = { version = "4.2.1", features = ["async", "async_tokio", "html_reports"], package = "codspeed-criterion-compat" }
|
||||
futures-util.workspace = true
|
||||
matrix-sdk = { workspace = true, features = ["e2e-encryption", "sqlite", "testing"] }
|
||||
matrix-sdk-base.workspace = true
|
||||
matrix-sdk-crypto.workspace = true
|
||||
criterion = { version = "0.5.1", features = ["async", "async_tokio", "html_reports"] }
|
||||
matrix-sdk-base = { workspace = true }
|
||||
matrix-sdk-crypto = { workspace = true }
|
||||
matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
|
||||
matrix-sdk-test.workspace = true
|
||||
matrix-sdk-ui.workspace = true
|
||||
rand.workspace = true
|
||||
ruma.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tempfile.workspace = true
|
||||
tokio = { workspace = true, features = ["rt-multi-thread"] }
|
||||
wiremock.workspace = true
|
||||
matrix-sdk-test = { workspace = true }
|
||||
matrix-sdk = { workspace = true }
|
||||
ruma = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tempfile = "3.3.0"
|
||||
tokio = { version = "1.24.2", default-features = false, features = ["rt-multi-thread"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
pprof = { version = "0.13.0", features = ["flamegraph", "criterion"] }
|
||||
|
||||
[[bench]]
|
||||
name = "crypto_bench"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "linked_chunk"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "store_bench"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "room_bench"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "timeline"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "event_cache"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "room_list"
|
||||
harness = false
|
||||
|
||||
+3
-10
@@ -8,7 +8,7 @@ can be found [here](https://bheisler.github.io/criterion.rs/book/criterion_rs.ht
|
||||
|
||||
## Running the benchmarks
|
||||
|
||||
The benchmark can be run by using the `bench` command of `cargo`:
|
||||
The benchmark can be simply run by using the `bench` command of `cargo`:
|
||||
|
||||
```bash
|
||||
$ cargo bench
|
||||
@@ -16,13 +16,6 @@ $ cargo bench
|
||||
|
||||
This will work from the workspace directory of the rust-sdk.
|
||||
|
||||
To lower compile times, you might be interested in using the `profiling` profile, that's optimized
|
||||
for a fair tradeoff between compile times and runtime performance:
|
||||
|
||||
```bash
|
||||
$ cargo bench --profile profiling
|
||||
```
|
||||
|
||||
If you want to pass options to the benchmark [you'll need to specify the name of
|
||||
the benchmark](https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options):
|
||||
|
||||
@@ -30,7 +23,7 @@ the benchmark](https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench
|
||||
$ cargo bench --bench crypto_bench -- # Your options go here
|
||||
```
|
||||
|
||||
If you want to run only a specific benchmark, pass the name of the
|
||||
If you want to run only a specific benchmark, simply pass the name of the
|
||||
benchmark as an argument:
|
||||
|
||||
```bash
|
||||
@@ -72,7 +65,7 @@ permisive value is `-1`:
|
||||
$ echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid
|
||||
```
|
||||
|
||||
To generate flame graphs feature, enable the profiling mode using the
|
||||
To generate flame graphs feature simply enable the profiling mode using the
|
||||
`--profile-time` command line flag:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk_crypto::{EncryptionSettings, OlmMachine};
|
||||
use matrix_sdk_sqlite::SqliteCryptoStore;
|
||||
use matrix_sdk_test::ruma_response_from_json;
|
||||
use matrix_sdk_test::response_from_file;
|
||||
use ruma::{
|
||||
DeviceId, OwnedUserId, TransactionId, UserId,
|
||||
api::client::{
|
||||
keys::{claim_keys, get_keys},
|
||||
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
|
||||
api::{
|
||||
client::{
|
||||
keys::{claim_keys, get_keys},
|
||||
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
|
||||
},
|
||||
IncomingResponse,
|
||||
},
|
||||
device_id, room_id, user_id,
|
||||
device_id, room_id, user_id, DeviceId, OwnedUserId, TransactionId, UserId,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use tokio::runtime::Builder;
|
||||
@@ -26,19 +28,25 @@ fn alice_device_id() -> &'static DeviceId {
|
||||
fn keys_query_response() -> get_keys::v3::Response {
|
||||
let data = include_bytes!("crypto_bench/keys_query.json");
|
||||
let data: Value = serde_json::from_slice(data).unwrap();
|
||||
ruma_response_from_json(&data)
|
||||
let data = response_from_file(&data);
|
||||
get_keys::v3::Response::try_from_http_response(data)
|
||||
.expect("Can't parse the `/keys/upload` response")
|
||||
}
|
||||
|
||||
fn keys_claim_response() -> claim_keys::v3::Response {
|
||||
let data = include_bytes!("crypto_bench/keys_claim.json");
|
||||
let data: Value = serde_json::from_slice(data).unwrap();
|
||||
ruma_response_from_json(&data)
|
||||
let data = response_from_file(&data);
|
||||
claim_keys::v3::Response::try_from_http_response(data)
|
||||
.expect("Can't parse the `/keys/upload` response")
|
||||
}
|
||||
|
||||
fn huge_keys_query_response() -> get_keys::v3::Response {
|
||||
let data = include_bytes!("crypto_bench/keys_query_2000_members.json");
|
||||
let data: Value = serde_json::from_slice(data).unwrap();
|
||||
ruma_response_from_json(&data)
|
||||
let data = response_from_file(&data);
|
||||
get_keys::v3::Response::try_from_http_response(data)
|
||||
.expect("Can't parse the `/keys/query` response")
|
||||
}
|
||||
|
||||
pub fn keys_query(c: &mut Criterion) {
|
||||
@@ -59,31 +67,22 @@ pub fn keys_query(c: &mut Criterion) {
|
||||
|
||||
// Benchmark memory store.
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("Device keys query [memory]", &name),
|
||||
&response,
|
||||
|b, response| {
|
||||
b.to_async(&runtime)
|
||||
.iter(|| async { machine.mark_request_as_sent(&txn_id, response).await.unwrap() })
|
||||
},
|
||||
);
|
||||
group.bench_with_input(BenchmarkId::new("memory store", &name), &response, |b, response| {
|
||||
b.to_async(&runtime)
|
||||
.iter(|| async { machine.mark_request_as_sent(&txn_id, response).await.unwrap() })
|
||||
});
|
||||
|
||||
// Benchmark sqlite store.
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = Arc::new(runtime.block_on(SqliteCryptoStore::open(dir.path(), None)).unwrap());
|
||||
let machine = runtime
|
||||
.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store, None))
|
||||
.unwrap();
|
||||
let machine =
|
||||
runtime.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store)).unwrap();
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("Device keys query [SQLite]", &name),
|
||||
&response,
|
||||
|b, response| {
|
||||
b.to_async(&runtime)
|
||||
.iter(|| async { machine.mark_request_as_sent(&txn_id, response).await.unwrap() })
|
||||
},
|
||||
);
|
||||
group.bench_with_input(BenchmarkId::new("sqlite store", &name), &response, |b, response| {
|
||||
b.to_async(&runtime)
|
||||
.iter(|| async { machine.mark_request_as_sent(&txn_id, response).await.unwrap() })
|
||||
});
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
@@ -93,8 +92,6 @@ pub fn keys_query(c: &mut Criterion) {
|
||||
group.finish()
|
||||
}
|
||||
|
||||
/// This test panics on the CI, not sure why so we're disabling it for now.
|
||||
#[cfg(not(feature = "codspeed"))]
|
||||
pub fn keys_claiming(c: &mut Criterion) {
|
||||
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
|
||||
|
||||
@@ -110,65 +107,49 @@ pub fn keys_claiming(c: &mut Criterion) {
|
||||
|
||||
let name = format!("{count} one-time keys");
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("One-time keys claiming [memory]", &name),
|
||||
&response,
|
||||
|b, response| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let machine = runtime.block_on(OlmMachine::new(alice_id(), alice_device_id()));
|
||||
runtime
|
||||
.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response))
|
||||
.unwrap();
|
||||
(machine, &runtime, &txn_id)
|
||||
},
|
||||
move |(machine, runtime, txn_id)| {
|
||||
runtime.block_on(async {
|
||||
machine.mark_request_as_sent(txn_id, response).await.unwrap();
|
||||
drop(machine);
|
||||
})
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("One-time keys claiming [SQLite]", &name),
|
||||
&response,
|
||||
|b, response| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = Arc::new(
|
||||
runtime.block_on(SqliteCryptoStore::open(dir.path(), None)).unwrap(),
|
||||
);
|
||||
|
||||
let machine = runtime
|
||||
.block_on(OlmMachine::with_store(
|
||||
alice_id(),
|
||||
alice_device_id(),
|
||||
store,
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
runtime
|
||||
.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response))
|
||||
.unwrap();
|
||||
(machine, &runtime, &txn_id)
|
||||
},
|
||||
move |(machine, runtime, txn_id)| {
|
||||
runtime.block_on(async {
|
||||
machine.mark_request_as_sent(txn_id, response).await.unwrap();
|
||||
});
|
||||
|
||||
let _ = runtime.enter();
|
||||
group.bench_with_input(BenchmarkId::new("memory store", &name), &response, |b, response| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let machine = runtime.block_on(OlmMachine::new(alice_id(), alice_device_id()));
|
||||
runtime
|
||||
.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response))
|
||||
.unwrap();
|
||||
(machine, &runtime, &txn_id)
|
||||
},
|
||||
move |(machine, runtime, txn_id)| {
|
||||
runtime.block_on(async {
|
||||
machine.mark_request_as_sent(txn_id, response).await.unwrap();
|
||||
drop(machine);
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
)
|
||||
},
|
||||
);
|
||||
})
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("sqlite store", &name), &response, |b, response| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store =
|
||||
Arc::new(runtime.block_on(SqliteCryptoStore::open(dir.path(), None)).unwrap());
|
||||
|
||||
let machine = runtime
|
||||
.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store))
|
||||
.unwrap();
|
||||
runtime
|
||||
.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response))
|
||||
.unwrap();
|
||||
(machine, &runtime, &txn_id)
|
||||
},
|
||||
move |(machine, runtime, txn_id)| {
|
||||
runtime.block_on(async {
|
||||
machine.mark_request_as_sent(txn_id, response).await.unwrap();
|
||||
drop(machine)
|
||||
})
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
|
||||
group.finish()
|
||||
}
|
||||
@@ -196,7 +177,7 @@ pub fn room_key_sharing(c: &mut Criterion) {
|
||||
|
||||
// Benchmark memory store.
|
||||
|
||||
group.bench_function(BenchmarkId::new("Room key sharing [memory]", &name), |b| {
|
||||
group.bench_function(BenchmarkId::new("memory store", &name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let requests = machine
|
||||
.share_room_key(
|
||||
@@ -213,7 +194,7 @@ pub fn room_key_sharing(c: &mut Criterion) {
|
||||
machine.mark_request_as_sent(&request.txn_id, &to_device_response).await.unwrap();
|
||||
}
|
||||
|
||||
machine.discard_room_key(room_id).await.unwrap();
|
||||
machine.invalidate_group_session(room_id).await.unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
@@ -222,13 +203,12 @@ pub fn room_key_sharing(c: &mut Criterion) {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = Arc::new(runtime.block_on(SqliteCryptoStore::open(dir.path(), None)).unwrap());
|
||||
|
||||
let machine = runtime
|
||||
.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store, None))
|
||||
.unwrap();
|
||||
let machine =
|
||||
runtime.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store)).unwrap();
|
||||
runtime.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response)).unwrap();
|
||||
runtime.block_on(machine.mark_request_as_sent(&txn_id, &response)).unwrap();
|
||||
|
||||
group.bench_function(BenchmarkId::new("Room key sharing [SQLite]", &name), |b| {
|
||||
group.bench_function(BenchmarkId::new("sqlite store", &name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let requests = machine
|
||||
.share_room_key(
|
||||
@@ -245,7 +225,7 @@ pub fn room_key_sharing(c: &mut Criterion) {
|
||||
machine.mark_request_as_sent(&request.txn_id, &to_device_response).await.unwrap();
|
||||
}
|
||||
|
||||
machine.discard_room_key(room_id).await.unwrap();
|
||||
machine.invalidate_group_session(room_id).await.unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
@@ -276,7 +256,7 @@ pub fn devices_missing_sessions_collecting(c: &mut Criterion) {
|
||||
|
||||
// Benchmark memory store.
|
||||
|
||||
group.bench_function(BenchmarkId::new("Devices collecting [memory]", &name), |b| {
|
||||
group.bench_function(BenchmarkId::new("memory store", &name), |b| {
|
||||
b.to_async(&runtime).iter_with_large_drop(|| async {
|
||||
machine.get_missing_sessions(users.iter().map(Deref::deref)).await.unwrap()
|
||||
})
|
||||
@@ -287,13 +267,12 @@ pub fn devices_missing_sessions_collecting(c: &mut Criterion) {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = Arc::new(runtime.block_on(SqliteCryptoStore::open(dir.path(), None)).unwrap());
|
||||
|
||||
let machine = runtime
|
||||
.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store, None))
|
||||
.unwrap();
|
||||
let machine =
|
||||
runtime.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store)).unwrap();
|
||||
|
||||
runtime.block_on(machine.mark_request_as_sent(&txn_id, &response)).unwrap();
|
||||
|
||||
group.bench_function(BenchmarkId::new("Devices collecting [SQLite]", &name), |b| {
|
||||
group.bench_function(BenchmarkId::new("sqlite store", &name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
machine.get_missing_sessions(users.iter().map(Deref::deref)).await.unwrap()
|
||||
})
|
||||
@@ -307,18 +286,21 @@ pub fn devices_missing_sessions_collecting(c: &mut Criterion) {
|
||||
group.finish()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "codspeed"))]
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
let criterion = Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
100,
|
||||
pprof::criterion::Output::Flamegraph(None),
|
||||
));
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let criterion = Criterion::default();
|
||||
|
||||
criterion
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = benches;
|
||||
config = Criterion::default();
|
||||
config = criterion();
|
||||
targets = keys_query, keys_claiming, room_key_sharing, devices_missing_sessions_collecting,
|
||||
}
|
||||
|
||||
#[cfg(feature = "codspeed")]
|
||||
criterion_group! {
|
||||
name = benches;
|
||||
config = Criterion::default();
|
||||
targets = keys_query, room_key_sharing, devices_missing_sessions_collecting,
|
||||
}
|
||||
|
||||
criterion_main!(benches);
|
||||
|
||||
@@ -1,361 +0,0 @@
|
||||
use std::{pin::Pin, sync::Arc};
|
||||
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use matrix_sdk::{
|
||||
RoomInfo, RoomState, SqliteEventCacheStore, StateStore,
|
||||
cross_process_lock::CrossProcessLockConfig,
|
||||
store::StoreConfig,
|
||||
sync::{JoinedRoomUpdate, RoomUpdates},
|
||||
test_utils::client::MockClientBuilder,
|
||||
};
|
||||
use matrix_sdk_base::event_cache::store::{DynEventCacheStore, IntoEventCacheStore, MemoryStore};
|
||||
use matrix_sdk_test::{ALICE, base64_sha256_hash, event_factory::EventFactory};
|
||||
use ruma::{
|
||||
OwnedRoomId, RoomId,
|
||||
events::{relation::RelationType, room::message::RoomMessageEventContentWithoutRelation},
|
||||
room_id,
|
||||
};
|
||||
use tempfile::tempdir;
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
type StoreBuilder = Box<dyn Fn() -> Pin<Box<dyn Future<Output = Arc<DynEventCacheStore>>>>>;
|
||||
|
||||
fn handle_room_updates(c: &mut Criterion) {
|
||||
// Create a new asynchronous runtime.
|
||||
let runtime = Builder::new_multi_thread()
|
||||
.enable_time()
|
||||
.enable_io()
|
||||
.build()
|
||||
.expect("Failed to create an asynchronous runtime");
|
||||
|
||||
let mut group = c.benchmark_group("Event cache room updates");
|
||||
group.sample_size(10);
|
||||
|
||||
const NUM_EVENTS: usize = 1000;
|
||||
|
||||
for num_rooms in [1, 10, 100] {
|
||||
// Add some joined rooms, each with NUM_EVENTS in it, to the sync response.
|
||||
let mut room_updates = RoomUpdates::default();
|
||||
|
||||
let mut changes = matrix_sdk::StateChanges::default();
|
||||
|
||||
for i in 0..num_rooms {
|
||||
// Synapse's room IDs for rooms v1 to v11 have an 18 characters localpart.
|
||||
let raw_room_id = format!("!firstbatchroom{i:04}:example.com");
|
||||
|
||||
let room_id = if i % 10 == 9 {
|
||||
// Make 1 in 10 rooms use a room v12 ID, which is a base64 hash similar to an
|
||||
// event ID.
|
||||
RoomId::new_v2(&base64_sha256_hash(raw_room_id.as_bytes())).unwrap()
|
||||
} else {
|
||||
OwnedRoomId::try_from(raw_room_id).unwrap()
|
||||
};
|
||||
let event_factory = EventFactory::new().room(&room_id).sender(&ALICE);
|
||||
|
||||
let mut joined_room_update = JoinedRoomUpdate::default();
|
||||
for j in 0..NUM_EVENTS {
|
||||
let event = event_factory.text_msg(format!("Message {j}")).into();
|
||||
joined_room_update.timeline.events.push(event);
|
||||
}
|
||||
room_updates.joined.insert(room_id.clone(), joined_room_update);
|
||||
|
||||
changes.add_room(RoomInfo::new(&room_id, RoomState::Joined));
|
||||
}
|
||||
|
||||
// Declare new stores for this set of events.
|
||||
let temp_dir = Arc::new(tempdir().unwrap());
|
||||
|
||||
let store_builders: Vec<(_, StoreBuilder)> = vec![
|
||||
(
|
||||
"memory",
|
||||
Box::new(|| Box::pin(async { MemoryStore::default().into_event_cache_store() })),
|
||||
),
|
||||
(
|
||||
"SQLite",
|
||||
Box::new(move || {
|
||||
let temp_dir = temp_dir.clone();
|
||||
Box::pin(async move {
|
||||
// Remove all the files in the temp_dir, to reset the event cache state.
|
||||
for entry in temp_dir.path().read_dir().unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
// If it's a directory, remove it recursively.
|
||||
std::fs::remove_dir_all(path).unwrap();
|
||||
} else {
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Recreate a new store.
|
||||
SqliteEventCacheStore::open(temp_dir.path().join("bench"), None)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_event_cache_store()
|
||||
})
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
let state_store = runtime.block_on(async {
|
||||
let state_store = matrix_sdk::MemoryStore::new();
|
||||
state_store.save_changes(&changes).await.unwrap();
|
||||
Arc::new(state_store)
|
||||
});
|
||||
|
||||
for (store_name, store_builder) in &store_builders {
|
||||
let client = runtime.block_on(async {
|
||||
let event_cache_store = store_builder().await;
|
||||
|
||||
let client = MockClientBuilder::new(None)
|
||||
.on_builder(|builder| {
|
||||
builder.store_config(
|
||||
StoreConfig::new(CrossProcessLockConfig::multi_process(
|
||||
"cross-process-store-locks-holder-name",
|
||||
))
|
||||
.state_store(state_store.clone())
|
||||
.event_cache_store(event_cache_store.clone()),
|
||||
)
|
||||
})
|
||||
.build()
|
||||
.await;
|
||||
|
||||
client.event_cache().subscribe().unwrap();
|
||||
|
||||
client
|
||||
});
|
||||
|
||||
// Define a state store with all rooms known in it.
|
||||
// Define the throughput.
|
||||
group.throughput(Throughput::Elements(num_rooms));
|
||||
|
||||
// Bench the handling of room updates.
|
||||
group.bench_function(
|
||||
BenchmarkId::new(
|
||||
format!("Event cache room updates[{store_name}]"),
|
||||
format!("room count: {num_rooms}"),
|
||||
),
|
||||
|bencher| {
|
||||
bencher.to_async(&runtime).iter(
|
||||
// The routine itself.
|
||||
|| {
|
||||
let room_updates = room_updates.clone();
|
||||
let client = client.clone();
|
||||
|
||||
async move {
|
||||
client.event_cache().clear_all_rooms().await.unwrap();
|
||||
|
||||
client
|
||||
.event_cache()
|
||||
.handle_room_updates(room_updates.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
group.finish()
|
||||
}
|
||||
|
||||
fn find_event_relations(c: &mut Criterion) {
|
||||
// Number of other events to saturate the DB, but that will not be affected by
|
||||
// the benchmark. A small multiple of this number will be added.
|
||||
// When running locally, run with more events than in Codespeed CI.
|
||||
#[cfg(feature = "codspeed")]
|
||||
const NUM_OTHER_EVENTS: usize = 100;
|
||||
#[cfg(not(feature = "codspeed"))]
|
||||
const NUM_OTHER_EVENTS: usize = 1000;
|
||||
|
||||
// Create a new asynchronous runtime.
|
||||
let runtime = Builder::new_multi_thread()
|
||||
.enable_time()
|
||||
.enable_io()
|
||||
.build()
|
||||
.expect("Failed to create an asynchronous runtime");
|
||||
|
||||
let mut group = c.benchmark_group("Event cache room updates");
|
||||
group.sample_size(10);
|
||||
|
||||
// Room v1-v11 ID.
|
||||
let room_id = room_id!("!initialtestingroom:ben.ch");
|
||||
// Room v12 ID.
|
||||
let other_room_id = room_id!("!ICMDdumUm6RRX_eWYY2wMb2w0CY0Z_5OvlY2gBR6ELc");
|
||||
|
||||
// Make the state store aware of the room, so that `client.get_room()` works
|
||||
// with it.
|
||||
let mut changes = matrix_sdk::StateChanges::default();
|
||||
changes.add_room(RoomInfo::new(room_id, RoomState::Joined));
|
||||
changes.add_room(RoomInfo::new(other_room_id, RoomState::Joined));
|
||||
let state_store = runtime.block_on(async {
|
||||
let state_store = matrix_sdk::MemoryStore::new();
|
||||
state_store.save_changes(&changes).await.unwrap();
|
||||
Arc::new(state_store)
|
||||
});
|
||||
|
||||
for num_related_events in [10, 100, 1000] {
|
||||
// Prefill the event cache store with one event and N related events.
|
||||
let mut room_updates = RoomUpdates::default();
|
||||
|
||||
let event_factory = EventFactory::new().room(room_id).sender(&ALICE);
|
||||
|
||||
let mut joined_room_update = JoinedRoomUpdate::default();
|
||||
|
||||
// Add the target event.
|
||||
let target_event = event_factory.text_msg("hello world").into_event();
|
||||
let target_event_id =
|
||||
{ &target_event.event_id().expect("generated event has an event ID") };
|
||||
joined_room_update.timeline.events.push(target_event);
|
||||
|
||||
// Add the numerous edits.
|
||||
for i in 0..num_related_events {
|
||||
let event = event_factory
|
||||
.text_msg(format!("* edit {i}"))
|
||||
.edit(
|
||||
target_event_id,
|
||||
RoomMessageEventContentWithoutRelation::text_plain(format!("edit {i}")),
|
||||
)
|
||||
.into();
|
||||
joined_room_update.timeline.events.push(event);
|
||||
}
|
||||
|
||||
// Add other events, in the same room, without a relation.
|
||||
for i in 0..NUM_OTHER_EVENTS {
|
||||
let event = event_factory.text_msg(format!("unrelated message {i}")).into();
|
||||
joined_room_update.timeline.events.push(event);
|
||||
}
|
||||
|
||||
// Add other events, in the same room, related to other events.
|
||||
let other_target_event = event_factory.text_msg("hello world").into_event();
|
||||
let other_target_event_id =
|
||||
other_target_event.event_id().expect("generated event has an event ID");
|
||||
joined_room_update.timeline.events.push(other_target_event);
|
||||
|
||||
for _i in 0..NUM_OTHER_EVENTS {
|
||||
let event = event_factory.reaction(&other_target_event_id, "👍").into();
|
||||
joined_room_update.timeline.events.push(event);
|
||||
}
|
||||
|
||||
room_updates.joined.insert(room_id.to_owned(), joined_room_update);
|
||||
|
||||
// Add other events, in another room.
|
||||
let mut other_joined_room_update = JoinedRoomUpdate::default();
|
||||
let event_factory = event_factory.room(other_room_id);
|
||||
for i in 0..NUM_OTHER_EVENTS {
|
||||
let event = event_factory.text_msg(format!("hi {i}")).into();
|
||||
other_joined_room_update.timeline.events.push(event);
|
||||
}
|
||||
room_updates.joined.insert(other_room_id.to_owned(), other_joined_room_update);
|
||||
|
||||
changes.add_room(RoomInfo::new(room_id, RoomState::Joined));
|
||||
|
||||
// Declare new stores for this set of events.
|
||||
let temp_dir = Arc::new(tempdir().unwrap());
|
||||
|
||||
let stores = vec![
|
||||
("memory", MemoryStore::default().into_event_cache_store()),
|
||||
(
|
||||
"SQLite",
|
||||
runtime.block_on(async {
|
||||
SqliteEventCacheStore::open(temp_dir.path().join("bench"), None)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_event_cache_store()
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
for (store_name, event_cache_store) in stores {
|
||||
let (client, room_event_cache, _drop_handles) = runtime.block_on(async {
|
||||
let client = MockClientBuilder::new(None)
|
||||
.on_builder(|builder| {
|
||||
builder.store_config(
|
||||
StoreConfig::new(CrossProcessLockConfig::multi_process(
|
||||
"cross-process-store-locks-holder-name",
|
||||
))
|
||||
.state_store(state_store.clone())
|
||||
.event_cache_store(event_cache_store),
|
||||
)
|
||||
})
|
||||
.build()
|
||||
.await;
|
||||
|
||||
client.event_cache().subscribe().unwrap();
|
||||
|
||||
// Sync the updates before starting the benchmark.
|
||||
let mut update_recv = client.event_cache().subscribe_to_room_generic_updates();
|
||||
|
||||
client.event_cache().handle_room_updates(room_updates.clone()).await.unwrap();
|
||||
|
||||
// Wait for the event cache to notify us of the room updates.
|
||||
let update = update_recv.recv().await.unwrap();
|
||||
assert!(update.room_id == room_id || update.room_id == other_room_id);
|
||||
|
||||
let update = update_recv.recv().await.unwrap();
|
||||
assert!(update.room_id == room_id || update.room_id == other_room_id);
|
||||
|
||||
let room = client.get_room(room_id).unwrap();
|
||||
let room_event_cache = room.event_cache().await.unwrap();
|
||||
|
||||
(client, room_event_cache.0, room_event_cache.1)
|
||||
});
|
||||
|
||||
// Define the throughput.
|
||||
group.throughput(Throughput::Elements(num_related_events));
|
||||
|
||||
for filter in [None, Some(vec![RelationType::Replacement])] {
|
||||
group.bench_function(
|
||||
BenchmarkId::new(
|
||||
format!("Event cache find_event_relations[{store_name}]"),
|
||||
format!(
|
||||
"{num_related_events} events, {} filter",
|
||||
if filter.is_some() { "edits" } else { "#no" },
|
||||
),
|
||||
),
|
||||
|bencher| {
|
||||
bencher.to_async(&runtime).iter_batched(
|
||||
// The setup.
|
||||
|| (room_event_cache.clone(), filter.clone()),
|
||||
// The routine itself.
|
||||
|(room_event_cache, filter)| async move {
|
||||
let (target, relations) = room_event_cache
|
||||
.find_event_with_relations(target_event_id, filter)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(target.event_id().unwrap(), *target_event_id);
|
||||
assert_eq!(relations.len(), num_related_events as usize);
|
||||
},
|
||||
criterion::BatchSize::PerIteration,
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(room_event_cache);
|
||||
drop(client);
|
||||
drop(_drop_handles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(state_store);
|
||||
}
|
||||
|
||||
group.finish()
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = event_cache;
|
||||
config = Criterion::default();
|
||||
targets = handle_room_updates, find_event_relations,
|
||||
}
|
||||
|
||||
criterion_main!(event_cache);
|
||||
@@ -1,263 +0,0 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use criterion::{BatchSize, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use matrix_sdk::{
|
||||
SqliteEventCacheStore,
|
||||
linked_chunk::{LinkedChunk, LinkedChunkId, Update, lazy_loader},
|
||||
};
|
||||
use matrix_sdk_base::event_cache::{
|
||||
Event, Gap,
|
||||
store::{DEFAULT_CHUNK_CAPACITY, DynEventCacheStore, IntoEventCacheStore, MemoryStore},
|
||||
};
|
||||
use matrix_sdk_test::{ALICE, event_factory::EventFactory};
|
||||
use ruma::room_id;
|
||||
use tempfile::tempdir;
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Operation {
|
||||
PushItemsBack(Vec<Event>),
|
||||
PushGapBack(Gap),
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "codspeed"))]
|
||||
const NUMBER_OF_EVENTS: &[u64] = &[10, 100, 1000, 10_000, 100_000];
|
||||
#[cfg(feature = "codspeed")]
|
||||
const NUMBER_OF_EVENTS: &[u64] = &[10, 100, 1000];
|
||||
|
||||
fn writing(c: &mut Criterion) {
|
||||
// Create a new asynchronous runtime.
|
||||
let runtime = Builder::new_multi_thread()
|
||||
.enable_time()
|
||||
.enable_io()
|
||||
.build()
|
||||
.expect("Failed to create an asynchronous runtime");
|
||||
|
||||
let room_id = room_id!("!fabricandofitfaber:bar.baz");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let event_factory = EventFactory::new().room(room_id).sender(&ALICE);
|
||||
|
||||
let mut group = c.benchmark_group("Linked chunk writing");
|
||||
group.sample_size(10).measurement_time(Duration::from_secs(30));
|
||||
|
||||
for &number_of_events in NUMBER_OF_EVENTS {
|
||||
let sqlite_temp_dir = tempdir().unwrap();
|
||||
|
||||
// Declare new stores for this set of events.
|
||||
let stores: [(&str, Option<Arc<DynEventCacheStore>>); 3] = [
|
||||
("none", None),
|
||||
("memory store", Some(MemoryStore::default().into_event_cache_store())),
|
||||
(
|
||||
"sqlite store",
|
||||
runtime.block_on(async {
|
||||
Some(
|
||||
SqliteEventCacheStore::open(sqlite_temp_dir.path().join("bench"), None)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_event_cache_store(),
|
||||
)
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
for (store_name, store) in stores {
|
||||
// Create the operations we want to bench.
|
||||
let mut operations = Vec::new();
|
||||
|
||||
{
|
||||
let mut events = (0..number_of_events)
|
||||
.map(|nth| event_factory.text_msg(format!("foo {nth}")).into_event())
|
||||
.peekable();
|
||||
|
||||
let mut gap_nth = 0;
|
||||
|
||||
while events.peek().is_some() {
|
||||
{
|
||||
let events_to_push_back = events.by_ref().take(80).collect::<Vec<_>>();
|
||||
|
||||
if events_to_push_back.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
operations.push(Operation::PushItemsBack(events_to_push_back));
|
||||
}
|
||||
|
||||
{
|
||||
operations
|
||||
.push(Operation::PushGapBack(Gap { token: format!("gap{gap_nth}") }));
|
||||
gap_nth += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Define the throughput.
|
||||
group.throughput(Throughput::Elements(number_of_events));
|
||||
|
||||
// Get a bencher.
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new(format!("Linked chunk writing [{store_name}]"), number_of_events),
|
||||
&operations,
|
||||
|bencher, operations| {
|
||||
// Bench the routine.
|
||||
bencher.to_async(&runtime).iter_batched(
|
||||
|| operations.clone(),
|
||||
|operations| async {
|
||||
// The routine to bench!
|
||||
|
||||
let mut linked_chunk = LinkedChunk::<DEFAULT_CHUNK_CAPACITY, Event, Gap>::new_with_update_history();
|
||||
|
||||
for operation in operations {
|
||||
match operation {
|
||||
Operation::PushItemsBack(events) => linked_chunk.push_items_back(events),
|
||||
Operation::PushGapBack(gap) => linked_chunk.push_gap_back(gap),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(store) = &store {
|
||||
let updates = linked_chunk.updates().unwrap().take();
|
||||
store.handle_linked_chunk_updates(linked_chunk_id, updates).await.unwrap();
|
||||
// Empty the store.
|
||||
store.handle_linked_chunk_updates(linked_chunk_id, vec![Update::Clear]).await.unwrap();
|
||||
}
|
||||
|
||||
},
|
||||
BatchSize::SmallInput
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(store);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.finish()
|
||||
}
|
||||
|
||||
fn reading(c: &mut Criterion) {
|
||||
// Create a new asynchronous runtime.
|
||||
let runtime = Builder::new_multi_thread()
|
||||
.enable_time()
|
||||
.enable_io()
|
||||
.build()
|
||||
.expect("Failed to create an asynchronous runtime");
|
||||
|
||||
let room_id = room_id!("!fabricandofitfaber:bar.baz");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let event_factory = EventFactory::new().room(room_id).sender(&ALICE);
|
||||
|
||||
let mut group = c.benchmark_group("Linked chunk reading");
|
||||
group.sample_size(10);
|
||||
|
||||
for &num_events in NUMBER_OF_EVENTS {
|
||||
let sqlite_temp_dir = tempdir().unwrap();
|
||||
|
||||
// Declare new stores for this set of events.
|
||||
let stores: [(&str, Arc<DynEventCacheStore>); 2] = [
|
||||
("memory store", MemoryStore::default().into_event_cache_store()),
|
||||
(
|
||||
"sqlite store",
|
||||
runtime.block_on(async {
|
||||
SqliteEventCacheStore::open(sqlite_temp_dir.path().join("bench"), None)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_event_cache_store()
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
for (store_name, store) in stores {
|
||||
// Store some events and gap chunks in the store.
|
||||
{
|
||||
let mut events = (0..num_events)
|
||||
.map(|nth| event_factory.text_msg(format!("foo {nth}")).into_event())
|
||||
.peekable();
|
||||
|
||||
let mut lc =
|
||||
LinkedChunk::<DEFAULT_CHUNK_CAPACITY, Event, Gap>::new_with_update_history();
|
||||
let mut num_gaps = 0;
|
||||
|
||||
while events.peek().is_some() {
|
||||
let events_chunk = events.by_ref().take(80).collect::<Vec<_>>();
|
||||
|
||||
if events_chunk.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
lc.push_items_back(events_chunk);
|
||||
lc.push_gap_back(Gap { token: format!("gap{num_gaps}") });
|
||||
|
||||
num_gaps += 1;
|
||||
}
|
||||
|
||||
// Now persist the updates to recreate this full linked chunk.
|
||||
let updates = lc.updates().unwrap().take();
|
||||
runtime
|
||||
.block_on(store.handle_linked_chunk_updates(linked_chunk_id, updates))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Define the throughput.
|
||||
group.throughput(Throughput::Elements(num_events));
|
||||
|
||||
// Bench the lazy loader.
|
||||
group.bench_function(
|
||||
BenchmarkId::new(format!("Linked chunk lazy loader[{store_name}]"), num_events),
|
||||
|bencher| {
|
||||
// Bench the routine.
|
||||
bencher.to_async(&runtime).iter(|| async {
|
||||
// Load the last chunk first,
|
||||
let (last_chunk, chunk_id_gen) =
|
||||
store.load_last_chunk(linked_chunk_id).await.unwrap();
|
||||
|
||||
let mut lc =
|
||||
lazy_loader::from_last_chunk::<128, _, _>(last_chunk, chunk_id_gen)
|
||||
.expect("no error when reconstructing the linked chunk")
|
||||
.expect("there is a linked chunk in the store");
|
||||
|
||||
// Then load until the start of the linked chunk.
|
||||
let mut cur_chunk_id = lc.chunks().next().unwrap().identifier();
|
||||
while let Some(prev) =
|
||||
store.load_previous_chunk(linked_chunk_id, cur_chunk_id).await.unwrap()
|
||||
{
|
||||
cur_chunk_id = prev.identifier;
|
||||
lazy_loader::insert_new_first_chunk(&mut lc, prev)
|
||||
.expect("no error when linking the previous lazy-loaded chunk");
|
||||
}
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
// Bench the metadata loader.
|
||||
group.bench_function(
|
||||
BenchmarkId::new(format!("Linked chunk metadata loader[{store_name}]"), num_events),
|
||||
|bencher| {
|
||||
// Bench the routine.
|
||||
bencher.to_async(&runtime).iter(|| async {
|
||||
let _metadata = store
|
||||
.load_all_chunks_metadata(linked_chunk_id)
|
||||
.await
|
||||
.expect("metadata must load");
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(store);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.finish()
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = event_cache;
|
||||
config = Criterion::default();
|
||||
targets = writing, reading,
|
||||
}
|
||||
|
||||
criterion_main!(event_cache);
|
||||
@@ -1,207 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use matrix_sdk::{
|
||||
cross_process_lock::CrossProcessLockConfig, store::RoomLoadSettings,
|
||||
test_utils::mocks::MatrixMockServer,
|
||||
};
|
||||
use matrix_sdk_base::{
|
||||
BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore, ThreadingSupport,
|
||||
store::StoreConfig,
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteStateStore;
|
||||
use matrix_sdk_test::{JoinedRoomBuilder, base64_sha256_hash, event_factory::EventFactory};
|
||||
use matrix_sdk_ui::timeline::{TimelineBuilder, TimelineFocus};
|
||||
use ruma::{
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId,
|
||||
api::client::membership::get_member_events,
|
||||
events::room::member::{MembershipState, RoomMemberEvent},
|
||||
mxc_uri, owned_device_id, owned_room_id, owned_user_id,
|
||||
serde::Raw,
|
||||
};
|
||||
use tokio::runtime::Builder;
|
||||
use wiremock::{Request, ResponseTemplate};
|
||||
|
||||
pub fn receive_all_members_benchmark(c: &mut Criterion) {
|
||||
const MEMBERS_IN_ROOM: usize = 100000;
|
||||
|
||||
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
|
||||
let room_id = owned_room_id!("!homohominilupusest:example.com");
|
||||
|
||||
let f = EventFactory::new().room(&room_id);
|
||||
let mut member_events: Vec<Raw<RoomMemberEvent>> = Vec::with_capacity(MEMBERS_IN_ROOM);
|
||||
for i in 0..MEMBERS_IN_ROOM {
|
||||
let user_id = OwnedUserId::try_from(format!("@user_{i}:matrix.org")).unwrap();
|
||||
let event = f
|
||||
.member(&user_id)
|
||||
.membership(MembershipState::Join)
|
||||
.avatar_url(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
|
||||
.display_name("Alice Margatroid")
|
||||
.reason("Looking for support")
|
||||
.into_raw();
|
||||
member_events.push(event);
|
||||
}
|
||||
|
||||
// Create a fake list of changes, and a session to recover from.
|
||||
let mut changes = StateChanges::default();
|
||||
changes.add_room(RoomInfo::new(&room_id, RoomState::Joined));
|
||||
for member_event in member_events.iter() {
|
||||
let event = member_event.clone().cast();
|
||||
changes.add_state_event(&room_id, event.deserialize().unwrap(), event);
|
||||
}
|
||||
|
||||
// Sqlite
|
||||
let sqlite_dir = tempfile::tempdir().unwrap();
|
||||
let sqlite_store = runtime.block_on(SqliteStateStore::open(sqlite_dir.path(), None)).unwrap();
|
||||
runtime
|
||||
.block_on(sqlite_store.save_changes(&changes))
|
||||
.expect("initial filling of sqlite failed");
|
||||
|
||||
let base_client = BaseClient::new(
|
||||
StoreConfig::new(CrossProcessLockConfig::multi_process(
|
||||
"cross-process-store-locks-holder-name",
|
||||
))
|
||||
.state_store(sqlite_store),
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
runtime
|
||||
.block_on(base_client.activate(
|
||||
SessionMeta {
|
||||
user_id: owned_user_id!("@somebody:example.com"),
|
||||
device_id: owned_device_id!("DEVICE_ID"),
|
||||
},
|
||||
RoomLoadSettings::default(),
|
||||
None,
|
||||
))
|
||||
.expect("Could not set session meta");
|
||||
|
||||
base_client.get_or_create_room(&room_id, RoomState::Joined);
|
||||
|
||||
let request = get_member_events::v3::Request::new(room_id.clone());
|
||||
let response = get_member_events::v3::Response::new(member_events);
|
||||
|
||||
let count = MEMBERS_IN_ROOM;
|
||||
let name = format!("{count} members");
|
||||
let mut group = c.benchmark_group("Test");
|
||||
group.throughput(Throughput::Elements(count as u64));
|
||||
group.sample_size(50);
|
||||
|
||||
group.bench_function(BenchmarkId::new("Handle /members request [SQLite]", name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
base_client.receive_all_members(&room_id, &request, &response).await.unwrap();
|
||||
});
|
||||
});
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(base_client);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
const PINNED_EVENTS_COUNT: usize = 100;
|
||||
|
||||
let runtime = Builder::new_multi_thread().enable_all().build().expect("Can't create runtime");
|
||||
let room_id = owned_room_id!("!homohominilupusest:example.com");
|
||||
let sender_id = owned_user_id!("@sender:example.com");
|
||||
|
||||
let f = EventFactory::new().room(&room_id).sender(&sender_id);
|
||||
|
||||
let mut joined_room_builder =
|
||||
JoinedRoomBuilder::new(&room_id).add_state_event(f.room_encryption());
|
||||
|
||||
let pinned_event_ids: Vec<OwnedEventId> = (0..PINNED_EVENTS_COUNT)
|
||||
.map(|i| {
|
||||
EventId::new_v2_or_v3(&base64_sha256_hash(format!("${i}").as_bytes()))
|
||||
.expect("Invalid event id")
|
||||
})
|
||||
.collect();
|
||||
joined_room_builder =
|
||||
joined_room_builder.add_state_bulk(vec![f.room_pinned_events(pinned_event_ids).into()]);
|
||||
|
||||
let (server, client, room) = runtime.block_on(async move {
|
||||
let server = MatrixMockServer::new().await;
|
||||
let client = server.client_builder().build().await;
|
||||
|
||||
let room = server.sync_room(&client, joined_room_builder).await;
|
||||
|
||||
server
|
||||
.mock_room_event()
|
||||
.respond_with(move |r: &Request| {
|
||||
let segments: Vec<&str> = r.url.path_segments().expect("Invalid path").collect();
|
||||
let event_id_str = segments[6];
|
||||
let event_id = EventId::parse(event_id_str).expect("Invalid event id in response");
|
||||
let event = f
|
||||
.text_msg(format!("Message {event_id_str}"))
|
||||
.event_id(&event_id)
|
||||
.server_ts(MilliSecondsSinceUnixEpoch::now())
|
||||
.into_raw_sync();
|
||||
ResponseTemplate::new(200)
|
||||
.set_delay(Duration::from_millis(50))
|
||||
.set_body_json(event.json())
|
||||
})
|
||||
.mount()
|
||||
.await;
|
||||
|
||||
client.event_cache().subscribe().unwrap();
|
||||
|
||||
(server, client, room)
|
||||
});
|
||||
|
||||
let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
|
||||
assert!(!pinned_event_ids.is_empty());
|
||||
assert_eq!(pinned_event_ids.len(), PINNED_EVENTS_COUNT);
|
||||
|
||||
let count = PINNED_EVENTS_COUNT;
|
||||
let name = format!("{count} pinned events");
|
||||
let mut group = c.benchmark_group("Load pinned events");
|
||||
group.throughput(Throughput::Elements(count as u64));
|
||||
group.sample_size(10);
|
||||
|
||||
group.bench_function(BenchmarkId::new("Load pinned events [memory]", name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
|
||||
assert!(!pinned_event_ids.is_empty());
|
||||
assert_eq!(pinned_event_ids.len(), PINNED_EVENTS_COUNT);
|
||||
|
||||
// Reset cache so it always loads the events from the mocked endpoint
|
||||
client
|
||||
.event_cache_store()
|
||||
.lock()
|
||||
.await
|
||||
.unwrap()
|
||||
.as_clean()
|
||||
.unwrap()
|
||||
.clear_all_linked_chunks()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let timeline = TimelineBuilder::new(&room)
|
||||
.with_focus(TimelineFocus::PinnedEvents)
|
||||
.build()
|
||||
.await
|
||||
.expect("Could not create timeline");
|
||||
|
||||
let (items, _) = timeline.subscribe().await;
|
||||
assert_eq!(items.len(), PINNED_EVENTS_COUNT + 1);
|
||||
timeline.clear().await;
|
||||
});
|
||||
});
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(server);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = room;
|
||||
config = Criterion::default();
|
||||
targets = receive_all_members_benchmark, load_pinned_events_benchmark,
|
||||
}
|
||||
criterion_main!(room);
|
||||
@@ -1,101 +0,0 @@
|
||||
use assert_matches::assert_matches;
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use futures_util::pin_mut;
|
||||
use matrix_sdk::{stream::StreamExt, test_utils::mocks::MatrixMockServer};
|
||||
use matrix_sdk_test::{JoinedRoomBuilder, base64_sha256_hash, event_factory::EventFactory};
|
||||
use matrix_sdk_ui::{
|
||||
RoomListService, eyeball_im::VectorDiff, room_list_service::filters::new_filter_non_left,
|
||||
};
|
||||
use rand::{distr::Uniform, prelude::Distribution};
|
||||
use ruma::{OwnedRoomId, RoomId, owned_user_id};
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
/// Benchmark the time it takes to create a room list.
|
||||
pub fn create(c: &mut Criterion) {
|
||||
const NUMBER_OF_ROOMS: usize = 1000;
|
||||
const NUMBER_OF_EVENTS_PER_ROOM: usize = 1000;
|
||||
|
||||
let runtime = Builder::new_multi_thread().enable_all().build().expect("Can't create runtime");
|
||||
|
||||
let (server, client) = runtime.block_on(async {
|
||||
let server = MatrixMockServer::new().await;
|
||||
let client = server.client_builder().build().await;
|
||||
client.event_cache().subscribe().unwrap();
|
||||
|
||||
(server, client)
|
||||
});
|
||||
|
||||
let sender_id = owned_user_id!("@mnt_io:matrix.org");
|
||||
let mut rand = rand::rng();
|
||||
let server_ts_range = Uniform::try_from(100..1000).unwrap();
|
||||
|
||||
for room_nth in 0..NUMBER_OF_ROOMS {
|
||||
// Synapse's room IDs for rooms v1 to v11 have an 18 characters localpart.
|
||||
let raw_room_id = format!("!arsgratiaartis{room_nth:04}:example.com");
|
||||
|
||||
let room_id = if room_nth % 10 == 9 {
|
||||
// Make 1 in 10 rooms use a room v12 ID, which is a base64 hash similar to an
|
||||
// event ID.
|
||||
RoomId::new_v2(&base64_sha256_hash(raw_room_id.as_bytes())).unwrap()
|
||||
} else {
|
||||
OwnedRoomId::try_from(raw_room_id).unwrap()
|
||||
};
|
||||
|
||||
let first_server_ts = server_ts_range.sample(&mut rand);
|
||||
let event_factory = EventFactory::new().room(&room_id).server_ts(first_server_ts);
|
||||
|
||||
let events = (0..NUMBER_OF_EVENTS_PER_ROOM)
|
||||
.map(|event_nth| {
|
||||
event_factory
|
||||
.text_msg(format!("a {room_nth}_{event_nth}"))
|
||||
.sender(&sender_id)
|
||||
.into_raw_sync()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let _room = runtime.block_on(async {
|
||||
server
|
||||
.sync_room(&client, JoinedRoomBuilder::new(&room_id).add_timeline_bulk(events))
|
||||
.await
|
||||
});
|
||||
}
|
||||
|
||||
let mut group = c.benchmark_group("RoomList");
|
||||
group.throughput(Throughput::Elements(NUMBER_OF_ROOMS.try_into().unwrap()));
|
||||
|
||||
group.bench_function(
|
||||
BenchmarkId::new(
|
||||
"Create",
|
||||
format!("{NUMBER_OF_ROOMS} rooms × {NUMBER_OF_EVENTS_PER_ROOM} events"),
|
||||
),
|
||||
|bencher| {
|
||||
bencher.to_async(&runtime).iter(|| async {
|
||||
let room_list_service = RoomListService::new(client.clone())
|
||||
.await
|
||||
.expect("build the room list service");
|
||||
let room_list = room_list_service.all_rooms().await.expect("fetch `all_rooms`");
|
||||
let (entries_stream, entries_controller) =
|
||||
room_list.entries_with_dynamic_adapters(20);
|
||||
|
||||
// Setting the filter will trigger the entries stream computation.
|
||||
entries_controller.set_filter(Box::new(new_filter_non_left()));
|
||||
|
||||
pin_mut!(entries_stream);
|
||||
let update = entries_stream.next().await.expect("receiving the reset update");
|
||||
assert_eq!(update.len(), 1);
|
||||
assert_matches!(&update[0], VectorDiff::Reset { values } => {
|
||||
assert_eq!(values.len(), 20);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = room_list;
|
||||
config = Criterion::default();
|
||||
targets = create
|
||||
}
|
||||
criterion_main!(room_list);
|
||||
@@ -1,17 +1,29 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::{
|
||||
Client, RoomInfo, RoomState, SessionTokens, StateChanges,
|
||||
authentication::matrix::MatrixSession, config::StoreConfig,
|
||||
cross_process_lock::CrossProcessLockConfig,
|
||||
config::StoreConfig,
|
||||
matrix_auth::{MatrixSession, MatrixSessionTokens},
|
||||
Client, RoomInfo, RoomState, StateChanges,
|
||||
};
|
||||
use matrix_sdk_base::{SessionMeta, StateStore as _, store::MemoryStore};
|
||||
use matrix_sdk_base::{store::MemoryStore, SessionMeta, StateStore as _};
|
||||
use matrix_sdk_sqlite::SqliteStateStore;
|
||||
use matrix_sdk_test::base64_sha256_hash;
|
||||
use ruma::{OwnedRoomId, RoomId, owned_device_id, owned_user_id};
|
||||
use ruma::{device_id, user_id, RoomId};
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
let criterion = Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
100,
|
||||
pprof::criterion::Output::Flamegraph(None),
|
||||
));
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let criterion = Criterion::default();
|
||||
|
||||
criterion
|
||||
}
|
||||
|
||||
/// Number of joined rooms in the benchmark.
|
||||
const NUM_JOINED_ROOMS: usize = 10000;
|
||||
|
||||
@@ -19,45 +31,27 @@ const NUM_JOINED_ROOMS: usize = 10000;
|
||||
const NUM_STRIPPED_JOINED_ROOMS: usize = 10000;
|
||||
|
||||
pub fn restore_session(c: &mut Criterion) {
|
||||
let runtime = Builder::new_multi_thread().enable_time().build().expect("Can't create runtime");
|
||||
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
|
||||
|
||||
// Create a fake list of changes, and a session to recover from.
|
||||
let mut changes = StateChanges::default();
|
||||
|
||||
for i in 0..NUM_JOINED_ROOMS {
|
||||
// Synapse's room IDs for rooms v1 to v11 have an 18 characters localpart.
|
||||
let raw_room_id = format!("!joinedchamber{i:05}:example.com");
|
||||
|
||||
let room_id = if i % 20 == 19 {
|
||||
// Make 1 in 20 rooms use a room v12 ID, which is a base64 hash similar to an
|
||||
// event ID.
|
||||
RoomId::new_v2(&base64_sha256_hash(raw_room_id.as_bytes())).unwrap()
|
||||
} else {
|
||||
OwnedRoomId::try_from(raw_room_id).unwrap()
|
||||
};
|
||||
let room_id = RoomId::parse(format!("!room{i}:example.com")).unwrap().to_owned();
|
||||
changes.add_room(RoomInfo::new(&room_id, RoomState::Joined));
|
||||
}
|
||||
|
||||
for i in 0..NUM_STRIPPED_JOINED_ROOMS {
|
||||
// Synapse's room IDs for rooms v1 to v11 have an 18 characters localpart.
|
||||
let raw_room_id = format!("!strippedlodge{i:05}:example.com");
|
||||
|
||||
let room_id = if i % 20 == 19 {
|
||||
// Make 1 in 20 rooms use a room v12 ID, which is a base64 hash similar to an
|
||||
// event ID.
|
||||
RoomId::new_v2(&base64_sha256_hash(raw_room_id.as_bytes())).unwrap()
|
||||
} else {
|
||||
OwnedRoomId::try_from(raw_room_id).unwrap()
|
||||
};
|
||||
let room_id = RoomId::parse(format!("!strippedroom{i}:example.com")).unwrap().to_owned();
|
||||
changes.add_room(RoomInfo::new(&room_id, RoomState::Invited));
|
||||
}
|
||||
|
||||
let session = MatrixSession {
|
||||
meta: SessionMeta {
|
||||
user_id: owned_user_id!("@somebody:example.com"),
|
||||
device_id: owned_device_id!("DEVICE_ID"),
|
||||
user_id: user_id!("@somebody:example.com").to_owned(),
|
||||
device_id: device_id!("DEVICE_ID").to_owned(),
|
||||
},
|
||||
tokens: SessionTokens { access_token: "OHEY".to_owned(), refresh_token: None },
|
||||
tokens: MatrixSessionTokens { access_token: "OHEY".to_owned(), refresh_token: None },
|
||||
};
|
||||
|
||||
// Start the benchmark.
|
||||
@@ -65,20 +59,17 @@ pub fn restore_session(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("Client reload");
|
||||
group.throughput(Throughput::Elements(100));
|
||||
|
||||
const NAME: &str = "restore a session";
|
||||
|
||||
// Memory
|
||||
let mem_store = Arc::new(MemoryStore::new());
|
||||
runtime.block_on(mem_store.save_changes(&changes)).expect("initial filling of mem failed");
|
||||
|
||||
group.bench_with_input("Restore session [memory store]", &mem_store, |b, store| {
|
||||
group.bench_with_input(BenchmarkId::new("memory store", NAME), &mem_store, |b, store| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let client = Client::builder()
|
||||
.homeserver_url("https://matrix.example.com")
|
||||
.store_config(
|
||||
StoreConfig::new(CrossProcessLockConfig::multi_process(
|
||||
"cross-process-store-locks-holder-name",
|
||||
))
|
||||
.state_store(store.clone()),
|
||||
)
|
||||
.store_config(StoreConfig::new().state_store(store.clone()))
|
||||
.build()
|
||||
.await
|
||||
.expect("Can't build client");
|
||||
@@ -99,18 +90,13 @@ pub fn restore_session(c: &mut Criterion) {
|
||||
.expect("initial filling of sqlite failed");
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("Restore session [SQLite]", encrypted_suffix),
|
||||
BenchmarkId::new(format!("sqlite store {encrypted_suffix}"), NAME),
|
||||
&sqlite_store,
|
||||
|b, store| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let client = Client::builder()
|
||||
.homeserver_url("https://matrix.example.com")
|
||||
.store_config(
|
||||
StoreConfig::new(CrossProcessLockConfig::multi_process(
|
||||
"cross-process-store-locks-holder-name",
|
||||
))
|
||||
.state_store(store.clone()),
|
||||
)
|
||||
.store_config(StoreConfig::new().state_store(store.clone()))
|
||||
.build()
|
||||
.await
|
||||
.expect("Can't build client");
|
||||
@@ -133,7 +119,7 @@ pub fn restore_session(c: &mut Criterion) {
|
||||
|
||||
criterion_group! {
|
||||
name = benches;
|
||||
config = Criterion::default();
|
||||
config = criterion();
|
||||
targets = restore_session
|
||||
}
|
||||
criterion_main!(benches);
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use matrix_sdk::test_utils::mocks::MatrixMockServer;
|
||||
use matrix_sdk_test::{JoinedRoomBuilder, event_factory::EventFactory};
|
||||
use matrix_sdk_ui::timeline::{TimelineBuilder, TimelineReadReceiptTracking};
|
||||
use ruma::{
|
||||
OwnedEventId, events::room::message::RoomMessageEventContentWithoutRelation, owned_room_id,
|
||||
owned_user_id,
|
||||
};
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
/// Benchmark the time it takes to create a timeline (with read receipt
|
||||
/// support), when there are many initial events at rest in the event cache.
|
||||
///
|
||||
/// `NUM_EVENTS` is the number of events that will be stored initially in the
|
||||
/// event cache. It will be a mix of messages, reactions, edits and redactions,
|
||||
/// so there are some aggregations to take into account by the timeline as well.
|
||||
pub fn create_timeline_with_initial_events(c: &mut Criterion) {
|
||||
const NUM_EVENTS: usize = 10000;
|
||||
|
||||
let runtime = Builder::new_multi_thread().enable_all().build().expect("Can't create runtime");
|
||||
let room_id = owned_room_id!("!fortesfortunajuvat:example.com");
|
||||
|
||||
let sender_id = owned_user_id!("@sender:example.com");
|
||||
let other_sender_id = owned_user_id!("@other_sender:example.com");
|
||||
let another_sender_id = owned_user_id!("@another_sender:example.com");
|
||||
|
||||
let f = EventFactory::new().room(&room_id);
|
||||
|
||||
let mut events = Vec::new();
|
||||
for i in 0..NUM_EVENTS {
|
||||
let sender = match i % 3 {
|
||||
0 => &sender_id,
|
||||
1 => &other_sender_id,
|
||||
2 => &another_sender_id,
|
||||
_ => unreachable!("math genius over here"),
|
||||
};
|
||||
|
||||
let j = i % 10;
|
||||
if j < 6 {
|
||||
// Messages.
|
||||
events.push(f.text_msg(format!("Message {i}")).sender(sender).into_raw_sync());
|
||||
} else if j < 8 {
|
||||
// Reactions.
|
||||
let prev_event_id = events[i - 2]
|
||||
.get_field::<OwnedEventId>("event_id")
|
||||
.expect("invalid event ID")
|
||||
.expect("missing event ID");
|
||||
events.push(f.reaction(&prev_event_id, "👍").sender(sender).into_raw_sync());
|
||||
} else if j == 8 {
|
||||
// Edit.
|
||||
// Note: (i-3)%3 is the same as i%3 -> same sender!
|
||||
let prev_event_id = events[i - 3]
|
||||
.get_field::<OwnedEventId>("event_id")
|
||||
.expect("invalid event ID")
|
||||
.expect("missing event ID");
|
||||
events.push(
|
||||
f.text_msg(format!("* Message {}v2", i - 3))
|
||||
.edit(
|
||||
&prev_event_id,
|
||||
RoomMessageEventContentWithoutRelation::text_plain(format!(
|
||||
"Message {}v2",
|
||||
i - 3
|
||||
)),
|
||||
)
|
||||
.sender(sender)
|
||||
.into_raw_sync(),
|
||||
);
|
||||
} else if j == 9 {
|
||||
// Redaction.
|
||||
// Note: (i-6)%3 is the same as i%6 -> same sender!
|
||||
let prev_event_id = events[i - 6]
|
||||
.get_field::<OwnedEventId>("event_id")
|
||||
.expect("invalid event ID")
|
||||
.expect("missing event ID");
|
||||
events.push(f.redaction(&prev_event_id).sender(sender).into_raw_sync());
|
||||
}
|
||||
}
|
||||
|
||||
let builder = JoinedRoomBuilder::new(&room_id)
|
||||
.add_state_event(f.room_encryption().sender(&sender_id))
|
||||
.add_timeline_bulk(events);
|
||||
|
||||
let room = runtime.block_on(async move {
|
||||
let server = MatrixMockServer::new().await;
|
||||
let client = server.client_builder().build().await;
|
||||
|
||||
client.event_cache().subscribe().unwrap();
|
||||
|
||||
let room = server.sync_room(&client, builder).await;
|
||||
drop(server);
|
||||
|
||||
room
|
||||
});
|
||||
|
||||
let mut group = c.benchmark_group("Create a timeline");
|
||||
group.throughput(Throughput::Elements(NUM_EVENTS as _));
|
||||
group.sample_size(10);
|
||||
|
||||
group.bench_function(
|
||||
BenchmarkId::new("Create a timeline with initial events", format!("{NUM_EVENTS} events")),
|
||||
|b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let timeline = TimelineBuilder::new(&room)
|
||||
.track_read_marker_and_receipts(TimelineReadReceiptTracking::AllEvents)
|
||||
.build()
|
||||
.await
|
||||
.expect("Could not create timeline");
|
||||
|
||||
let (items, _) = timeline.subscribe().await;
|
||||
assert_eq!(items.len(), 20);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = room;
|
||||
config = Criterion::default();
|
||||
targets = create_timeline_with_initial_events
|
||||
}
|
||||
criterion_main!(room);
|
||||
+2
-2
@@ -11,7 +11,7 @@ maintained by the owners of the Matrix Rust SDK project.
|
||||
|
||||
There are also external bindings in other repositories:
|
||||
|
||||
* [`matrix-sdk-crypto-wasm`], JavaScript / WebAssembly bindings of the
|
||||
* [`matrix-sdk-crypto-js`], JavaScript bindings of the
|
||||
[`matrix-sdk-crypto`] crate,
|
||||
* [`matrix-sdk-crypto-nodejs`], Node.js bindings of the
|
||||
[`matrix-sdk-crypto`] crate
|
||||
@@ -22,7 +22,7 @@ There are also external bindings in other repositories:
|
||||
[`matrix-sdk-ffi`]: ./matrix-sdk-ffi
|
||||
[`matrix-sdk`]: ../crates/matrix-sdk
|
||||
|
||||
[`matrix-sdk-crypto-wasm`]: https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm
|
||||
[`matrix-sdk-crypto-js`]: https://github.com/matrix-org/matrix-rust-sdk-crypto-web
|
||||
[`matrix-sdk-crypto-nodejs`]: https://github.com/matrix-org/matrix-rust-sdk-crypto-nodejs
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// swift-tools-version:5.7
|
||||
// swift-tools-version:5.6
|
||||
|
||||
// A package manifest for local development. This file will be copied
|
||||
// into the root of the repo when generating an XCFramework.
|
||||
@@ -8,12 +8,11 @@ import PackageDescription
|
||||
let package = Package(
|
||||
name: "MatrixRustSDK",
|
||||
platforms: [
|
||||
.iOS(.v16),
|
||||
.iOS(.v15),
|
||||
.macOS(.v12)
|
||||
],
|
||||
products: [
|
||||
.library(name: "MatrixRustSDK",
|
||||
type: .dynamic,
|
||||
targets: ["MatrixRustSDK"]),
|
||||
],
|
||||
targets: [
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
Pod::Spec.new do |s|
|
||||
|
||||
s.name = "MatrixSDKCrypto"
|
||||
s.version = "0.4.1"
|
||||
s.version = "0.0.1"
|
||||
s.summary = "Uniffi based bindings for the Rust SDK crypto crate."
|
||||
s.homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
s.license = { :type => "Apache License, Version 2.0", :file => "LICENSE" }
|
||||
s.author = { "matrix.org" => "support@matrix.org" }
|
||||
|
||||
s.ios.deployment_target = "13.0"
|
||||
s.osx.deployment_target = "10.15"
|
||||
|
||||
s.ios.deployment_target = "11.0"
|
||||
s.osx.deployment_target = "10.10"
|
||||
|
||||
s.swift_versions = ['5.1', '5.2']
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
Pod::Spec.new do |s|
|
||||
|
||||
s.name = "MatrixSDKCrypto"
|
||||
s.version = "0.4.1" # Version is only incremented manually and locally before pushing to CocoaPods
|
||||
s.version = "0.0.1" # Version is only incremented manually and locally before pushing to CocoaPods
|
||||
s.summary = "Uniffi based bindings for the Rust SDK crypto crate."
|
||||
s.homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
s.license = { :type => "Apache License, Version 2.0", :file => "LICENSE" }
|
||||
s.author = { "matrix.org" => "support@matrix.org" }
|
||||
|
||||
s.ios.deployment_target = "13.0"
|
||||
s.osx.deployment_target = "10.15"
|
||||
s.ios.deployment_target = "11.0"
|
||||
s.osx.deployment_target = "10.10"
|
||||
|
||||
s.swift_versions = ['5.1', '5.2']
|
||||
s.swift_versions = ['5.0']
|
||||
|
||||
s.source = { :http => "https://github.com/matrix-org/matrix-rust-sdk/releases/download/matrix-sdk-crypto-ffi-#{s.version}/MatrixSDKCryptoFFI.zip" }
|
||||
s.vendored_frameworks = "MatrixSDKCryptoFFI.xcframework"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// swift-tools-version: 5.7
|
||||
// swift-tools-version: 5.6
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
@@ -6,7 +6,7 @@ import PackageDescription
|
||||
let package = Package(
|
||||
name: "MatrixRustSDK",
|
||||
platforms: [
|
||||
.iOS(.v16),
|
||||
.iOS(.v15),
|
||||
.macOS(.v12)
|
||||
],
|
||||
products: [
|
||||
|
||||
@@ -2,9 +2,9 @@ import XCTest
|
||||
@testable import MatrixRustSDK
|
||||
|
||||
final class ClientTests: XCTestCase {
|
||||
func testBuildingWithHomeserverURL() async {
|
||||
func testBuildingWithHomeserverURL() {
|
||||
do {
|
||||
_ = try await ClientBuilder()
|
||||
_ = try ClientBuilder()
|
||||
.homeserverUrl(url: "https://localhost:8008")
|
||||
.build()
|
||||
} catch {
|
||||
@@ -12,9 +12,9 @@ final class ClientTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testBuildingWithHomeserverURLAndUserAgent() async {
|
||||
func testBuildingWithHomeserverURLAndUserAgent() {
|
||||
do {
|
||||
_ = try await ClientBuilder()
|
||||
_ = try ClientBuilder()
|
||||
.homeserverUrl(url: "https://localhost:8008")
|
||||
.userAgent(userAgent: "golden-eye/007")
|
||||
.build()
|
||||
@@ -23,14 +23,24 @@ final class ClientTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testBuildingWithInvalidUsername() async {
|
||||
func testBuildingWithUsername() {
|
||||
do {
|
||||
_ = try await ClientBuilder()
|
||||
_ = try ClientBuilder()
|
||||
.username(username: "@test:matrix.org")
|
||||
.build()
|
||||
} catch {
|
||||
XCTFail("The client should build successfully when given a username.")
|
||||
}
|
||||
}
|
||||
|
||||
func testBuildingWithInvalidUsername() {
|
||||
do {
|
||||
_ = try ClientBuilder()
|
||||
.username(username: "@test:invalid")
|
||||
.build()
|
||||
|
||||
XCTFail("The client should not build when given an invalid username.")
|
||||
} catch ClientBuildError.ServerUnreachable(let message) {
|
||||
} catch ClientError.Generic(let message) {
|
||||
XCTAssertTrue(message.contains(".well-known"), "The client should fail to do the well-known lookup.")
|
||||
} catch {
|
||||
XCTFail("Not expecting any other kind of exception")
|
||||
|
||||
@@ -1,22 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eEu
|
||||
|
||||
helpFunction() {
|
||||
echo ""
|
||||
echo "Usage: $0 -only_ios"
|
||||
echo -e "\t-i Option to build only for iOS. Default will build for all targets."
|
||||
exit 1
|
||||
}
|
||||
|
||||
only_ios='false'
|
||||
|
||||
while getopts ':i' 'opt'; do
|
||||
case ${opt} in
|
||||
'i') only_ios='true' ;;
|
||||
?) helpFunction ;;
|
||||
esac
|
||||
done
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Path to the repo root
|
||||
@@ -38,63 +22,52 @@ TARGET_CRATE=matrix-sdk-crypto-ffi
|
||||
# Required by olm-sys crate
|
||||
export IOS_SDK_PATH=`xcrun --show-sdk-path --sdk iphoneos`
|
||||
|
||||
if ${only_ios}; then
|
||||
# iOS
|
||||
echo -e "Building only for iOS"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios"
|
||||
else
|
||||
# iOS
|
||||
echo -e "Building for iOS [1/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios"
|
||||
# iOS
|
||||
echo -e "Building for iOS [1/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios"
|
||||
|
||||
# MacOS
|
||||
echo -e "\nBuilding for macOS (Apple Silicon) [2/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-darwin"
|
||||
echo -e "\nBuilding for macOS (Intel) [3/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "x86_64-apple-darwin"
|
||||
# MacOS
|
||||
echo -e "\nBuilding for macOS (Apple Silicon) [2/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-darwin"
|
||||
echo -e "\nBuilding for macOS (Intel) [3/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "x86_64-apple-darwin"
|
||||
|
||||
# iOS Simulator
|
||||
echo -e "\nBuilding for iOS Simulator (Apple Silicon) [4/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios-sim"
|
||||
echo -e "\nBuilding for iOS Simulator (Intel) [5/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "x86_64-apple-ios"
|
||||
fi
|
||||
# iOS Simulator
|
||||
echo -e "\nBuilding for iOS Simulator (Apple Silicon) [4/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios-sim"
|
||||
echo -e "\nBuilding for iOS Simulator (Intel) [5/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "x86_64-apple-ios"
|
||||
|
||||
echo -e "\nCreating XCFramework"
|
||||
# Lipo together the libraries for the same platform
|
||||
|
||||
if ! ${only_ios}; then
|
||||
echo "Lipo together the libraries for the same platform"
|
||||
# MacOS
|
||||
lipo -create \
|
||||
"${TARGET_DIR}/x86_64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
"${TARGET_DIR}/aarch64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-output "${GENERATED_DIR}/macos/libmatrix_sdk_crypto_ffi.a"
|
||||
# MacOS
|
||||
lipo -create \
|
||||
"${TARGET_DIR}/x86_64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
"${TARGET_DIR}/aarch64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-output "${GENERATED_DIR}/macos/libmatrix_sdk_crypto_ffi.a"
|
||||
|
||||
# iOS Simulator
|
||||
lipo -create \
|
||||
"${TARGET_DIR}/x86_64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
"${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-output "${GENERATED_DIR}/simulator/libmatrix_sdk_crypto_ffi.a"
|
||||
fi
|
||||
# iOS Simulator
|
||||
lipo -create \
|
||||
"${TARGET_DIR}/x86_64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
"${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-output "${GENERATED_DIR}/simulator/libmatrix_sdk_crypto_ffi.a"
|
||||
|
||||
# Generate uniffi files
|
||||
cd ../matrix-sdk-crypto-ffi && cargo run --bin matrix_sdk_crypto_ffi generate \
|
||||
cargo uniffi-bindgen generate \
|
||||
--language swift \
|
||||
--library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
--out-dir ${GENERATED_DIR}
|
||||
--lib-file "${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
--config "${SRC_ROOT}/bindings/${TARGET_CRATE}/uniffi.toml" \
|
||||
--out-dir ${GENERATED_DIR} \
|
||||
"${SRC_ROOT}/bindings/${TARGET_CRATE}/src/olm.udl"
|
||||
|
||||
# Move headers to the right place
|
||||
HEADERS_DIR=${GENERATED_DIR}/headers
|
||||
mkdir -p ${HEADERS_DIR}
|
||||
mv ${GENERATED_DIR}/*.h ${HEADERS_DIR}
|
||||
|
||||
# Rename and merge the modulemap files into a single file to the right place
|
||||
for f in ${GENERATED_DIR}/*.modulemap
|
||||
do
|
||||
cat $f; echo;
|
||||
done > ${HEADERS_DIR}/module.modulemap
|
||||
rm ${GENERATED_DIR}/*.modulemap
|
||||
# Rename and move modulemap to the right place
|
||||
mv ${GENERATED_DIR}/*.modulemap ${HEADERS_DIR}/module.modulemap
|
||||
|
||||
# Move source files to the right place
|
||||
SWIFT_DIR="${GENERATED_DIR}/Sources"
|
||||
@@ -104,21 +77,15 @@ mv ${GENERATED_DIR}/*.swift ${SWIFT_DIR}
|
||||
# Build the xcframework
|
||||
|
||||
if [ -d "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework" ]; then rm -rf "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"; fi
|
||||
if ${only_ios}; then
|
||||
xcodebuild -create-xcframework \
|
||||
-library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-output "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"
|
||||
else
|
||||
xcodebuild -create-xcframework \
|
||||
-library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-library "${GENERATED_DIR}/macos/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-library "${GENERATED_DIR}/simulator/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-output "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"
|
||||
fi
|
||||
|
||||
xcodebuild -create-xcframework \
|
||||
-library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-library "${GENERATED_DIR}/macos/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-library "${GENERATED_DIR}/simulator/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-output "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"
|
||||
|
||||
# Cleanup
|
||||
if [ -d "${GENERATED_DIR}/macos" ]; then rm -rf "${GENERATED_DIR}/macos"; fi
|
||||
|
||||
@@ -2,77 +2,60 @@
|
||||
name = "matrix-sdk-crypto-ffi"
|
||||
version = "0.1.0"
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk>"]
|
||||
edition = "2024"
|
||||
rust-version.workspace = true
|
||||
edition = "2021"
|
||||
rust-version = { workspace = true }
|
||||
description = "Uniffi based bindings for the Rust SDK crypto crate"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
license = "Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "staticlib"]
|
||||
|
||||
[[bin]]
|
||||
name = "matrix_sdk_crypto_ffi"
|
||||
path = "uniffi-bindgen.rs"
|
||||
|
||||
[features]
|
||||
default = ["bundled-sqlite"]
|
||||
bundled-sqlite = ["matrix-sdk-sqlite/bundled"]
|
||||
|
||||
# Enable experimental support for encrypting state events; see
|
||||
# https://github.com/matrix-org/matrix-rust-sdk/issues/5397.
|
||||
experimental-encrypted-state-events = [
|
||||
"matrix-sdk-crypto/experimental-encrypted-state-events",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
futures-util.workspace = true
|
||||
hmac.workspace = true
|
||||
http.workspace = true
|
||||
matrix-sdk-common = { workspace = true, features = ["uniffi"] }
|
||||
matrix-sdk-ffi-macros.workspace = true
|
||||
pbkdf2.workspace = true
|
||||
rand.workspace = true
|
||||
ruma.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
anyhow = { workspace = true }
|
||||
futures-util = "0.3.28"
|
||||
hmac = "0.12.1"
|
||||
http = { workspace = true }
|
||||
matrix-sdk-common = { workspace = true }
|
||||
pbkdf2 = "0.12.2"
|
||||
rand = { workspace = true }
|
||||
ruma = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
# keep in sync with uniffi dependency in matrix-sdk-ffi, and uniffi_bindgen in ffi CI job
|
||||
uniffi = { workspace = true, features = ["cli"] }
|
||||
vodozemac.workspace = true
|
||||
uniffi = { workspace = true }
|
||||
vodozemac = { workspace = true }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
[dependencies.js_int]
|
||||
version = "0.2.2"
|
||||
default-features = false
|
||||
features = ["lax_deserialize"]
|
||||
|
||||
[dependencies.matrix-sdk-crypto]
|
||||
workspace = true
|
||||
features = ["qrcode", "automatic-room-key-forwarding", "uniffi"]
|
||||
features = ["qrcode", "automatic-room-key-forwarding"]
|
||||
|
||||
[dependencies.matrix-sdk-sqlite]
|
||||
workspace = true
|
||||
features = ["crypto-store"]
|
||||
|
||||
[dependencies.tokio]
|
||||
workspace = true
|
||||
version = "1.33.0"
|
||||
default_features = false
|
||||
features = ["rt-multi-thread"]
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { workspace = true, default-features = false, features = ["build"] }
|
||||
vergen-gitcl = { workspace = true, default-features = false, features = ["build"] }
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
vergen = { version = "8.2.5", features = ["build", "git", "gitcl"] }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches2.workspace = true
|
||||
tempfile.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
tempfile = "3.8.0"
|
||||
assert_matches2 = { workspace = true }
|
||||
|
||||
@@ -33,22 +33,15 @@ Rust supports many different [targets], you'll have to make sure to pick the
|
||||
right one for your device or emulator.
|
||||
|
||||
After this is done, we'll have to configure [Cargo] to use the correct linker
|
||||
for our target, by providing the Cargo setting of
|
||||
[target.<triple>.linker](https://doc.rust-lang.org/cargo/reference/config.html#targettriplelinker)
|
||||
with a value of the path to an appropriate linker in your NDK installation.
|
||||
|
||||
This may be set through an environment variable:
|
||||
|
||||
```
|
||||
$ export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="<path-to-ndk-installation>/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang"
|
||||
```
|
||||
|
||||
Alternatively, it may be set in the `.cargo/config.toml` file in the current directory,
|
||||
any parent directory, or your home directory:
|
||||
for our target. Cargo is configured using a TOML file that will be found in
|
||||
`%USERPROFILE%\.cargo\config.toml` on Windows or `$HOME/.cargo/config` on Unix
|
||||
platforms. More details and configuration options for Cargo can be found in the
|
||||
official docs over [here](https://doc.rust-lang.org/cargo/reference/config.html).
|
||||
|
||||
```
|
||||
[target.aarch64-linux-android]
|
||||
linker = "<path-to-ndk-installation>/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang"
|
||||
ar = "NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/ar"
|
||||
linker = "NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang"
|
||||
```
|
||||
|
||||
## Building
|
||||
@@ -58,13 +51,7 @@ we'll need to set the `ANDROID_NDK` environment variable to the location of our
|
||||
Android NDK installation.
|
||||
|
||||
```
|
||||
$ export ANDROID_NDK=$HOME/Android/Sdk/ndk/<some-installed-version>
|
||||
```
|
||||
|
||||
Also, include the NDK tools directory in your `PATH`:
|
||||
|
||||
```
|
||||
$ export PATH="$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH"
|
||||
$ export ANDROID_NDK=$HOME/Android/Sdk/ndk/22.0.7026061/
|
||||
```
|
||||
|
||||
### Building for a target
|
||||
@@ -75,16 +62,19 @@ The bindings can built for the `aarch64` target with:
|
||||
$ cargo build --target aarch64-linux-android
|
||||
```
|
||||
|
||||
After that, a dynamic library can be found in the `target/aarch64-linux-android/debug` directory,
|
||||
under the repository root directory.
|
||||
The library will be called `libmatrix_sdk_crypto_ffi.so` and needs to be renamed and
|
||||
After that, a dynamic library can be found in the `target/aarch64-linux-android/debug` directory.
|
||||
The library will be called `libmatrix_crypto.so` and needs to be renamed and
|
||||
copied into the `jniLibs` directory of your Android project, for Element Android:
|
||||
|
||||
```
|
||||
$ cp ../../target/aarch64-linux-android/debug/libmatrix_sdk_crypto_ffi.so \
|
||||
$ cp ../../target/aarch64-linux-android/debug/libmatrix_crypto.so \
|
||||
/home/example/matrix-sdk-android/src/main/jniLibs/aarch64/libuniffi_olm.so
|
||||
```
|
||||
|
||||
## Minimum Supported Rust Version (MSRV)
|
||||
|
||||
These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.62`.
|
||||
|
||||
## License
|
||||
|
||||
[Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
@@ -1,66 +1,39 @@
|
||||
use std::{
|
||||
env,
|
||||
error::Error,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
use std::{env, error::Error};
|
||||
|
||||
use vergen_gitcl::{Emitter, GitclBuilder};
|
||||
use vergen::EmitBuilder;
|
||||
|
||||
/// Adds a temporary workaround for an issue with the Rust compiler and Android
|
||||
/// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717.
|
||||
/// The workaround is based on: https://github.com/mozilla/application-services/pull/5442
|
||||
///
|
||||
/// IMPORTANT: if you modify this, make sure to modify
|
||||
/// [../matrix-sdk-ffi/build.rs] too!
|
||||
/// The workaround comes from: https://github.com/mozilla/application-services/pull/5442
|
||||
fn setup_x86_64_android_workaround() {
|
||||
let target_os = env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not set");
|
||||
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").expect("CARGO_CFG_TARGET_ARCH not set");
|
||||
if target_arch == "x86_64" && target_os == "android" {
|
||||
// Configure rust to statically link against the `libclang_rt.builtins` supplied
|
||||
// with clang.
|
||||
|
||||
// cargo-ndk sets CC_x86_64-linux-android to the path to `clang`, within the
|
||||
// Android NDK.
|
||||
let clang_path = PathBuf::from(
|
||||
env::var("CC_x86_64-linux-android").expect("CC_x86_64-linux-android not set"),
|
||||
let android_ndk_home = env::var("ANDROID_NDK_HOME").expect("ANDROID_NDK_HOME not set");
|
||||
let build_os = match env::consts::OS {
|
||||
"linux" => "linux",
|
||||
"macos" => "darwin",
|
||||
"windows" => "windows",
|
||||
_ => panic!(
|
||||
"Unsupported OS. You must use either Linux, MacOS or Windows to build the crate."
|
||||
),
|
||||
};
|
||||
const DEFAULT_CLANG_VERSION: &str = "14.0.7";
|
||||
let clang_version =
|
||||
env::var("NDK_CLANG_VERSION").unwrap_or_else(|_| DEFAULT_CLANG_VERSION.to_owned());
|
||||
let linux_x86_64_lib_dir = format!(
|
||||
"toolchains/llvm/prebuilt/{build_os}-x86_64/lib64/clang/{clang_version}/lib/linux/"
|
||||
);
|
||||
|
||||
// clang_path should now look something like
|
||||
// `.../sdk/ndk/28.0.12674087/toolchains/llvm/prebuilt/linux-x86_64/bin/clang`.
|
||||
// We strip `/bin/clang` from the end to get the toolchain path.
|
||||
let toolchain_path = clang_path
|
||||
.ancestors()
|
||||
.nth(2)
|
||||
.expect("could not find NDK toolchain path")
|
||||
.to_str()
|
||||
.expect("NDK toolchain path is not valid UTF-8");
|
||||
|
||||
let clang_version = get_clang_major_version(&clang_path);
|
||||
|
||||
println!("cargo:rustc-link-search={toolchain_path}/lib/clang/{clang_version}/lib/linux/");
|
||||
println!("cargo:rustc-link-search={android_ndk_home}/{linux_x86_64_lib_dir}");
|
||||
println!("cargo:rustc-link-lib=static=clang_rt.builtins-x86_64-android");
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the clang binary at `clang_path`, and return its major version number
|
||||
fn get_clang_major_version(clang_path: &Path) -> String {
|
||||
let clang_output =
|
||||
Command::new(clang_path).arg("-dumpversion").output().expect("failed to start clang");
|
||||
|
||||
if !clang_output.status.success() {
|
||||
panic!("failed to run clang: {}", String::from_utf8_lossy(&clang_output.stderr));
|
||||
}
|
||||
|
||||
let clang_version = String::from_utf8(clang_output.stdout).expect("clang output is not utf8");
|
||||
clang_version.split('.').next().expect("could not parse clang output").to_owned()
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
setup_x86_64_android_workaround();
|
||||
uniffi::generate_scaffolding("./src/olm.udl")?;
|
||||
|
||||
let git_config = GitclBuilder::default().sha(true).describe(true, false, None).build()?;
|
||||
Emitter::default().add_instructions(&git_config)?.emit()?;
|
||||
EmitBuilder::builder().git_sha(true).git_describe(true, false, None).emit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ use std::{collections::HashMap, iter, ops::DerefMut, sync::Arc};
|
||||
use hmac::Hmac;
|
||||
use matrix_sdk_crypto::{
|
||||
backups::DecryptionError,
|
||||
store::{CryptoStoreError as InnerStoreError, types::BackupDecryptionKey},
|
||||
store::{BackupDecryptionKey, CryptoStoreError as InnerStoreError},
|
||||
};
|
||||
use pbkdf2::pbkdf2;
|
||||
use rand::{RngExt, distr::Alphanumeric, rng};
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
use sha2::Sha512;
|
||||
use thiserror::Error;
|
||||
use zeroize::Zeroize;
|
||||
@@ -69,13 +69,17 @@ impl BackupRecoveryKey {
|
||||
const PBKDF_ROUNDS: i32 = 500_000;
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
impl BackupRecoveryKey {
|
||||
/// Create a new random [`BackupRecoveryKey`].
|
||||
#[allow(clippy::new_without_default)]
|
||||
#[uniffi::constructor]
|
||||
pub fn new() -> Arc<Self> {
|
||||
Arc::new(Self { inner: BackupDecryptionKey::new(), passphrase_info: None })
|
||||
Arc::new(Self {
|
||||
inner: BackupDecryptionKey::new()
|
||||
.expect("Can't gather enough randomness to create a recovery key"),
|
||||
passphrase_info: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Try to create a [`BackupRecoveryKey`] from a base 64 encoded string.
|
||||
@@ -93,7 +97,7 @@ impl BackupRecoveryKey {
|
||||
/// Create a new [`BackupRecoveryKey`] from the given passphrase.
|
||||
#[uniffi::constructor]
|
||||
pub fn new_from_passphrase(passphrase: String) -> Arc<Self> {
|
||||
let mut rng = rng();
|
||||
let mut rng = thread_rng();
|
||||
let salt: String = iter::repeat(())
|
||||
.map(|()| rng.sample(Alphanumeric))
|
||||
.map(char::from)
|
||||
|
||||
@@ -1,26 +1,19 @@
|
||||
use std::{mem::ManuallyDrop, sync::Arc};
|
||||
|
||||
use matrix_sdk_common::executor::Handle;
|
||||
use matrix_sdk_crypto::{
|
||||
DecryptionSettings,
|
||||
dehydrated_devices::{
|
||||
DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices,
|
||||
RehydratedDevice as InnerRehydratedDevice,
|
||||
},
|
||||
store::types::DehydratedDeviceKey as InnerDehydratedDeviceKey,
|
||||
use matrix_sdk_crypto::dehydrated_devices::{
|
||||
DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices,
|
||||
RehydratedDevice as InnerRehydratedDevice,
|
||||
};
|
||||
use ruma::{OwnedDeviceId, api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw};
|
||||
use ruma::{api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw, OwnedDeviceId};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{CryptoStoreError, DehydratedDeviceKey};
|
||||
use tokio::runtime::Handle;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum DehydrationError {
|
||||
#[error(transparent)]
|
||||
Pickle(#[from] matrix_sdk_crypto::vodozemac::DehydratedDeviceError),
|
||||
#[error(transparent)]
|
||||
LegacyPickle(#[from] matrix_sdk_crypto::vodozemac::LibolmPickleError),
|
||||
Pickle(#[from] matrix_sdk_crypto::vodozemac::LibolmPickleError),
|
||||
#[error(transparent)]
|
||||
MissingSigningKey(#[from] matrix_sdk_crypto::SignatureError),
|
||||
#[error(transparent)]
|
||||
@@ -36,16 +29,10 @@ impl From<matrix_sdk_crypto::dehydrated_devices::DehydrationError> for Dehydrati
|
||||
match value {
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::Json(e) => Self::Json(e),
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::Pickle(e) => Self::Pickle(e),
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::LegacyPickle(e) => {
|
||||
Self::LegacyPickle(e)
|
||||
}
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::MissingSigningKey(e) => {
|
||||
Self::MissingSigningKey(e)
|
||||
}
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::Store(e) => Self::Store(e),
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::PickleKeyLength(l) => {
|
||||
Self::PickleKeyLength(l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,7 +53,7 @@ impl Drop for DehydratedDevices {
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
impl DehydratedDevices {
|
||||
pub fn create(&self) -> Result<Arc<DehydratedDevice>, DehydrationError> {
|
||||
let inner = self.runtime.block_on(self.inner.create())?;
|
||||
@@ -79,14 +66,14 @@ impl DehydratedDevices {
|
||||
|
||||
pub fn rehydrate(
|
||||
&self,
|
||||
pickle_key: &DehydratedDeviceKey,
|
||||
pickle_key: Vec<u8>,
|
||||
device_id: String,
|
||||
device_data: String,
|
||||
) -> Result<Arc<RehydratedDevice>, DehydrationError> {
|
||||
let device_data: Raw<_> = serde_json::from_str(&device_data)?;
|
||||
let device_id: OwnedDeviceId = device_id.into();
|
||||
|
||||
let key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?;
|
||||
let mut key = get_pickle_key(&pickle_key)?;
|
||||
|
||||
let ret = RehydratedDevice {
|
||||
runtime: self.runtime.to_owned(),
|
||||
@@ -98,41 +85,10 @@ impl DehydratedDevices {
|
||||
}
|
||||
.into();
|
||||
|
||||
key.zeroize();
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Get the cached dehydrated device pickle key if any.
|
||||
///
|
||||
/// None if the key was not previously cached (via
|
||||
/// [`Self::save_dehydrated_device_pickle_key`]).
|
||||
///
|
||||
/// Should be used to periodically rotate the dehydrated device to avoid
|
||||
/// OTK exhaustion and accumulation of to_device messages.
|
||||
pub fn get_dehydrated_device_key(
|
||||
&self,
|
||||
) -> Result<Option<crate::DehydratedDeviceKey>, CryptoStoreError> {
|
||||
Ok(self
|
||||
.runtime
|
||||
.block_on(self.inner.get_dehydrated_device_pickle_key())?
|
||||
.map(crate::DehydratedDeviceKey::from))
|
||||
}
|
||||
|
||||
/// Store the dehydrated device pickle key in the crypto store.
|
||||
///
|
||||
/// This is useful if the client wants to periodically rotate dehydrated
|
||||
/// devices to avoid OTK exhaustion and accumulated to_device problems.
|
||||
pub fn save_dehydrated_device_key(
|
||||
&self,
|
||||
pickle_key: &crate::DehydratedDeviceKey,
|
||||
) -> Result<(), CryptoStoreError> {
|
||||
let pickle_key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?;
|
||||
Ok(self.runtime.block_on(self.inner.save_dehydrated_device_pickle_key(&pickle_key))?)
|
||||
}
|
||||
|
||||
/// Deletes the previously stored dehydrated device pickle key.
|
||||
pub fn delete_dehydrated_device_key(&self) -> Result<(), CryptoStoreError> {
|
||||
Ok(self.runtime.block_on(self.inner.delete_dehydrated_device_pickle_key())?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
@@ -151,15 +107,11 @@ impl Drop for RehydratedDevice {
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
impl RehydratedDevice {
|
||||
pub fn receive_events(
|
||||
&self,
|
||||
events: String,
|
||||
decryption_settings: &DecryptionSettings,
|
||||
) -> Result<(), crate::CryptoStoreError> {
|
||||
pub fn receive_events(&self, events: String) -> Result<(), crate::CryptoStoreError> {
|
||||
let events: Vec<Raw<AnyToDeviceEvent>> = serde_json::from_str(&events)?;
|
||||
self.runtime.block_on(self.inner.receive_events(events, decryption_settings))?;
|
||||
self.runtime.block_on(self.inner.receive_events(events))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -181,18 +133,20 @@ impl Drop for DehydratedDevice {
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
impl DehydratedDevice {
|
||||
pub fn keys_for_upload(
|
||||
&self,
|
||||
device_display_name: String,
|
||||
pickle_key: &DehydratedDeviceKey,
|
||||
pickle_key: Vec<u8>,
|
||||
) -> Result<UploadDehydratedDeviceRequest, DehydrationError> {
|
||||
let key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?;
|
||||
let mut key = get_pickle_key(&pickle_key)?;
|
||||
|
||||
let request =
|
||||
self.runtime.block_on(self.inner.keys_for_upload(device_display_name, &key))?;
|
||||
|
||||
key.zeroize();
|
||||
|
||||
Ok(request.into())
|
||||
}
|
||||
}
|
||||
@@ -223,34 +177,15 @@ impl From<dehydrated_device::put_dehydrated_device::unstable::Request>
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{DehydratedDeviceKey, dehydrated_devices::DehydrationError};
|
||||
fn get_pickle_key(pickle_key: &[u8]) -> Result<Box<[u8; 32]>, DehydrationError> {
|
||||
let pickle_key_length = pickle_key.len();
|
||||
|
||||
#[test]
|
||||
fn test_creating_dehydrated_key() {
|
||||
let dehydrated_device_key = DehydratedDeviceKey::new();
|
||||
let base_64 = dehydrated_device_key.to_base64();
|
||||
let inner_bytes = dehydrated_device_key.inner;
|
||||
if pickle_key_length == 32 {
|
||||
let mut key = Box::new([0u8; 32]);
|
||||
key.copy_from_slice(pickle_key);
|
||||
|
||||
let copy = DehydratedDeviceKey::from_slice(&inner_bytes).unwrap();
|
||||
|
||||
assert_eq!(base_64, copy.to_base64());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_creating_dehydrated_key_failure() {
|
||||
let bytes = [0u8; 24];
|
||||
|
||||
let pickle_key = DehydratedDeviceKey::from_slice(&bytes);
|
||||
|
||||
assert!(pickle_key.is_err());
|
||||
|
||||
match pickle_key {
|
||||
Err(DehydrationError::PickleKeyLength(pickle_key_length)) => {
|
||||
assert_eq!(bytes.len(), pickle_key_length);
|
||||
}
|
||||
_ => panic!("Should have failed!"),
|
||||
}
|
||||
Ok(key)
|
||||
} else {
|
||||
Err(DehydrationError::PickleKeyLength(pickle_key_length))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,11 +25,9 @@ pub struct Device {
|
||||
/// Is our cross signing identity trusted and does the identity trust the
|
||||
/// device.
|
||||
pub cross_signing_trusted: bool,
|
||||
/// The first time this device was seen in local timestamp, milliseconds
|
||||
/// since epoch.
|
||||
/// The first time this device was seen in local timestamp, seconds since
|
||||
/// epoch.
|
||||
pub first_time_seen_ts: u64,
|
||||
/// Whether or not the device is a dehydrated device.
|
||||
pub dehydrated: bool,
|
||||
}
|
||||
|
||||
impl From<InnerDevice> for Device {
|
||||
@@ -44,7 +42,6 @@ impl From<InnerDevice> for Device {
|
||||
locally_trusted: d.is_locally_trusted(),
|
||||
cross_signing_trusted: d.is_cross_signing_trusted(),
|
||||
first_time_seen_ts: d.first_time_seen_ts().0.into(),
|
||||
dehydrated: d.is_dehydrated(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use matrix_sdk_crypto::{
|
||||
KeyExportError, MegolmError, OlmError, SecretImportError as RustSecretImportError,
|
||||
SignatureError as InnerSignatureError,
|
||||
store::{CryptoStoreError as InnerStoreError, DehydrationError as InnerDehydrationError},
|
||||
store::CryptoStoreError as InnerStoreError, KeyExportError, MegolmError, OlmError,
|
||||
SecretImportError as RustSecretImportError, SignatureError as InnerSignatureError,
|
||||
};
|
||||
use matrix_sdk_sqlite::OpenStoreError;
|
||||
use ruma::{IdParseError, OwnedUserId};
|
||||
@@ -58,8 +57,6 @@ pub enum CryptoStoreError {
|
||||
InvalidUserId(String, IdParseError),
|
||||
#[error(transparent)]
|
||||
Identifier(#[from] IdParseError),
|
||||
#[error(transparent)]
|
||||
DehydrationError(#[from] InnerDehydrationError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
@@ -76,55 +73,12 @@ pub enum DecryptionError {
|
||||
Store { error: String },
|
||||
}
|
||||
|
||||
/// Error describing what went wrong when exporting a [`SecretsBundle`].
|
||||
///
|
||||
/// The [`SecretsBundle`] can only be exported if we have all cross-signing
|
||||
/// private keys in the store.
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
pub enum SecretsBundleExportError {
|
||||
/// The store itself had an error.
|
||||
#[error(transparent)]
|
||||
CryptoStore(CryptoStoreError),
|
||||
/// We're missing one or more cross-signing keys.
|
||||
#[error("The store doesn't contain all the cross-signing keys")]
|
||||
MissingCrossSigningKeys,
|
||||
/// We have a backup key stored, but we don't know the version of the
|
||||
/// backup.
|
||||
#[error("The store contains a backup key, but no backup version")]
|
||||
MissingBackupVersion,
|
||||
#[error("serialization error: {error}")]
|
||||
Serialization { error: String },
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::store::SecretsBundleExportError> for SecretsBundleExportError {
|
||||
fn from(value: matrix_sdk_crypto::store::SecretsBundleExportError) -> Self {
|
||||
match value {
|
||||
matrix_sdk_crypto::store::SecretsBundleExportError::Store(e) => {
|
||||
Self::CryptoStore(e.into())
|
||||
}
|
||||
matrix_sdk_crypto::store::SecretsBundleExportError::MissingCrossSigningKey(_)
|
||||
| matrix_sdk_crypto::store::SecretsBundleExportError::MissingCrossSigningKeys => {
|
||||
Self::MissingCrossSigningKeys
|
||||
}
|
||||
matrix_sdk_crypto::store::SecretsBundleExportError::MissingBackupVersion => {
|
||||
Self::MissingBackupVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for SecretsBundleExportError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
Self::Serialization { error: err.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MegolmError> for DecryptionError {
|
||||
fn from(value: MegolmError) -> Self {
|
||||
match &value {
|
||||
match value {
|
||||
MegolmError::MissingRoomKey(withheld_code) => Self::MissingRoomKey {
|
||||
error: value.to_string(),
|
||||
withheld_code: withheld_code.as_ref().map(|w| w.as_str().to_owned()),
|
||||
error: "Withheld Inbound group session".to_owned(),
|
||||
withheld_code: withheld_code.map(|w| w.as_str().to_owned()),
|
||||
},
|
||||
_ => Self::Megolm { error: value.to_string() },
|
||||
}
|
||||
@@ -158,7 +112,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_withheld_error_mapping() {
|
||||
use matrix_sdk_common::deserialized_responses::WithheldCode;
|
||||
use matrix_sdk_crypto::types::events::room_key_withheld::WithheldCode;
|
||||
|
||||
let inner_error = MegolmError::MissingRoomKey(Some(WithheldCode::Unverified));
|
||||
|
||||
|
||||
@@ -16,13 +16,9 @@ mod responses;
|
||||
mod users;
|
||||
mod verification;
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use std::{collections::HashMap, str::FromStr, sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Context;
|
||||
pub use backup_recovery_key::{
|
||||
BackupRecoveryKey, DecodeError, MegolmV1BackupKey, PassphraseInfo, PkDecryptionError,
|
||||
};
|
||||
@@ -31,22 +27,15 @@ pub use error::{
|
||||
CryptoStoreError, DecryptionError, KeyImportError, SecretImportError, SignatureError,
|
||||
};
|
||||
use js_int::UInt;
|
||||
pub use logger::{Logger, set_logger};
|
||||
pub use logger::{set_logger, Logger};
|
||||
pub use machine::{KeyRequestPair, OlmMachine, SignatureVerification};
|
||||
use matrix_sdk_common::deserialized_responses::{ShieldState as RustShieldState, ShieldStateCode};
|
||||
use matrix_sdk_common::deserialized_responses::ShieldState as RustShieldState;
|
||||
use matrix_sdk_crypto::{
|
||||
CollectStrategy, EncryptionSettings as RustEncryptionSettings,
|
||||
olm::{IdentityKeys, InboundGroupSession, SenderData, Session},
|
||||
store::{
|
||||
CryptoStore,
|
||||
types::{
|
||||
Changes, DehydratedDeviceKey as InnerDehydratedDeviceKey, PendingChanges,
|
||||
RoomSettings as RustRoomSettings,
|
||||
},
|
||||
},
|
||||
types::{
|
||||
DeviceKey, DeviceKeys, EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey,
|
||||
},
|
||||
backups::SignatureState,
|
||||
olm::{IdentityKeys, InboundGroupSession, Session},
|
||||
store::{Changes, CryptoStore, PendingChanges, RoomSettings as RustRoomSettings},
|
||||
types::{EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey},
|
||||
EncryptionSettings as RustEncryptionSettings, LocalTrust,
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteCryptoStore;
|
||||
pub use responses::{
|
||||
@@ -54,9 +43,9 @@ pub use responses::{
|
||||
Request, RequestType, SignatureUploadRequest, UploadSigningKeysRequest,
|
||||
};
|
||||
use ruma::{
|
||||
DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId,
|
||||
RoomId, SecondsSinceUnixEpoch, UserId,
|
||||
events::room::history_visibility::HistoryVisibility as RustHistoryVisibility,
|
||||
DeviceKeyAlgorithm, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId, RoomId,
|
||||
SecondsSinceUnixEpoch, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::runtime::Runtime;
|
||||
@@ -68,8 +57,6 @@ pub use verification::{
|
||||
};
|
||||
use vodozemac::{Curve25519PublicKey, Ed25519PublicKey};
|
||||
|
||||
use crate::dehydrated_devices::DehydrationError;
|
||||
|
||||
/// Struct collecting data that is important to migrate to the rust-sdk
|
||||
#[derive(Deserialize, Serialize, uniffi::Record)]
|
||||
pub struct MigrationData {
|
||||
@@ -142,10 +129,10 @@ pub struct PickledSession {
|
||||
pub sender_key: String,
|
||||
/// Was the session created using a fallback key.
|
||||
pub created_using_fallback_key: bool,
|
||||
/// Unix timestamp (in seconds) when the session was created.
|
||||
pub creation_time: u64,
|
||||
/// Unix timestamp (in seconds) when the session was last used.
|
||||
pub last_use_time: u64,
|
||||
/// The Unix timestamp when the session was created.
|
||||
pub creation_time: String,
|
||||
/// The Unix timestamp when the session was last used.
|
||||
pub last_use_time: String,
|
||||
}
|
||||
|
||||
/// A pickled version of an `InboundGroupSession`.
|
||||
@@ -199,12 +186,12 @@ impl From<anyhow::Error> for MigrationError {
|
||||
/// * `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.
|
||||
/// 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.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
/// progress of the migration.
|
||||
#[uniffi::export]
|
||||
pub fn migrate(
|
||||
data: MigrationData,
|
||||
path: String,
|
||||
@@ -224,7 +211,7 @@ async fn migrate_data(
|
||||
passphrase: Option<String>,
|
||||
progress_listener: Box<dyn ProgressListener>,
|
||||
) -> anyhow::Result<()> {
|
||||
use matrix_sdk_crypto::{olm::PrivateCrossSigningIdentity, store::types::BackupDecryptionKey};
|
||||
use matrix_sdk_crypto::{olm::PrivateCrossSigningIdentity, store::BackupDecryptionKey};
|
||||
use vodozemac::olm::Account;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
@@ -257,11 +244,9 @@ async fn migrate_data(
|
||||
user_id: parse_user_id(&data.account.user_id)?,
|
||||
device_id: device_id.clone(),
|
||||
pickle,
|
||||
dehydrated: false, // dehydrated devices are never involved in migration
|
||||
shared: data.account.shared,
|
||||
uploaded_signed_key_count: data.account.uploaded_signed_key_count as u64,
|
||||
creation_local_time: MilliSecondsSinceUnixEpoch::now(),
|
||||
fallback_key_creation_timestamp: Some(MilliSecondsSinceUnixEpoch::now()),
|
||||
creation_local_time: MilliSecondsSinceUnixEpoch(UInt::default()),
|
||||
};
|
||||
let account = matrix_sdk_crypto::olm::Account::from_pickle(pickled_account)?;
|
||||
|
||||
@@ -362,12 +347,12 @@ async fn save_changes(
|
||||
/// * `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.
|
||||
/// 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.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
/// progress of the migration.
|
||||
#[uniffi::export]
|
||||
pub fn migrate_sessions(
|
||||
data: SessionMigrationData,
|
||||
path: String,
|
||||
@@ -433,38 +418,13 @@ fn collect_sessions(
|
||||
) -> anyhow::Result<(Vec<Session>, Vec<InboundGroupSession>)> {
|
||||
let mut sessions = Vec::new();
|
||||
|
||||
// Create a DeviceKeys struct with enough information to get a working
|
||||
// Session, but we will won't actually use the Sessions (and we'll clear
|
||||
// the session cache after migration) so we don't need to worry about
|
||||
// signatures.
|
||||
let device_keys = DeviceKeys::new(
|
||||
user_id,
|
||||
device_id.clone(),
|
||||
Default::default(),
|
||||
BTreeMap::from([
|
||||
(
|
||||
DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, &device_id),
|
||||
DeviceKey::Ed25519(identity_keys.ed25519),
|
||||
),
|
||||
(
|
||||
DeviceKeyId::from_parts(DeviceKeyAlgorithm::Curve25519, &device_id),
|
||||
DeviceKey::Curve25519(identity_keys.curve25519),
|
||||
),
|
||||
]),
|
||||
Default::default(),
|
||||
);
|
||||
|
||||
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::new(session_pickle.creation_time).context("invalid creation timestamp")?,
|
||||
);
|
||||
let last_use_time = SecondsSinceUnixEpoch(
|
||||
UInt::new(session_pickle.last_use_time).context("invalid last use timestamp")?,
|
||||
);
|
||||
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,
|
||||
@@ -474,7 +434,8 @@ fn collect_sessions(
|
||||
last_use_time,
|
||||
};
|
||||
|
||||
let session = Session::from_pickle(device_keys.clone(), pickle)?;
|
||||
let session =
|
||||
Session::from_pickle(user_id.clone(), device_id.clone(), identity_keys.clone(), pickle);
|
||||
|
||||
sessions.push(session);
|
||||
processed_steps += 1;
|
||||
@@ -505,13 +466,10 @@ fn collect_sessions(
|
||||
Ok((algorithm, key))
|
||||
})
|
||||
.collect::<anyhow::Result<_>>()?,
|
||||
sender_data: SenderData::legacy(),
|
||||
forwarder_data: None,
|
||||
room_id: RoomId::parse(session.room_id)?,
|
||||
imported: session.imported,
|
||||
backed_up: session.backed_up,
|
||||
history_visibility: None,
|
||||
shared_history: false,
|
||||
algorithm: RustEventEncryptionAlgorithm::MegolmV1AesSha2,
|
||||
};
|
||||
|
||||
@@ -540,9 +498,9 @@ fn collect_sessions(
|
||||
/// * `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.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
/// rest in the Sqlite store. **Warning**, if no passphrase is given, the store
|
||||
/// and all its data will remain unencrypted.
|
||||
#[uniffi::export]
|
||||
pub fn migrate_room_settings(
|
||||
room_settings: HashMap<String, RoomSettings>,
|
||||
path: String,
|
||||
@@ -568,7 +526,7 @@ pub fn migrate_room_settings(
|
||||
}
|
||||
|
||||
/// Callback that will be passed over the FFI to report progress
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait ProgressListener {
|
||||
/// The callback that should be called on the Rust side
|
||||
///
|
||||
@@ -666,9 +624,6 @@ impl From<HistoryVisibility> for RustHistoryVisibility {
|
||||
pub struct EncryptionSettings {
|
||||
/// The encryption algorithm that should be used in the room.
|
||||
pub algorithm: EventEncryptionAlgorithm,
|
||||
/// Whether state event encryption is enabled.
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
pub encrypt_state_events: bool,
|
||||
/// How long can the room key be used before it should be rotated. Time in
|
||||
/// seconds.
|
||||
pub rotation_period: u64,
|
||||
@@ -681,29 +636,16 @@ pub struct EncryptionSettings {
|
||||
/// Should untrusted devices receive the room key, or should they be
|
||||
/// excluded from the conversation.
|
||||
pub only_allow_trusted_devices: bool,
|
||||
/// Should fail to send when a verified user has unverified devices, or when
|
||||
/// a previously verified user replaces their identity.
|
||||
pub error_on_verified_user_problem: bool,
|
||||
}
|
||||
|
||||
impl From<EncryptionSettings> for RustEncryptionSettings {
|
||||
fn from(v: EncryptionSettings) -> Self {
|
||||
let sharing_strategy = if v.only_allow_trusted_devices {
|
||||
CollectStrategy::OnlyTrustedDevices
|
||||
} else if v.error_on_verified_user_problem {
|
||||
CollectStrategy::ErrorOnVerifiedUserProblem
|
||||
} else {
|
||||
CollectStrategy::AllDevices
|
||||
};
|
||||
|
||||
RustEncryptionSettings {
|
||||
algorithm: v.algorithm.into(),
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
encrypt_state_events: false,
|
||||
rotation_period: Duration::from_secs(v.rotation_period),
|
||||
rotation_period_msgs: v.rotation_period_msgs,
|
||||
history_visibility: v.history_visibility.into(),
|
||||
sharing_strategy,
|
||||
only_allow_trusted_devices: v.only_allow_trusted_devices,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -748,24 +690,19 @@ pub enum ShieldColor {
|
||||
#[allow(missing_docs)]
|
||||
pub struct ShieldState {
|
||||
color: ShieldColor,
|
||||
code: Option<ShieldStateCode>,
|
||||
message: Option<String>,
|
||||
}
|
||||
|
||||
impl From<RustShieldState> for ShieldState {
|
||||
fn from(value: RustShieldState) -> Self {
|
||||
match value {
|
||||
RustShieldState::Red { code, message } => Self {
|
||||
color: ShieldColor::Red,
|
||||
code: Some(code),
|
||||
message: Some(message.to_owned()),
|
||||
},
|
||||
RustShieldState::Grey { code, message } => Self {
|
||||
color: ShieldColor::Grey,
|
||||
code: Some(code),
|
||||
message: Some(message.to_owned()),
|
||||
},
|
||||
RustShieldState::None => Self { color: ShieldColor::None, code: None, message: None },
|
||||
RustShieldState::Red { message } => {
|
||||
Self { color: ShieldColor::Red, message: Some(message.to_owned()) }
|
||||
}
|
||||
RustShieldState::Grey { message } => {
|
||||
Self { color: ShieldColor::Grey, message: Some(message.to_owned()) }
|
||||
}
|
||||
RustShieldState::None => Self { color: ShieldColor::None, message: None },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -814,7 +751,7 @@ pub struct BackupKeys {
|
||||
backup_version: String,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
impl BackupKeys {
|
||||
/// Get the recovery key that we're holding on to.
|
||||
pub fn recovery_key(&self) -> Arc<BackupRecoveryKey> {
|
||||
@@ -827,10 +764,10 @@ impl BackupKeys {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<matrix_sdk_crypto::store::types::BackupKeys> for BackupKeys {
|
||||
impl TryFrom<matrix_sdk_crypto::store::BackupKeys> for BackupKeys {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(keys: matrix_sdk_crypto::store::types::BackupKeys) -> Result<Self, Self::Error> {
|
||||
fn try_from(keys: matrix_sdk_crypto::store::BackupKeys) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
recovery_key: BackupRecoveryKey {
|
||||
inner: keys.decryption_key.ok_or(())?,
|
||||
@@ -842,41 +779,8 @@ impl TryFrom<matrix_sdk_crypto::store::types::BackupKeys> for BackupKeys {
|
||||
}
|
||||
}
|
||||
|
||||
/// Dehydrated device key
|
||||
#[derive(uniffi::Record, Clone)]
|
||||
pub struct DehydratedDeviceKey {
|
||||
pub(crate) inner: Vec<u8>,
|
||||
}
|
||||
|
||||
impl DehydratedDeviceKey {
|
||||
/// Generates a new random pickle key.
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
InnerDehydratedDeviceKey::new().into()
|
||||
}
|
||||
|
||||
/// Creates a new dehydration pickle key from the given slice.
|
||||
///
|
||||
/// Fail if the slice length is not 32.
|
||||
pub fn from_slice(slice: &[u8]) -> Result<Self, DehydrationError> {
|
||||
let inner = InnerDehydratedDeviceKey::from_slice(slice)?;
|
||||
Ok(inner.into())
|
||||
}
|
||||
|
||||
/// Export the [`DehydratedDeviceKey`] as a base64 encoded string.
|
||||
pub fn to_base64(&self) -> String {
|
||||
let inner = InnerDehydratedDeviceKey::from_slice(&self.inner).unwrap();
|
||||
inner.to_base64()
|
||||
}
|
||||
}
|
||||
impl From<InnerDehydratedDeviceKey> for DehydratedDeviceKey {
|
||||
fn from(pickle_key: InnerDehydratedDeviceKey) -> Self {
|
||||
DehydratedDeviceKey { inner: pickle_key.into() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::store::types::RoomKeyCounts> for RoomKeyCounts {
|
||||
fn from(count: matrix_sdk_crypto::store::types::RoomKeyCounts) -> Self {
|
||||
impl From<matrix_sdk_crypto::store::RoomKeyCounts> for RoomKeyCounts {
|
||||
fn from(count: matrix_sdk_crypto::store::RoomKeyCounts) -> Self {
|
||||
Self { total: count.total as i64, backed_up: count.backed_up as i64 }
|
||||
}
|
||||
}
|
||||
@@ -916,10 +820,6 @@ impl From<matrix_sdk_crypto::CrossSigningStatus> for CrossSigningStatus {
|
||||
pub struct RoomSettings {
|
||||
/// The encryption algorithm that should be used in the room.
|
||||
pub algorithm: EventEncryptionAlgorithm,
|
||||
/// Whether state event encryption is enabled.
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
#[serde(default)]
|
||||
pub encrypt_state_events: bool,
|
||||
/// Should untrusted devices receive the room key, or should they be
|
||||
/// excluded from the conversation.
|
||||
pub only_allow_trusted_devices: bool,
|
||||
@@ -930,12 +830,7 @@ impl TryFrom<RustRoomSettings> for RoomSettings {
|
||||
|
||||
fn try_from(value: RustRoomSettings) -> Result<Self, Self::Error> {
|
||||
let algorithm = value.algorithm.try_into()?;
|
||||
Ok(Self {
|
||||
algorithm,
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
encrypt_state_events: value.encrypt_state_events,
|
||||
only_allow_trusted_devices: value.only_allow_trusted_devices,
|
||||
})
|
||||
Ok(Self { algorithm, only_allow_trusted_devices: value.only_allow_trusted_devices })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -944,7 +839,6 @@ impl From<RoomSettings> for RustRoomSettings {
|
||||
Self {
|
||||
algorithm: value.algorithm.into(),
|
||||
only_allow_trusted_devices: value.only_allow_trusted_devices,
|
||||
..RustRoomSettings::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -953,7 +847,7 @@ fn parse_user_id(user_id: &str) -> Result<OwnedUserId, CryptoStoreError> {
|
||||
ruma::UserId::parse(user_id).map_err(|e| CryptoStoreError::InvalidUserId(user_id.to_owned(), e))
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
fn version_info() -> VersionInfo {
|
||||
VersionInfo {
|
||||
version: matrix_sdk_crypto::VERSION.to_owned(),
|
||||
@@ -977,83 +871,26 @@ pub struct VersionInfo {
|
||||
pub git_description: String,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
fn version() -> String {
|
||||
matrix_sdk_crypto::VERSION.to_owned()
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
fn vodozemac_version() -> String {
|
||||
vodozemac::VERSION.to_owned()
|
||||
}
|
||||
|
||||
/// The encryption component of PkEncryption support.
|
||||
///
|
||||
/// This struct can be created using a [`Curve25519PublicKey`] corresponding to
|
||||
/// a `PkDecryption` object, allowing messages to be encrypted for the
|
||||
/// associated decryption object.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct PkEncryption {
|
||||
inner: matrix_sdk_crypto::vodozemac::pk_encryption::PkEncryption,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl PkEncryption {
|
||||
/// Create a new [`PkEncryption`] object from a `Curve25519PublicKey`
|
||||
/// encoded as Base64.
|
||||
///
|
||||
/// The public key should come from an existing `PkDecryption` object.
|
||||
/// Returns a `DecodeError` if the Curve25519 key could not be decoded
|
||||
/// correctly.
|
||||
#[uniffi::constructor]
|
||||
pub fn from_base64(key: &str) -> Result<Arc<Self>, DecodeError> {
|
||||
let key = vodozemac::Curve25519PublicKey::from_base64(key)
|
||||
.map_err(matrix_sdk_crypto::backups::DecodeError::PublicKey)?;
|
||||
let inner = vodozemac::pk_encryption::PkEncryption::from_key(key);
|
||||
|
||||
Ok(Self { inner }.into())
|
||||
}
|
||||
|
||||
/// Encrypt a message using this [`PkEncryption`] object.
|
||||
pub fn encrypt(&self, plaintext: &str) -> Option<PkMessage> {
|
||||
use vodozemac::base64_encode;
|
||||
|
||||
let message = self.inner.encrypt(plaintext.as_ref()).ok()?;
|
||||
|
||||
let vodozemac::pk_encryption::Message { ciphertext, mac, ephemeral_key } = message;
|
||||
|
||||
Some(PkMessage {
|
||||
ciphertext: base64_encode(ciphertext),
|
||||
mac: base64_encode(mac),
|
||||
ephemeral_key: ephemeral_key.to_base64(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A message that was encrypted using a [`PkEncryption`] object.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct PkMessage {
|
||||
/// The ciphertext of the message.
|
||||
pub ciphertext: String,
|
||||
/// The message authentication code of the message.
|
||||
///
|
||||
/// *Warning*: This does not authenticate the ciphertext.
|
||||
pub mac: String,
|
||||
/// The ephemeral Curve25519 key of the message which was used to derive the
|
||||
/// individual message key.
|
||||
pub ephemeral_key: String,
|
||||
}
|
||||
|
||||
uniffi::setup_scaffolding!();
|
||||
uniffi::include_scaffolding!("olm");
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
use serde_json::{Value, json};
|
||||
use serde_json::{json, Value};
|
||||
use tempfile::tempdir;
|
||||
|
||||
use super::MigrationData;
|
||||
use crate::{EventEncryptionAlgorithm, OlmMachine, RoomSettings, migrate};
|
||||
use crate::{migrate, EventEncryptionAlgorithm, OlmMachine, RoomSettings};
|
||||
|
||||
#[test]
|
||||
fn android_migration() -> Result<()> {
|
||||
@@ -1064,35 +901,36 @@ mod tests {
|
||||
"pickle":"FFGTGho89T3Xgd56l+EedOPV37s09RR8aYnS9305qPKF66LG+ly29YpCibjJOvkwm0dZwN9A2bOH/z7WscriqwZn/p0GE6YSNwLzffCy5iROzYzpYzFe0HtiyJmCQWCezvLc5lHV8YsfD00C1pKGX2R9M1wwp3/n4/3VjtTyPsdnmtwAPu4WdcPSkisCaQ3a6JaSKqv8zYzUjnpzgcpXHvPUR5d5+TzXgrVz3BeCOe8NEOWIW6xYUxFtGteYP0BczOkkJ22t7Css0tSMSrYgCll4zZUGNrd6D9b/z7KwcDnb978epsZ16DcZ/aaTxPdM5uDIkHgF/qHWerfxcaqsqs4EQfJdSgOTeqhjHBw1k0uWF2bByJLK+n7sGkYXEAuTzc4+0XvSFvu3Qp+1bHZuT7QejngRZzyxznORyBxd8la3/JjeJlehSK80OL7zSmohoYZD59S6i3tFWfopjQThJ0/eIyVOhEN/c3tfIcVr3lFEQeokgpCRNOVldhPcQWq994NHaL7jtb6yhUqT1gShY4zYayFL/VRz6nBSXXYwzrC9jho67knqXSri3lIKYevP9aOi384IvzbkinQdumc804dYwiCbs5hZppfEnfhfgiDDm+kVrJ9WaPRF4SySCTlS8jdGmBeL2CfCQ5IcZ5nK6X7tZM3tmtYwva0RuQiTNltp3XTfbMa0EoaEBximv25165hFTpzrWgoszBTpZPfgsMuWENWBcIX4AcLSk0CJ0qzPDeUwvmRcFStstGYV4drs5u5HEqovFSI48CoHPSEZfwwERCI4c/0efZ0CVEfnm8VcMv3AbnAfedD7v3QNdVwWOEhz/fGR76BQi2WjZP4MWvYRJ/vsLO5hcVWUvaJGQs5kANUFZMWuJQeJv3DmkV9kKKXnyfFUerlQ4Uk/5tp2mXiG+adHjuRp/Eeh5V/biCcIaX3rNuIY6MJaPz6SOwlFe79MMBaNwaS3j4Kh/Aq9BRw0QXdjO4CqMI4p2xCE1N5QTPdeaRTHTZ3r7mLkHX3FpZMxitc8vDl9L2FRoSOMMh/sRD1boBCkjrsty9rvTUGYY3li05jBuTXnYMjA4zj79dC9TGo4g+/wi+h537EhtP5+170LwqnIzfHt8yfjbsMMC7iwLpC1C57sTwxpMkNo3nQEvZOfqCxjq+ihiGuL9iN5lSstu9/C4qP2tQll86ASXf1axxRZQlUB0hlLHbEW6/7O7xOU6FTs4yXAZC04souRkggmfhDzZ9kQmN/zRTbqlATFI7l9/0VGxwLOVnCMUhgiDX5yL8CYK9I4ENMLf5zOuO6P3GbYISjEoHC7fUOzQ6OwGgLyI0wCEVdSJzQcdKh+W15VV+eDjhE/qEJHQWx024hTQFTKYHlDn95+lMmRI9BJLP1HU2JW6onVWsTsE5zSYu9jLj739EKfV4gS/pWzoQDRa7a9ZG6+m+RrwyJhCso3gkUekDNobhFlDX6YeH+Btj91N0uS3F9qr8lbo491s/z2fNV42zT4NYObzgrAYDQAV/2WYF8tXtxLV/Jzk8AMmyr/cfNaT2dXxVJKWq+nN2BYHBmg9CCWPJ2aB/1WWIcHfcDOlngtH991gP6246f/DEaVC/Ayxz7bPtSH5tlZ4Xbpc2P4BYxaRp/yxhhQ2C9H2I/PTt3mnNNgky/t8PZrN3W5+eiSVE9sONF8G3mYsa4XFqM+KxfbPUqsrEnrRBmvmJ250hpTPkFcIF775RvvRRKALXdlTKs+S4HKDW7KoP0Dm9+r4RlO0UHpWND9w0WSMItvWQyo0VViXJgZfBjYtWDoO0Ud+Kc7PLWNX6RUKY7RlDjXadJTC4adH6CN3UBC/ouqqfTrYvPOkyd2oKf4RLjEVcFAUIftFbLy+WBcWv8072nnAFJIlm3CxGq++80TyjqFR45P+qfIJavxQNIt5zhHPfMgHjX27OA3+l7rHDxqfMLBPxhtARwlyF+qx1IJiSWbmlHkdz2ylD9unoLSpf+DmmFvvgTj+3EEP4bY2jA/t91XFeG3uaTQSy3ryDvhbX21U7G2HGOEl9rCkmz+hG0YRB/6KxZZ0eMIDr7OWfpPEuHV8oYwDNYbsT9zCGsR1hHxBJtdo60b36mjMemtf761DhJ/oQZ4eU738yzx1hvVS3aCJsfyp70H5u+pUjgrA565uG2lEMNLu4T4NFVw0UdrVudyrhmT8P7vF4v+mR4pp+OzRbLf8AtZrKmHlMqRst+/wOHUHug/Tpz6EwZPDWGiQyFyPUkjHWW7ACouegBFOWFabsk+zCDhyxoSNrSMCtdB1L+qK72jRPGOvXk8p/1kBOIJfAjaK1ZWz8hTc30hOSWYxkRP296zPHiQF0ibNYSPNZ9tNxgq9nV/cEQ68TsNr3SULfDr0TSjCPf4AfmJ0k1k5xphSYv/TtGIbjg/9yGVFqclg4Y/6rrfkApbx36PQEBNxLiRsZ4hGpCfVU6h0jOekk8TV6CAguXVX/G31UqsAEa4sOD2g10Ir+5JD7bdd3JE/999kHGdiCqc0DNcgSqWYbq2QYwrN/mb+mMUbiQSNMcc34kK1n+7dGxppnt7YN7UsJqBWJdH0Lw1Epxi11ViTeVma9bqioJYXi6N5exdpZTT7KmcGYFsoTqO958EX6AppgcML7N9oP3TO8qSgCpV3Bbbemq4bvjV43aM6Rdx17pC4GZo0jjU97p4K8jE4PvgoHlYkuPwSJDOSAdnYPh+Inq/vCk48UfIlup0ATJFVUXD7uf84v9roZSwZPXZ5j/88+MkHBIJwPv8cugmz5uN2EuBW5IScMuEqG7Cmk72SU3/QA39G79S0Gpw7iPhTos5LXxhfvohGcnSaNEvfNeecQf7fpVciTdHwuvcgqJizUKpSFg2P+LDBiO44mJD15RNAaT37Rrj5P06YITO4PDj+FMdc6gx+JQUFbcSRhScE/0gfsVm0P1BYIH5q0k/QDgEVoerf/n19lITTzPib1F2OHP4hyF3BEq1pd9NwuPhhsVVqTVTK5MzFwFIOH7cwJyY7aBykmsWBavdb2J7UA5wjKqMHl1auUGPlNL+lZjqG4tw05bchtFAF+PGWQXJhJCtRSkkzTOCrLRyYyyI9mWYEjoc23cGLanlIs7WA1Nd0Jz+5RSNlf9Gtnd65yQp/W1eqY6yzURPHUUa7FrynyORmjaR9adT9utSQkXy8++IeDNzhMtFr+SqQ/gKECLe0GeuyTs6E5bImUtqpN+xopBXnEeq8wp+bvLf76d98qPE5ibTRwlsSyCE4c1Y7vrJrlc15Yc2R9ciIuKUS8rUKLSdGBFe/TD4R3cPhCKAnnRLGWnJiPPgxoTVwHVZMISdsAjNaWblBmiAOzFcu7443d3PCLyXVcfR9xgvW51HTumo91t5Qyx4HIXGoZxayZYFm2hrhSlieUqLnDL2j2gYgGU5NGoQl4OnEY2QqobpRUF4xJ4HhLzYbLrBeXmTDPvj0MasC3kKsRlm/HrsRRWZ2iPSMw9601tLvDfyjG53ddPISiVNnkdXcaAN5np7dwipdBOC1s4a0sEmKakNbkkDb8LsGBNte/g6UYs5yYaKr0bnXlDjMCznHQa7pypBjE7S55T3UeRpwo3IvZ1tfIGdb+z9RIA/PDvUksxJ3Xq3lqtZzkZJF5aeedfIOekGS/G0LiCSYsELgRceH5veknHqoGoL6xi4Q6/VjmfpZVXT19bDcTNtaR9Dlaq4LDjpQl9rl5C3O/X1hgADvJUuINCiLrD114sLY1DG/TDXE0sp+TK7utnjLAoHuAuj+6anY5vN66CSbwyUNmvo+m8li/AMkRYdtSDoPWkV7Y1ixMBPcua0Llwn2HSKKwnCjvhDIDIIVwbWwb1s6b9cztH81WF5RWUgFujewPvTElM1Sy10y7BcZohKw28uLRFVsKunc9yX2PiQoTSB4PHBHRA4U5dEQV3GHQJ93nee7VT3oeQPMVebWhuhOhi34Z33LQajzpCF3OjIbJb0tOPP6L6N/ODqkNsYViI3kgCnkNhexadOuGFWIqen2Q8iv2uOZWbPirt0YEeKZIk2dpND07L8Q3OsoQCk2rjpnw9LuFrjgu7gN9gFyPq25HJRBn7PM/lS60DF+xVkJq94PwN+CiZWC43SVcBGx65DFZIs/N78MZCUzZbFlsS7FsIrDJt878cp9eZdq/Ai4LZhL8QYHpVUrQxRxZGSqooA755N6nOxw66JkA1VPnjECCMgoNNtWox0JzhMe8PBdh2ZliXf8yQ6/eTvsG6FD84F+49pc7m0L99pfWHb9ClyO3KRHscp/MOIC1MJmqoB4dNxV20U+z8/lSTIvcmM8DiaAZj/yxlst90drlGydlyPjQzYd/XtIYcO5gHoeD1KUCZRapE5dkyk5vh97WZJn/JkR8hsslU3D6x3rNGwJbQVRu0IiA3PpeAQNZBNAJHHfv8IzIYxPhMJdYq0YqLIGSUYu87D04cDOxJY7hgawYs+ExOWb7XkbpuRoITQd8zpwVDFlSCS+wFO+qah3Vn8RBTc6cXHO5xRWfUNj+NrEtPdVmax+9EXqXtHQyFpxaauvL96RH+mGwpKHOk3aisXbZ6gLE2mF4egGjjJOIJdHyb2ZR+kj+4GIvkoBwipDgUfr4UBXY8pvFxQOxRgtI4LgOY9Z1Aco7Mwp6qi1KoMFJW8d+gJwsgM3cMsyEeYH1n/mdpJW6VDbIWzOHkP5n+OKKNm2vJTkQFFwF9eOtGy9fNBtS4qo4jvOUJnnAPsrPbGMbBYd1dMC3daHLEwvIKCAVBn7q1Z2c4zAD5eEoY0EwZj/j8x8lGQ8TswFT81ZotW7ZBDai/YtV8mkGfuaWJRI5yHc/bV7GWLF+yrMji/jicBF5jy2UoqwxseqjgTut49FRgBH3h1qwnfYbXD3FvQljyAAgBCiZV726pFRG+sZv0FjDbq0iCKILVSEUDZgmQ",
|
||||
"shared":true,
|
||||
"uploaded_signed_key_count":50
|
||||
|
||||
},
|
||||
"sessions":[
|
||||
{
|
||||
"pickle":"cryZlFaQv0hwWe6tTgv75RExFKGnC8tMHBXJYMHOw4s+SdrKUYAMUdGcYD7QukrPklEOy7fJho9YGK/jV04QdA8JABiOfD+ngJTR4V8eZdmDuG08+Q5EL79V81hQwU2fKndP0y/9nAXPUIADYq0Zrg4EsOnXz7aE+hAeBAm0IBog1s8RYUvynZ15uwjbd/OTLP+gpqpX33DwVg2leiBkQetiUSpOpZCuQ8CcZwIA0MoGCqvaT7h76VHX9JxJx+2fCMhsJMx1nhd99WJH1W9ge5CtdbC4KUP92OSxIrPOnMrNcOPJPp/paZP+HFNQ3PDL+z8pGKXmCnrXGSbd7iPHurPYESrVkBzr",
|
||||
"sender_key":"WJ6Ce7U67a6jqkHYHd8o0+5H4bqdi9hInZdk0+swuXs",
|
||||
"created_using_fallback_key":false,
|
||||
"creation_time": 1649425011424u64,
|
||||
"last_use_time": 1649425011424u64
|
||||
"creation_time":"1649425011424",
|
||||
"last_use_time":"1649425011424"
|
||||
},
|
||||
{
|
||||
"pickle":"cryZlFaQv0hwWe6tTgv75RExFKGnC8tMHBXJYMHOw4t2W/lowyrV6SXVZp+uG59im0AAfNSKjhjZuiOpQlX7MS+AOJkCNvyujJ2g3KSjLZ94IkoHxkBDHLWSjwaLPu40rfOzJPDpm0XZsR6bQrsxKOmXLGEw2qw5jOTouzMVL2gvuuTix97nSYSU8j3XvTMRUoh0AF/tUpRLcvEFZeGrdUYmTMlyTv4na+FVUalUZ+jrk8t1/sM99JNq3SY1IBSjrBq/0rCOHieiippz0sw2fe2b87id4rqj1g3R9w2MWTWEdOz3ugjMGYF1YDBQZA1tJZ/hmgppk2AU2xKQXE2X3DgSC6fC66D4",
|
||||
"sender_key":"RzRROfmHNlBfzxnNCUYBfn/5oZNQ11XYjDg59hS+mV0",
|
||||
"created_using_fallback_key":false,
|
||||
"creation_time": 1649425011503u64,
|
||||
"last_use_time": 1649425011503u64
|
||||
"creation_time":"1649425011503",
|
||||
"last_use_time":"1649425011503"
|
||||
},
|
||||
{
|
||||
"pickle":"cryZlFaQv0hwWe6tTgv75RExFKGnC8tMHBXJYMHOw4titbL3SS12PYHpcBPJc6hXnOnZXqrjtjYOD545fck+3utEo8cqqwWubc9tsvxGW3tOWPttLBdAW30Vn8V1M8ebqVCNVWEAb1GKjV4ni8xG7G9SlEcCjLjnF4lJpddSZkqVMFoN0ITr9aSz/eJwXpc3HLreUFXwc8LuQp7krQ4Vt1e5EE/klduqsdurZf5V14RHsmWz2lKjt7nVgtIz/dhtF5F/sGJdg8kCGaHIMSbGAPuPPpa4/Laicb/5otrYt4pg4W4KdFpSGJIcvUQNjXaOZMx3cu/RPJIOyNhx7whG1QiYAUBqAJvr",
|
||||
"sender_key":"IXSZugAHig1v8MowE1jxi2wDDDfuZBeJynHlegJVwUc",
|
||||
"created_using_fallback_key":false,
|
||||
"creation_time": 1649425011566u64,
|
||||
"last_use_time": 1649425011566u64
|
||||
"creation_time":"1649425011566",
|
||||
"last_use_time":"1649425011566"
|
||||
},
|
||||
{
|
||||
"pickle":"SmkDiFZjNukiarQ7XHQo25FILHsuhNOnxy56cMSQU/Y71jaGbJes4YrvN4Dfy4RSONfejEDXDkbW2JudlHHRP/rWEmnfJiGbK6ArbrG2puqIZgOecPnOUgPfCisr49p1Gmf36dPaO5lm/ZSrngfSoxahoeJJE/CcJN98sYM15XytRk2LBwc+CyYDqr4V1qxfsBt6tzJ4+tsAZeRdD0UtipQgysgH56o8N7nKTCkaZz5lfpYCl3FEgwXpLJ0MGQvtQmbORFvOLqR1jZ/EbmNGKiqDDIYsqG0sf78ii1jqfpLDBXLuYDccsg",
|
||||
"sender_key":"EB9SC4jVAydKhM6/GcwMc9biKwVNywqW3TerNTrtb1M",
|
||||
"created_using_fallback_key":false,
|
||||
"creation_time": 1649542063182u64,
|
||||
"last_use_time": 1649542063182u64
|
||||
"creation_time":"1649542063182",
|
||||
"last_use_time":"1649542063182"
|
||||
}
|
||||
],
|
||||
"inbound_group_sessions":[
|
||||
@@ -1172,8 +1010,7 @@ mod tests {
|
||||
"JGgPQRuYj3ScMdPS+A0P+k/1qS9Hr3qeKXLscI+hS78"
|
||||
);
|
||||
|
||||
let room_keys =
|
||||
machine.runtime.block_on(machine.inner.store().export_room_keys(|_| true))?;
|
||||
let room_keys = machine.runtime.block_on(machine.inner.export_room_keys(|_| true))?;
|
||||
assert_eq!(room_keys.len(), 2);
|
||||
|
||||
let cross_signing_status = machine.cross_signing_status();
|
||||
@@ -1188,8 +1025,6 @@ mod tests {
|
||||
assert_eq!(
|
||||
Some(RoomSettings {
|
||||
algorithm: EventEncryptionAlgorithm::OlmV1Curve25519AesSha2,
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
encrypt_state_events: false,
|
||||
only_allow_trusted_devices: true
|
||||
}),
|
||||
settings1
|
||||
@@ -1199,8 +1034,6 @@ mod tests {
|
||||
assert_eq!(
|
||||
Some(RoomSettings {
|
||||
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2,
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
encrypt_state_events: false,
|
||||
only_allow_trusted_devices: false
|
||||
}),
|
||||
settings2
|
||||
|
||||
@@ -3,11 +3,11 @@ use std::{
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use tracing_subscriber::{EnvFilter, fmt::MakeWriter};
|
||||
use tracing_subscriber::{fmt::MakeWriter, EnvFilter};
|
||||
|
||||
/// Trait that can be used to forward Rust logs over FFI to a language specific
|
||||
/// logger.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait Logger: Send {
|
||||
/// Called every time the Rust side wants to post a log line.
|
||||
fn log(&self, log_line: String);
|
||||
@@ -42,7 +42,7 @@ pub struct LoggerWrapper {
|
||||
}
|
||||
|
||||
/// Set the logger that should be used to forward Rust logs over FFI.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
pub fn set_logger(logger: Box<dyn Logger>) {
|
||||
let logger = LoggerWrapper { inner: Arc::new(Mutex::new(logger)) };
|
||||
|
||||
|
||||
@@ -10,22 +10,17 @@ use std::{
|
||||
use js_int::UInt;
|
||||
use matrix_sdk_common::deserialized_responses::AlgorithmInfo;
|
||||
use matrix_sdk_crypto::{
|
||||
CollectStrategy, DecryptionSettings, LocalTrust, OlmMachine as InnerMachine,
|
||||
UserIdentity as SdkUserIdentity,
|
||||
backups::{
|
||||
MegolmV1BackupKey as RustBackupKey, SignatureState,
|
||||
SignatureVerification as RustSignatureCheckResult,
|
||||
},
|
||||
decrypt_room_key_export, encrypt_room_key_export,
|
||||
olm::ExportedRoomKey,
|
||||
store::types::{BackupDecryptionKey, Changes},
|
||||
types::requests::ToDeviceRequest,
|
||||
store::{BackupDecryptionKey, Changes},
|
||||
LocalTrust, OlmMachine as InnerMachine, UserIdentities,
|
||||
};
|
||||
use ruma::{
|
||||
DeviceKeyAlgorithm, EventId, OneTimeKeyAlgorithm, OwnedTransactionId, OwnedUserId, RoomId,
|
||||
UserId,
|
||||
api::{
|
||||
IncomingResponse,
|
||||
client::{
|
||||
backup::add_backup_keys::v3::Response as KeysBackupResponse,
|
||||
keys::{
|
||||
@@ -35,35 +30,33 @@ use ruma::{
|
||||
upload_signatures::v3::Response as SignatureUploadResponse,
|
||||
},
|
||||
message::send_message_event::v3::Response as RoomMessageResponse,
|
||||
sync::sync_events::{DeviceLists as RumaDeviceLists, v3::ToDevice},
|
||||
sync::sync_events::{v3::ToDevice, DeviceLists as RumaDeviceLists},
|
||||
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
|
||||
},
|
||||
IncomingResponse,
|
||||
},
|
||||
events::{
|
||||
AnyMessageLikeEvent, AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
|
||||
key::verification::VerificationMethod, room::message::MessageType,
|
||||
key::verification::VerificationMethod, room::message::MessageType, AnyMessageLikeEvent,
|
||||
AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
to_device::DeviceIdOrAllDevices,
|
||||
DeviceKeyAlgorithm, EventId, OwnedTransactionId, OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, value::RawValue};
|
||||
use serde_json::{value::RawValue, Value};
|
||||
use tokio::runtime::Runtime;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{
|
||||
dehydrated_devices::DehydratedDevices,
|
||||
error::{CryptoStoreError, DecryptionError, SecretImportError, SignatureError},
|
||||
parse_user_id,
|
||||
responses::{response_from_string, OwnedResponse},
|
||||
BackupKeys, BackupRecoveryKey, BootstrapCrossSigningResult, CrossSigningKeyExport,
|
||||
CrossSigningStatus, DecodeError, DecryptedEvent, Device, DeviceLists, EncryptionSettings,
|
||||
EventEncryptionAlgorithm, KeyImportError, KeysImportResult, MegolmV1BackupKey,
|
||||
ProgressListener, Request, RequestType, RequestVerificationResult, RoomKeyCounts, RoomSettings,
|
||||
Sas, SignatureUploadRequest, StartSasResult, UserIdentity, Verification, VerificationRequest,
|
||||
dehydrated_devices::DehydratedDevices,
|
||||
error::{
|
||||
CryptoStoreError, DecryptionError, SecretImportError, SecretsBundleExportError,
|
||||
SignatureError,
|
||||
},
|
||||
parse_user_id,
|
||||
responses::{OwnedResponse, response_from_string},
|
||||
};
|
||||
|
||||
/// The return value for the [`OlmMachine::receive_sync_changes()`] method.
|
||||
@@ -100,8 +93,8 @@ pub struct RoomKeyInfo {
|
||||
pub session_id: String,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::store::types::RoomKeyInfo> for RoomKeyInfo {
|
||||
fn from(value: matrix_sdk_crypto::store::types::RoomKeyInfo) -> Self {
|
||||
impl From<matrix_sdk_crypto::store::RoomKeyInfo> for RoomKeyInfo {
|
||||
fn from(value: matrix_sdk_crypto::store::RoomKeyInfo) -> Self {
|
||||
Self {
|
||||
algorithm: value.algorithm.to_string(),
|
||||
room_id: value.room_id.to_string(),
|
||||
@@ -183,7 +176,7 @@ impl From<RustSignatureCheckResult> for SignatureVerification {
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
impl OlmMachine {
|
||||
/// Create a new `OlmMachine`
|
||||
///
|
||||
@@ -214,12 +207,8 @@ impl OlmMachine {
|
||||
|
||||
passphrase.zeroize();
|
||||
|
||||
let inner = runtime.block_on(InnerMachine::with_store(
|
||||
&user_id,
|
||||
device_id,
|
||||
Arc::new(store),
|
||||
None,
|
||||
))?;
|
||||
let inner =
|
||||
runtime.block_on(InnerMachine::with_store(&user_id, device_id, Arc::new(store)))?;
|
||||
|
||||
Ok(Arc::new(OlmMachine { inner: ManuallyDrop::new(inner), runtime }))
|
||||
}
|
||||
@@ -257,12 +246,12 @@ impl OlmMachine {
|
||||
///
|
||||
/// * `user_id` - The unique id of the user that the identity belongs to
|
||||
///
|
||||
/// * `timeout` - The time in seconds we should wait before returning if the
|
||||
/// user's device list has been marked as stale. Passing a 0 as the
|
||||
/// timeout means that we won't wait at all. **Note**, this assumes that
|
||||
/// the requests from [`OlmMachine::outgoing_requests`] are being
|
||||
/// processed and sent out. Namely, this waits for a `/keys/query`
|
||||
/// response to be received.
|
||||
/// * `timeout` - The time in seconds we should wait before returning if
|
||||
/// the user's device list has been marked as stale. Passing a 0 as the
|
||||
/// timeout means that we won't wait at all. **Note**, this assumes that
|
||||
/// the requests from [`OlmMachine::outgoing_requests`] are being processed
|
||||
/// and sent out. Namely, this waits for a `/keys/query` response to be
|
||||
/// received.
|
||||
pub fn get_identity(
|
||||
&self,
|
||||
user_id: String,
|
||||
@@ -291,7 +280,10 @@ impl OlmMachine {
|
||||
if let Some(identity) =
|
||||
self.runtime.block_on(self.inner.get_identity(&user_id, None))?
|
||||
{
|
||||
identity.is_verified()
|
||||
match identity {
|
||||
UserIdentities::Own(i) => i.is_verified(),
|
||||
UserIdentities::Other(i) => i.is_verified(),
|
||||
}
|
||||
} else {
|
||||
false
|
||||
},
|
||||
@@ -319,8 +311,8 @@ impl OlmMachine {
|
||||
|
||||
if let Some(user_identity) = user_identity {
|
||||
Ok(match user_identity {
|
||||
SdkUserIdentity::Own(i) => self.runtime.block_on(i.verify())?,
|
||||
SdkUserIdentity::Other(i) => self.runtime.block_on(i.verify())?,
|
||||
UserIdentities::Own(i) => self.runtime.block_on(i.verify())?,
|
||||
UserIdentities::Other(i) => self.runtime.block_on(i.verify())?,
|
||||
}
|
||||
.into())
|
||||
} else {
|
||||
@@ -336,12 +328,12 @@ impl OlmMachine {
|
||||
///
|
||||
/// * `device_id` - The id of the device itself.
|
||||
///
|
||||
/// * `timeout` - The time in seconds we should wait before returning if the
|
||||
/// user's device list has been marked as stale. Passing a 0 as the
|
||||
/// timeout means that we won't wait at all. **Note**, this assumes that
|
||||
/// the requests from [`OlmMachine::outgoing_requests`] are being
|
||||
/// processed and sent out. Namely, this waits for a `/keys/query`
|
||||
/// response to be received.
|
||||
/// * `timeout` - The time in seconds we should wait before returning if
|
||||
/// the user's device list has been marked as stale. Passing a 0 as the
|
||||
/// timeout means that we won't wait at all. **Note**, this assumes that
|
||||
/// the requests from [`OlmMachine::outgoing_requests`] are being processed
|
||||
/// and sent out. Namely, this waits for a `/keys/query` response to be
|
||||
/// received.
|
||||
pub fn get_device(
|
||||
&self,
|
||||
user_id: String,
|
||||
@@ -358,7 +350,7 @@ impl OlmMachine {
|
||||
.map(|d| d.into()))
|
||||
}
|
||||
|
||||
/// Manually verify the device of the given user with the given device ID.
|
||||
/// Manually the device of the given user with the given device ID.
|
||||
///
|
||||
/// This method will attempt to sign the device using our private cross
|
||||
/// signing key.
|
||||
@@ -419,12 +411,12 @@ impl OlmMachine {
|
||||
///
|
||||
/// * `user_id` - The id of the device owner.
|
||||
///
|
||||
/// * `timeout` - The time in seconds we should wait before returning if the
|
||||
/// user's device list has been marked as stale. Passing a 0 as the
|
||||
/// timeout means that we won't wait at all. **Note**, this assumes that
|
||||
/// the requests from [`OlmMachine::outgoing_requests`] are being
|
||||
/// processed and sent out. Namely, this waits for a `/keys/query`
|
||||
/// response to be received.
|
||||
/// * `timeout` - The time in seconds we should wait before returning if
|
||||
/// the user's device list has been marked as stale. Passing a 0 as the
|
||||
/// timeout means that we won't wait at all. **Note**, this assumes that
|
||||
/// the requests from [`OlmMachine::outgoing_requests`] are being processed
|
||||
/// and sent out. Namely, this waits for a `/keys/query` response to be
|
||||
/// received.
|
||||
pub fn get_user_devices(
|
||||
&self,
|
||||
user_id: String,
|
||||
@@ -520,7 +512,7 @@ impl OlmMachine {
|
||||
/// current sync response.
|
||||
///
|
||||
/// * `device_changes` - The list of devices that have changed in some way
|
||||
/// since the previous sync.
|
||||
/// since the previous sync.
|
||||
///
|
||||
/// * `key_counts` - The map of uploaded one-time key types and counts.
|
||||
pub fn receive_sync_changes(
|
||||
@@ -530,15 +522,14 @@ impl OlmMachine {
|
||||
key_counts: HashMap<String, i32>,
|
||||
unused_fallback_keys: Option<Vec<String>>,
|
||||
next_batch_token: String,
|
||||
decryption_settings: &DecryptionSettings,
|
||||
) -> Result<SyncChangesResult, CryptoStoreError> {
|
||||
let to_device: ToDevice = serde_json::from_str(&events)?;
|
||||
let device_changes: RumaDeviceLists = device_changes.into();
|
||||
let key_counts: BTreeMap<OneTimeKeyAlgorithm, UInt> = key_counts
|
||||
let key_counts: BTreeMap<DeviceKeyAlgorithm, UInt> = key_counts
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
OneTimeKeyAlgorithm::from(k),
|
||||
DeviceKeyAlgorithm::from(k),
|
||||
v.clamp(0, i32::MAX)
|
||||
.try_into()
|
||||
.expect("Couldn't convert key counts into an UInt"),
|
||||
@@ -546,25 +537,21 @@ impl OlmMachine {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let unused_fallback_keys: Option<Vec<OneTimeKeyAlgorithm>> =
|
||||
unused_fallback_keys.map(|u| u.into_iter().map(OneTimeKeyAlgorithm::from).collect());
|
||||
let unused_fallback_keys: Option<Vec<DeviceKeyAlgorithm>> =
|
||||
unused_fallback_keys.map(|u| u.into_iter().map(DeviceKeyAlgorithm::from).collect());
|
||||
|
||||
let (to_device_events, room_key_infos) =
|
||||
self.runtime.block_on(self.inner.receive_sync_changes(
|
||||
matrix_sdk_crypto::EncryptionSyncChanges {
|
||||
to_device_events: to_device.events,
|
||||
changed_devices: &device_changes,
|
||||
one_time_keys_counts: &key_counts,
|
||||
unused_fallback_keys: unused_fallback_keys.as_deref(),
|
||||
next_batch_token: Some(next_batch_token),
|
||||
},
|
||||
decryption_settings,
|
||||
))?;
|
||||
let (to_device_events, room_key_infos) = self.runtime.block_on(
|
||||
self.inner.receive_sync_changes(matrix_sdk_crypto::EncryptionSyncChanges {
|
||||
to_device_events: to_device.events,
|
||||
changed_devices: &device_changes,
|
||||
one_time_keys_counts: &key_counts,
|
||||
unused_fallback_keys: unused_fallback_keys.as_deref(),
|
||||
next_batch_token: Some(next_batch_token),
|
||||
}),
|
||||
)?;
|
||||
|
||||
let to_device_events = to_device_events
|
||||
.into_iter()
|
||||
.map(|event| event.to_raw().json().get().to_owned())
|
||||
.collect();
|
||||
let to_device_events =
|
||||
to_device_events.into_iter().map(|event| event.json().get().to_owned()).collect();
|
||||
let room_key_infos = room_key_infos.into_iter().map(|info| info.into()).collect();
|
||||
|
||||
Ok(SyncChangesResult { to_device_events, room_key_infos })
|
||||
@@ -616,7 +603,7 @@ impl OlmMachine {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `users` - The list of users for which we would like to establish 1:1
|
||||
/// Olm sessions for.
|
||||
/// Olm sessions for.
|
||||
pub fn get_missing_sessions(
|
||||
&self,
|
||||
users: Vec<String>,
|
||||
@@ -737,11 +724,11 @@ impl OlmMachine {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The unique id of the room, note that this doesn't strictly
|
||||
/// need to be a Matrix room, it just needs to be an unique identifier for
|
||||
/// the group that will participate in the conversation.
|
||||
/// need to be a Matrix room, it just needs to be an unique identifier for
|
||||
/// the group that will participate in the conversation.
|
||||
///
|
||||
/// * `users` - The list of users which are considered to be members of the
|
||||
/// room and should receive the room key.
|
||||
/// room and should receive the room key.
|
||||
///
|
||||
/// * `settings` - The settings that should be used for the room key.
|
||||
pub fn share_room_key(
|
||||
@@ -805,63 +792,12 @@ impl OlmMachine {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
let content = serde_json::from_str(&content)?;
|
||||
|
||||
let result = self
|
||||
let encrypted_content = self
|
||||
.runtime
|
||||
.block_on(self.inner.encrypt_room_event_raw(&room_id, &event_type, &content))
|
||||
.expect("Encrypting an event produced an error");
|
||||
|
||||
Ok(serde_json::to_string(&result.content)?)
|
||||
}
|
||||
|
||||
/// Encrypt the given event with the given type and content for the given
|
||||
/// device. This method is used to send an event to a specific device.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user who owns the target device.
|
||||
/// * `device_id` - The ID of the device to which the message will be sent.
|
||||
/// * `event_type` - The event type.
|
||||
/// * `content` - The serialized content of the event.
|
||||
///
|
||||
/// # Returns
|
||||
/// A `Result` containing the request to be sent out if the encryption was
|
||||
/// successful. If the device is not found, the result will be `Ok(None)`.
|
||||
///
|
||||
/// The caller should ensure that there is an olm session (see
|
||||
/// `get_missing_sessions`) with the target device before calling this
|
||||
/// method.
|
||||
pub fn create_encrypted_to_device_request(
|
||||
&self,
|
||||
user_id: String,
|
||||
device_id: String,
|
||||
event_type: String,
|
||||
content: String,
|
||||
share_strategy: CollectStrategy,
|
||||
) -> Result<Option<Request>, CryptoStoreError> {
|
||||
let user_id = parse_user_id(&user_id)?;
|
||||
let device_id = device_id.as_str().into();
|
||||
let content = serde_json::from_str(&content)?;
|
||||
|
||||
let device = self.runtime.block_on(self.inner.get_device(&user_id, device_id, None))?;
|
||||
|
||||
if let Some(device) = device {
|
||||
let encrypted_content = self.runtime.block_on(device.encrypt_event_raw(
|
||||
&event_type,
|
||||
&content,
|
||||
share_strategy,
|
||||
))?;
|
||||
|
||||
let request = ToDeviceRequest::new(
|
||||
user_id.as_ref(),
|
||||
DeviceIdOrAllDevices::DeviceId(device_id.to_owned()),
|
||||
"m.room.encrypted",
|
||||
encrypted_content.cast(),
|
||||
);
|
||||
|
||||
Ok(Some(request.into()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
Ok(serde_json::to_string(&encrypted_content)?)
|
||||
}
|
||||
|
||||
/// Decrypt the given event that was sent in the given room.
|
||||
@@ -872,26 +808,15 @@ impl OlmMachine {
|
||||
///
|
||||
/// * `room_id` - The unique id of the room where the event was sent to.
|
||||
///
|
||||
/// * `handle_verification_events` - if the supplied event is a verification
|
||||
/// event, use it to update the verification state. **Note**: it is
|
||||
/// recommended to avoid setting this flag to true and use the explicit
|
||||
/// [`OlmMachine::receive_verification_event`] method instead:
|
||||
/// verification events sometimes need preparation before we can handle
|
||||
/// them: see the documentation for
|
||||
/// [`OlmMachine::receive_verification_event`].
|
||||
///
|
||||
/// * `strict_shields` - If `true`, messages will be decorated with strict
|
||||
/// warnings (use `false` to match legacy behaviour where unsafe keys have
|
||||
/// lower severity warnings and unverified identities are not decorated).
|
||||
///
|
||||
/// * `decryption_settings` - The setting for decrypting messages.
|
||||
pub fn decrypt_room_event(
|
||||
&self,
|
||||
event: String,
|
||||
room_id: String,
|
||||
handle_verification_events: bool,
|
||||
strict_shields: bool,
|
||||
decryption_settings: DecryptionSettings,
|
||||
) -> Result<DecryptedEvent, DecryptionError> {
|
||||
// Element Android wants only the content and the type and will create a
|
||||
// decrypted event with those two itself, this struct makes sure we
|
||||
@@ -907,52 +832,47 @@ impl OlmMachine {
|
||||
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,
|
||||
&decryption_settings,
|
||||
))?;
|
||||
let decrypted = self.runtime.block_on(self.inner.decrypt_room_event(&event, &room_id))?;
|
||||
|
||||
if handle_verification_events
|
||||
&& let Ok(AnyTimelineEvent::MessageLike(e)) = decrypted.event.deserialize()
|
||||
{
|
||||
match &e {
|
||||
AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original(original_event)) => {
|
||||
if let MessageType::VerificationRequest(_) = &original_event.content.msgtype {
|
||||
if handle_verification_events {
|
||||
if let Ok(AnyTimelineEvent::MessageLike(e)) = decrypted.event.deserialize() {
|
||||
match &e {
|
||||
AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original(
|
||||
original_event,
|
||||
)) => {
|
||||
if let MessageType::VerificationRequest(_) = &original_event.content.msgtype
|
||||
{
|
||||
self.runtime.block_on(self.inner.receive_verification_event(&e))?;
|
||||
}
|
||||
}
|
||||
_ if e.event_type().to_string().starts_with("m.key.verification") => {
|
||||
self.runtime.block_on(self.inner.receive_verification_event(&e))?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
_ if e.event_type().to_string().starts_with("m.key.verification") => {
|
||||
self.runtime.block_on(self.inner.receive_verification_event(&e))?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
let encryption_info = decrypted.encryption_info;
|
||||
let encryption_info =
|
||||
decrypted.encryption_info.expect("Decrypted event didn't contain any encryption info");
|
||||
|
||||
let event_json: Event<'_> = serde_json::from_str(decrypted.event.json().get())?;
|
||||
|
||||
Ok(match &encryption_info.algorithm_info {
|
||||
AlgorithmInfo::MegolmV1AesSha2 {
|
||||
curve25519_key,
|
||||
sender_claimed_keys,
|
||||
session_id: _,
|
||||
} => DecryptedEvent {
|
||||
clear_event: serde_json::to_string(&event_json)?,
|
||||
sender_curve25519_key: curve25519_key.to_owned(),
|
||||
claimed_ed25519_key: sender_claimed_keys.get(&DeviceKeyAlgorithm::Ed25519).cloned(),
|
||||
forwarding_curve25519_chain: vec![],
|
||||
shield_state: if strict_shields {
|
||||
encryption_info.verification_state.to_shield_state_strict().into()
|
||||
} else {
|
||||
encryption_info.verification_state.to_shield_state_lax().into()
|
||||
},
|
||||
},
|
||||
AlgorithmInfo::OlmV1Curve25519AesSha2 { .. } => {
|
||||
// cannot happen because `decrypt_room_event` would have fail to decrypt olm for
|
||||
// a room (EventError::UnsupportedAlgorithm)
|
||||
panic!("Unsupported olm algorithm in room")
|
||||
AlgorithmInfo::MegolmV1AesSha2 { curve25519_key, sender_claimed_keys } => {
|
||||
DecryptedEvent {
|
||||
clear_event: serde_json::to_string(&event_json)?,
|
||||
sender_curve25519_key: curve25519_key.to_owned(),
|
||||
claimed_ed25519_key: sender_claimed_keys
|
||||
.get(&DeviceKeyAlgorithm::Ed25519)
|
||||
.cloned(),
|
||||
forwarding_curve25519_chain: vec![],
|
||||
shield_state: if strict_shields {
|
||||
encryption_info.verification_state.to_shield_state_strict().into()
|
||||
} else {
|
||||
encryption_info.verification_state.to_shield_state_lax().into()
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -963,7 +883,7 @@ impl OlmMachine {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event` - The undecryptable event that we would wish to request a room
|
||||
/// key for.
|
||||
/// key for.
|
||||
///
|
||||
/// * `room_id` - The id of the room the event was sent to.
|
||||
pub fn request_room_key(
|
||||
@@ -988,16 +908,16 @@ impl OlmMachine {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `passphrase` - The passphrase that should be used to encrypt the key
|
||||
/// export.
|
||||
/// export.
|
||||
///
|
||||
/// * `rounds` - The number of rounds that should be used when expanding the
|
||||
/// passphrase into an key.
|
||||
/// passphrase into an key.
|
||||
pub fn export_room_keys(
|
||||
&self,
|
||||
passphrase: String,
|
||||
rounds: i32,
|
||||
) -> Result<String, CryptoStoreError> {
|
||||
let keys = self.runtime.block_on(self.inner.store().export_room_keys(|_| true))?;
|
||||
let keys = self.runtime.block_on(self.inner.export_room_keys(|_| true))?;
|
||||
|
||||
let encrypted = encrypt_room_key_export(&keys, &passphrase, rounds as u32)
|
||||
.map_err(CryptoStoreError::Serialization)?;
|
||||
@@ -1014,7 +934,7 @@ impl OlmMachine {
|
||||
/// * `passphrase` - The passphrase that was used to encrypt the key export.
|
||||
///
|
||||
/// * `progress_listener` - A callback that can be used to introspect the
|
||||
/// progress of the key import.
|
||||
/// progress of the key import.
|
||||
pub fn import_room_keys(
|
||||
&self,
|
||||
keys: String,
|
||||
@@ -1023,7 +943,7 @@ impl OlmMachine {
|
||||
) -> Result<KeysImportResult, KeyImportError> {
|
||||
let keys = Cursor::new(keys);
|
||||
let keys = decrypt_room_key_export(keys, &passphrase)?;
|
||||
self.import_room_keys_helper(keys, None, progress_listener)
|
||||
self.import_room_keys_helper(keys, false, progress_listener)
|
||||
}
|
||||
|
||||
/// Import room keys from the given serialized unencrypted key export.
|
||||
@@ -1033,52 +953,22 @@ impl OlmMachine {
|
||||
/// should be used if the room keys are coming from the server-side backup,
|
||||
/// the method will mark all imported room keys as backed up.
|
||||
///
|
||||
/// **Note**: This has been deprecated. Use
|
||||
/// [`OlmMachine::import_room_keys_from_backup`] instead.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `keys` - The serialized version of the unencrypted key export.
|
||||
///
|
||||
/// * `progress_listener` - A callback that can be used to introspect the
|
||||
/// progress of the key import.
|
||||
/// progress of the key import.
|
||||
pub fn import_decrypted_room_keys(
|
||||
&self,
|
||||
keys: String,
|
||||
progress_listener: Box<dyn ProgressListener>,
|
||||
) -> Result<KeysImportResult, KeyImportError> {
|
||||
// Assume that the keys came from the current backup version.
|
||||
let backup_version = self.runtime.block_on(self.inner.backup_machine().backup_version());
|
||||
let keys: Vec<Value> = serde_json::from_str(&keys)?;
|
||||
let keys = keys.into_iter().map(serde_json::from_value).filter_map(|k| k.ok()).collect();
|
||||
self.import_room_keys_helper(keys, backup_version.as_deref(), progress_listener)
|
||||
}
|
||||
|
||||
/// Import room keys from the given serialized unencrypted key export.
|
||||
///
|
||||
/// This method is the same as [`OlmMachine::import_room_keys`] but the
|
||||
/// decryption step is skipped and should be performed by the caller. This
|
||||
/// should be used if the room keys are coming from the server-side backup.
|
||||
/// The method will mark all imported room keys as backed up.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `keys` - The serialized version of the unencrypted key export.
|
||||
///
|
||||
/// * `backup_version` - The version of the backup that these keys came
|
||||
/// from.
|
||||
///
|
||||
/// * `progress_listener` - A callback that can be used to introspect the
|
||||
/// progress of the key import.
|
||||
pub fn import_room_keys_from_backup(
|
||||
&self,
|
||||
keys: String,
|
||||
backup_version: String,
|
||||
progress_listener: Box<dyn ProgressListener>,
|
||||
) -> Result<KeysImportResult, KeyImportError> {
|
||||
let keys: Vec<Value> = serde_json::from_str(&keys)?;
|
||||
let keys = keys.into_iter().map(serde_json::from_value).filter_map(|k| k.ok()).collect();
|
||||
self.import_room_keys_helper(keys, Some(&backup_version), progress_listener)
|
||||
|
||||
self.import_room_keys_helper(keys, true, progress_listener)
|
||||
}
|
||||
|
||||
/// Discard the currently active room key for the given room if there is
|
||||
@@ -1086,7 +976,7 @@ impl OlmMachine {
|
||||
pub fn discard_room_key(&self, room_id: String) -> Result<(), CryptoStoreError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
|
||||
self.runtime.block_on(self.inner.discard_room_key(&room_id))?;
|
||||
self.runtime.block_on(self.inner.invalidate_group_session(&room_id))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1109,14 +999,6 @@ impl OlmMachine {
|
||||
///
|
||||
/// This method can be used to pass verification events that are happening
|
||||
/// in rooms to the `OlmMachine`. The event should be in the decrypted form.
|
||||
///
|
||||
/// **Note**: If the supplied event is an `m.room.message` event with
|
||||
/// `msgtype: m.key.verification.request`, then the device information for
|
||||
/// the sending user must be up-to-date before calling this method
|
||||
/// (otherwise, the request will be ignored). It is hard to guarantee this
|
||||
/// is the case, but you can maximize your chances by explicitly making a
|
||||
/// request to /keys/query for the user's device info, and processing the
|
||||
/// response with [`OlmMachine::mark_request_as_sent`].
|
||||
pub fn receive_verification_event(
|
||||
&self,
|
||||
event: String,
|
||||
@@ -1137,7 +1019,7 @@ impl OlmMachine {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user for which we would like to fetch the
|
||||
/// verification requests.
|
||||
/// verification requests.
|
||||
pub fn get_verification_requests(&self, user_id: String) -> Vec<Arc<VerificationRequest>> {
|
||||
let Ok(user_id) = UserId::parse(user_id) else {
|
||||
return vec![];
|
||||
@@ -1158,7 +1040,7 @@ impl OlmMachine {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user for which we would like to fetch the
|
||||
/// verification requests.
|
||||
/// verification requests.
|
||||
///
|
||||
/// * `flow_id` - The ID that uniquely identifies the verification flow.
|
||||
pub fn get_verification_request(
|
||||
@@ -1178,10 +1060,10 @@ impl OlmMachine {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user which we would like to request to
|
||||
/// verify.
|
||||
/// verify.
|
||||
///
|
||||
/// * `methods` - The list of verification methods we want to advertise to
|
||||
/// support.
|
||||
/// support.
|
||||
pub fn verification_request_content(
|
||||
&self,
|
||||
user_id: String,
|
||||
@@ -1194,7 +1076,8 @@ impl OlmMachine {
|
||||
let methods = methods.into_iter().map(VerificationMethod::from).collect();
|
||||
|
||||
Ok(if let Some(identity) = identity.and_then(|i| i.other()) {
|
||||
let content = identity.verification_request_content(Some(methods));
|
||||
let content =
|
||||
self.runtime.block_on(identity.verification_request_content(Some(methods)));
|
||||
Some(serde_json::to_string(&content)?)
|
||||
} else {
|
||||
None
|
||||
@@ -1207,18 +1090,18 @@ impl OlmMachine {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user which we would like to request to
|
||||
/// verify.
|
||||
/// verify.
|
||||
///
|
||||
/// * `room_id` - The ID of the room that represents a DM with the given
|
||||
/// user.
|
||||
/// user.
|
||||
///
|
||||
/// * `event_id` - The event ID of the `m.key.verification.request` event
|
||||
/// that we sent out to request the verification to begin. The content for
|
||||
/// this request can be created using the [verification_request_content()]
|
||||
/// method.
|
||||
/// that we sent out to request the verification to begin. The content for
|
||||
/// this request can be created using the [verification_request_content()]
|
||||
/// method.
|
||||
///
|
||||
/// * `methods` - The list of verification methods we advertised as
|
||||
/// supported in the `m.key.verification.request` event.
|
||||
/// supported in the `m.key.verification.request` event.
|
||||
///
|
||||
/// [verification_request_content()]: Self::verification_request_content
|
||||
pub fn request_verification(
|
||||
@@ -1237,7 +1120,11 @@ impl OlmMachine {
|
||||
let methods = methods.into_iter().map(VerificationMethod::from).collect();
|
||||
|
||||
Ok(if let Some(identity) = identity.and_then(|i| i.other()) {
|
||||
let request = identity.request_verification(&room_id, &event_id, Some(methods));
|
||||
let request = self.runtime.block_on(identity.request_verification(
|
||||
&room_id,
|
||||
&event_id,
|
||||
Some(methods),
|
||||
));
|
||||
|
||||
Some(
|
||||
VerificationRequest { inner: request, runtime: self.runtime.handle().to_owned() }
|
||||
@@ -1253,12 +1140,12 @@ impl OlmMachine {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user which we would like to request to
|
||||
/// verify.
|
||||
/// verify.
|
||||
///
|
||||
/// * `device_id` - The ID of the device that we wish to verify.
|
||||
///
|
||||
/// * `methods` - The list of verification methods we advertised as
|
||||
/// supported in the `m.key.verification.request` event.
|
||||
/// supported in the `m.key.verification.request` event.
|
||||
pub fn request_verification_with_device(
|
||||
&self,
|
||||
user_id: String,
|
||||
@@ -1274,7 +1161,8 @@ impl OlmMachine {
|
||||
if let Some(device) =
|
||||
self.runtime.block_on(self.inner.get_device(&user_id, device_id, None))?
|
||||
{
|
||||
let (verification, request) = device.request_verification_with_methods(methods);
|
||||
let (verification, request) =
|
||||
self.runtime.block_on(device.request_verification_with_methods(methods));
|
||||
|
||||
Some(RequestVerificationResult {
|
||||
verification: VerificationRequest {
|
||||
@@ -1327,7 +1215,7 @@ impl OlmMachine {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user for which we would like to fetch the
|
||||
/// verification.
|
||||
/// verification.
|
||||
///
|
||||
/// * `flow_id` - The ID that uniquely identifies the verification flow.
|
||||
pub fn get_verification(&self, user_id: String, flow_id: String) -> Option<Arc<Verification>> {
|
||||
@@ -1347,7 +1235,7 @@ impl OlmMachine {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user for which we would like to start the
|
||||
/// SAS verification.
|
||||
/// SAS verification.
|
||||
///
|
||||
/// * `device_id` - The ID of device we would like to verify.
|
||||
///
|
||||
@@ -1367,8 +1255,7 @@ impl OlmMachine {
|
||||
let (sas, request) = self.runtime.block_on(device.start_verification())?;
|
||||
|
||||
Some(StartSasResult {
|
||||
sas: Sas { inner: Box::new(sas), runtime: self.runtime.handle().to_owned() }
|
||||
.into(),
|
||||
sas: Sas { inner: sas, runtime: self.runtime.handle().to_owned() }.into(),
|
||||
request: request.into(),
|
||||
})
|
||||
} else {
|
||||
@@ -1409,24 +1296,6 @@ impl OlmMachine {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Export all the secrets we have in the store into a serialized
|
||||
/// SecretsBundle.
|
||||
///
|
||||
/// This method will export all the private cross-signing keys and, if
|
||||
/// available, the private part of a backup key and its accompanying
|
||||
/// version.
|
||||
///
|
||||
/// The method will fail if we don't have all three private cross-signing
|
||||
/// keys available.
|
||||
///
|
||||
/// **Warning**: Only export this and share it with a trusted recipient,
|
||||
/// i.e. if an existing device is sharing this with a new device.
|
||||
pub fn export_secrets_bundle(&self) -> Result<String, SecretsBundleExportError> {
|
||||
let bundle = self.runtime.block_on(self.inner.store().export_secrets_bundle())?;
|
||||
|
||||
Ok(serde_json::to_string(&bundle)?)
|
||||
}
|
||||
|
||||
/// Request missing local secrets from our devices (cross signing private
|
||||
/// keys, megolm backup). This will ask the sdk to create outgoing
|
||||
/// request to get the missing secrets.
|
||||
@@ -1589,18 +1458,16 @@ impl OlmMachine {
|
||||
fn import_room_keys_helper(
|
||||
&self,
|
||||
keys: Vec<ExportedRoomKey>,
|
||||
from_backup_version: Option<&str>,
|
||||
from_backup: bool,
|
||||
progress_listener: Box<dyn ProgressListener>,
|
||||
) -> Result<KeysImportResult, KeyImportError> {
|
||||
let listener = |progress: usize, total: usize| {
|
||||
progress_listener.on_progress(progress as i32, total as i32)
|
||||
};
|
||||
|
||||
let result = self.runtime.block_on(self.inner.store().import_room_keys(
|
||||
keys,
|
||||
from_backup_version,
|
||||
listener,
|
||||
))?;
|
||||
#[allow(deprecated)]
|
||||
let result =
|
||||
self.runtime.block_on(self.inner.import_room_keys(keys, from_backup, listener))?;
|
||||
|
||||
Ok(KeysImportResult {
|
||||
imported: result.imported_count as i64,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace matrix_sdk_crypto_ffi {};
|
||||
|
||||
enum LocalTrust {
|
||||
"Verified",
|
||||
"BlackListed",
|
||||
"Ignored",
|
||||
"Unset",
|
||||
};
|
||||
|
||||
enum SignatureState {
|
||||
"Missing",
|
||||
"Invalid",
|
||||
"ValidButNotTrusted",
|
||||
"ValidAndTrusted",
|
||||
};
|
||||
@@ -4,15 +4,11 @@ use std::collections::HashMap;
|
||||
|
||||
use http::Response;
|
||||
use matrix_sdk_crypto::{
|
||||
CrossSigningBootstrapRequests,
|
||||
types::requests::{
|
||||
AnyIncomingResponse, KeysBackupRequest, OutgoingRequest,
|
||||
OutgoingVerificationRequest as SdkVerificationRequest, RoomMessageRequest, ToDeviceRequest,
|
||||
UploadSigningKeysRequest as RustUploadSigningKeysRequest,
|
||||
},
|
||||
CrossSigningBootstrapRequests, IncomingResponse, KeysBackupRequest, OutgoingRequest,
|
||||
OutgoingVerificationRequest as SdkVerificationRequest, RoomMessageRequest, ToDeviceRequest,
|
||||
UploadSigningKeysRequest as RustUploadSigningKeysRequest,
|
||||
};
|
||||
use ruma::{
|
||||
OwnedTransactionId, UserId,
|
||||
api::client::{
|
||||
backup::add_backup_keys::v3::Response as KeysBackupResponse,
|
||||
keys::{
|
||||
@@ -28,7 +24,8 @@ use ruma::{
|
||||
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
|
||||
},
|
||||
assign,
|
||||
events::MessageLikeEventContent,
|
||||
events::EventContent,
|
||||
OwnedTransactionId, UserId,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
@@ -139,7 +136,7 @@ pub enum Request {
|
||||
|
||||
impl From<OutgoingRequest> for Request {
|
||||
fn from(r: OutgoingRequest) -> Self {
|
||||
use matrix_sdk_crypto::types::requests::AnyOutgoingRequest::*;
|
||||
use matrix_sdk_crypto::OutgoingRequests::*;
|
||||
|
||||
match r.request() {
|
||||
KeysUpload(u) => {
|
||||
@@ -167,6 +164,7 @@ impl From<OutgoingRequest> for Request {
|
||||
},
|
||||
RoomMessage(r) => Request::from(r),
|
||||
KeysClaim(c) => (r.request_id().to_owned(), c.clone()).into(),
|
||||
KeysBackup(b) => (r.request_id().to_owned(), b.clone()).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,8 +222,8 @@ impl From<&ToDeviceRequest> for Request {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Box<RoomMessageRequest>> for Request {
|
||||
fn from(r: &Box<RoomMessageRequest>) -> Self {
|
||||
impl From<&RoomMessageRequest> for Request {
|
||||
fn from(r: &RoomMessageRequest) -> Self {
|
||||
Self::RoomMessage {
|
||||
request_id: r.txn_id.to_string(),
|
||||
room_id: r.room_id.to_string(),
|
||||
@@ -341,16 +339,16 @@ impl From<RoomMessageResponse> for OwnedResponse {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a OwnedResponse> for AnyIncomingResponse<'a> {
|
||||
impl<'a> From<&'a OwnedResponse> for IncomingResponse<'a> {
|
||||
fn from(r: &'a OwnedResponse) -> Self {
|
||||
match r {
|
||||
OwnedResponse::KeysClaim(r) => AnyIncomingResponse::KeysClaim(r),
|
||||
OwnedResponse::KeysQuery(r) => AnyIncomingResponse::KeysQuery(r),
|
||||
OwnedResponse::KeysUpload(r) => AnyIncomingResponse::KeysUpload(r),
|
||||
OwnedResponse::ToDevice(r) => AnyIncomingResponse::ToDevice(r),
|
||||
OwnedResponse::SignatureUpload(r) => AnyIncomingResponse::SignatureUpload(r),
|
||||
OwnedResponse::KeysBackup(r) => AnyIncomingResponse::KeysBackup(r),
|
||||
OwnedResponse::RoomMessage(r) => AnyIncomingResponse::RoomMessage(r),
|
||||
OwnedResponse::KeysClaim(r) => IncomingResponse::KeysClaim(r),
|
||||
OwnedResponse::KeysQuery(r) => IncomingResponse::KeysQuery(r),
|
||||
OwnedResponse::KeysUpload(r) => IncomingResponse::KeysUpload(r),
|
||||
OwnedResponse::ToDevice(r) => IncomingResponse::ToDevice(r),
|
||||
OwnedResponse::SignatureUpload(r) => IncomingResponse::SignatureUpload(r),
|
||||
OwnedResponse::KeysBackup(r) => IncomingResponse::KeysBackup(r),
|
||||
OwnedResponse::RoomMessage(r) => IncomingResponse::RoomMessage(r),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use matrix_sdk_crypto::{UserIdentity as SdkUserIdentity, types::CrossSigningKey};
|
||||
use matrix_sdk_crypto::{types::CrossSigningKey, UserIdentities};
|
||||
|
||||
use crate::CryptoStoreError;
|
||||
|
||||
/// Enum representing cross signing identity of our own user or some other
|
||||
/// Enum representing cross signing identities of our own user or some other
|
||||
/// user.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum UserIdentity {
|
||||
@@ -18,8 +18,6 @@ pub enum UserIdentity {
|
||||
user_signing_key: String,
|
||||
/// The public self-signing key of our identity.
|
||||
self_signing_key: String,
|
||||
/// True if this identity was verified at some point but is not anymore.
|
||||
has_verification_violation: bool,
|
||||
},
|
||||
/// The user identity of other users.
|
||||
Other {
|
||||
@@ -29,15 +27,13 @@ pub enum UserIdentity {
|
||||
master_key: String,
|
||||
/// The public self-signing key of our identity.
|
||||
self_signing_key: String,
|
||||
/// True if this identity was verified at some point but is not anymore.
|
||||
has_verification_violation: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl UserIdentity {
|
||||
pub(crate) async fn from_rust(i: SdkUserIdentity) -> Result<Self, CryptoStoreError> {
|
||||
pub(crate) async fn from_rust(i: UserIdentities) -> Result<Self, CryptoStoreError> {
|
||||
Ok(match i {
|
||||
SdkUserIdentity::Own(i) => {
|
||||
UserIdentities::Own(i) => {
|
||||
let master: CrossSigningKey = i.master_key().as_ref().to_owned();
|
||||
let user_signing: CrossSigningKey = i.user_signing_key().as_ref().to_owned();
|
||||
let self_signing: CrossSigningKey = i.self_signing_key().as_ref().to_owned();
|
||||
@@ -48,10 +44,9 @@ impl UserIdentity {
|
||||
master_key: serde_json::to_string(&master)?,
|
||||
user_signing_key: serde_json::to_string(&user_signing)?,
|
||||
self_signing_key: serde_json::to_string(&self_signing)?,
|
||||
has_verification_violation: i.has_verification_violation(),
|
||||
}
|
||||
}
|
||||
SdkUserIdentity::Other(i) => {
|
||||
UserIdentities::Other(i) => {
|
||||
let master: CrossSigningKey = i.master_key().as_ref().to_owned();
|
||||
let self_signing: CrossSigningKey = i.self_signing_key().as_ref().to_owned();
|
||||
|
||||
@@ -59,7 +54,6 @@ impl UserIdentity {
|
||||
user_id: i.user_id().to_string(),
|
||||
master_key: serde_json::to_string(&master)?,
|
||||
self_signing_key: serde_json::to_string(&self_signing)?,
|
||||
has_verification_violation: i.has_verification_violation(),
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::{Stream, StreamExt};
|
||||
use matrix_sdk_common::executor::Handle;
|
||||
use matrix_sdk_crypto::{
|
||||
CancelInfo as RustCancelInfo, QrVerification as InnerQr, QrVerificationState, Sas as InnerSas,
|
||||
SasState as RustSasState, Verification as InnerVerification,
|
||||
VerificationRequest as InnerVerificationRequest,
|
||||
matrix_sdk_qrcode::QrVerificationData, CancelInfo as RustCancelInfo, QrVerification as InnerQr,
|
||||
QrVerificationState, Sas as InnerSas, SasState as RustSasState,
|
||||
Verification as InnerVerification, VerificationRequest as InnerVerificationRequest,
|
||||
VerificationRequestState as RustVerificationRequestState,
|
||||
matrix_sdk_qrcode::QrVerificationData,
|
||||
};
|
||||
use ruma::events::key::verification::VerificationMethod;
|
||||
use tokio::runtime::Handle;
|
||||
use vodozemac::{base64_decode, base64_encode};
|
||||
|
||||
use crate::{CryptoStoreError, OutgoingVerificationRequest, SignatureUploadRequest};
|
||||
|
||||
/// Listener that will be passed over the FFI to report changes to a SAS
|
||||
/// verification.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait SasListener: Send {
|
||||
/// The callback that should be called on the Rust side
|
||||
///
|
||||
@@ -29,11 +28,8 @@ pub trait SasListener: Send {
|
||||
/// An Enum describing the state the SAS verification is in.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum SasState {
|
||||
/// The verification has been created, the protocols that should be used
|
||||
/// have been proposed to the other party.
|
||||
Created,
|
||||
/// The verification has been started, the other party proposed the
|
||||
/// protocols that should be used and that can be accepted.
|
||||
/// The verification has been started, the protocols that should be used
|
||||
/// have been proposed and can be accepted.
|
||||
Started,
|
||||
/// The verification has been accepted and both sides agreed to a set of
|
||||
/// protocols that will be used for the verification process.
|
||||
@@ -62,7 +58,6 @@ pub enum SasState {
|
||||
impl From<RustSasState> for SasState {
|
||||
fn from(s: RustSasState) -> Self {
|
||||
match s {
|
||||
RustSasState::Created { .. } => Self::Created,
|
||||
RustSasState::Started { .. } => Self::Started,
|
||||
RustSasState::Accepted { .. } => Self::Accepted,
|
||||
RustSasState::KeysExchanged { emojis, decimals } => Self::KeysExchanged {
|
||||
@@ -83,13 +78,13 @@ pub struct Verification {
|
||||
pub(crate) runtime: Handle,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
impl Verification {
|
||||
/// Try to represent the `Verification` as an `Sas` verification object,
|
||||
/// returns `None` if the verification is not a `Sas` verification.
|
||||
pub fn as_sas(&self) -> Option<Arc<Sas>> {
|
||||
if let InnerVerification::SasV1(sas) = &self.inner {
|
||||
Some(Sas { inner: sas.clone(), runtime: self.runtime.to_owned() }.into())
|
||||
Some(Sas { inner: sas.to_owned(), runtime: self.runtime.to_owned() }.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -99,7 +94,7 @@ impl Verification {
|
||||
/// returns `None` if the verification is not a `QrCode` verification.
|
||||
pub fn as_qr(&self) -> Option<Arc<QrCode>> {
|
||||
if let InnerVerification::QrV1(qr) = &self.inner {
|
||||
Some(QrCode { inner: qr.clone(), runtime: self.runtime.to_owned() }.into())
|
||||
Some(QrCode { inner: qr.to_owned(), runtime: self.runtime.to_owned() }.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -109,11 +104,11 @@ impl Verification {
|
||||
/// The `m.sas.v1` verification flow.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Sas {
|
||||
pub(crate) inner: Box<InnerSas>,
|
||||
pub(crate) inner: InnerSas,
|
||||
pub(crate) runtime: Handle,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
impl Sas {
|
||||
/// Get the user id of the other side.
|
||||
pub fn other_user_id(&self) -> String {
|
||||
@@ -170,8 +165,8 @@ impl Sas {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cancel_code` - The error code for why the verification was cancelled,
|
||||
/// manual cancellatio usually happens with `m.user` cancel code. The full
|
||||
/// list of cancel codes can be found in the [spec]
|
||||
/// manual cancellatio usually happens with `m.user` cancel code. The full
|
||||
/// list of cancel codes can be found in the [spec]
|
||||
///
|
||||
/// [spec]: https://spec.matrix.org/unstable/client-server-api/#mkeyverificationcancel
|
||||
pub fn cancel(&self, cancel_code: String) -> Option<OutgoingVerificationRequest> {
|
||||
@@ -210,7 +205,7 @@ impl Sas {
|
||||
///
|
||||
/// # Flowchart
|
||||
///
|
||||
/// The flow of the verification process is pictured below. Please note
|
||||
/// The flow of the verification process is pictured bellow. Please note
|
||||
/// that the process can be cancelled at each step of the process.
|
||||
/// Either side can cancel the process.
|
||||
///
|
||||
@@ -277,7 +272,7 @@ impl Sas {
|
||||
|
||||
/// Listener that will be passed over the FFI to report changes to a QrCode
|
||||
/// verification.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait QrCodeListener: Send {
|
||||
/// The callback that should be called on the Rust side
|
||||
///
|
||||
@@ -325,11 +320,11 @@ impl From<QrVerificationState> for QrCodeState {
|
||||
/// verification flow.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct QrCode {
|
||||
pub(crate) inner: Box<InnerQr>,
|
||||
pub(crate) inner: InnerQr,
|
||||
pub(crate) runtime: Handle,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
impl QrCode {
|
||||
/// Get the user id of the other side.
|
||||
pub fn other_user_id(&self) -> String {
|
||||
@@ -392,8 +387,8 @@ impl QrCode {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cancel_code` - The error code for why the verification was cancelled,
|
||||
/// manual cancellatio usually happens with `m.user` cancel code. The full
|
||||
/// list of cancel codes can be found in the [spec]
|
||||
/// manual cancellatio usually happens with `m.user` cancel code. The full
|
||||
/// list of cancel codes can be found in the [spec]
|
||||
///
|
||||
/// [spec]: https://spec.matrix.org/unstable/client-server-api/#mkeyverificationcancel
|
||||
pub fn cancel(&self, cancel_code: String) -> Option<OutgoingVerificationRequest> {
|
||||
@@ -523,7 +518,7 @@ pub struct ConfirmVerificationResult {
|
||||
|
||||
/// Listener that will be passed over the FFI to report changes to a
|
||||
/// verification request.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait VerificationRequestListener: Send {
|
||||
/// The callback that should be called on the Rust side
|
||||
///
|
||||
@@ -563,7 +558,7 @@ pub struct VerificationRequest {
|
||||
pub(crate) runtime: Handle,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
impl VerificationRequest {
|
||||
/// The id of the other user that is participating in this verification
|
||||
/// request.
|
||||
@@ -641,12 +636,12 @@ impl VerificationRequest {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user for which we would like to accept the
|
||||
/// verification requests.
|
||||
/// verification requests.
|
||||
///
|
||||
/// * `flow_id` - The ID that uniquely identifies the verification flow.
|
||||
///
|
||||
/// * `methods` - A list of verification methods that we want to advertise
|
||||
/// as supported.
|
||||
/// as supported.
|
||||
pub fn accept(&self, methods: Vec<String>) -> Option<OutgoingVerificationRequest> {
|
||||
let methods = methods.into_iter().map(VerificationMethod::from).collect();
|
||||
self.inner.accept_with_methods(methods).map(|r| r.into())
|
||||
@@ -664,13 +659,13 @@ impl VerificationRequest {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user for which we would like to start the
|
||||
/// SAS verification.
|
||||
/// SAS verification.
|
||||
///
|
||||
/// * `flow_id` - The ID of the verification request that initiated the
|
||||
/// verification flow.
|
||||
/// verification flow.
|
||||
pub fn start_sas_verification(&self) -> Result<Option<StartSasResult>, CryptoStoreError> {
|
||||
Ok(self.runtime.block_on(self.inner.start_sas())?.map(|(sas, r)| StartSasResult {
|
||||
sas: Arc::new(Sas { inner: Box::new(sas), runtime: self.runtime.clone() }),
|
||||
sas: Arc::new(Sas { inner: sas, runtime: self.runtime.clone() }),
|
||||
request: r.into(),
|
||||
}))
|
||||
}
|
||||
@@ -682,16 +677,16 @@ impl VerificationRequest {
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user for which we would like to start the QR
|
||||
/// code verification.
|
||||
/// * `user_id` - The ID of the user for which we would like to start the
|
||||
/// QR code verification.
|
||||
///
|
||||
/// * `flow_id` - The ID of the verification request that initiated the
|
||||
/// verification flow.
|
||||
/// verification flow.
|
||||
pub fn start_qr_verification(&self) -> Result<Option<Arc<QrCode>>, CryptoStoreError> {
|
||||
Ok(self
|
||||
.runtime
|
||||
.block_on(self.inner.generate_qr_code())?
|
||||
.map(|qr| QrCode { inner: Box::new(qr), runtime: self.runtime.clone() }.into()))
|
||||
.map(|qr| QrCode { inner: qr, runtime: self.runtime.clone() }.into()))
|
||||
}
|
||||
|
||||
/// Pass data from a scanned QR code to an active verification request and
|
||||
@@ -702,14 +697,14 @@ impl VerificationRequest {
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user for which we would like to start the QR
|
||||
/// code verification.
|
||||
/// * `user_id` - The ID of the user for which we would like to start the
|
||||
/// QR code verification.
|
||||
///
|
||||
/// * `flow_id` - The ID of the verification request that initiated the
|
||||
/// verification flow.
|
||||
/// verification flow.
|
||||
///
|
||||
/// * `data` - The data that was extracted from the scanned QR code as an
|
||||
/// base64 encoded string, without padding.
|
||||
/// base64 encoded string, without padding.
|
||||
pub fn scan_qr_code(&self, data: String) -> Option<ScanResult> {
|
||||
let data = base64_decode(data).ok()?;
|
||||
let data = QrVerificationData::from_bytes(data).ok()?;
|
||||
@@ -718,7 +713,7 @@ impl VerificationRequest {
|
||||
let request = qr.reciprocate()?;
|
||||
|
||||
Some(ScanResult {
|
||||
qr: QrCode { inner: Box::new(qr), runtime: self.runtime.clone() }.into(),
|
||||
qr: QrCode { inner: qr, runtime: self.runtime.clone() }.into(),
|
||||
request: request.into(),
|
||||
})
|
||||
} else {
|
||||
@@ -753,7 +748,7 @@ impl VerificationRequest {
|
||||
RustVerificationRequestState::Ready {
|
||||
their_methods,
|
||||
our_methods,
|
||||
other_device_data: _,
|
||||
other_device_id: _,
|
||||
} => VerificationRequestState::Ready {
|
||||
their_methods: their_methods.iter().map(|m| m.to_string()).collect(),
|
||||
our_methods: our_methods.iter().map(|m| m.to_string()).collect(),
|
||||
@@ -792,7 +787,8 @@ impl VerificationRequest {
|
||||
// task.
|
||||
let should_break = matches!(
|
||||
state,
|
||||
RustVerificationRequestState::Done | RustVerificationRequestState::Cancelled { .. }
|
||||
RustVerificationRequestState::Done { .. }
|
||||
| RustVerificationRequestState::Cancelled { .. }
|
||||
);
|
||||
|
||||
let state = Self::convert_verification_request(&request, state);
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
uniffi::uniffi_bindgen_main()
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
[package]
|
||||
description = "Helper macros to write FFI bindings"
|
||||
edition = "2024"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ruma"]
|
||||
license = "Apache-2.0"
|
||||
name = "matrix-sdk-ffi-macros"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version.workspace = true
|
||||
version = "0.7.0"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
test = false
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = { workspace = true , features = ["proc-macro"] }
|
||||
quote.workspace = true
|
||||
syn = { workspace = true, features = ["full", "extra-traits", "proc-macro"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
@@ -1,14 +0,0 @@
|
||||
[](https://travis-ci.org/matrix-org/matrix-rust-sdk)
|
||||
[](https://codecov.io/gh/matrix-org/matrix-rust-sdk)
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://matrix.to/#/#matrix-rust-sdk:matrix.org)
|
||||
|
||||
# matrix-sdk-ffi-macros
|
||||
|
||||
Internal macros used for the FFI layer (bindings) of the Rust Matrix SDK.
|
||||
|
||||
**NOTE:** These are just macros that help build the matrix-rust-sdk bindings, you're probably
|
||||
interested in the main [rust-sdk](https://github.com/matrix-org/matrix-rust-sdk/) crate.
|
||||
|
||||
[Matrix]: https://matrix.org/
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
@@ -1,70 +0,0 @@
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{ImplItem, Item, TraitItem};
|
||||
|
||||
/// Attribute to specify the async runtime parameter for the `uniffi`
|
||||
/// export macros if there any `async fn`s in the input.
|
||||
#[proc_macro_attribute]
|
||||
pub fn export(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let has_async_fn = |item| {
|
||||
if let Item::Fn(fun) = &item {
|
||||
if fun.sig.asyncness.is_some() {
|
||||
return true;
|
||||
}
|
||||
} else if let Item::Impl(blk) = &item {
|
||||
for item in &blk.items {
|
||||
if let ImplItem::Fn(fun) = item
|
||||
&& fun.sig.asyncness.is_some()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if let Item::Trait(blk) = &item {
|
||||
for item in &blk.items {
|
||||
if let TraitItem::Fn(fun) = item
|
||||
&& fun.sig.asyncness.is_some()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
};
|
||||
|
||||
let attr2 = proc_macro2::TokenStream::from(attr);
|
||||
let item2 = proc_macro2::TokenStream::from(item.clone());
|
||||
|
||||
let res = match syn::parse(item) {
|
||||
Ok(item) => match has_async_fn(item) {
|
||||
true => {
|
||||
quote! {
|
||||
#[cfg_attr(target_family = "wasm", uniffi::export(#attr2))]
|
||||
#[cfg_attr(not(target_family = "wasm"), uniffi::export(async_runtime = "tokio", #attr2))]
|
||||
}
|
||||
}
|
||||
false => quote! { #[uniffi::export(#attr2)] },
|
||||
},
|
||||
Err(e) => e.into_compile_error(),
|
||||
};
|
||||
|
||||
quote! {
|
||||
#res
|
||||
#item2
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@@ -1,583 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
<!-- next-header -->
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Add `Client::set_avatar_url` to manually set the avatar URL of the user to a provided MXC one.
|
||||
- Allow setting a custom Sliding Sync connection ID and timeline limit on `RoomListService`.
|
||||
([#6289](https://github.com/matrix-org/matrix-rust-sdk/pull/6289))
|
||||
- Fix devices on Android 11 crashing because the SDK could not be initialized using `libloading`
|
||||
to get a reference to the JVM. Replaced `libloading` with `jvm-getter`, which works like a
|
||||
compatibility layer. ([#6370](https://github.com/matrix-org/matrix-rust-sdk/pull/6370))
|
||||
- Added `android_platform.rs` for fixing the `rustls` integration on Android, which was broken.
|
||||
([#6306](https://github.com/matrix-org/matrix-rust-sdk/pull/6306))
|
||||
- [**breaking**] `OtherState` properly supports redacted events that still have fields in the
|
||||
content. The following fields are no longer optional:
|
||||
- `federate` in `OtherState::RoomCreate`.
|
||||
- `history_visibility` in `OtherState::RoomHistoryVisibility`.
|
||||
- `thresholds` in `OtherState::RoomPowerLevels`.
|
||||
- `omit_checksums` option is now enabled for the Kotlin bindings in all FFI-exporting crates.
|
||||
We enabled them because with JNA direct mapping enabled they result in invalid checks in
|
||||
ARM 32bit devices, preventing the SDK from working altogether (see
|
||||
[this issue](https://github.com/mozilla/uniffi-rs/issues/2740)).
|
||||
([#6069](https://github.com/matrix-org/matrix-rust-sdk/pull/6069),
|
||||
[#6112](https://github.com/matrix-org/matrix-rust-sdk/pull/6112),
|
||||
[#6115](https://github.com/matrix-org/matrix-rust-sdk/pull/6115),
|
||||
[#6116](https://github.com/matrix-org/matrix-rust-sdk/pull/6116)).
|
||||
- `Client::create_room` now uses `RoomPowerLevelsContentOverride` under the hood instead of
|
||||
`RoomPowerLevelsEventContent` to be able to explicitly set values which would previously be
|
||||
ignored if they matched the default power level values specified by the spec: these may not be
|
||||
the same in the homeserver and result in rooms with incorrect power levels being created.
|
||||
([#6034](https://github.com/matrix-org/matrix-rust-sdk/pull/6034))
|
||||
- Fix the `is_last_admin` check in `LeaveSpaceRoom` since it was not
|
||||
accounting for the membership state.
|
||||
[#6032](https://github.com/matrix-org/matrix-rust-sdk/pull/6032)
|
||||
- [**breaking**] `LatestEventValue::Local { is_sending: bool }` is replaced
|
||||
by [`state: LatestEventValueLocalState`] to represent 3 states: `IsSending`,
|
||||
`HasBeenSent` and `CannotBeSent`.
|
||||
([#5968](https://github.com/matrix-org/matrix-rust-sdk/pull/5968/))
|
||||
|
||||
### Features
|
||||
|
||||
- Add `Client::get_dm_rooms` function to get a list with the DMs for the provided user id.
|
||||
([#6487](https://github.com/matrix-org/matrix-rust-sdk/pull/6487))
|
||||
- Expose `ffi::NotificationRoomInfo::service_members` so clients can use the list of service
|
||||
members to calculate if a room is a DM from the notification info.
|
||||
([#6474](https://github.com/matrix-org/matrix-rust-sdk/pull/6474))
|
||||
- Enable `experimental-push-secrets` feature by default.
|
||||
([#6473](https://github.com/matrix-org/matrix-rust-sdk/pull/6394))
|
||||
- Add new high-level search helpers `RoomSearchIterator` and `GlobalSearchIterator` to perform
|
||||
searches for messages in a room or across all rooms.
|
||||
([6394](https://github.com/matrix-org/matrix-rust-sdk/pull/6394))
|
||||
- Added the `Client.request_openid_token()` method.
|
||||
([#6458](https://github.com/matrix-org/matrix-rust-sdk/pull/6458))
|
||||
- Added the `Client::import_secrets_bundle` method.
|
||||
([#6212](https://github.com/matrix-org/matrix-rust-sdk/pull/6212))
|
||||
- [**breaking**] Remove support for `native-tls` and remove all feature
|
||||
flags for selecting TLS backend, as `rustls` is the now the only supported
|
||||
TLS backend.
|
||||
([#6409](https://github.com/matrix-org/matrix-rust-sdk/pull/6409))
|
||||
- Expose `event_type_raw` and `latest_json()` on `EventTimelineItem`,
|
||||
allowing clients to access the raw event type string and full event JSON for
|
||||
custom event handling without pattern-matching through nested enums.
|
||||
([#6387](https://github.com/matrix-org/matrix-rust-sdk/pull/6387))
|
||||
([#6424](https://github.com/matrix-org/matrix-rust-sdk/pull/6424))
|
||||
- Expose sync v2 API through FFI via `Client.sync_v2()` and
|
||||
`Client.sync_once_v2()`, enabling mobile clients to sync without
|
||||
requiring Sliding Sync support on the homeserver. `Client.sync_v2()`
|
||||
accepts a `SyncListenerV2` callback that receives a `SyncResponseV2`
|
||||
after each successful sync.
|
||||
([#6359](https://github.com/matrix-org/matrix-rust-sdk/pull/6359))
|
||||
- Added `HomeserverCapabilities` and `Client::homeserver_capabilities()` to get the capabilities
|
||||
of the homeserver. ([#6371](https://github.com/matrix-org/matrix-rust-sdk/pull/6371))
|
||||
- Expose `Room.send_state_event_raw()` for sending arbitrary state events
|
||||
through the FFI layer.
|
||||
([#6350](https://github.com/matrix-org/matrix-rust-sdk/pull/6350))
|
||||
- Introduce a `ThreadListService` which offers reactive interfaces for rendering
|
||||
and managing the list of threads from a particular room.
|
||||
([6311](https://github.com/matrix-org/matrix-rust-sdk/pull/6311))
|
||||
- [**breaking**] Move `LiveLocation` out of `TimelineItemContent` and into `MsgLikeKind`
|
||||
so it has access to `MsgLikeContent` `reactions`.
|
||||
([#6286](https://github.com/matrix-org/matrix-rust-sdk/pull/6286))
|
||||
- Add `HumanQrLoginError::UnsupportedQrCodeType` for when a QR is parseable but cannot be used to
|
||||
complete a login.
|
||||
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6285)
|
||||
- Add `HumanQrGrantLoginError::UnsupportedQrCodeType` for when a QR is parseable but cannot be used
|
||||
to grant a login.
|
||||
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6285)
|
||||
- Add the `QrCodeData::base_url` and `QrCodeData::intent` methods.
|
||||
([#6283](https://github.com/matrix-org/matrix-rust-sdk/pull/6283))
|
||||
- Add `Encryption::recover_and_fix_backup` to automatically fix key storage backup if the
|
||||
private backup decryption key is missing, invalid or inconsistent with the public key.
|
||||
([#6252](https://github.com/matrix-org/matrix-rust-sdk/pull/6252))
|
||||
- Add support for [MSC3489](https://github.com/matrix-org/matrix-spec-proposals/pull/3489)
|
||||
live location sharing through a new `TimelineItemContent::LiveLocation` variant.
|
||||
([#6232](https://github.com/matrix-org/matrix-rust-sdk/pull/6232))
|
||||
- Add `HumanQrGrantLoginError::ConnectionInsecure` for errors establishing the secure channel
|
||||
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6141)
|
||||
- Add `HumanQrGrantLoginError::Expired` for when a timeout is encountered during the grant
|
||||
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6141)
|
||||
- Add `HumanQrGrantLoginError::Cancelled` for when the grant is cancelled
|
||||
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6141)
|
||||
- Add `HumanQrGrantLoginError::OtherDeviceAlreadySignedIn` for when the other device is already signed in
|
||||
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6141)
|
||||
- Add `HumanQrGrantLoginError::DeviceNotFound` for when the requested device was not returned by the homeserver
|
||||
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6141)
|
||||
- Add `RoomInfo::is_low_priority` for getting the room's `m.lowpriority` tag state
|
||||
([#6183](https://github.com/matrix-org/matrix-rust-sdk/pull/6183))
|
||||
- Add `Client::subscribe_to_duplicate_key_upload_errors` for listening to duplicate key
|
||||
upload errors from `/keys/upload`.
|
||||
([#6135](https://github.com/matrix-org/matrix-rust-sdk/pull/6135/))
|
||||
- Add `NotificationItem::raw_event` to get the raw event content of the event that triggered the notification, which can be useful for debugging and to support clients that want to implement custom handling for certain notifications. ([#6122](https://github.com/matrix-org/matrix-rust-sdk/pull/6122))
|
||||
- [**breaking**] Extend `TimelineFocus::Event` to allow marking the target
|
||||
event as the root of a thread.
|
||||
[#6050](https://github.com/matrix-org/matrix-rust-sdk/pull/6050)
|
||||
- [**breaking**] Remove `TimelineFilter::EventTypeFilter` which has been replaced by
|
||||
the more generic `TimelineFilter::EventFilter`. Users of `TimelineEventTypeFilter::include`
|
||||
and `TimelineEventTypeFilter::exclude` can switch to `TimelineEventFilter::include_event_types`
|
||||
and `TimelineEventFilter::exclude_event_types`.
|
||||
([#6070](https://github.com/matrix-org/matrix-rust-sdk/pull/6070/))
|
||||
- Add `TimelineFilter::EventFilter` for filtering events based on their type or
|
||||
content. For content filtering, only membership and profile change filters
|
||||
are available as of now.
|
||||
([#6048](https://github.com/matrix-org/matrix-rust-sdk/pull/6048/))
|
||||
- Introduce `SpaceFilter`s as a mechanism for narrowing down what's displayed in
|
||||
the room list ([#6025](https://github.com/matrix-org/matrix-rust-sdk/pull/6025))
|
||||
- Expose room power level thresholds in `OtherState::RoomPowerLevels` (ban, kick, invite, redact, state &
|
||||
events defaults, per-event overrides, notifications), so clients can compute the required power level
|
||||
for actions and compare with previous values. ([#5931](https://github.com/matrix-org/matrix-rust-sdk/pull/5931))
|
||||
- Add `RoomCreationParameters::is_space` parameter to be able to create spaces. ([#6010](https://github.com/matrix-org/matrix-rust-sdk/pull/6010/))
|
||||
- [**breaking**] `LazyTimelineItemProvider::get_shields` no longer returns an
|
||||
an `Option`: the `ShieldState` type contains a `None` variant, so the
|
||||
`Option` was redundant. The `message` field has also been removed: since there
|
||||
was no way to localise the returned string, applications should not be using it.
|
||||
([#5959](https://github.com/matrix-org/matrix-rust-sdk/pull/5959))
|
||||
- Add `Room::list_threads` to list all the threads in a room.
|
||||
([#5953](https://github.com/matrix-org/matrix-rust-sdk/pull/5953))
|
||||
- Add `SpaceService::get_space_room` to get a space given its id from the space graph if available.
|
||||
[#5944](https://github.com/matrix-org/matrix-rust-sdk/pull/5944)
|
||||
- Add `QrCodeData::to_bytes()` to allow generation of a QR code.
|
||||
([#5939](https://github.com/matrix-org/matrix-rust-sdk/pull/5939))
|
||||
- [**breaking**]: The new Latest Event API replaces the old API.
|
||||
`Room::new_latest_event` overwrites the `Room::latest_event` method. See the
|
||||
documentation of `matrix_sdk::latest_event` to learn about the new API.
|
||||
[#5624](https://github.com/matrix-org/matrix-rust-sdk/pull/5624/)
|
||||
- Created `RoomPowerLevels::events` function which returns a `HashMap<TimelineEventType, i64>` with all the power
|
||||
levels per event type. ([#5937](https://github.com/matrix-org/matrix-rust-sdk/pull/5937))
|
||||
- Expose `EventTimelineItem::forwarder` and `forwarder_profile`, which, if present, provide the ID and profile of
|
||||
the user who forwarded the keys used to decrypt the event as part of an [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268)
|
||||
key bundle.
|
||||
([#6000](https://github.com/matrix-org/matrix-rust-sdk/pull/6000))
|
||||
- Add `NonFavorite` filter to the Room List API. ([#5991](https://github.com/matrix-org/matrix-rust-sdk/pull/5991)
|
||||
- Add `call_intent` (either `RtcCallIntent::Audio` or `RtcCallIntent::Video`) field to `RtcNotification` event content. ([#6207](https://github.com/matrix-org/matrix-rust-sdk/pull/6207))
|
||||
- Add `RoomInfo::active_room_call_consensus_intent` method to get the call intent for the current call,
|
||||
based on what members are advertising.
|
||||
([#6274](https://github.com/matrix-org/matrix-rust-sdk/pull/6274))
|
||||
|
||||
### Refactor
|
||||
|
||||
- [**breaking**] `Room::observe_live_location_shares` has been replaced by
|
||||
`Room::live_location_shares`. Call [`LiveLocationShares::subscribe`] on it to
|
||||
receive an initial snapshot and a stream of incremental updates.The stream is seeded from the event cache
|
||||
on creation and includes the own user's shares (previously excluded). `LiveLocationShare.is_live`
|
||||
has been removed; instead `ts` (start timestamp) and `timeout` (duration in milliseconds) are now
|
||||
exposed so clients can compute liveness themselves via `current_time < ts + timeout`. Non-live
|
||||
shares are automatically removed from the list. A new `LiveLocationShareListener` callback
|
||||
interface must be implemented and passed to the method.
|
||||
([#6385](https://github.com/matrix-org/matrix-rust-sdk/pull/6385))
|
||||
- [**breaking**] The `RoomAliases` variants of `StateEventContent`, `StateEventType` and
|
||||
`OtherState` was removed. This state event type was removed from the Matrix specification a while
|
||||
ago, and support for it has been removed in Ruma.
|
||||
([#6414](https://github.com/matrix-org/matrix-rust-sdk/pull/6414))
|
||||
- `Client::new` no longer unnecessarily instantiates an `OAuth` component if `CrossProcessLockConfig::SingleProcess`
|
||||
is used. ([#6293](https://github.com/matrix-org/matrix-rust-sdk/pull/6293))
|
||||
- [**breaking**] `Room::report_content()` no longer takes a `score` argument, because it was
|
||||
removed from the Matrix specification.
|
||||
([#6256](https://github.com/matrix-org/matrix-rust-sdk/pull/6256))
|
||||
- [**breaking**] The `current_version` field of `ErrorKind::WrongRoomKeysVersion`
|
||||
is no longer optional.
|
||||
([#6241](https://github.com/matrix-org/matrix-rust-sdk/pull/6241))
|
||||
- [**breaking**] The following variants of `AccountManagementAction` were
|
||||
renamed to match their new names after being merge in the Matrix specification:
|
||||
- `SessionsList` is renamed to `DevicesList`
|
||||
- `SessionView` is renamed to `DeviceView`
|
||||
- `SessionEnd` is renamed to `DeviceDelete`
|
||||
([#6217](https://github.com/matrix-org/matrix-rust-sdk/pull/6217))
|
||||
- [**breaking**] `HumanQrGrantLoginError::UnableToCreateDevice` has been removed
|
||||
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6141)
|
||||
- [**breaking**] Removed `ClientBuilder::enable_oidc_refresh_lock` in favour of using `ClientBuilder::cross_process_lock_config`
|
||||
to configure that lock when a `MultiProcess` configuration is supplied. ([#6204](https://github.com/matrix-org/matrix-rust-sdk/pull/6204))
|
||||
- `RoomPaginationStatus` is renamed to `PaginationStatus`.
|
||||
([#6174](https://github.com/matrix-org/matrix-rust-sdk/pull/6174/))
|
||||
- [**breaking**] Replaced `ClientBuilder::cross_process_store_locks_holder_name` with `ClientBuilder::cross_process_lock_config`,
|
||||
which accepts a `CrossProcessLockConfig` value to specify whether the resulting `Client` will be used in a single
|
||||
process or multiple processes. ([#6160](https://github.com/matrix-org/matrix-rust-sdk/pull/6160))
|
||||
- [**breaking**] Refactored `is_last_admin` to `is_last_owner` the check will now
|
||||
account also for v12 rooms, where creators and users with PL 150 matter.
|
||||
([#6036](https://github.com/matrix-org/matrix-rust-sdk/pull/6036))
|
||||
- [**breaking**] The existing `TimelineEventType` was renamed to `TimelineEventContent`, because it contained the
|
||||
actual contents of the event. Then, we created a new `TimelineEventType` enum that actually contains *just* the
|
||||
event type. ([#5937](https://github.com/matrix-org/matrix-rust-sdk/pull/5937))
|
||||
- [**breaking**] The function `TimelineEvent::event_type` is now `TimelineEvent::content`.
|
||||
([#5937](https://github.com/matrix-org/matrix-rust-sdk/pull/5937))
|
||||
- [**breaking**] The `SpaceService` will no longer auto-subscribe to required
|
||||
client events when invoking the `subscribe_to_joined_spaces` but instead do it
|
||||
through its, now async, constructor.
|
||||
([#5972](https://github.com/matrix-org/matrix-rust-sdk/pull/5972))
|
||||
- [**breaking**] The `SpaceService`'s `joined_spaces` method has been renamed
|
||||
`top_level_joined_spaces` and `subscribe_to_joined_spaces` to `space_service.subscribe_to_top_level_joined_spaces`
|
||||
([#5972](https://github.com/matrix-org/matrix-rust-sdk/pull/5972))
|
||||
|
||||
## [0.16.0] - 2025-12-04
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- `TimelineConfiguration::track_read_receipts`'s type is now an enum to allow tracking to be enabled for all events
|
||||
(like before) or only for message-like events (which prevents read receipts from being placed on state events).
|
||||
([#5900](https://github.com/matrix-org/matrix-rust-sdk/pull/5900))
|
||||
- `Client::reset_server_info()` has been split into `reset_supported_versions()`
|
||||
and `reset_well_known()`.
|
||||
([#5910](https://github.com/matrix-org/matrix-rust-sdk/pull/5910))
|
||||
- Add `HumanQrLoginError::NotFound` for non-existing / expired rendezvous sessions
|
||||
([#5898](https://github.com/matrix-org/matrix-rust-sdk/pull/5898))
|
||||
- Add `HumanQrGrantLoginError::NotFound` for non-existing / expired rendezvous sessions
|
||||
([#5898](https://github.com/matrix-org/matrix-rust-sdk/pull/5898))
|
||||
- The `LatestEventValue::Local` type gains 2 new fields: `sender` and `profile`.
|
||||
([#5885](https://github.com/matrix-org/matrix-rust-sdk/pull/5885))
|
||||
- The `Encryption::user_identity()` method has received a new argument. The
|
||||
`fallback_to_server` argument controls if we should attempt to fetch the user
|
||||
identity from the homeserver if it wasn't found in the local storage.
|
||||
([#5870](https://github.com/matrix-org/matrix-rust-sdk/pull/5870))
|
||||
- Expose the power level required to modify `m.space.child` on
|
||||
`room::power_levels::RoomPowerLevelsValues`.
|
||||
- Rename `Client::login_with_qr_code` to `Client::new_login_with_qr_code_handler`.
|
||||
([#5836](https://github.com/matrix-org/matrix-rust-sdk/pull/5836))
|
||||
- Add the `sqlite` feature, along with the `indexeddb` feature, to enable either
|
||||
the SQLite or IndexedDB store. The `session_paths`, `session_passphrase`,
|
||||
`session_pool_max_size`, `session_cache_size` and `session_journal_size_limit`
|
||||
methods on `ClientBuilder` have been removed. New methods are added:
|
||||
`ClientBuilder::in_memory_store` if one wants non-persistent stores,
|
||||
`ClientBuilder::sqlite_store` to configure and to use SQLite stores (if
|
||||
the `sqlite` feature is enabled), and `ClientBuilder::indexeddb_store` to
|
||||
configure and to use IndexedDB stores (if the `indexeddb` feature is enabled).
|
||||
([#5811](https://github.com/matrix-org/matrix-rust-sdk/pull/5811))
|
||||
|
||||
The code:
|
||||
|
||||
```rust
|
||||
client_builder
|
||||
.session_paths("data_path", "cache_path")
|
||||
.passphrase("foobar")
|
||||
```
|
||||
|
||||
now becomes:
|
||||
|
||||
```rust
|
||||
client_builder
|
||||
.sqlite_store(
|
||||
SqliteSessionStoreBuilder::new("data_path", "cache_path")
|
||||
.passphrase("foobar")
|
||||
)
|
||||
```
|
||||
|
||||
- UniFFI was upgraded to `v0.30.0` ([#5808](https://github.com/matrix-org/matrix-rust-sdk/pull/5808)).
|
||||
- The `waveform` parameter in `Timeline::send_voice_message` format changed to a list of `f32`
|
||||
between 0 and 1.
|
||||
([#5732](https://github.com/matrix-org/matrix-rust-sdk/pull/5732))
|
||||
- The `normalized_power_level` field has been removed from the `RoomMember`
|
||||
struct.
|
||||
([#5635](https://github.com/matrix-org/matrix-rust-sdk/pull/5635))
|
||||
- Remove the deprecated `CallNotify` event (`org.matrix.msc4075.call.notify`) in favor of the new
|
||||
`RtcNotification` event (`org.matrix.msc4075.rtc.notification`).
|
||||
([#5668](https://github.com/matrix-org/matrix-rust-sdk/pull/5668))
|
||||
- Add `QrLoginProgress::SyncingSecrets` to indicate that secrets are being synced between the two
|
||||
devices.
|
||||
([#5760](https://github.com/matrix-org/matrix-rust-sdk/pull/5760))
|
||||
- Add `Room::subscribe_to_send_queue_updates` to observe room send queue updates.
|
||||
([#5761](https://github.com/matrix-org/matrix-rust-sdk/pull/5761))
|
||||
- `Client::login_with_qr_code` now returns a handler that allows performing the flow with either the
|
||||
current device scanning or generating the QR code. Additionally, new errors `HumanQrLoginError::CheckCodeAlreadySent`
|
||||
and `HumanQrLoginError::CheckCodeCannotBeSent` were added.
|
||||
([#5786](https://github.com/matrix-org/matrix-rust-sdk/pull/5786))
|
||||
- `ComposerDraft` now includes attachments alongside the text message.
|
||||
([#5794](https://github.com/matrix-org/matrix-rust-sdk/pull/5794))
|
||||
- Add `Client::subscribe_to_send_queue_updates` to observe global send queue updates.
|
||||
([#5784](https://github.com/matrix-org/matrix-rust-sdk/pull/5784))
|
||||
|
||||
### Features
|
||||
|
||||
- Add `Client::get_store_sizes()` so to query the size of the existing stores, if available. ([#5911](https://github.com/matrix-org/matrix-rust-sdk/pull/5911))
|
||||
- Expose `is_space` in `NotificationRoomInfo`, allowing clients to determine if the room that triggered the notification is a space.
|
||||
- Add push actions to `NotificationItem` and replace `SyncNotification` with `NotificationItem`.
|
||||
([#5835](https://github.com/matrix-org/matrix-rust-sdk/pull/5835))
|
||||
- Add `Client::new_grant_login_with_qr_code_handler` for granting login to a new device by way of
|
||||
a QR code.
|
||||
([#5836](https://github.com/matrix-org/matrix-rust-sdk/pull/5836))
|
||||
- Add `Client::register_notification_handler` for observing notifications generated from sync responses.
|
||||
([#5831](https://github.com/matrix-org/matrix-rust-sdk/pull/5831))
|
||||
- Add `Room::mark_as_fully_read_unchecked` so clients can mark a room as read without needing a `Timeline` instance. Note this method is not recommended as it can potentially cause incorrect read receipts, but it can needed in certain cases.
|
||||
- Add `Timeline::latest_event_id` to be able to fetch the event id of the latest event of the timeline.
|
||||
- Add `Room::load_or_fetch_event` so we can get a `TimelineEvent` given its event id ([#5678](https://github.com/matrix-org/matrix-rust-sdk/pull/5678)).
|
||||
- Add `TimelineEvent::thread_root_event_id` to expose the thread root event id for this type too ([#5678](https://github.com/matrix-org/matrix-rust-sdk/pull/5678)).
|
||||
- Add `NotificationSettings::get_raw_push_rules` so clients can fetch the raw JSON content of the push rules of the current user and include it in bug reports ([#5706](https://github.com/matrix-org/matrix-rust-sdk/pull/5706)).
|
||||
- Add new API to decline calls ([MSC4310](https://github.com/matrix-org/matrix-spec-proposals/pull/4310)): `Room::decline_call` and `Room::subscribe_to_call_decline_events`
|
||||
([#5614](https://github.com/matrix-org/matrix-rust-sdk/pull/5614))
|
||||
- Expose `m.federate` in `OtherState::RoomCreate` and `history_visibility` in `OtherState::RoomHistoryVisibility`, allowing clients to know whether a room federates and how its history is shared in the appropriate timeline events.
|
||||
- Expose `join_rule` in `OtherState::RoomJoinRules`, allowing clients to know the join rules of a room from the appropriate timeline events.
|
||||
|
||||
### Changes
|
||||
|
||||
- `Timeline::latest_event_id` now uses its `ui::Timeline::latest_event_id` counterpart, instead of getting the latest event from the timeline and then its id.([#5864](https://github.com/matrix-org/matrix-rust-sdk/pull/5864))
|
||||
- Build Android ARM64 bindings using better default RUSTFLAGS (the same used for iOS ARM64). This should improve performance. [(#5854)](https://github.com/matrix-org/matrix-rust-sdk/pull/5854)
|
||||
|
||||
## [0.14.0] - 2025-09-04
|
||||
|
||||
### Features:
|
||||
|
||||
- Add `LowPriority` and `NonLowPriority` variants to `RoomListEntriesDynamicFilterKind` for filtering
|
||||
rooms based on their low priority status. These filters allow clients to show only low priority rooms
|
||||
or exclude low priority rooms from the room list.
|
||||
([#5508](https://github.com/matrix-org/matrix-rust-sdk/pull/5508))
|
||||
- Add `room_version` and `privileged_creators_role` to `RoomInfo` ([#5449](https://github.com/matrix-org/matrix-rust-sdk/pull/5449)).
|
||||
- The [`unstable-hydra`] feature has been enabled, which enables room v12 changes in the SDK.
|
||||
([#5450](https://github.com/matrix-org/matrix-rust-sdk/pull/5450)).
|
||||
- Add experimental support for
|
||||
[MSC4306](https://github.com/matrix-org/matrix-spec-proposals/pull/4306), with the
|
||||
`Room::fetch_thread_subscription()` and `Room::set_thread_subscription()` methods.
|
||||
([#5442](https://github.com/matrix-org/matrix-rust-sdk/pull/5442))
|
||||
- [**breaking**] [`GalleryUploadParameters::reply`] and [`UploadParameters::reply`] have been both
|
||||
replaced with a new optional `in_reply_to` field, that's a string which will be parsed into an
|
||||
`OwnedEventId` when sending the event. The thread relationship will be automatically filled in,
|
||||
based on the timeline focus.
|
||||
([5427](https://github.com/matrix-org/matrix-rust-sdk/pull/5427))
|
||||
- [**breaking**] [`Timeline::send_reply()`] now automatically fills in the thread relationship,
|
||||
based on the timeline focus. As a result, it only takes an `OwnedEventId` parameter, instead of
|
||||
the `Reply` type. The proper way to start a thread is now thus to create a threaded-focused
|
||||
timeline, and then use `Timeline::send()`.
|
||||
([5427](https://github.com/matrix-org/matrix-rust-sdk/pull/5427))
|
||||
- Add `HomeserverLoginDetails::supports_sso_login` for legacy SSO support information.
|
||||
This is primarily for Element X to give a dedicated error message in case
|
||||
it connects a homeserver with only this method available.
|
||||
([#5222](https://github.com/matrix-org/matrix-rust-sdk/pull/5222))
|
||||
|
||||
### Breaking changes:
|
||||
|
||||
- The timeline will now always use the send queue to upload medias, so the
|
||||
`UploadParameters::use_send_queue` bool has been removed. Make sure to listen to the send queue's
|
||||
error updates, and to handle send queue restarts.
|
||||
([#5525](https://github.com/matrix-org/matrix-rust-sdk/pull/5525))
|
||||
- Support for the legacy media upload progress has been disabled. Media upload progress is
|
||||
available through the send queue, and can be enabled thanks to
|
||||
`Client::enable_send_queue_upload_progress()`.
|
||||
([#5525](https://github.com/matrix-org/matrix-rust-sdk/pull/5525))
|
||||
- `TimelineDiff` is now exported as a true `uniffi::Enum` instead of the weird `uniffi::Object` hybrid. This matches
|
||||
both `RoomDirectorySearchEntryUpdate` and `RoomListEntriesUpdate` and can be used in the same way.
|
||||
([#5474](https://github.com/matrix-org/matrix-rust-sdk/pull/5474))
|
||||
- The `creator` field of `RoomInfo` has been renamed to `creators` and can now contain a list of
|
||||
user IDs, to reflect that a room can now have several creators, as introduced in room version 12.
|
||||
([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436))
|
||||
- The `PowerLevel` type was introduced to represent power levels instead of `i64` to differentiate
|
||||
the infinite power level of creators, as introduced in room version 12. It is used in
|
||||
`suggested_role_for_power_level`, `suggested_power_level_for_role` and `RoomMember`.
|
||||
([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436))
|
||||
- `Client::get_url` now returns a `Vec<u8>` instead of a `String`. It also throws an error when the
|
||||
response isn't status code 200 OK, instead of providing the error in the response body.
|
||||
([#5438](https://github.com/matrix-org/matrix-rust-sdk/pull/5438))
|
||||
- `RoomPreview::info()` doesn't return a result anymore. All unknown join rules are handled in the
|
||||
`JoinRule::Custom` variant.
|
||||
([#5337](https://github.com/matrix-org/matrix-rust-sdk/pull/5337))
|
||||
- The `reason` argument of `Room::report_room` is now required, do to a clarification in the spec.
|
||||
([#5337](https://github.com/matrix-org/matrix-rust-sdk/pull/5337))
|
||||
- `PublicRoomJoinRule` has more variants, supporting all the known values from the spec.
|
||||
([#5337](https://github.com/matrix-org/matrix-rust-sdk/pull/5337))
|
||||
- The fields of `MediaPreviewConfig` are both optional, allowing to use the type for room account
|
||||
data as well as global account data.
|
||||
([#5337](https://github.com/matrix-org/matrix-rust-sdk/pull/5337))
|
||||
- The `event_id` field of `PredecessorRoom` was removed, due to its removal in the Matrix
|
||||
specification with MSC4291.
|
||||
([#5419](https://github.com/matrix-org/matrix-rust-sdk/pull/5419))
|
||||
- `Client::url_for_oidc` now allows requesting additional scopes for the OAuth2 authorization code grant.
|
||||
([#5395](https://github.com/matrix-org/matrix-rust-sdk/pull/5395))
|
||||
- `Client::url_for_oidc` now allows passing an optional existing device id from a previous login call.
|
||||
([#5394](https://github.com/matrix-org/matrix-rust-sdk/pull/5394))
|
||||
- `ClientBuilder::build_with_qr_code` has been removed. Instead, the Client should be built by passing
|
||||
`QrCodeData::server_name` to `ClientBuilder::server_name_or_homeserver_url`, after which QR login can be performed by
|
||||
calling `Client::login_with_qr_code`. ([#5388](https://github.com/matrix-org/matrix-rust-sdk/pull/5388))
|
||||
- The MSRV has been bumped to Rust 1.88.
|
||||
([#5431](https://github.com/matrix-org/matrix-rust-sdk/pull/5431))
|
||||
- `Room::send_call_notification` and `Room::send_call_notification_if_needed` have been removed, since the event type they send is outdated, and `Client` is not actually supposed to be able to join MatrixRTC sessions (yet). In practice, users of these methods probably already rely on another MatrixRTC implementation to participate in sessions, and such an implementation should be capable of sending notifications itself.
|
||||
- The `GalleryItemInfo` variants now take an `UploadSource` rather than a `String` path to enable uploading
|
||||
from bytes directly.
|
||||
([#5529](https://github.com/matrix-org/matrix-rust-sdk/pull/5529))
|
||||
- Media and gallery uploads now use `UploadSource` to specify the thumbnail.
|
||||
([#5530](https://github.com/matrix-org/matrix-rust-sdk/pull/5530))
|
||||
|
||||
## [0.13.0] - 2025-07-10
|
||||
|
||||
### Features
|
||||
|
||||
- Add `NotificationRoomInfo::topic` to the `NotificationRoomInfo` struct, which
|
||||
contains the topic of the room. This is useful for displaying the room topic
|
||||
in notifications. ([#5300](https://github.com/matrix-org/matrix-rust-sdk/pull/5300))
|
||||
- Add `EmbeddedEventDetails::timestamp` and `EmbeddedEventDetails::event_or_transaction_id`
|
||||
which are already available in regular timeline items.
|
||||
([#5331](https://github.com/matrix-org/matrix-rust-sdk/pull/5331))
|
||||
- `RoomListService::subscribe_to_rooms` becomes `async` and automatically calls
|
||||
`matrix_sdk::latest_events::LatestEvents::listen_to_room`
|
||||
([#5369](https://github.com/matrix-org/matrix-rust-sdk/pull/5369))
|
||||
|
||||
### Refactor
|
||||
|
||||
- Adjust features in the `matrix-sdk-ffi` crate to expose more platform-specific knobs.
|
||||
Previously the `matrix-sdk-ffi` was configured primarily by target configs, choosing
|
||||
between the tls flavor (`rustls-tls` or `native-tls`) and features like `sentry` based
|
||||
purely on the target. As we work to add an additional Wasm target to this crate,
|
||||
the cross product of target specific features has become somewhat chaotic, and we
|
||||
have shifted to externalize these choices as feature flags.
|
||||
|
||||
To maintain existing compatibility on the major platforms, these features should be used:
|
||||
Android: `"bundled-sqlite,unstable-msc4274,rustls-tls,sentry"`
|
||||
iOS: `"bundled-sqlite,unstable-msc4274,native-tls,sentry"`
|
||||
Javascript/Wasm: `"unstable-msc4274,native-tls"`
|
||||
|
||||
In the future additional choices (such as session storage, `sqlite` and `indexeddb`)
|
||||
will likely be added as well.
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- `Client::reset_server_capabilities` has been renamed to `Client::reset_server_info`.
|
||||
([#5167](https://github.com/matrix-org/matrix-rust-sdk/pull/5167))
|
||||
- `RoomPreview::join_rule`, `NotificationItem::join_rule`, `RoomInfo::is_public`, and
|
||||
`Room::is_public()` return values are now optional. They will be set to `None` if the join rule
|
||||
state event is missing for a given room. `NotificationRoomInfo::is_public` has been removed;
|
||||
callers can inspect the value of `NotificationItem::join_rule` to determine if the room is public
|
||||
(i.e. if the join rule is `Public`).
|
||||
([#5278](https://github.com/matrix-org/matrix-rust-sdk/pull/5278))
|
||||
|
||||
## [0.12.0] - 2025-06-10
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- `Client::send_call_notification_if_needed` now returns `Result<bool>` instead of `Result<()>` so we can check if
|
||||
the event was sent.
|
||||
- `Client::upload_avatar` and `Timeline::send_attachment` now may fail if a file too large for the homeserver media
|
||||
config is uploaded.
|
||||
- `UploadParameters` replaces field `filename: String` with `source: UploadSource`.
|
||||
`UploadSource` is an enum which may take a filename or a filename and bytes, which
|
||||
allows a foreign language to read file contents natively and then pass those contents to
|
||||
the foreign function when uploading a file through the `Timeline`.
|
||||
([#4948](https://github.com/matrix-org/matrix-rust-sdk/pull/4948))
|
||||
- `RoomInfo` replaces its field `is_tombstoned: bool` with `tombstone: Option<RoomTombstoneInfo>`,
|
||||
containing the data needed to implement the room migration UI, a message and the replacement room id.
|
||||
([#5027](https://github.com/matrix-org/matrix-rust-sdk/pull/5027))
|
||||
|
||||
Additions:
|
||||
|
||||
- `Client::subscribe_to_room_info` allows clients to subscribe to room info updates in rooms which may not be known yet.
|
||||
This is useful when displaying a room preview for an unknown room, so when we receive any membership change for it,
|
||||
we can automatically update the UI.
|
||||
- `Client::get_max_media_upload_size` to get the max size of a request sent to the homeserver so we can tweak our media
|
||||
uploads by compressing/transcoding the media.
|
||||
- Add `ClientBuilder::enable_share_history_on_invite` to enable experimental support for sharing encrypted room history
|
||||
on invite, per [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268).
|
||||
([#5141](https://github.com/matrix-org/matrix-rust-sdk/pull/5141))
|
||||
- Support for adding a Sentry layer to the FFI bindings has been added. Only `tracing` statements with
|
||||
the field `sentry=true` will be forwarded to Sentry, in addition to default Sentry filters.
|
||||
- Add room topic string to `StateEventContent`
|
||||
- Add `UploadSource` for representing upload data - this is analogous to `matrix_sdk_ui::timeline::AttachmentSource`
|
||||
- Add `Client::observe_account_data_event` and `Client::observe_room_account_data_event` to
|
||||
subscribe to global and room account data changes.
|
||||
([#4994](https://github.com/matrix-org/matrix-rust-sdk/pull/4994))
|
||||
- Add `Timeline::send_gallery` to send MSC4274-style galleries.
|
||||
([#5163](https://github.com/matrix-org/matrix-rust-sdk/pull/5163))
|
||||
- Add `reply_params` to `GalleryUploadParameters` to allow sending galleries as (threaded) replies.
|
||||
([#5173](https://github.com/matrix-org/matrix-rust-sdk/pull/5173))
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- `contacts` has been removed from `OidcConfiguration` (it was unused since the switch to OAuth).
|
||||
|
||||
## [0.11.0] - 2025-04-11
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- `TracingConfiguration` now includes a new field `trace_log_packs`, which gives a convenient way
|
||||
to set the TRACE log level for multiple targets related to a given feature.
|
||||
([#4824](https://github.com/matrix-org/matrix-rust-sdk/pull/4824))
|
||||
|
||||
- `setup_tracing` has been renamed `init_platform`; in addition to the `TracingConfiguration`
|
||||
parameter it also now takes a boolean indicating whether to spawn a minimal tokio runtime for the
|
||||
application; in general for main app processes this can be set to `false`, and memory-constrained
|
||||
programs can set it to `true`.
|
||||
|
||||
- Matrix client API errors coming from API responses will now be mapped to `ClientError::MatrixApi`, containing both the
|
||||
original message and the associated error code and kind.
|
||||
|
||||
- `EventSendState` now has two additional variants: `CrossSigningNotSetup` and
|
||||
`SendingFromUnverifiedDevice`. These indicate that your own device is not
|
||||
properly cross-signed, which is a requirement when using the identity-based
|
||||
strategy, and can only be returned when using the identity-based strategy.
|
||||
|
||||
In addition, the `VerifiedUserHasUnsignedDevice` and
|
||||
`VerifiedUserChangedIdentity` variants can be returned when using the
|
||||
identity-based strategy, in addition to when using the device-based strategy
|
||||
with `error_on_verified_user_problem` is set.
|
||||
|
||||
- `EventSendState` now has two additional variants: `VerifiedUserHasUnsignedDevice` and
|
||||
`VerifiedUserChangedIdentity`. These reflect problems with verified users in the room
|
||||
and as such can only be returned when the room key recipient strategy has
|
||||
`error_on_verified_user_problem` set.
|
||||
|
||||
- The `AuthenticationService` has been removed:
|
||||
- Instead of calling `configure_homeserver`, build your own client with the `serverNameOrHomeserverUrl` builder
|
||||
method to keep the same behaviour.
|
||||
- The parts of `AuthenticationError` related to discovery will be represented in the `ClientBuildError` returned
|
||||
when calling `build()`.
|
||||
- The remaining methods can be found on the built `Client`.
|
||||
- There is a new `abortOidcLogin` method that should be called if the webview is dismissed without a callback (
|
||||
or fails to present).
|
||||
- The rest of `AuthenticationError` is now found in the OidcError type.
|
||||
|
||||
- `OidcAuthenticationData` is now called `OidcAuthorizationData`.
|
||||
|
||||
- The `get_element_call_required_permissions` function now requires the device_id.
|
||||
|
||||
- Some `OidcPrompt` cases have been removed (`None`, `SelectAccount`).
|
||||
|
||||
- `Room::is_encrypted` is replaced by `Room::latest_encryption_state`
|
||||
which returns a value of the new `EncryptionState` enum; another
|
||||
`Room::encryption_state` non-async and infallible method is added to get the
|
||||
`EncryptionState` without running a network request.
|
||||
([#4777](https://github.com/matrix-org/matrix-rust-sdk/pull/4777)). One can
|
||||
safely replace:
|
||||
|
||||
```rust
|
||||
room.is_encrypted().await?
|
||||
```
|
||||
|
||||
by
|
||||
|
||||
```rust
|
||||
room.latest_encryption_state().await?.is_encrypted()
|
||||
```
|
||||
|
||||
- `ClientBuilder::passphrase` is renamed `session_passphrase`
|
||||
([#4870](https://github.com/matrix-org/matrix-rust-sdk/pull/4870/))
|
||||
|
||||
- Merge `Timeline::send_thread_reply` into `Timeline::send_reply`. This
|
||||
changes the parameters of `send_reply` which now requires passing the
|
||||
event ID (and thread reply behaviour) inside a `ReplyParameters` struct.
|
||||
([#4880](https://github.com/matrix-org/matrix-rust-sdk/pull/4880/))
|
||||
|
||||
- The `dynamic_registrations_file` field of `OidcConfiguration` was removed.
|
||||
Clients are supposed to re-register with the homeserver for every login.
|
||||
|
||||
- `RoomPreview::own_membership_details` is now `RoomPreview::member_with_sender_info`, takes any user id and returns an
|
||||
`Option<RoomMemberWithSenderInfo>`.
|
||||
|
||||
Additions:
|
||||
|
||||
- Add `Encryption::get_user_identity` which returns `UserIdentity`
|
||||
- Add `ClientBuilder::room_key_recipient_strategy`
|
||||
- Add `Room::send_raw`
|
||||
- Add `NotificationSettings::set_custom_push_rule`
|
||||
- Expose `withdraw_verification` to `UserIdentity`
|
||||
- Expose `report_room` to `Room`
|
||||
- Add `RoomInfo::encryption_state`
|
||||
([#4788](https://github.com/matrix-org/matrix-rust-sdk/pull/4788))
|
||||
- Add `Timeline::send_thread_reply` for clients that need to start threads
|
||||
themselves.
|
||||
([4819](https://github.com/matrix-org/matrix-rust-sdk/pull/4819))
|
||||
- Add `ClientBuilder::session_pool_max_size`, `::session_cache_size` and `::session_journal_size_limit` to control the
|
||||
stores configuration, especially their memory consumption
|
||||
([#4870](https://github.com/matrix-org/matrix-rust-sdk/pull/4870/))
|
||||
- Add `ClientBuilder::system_is_memory_constrained` to indicate that the system
|
||||
has less memory available than the current standard
|
||||
([#4894](https://github.com/matrix-org/matrix-rust-sdk/pull/4894))
|
||||
- Add `Room::member_with_sender_info` to get both a room member's info and for the user who sent the `m.room.member`
|
||||
event the `RoomMember` is based on.
|
||||
@@ -1,143 +1,86 @@
|
||||
[package]
|
||||
name = "matrix-sdk-ffi"
|
||||
version = "0.16.0"
|
||||
edition = "2024"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ffi"]
|
||||
license = "Apache-2.0"
|
||||
readme = "README.md"
|
||||
rust-version.workspace = true
|
||||
rust-version = { workspace = true }
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
publish = false
|
||||
|
||||
[package.metadata.release]
|
||||
release = true
|
||||
|
||||
[lib]
|
||||
crate-type = [
|
||||
# Needed by uniffi for Android bindings
|
||||
"cdylib",
|
||||
# Needed by uniffi for iOS bindings
|
||||
"staticlib",
|
||||
# Needed by uniffi for JS/Wasm bindings, which use rust as an intermediate language
|
||||
"lib"
|
||||
]
|
||||
crate-type = ["cdylib", "staticlib"]
|
||||
|
||||
[features]
|
||||
default = ["bundled-sqlite", "unstable-msc4274", "experimental-element-recent-emojis", "experimental-push-secrets"]
|
||||
# Use SQLite for the session storage.
|
||||
sqlite = ["matrix-sdk/sqlite"]
|
||||
# Use an embedded version of SQLite.
|
||||
bundled-sqlite = ["sqlite", "matrix-sdk/bundled-sqlite"]
|
||||
# Use IndexedDB for the session storage.
|
||||
indexeddb = ["matrix-sdk/indexeddb"]
|
||||
unstable-msc4274 = ["matrix-sdk-ui/unstable-msc4274"]
|
||||
# Required when targeting a Javascript environment, like Wasm in a browser.
|
||||
js = ["matrix-sdk-ui/js"]
|
||||
# Enable sentry error monitoring, not compatible with Wasm platforms.
|
||||
sentry = ["dep:sentry", "dep:sentry-tracing"]
|
||||
|
||||
experimental-element-recent-emojis = ["matrix-sdk/experimental-element-recent-emojis"]
|
||||
experimental-push-secrets = ["matrix-sdk/experimental-push-secrets"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
chrono.workspace = true
|
||||
extension-trait = "1.0.2"
|
||||
eyeball-im.workspace = true
|
||||
futures-util.workspace = true
|
||||
language-tags = "0.3.2"
|
||||
log-panics = { version = "2.1.0", default-features = false, features = ["with-backtrace"] }
|
||||
matrix-sdk = { workspace = true, features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
"experimental-widgets",
|
||||
"markdown",
|
||||
"socks",
|
||||
"uniffi",
|
||||
"federation-api",
|
||||
"experimental-search"
|
||||
] }
|
||||
matrix-sdk-base.workspace = true
|
||||
matrix-sdk-common.workspace = true
|
||||
matrix-sdk-ffi-macros.workspace = true
|
||||
matrix-sdk-ui = { workspace = true, features = ["uniffi"] }
|
||||
mime = { version = "0.3.17", default-features = false }
|
||||
ruma = { workspace = true, features = [
|
||||
"html",
|
||||
"unstable-msc3488",
|
||||
"compat-unset-avatar",
|
||||
"unstable-msc3245-v1-compat",
|
||||
"unstable-msc4278",
|
||||
"unstable-msc3230",
|
||||
# Audio event type
|
||||
"unstable-msc3927",
|
||||
# File event type
|
||||
"unstable-msc3551",
|
||||
# Image event type
|
||||
"unstable-msc3552",
|
||||
# Video event type
|
||||
"unstable-msc3553",
|
||||
# Voice event type
|
||||
"unstable-msc3245",
|
||||
# Emote event type
|
||||
"unstable-msc3954",
|
||||
# Image pack event type
|
||||
"unstable-msc2545",
|
||||
# Room language event type
|
||||
"unstable-msc4334",
|
||||
] }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sentry = { workspace = true, optional = true, features = [
|
||||
# Most default features enabled otherwise.
|
||||
"backtrace",
|
||||
"contexts",
|
||||
"debug-images",
|
||||
"panic",
|
||||
"reqwest",
|
||||
"sentry-debug-images",
|
||||
] }
|
||||
sentry-tracing = { workspace = true, optional = true }
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-appender.workspace = true
|
||||
tracing-core.workspace = true
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
url.workspace = true
|
||||
uuid = { version = "1.4.1", default-features = false, features = ["std", "v4"] }
|
||||
zeroize.workspace = true
|
||||
oauth2.workspace = true
|
||||
|
||||
[target.'cfg(target_family = "wasm")'.dependencies]
|
||||
console_error_panic_hook = { version = "0.1.7", default-features = false }
|
||||
tokio = { workspace = true, features = ["sync", "macros"] }
|
||||
uniffi = { workspace = true, features = ["wasm-unstable-single-threaded"] }
|
||||
futures-executor.workspace = true
|
||||
|
||||
[target.'cfg(not(target_family = "wasm"))'.dependencies]
|
||||
async-compat.workspace = true
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
uniffi = { workspace = true, features = ["tokio"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
paranoid-android = { version = "0.2.2", default-features = false }
|
||||
# Needed for `rustls-platform-verifier`. Newer versions aren't compatible with it.
|
||||
jni = "0.21.1"
|
||||
# Used to access the credential storage on Android
|
||||
rustls-platform-verifier = "0.6.2"
|
||||
# Needed for intializing and keeping the JavaVM reference around
|
||||
once_cell = "1.21.4"
|
||||
# Gobley's jvm-getter is used to get a JVM pointer from all Android versions
|
||||
jvm-getter = "0.1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
similar-asserts.workspace = true
|
||||
tempfile.workspace = true
|
||||
default = ["bundled-sqlite"]
|
||||
bundled-sqlite = ["matrix-sdk/bundled-sqlite"]
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
vergen-gitcl = { workspace = true, features = ["build"] }
|
||||
vergen = { version = "8.1.3", features = ["build", "git", "gitcl"] }
|
||||
|
||||
[lints]
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
as_variant = { workspace = true }
|
||||
async-compat = "0.2.1"
|
||||
base64 = "0.21"
|
||||
eyeball-im = { workspace = true }
|
||||
extension-trait = "1.0.1"
|
||||
futures-core = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
matrix-sdk-ui = { workspace = true, features = ["e2e-encryption", "uniffi"] }
|
||||
mime = "0.3.16"
|
||||
once_cell = { workspace = true }
|
||||
opentelemetry = "0.21.0"
|
||||
opentelemetry_sdk = { version = "0.21.0", features = ["rt-tokio"] }
|
||||
opentelemetry-otlp = { version = "0.14.0", features = ["tokio", "reqwest-client", "http-proto"] }
|
||||
ruma = { workspace = true, features = ["html", "unstable-unspecified", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat"] }
|
||||
sanitize-filename-reader-friendly = "2.2.1"
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-core = { workspace = true }
|
||||
tracing-opentelemetry = "0.22.0"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tracing-appender = { version = "0.2.2" }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
tokio-stream = { workspace = true, features = ["time"] }
|
||||
uniffi = { workspace = true, features = ["tokio"] }
|
||||
url = "2.2.2"
|
||||
zeroize = { workspace = true }
|
||||
uuid = { version = "1.4.1", features = ["v4"] }
|
||||
language-tags = "0.3.2"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
log-panics = { version = "2", features = ["with-backtrace"] }
|
||||
paranoid-android = "0.2.1"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies.matrix-sdk]
|
||||
workspace = true
|
||||
features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
"experimental-oidc",
|
||||
"experimental-sliding-sync",
|
||||
"experimental-widgets",
|
||||
"markdown",
|
||||
"rustls-tls", # note: differ from block below
|
||||
"socks",
|
||||
"sqlite",
|
||||
]
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies.matrix-sdk]
|
||||
workspace = true
|
||||
features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
"experimental-oidc",
|
||||
"experimental-sliding-sync",
|
||||
"experimental-widgets",
|
||||
"markdown",
|
||||
"native-tls", # note: differ from block above
|
||||
"socks",
|
||||
"sqlite",
|
||||
]
|
||||
|
||||
@@ -2,29 +2,20 @@
|
||||
|
||||
This uses [`uniffi`](https://mozilla.github.io/uniffi-rs/Overview.html) to build the matrix bindings for native support and wasm-bindgen for web-browser assembly support. Please refer to the specific section to figure out how to build and use the bindings for your platform.
|
||||
|
||||
## Features
|
||||
# OpenTelemetry support
|
||||
|
||||
Given the number of platforms targeted, we have broken out a number of features
|
||||
|
||||
### Functionality
|
||||
|
||||
- `sentry`: Enable error monitoring using Sentry, not supports on Wasm platforms.
|
||||
- `sqlite`: Use SQLite for the session storage.
|
||||
- `bundled-sqlite`: Use an embedded version of SQLite instead of the system provided one.
|
||||
- `indexeddb`: Use IndexedDB for the session storage.
|
||||
|
||||
### Unstable specs
|
||||
|
||||
- `unstable-msc4274`: Adds support for gallery message types, which contain multiple media elements.
|
||||
The bindings have support for OpenTelemetry, allowing for the upload of traces to
|
||||
any OpenTelemetry collector. This support is provided through the
|
||||
[`opentelemetry`](https://docs.rs/opentelemetry/latest/opentelemetry/)
|
||||
crate, which requires the [`protoc`] binary to be installed during the build
|
||||
process.
|
||||
|
||||
## Platforms
|
||||
|
||||
Each supported target should use features to build the relevant system. Here are some suggested feature flags for the major platforms:
|
||||
|
||||
- Android: `"bundled-sqlite,unstable-msc4274,sentry"`
|
||||
- iOS: `"bundled-sqlite,unstable-msc4274,sentry"`
|
||||
- JavaScript/Wasm: `"indexeddb,unstable-msc4274"`
|
||||
|
||||
### Swift/iOS sync
|
||||
|
||||
|
||||
|
||||
### Swift/iOS async
|
||||
|
||||
TBD
|
||||
|
||||
@@ -1,99 +1,37 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
use std::{env, error::Error};
|
||||
|
||||
use std::{
|
||||
env,
|
||||
error::Error,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use vergen_gitcl::{Emitter, GitclBuilder};
|
||||
use vergen::EmitBuilder;
|
||||
|
||||
/// Adds a temporary workaround for an issue with the Rust compiler and Android
|
||||
/// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717.
|
||||
/// The workaround is based on: https://github.com/mozilla/application-services/pull/5442
|
||||
///
|
||||
/// IMPORTANT: if you modify this, make sure to modify
|
||||
/// [../matrix-sdk-crypto-ffi/build.rs] too!
|
||||
/// The workaround comes from: https://github.com/mozilla/application-services/pull/5442
|
||||
fn setup_x86_64_android_workaround() {
|
||||
let target_os = env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not set");
|
||||
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").expect("CARGO_CFG_TARGET_ARCH not set");
|
||||
if target_arch == "x86_64" && target_os == "android" {
|
||||
// Configure rust to statically link against the `libclang_rt.builtins` supplied
|
||||
// with clang.
|
||||
|
||||
// cargo-ndk sets CC_x86_64-linux-android to the path to `clang`, within the
|
||||
// Android NDK.
|
||||
let clang_path = PathBuf::from(
|
||||
env::var("CC_x86_64-linux-android").expect("CC_x86_64-linux-android not set"),
|
||||
let android_ndk_home = env::var("ANDROID_NDK_HOME").expect("ANDROID_NDK_HOME not set");
|
||||
let build_os = match env::consts::OS {
|
||||
"linux" => "linux",
|
||||
"macos" => "darwin",
|
||||
"windows" => "windows",
|
||||
_ => panic!(
|
||||
"Unsupported OS. You must use either Linux, MacOS or Windows to build the crate."
|
||||
),
|
||||
};
|
||||
const DEFAULT_CLANG_VERSION: &str = "14.0.7";
|
||||
let clang_version =
|
||||
env::var("NDK_CLANG_VERSION").unwrap_or_else(|_| DEFAULT_CLANG_VERSION.to_owned());
|
||||
let linux_x86_64_lib_dir = format!(
|
||||
"toolchains/llvm/prebuilt/{build_os}-x86_64/lib64/clang/{clang_version}/lib/linux/"
|
||||
);
|
||||
|
||||
// clang_path should now look something like
|
||||
// `.../sdk/ndk/28.0.12674087/toolchains/llvm/prebuilt/linux-x86_64/bin/clang`.
|
||||
// We strip `/bin/clang` from the end to get the toolchain path.
|
||||
let toolchain_path = clang_path
|
||||
.ancestors()
|
||||
.nth(2)
|
||||
.expect("could not find NDK toolchain path")
|
||||
.to_str()
|
||||
.expect("NDK toolchain path is not valid UTF-8");
|
||||
|
||||
let clang_version = get_clang_major_version(&clang_path);
|
||||
|
||||
println!("cargo:rustc-link-search={toolchain_path}/lib/clang/{clang_version}/lib/linux/");
|
||||
println!("cargo:rustc-link-search={android_ndk_home}/{linux_x86_64_lib_dir}");
|
||||
println!("cargo:rustc-link-lib=static=clang_rt.builtins-x86_64-android");
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a workaround for watchOS simulator builds to manually link against the
|
||||
/// CoreFoundation framework in order to avoid linker errors. Otherwise, errors
|
||||
/// like the following may occur:
|
||||
///
|
||||
/// = note: Undefined symbols for architecture arm64:
|
||||
/// "_CFArrayCreate", referenced from:
|
||||
/// "_CFDataCreate", referenced from:
|
||||
/// "_CFRelease", referenced from:
|
||||
/// etc.
|
||||
fn setup_watchos_simulator_workaround() {
|
||||
let target = env::var("TARGET").expect("TARGET not set");
|
||||
if target.ends_with("watchos-sim") {
|
||||
println!("cargo:rustc-link-arg=-framework");
|
||||
println!("cargo:rustc-link-arg=CoreFoundation");
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the clang binary at `clang_path`, and return its major version number
|
||||
fn get_clang_major_version(clang_path: &Path) -> String {
|
||||
let clang_output =
|
||||
Command::new(clang_path).arg("-dumpversion").output().expect("failed to start clang");
|
||||
|
||||
if !clang_output.status.success() {
|
||||
panic!("failed to run clang: {}", String::from_utf8_lossy(&clang_output.stderr));
|
||||
}
|
||||
|
||||
let clang_version = String::from_utf8(clang_output.stdout).expect("clang output is not utf8");
|
||||
clang_version.split('.').next().expect("could not parse clang output").to_owned()
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
setup_x86_64_android_workaround();
|
||||
setup_watchos_simulator_workaround();
|
||||
uniffi::generate_scaffolding("./src/api.udl").expect("Building the UDL file failed");
|
||||
|
||||
let git_config = GitclBuilder::default().sha(true).build()?;
|
||||
Emitter::default().add_instructions(&git_config)?.emit()?;
|
||||
|
||||
EmitBuilder::builder().git_sha(true).emit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
namespace matrix_sdk_ffi {};
|
||||
|
||||
[Remote]
|
||||
dictionary Mentions {
|
||||
sequence<string> user_ids;
|
||||
boolean room;
|
||||
};
|
||||
|
||||
[Remote]
|
||||
interface RoomMessageEventContentWithoutRelation {
|
||||
RoomMessageEventContentWithoutRelation with_mentions(Mentions mentions);
|
||||
};
|
||||
|
||||
[Error]
|
||||
interface ClientError {
|
||||
Generic(string msg);
|
||||
};
|
||||
|
||||
interface MediaSource {
|
||||
[Name=from_json, Throws=ClientError]
|
||||
constructor(string json);
|
||||
string to_json();
|
||||
string url();
|
||||
};
|
||||
|
||||
@@ -1,276 +0,0 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::{self, Debug},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use matrix_sdk::{
|
||||
Error,
|
||||
authentication::oauth::{
|
||||
ClientId, ClientRegistrationData, OAuthError as SdkOAuthError,
|
||||
error::OAuthAuthorizationCodeError,
|
||||
registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType},
|
||||
},
|
||||
};
|
||||
use ruma::serde::Raw;
|
||||
use url::Url;
|
||||
|
||||
use crate::client::{Client, OidcPrompt, SlidingSyncVersion};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct HomeserverLoginDetails {
|
||||
pub(crate) url: String,
|
||||
pub(crate) sliding_sync_version: SlidingSyncVersion,
|
||||
pub(crate) supports_oidc_login: bool,
|
||||
pub(crate) supported_oidc_prompts: Vec<OidcPrompt>,
|
||||
pub(crate) supports_sso_login: bool,
|
||||
pub(crate) supports_password_login: bool,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl HomeserverLoginDetails {
|
||||
/// The URL of the currently configured homeserver.
|
||||
pub fn url(&self) -> String {
|
||||
self.url.clone()
|
||||
}
|
||||
|
||||
/// The sliding sync version.
|
||||
pub fn sliding_sync_version(&self) -> SlidingSyncVersion {
|
||||
self.sliding_sync_version.clone()
|
||||
}
|
||||
|
||||
/// Whether the current homeserver supports login using OIDC.
|
||||
pub fn supports_oidc_login(&self) -> bool {
|
||||
self.supports_oidc_login
|
||||
}
|
||||
|
||||
/// Whether the current homeserver supports login using legacy SSO.
|
||||
pub fn supports_sso_login(&self) -> bool {
|
||||
self.supports_sso_login
|
||||
}
|
||||
|
||||
/// The prompts advertised by the authentication issuer for use in the login
|
||||
/// URL.
|
||||
pub fn supported_oidc_prompts(&self) -> Vec<OidcPrompt> {
|
||||
self.supported_oidc_prompts.clone()
|
||||
}
|
||||
|
||||
/// Whether the current homeserver supports the password login flow.
|
||||
pub fn supports_password_login(&self) -> bool {
|
||||
self.supports_password_login
|
||||
}
|
||||
}
|
||||
|
||||
/// An object encapsulating the SSO login flow
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SsoHandler {
|
||||
/// The wrapped Client.
|
||||
pub(crate) client: Arc<Client>,
|
||||
|
||||
/// The underlying URL for authentication.
|
||||
pub(crate) url: String,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SsoHandler {
|
||||
/// Returns the URL for starting SSO authentication. The URL should be
|
||||
/// opened in a web view. Once the web view succeeds, call `finish` with
|
||||
/// the callback URL.
|
||||
pub fn url(&self) -> String {
|
||||
self.url.clone()
|
||||
}
|
||||
|
||||
/// Completes the SSO login process.
|
||||
pub async fn finish(&self, callback_url: String) -> Result<(), SsoError> {
|
||||
let auth = self.client.inner.matrix_auth();
|
||||
let url = Url::parse(&callback_url).map_err(|_| SsoError::CallbackUrlInvalid)?;
|
||||
let builder =
|
||||
auth.login_with_sso_callback(url.into()).map_err(|_| SsoError::CallbackUrlInvalid)?;
|
||||
builder.await.map_err(|_| SsoError::LoginWithTokenFailed)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for SsoHandler {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
fmt.debug_struct("SsoHandler").field("url", &self.url).finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum SsoError {
|
||||
#[error("The supplied callback URL used to complete SSO is invalid.")]
|
||||
CallbackUrlInvalid,
|
||||
#[error("Logging in with the token from the supplied callback URL failed.")]
|
||||
LoginWithTokenFailed,
|
||||
|
||||
#[error("An error occurred: {message}")]
|
||||
Generic { message: String },
|
||||
}
|
||||
|
||||
/// The configuration to use when authenticating with OIDC.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct OidcConfiguration {
|
||||
/// The name of the client that will be shown during OIDC authentication.
|
||||
pub client_name: Option<String>,
|
||||
/// The redirect URI that will be used when OIDC authentication is
|
||||
/// successful.
|
||||
pub redirect_uri: String,
|
||||
/// A URI that contains information about the client.
|
||||
pub client_uri: String,
|
||||
/// A URI that contains the client's logo.
|
||||
pub logo_uri: Option<String>,
|
||||
/// A URI that contains the client's terms of service.
|
||||
pub tos_uri: Option<String>,
|
||||
/// A URI that contains the client's privacy policy.
|
||||
pub policy_uri: Option<String>,
|
||||
|
||||
/// Pre-configured registrations for use with homeservers that don't support
|
||||
/// dynamic client registration.
|
||||
///
|
||||
/// The keys of the map should be the URLs of the homeservers, but keys
|
||||
/// using `issuer` URLs are also supported.
|
||||
pub static_registrations: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl OidcConfiguration {
|
||||
pub(crate) fn redirect_uri(&self) -> Result<Url, OidcError> {
|
||||
Url::parse(&self.redirect_uri).map_err(|_| OidcError::CallbackUrlInvalid)
|
||||
}
|
||||
|
||||
pub(crate) fn client_metadata(&self) -> Result<Raw<ClientMetadata>, OidcError> {
|
||||
let redirect_uri = self.redirect_uri()?;
|
||||
let client_name = self.client_name.as_ref().map(|n| Localized::new(n.to_owned(), []));
|
||||
let client_uri = self.client_uri.localized_url()?;
|
||||
let logo_uri = self.logo_uri.localized_url()?;
|
||||
let policy_uri = self.policy_uri.localized_url()?;
|
||||
let tos_uri = self.tos_uri.localized_url()?;
|
||||
|
||||
let metadata = ClientMetadata {
|
||||
// The server should display the following fields when getting the user's consent.
|
||||
client_name,
|
||||
logo_uri,
|
||||
policy_uri,
|
||||
tos_uri,
|
||||
..ClientMetadata::new(
|
||||
ApplicationType::Native,
|
||||
vec![
|
||||
OAuthGrantType::AuthorizationCode { redirect_uris: vec![redirect_uri] },
|
||||
OAuthGrantType::DeviceCode,
|
||||
],
|
||||
client_uri,
|
||||
)
|
||||
};
|
||||
|
||||
Raw::new(&metadata).map_err(|_| OidcError::MetadataInvalid)
|
||||
}
|
||||
|
||||
pub(crate) fn registration_data(&self) -> Result<ClientRegistrationData, OidcError> {
|
||||
let client_metadata = self.client_metadata()?;
|
||||
|
||||
let mut registration_data = ClientRegistrationData::new(client_metadata);
|
||||
|
||||
if !self.static_registrations.is_empty() {
|
||||
let static_registrations = self
|
||||
.static_registrations
|
||||
.iter()
|
||||
.filter_map(|(issuer, client_id)| {
|
||||
let Ok(issuer) = Url::parse(issuer) else {
|
||||
tracing::error!("Failed to parse {issuer:?}");
|
||||
return None;
|
||||
};
|
||||
Some((issuer, ClientId::new(client_id.clone())))
|
||||
})
|
||||
.collect();
|
||||
|
||||
registration_data.static_registrations = Some(static_registrations);
|
||||
}
|
||||
|
||||
Ok(registration_data)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum OidcError {
|
||||
#[error(
|
||||
"The homeserver doesn't provide an authentication issuer in its well-known configuration."
|
||||
)]
|
||||
NotSupported,
|
||||
#[error("Unable to use OIDC as the supplied client metadata is invalid.")]
|
||||
MetadataInvalid,
|
||||
#[error("The supplied callback URL used to complete OIDC is invalid.")]
|
||||
CallbackUrlInvalid,
|
||||
#[error("The OIDC login was cancelled by the user.")]
|
||||
Cancelled,
|
||||
|
||||
#[error("An error occurred: {message}")]
|
||||
Generic { message: String },
|
||||
}
|
||||
|
||||
impl From<SdkOAuthError> for OidcError {
|
||||
fn from(e: SdkOAuthError) -> OidcError {
|
||||
match e {
|
||||
SdkOAuthError::Discovery(error) if error.is_not_supported() => OidcError::NotSupported,
|
||||
SdkOAuthError::AuthorizationCode(OAuthAuthorizationCodeError::RedirectUri(_))
|
||||
| SdkOAuthError::AuthorizationCode(OAuthAuthorizationCodeError::InvalidState) => {
|
||||
OidcError::CallbackUrlInvalid
|
||||
}
|
||||
SdkOAuthError::AuthorizationCode(OAuthAuthorizationCodeError::Cancelled) => {
|
||||
OidcError::Cancelled
|
||||
}
|
||||
_ => OidcError::Generic { message: e.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for OidcError {
|
||||
fn from(e: Error) -> OidcError {
|
||||
match e {
|
||||
Error::OAuth(e) => (*e).into(),
|
||||
_ => OidcError::Generic { message: e.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Helpers */
|
||||
|
||||
trait OptionExt {
|
||||
/// Convenience method to convert an `Option<String>` to a URL and returns
|
||||
/// it as a Localized URL. No localization is actually performed.
|
||||
fn localized_url(&self) -> Result<Option<Localized<Url>>, OidcError>;
|
||||
}
|
||||
|
||||
impl OptionExt for Option<String> {
|
||||
fn localized_url(&self) -> Result<Option<Localized<Url>>, OidcError> {
|
||||
self.as_deref().map(StrExt::localized_url).transpose()
|
||||
}
|
||||
}
|
||||
|
||||
trait StrExt {
|
||||
/// Convenience method to convert a string to a URL and returns it as a
|
||||
/// Localized URL. No localization is actually performed.
|
||||
fn localized_url(&self) -> Result<Localized<Url>, OidcError>;
|
||||
}
|
||||
|
||||
impl StrExt for str {
|
||||
fn localized_url(&self) -> Result<Localized<Url>, OidcError> {
|
||||
Ok(Localized::new(Url::parse(self).map_err(|_| OidcError::MetadataInvalid)?, []))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use matrix_sdk::{
|
||||
oidc::{
|
||||
types::{
|
||||
client_credentials::ClientCredentials,
|
||||
errors::ClientErrorCode::AccessDenied,
|
||||
iana::oauth::OAuthClientAuthenticationMethod,
|
||||
oidc::ApplicationType,
|
||||
registration::{ClientMetadata, Localized, VerifiedClientMetadata},
|
||||
requests::{GrantType, Prompt},
|
||||
},
|
||||
AuthorizationResponse, Oidc, OidcError,
|
||||
},
|
||||
AuthSession,
|
||||
};
|
||||
use matrix_sdk_ui::authentication::oidc::{ClientId, OidcRegistrations, OidcRegistrationsError};
|
||||
use ruma::{
|
||||
api::client::discovery::discover_homeserver::AuthenticationServerInfo, IdParseError,
|
||||
OwnedUserId,
|
||||
};
|
||||
use url::Url;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use super::{client::Client, client_builder::ClientBuilder, RUNTIME};
|
||||
use crate::{client::ClientSessionDelegate, client_builder::UrlScheme, error::ClientError};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct AuthenticationService {
|
||||
base_path: String,
|
||||
passphrase: Option<String>,
|
||||
user_agent: Option<String>,
|
||||
client: RwLock<Option<Arc<Client>>>,
|
||||
homeserver_details: RwLock<Option<Arc<HomeserverLoginDetails>>>,
|
||||
oidc_configuration: Option<OidcConfiguration>,
|
||||
custom_sliding_sync_proxy: RwLock<Option<String>>,
|
||||
cross_process_refresh_lock_id: Option<String>,
|
||||
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
|
||||
}
|
||||
|
||||
impl Drop for AuthenticationService {
|
||||
fn drop(&mut self) {
|
||||
self.passphrase.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
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 sliding sync proxy in its well-known configuration.")]
|
||||
SlidingSyncNotAvailable,
|
||||
#[error("Login was successful but is missing a valid Session to configure the file store.")]
|
||||
SessionMissing,
|
||||
#[error("Failed to use the supplied base path.")]
|
||||
InvalidBasePath,
|
||||
#[error(
|
||||
"The homeserver doesn't provide an authentication issuer in its well-known configuration."
|
||||
)]
|
||||
OidcNotSupported,
|
||||
#[error("Unable to use OIDC as no client metadata has been supplied.")]
|
||||
OidcMetadataMissing,
|
||||
#[error("Unable to use OIDC as the supplied client metadata is invalid.")]
|
||||
OidcMetadataInvalid,
|
||||
#[error("The supplied callback URL used to complete OIDC is invalid.")]
|
||||
OidcCallbackUrlInvalid,
|
||||
#[error("The OIDC login was cancelled by the user.")]
|
||||
OidcCancelled,
|
||||
#[error("An error occurred with OIDC: {message}")]
|
||||
OidcError { message: String },
|
||||
#[error("An error occurred: {message}")]
|
||||
Generic { message: String },
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for AuthenticationError {
|
||||
fn from(e: anyhow::Error) -> AuthenticationError {
|
||||
AuthenticationError::Generic { message: e.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IdParseError> for AuthenticationError {
|
||||
fn from(e: IdParseError) -> AuthenticationError {
|
||||
AuthenticationError::InvalidServerName { message: e.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OidcRegistrationsError> for AuthenticationError {
|
||||
fn from(e: OidcRegistrationsError) -> AuthenticationError {
|
||||
match e {
|
||||
OidcRegistrationsError::InvalidBasePath => AuthenticationError::InvalidBasePath,
|
||||
_ => AuthenticationError::OidcError { message: e.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OidcError> for AuthenticationError {
|
||||
fn from(e: OidcError) -> AuthenticationError {
|
||||
AuthenticationError::OidcError { message: e.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
/// The configuration to use when authenticating with OIDC.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct OidcConfiguration {
|
||||
/// The name of the client that will be shown during OIDC authentication.
|
||||
pub client_name: Option<String>,
|
||||
/// The redirect URI that will be used when OIDC authentication is
|
||||
/// successful.
|
||||
pub redirect_uri: String,
|
||||
/// A URI that contains information about the client.
|
||||
pub client_uri: Option<String>,
|
||||
/// A URI that contains the client's logo.
|
||||
pub logo_uri: Option<String>,
|
||||
/// A URI that contains the client's terms of service.
|
||||
pub tos_uri: Option<String>,
|
||||
/// A URI that contains the client's privacy policy.
|
||||
pub policy_uri: Option<String>,
|
||||
/// An array of e-mail addresses of people responsible for this client.
|
||||
pub contacts: Option<Vec<String>>,
|
||||
|
||||
/// Pre-configured registrations for use with issuers that don't support
|
||||
/// dynamic client registration.
|
||||
pub static_registrations: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// The data required to authenticate against an OIDC server.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct OidcAuthenticationData {
|
||||
/// The underlying URL for authentication.
|
||||
url: Url,
|
||||
/// A unique identifier for the request, used to ensure the response
|
||||
/// originated from the authentication issuer.
|
||||
state: String,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl OidcAuthenticationData {
|
||||
/// The login URL to use for authentication.
|
||||
pub fn login_url(&self) -> String {
|
||||
self.url.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct HomeserverLoginDetails {
|
||||
url: String,
|
||||
supports_oidc_login: bool,
|
||||
supports_password_login: bool,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl HomeserverLoginDetails {
|
||||
/// The URL of the currently configured homeserver.
|
||||
pub fn url(&self) -> String {
|
||||
self.url.clone()
|
||||
}
|
||||
|
||||
/// Whether the current homeserver supports login using OIDC.
|
||||
pub fn supports_oidc_login(&self) -> bool {
|
||||
self.supports_oidc_login
|
||||
}
|
||||
|
||||
/// Whether the current homeserver supports the password login flow.
|
||||
pub fn supports_password_login(&self) -> bool {
|
||||
self.supports_password_login
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl AuthenticationService {
|
||||
/// Creates a new service to authenticate a user with.
|
||||
#[uniffi::constructor]
|
||||
pub fn new(
|
||||
base_path: String,
|
||||
passphrase: Option<String>,
|
||||
user_agent: Option<String>,
|
||||
oidc_configuration: Option<OidcConfiguration>,
|
||||
custom_sliding_sync_proxy: Option<String>,
|
||||
session_delegate: Option<Box<dyn ClientSessionDelegate>>,
|
||||
cross_process_refresh_lock_id: Option<String>,
|
||||
) -> Arc<Self> {
|
||||
Arc::new(AuthenticationService {
|
||||
base_path,
|
||||
passphrase,
|
||||
user_agent,
|
||||
client: RwLock::new(None),
|
||||
homeserver_details: RwLock::new(None),
|
||||
oidc_configuration,
|
||||
custom_sliding_sync_proxy: RwLock::new(custom_sliding_sync_proxy),
|
||||
session_delegate: session_delegate.map(Into::into),
|
||||
cross_process_refresh_lock_id,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn homeserver_details(&self) -> Option<Arc<HomeserverLoginDetails>> {
|
||||
self.homeserver_details.read().unwrap().clone()
|
||||
}
|
||||
|
||||
/// Updates the service to authenticate with the homeserver for the
|
||||
/// specified address.
|
||||
pub fn configure_homeserver(
|
||||
&self,
|
||||
server_name_or_homeserver_url: String,
|
||||
) -> Result<(), AuthenticationError> {
|
||||
let mut builder = self.new_client_builder();
|
||||
|
||||
// Attempt discovery as a server name first.
|
||||
let result = matrix_sdk::sanitize_server_name(&server_name_or_homeserver_url);
|
||||
|
||||
match result {
|
||||
Ok(server_name) => {
|
||||
let protocol = if server_name_or_homeserver_url.starts_with("http://") {
|
||||
UrlScheme::Http
|
||||
} else {
|
||||
UrlScheme::Https
|
||||
};
|
||||
builder = builder.server_name_with_protocol(server_name.to_string(), protocol);
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
// When the input isn't a valid server name check it is a URL.
|
||||
// If this is the case, build the client with a homeserver URL.
|
||||
if Url::parse(&server_name_or_homeserver_url).is_ok() {
|
||||
builder = builder.homeserver_url(server_name_or_homeserver_url.clone());
|
||||
} else {
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let client = builder.build_inner().or_else(|e| {
|
||||
if !server_name_or_homeserver_url.starts_with("http://")
|
||||
&& !server_name_or_homeserver_url.starts_with("https://")
|
||||
{
|
||||
return Err(e);
|
||||
}
|
||||
// When discovery fails, fallback to the homeserver URL if supplied.
|
||||
let mut builder = self.new_client_builder();
|
||||
builder = builder.homeserver_url(server_name_or_homeserver_url);
|
||||
builder.build_inner()
|
||||
})?;
|
||||
|
||||
let details = RUNTIME.block_on(self.details_from_client(&client))?;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
*self.client.write().unwrap() = Some(client);
|
||||
*self.homeserver_details.write().unwrap() = Some(Arc::new(details));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Performs a password login using the current homeserver.
|
||||
pub fn login(
|
||||
&self,
|
||||
username: String,
|
||||
password: String,
|
||||
initial_device_name: Option<String>,
|
||||
device_id: Option<String>,
|
||||
) -> Result<Arc<Client>, AuthenticationError> {
|
||||
let Some(client) = self.client.read().unwrap().clone() else {
|
||||
return Err(AuthenticationError::ClientMissing);
|
||||
};
|
||||
|
||||
// Login and ask the server for the full user ID as this could be different from
|
||||
// the username that was entered.
|
||||
client.login(username, password, initial_device_name, device_id).map_err(|e| match e {
|
||||
ClientError::Generic { msg } => AuthenticationError::Generic { message: msg },
|
||||
})?;
|
||||
let whoami = client.whoami()?;
|
||||
let session =
|
||||
client.inner.matrix_auth().session().ok_or(AuthenticationError::SessionMissing)?;
|
||||
|
||||
self.finalize_client(client, session, whoami.user_id)
|
||||
}
|
||||
|
||||
/// Requests the URL needed for login in a web view using OIDC. Once the web
|
||||
/// view has succeeded, call `login_with_oidc_callback` with the callback it
|
||||
/// returns.
|
||||
pub fn url_for_oidc_login(&self) -> Result<Arc<OidcAuthenticationData>, AuthenticationError> {
|
||||
let Some(client) = self.client.read().unwrap().clone() else {
|
||||
return Err(AuthenticationError::ClientMissing);
|
||||
};
|
||||
|
||||
let Some(authentication_server) = client.discovered_authentication_server() else {
|
||||
return Err(AuthenticationError::OidcNotSupported);
|
||||
};
|
||||
|
||||
let Some(oidc_configuration) = &self.oidc_configuration else {
|
||||
return Err(AuthenticationError::OidcMetadataMissing);
|
||||
};
|
||||
|
||||
let redirect_url = Url::parse(&oidc_configuration.redirect_uri)
|
||||
.map_err(|_e| AuthenticationError::OidcMetadataInvalid)?;
|
||||
|
||||
let oidc = client.inner.oidc();
|
||||
|
||||
RUNTIME.block_on(async {
|
||||
self.configure_oidc(&oidc, authentication_server, oidc_configuration).await?;
|
||||
|
||||
let mut data_builder = oidc.login(redirect_url, None)?;
|
||||
// TODO: Add a check for the Consent prompt when MAS is updated.
|
||||
data_builder = data_builder.prompt(vec![Prompt::Consent]);
|
||||
let data = data_builder.build().await?;
|
||||
|
||||
Ok(Arc::new(OidcAuthenticationData { url: data.url, state: data.state }))
|
||||
})
|
||||
}
|
||||
|
||||
/// Completes the OIDC login process.
|
||||
pub fn login_with_oidc_callback(
|
||||
&self,
|
||||
authentication_data: Arc<OidcAuthenticationData>,
|
||||
callback_url: String,
|
||||
) -> Result<Arc<Client>, AuthenticationError> {
|
||||
let Some(client) = self.client.read().unwrap().clone() else {
|
||||
return Err(AuthenticationError::ClientMissing);
|
||||
};
|
||||
|
||||
let oidc = client.inner.oidc();
|
||||
|
||||
let url =
|
||||
Url::parse(&callback_url).map_err(|_| AuthenticationError::OidcCallbackUrlInvalid)?;
|
||||
|
||||
let response = AuthorizationResponse::parse_uri(&url)
|
||||
.map_err(|_| AuthenticationError::OidcCallbackUrlInvalid)?;
|
||||
|
||||
let code = match response {
|
||||
AuthorizationResponse::Success(code) => code,
|
||||
AuthorizationResponse::Error(err) => {
|
||||
if err.error.error == AccessDenied {
|
||||
// The user cancelled the login in the web view.
|
||||
return Err(AuthenticationError::OidcCancelled);
|
||||
}
|
||||
return Err(AuthenticationError::OidcError {
|
||||
message: err.error.error.to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if code.state != authentication_data.state {
|
||||
return Err(AuthenticationError::OidcCallbackUrlInvalid);
|
||||
};
|
||||
|
||||
RUNTIME.block_on(async move {
|
||||
oidc.finish_authorization(code).await?;
|
||||
|
||||
oidc.finish_login()
|
||||
.await
|
||||
.map_err(|e| AuthenticationError::OidcError { message: e.to_string() })
|
||||
})?;
|
||||
|
||||
let user_id = client.inner.user_id().unwrap().to_owned();
|
||||
let session =
|
||||
client.inner.oidc().full_session().ok_or(AuthenticationError::SessionMissing)?;
|
||||
self.finalize_client(client, session, user_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthenticationService {
|
||||
/// A new client builder pre-configured with the service's base path and
|
||||
/// user agent if specified
|
||||
fn new_client_builder(&self) -> Arc<ClientBuilder> {
|
||||
let mut builder = ClientBuilder::new().base_path(self.base_path.clone());
|
||||
|
||||
if let Some(user_agent) = self.user_agent.clone() {
|
||||
builder = builder.user_agent(user_agent);
|
||||
}
|
||||
|
||||
builder
|
||||
}
|
||||
|
||||
/// Get the homeserver login details from a client.
|
||||
async fn details_from_client(
|
||||
&self,
|
||||
client: &Arc<Client>,
|
||||
) -> Result<HomeserverLoginDetails, AuthenticationError> {
|
||||
let supports_oidc_login = client.discovered_authentication_server().is_some();
|
||||
let supports_password_login = client.supports_password_login().await.ok().unwrap_or(false);
|
||||
let url = client.homeserver();
|
||||
|
||||
Ok(HomeserverLoginDetails { url, supports_oidc_login, supports_password_login })
|
||||
}
|
||||
|
||||
/// Handle any necessary configuration in order for login via OIDC to
|
||||
/// succeed. This includes performing dynamic client registration against
|
||||
/// the homeserver's issuer or restoring a previous registration if one has
|
||||
/// been stored.
|
||||
async fn configure_oidc(
|
||||
&self,
|
||||
oidc: &Oidc,
|
||||
authentication_server: AuthenticationServerInfo,
|
||||
configuration: &OidcConfiguration,
|
||||
) -> Result<(), AuthenticationError> {
|
||||
if oidc.client_credentials().is_some() {
|
||||
tracing::info!("OIDC is already configured.");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let oidc_metadata = self.oidc_metadata(configuration)?;
|
||||
|
||||
if self.load_client_registration(oidc, &authentication_server, oidc_metadata.clone()).await
|
||||
{
|
||||
tracing::info!("OIDC configuration loaded from disk.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tracing::info!("Registering this client for OIDC.");
|
||||
let registration_response = oidc
|
||||
.register_client(&authentication_server.issuer, oidc_metadata.clone(), None)
|
||||
.await?;
|
||||
|
||||
// The format of the credentials changes according to the client metadata that
|
||||
// was sent. Public clients only get a client ID.
|
||||
let credentials =
|
||||
ClientCredentials::None { client_id: registration_response.client_id.clone() };
|
||||
oidc.restore_registered_client(authentication_server, oidc_metadata, credentials);
|
||||
|
||||
tracing::info!("Persisting OIDC registration data.");
|
||||
self.store_client_registration(oidc).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stores the current OIDC dynamic client registration so it can be re-used
|
||||
/// if we ever log in via the same issuer again.
|
||||
async fn store_client_registration(&self, oidc: &Oidc) -> Result<(), AuthenticationError> {
|
||||
let issuer = Url::parse(oidc.issuer().ok_or(AuthenticationError::OidcNotSupported)?)
|
||||
.map_err(|_| AuthenticationError::OidcError {
|
||||
message: String::from("Failed to parse issuer URL."),
|
||||
})?;
|
||||
let client_id = oidc
|
||||
.client_credentials()
|
||||
.ok_or(AuthenticationError::OidcError {
|
||||
message: String::from("Missing client registration."),
|
||||
})?
|
||||
.client_id()
|
||||
.to_owned();
|
||||
|
||||
let metadata = oidc.client_metadata().ok_or(AuthenticationError::OidcError {
|
||||
message: String::from("Missing client metadata."),
|
||||
})?;
|
||||
|
||||
let registrations = OidcRegistrations::new(
|
||||
&self.base_path,
|
||||
metadata.clone(),
|
||||
self.oidc_static_registrations(),
|
||||
)?;
|
||||
registrations.set_and_write_client_id(ClientId(client_id), issuer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempts to load an existing OIDC dynamic client registration for the
|
||||
/// currently configured issuer.
|
||||
async fn load_client_registration(
|
||||
&self,
|
||||
oidc: &Oidc,
|
||||
authentication_server: &AuthenticationServerInfo,
|
||||
oidc_metadata: VerifiedClientMetadata,
|
||||
) -> bool {
|
||||
let Ok(issuer) = Url::parse(&authentication_server.issuer) else {
|
||||
tracing::error!("Failed to parse {:?}", authentication_server.issuer);
|
||||
return false;
|
||||
};
|
||||
let Some(registrations) = OidcRegistrations::new(
|
||||
&self.base_path,
|
||||
oidc_metadata.clone(),
|
||||
self.oidc_static_registrations(),
|
||||
)
|
||||
.ok() else {
|
||||
return false;
|
||||
};
|
||||
let Some(client_id) = registrations.client_id(&issuer) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
oidc.restore_registered_client(
|
||||
authentication_server.clone(),
|
||||
oidc_metadata,
|
||||
ClientCredentials::None { client_id: client_id.0 },
|
||||
);
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Creates and verifies OIDC client metadata for the supplied OIDC
|
||||
/// configuration.
|
||||
fn oidc_metadata(
|
||||
&self,
|
||||
configuration: &OidcConfiguration,
|
||||
) -> Result<VerifiedClientMetadata, AuthenticationError> {
|
||||
let redirect_uri = Url::parse(&configuration.redirect_uri)
|
||||
.map_err(|_| AuthenticationError::OidcCallbackUrlInvalid)?;
|
||||
let client_name =
|
||||
configuration.client_name.as_ref().map(|n| Localized::new(n.to_owned(), []));
|
||||
let client_uri = configuration.client_uri.localized_url()?;
|
||||
let logo_uri = configuration.logo_uri.localized_url()?;
|
||||
let policy_uri = configuration.policy_uri.localized_url()?;
|
||||
let tos_uri = configuration.tos_uri.localized_url()?;
|
||||
let contacts = configuration.contacts.clone();
|
||||
|
||||
ClientMetadata {
|
||||
application_type: Some(ApplicationType::Native),
|
||||
redirect_uris: Some(vec![redirect_uri]),
|
||||
grant_types: Some(vec![GrantType::RefreshToken, GrantType::AuthorizationCode]),
|
||||
// A native client shouldn't use authentication as the credentials could be intercepted.
|
||||
token_endpoint_auth_method: Some(OAuthClientAuthenticationMethod::None),
|
||||
// The server should display the following fields when getting the user's consent.
|
||||
client_name,
|
||||
contacts,
|
||||
client_uri,
|
||||
logo_uri,
|
||||
policy_uri,
|
||||
tos_uri,
|
||||
..Default::default()
|
||||
}
|
||||
.validate()
|
||||
.map_err(|_| AuthenticationError::OidcMetadataInvalid)
|
||||
}
|
||||
|
||||
fn oidc_static_registrations(&self) -> HashMap<Url, ClientId> {
|
||||
let registrations = self
|
||||
.oidc_configuration
|
||||
.as_ref()
|
||||
.map(|c| c.static_registrations.clone())
|
||||
.unwrap_or_default();
|
||||
registrations
|
||||
.iter()
|
||||
.filter_map(|(issuer, client_id)| {
|
||||
let Ok(issuer) = Url::parse(issuer) else {
|
||||
tracing::error!("Failed to parse {:?}", issuer);
|
||||
return None;
|
||||
};
|
||||
Some((issuer, ClientId(client_id.clone())))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Creates a new client to setup the store path now the user ID is known.
|
||||
fn finalize_client(
|
||||
&self,
|
||||
client: Arc<Client>,
|
||||
session: impl Into<AuthSession>,
|
||||
user_id: OwnedUserId,
|
||||
) -> Result<Arc<Client>, AuthenticationError> {
|
||||
let homeserver_url = client.homeserver();
|
||||
|
||||
let sliding_sync_proxy: Option<String>;
|
||||
if let Some(custom_proxy) = self.custom_sliding_sync_proxy.read().unwrap().clone() {
|
||||
sliding_sync_proxy = Some(custom_proxy);
|
||||
} else if let Some(discovered_proxy) = client.discovered_sliding_sync_proxy() {
|
||||
sliding_sync_proxy = Some(discovered_proxy.to_string());
|
||||
} else {
|
||||
sliding_sync_proxy = None;
|
||||
}
|
||||
|
||||
let mut client = self
|
||||
.new_client_builder()
|
||||
.passphrase(self.passphrase.clone())
|
||||
.homeserver_url(homeserver_url)
|
||||
.sliding_sync_proxy(sliding_sync_proxy)
|
||||
.username(user_id.to_string());
|
||||
|
||||
if let Some(id) = &self.cross_process_refresh_lock_id {
|
||||
let Some(ref session_delegate) = self.session_delegate else {
|
||||
return Err(AuthenticationError::OidcError {
|
||||
message: "cross-process refresh lock requires session delegate".to_owned(),
|
||||
});
|
||||
};
|
||||
client = client
|
||||
.enable_cross_process_refresh_lock_inner(id.clone(), session_delegate.clone());
|
||||
} else if let Some(ref session_delegate) = self.session_delegate {
|
||||
client = client.set_session_delegate_inner(session_delegate.clone());
|
||||
}
|
||||
|
||||
let client = client.build_inner()?;
|
||||
|
||||
// Restore the client using the session from the login request.
|
||||
client.restore_session_inner(session)?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
|
||||
trait OptionExt {
|
||||
/// Convenience method to convert a string to a URL and returns it as a
|
||||
/// Localized URL. No localization is actually performed.
|
||||
fn localized_url(&self) -> Result<Option<Localized<Url>>, AuthenticationError>;
|
||||
}
|
||||
|
||||
impl OptionExt for Option<String> {
|
||||
fn localized_url(&self) -> Result<Option<Localized<Url>>, AuthenticationError> {
|
||||
self.as_deref()
|
||||
.map(|uri| -> Result<Localized<Url>, AuthenticationError> {
|
||||
Ok(Localized::new(
|
||||
Url::parse(uri).map_err(|_| AuthenticationError::OidcMetadataInvalid)?,
|
||||
[],
|
||||
))
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,3 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! A generic `ChunkIterator` that operates over a `Vec`.
|
||||
//!
|
||||
//! This type is not designed to work over FFI, but it can be embedded inside an
|
||||
|
||||
+513
-2496
File diff suppressed because it is too large
Load Diff
@@ -1,211 +1,57 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
use std::{fs, path::PathBuf, sync::Arc};
|
||||
|
||||
// Allow UniFFI to use methods marked as `#[deprecated]`.
|
||||
#![allow(deprecated)]
|
||||
|
||||
use std::{fs, num::NonZeroUsize, path::PathBuf, sync::Arc, time::Duration};
|
||||
|
||||
#[cfg(not(any(target_family = "wasm", target_os = "android")))]
|
||||
use matrix_sdk::reqwest::Certificate;
|
||||
use matrix_sdk::{
|
||||
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
|
||||
RumaApiError, ThreadingSupport,
|
||||
cross_process_lock::CrossProcessLockConfig as SdkCrossProcessLockConfig,
|
||||
encryption::{BackupDownloadStrategy, EncryptionSettings},
|
||||
event_cache::EventCacheError,
|
||||
ruma::{ServerName, UserId},
|
||||
search_index::SearchIndexStoreKind,
|
||||
sliding_sync::{
|
||||
Error as MatrixSlidingSyncError, VersionBuilder as MatrixSlidingSyncVersionBuilder,
|
||||
VersionBuilderError,
|
||||
ruma::{
|
||||
api::{error::UnknownVersionError, MatrixVersion},
|
||||
ServerName, UserId,
|
||||
},
|
||||
Client as MatrixClient, ClientBuilder as MatrixClientBuilder,
|
||||
};
|
||||
use matrix_sdk_base::crypto::{CollectStrategy, DecryptionSettings, TrustRequirement};
|
||||
use ruma::api::error::{DeserializationError, FromHttpResponseError};
|
||||
use tracing::debug;
|
||||
use sanitize_filename_reader_friendly::sanitize;
|
||||
use url::Url;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use super::client::Client;
|
||||
#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
|
||||
use crate::store;
|
||||
use crate::{
|
||||
client::ClientSessionDelegate,
|
||||
error::ClientError,
|
||||
helpers::unwrap_or_clone_arc,
|
||||
store::{StoreBuilder, StoreBuilderOutcome},
|
||||
};
|
||||
use super::{client::Client, RUNTIME};
|
||||
use crate::{client::ClientSessionDelegate, error::ClientError, helpers::unwrap_or_clone_arc};
|
||||
|
||||
/// A list of bytes containing a certificate in DER or PEM form.
|
||||
pub type CertificateBytes = Vec<u8>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum HomeserverConfig {
|
||||
Url(String),
|
||||
ServerName(String),
|
||||
ServerNameOrUrl(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum ClientBuildError {
|
||||
#[error("The supplied server name is invalid.")]
|
||||
InvalidServerName,
|
||||
#[error(transparent)]
|
||||
ServerUnreachable(HttpError),
|
||||
#[error(transparent)]
|
||||
WellKnownLookupFailed(RumaApiError),
|
||||
#[error(transparent)]
|
||||
WellKnownDeserializationError(DeserializationError),
|
||||
#[error(transparent)]
|
||||
#[allow(dead_code)] // rustc's drunk, this is used
|
||||
SlidingSync(MatrixSlidingSyncError),
|
||||
#[error(transparent)]
|
||||
SlidingSyncVersion(VersionBuilderError),
|
||||
#[error(transparent)]
|
||||
Sdk(MatrixClientBuildError),
|
||||
#[error(transparent)]
|
||||
EventCache(#[from] EventCacheError),
|
||||
#[error("Failed to build the client: {message}")]
|
||||
Generic { message: String },
|
||||
}
|
||||
|
||||
impl From<MatrixClientBuildError> for ClientBuildError {
|
||||
fn from(e: MatrixClientBuildError) -> Self {
|
||||
match e {
|
||||
MatrixClientBuildError::InvalidServerName => ClientBuildError::InvalidServerName,
|
||||
MatrixClientBuildError::Http(e) => ClientBuildError::ServerUnreachable(e),
|
||||
MatrixClientBuildError::AutoDiscovery(FromHttpResponseError::Server(e)) => {
|
||||
ClientBuildError::WellKnownLookupFailed(e)
|
||||
}
|
||||
MatrixClientBuildError::AutoDiscovery(FromHttpResponseError::Deserialization(e)) => {
|
||||
ClientBuildError::WellKnownDeserializationError(e)
|
||||
}
|
||||
MatrixClientBuildError::SlidingSyncVersion(e) => {
|
||||
ClientBuildError::SlidingSyncVersion(e)
|
||||
}
|
||||
_ => ClientBuildError::Sdk(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IdParseError> for ClientBuildError {
|
||||
fn from(e: IdParseError) -> ClientBuildError {
|
||||
ClientBuildError::Generic { message: format!("{e:#}") }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for ClientBuildError {
|
||||
fn from(e: std::io::Error) -> ClientBuildError {
|
||||
ClientBuildError::Generic { message: format!("{e:#}") }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<url::ParseError> for ClientBuildError {
|
||||
fn from(e: url::ParseError) -> ClientBuildError {
|
||||
ClientBuildError::Generic { message: format!("{e:#}") }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ClientError> for ClientBuildError {
|
||||
fn from(e: ClientError) -> ClientBuildError {
|
||||
ClientBuildError::Generic { message: format!("{e:#}") }
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum UrlScheme {
|
||||
Http,
|
||||
Https,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct ClientBuilder {
|
||||
store: Option<StoreBuilder>,
|
||||
system_is_memory_constrained: bool,
|
||||
base_path: Option<String>,
|
||||
username: Option<String>,
|
||||
homeserver_cfg: Option<HomeserverConfig>,
|
||||
sliding_sync_version_builder: SlidingSyncVersionBuilder,
|
||||
disable_automatic_token_refresh: bool,
|
||||
cross_process_lock_config: CrossProcessLockConfig,
|
||||
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
|
||||
encryption_settings: EncryptionSettings,
|
||||
room_key_recipient_strategy: CollectStrategy,
|
||||
decryption_settings: DecryptionSettings,
|
||||
enable_share_history_on_invite: bool,
|
||||
request_config: Option<RequestConfig>,
|
||||
search_index_store: Option<SearchIndexStoreKind>,
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
server_name: Option<(String, UrlScheme)>,
|
||||
homeserver_url: Option<String>,
|
||||
server_versions: Option<Vec<String>>,
|
||||
passphrase: Zeroizing<Option<String>>,
|
||||
user_agent: Option<String>,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
sliding_sync_proxy: Option<String>,
|
||||
proxy: Option<String>,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
disable_ssl_verification: bool,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
disable_built_in_root_certificates: bool,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
additional_root_certificates: Vec<Vec<u8>>,
|
||||
|
||||
threading_support: ThreadingSupport,
|
||||
disable_automatic_token_refresh: bool,
|
||||
inner: MatrixClientBuilder,
|
||||
cross_process_refresh_lock_id: Option<String>,
|
||||
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
|
||||
}
|
||||
|
||||
/// The timeout applies to each read operation, and resets after a successful
|
||||
/// read. This is more appropriate for detecting stalled connections when the
|
||||
/// size isn’t known beforehand.
|
||||
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
impl ClientBuilder {
|
||||
#[uniffi::constructor]
|
||||
pub fn new() -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
store: None,
|
||||
system_is_memory_constrained: false,
|
||||
username: None,
|
||||
homeserver_cfg: None,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
user_agent: None,
|
||||
sliding_sync_version_builder: SlidingSyncVersionBuilder::None,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
proxy: None,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
disable_ssl_verification: false,
|
||||
disable_automatic_token_refresh: false,
|
||||
cross_process_lock_config: CrossProcessLockConfig::SingleProcess,
|
||||
session_delegate: None,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
additional_root_certificates: Default::default(),
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
disable_built_in_root_certificates: false,
|
||||
encryption_settings: EncryptionSettings {
|
||||
auto_enable_cross_signing: false,
|
||||
backup_download_strategy:
|
||||
matrix_sdk::encryption::BackupDownloadStrategy::AfterDecryptionFailure,
|
||||
auto_enable_backups: false,
|
||||
},
|
||||
room_key_recipient_strategy: Default::default(),
|
||||
decryption_settings: DecryptionSettings {
|
||||
sender_device_trust_requirement: TrustRequirement::Untrusted,
|
||||
},
|
||||
enable_share_history_on_invite: false,
|
||||
request_config: Default::default(),
|
||||
threading_support: ThreadingSupport::Disabled,
|
||||
search_index_store: None,
|
||||
})
|
||||
Arc::new(Self::default())
|
||||
}
|
||||
|
||||
pub fn cross_process_lock_config(
|
||||
pub fn enable_cross_process_refresh_lock(
|
||||
self: Arc<Self>,
|
||||
cross_process_lock_config: CrossProcessLockConfig,
|
||||
process_id: String,
|
||||
session_delegate: Box<dyn ClientSessionDelegate>,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.cross_process_lock_config = cross_process_lock_config;
|
||||
Arc::new(builder)
|
||||
self.enable_cross_process_refresh_lock_inner(process_id, session_delegate.into())
|
||||
}
|
||||
|
||||
pub fn set_session_delegate(
|
||||
@@ -217,16 +63,9 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Tell the client that the system is memory constrained, like in a push
|
||||
/// notification process for example.
|
||||
///
|
||||
/// So far, at the time of writing (2025-04-07), it changes the defaults of
|
||||
/// `matrix_sdk::SqliteStoreConfig` (if the `sqlite` feature is enabled).
|
||||
/// Please check
|
||||
/// `matrix_sdk::SqliteStoreConfig::with_low_memory_config`.
|
||||
pub fn system_is_memory_constrained(self: Arc<Self>) -> Arc<Self> {
|
||||
pub fn base_path(self: Arc<Self>, path: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.system_is_memory_constrained = true;
|
||||
builder.base_path = Some(path);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
@@ -236,30 +75,52 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn server_versions(self: Arc<Self>, versions: Vec<String>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.server_versions = Some(versions);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn server_name(self: Arc<Self>, server_name: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.homeserver_cfg = Some(HomeserverConfig::ServerName(server_name));
|
||||
// Assume HTTPS if no protocol is provided.
|
||||
builder.server_name = Some((server_name, UrlScheme::Https));
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn homeserver_url(self: Arc<Self>, url: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.homeserver_cfg = Some(HomeserverConfig::Url(url));
|
||||
builder.homeserver_url = Some(url);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn server_name_or_homeserver_url(self: Arc<Self>, server_name_or_url: String) -> Arc<Self> {
|
||||
pub fn passphrase(self: Arc<Self>, passphrase: Option<String>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.homeserver_cfg = Some(HomeserverConfig::ServerNameOrUrl(server_name_or_url));
|
||||
builder.passphrase = Zeroizing::new(passphrase);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn sliding_sync_version_builder(
|
||||
self: Arc<Self>,
|
||||
version_builder: SlidingSyncVersionBuilder,
|
||||
) -> Arc<Self> {
|
||||
pub fn user_agent(self: Arc<Self>, user_agent: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.sliding_sync_version_builder = version_builder;
|
||||
builder.user_agent = Some(user_agent);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn sliding_sync_proxy(self: Arc<Self>, sliding_sync_proxy: Option<String>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.sliding_sync_proxy = sliding_sync_proxy;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn proxy(self: Arc<Self>, url: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.proxy = Some(url);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn disable_ssl_verification(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.disable_ssl_verification = true;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
@@ -269,431 +130,148 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn auto_enable_cross_signing(
|
||||
pub fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientError> {
|
||||
Ok(self.build_inner()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientBuilder {
|
||||
pub(crate) fn enable_cross_process_refresh_lock_inner(
|
||||
self: Arc<Self>,
|
||||
auto_enable_cross_signing: bool,
|
||||
process_id: String,
|
||||
session_delegate: Arc<dyn ClientSessionDelegate>,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.encryption_settings.auto_enable_cross_signing = auto_enable_cross_signing;
|
||||
builder.cross_process_refresh_lock_id = Some(process_id);
|
||||
builder.session_delegate = Some(session_delegate);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Select a strategy to download room keys from the backup. By default
|
||||
/// we download after a decryption failure.
|
||||
///
|
||||
/// Take a look at the [`BackupDownloadStrategy`] enum for more options.
|
||||
pub fn backup_download_strategy(
|
||||
pub(crate) fn set_session_delegate_inner(
|
||||
self: Arc<Self>,
|
||||
backup_download_strategy: BackupDownloadStrategy,
|
||||
session_delegate: Arc<dyn ClientSessionDelegate>,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.encryption_settings.backup_download_strategy = backup_download_strategy;
|
||||
builder.session_delegate = Some(session_delegate);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Automatically create a backup version if no backup exists.
|
||||
pub fn auto_enable_backups(self: Arc<Self>, auto_enable_backups: bool) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.encryption_settings.auto_enable_backups = auto_enable_backups;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set the strategy to be used for picking recipient devices when sending
|
||||
/// an encrypted message.
|
||||
pub fn room_key_recipient_strategy(self: Arc<Self>, strategy: CollectStrategy) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.room_key_recipient_strategy = strategy;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set the trust requirement to be used when decrypting events.
|
||||
pub fn decryption_settings(
|
||||
pub(crate) fn server_name_with_protocol(
|
||||
self: Arc<Self>,
|
||||
decryption_settings: DecryptionSettings,
|
||||
server_name: String,
|
||||
protocol: UrlScheme,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.decryption_settings = decryption_settings;
|
||||
builder.server_name = Some((server_name, protocol));
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set whether to enable the experimental support for sending and receiving
|
||||
/// encrypted room history on invite, per [MSC4268].
|
||||
///
|
||||
/// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
|
||||
pub fn enable_share_history_on_invite(
|
||||
self: Arc<Self>,
|
||||
enable_share_history_on_invite: bool,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.enable_share_history_on_invite = enable_share_history_on_invite;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Add a default request config to this client.
|
||||
pub fn request_config(self: Arc<Self>, config: RequestConfig) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.request_config = Some(config);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Whether the client should support threads client-side or not, and enable
|
||||
/// experimental support for MSC4306 (threads subscriptions) or not.
|
||||
pub fn threads_enabled(
|
||||
self: Arc<Self>,
|
||||
enabled: bool,
|
||||
thread_subscriptions: bool,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
let support = if enabled {
|
||||
ThreadingSupport::Enabled { with_subscriptions: thread_subscriptions }
|
||||
} else {
|
||||
ThreadingSupport::Disabled
|
||||
};
|
||||
builder.threading_support = support;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Use in-memory session storage.
|
||||
pub fn in_memory_store(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.store = Some(StoreBuilder::InMemory);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set up the search index store for this client, which is used to store
|
||||
/// the message search index locally.
|
||||
///
|
||||
/// As soon as this is enabled, messages will start to be indexed, and can
|
||||
/// be later queried for search.
|
||||
///
|
||||
/// `path` is the directory where the search index will be stored. It must
|
||||
/// be unique per session.
|
||||
///
|
||||
/// `password` is an optional password to encrypt the search index at rest.
|
||||
/// If `None`, the search index will be stored unencrypted.
|
||||
pub fn with_search_index_store(
|
||||
self: Arc<Self>,
|
||||
path: String,
|
||||
password: Option<String>,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
|
||||
// Note: creation of the path is deferred to later.
|
||||
let path = PathBuf::from(path);
|
||||
|
||||
let kind = if let Some(password) = password {
|
||||
SearchIndexStoreKind::EncryptedDirectory(path, password)
|
||||
} else {
|
||||
SearchIndexStoreKind::UnencryptedDirectory(path)
|
||||
};
|
||||
|
||||
builder.search_index_store = Some(kind);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub async fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientBuildError> {
|
||||
pub(crate) fn build_inner(self: Arc<Self>) -> anyhow::Result<Arc<Client>> {
|
||||
let builder = unwrap_or_clone_arc(self);
|
||||
let mut inner_builder = MatrixClient::builder()
|
||||
.cross_process_store_config(builder.cross_process_lock_config.into());
|
||||
let mut inner_builder = builder.inner;
|
||||
|
||||
let store_path = if let Some(store) = &builder.store {
|
||||
match store.build()? {
|
||||
#[cfg(feature = "sqlite")]
|
||||
StoreBuilderOutcome::Sqlite { config, cache_path, store_path: data_path } => {
|
||||
inner_builder = inner_builder
|
||||
.sqlite_store_with_config_and_cache_path(config, Some(cache_path));
|
||||
if let (Some(base_path), Some(username)) = (builder.base_path, &builder.username) {
|
||||
// Determine store path
|
||||
let data_path = PathBuf::from(base_path).join(sanitize(username));
|
||||
fs::create_dir_all(&data_path)?;
|
||||
|
||||
Some(data_path)
|
||||
}
|
||||
#[cfg(feature = "indexeddb")]
|
||||
StoreBuilderOutcome::IndexedDb { name, passphrase } => {
|
||||
inner_builder = inner_builder.indexeddb_store(&name, passphrase.as_deref());
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
StoreBuilderOutcome::InMemory => None,
|
||||
}
|
||||
} else {
|
||||
debug!("Not using a session store");
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(search_index_store) = builder.search_index_store {
|
||||
// Create the search index directory.
|
||||
match search_index_store {
|
||||
SearchIndexStoreKind::UnencryptedDirectory(ref path)
|
||||
| SearchIndexStoreKind::EncryptedDirectory(ref path, _) => {
|
||||
fs::create_dir_all(path)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Configure the inner builder to use the search index store.
|
||||
inner_builder = inner_builder.search_index_store(search_index_store);
|
||||
inner_builder = inner_builder.sqlite_store(&data_path, builder.passphrase.as_deref());
|
||||
}
|
||||
|
||||
// Determine server either from URL, server name or user ID.
|
||||
inner_builder = match builder.homeserver_cfg {
|
||||
Some(HomeserverConfig::Url(url)) => inner_builder.homeserver_url(url),
|
||||
Some(HomeserverConfig::ServerName(server_name)) => {
|
||||
let server_name = ServerName::parse(server_name)?;
|
||||
inner_builder.server_name(&server_name)
|
||||
}
|
||||
Some(HomeserverConfig::ServerNameOrUrl(server_name_or_url)) => {
|
||||
inner_builder.server_name_or_homeserver_url(server_name_or_url)
|
||||
}
|
||||
None => {
|
||||
if let Some(username) = builder.username {
|
||||
let user = UserId::parse(username)?;
|
||||
inner_builder.server_name(user.server_name())
|
||||
} else {
|
||||
return Err(ClientBuildError::Generic {
|
||||
message: "Failed to build: One of homeserver_url, server_name, server_name_or_homeserver_url or username must be called.".to_owned(),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Some(homeserver_url) = builder.homeserver_url {
|
||||
inner_builder = inner_builder.homeserver_url(homeserver_url);
|
||||
} else if let Some((server_name, protocol)) = builder.server_name {
|
||||
let server_name = ServerName::parse(server_name)?;
|
||||
inner_builder = match protocol {
|
||||
UrlScheme::Http => inner_builder.insecure_server_name_no_tls(&server_name),
|
||||
UrlScheme::Https => inner_builder.server_name(&server_name),
|
||||
};
|
||||
} else if let Some(username) = builder.username {
|
||||
let user = UserId::parse(username)?;
|
||||
inner_builder = inner_builder.server_name(user.server_name());
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"Failed to build: One of homeserver_url, server_name or username must be called."
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
inner_builder =
|
||||
inner_builder.add_raw_root_certificates(builder.additional_root_certificates)
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let mut certificates = Vec::new();
|
||||
for certificate in builder.additional_root_certificates {
|
||||
// We don't really know what type of certificate we may get here, so let's try
|
||||
// first one type, then the other.
|
||||
match Certificate::from_der(&certificate) {
|
||||
Ok(cert) => {
|
||||
certificates.push(cert);
|
||||
}
|
||||
Err(der_error) => {
|
||||
let cert = Certificate::from_pem(&certificate).map_err(|pem_error| {
|
||||
ClientBuildError::Generic {
|
||||
message: format!("Failed to add a root certificate as DER ({der_error:?}) or PEM ({pem_error:?})"),
|
||||
}
|
||||
})?;
|
||||
certificates.push(cert);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(proxy) = builder.proxy {
|
||||
inner_builder = inner_builder.proxy(proxy);
|
||||
}
|
||||
|
||||
inner_builder = inner_builder.add_root_certificates(certificates);
|
||||
}
|
||||
|
||||
if builder.disable_built_in_root_certificates {
|
||||
inner_builder = inner_builder.disable_built_in_root_certificates();
|
||||
}
|
||||
|
||||
if let Some(proxy) = builder.proxy {
|
||||
inner_builder = inner_builder.proxy(proxy);
|
||||
}
|
||||
|
||||
if builder.disable_ssl_verification {
|
||||
inner_builder = inner_builder.disable_ssl_verification();
|
||||
}
|
||||
|
||||
if let Some(user_agent) = builder.user_agent {
|
||||
inner_builder = inner_builder.user_agent(user_agent);
|
||||
}
|
||||
if builder.disable_ssl_verification {
|
||||
inner_builder = inner_builder.disable_ssl_verification();
|
||||
}
|
||||
|
||||
if !builder.disable_automatic_token_refresh {
|
||||
inner_builder = inner_builder.handle_refresh_tokens();
|
||||
}
|
||||
|
||||
inner_builder = inner_builder
|
||||
.with_encryption_settings(builder.encryption_settings)
|
||||
.with_room_key_recipient_strategy(builder.room_key_recipient_strategy)
|
||||
.with_decryption_settings(builder.decryption_settings)
|
||||
.with_enable_share_history_on_invite(builder.enable_share_history_on_invite);
|
||||
|
||||
match builder.sliding_sync_version_builder {
|
||||
SlidingSyncVersionBuilder::None => {
|
||||
inner_builder = inner_builder
|
||||
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::None)
|
||||
}
|
||||
SlidingSyncVersionBuilder::Native => {
|
||||
inner_builder = inner_builder
|
||||
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::Native)
|
||||
}
|
||||
SlidingSyncVersionBuilder::DiscoverNative => {
|
||||
inner_builder = inner_builder
|
||||
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::DiscoverNative)
|
||||
}
|
||||
if let Some(user_agent) = builder.user_agent {
|
||||
inner_builder = inner_builder.user_agent(user_agent);
|
||||
}
|
||||
|
||||
if let Some(config) = builder.request_config {
|
||||
let mut updated_config = matrix_sdk::config::RequestConfig::default();
|
||||
if let Some(retry_limit) = config.retry_limit {
|
||||
updated_config =
|
||||
updated_config.retry_limit(retry_limit.try_into().unwrap_or(usize::MAX));
|
||||
}
|
||||
if let Some(timeout) = config.timeout {
|
||||
updated_config = updated_config.timeout(Duration::from_millis(timeout));
|
||||
}
|
||||
updated_config = updated_config.read_timeout(DEFAULT_READ_TIMEOUT);
|
||||
if let Some(max_concurrent_requests) = config.max_concurrent_requests
|
||||
&& max_concurrent_requests > 0
|
||||
{
|
||||
updated_config = updated_config
|
||||
.max_concurrent_requests(NonZeroUsize::new(max_concurrent_requests as usize));
|
||||
}
|
||||
if let Some(max_retry_time) = config.max_retry_time {
|
||||
updated_config =
|
||||
updated_config.max_retry_time(Duration::from_millis(max_retry_time));
|
||||
}
|
||||
inner_builder = inner_builder.request_config(updated_config);
|
||||
if let Some(server_versions) = builder.server_versions {
|
||||
inner_builder = inner_builder.server_versions(
|
||||
server_versions
|
||||
.iter()
|
||||
.map(|s| MatrixVersion::try_from(s.as_str()))
|
||||
.collect::<Result<Vec<MatrixVersion>, UnknownVersionError>>()?,
|
||||
);
|
||||
}
|
||||
|
||||
inner_builder = inner_builder.with_threading_support(builder.threading_support);
|
||||
let sdk_client = RUNTIME.block_on(async move { inner_builder.build().await })?;
|
||||
|
||||
let sdk_client = inner_builder.build().await?;
|
||||
// At this point, `sdk_client` might contain a `sliding_sync_proxy` that has
|
||||
// been configured by the homeserver (if it's a `ServerName` and the
|
||||
// `.well-known` file is filled as expected).
|
||||
//
|
||||
// If `builder.sliding_sync_proxy` contains `Some(_)`, it means one wants to
|
||||
// overwrite this value. It would be an error to call
|
||||
// `sdk_client.set_sliding_sync_proxy()` with `None`, as it would erase the
|
||||
// `sliding_sync_proxy` if any, and it's not the intended behavior.
|
||||
//
|
||||
// So let's call `sdk_client.set_sliding_sync_proxy()` if and only if there is
|
||||
// `Some(_)` value in `builder.sliding_sync_proxy`. That's really important: It
|
||||
// might not break an existing app session, but it is likely to break a new
|
||||
// session, which not immediate to detect if there is no test.
|
||||
if let Some(sliding_sync_proxy) = builder.sliding_sync_proxy {
|
||||
sdk_client.set_sliding_sync_proxy(Some(Url::parse(&sliding_sync_proxy)?));
|
||||
}
|
||||
|
||||
Ok(Arc::new(Client::new(sdk_client, builder.session_delegate, store_path).await?))
|
||||
Ok(Client::new(
|
||||
sdk_client,
|
||||
builder.cross_process_refresh_lock_id,
|
||||
builder.session_delegate,
|
||||
)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl ClientBuilder {
|
||||
/// Use SQLite as the session storage.
|
||||
pub fn sqlite_store(self: Arc<Self>, config: Arc<store::SqliteStoreBuilder>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.store = Some(StoreBuilder::Sqlite(unwrap_or_clone_arc(config)));
|
||||
Arc::new(builder)
|
||||
}
|
||||
impl Default for ClientBuilder {
|
||||
fn default() -> Self {
|
||||
let encryption_settings = EncryptionSettings {
|
||||
auto_enable_cross_signing: true,
|
||||
auto_enable_backups: true,
|
||||
backup_download_strategy: BackupDownloadStrategy::AfterDecryptionFailure,
|
||||
};
|
||||
let inner = MatrixClient::builder().with_encryption_settings(encryption_settings);
|
||||
|
||||
/// Sets the paths that the client will use to store its data and caches
|
||||
/// with SQLite.
|
||||
///
|
||||
/// Both paths **must** be unique per session as the SDK
|
||||
/// stores aren't capable of handling multiple users, however it is
|
||||
/// valid to use the same path for both stores on a single session.
|
||||
#[deprecated = "Use `ClientBuilder::session_store_with_sqlite` instead"]
|
||||
pub fn session_paths(self: Arc<Self>, data_path: String, cache_path: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.store =
|
||||
Some(StoreBuilder::Sqlite(store::SqliteStoreBuilder::raw_new(data_path, cache_path)));
|
||||
Arc::new(builder)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "indexeddb")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl ClientBuilder {
|
||||
/// Use IndexedDB as the session storage.
|
||||
pub fn indexeddb_store(
|
||||
self: Arc<Self>,
|
||||
config: Arc<store::IndexedDbStoreBuilder>,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.store = Some(StoreBuilder::IndexedDb(unwrap_or_clone_arc(config)));
|
||||
Arc::new(builder)
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl ClientBuilder {
|
||||
pub fn proxy(self: Arc<Self>, url: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
builder.proxy = Some(url);
|
||||
}
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn disable_ssl_verification(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
builder.disable_ssl_verification = true;
|
||||
}
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn add_root_certificates(
|
||||
self: Arc<Self>,
|
||||
certificates: Vec<CertificateBytes>,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
builder.additional_root_certificates = certificates;
|
||||
}
|
||||
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Don't trust any system root certificates, only trust the certificates
|
||||
/// provided through
|
||||
/// [`add_root_certificates`][ClientBuilder::add_root_certificates].
|
||||
pub fn disable_built_in_root_certificates(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
builder.disable_built_in_root_certificates = true;
|
||||
}
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn user_agent(self: Arc<Self>, user_agent: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
builder.user_agent = Some(user_agent);
|
||||
}
|
||||
Arc::new(builder)
|
||||
}
|
||||
}
|
||||
|
||||
/// The config to use for HTTP requests by default in this client.
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct RequestConfig {
|
||||
/// Max number of retries.
|
||||
retry_limit: Option<u64>,
|
||||
/// Timeout for a request in milliseconds.
|
||||
timeout: Option<u64>,
|
||||
/// Max number of concurrent requests. No value means no limits.
|
||||
max_concurrent_requests: Option<u64>,
|
||||
/// Base delay between retries.
|
||||
max_retry_time: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum SlidingSyncVersionBuilder {
|
||||
None,
|
||||
Native,
|
||||
DiscoverNative,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, uniffi::Enum)]
|
||||
/// The cross-process lock config to use.
|
||||
pub enum CrossProcessLockConfig {
|
||||
/// The client will run using multiple processes.
|
||||
MultiProcess {
|
||||
/// The holder name to use for the lock.
|
||||
holder_name: String,
|
||||
},
|
||||
/// The client will run in a single process, there is no need for a
|
||||
/// cross-process lock.
|
||||
SingleProcess,
|
||||
}
|
||||
|
||||
impl From<CrossProcessLockConfig> for SdkCrossProcessLockConfig {
|
||||
fn from(lock_config: CrossProcessLockConfig) -> Self {
|
||||
match lock_config {
|
||||
CrossProcessLockConfig::MultiProcess { holder_name } => {
|
||||
SdkCrossProcessLockConfig::MultiProcess { holder_name }
|
||||
}
|
||||
CrossProcessLockConfig::SingleProcess => SdkCrossProcessLockConfig::SingleProcess,
|
||||
Self {
|
||||
base_path: None,
|
||||
username: None,
|
||||
server_name: None,
|
||||
homeserver_url: None,
|
||||
server_versions: None,
|
||||
passphrase: Zeroizing::new(None),
|
||||
user_agent: None,
|
||||
sliding_sync_proxy: None,
|
||||
proxy: None,
|
||||
disable_ssl_verification: false,
|
||||
disable_automatic_token_refresh: false,
|
||||
inner,
|
||||
cross_process_refresh_lock_id: None,
|
||||
session_delegate: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,66 +1,39 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::encryption::{self, backups, recovery};
|
||||
use matrix_sdk_base::crypto::types::{BackupSecrets, RoomKeyBackupInfo};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::OwnedUserId;
|
||||
use serde::de::Error;
|
||||
use matrix_sdk::encryption::{backups, recovery};
|
||||
use thiserror::Error;
|
||||
use tracing::{error, info};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{
|
||||
client::Client, error::ClientError, ruma::AuthData, runtime::get_runtime_handle,
|
||||
task_handle::TaskHandle,
|
||||
};
|
||||
use super::RUNTIME;
|
||||
use crate::{error::ClientError, task_handle::TaskHandle};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Encryption {
|
||||
pub(crate) inner: matrix_sdk::encryption::Encryption,
|
||||
|
||||
/// A reference to the FFI client.
|
||||
///
|
||||
/// Note: we do this to make it so that the FFI `NotificationClient` keeps
|
||||
/// the FFI `Client` and thus the SDK `Client` alive. Otherwise, we
|
||||
/// would need to repeat the hack done in the FFI `Client::drop` method.
|
||||
pub(crate) _client: Arc<Client>,
|
||||
inner: matrix_sdk::encryption::Encryption,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait BackupStateListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
impl From<matrix_sdk::encryption::Encryption> for Encryption {
|
||||
fn from(value: matrix_sdk::encryption::Encryption) -> Self {
|
||||
Self { inner: value }
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait BackupStateListener: Sync + Send {
|
||||
fn on_update(&self, status: BackupState);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait BackupSteadyStateListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait BackupSteadyStateListener: Sync + Send {
|
||||
fn on_update(&self, status: BackupUploadState);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RecoveryStateListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait RecoveryStateListener: Sync + Send {
|
||||
fn on_update(&self, status: RecoveryState);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait VerificationStateListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, status: VerificationState);
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum BackupUploadState {
|
||||
Waiting,
|
||||
@@ -93,14 +66,9 @@ pub enum RecoveryError {
|
||||
#[error(transparent)]
|
||||
Client { source: crate::ClientError },
|
||||
|
||||
/// Error in the secret storage subsystem, except for when importing a
|
||||
/// secret.
|
||||
/// Error in the secret storage subsystem.
|
||||
#[error("Error in the secret-storage subsystem: {error_message}")]
|
||||
SecretStorage { error_message: String },
|
||||
|
||||
/// Error when importing a secret from secret storage.
|
||||
#[error("Error importing a secret: {error_message}")]
|
||||
Import { error_message: String },
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::encryption::recovery::RecoveryError> for RecoveryError {
|
||||
@@ -108,9 +76,6 @@ impl From<matrix_sdk::encryption::recovery::RecoveryError> for RecoveryError {
|
||||
match value {
|
||||
recovery::RecoveryError::BackupExistsOnServer => Self::BackupExistsOnServer,
|
||||
recovery::RecoveryError::Sdk(e) => Self::Client { source: ClientError::from(e) },
|
||||
recovery::RecoveryError::SecretStorage(
|
||||
matrix_sdk::encryption::secret_storage::SecretStorageError::ImportError { .. },
|
||||
) => Self::Import { error_message: value.to_string() },
|
||||
recovery::RecoveryError::SecretStorage(e) => {
|
||||
Self::SecretStorage { error_message: e.to_string() }
|
||||
}
|
||||
@@ -188,8 +153,8 @@ impl From<recovery::RecoveryState> for RecoveryState {
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait EnableRecoveryProgressListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait EnableRecoveryProgressListener: Sync + Send {
|
||||
fn on_update(&self, status: EnableRecoveryProgress);
|
||||
}
|
||||
|
||||
@@ -221,260 +186,12 @@ impl From<recovery::EnableProgress> for EnableRecoveryProgress {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum VerificationState {
|
||||
Unknown,
|
||||
Verified,
|
||||
Unverified,
|
||||
}
|
||||
|
||||
impl From<encryption::VerificationState> for VerificationState {
|
||||
fn from(value: encryption::VerificationState) -> Self {
|
||||
match &value {
|
||||
encryption::VerificationState::Unknown => Self::Unknown,
|
||||
encryption::VerificationState::Verified => Self::Verified,
|
||||
encryption::VerificationState::Unverified => Self::Unverified,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct containing the bundle of secrets to fully activate a new device for
|
||||
/// end-to-end encryption.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SecretsBundleWithUserId {
|
||||
user_id: OwnedUserId,
|
||||
inner: matrix_sdk_base::crypto::types::SecretsBundle,
|
||||
}
|
||||
|
||||
/// Result for the check if a store has a valid secrets bundle.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum DetectedSecretsBundle {
|
||||
/// The store doesn't contain a secrets bundle at all.
|
||||
None,
|
||||
/// The store contains a bundle without a backup.
|
||||
WithoutBackup,
|
||||
/// The store contains a bundle with an unused backup, the backup key in the
|
||||
/// bundle isn't used on the homeserver.
|
||||
UnusedBackup,
|
||||
/// The store contains a complete secrets bundle.
|
||||
Complete,
|
||||
}
|
||||
|
||||
/// Error type describing failures that can happen while exporting a
|
||||
/// [`SecretsBundle`] from a SQLite store.
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
pub enum BundleExportError {
|
||||
/// The SQLite store couldn't be opened.
|
||||
#[error("the store couldn't be opened: {msg}")]
|
||||
OpenStoreError { msg: String },
|
||||
/// Data from the SQLite store couldn't be exported.
|
||||
#[error("the bundle couldn't be exported due to a storage error: {msg}")]
|
||||
StoreError { msg: String },
|
||||
/// The store doesn't contain a secrets bundle or it couldn't be read from
|
||||
/// the store.
|
||||
#[error("the bundle couldn't be exported: {msg}")]
|
||||
SecretError { msg: String },
|
||||
/// The store is empty and doesn't contain a secrets bundle.
|
||||
#[error("the store is completely empty")]
|
||||
StoreEmpty,
|
||||
/// A JSON object couldn't be deserialized while the secrets bundle was
|
||||
/// exported.
|
||||
#[error("Couldn't deserialize a JSON value: {msg}")]
|
||||
Json { msg: String },
|
||||
/// Error returned when the secrets bundle is missing a backup key or
|
||||
/// includes one that doesn’t match the key configured for the active backup
|
||||
/// version.
|
||||
#[error(
|
||||
"The bundle is missing a backup key or has one that isn't the one that's currently used"
|
||||
)]
|
||||
InvalidBackup,
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
impl From<matrix_sdk::encryption::BundleExportError> for BundleExportError {
|
||||
fn from(value: matrix_sdk::encryption::BundleExportError) -> Self {
|
||||
match value {
|
||||
matrix_sdk::encryption::BundleExportError::OpenStoreError(e) => {
|
||||
BundleExportError::OpenStoreError { msg: e.to_string() }
|
||||
}
|
||||
matrix_sdk::encryption::BundleExportError::StoreError(e) => {
|
||||
BundleExportError::StoreError { msg: e.to_string() }
|
||||
}
|
||||
matrix_sdk::encryption::BundleExportError::SecretExport(e) => {
|
||||
BundleExportError::SecretError { msg: e.to_string() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for BundleExportError {
|
||||
fn from(value: serde_json::Error) -> Self {
|
||||
Self::Json { msg: value.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SecretsBundleWithUserId {
|
||||
/// Attempt to export a [`SecretsBundle`] from a crypto store.
|
||||
///
|
||||
/// This method can be used to retrieve a [`SecretsBundle`] from an existing
|
||||
/// `matrix-sdk`-based client in order to import the [`SecretsBundle`] in
|
||||
/// another [`Client`] instance.
|
||||
///
|
||||
/// This can be useful for migration purposes or to allow existing client
|
||||
/// instances create new ones that will be fully verified.
|
||||
#[uniffi::constructor]
|
||||
pub async fn from_database(
|
||||
database_path: &str,
|
||||
mut passphrase: Option<String>,
|
||||
backup_info: &str,
|
||||
) -> Result<Arc<Self>, BundleExportError> {
|
||||
let backup_info = serde_json::from_str(backup_info)?;
|
||||
|
||||
let ret = if let Some((user_id, bundle)) =
|
||||
matrix_sdk::encryption::export_secrets_bundle_from_store(
|
||||
database_path,
|
||||
passphrase.as_deref(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
let is_backup_ok =
|
||||
bundle.backup.as_ref().is_some_and(|backup| is_valid_backup(backup, &backup_info));
|
||||
|
||||
if is_backup_ok {
|
||||
Ok(SecretsBundleWithUserId { user_id, inner: bundle }.into())
|
||||
} else {
|
||||
Err(BundleExportError::InvalidBackup)
|
||||
}
|
||||
} else {
|
||||
Err(BundleExportError::StoreEmpty)
|
||||
};
|
||||
|
||||
passphrase.zeroize();
|
||||
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SecretsBundleWithUserId {
|
||||
/// Attempt to create a [`SecretsBundle`] from a previously JSON serialized
|
||||
/// bundle.
|
||||
#[uniffi::constructor]
|
||||
pub fn from_str(
|
||||
user_id: &str,
|
||||
bundle: &str,
|
||||
backup_info: &str,
|
||||
) -> Result<Arc<Self>, BundleExportError> {
|
||||
let user_id =
|
||||
OwnedUserId::from_str(user_id).map_err(|e| serde_json::Error::custom(e.to_string()))?;
|
||||
let bundle: matrix_sdk_base::crypto::types::SecretsBundle = serde_json::from_str(bundle)?;
|
||||
let backup_info = serde_json::from_str(backup_info)?;
|
||||
|
||||
let is_backup_ok =
|
||||
bundle.backup.as_ref().is_some_and(|backup| is_valid_backup(backup, &backup_info));
|
||||
|
||||
if is_backup_ok {
|
||||
Ok(Self { user_id, inner: bundle }.into())
|
||||
} else {
|
||||
Err(BundleExportError::InvalidBackup)
|
||||
}
|
||||
}
|
||||
|
||||
/// Does the bundle contain a backup key.
|
||||
///
|
||||
/// Since enabling a backup is optional, the backup key might be missing
|
||||
/// from the bundle. Returns `false` if the backup key is missing,
|
||||
/// otherwise `true`.
|
||||
pub fn contains_backup_key(&self) -> bool {
|
||||
self.inner.backup.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid_backup(secrets: &BackupSecrets, info: &RoomKeyBackupInfo) -> bool {
|
||||
match secrets {
|
||||
BackupSecrets::MegolmBackupV1Curve25519AesSha2(secrets) => {
|
||||
secrets.key.backup_key_matches(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_bundle_and_info(
|
||||
bundle: &matrix_sdk_base::crypto::types::SecretsBundle,
|
||||
info: Option<&RoomKeyBackupInfo>,
|
||||
) -> DetectedSecretsBundle {
|
||||
match (&bundle.backup, info) {
|
||||
(None, None) => DetectedSecretsBundle::WithoutBackup,
|
||||
(None, Some(_)) => DetectedSecretsBundle::WithoutBackup,
|
||||
(Some(_), None) => DetectedSecretsBundle::UnusedBackup,
|
||||
(Some(backup), Some(info)) => {
|
||||
if is_valid_backup(backup, info) {
|
||||
DetectedSecretsBundle::Complete
|
||||
} else {
|
||||
DetectedSecretsBundle::UnusedBackup
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a JSON encoded string contains a valid [`SecretsBundle`].
|
||||
#[uniffi::export]
|
||||
pub fn json_string_contains_secrets_bundle(
|
||||
bundle: &str,
|
||||
backup_info: Option<String>,
|
||||
) -> Result<DetectedSecretsBundle, ClientError> {
|
||||
let info: Option<RoomKeyBackupInfo> =
|
||||
backup_info.map(|info| serde_json::from_str(&info)).transpose()?;
|
||||
|
||||
let bundle: matrix_sdk_base::crypto::types::SecretsBundle = serde_json::from_str(bundle)?;
|
||||
|
||||
Ok(check_bundle_and_info(&bundle, info.as_ref()))
|
||||
}
|
||||
|
||||
/// Check if a crypto store contains a valid [`SecretsBundle`].
|
||||
#[cfg(feature = "sqlite")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub async fn database_contains_secrets_bundle(
|
||||
database_path: &str,
|
||||
mut passphrase: Option<String>,
|
||||
backup_info: Option<String>,
|
||||
) -> Result<DetectedSecretsBundle, BundleExportError> {
|
||||
let info: Option<RoomKeyBackupInfo> =
|
||||
backup_info.map(|info| serde_json::from_str(&info)).transpose()?;
|
||||
|
||||
let maybe_bundle = matrix_sdk::encryption::export_secrets_bundle_from_store(
|
||||
database_path,
|
||||
passphrase.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
passphrase.zeroize();
|
||||
|
||||
Ok(match maybe_bundle {
|
||||
Some((_, bundle)) => check_bundle_and_info(&bundle, info.as_ref()),
|
||||
None => DetectedSecretsBundle::None,
|
||||
})
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl Encryption {
|
||||
/// Get the public ed25519 key of our own device. This is usually what is
|
||||
/// called the fingerprint of the device.
|
||||
pub async fn ed25519_key(&self) -> Option<String> {
|
||||
self.inner.ed25519_key().await
|
||||
}
|
||||
|
||||
/// Get the public curve25519 key of our own device in base64. This is
|
||||
/// usually what is called the identity key of the device.
|
||||
pub async fn curve25519_key(&self) -> Option<String> {
|
||||
self.inner.curve25519_key().await.map(|k| k.to_base64())
|
||||
}
|
||||
|
||||
pub fn backup_state_listener(&self, listener: Box<dyn BackupStateListener>) -> Arc<TaskHandle> {
|
||||
let mut stream = self.inner.backups().state_stream();
|
||||
|
||||
let stream_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
let stream_task = TaskHandle::new(RUNTIME.spawn(async move {
|
||||
while let Some(state) = stream.next().await {
|
||||
let Ok(state) = state else { continue };
|
||||
listener.on_update(state.into());
|
||||
@@ -498,7 +215,7 @@ impl Encryption {
|
||||
/// Therefore it is necessary to poll the server for an answer every time
|
||||
/// you want to differentiate between those two states.
|
||||
pub async fn backup_exists_on_server(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.backups().fetch_exists_on_server().await?)
|
||||
Ok(self.inner.backups().exists_on_server().await?)
|
||||
}
|
||||
|
||||
pub fn recovery_state(&self) -> RecoveryState {
|
||||
@@ -511,7 +228,7 @@ impl Encryption {
|
||||
) -> Arc<TaskHandle> {
|
||||
let mut stream = self.inner.recovery().state_stream();
|
||||
|
||||
let stream_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
let stream_task = TaskHandle::new(RUNTIME.spawn(async move {
|
||||
while let Some(state) = stream.next().await {
|
||||
listener.on_update(state.into());
|
||||
}
|
||||
@@ -525,16 +242,7 @@ impl Encryption {
|
||||
}
|
||||
|
||||
pub async fn is_last_device(&self) -> Result<bool> {
|
||||
Ok(self.inner.recovery().is_last_device().await?)
|
||||
}
|
||||
|
||||
/// Does the user have other devices that the current device can verify
|
||||
/// against?
|
||||
///
|
||||
/// The device must be signed by the user's cross-signing key, must have an
|
||||
/// identity, and must not be a dehydrated device.
|
||||
pub async fn has_devices_to_verify_against(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.has_devices_to_verify_against().await?)
|
||||
Ok(self.inner.recovery().are_we_the_last_man_standing().await?)
|
||||
}
|
||||
|
||||
pub async fn wait_for_backup_upload_steady_state(
|
||||
@@ -547,7 +255,7 @@ impl Encryption {
|
||||
let task = if let Some(listener) = progress_listener {
|
||||
let mut progress_stream = wait_for_steady_state.subscribe_to_progress();
|
||||
|
||||
Some(get_runtime_handle().spawn(async move {
|
||||
Some(RUNTIME.spawn(async move {
|
||||
while let Some(progress) = progress_stream.next().await {
|
||||
let Ok(progress) = progress else { continue };
|
||||
listener.on_update(progress.into());
|
||||
@@ -569,7 +277,6 @@ impl Encryption {
|
||||
pub async fn enable_recovery(
|
||||
&self,
|
||||
wait_for_backups_to_upload: bool,
|
||||
mut passphrase: Option<String>,
|
||||
progress_listener: Box<dyn EnableRecoveryProgressListener>,
|
||||
) -> Result<String> {
|
||||
let recovery = self.inner.recovery();
|
||||
@@ -580,15 +287,9 @@ impl Encryption {
|
||||
recovery.enable()
|
||||
};
|
||||
|
||||
let enable = if let Some(passphrase) = &passphrase {
|
||||
enable.with_passphrase(passphrase)
|
||||
} else {
|
||||
enable
|
||||
};
|
||||
|
||||
let mut progress_stream = enable.subscribe_to_progress();
|
||||
|
||||
let task = get_runtime_handle().spawn(async move {
|
||||
let task = RUNTIME.spawn(async move {
|
||||
while let Some(progress) = progress_stream.next().await {
|
||||
let Ok(progress) = progress else { continue };
|
||||
progress_listener.on_update(progress.into());
|
||||
@@ -598,7 +299,6 @@ impl Encryption {
|
||||
let ret = enable.await?;
|
||||
|
||||
task.abort();
|
||||
passphrase.zeroize();
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
@@ -619,19 +319,6 @@ impl Encryption {
|
||||
Ok(result?)
|
||||
}
|
||||
|
||||
/// Completely reset the current user's crypto identity: reset the cross
|
||||
/// signing keys, delete the existing backup and recovery key.
|
||||
pub async fn reset_identity(&self) -> Result<Option<Arc<IdentityResetHandle>>, ClientError> {
|
||||
if let Some(reset_handle) =
|
||||
self.inner.recovery().reset_identity().await.map_err(ClientError::from_err)?
|
||||
{
|
||||
return Ok(Some(Arc::new(IdentityResetHandle { inner: reset_handle })));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Download identity and key backup information from Recovery
|
||||
pub async fn recover(&self, mut recovery_key: String) -> Result<()> {
|
||||
let result = self.inner.recovery().recover(&recovery_key).await;
|
||||
|
||||
@@ -639,254 +326,4 @@ impl Encryption {
|
||||
|
||||
Ok(result?)
|
||||
}
|
||||
|
||||
/// Download identity and key backup information from Recovery, and, if the
|
||||
/// key backup information is inconsistent, create a new key backup.
|
||||
///
|
||||
/// This will create a new key backup if:
|
||||
///
|
||||
/// * Key backup is enabled and the backup decryption key is missing from
|
||||
/// Recovery, or
|
||||
/// * Key backup is enabled and the backup decryption key does not match the
|
||||
/// public key
|
||||
pub async fn recover_and_fix_backup(&self, mut recovery_key: String) -> Result<()> {
|
||||
let result = self.inner.recovery().recover_and_fix_backup(&recovery_key).await;
|
||||
|
||||
recovery_key.zeroize();
|
||||
|
||||
Ok(result?)
|
||||
}
|
||||
|
||||
pub fn verification_state(&self) -> VerificationState {
|
||||
self.inner.verification_state().get().into()
|
||||
}
|
||||
|
||||
pub fn verification_state_listener(
|
||||
self: Arc<Self>,
|
||||
listener: Box<dyn VerificationStateListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let mut subscriber = self.inner.verification_state();
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(verification_state) = subscriber.next().await {
|
||||
listener.on_update(verification_state.into());
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Waits for end-to-end encryption initialization tasks to finish, if any
|
||||
/// was running in the background.
|
||||
pub async fn wait_for_e2ee_initialization_tasks(&self) {
|
||||
self.inner.wait_for_e2ee_initialization_tasks().await;
|
||||
}
|
||||
|
||||
/// Get the E2EE identity of a user.
|
||||
///
|
||||
/// This method always tries to fetch the identity from the store, which we
|
||||
/// only have if the user is tracked, meaning that we are both members
|
||||
/// of the same encrypted room. If no user is found locally, a request will
|
||||
/// be made to the homeserver unless `fallback_to_server` is set to `false`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user that the identity belongs to.
|
||||
/// * `fallback_to_server` - Should we request the user identity from the
|
||||
/// homeserver if one isn't found locally.
|
||||
///
|
||||
/// Returns a `UserIdentity` if one is found. Returns an error if there
|
||||
/// was an issue with the crypto store or with the request to the
|
||||
/// homeserver.
|
||||
///
|
||||
/// This will always return `None` if the client hasn't been logged in.
|
||||
pub async fn user_identity(
|
||||
&self,
|
||||
user_id: String,
|
||||
fallback_to_server: bool,
|
||||
) -> Result<Option<Arc<UserIdentity>>, ClientError> {
|
||||
match self.inner.get_user_identity(user_id.as_str().try_into()?).await {
|
||||
Ok(Some(identity)) => {
|
||||
return Ok(Some(Arc::new(UserIdentity { inner: identity })));
|
||||
}
|
||||
Ok(None) => {
|
||||
info!("No identity found in the store.");
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Failed fetching identity from the store: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
info!("Requesting identity from the server.");
|
||||
|
||||
if fallback_to_server {
|
||||
let identity = self.inner.request_user_identity(user_id.as_str().try_into()?).await?;
|
||||
Ok(identity.map(|identity| Arc::new(UserIdentity { inner: identity })))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// This method will import all the private cross-signing keys and
|
||||
/// the private part of a backup key and its accompanying version into the
|
||||
/// store.
|
||||
///
|
||||
/// Importing all the secrets will mark the device as verified and enable
|
||||
/// backups.
|
||||
///
|
||||
/// **Warning**: Only import this from a trusted source, i.e. if an existing
|
||||
/// device is sharing this with a new device.
|
||||
///
|
||||
/// **Warning*: Only call this method right after logging in and before the
|
||||
/// initial sync has been started.
|
||||
pub async fn import_secrets_bundle(
|
||||
&self,
|
||||
secrets_bundle: &SecretsBundleWithUserId,
|
||||
) -> Result<(), ClientError> {
|
||||
let user_id = self._client.inner.user_id().expect(
|
||||
"We should have a user ID available now, this is only called once we're logged in",
|
||||
);
|
||||
|
||||
if user_id == secrets_bundle.user_id {
|
||||
self.inner
|
||||
.import_secrets_bundle(&secrets_bundle.inner)
|
||||
.await
|
||||
.map_err(ClientError::from_err)?;
|
||||
|
||||
self.inner.wait_for_e2ee_initialization_tasks().await;
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ClientError::Generic {
|
||||
msg: "Secrets bundle does not belong to the user which was logged in".to_owned(),
|
||||
details: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The E2EE identity of a user.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct UserIdentity {
|
||||
inner: matrix_sdk::encryption::identities::UserIdentity,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl UserIdentity {
|
||||
/// Remember this identity, ensuring it does not result in a pin violation.
|
||||
///
|
||||
/// When we first see a user, we assume their cryptographic identity has not
|
||||
/// been tampered with by the homeserver or another entity with
|
||||
/// man-in-the-middle capabilities. We remember this identity and call this
|
||||
/// action "pinning".
|
||||
///
|
||||
/// If the identity presented for the user changes later on, the newly
|
||||
/// presented identity is considered to be in "pin violation". This
|
||||
/// method explicitly accepts the new identity, allowing it to replace
|
||||
/// the previously pinned one and bringing it out of pin violation.
|
||||
///
|
||||
/// UIs should display a warning to the user when encountering an identity
|
||||
/// which is not verified and is in pin violation.
|
||||
pub(crate) async fn pin(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.pin().await?)
|
||||
}
|
||||
|
||||
/// Get the public part of the Master key of this user identity.
|
||||
///
|
||||
/// The public part of the Master key is usually used to uniquely identify
|
||||
/// the identity.
|
||||
///
|
||||
/// Returns None if the master key does not actually contain any keys.
|
||||
pub(crate) fn master_key(&self) -> Option<String> {
|
||||
self.inner.master_key().get_first_key().map(|k| k.to_base64())
|
||||
}
|
||||
|
||||
/// Is the user identity considered to be verified.
|
||||
///
|
||||
/// If the identity belongs to another user, our own user identity needs to
|
||||
/// be verified as well for the identity to be considered to be verified.
|
||||
pub fn is_verified(&self) -> bool {
|
||||
self.inner.is_verified()
|
||||
}
|
||||
|
||||
/// True if we verified this identity at some point in the past.
|
||||
///
|
||||
/// To reset this latch back to `false`, one must call
|
||||
/// [`UserIdentity::withdraw_verification()`].
|
||||
pub fn was_previously_verified(&self) -> bool {
|
||||
self.inner.was_previously_verified()
|
||||
}
|
||||
|
||||
/// Remove the requirement for this identity to be verified.
|
||||
///
|
||||
/// If an identity was previously verified and is not anymore it will be
|
||||
/// reported to the user. In order to remove this notice users have to
|
||||
/// verify again or to withdraw the verification requirement.
|
||||
pub(crate) async fn withdraw_verification(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.withdraw_verification().await?)
|
||||
}
|
||||
|
||||
/// Was this identity previously verified, and is no longer?
|
||||
pub fn has_verification_violation(&self) -> bool {
|
||||
self.inner.has_verification_violation()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct IdentityResetHandle {
|
||||
pub(crate) inner: matrix_sdk::encryption::recovery::IdentityResetHandle,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl IdentityResetHandle {
|
||||
/// Get the underlying [`CrossSigningResetAuthType`] this identity reset
|
||||
/// process is using.
|
||||
pub fn auth_type(&self) -> CrossSigningResetAuthType {
|
||||
self.inner.auth_type().into()
|
||||
}
|
||||
|
||||
/// This method starts the identity reset process and
|
||||
/// will go through the following steps:
|
||||
///
|
||||
/// 1. Disable backing up room keys and delete the active backup
|
||||
/// 2. Disable recovery and delete secret storage
|
||||
/// 3. Go through the cross-signing key reset flow
|
||||
/// 4. Finally, re-enable key backups only if they were enabled before
|
||||
pub async fn reset(&self, auth: Option<AuthData>) -> Result<(), ClientError> {
|
||||
self.inner.reset(auth.map(Into::into)).await.map_err(ClientError::from_err)
|
||||
}
|
||||
|
||||
pub async fn cancel(&self) {
|
||||
self.inner.cancel().await;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum CrossSigningResetAuthType {
|
||||
/// The homeserver requires user-interactive authentication.
|
||||
Uiaa,
|
||||
// /// OIDC is used for authentication and the user needs to open a URL to
|
||||
// /// approve the upload of cross-signing keys.
|
||||
Oidc {
|
||||
info: OidcCrossSigningResetInfo,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<&matrix_sdk::encryption::CrossSigningResetAuthType> for CrossSigningResetAuthType {
|
||||
fn from(value: &matrix_sdk::encryption::CrossSigningResetAuthType) -> Self {
|
||||
match value {
|
||||
encryption::CrossSigningResetAuthType::Uiaa(_) => Self::Uiaa,
|
||||
encryption::CrossSigningResetAuthType::OAuth(info) => Self::Oidc { info: info.into() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct OidcCrossSigningResetInfo {
|
||||
/// The URL where the user can approve the reset of the cross-signing keys.
|
||||
pub approval_url: String,
|
||||
}
|
||||
|
||||
impl From<&matrix_sdk::encryption::OAuthCrossSigningResetInfo> for OidcCrossSigningResetInfo {
|
||||
fn from(value: &matrix_sdk::encryption::OAuthCrossSigningResetInfo) -> Self {
|
||||
Self { approval_url: value.approval_url.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,328 +1,117 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{collections::HashMap, error::Error, fmt, fmt::Display};
|
||||
use std::fmt::Display;
|
||||
|
||||
use matrix_sdk::{
|
||||
HttpError, IdParseError, NotificationSettingsError as SdkNotificationSettingsError,
|
||||
QueueWedgeError as SdkQueueWedgeError, StoreError,
|
||||
authentication::oauth::OAuthError,
|
||||
encryption::{CryptoStoreError, identities::RequestVerificationError},
|
||||
event_cache::EventCacheError,
|
||||
reqwest,
|
||||
room::{calls::CallError, edit::EditError},
|
||||
send_queue::RoomSendQueueError,
|
||||
};
|
||||
use matrix_sdk_ui::{encryption_sync_service, notification_client, spaces, sync_service, timeline};
|
||||
use ruma::{
|
||||
MilliSecondsSinceUnixEpoch,
|
||||
api::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter, StandardErrorBody},
|
||||
self, encryption::CryptoStoreError, oidc::OidcError, HttpError, IdParseError,
|
||||
NotificationSettingsError as SdkNotificationSettingsError, StoreError,
|
||||
};
|
||||
use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline};
|
||||
use uniffi::UnexpectedUniFFICallbackError;
|
||||
|
||||
use crate::{room_list::RoomListError, timeline::FocusEventError};
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ClientError {
|
||||
#[error("client error: {msg}")]
|
||||
Generic { msg: String, details: Option<String> },
|
||||
#[error("api error {code}: {msg}")]
|
||||
MatrixApi { kind: ErrorKind, code: String, msg: String, details: Option<String> },
|
||||
Generic { msg: String },
|
||||
}
|
||||
|
||||
impl ClientError {
|
||||
pub(crate) fn from_str<E: Display>(error: E, details: Option<String>) -> Self {
|
||||
Self::Generic { msg: error.to_string(), details }
|
||||
}
|
||||
|
||||
pub(crate) fn from_err<E: Error>(e: E) -> Self {
|
||||
let details = Some(format!("{e:?}"));
|
||||
Self::from_str(e, details)
|
||||
fn new<E: Display>(error: E) -> Self {
|
||||
Self::Generic { msg: error.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for ClientError {
|
||||
fn from(e: anyhow::Error) -> ClientError {
|
||||
let details = format!("{e:?}");
|
||||
ClientError::Generic { msg: format!("{e:#}"), details: Some(details) }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for ClientError {
|
||||
fn from(e: reqwest::Error) -> Self {
|
||||
Self::from_err(e)
|
||||
ClientError::Generic { msg: format!("{e:#}") }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UnexpectedUniFFICallbackError> for ClientError {
|
||||
fn from(e: UnexpectedUniFFICallbackError) -> Self {
|
||||
Self::from_err(e)
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::Error> for ClientError {
|
||||
fn from(e: matrix_sdk::Error) -> Self {
|
||||
match e {
|
||||
matrix_sdk::Error::Http(http_error) => {
|
||||
if let Some(api_error) = http_error.as_client_api_error()
|
||||
&& let ErrorBody::Standard(StandardErrorBody { kind, message, .. }) =
|
||||
&api_error.body
|
||||
{
|
||||
let code = kind.errcode().to_string();
|
||||
let Ok(kind) = kind.to_owned().try_into() else {
|
||||
// We couldn't parse the API error, so we return a generic one instead
|
||||
return (*http_error).into();
|
||||
};
|
||||
return Self::MatrixApi {
|
||||
kind,
|
||||
code,
|
||||
msg: message.to_owned(),
|
||||
details: Some(format!("{api_error:?}")),
|
||||
};
|
||||
}
|
||||
(*http_error).into()
|
||||
}
|
||||
_ => Self::from_err(e),
|
||||
}
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StoreError> for ClientError {
|
||||
fn from(e: StoreError) -> Self {
|
||||
Self::from_err(e)
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CryptoStoreError> for ClientError {
|
||||
fn from(e: CryptoStoreError) -> Self {
|
||||
Self::from_err(e)
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HttpError> for ClientError {
|
||||
fn from(e: HttpError) -> Self {
|
||||
Self::from_err(e)
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IdParseError> for ClientError {
|
||||
fn from(e: IdParseError) -> Self {
|
||||
Self::from_err(e)
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for ClientError {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
Self::from_err(e)
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<url::ParseError> for ClientError {
|
||||
fn from(e: url::ParseError) -> Self {
|
||||
Self::from_err(e)
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<mime::FromStrError> for ClientError {
|
||||
fn from(e: mime::FromStrError) -> Self {
|
||||
Self::from_err(e)
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<encryption_sync_service::Error> for ClientError {
|
||||
fn from(e: encryption_sync_service::Error) -> Self {
|
||||
Self::from_err(e)
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<timeline::Error> for ClientError {
|
||||
fn from(e: timeline::Error) -> Self {
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<timeline::UnsupportedEditItem> for ClientError {
|
||||
fn from(e: timeline::UnsupportedEditItem) -> Self {
|
||||
Self::from_err(e)
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<notification_client::Error> for ClientError {
|
||||
fn from(e: notification_client::Error) -> Self {
|
||||
Self::from_err(e)
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sync_service::Error> for ClientError {
|
||||
fn from(e: sync_service::Error) -> Self {
|
||||
Self::from_err(e)
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OAuthError> for ClientError {
|
||||
fn from(e: OAuthError) -> Self {
|
||||
Self::from_err(e)
|
||||
impl From<OidcError> for ClientError {
|
||||
fn from(e: OidcError) -> Self {
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomError> for ClientError {
|
||||
fn from(e: RoomError) -> Self {
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomListError> for ClientError {
|
||||
fn from(e: RoomListError) -> Self {
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EventCacheError> for ClientError {
|
||||
fn from(e: EventCacheError) -> Self {
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EditError> for ClientError {
|
||||
fn from(e: EditError) -> Self {
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CallError> for ClientError {
|
||||
fn from(e: CallError) -> Self {
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomSendQueueError> for ClientError {
|
||||
fn from(e: RoomSendQueueError) -> Self {
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NotYetImplemented> for ClientError {
|
||||
fn from(_: NotYetImplemented) -> Self {
|
||||
Self::from_str("This functionality is not implemented yet.", None)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FocusEventError> for ClientError {
|
||||
fn from(e: FocusEventError) -> Self {
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RequestVerificationError> for ClientError {
|
||||
fn from(e: RequestVerificationError) -> Self {
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<spaces::Error> for ClientError {
|
||||
fn from(e: spaces::Error) -> Self {
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// Bindings version of the sdk type replacing OwnedUserId/DeviceIds with simple
|
||||
/// String.
|
||||
///
|
||||
/// Represent a failed to send unrecoverable error of an event sent via the
|
||||
/// send_queue. It is a serializable representation of a client error, see
|
||||
/// `From` implementation for more details. These errors can not be
|
||||
/// automatically retried, but yet some manual action can be taken before retry
|
||||
/// sending. If not the only solution is to delete the local event.
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum QueueWedgeError {
|
||||
/// This error occurs when there are some insecure devices in the room, and
|
||||
/// the current encryption setting prohibit sharing with them.
|
||||
InsecureDevices {
|
||||
/// The insecure devices as a Map of userID to deviceID.
|
||||
user_device_map: HashMap<String, Vec<String>>,
|
||||
},
|
||||
|
||||
/// This error occurs when a previously verified user is not anymore, and
|
||||
/// the current encryption setting prohibit sharing when it happens.
|
||||
IdentityViolations {
|
||||
/// The users that are expected to be verified but are not.
|
||||
users: Vec<String>,
|
||||
},
|
||||
|
||||
/// It is required to set up cross-signing and properly erify the current
|
||||
/// session before sending.
|
||||
CrossVerificationRequired,
|
||||
|
||||
/// Some media content to be sent has disappeared from the cache.
|
||||
MissingMediaContent,
|
||||
|
||||
/// Some mime type couldn't be parsed.
|
||||
InvalidMimeType { mime_type: String },
|
||||
|
||||
/// Other errors.
|
||||
GenericApiError { msg: String },
|
||||
}
|
||||
|
||||
/// Simple display implementation that strips out userIds/DeviceIds to avoid
|
||||
/// accidental logging.
|
||||
impl Display for QueueWedgeError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
QueueWedgeError::InsecureDevices { .. } => {
|
||||
f.write_str("There are insecure devices in the room")
|
||||
}
|
||||
QueueWedgeError::IdentityViolations { .. } => {
|
||||
f.write_str("Some users that were previously verified are not anymore")
|
||||
}
|
||||
QueueWedgeError::CrossVerificationRequired => {
|
||||
f.write_str("Own verification is required")
|
||||
}
|
||||
QueueWedgeError::MissingMediaContent => {
|
||||
f.write_str("Media to be sent disappeared from local storage")
|
||||
}
|
||||
QueueWedgeError::InvalidMimeType { mime_type } => {
|
||||
write!(f, "Invalid mime type '{mime_type}' for media upload")
|
||||
}
|
||||
QueueWedgeError::GenericApiError { msg } => f.write_str(msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SdkQueueWedgeError> for QueueWedgeError {
|
||||
fn from(value: SdkQueueWedgeError) -> Self {
|
||||
match value {
|
||||
SdkQueueWedgeError::InsecureDevices { user_device_map } => Self::InsecureDevices {
|
||||
user_device_map: user_device_map
|
||||
.iter()
|
||||
.map(|(user_id, devices)| {
|
||||
(
|
||||
user_id.to_string(),
|
||||
devices.iter().map(|device_id| device_id.to_string()).collect(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
SdkQueueWedgeError::IdentityViolations { users } => Self::IdentityViolations {
|
||||
users: users.iter().map(ruma::OwnedUserId::to_string).collect(),
|
||||
},
|
||||
SdkQueueWedgeError::CrossVerificationRequired => Self::CrossVerificationRequired,
|
||||
SdkQueueWedgeError::MissingMediaContent => Self::MissingMediaContent,
|
||||
SdkQueueWedgeError::InvalidMimeType { mime_type } => {
|
||||
Self::InvalidMimeType { mime_type }
|
||||
}
|
||||
SdkQueueWedgeError::GenericApiError { msg } => Self::GenericApiError { msg },
|
||||
}
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,45 +128,10 @@ pub enum RoomError {
|
||||
TimelineUnavailable,
|
||||
#[error("Invalid thumbnail data")]
|
||||
InvalidThumbnailData,
|
||||
#[error("Invalid replied to event ID")]
|
||||
InvalidRepliedToEventId,
|
||||
#[error("Failed sending attachment")]
|
||||
FailedSendingAttachment,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum LiveLocationError {
|
||||
#[error("Network error")]
|
||||
Network,
|
||||
#[error("Existing beacon information not found")]
|
||||
NotFound,
|
||||
#[error("Beacon event is redacted and cannot be processed")]
|
||||
Redacted,
|
||||
#[error("Must join the room to access beacon information")]
|
||||
Stripped,
|
||||
#[error("The beacon event has expired")]
|
||||
NotLive,
|
||||
#[error("Deserialization error")]
|
||||
Deserialization,
|
||||
#[error("Other error: {msg}")]
|
||||
Other { msg: String },
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::BeaconError> for LiveLocationError {
|
||||
fn from(value: matrix_sdk::BeaconError) -> Self {
|
||||
match value {
|
||||
matrix_sdk::BeaconError::Network(_) => Self::Network,
|
||||
matrix_sdk::BeaconError::NotFound => Self::NotFound,
|
||||
matrix_sdk::BeaconError::Redacted => Self::Redacted,
|
||||
matrix_sdk::BeaconError::Stripped => Self::Stripped,
|
||||
matrix_sdk::BeaconError::Deserialization(_) => Self::Deserialization,
|
||||
matrix_sdk::BeaconError::NotLive => Self::NotLive,
|
||||
matrix_sdk::BeaconError::Other(err) => Self::Other { msg: err.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum MediaInfoError {
|
||||
@@ -432,452 +186,3 @@ impl From<matrix_sdk::Error> for NotificationSettingsError {
|
||||
Self::Generic { msg: e.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Something has not been implemented yet.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("not implemented yet")]
|
||||
pub struct NotYetImplemented;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, uniffi::Enum)]
|
||||
// Please keep the variants sorted alphabetically.
|
||||
pub enum ErrorKind {
|
||||
/// `M_BAD_ALIAS`
|
||||
///
|
||||
/// One or more [room aliases] within the `m.room.canonical_alias` event do
|
||||
/// not point to the room ID for which the state event is to be sent to.
|
||||
///
|
||||
/// [room aliases]: https://spec.matrix.org/latest/client-server-api/#room-aliases
|
||||
BadAlias,
|
||||
|
||||
/// `M_BAD_JSON`
|
||||
///
|
||||
/// The request contained valid JSON, but it was malformed in some way, e.g.
|
||||
/// missing required keys, invalid values for keys.
|
||||
BadJson,
|
||||
|
||||
/// `M_BAD_STATE`
|
||||
///
|
||||
/// The state change requested cannot be performed, such as attempting to
|
||||
/// unban a user who is not banned.
|
||||
BadState,
|
||||
|
||||
/// `M_BAD_STATUS`
|
||||
///
|
||||
/// The application service returned a bad status.
|
||||
BadStatus {
|
||||
/// The HTTP status code of the response.
|
||||
status: Option<u16>,
|
||||
|
||||
/// The body of the response.
|
||||
body: Option<String>,
|
||||
},
|
||||
|
||||
/// `M_CANNOT_LEAVE_SERVER_NOTICE_ROOM`
|
||||
///
|
||||
/// The user is unable to reject an invite to join the [server notices]
|
||||
/// room.
|
||||
///
|
||||
/// [server notices]: https://spec.matrix.org/latest/client-server-api/#server-notices
|
||||
CannotLeaveServerNoticeRoom,
|
||||
|
||||
/// `M_CANNOT_OVERWRITE_MEDIA`
|
||||
///
|
||||
/// The [`create_content_async`] endpoint was called with a media ID that
|
||||
/// already has content.
|
||||
///
|
||||
/// [`create_content_async`]: crate::media::create_content_async
|
||||
CannotOverwriteMedia,
|
||||
|
||||
/// `M_CAPTCHA_INVALID`
|
||||
///
|
||||
/// The Captcha provided did not match what was expected.
|
||||
CaptchaInvalid,
|
||||
|
||||
/// `M_CAPTCHA_NEEDED`
|
||||
///
|
||||
/// A Captcha is required to complete the request.
|
||||
CaptchaNeeded,
|
||||
|
||||
/// `M_CONNECTION_FAILED`
|
||||
///
|
||||
/// The connection to the application service failed.
|
||||
ConnectionFailed,
|
||||
|
||||
/// `M_CONNECTION_TIMEOUT`
|
||||
///
|
||||
/// The connection to the application service timed out.
|
||||
ConnectionTimeout,
|
||||
|
||||
/// `M_DUPLICATE_ANNOTATION`
|
||||
///
|
||||
/// The request is an attempt to send a [duplicate annotation].
|
||||
///
|
||||
/// [duplicate annotation]: https://spec.matrix.org/latest/client-server-api/#avoiding-duplicate-annotations
|
||||
DuplicateAnnotation,
|
||||
|
||||
/// `M_EXCLUSIVE`
|
||||
///
|
||||
/// The resource being requested is reserved by an application service, or
|
||||
/// the application service making the request has not created the
|
||||
/// resource.
|
||||
Exclusive,
|
||||
|
||||
/// `M_FORBIDDEN`
|
||||
///
|
||||
/// Forbidden access, e.g. joining a room without permission, failed login.
|
||||
Forbidden,
|
||||
|
||||
/// `M_GUEST_ACCESS_FORBIDDEN`
|
||||
///
|
||||
/// The room or resource does not permit [guests] to access it.
|
||||
///
|
||||
/// [guests]: https://spec.matrix.org/latest/client-server-api/#guest-access
|
||||
GuestAccessForbidden,
|
||||
|
||||
/// `M_INCOMPATIBLE_ROOM_VERSION`
|
||||
///
|
||||
/// The client attempted to join a room that has a version the server does
|
||||
/// not support.
|
||||
IncompatibleRoomVersion {
|
||||
/// The room's version.
|
||||
room_version: String,
|
||||
},
|
||||
|
||||
/// `M_INVALID_PARAM`
|
||||
///
|
||||
/// A parameter that was specified has the wrong value. For example, the
|
||||
/// server expected an integer and instead received a string.
|
||||
InvalidParam,
|
||||
|
||||
/// `M_INVALID_ROOM_STATE`
|
||||
///
|
||||
/// The initial state implied by the parameters to the [`create_room`]
|
||||
/// request is invalid, e.g. the user's `power_level` is set below that
|
||||
/// necessary to set the room name.
|
||||
///
|
||||
/// [`create_room`]: crate::room::create_room
|
||||
InvalidRoomState,
|
||||
|
||||
/// `M_INVALID_USERNAME`
|
||||
///
|
||||
/// The desired user name is not valid.
|
||||
InvalidUsername,
|
||||
|
||||
/// `M_LIMIT_EXCEEDED`
|
||||
///
|
||||
/// The request has been refused due to [rate limiting]: too many requests
|
||||
/// have been sent in a short period of time.
|
||||
///
|
||||
/// [rate limiting]: https://spec.matrix.org/latest/client-server-api/#rate-limiting
|
||||
LimitExceeded {
|
||||
/// How long a client should wait before they can try again.
|
||||
retry_after_ms: Option<u64>,
|
||||
},
|
||||
|
||||
/// `M_MISSING_PARAM`
|
||||
///
|
||||
/// A required parameter was missing from the request.
|
||||
MissingParam,
|
||||
|
||||
/// `M_MISSING_TOKEN`
|
||||
///
|
||||
/// No [access token] was specified for the request, but one is required.
|
||||
///
|
||||
/// [access token]: https://spec.matrix.org/latest/client-server-api/#client-authentication
|
||||
MissingToken,
|
||||
|
||||
/// `M_NOT_FOUND`
|
||||
///
|
||||
/// No resource was found for this request.
|
||||
NotFound,
|
||||
|
||||
/// `M_NOT_JSON`
|
||||
///
|
||||
/// The request did not contain valid JSON.
|
||||
NotJson,
|
||||
|
||||
/// `M_NOT_YET_UPLOADED`
|
||||
///
|
||||
/// An `mxc:` URI generated with the [`create_mxc_uri`] endpoint was used
|
||||
/// and the content is not yet available.
|
||||
///
|
||||
/// [`create_mxc_uri`]: crate::media::create_mxc_uri
|
||||
NotYetUploaded,
|
||||
|
||||
/// `M_RESOURCE_LIMIT_EXCEEDED`
|
||||
///
|
||||
/// The request cannot be completed because the homeserver has reached a
|
||||
/// resource limit imposed on it. For example, a homeserver held in a
|
||||
/// shared hosting environment may reach a resource limit if it starts
|
||||
/// using too much memory or disk space.
|
||||
ResourceLimitExceeded {
|
||||
/// A URI giving a contact method for the server administrator.
|
||||
admin_contact: String,
|
||||
},
|
||||
|
||||
/// `M_ROOM_IN_USE`
|
||||
///
|
||||
/// The [room alias] specified in the [`create_room`] request is already
|
||||
/// taken.
|
||||
///
|
||||
/// [`create_room`]: crate::room::create_room
|
||||
/// [room alias]: https://spec.matrix.org/latest/client-server-api/#room-aliases
|
||||
RoomInUse,
|
||||
|
||||
/// `M_SERVER_NOT_TRUSTED`
|
||||
///
|
||||
/// The client's request used a third-party server, e.g. identity server,
|
||||
/// that this server does not trust.
|
||||
ServerNotTrusted,
|
||||
|
||||
/// `M_THREEPID_AUTH_FAILED`
|
||||
///
|
||||
/// Authentication could not be performed on the [third-party identifier].
|
||||
///
|
||||
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
|
||||
ThreepidAuthFailed,
|
||||
|
||||
/// `M_THREEPID_DENIED`
|
||||
///
|
||||
/// The server does not permit this [third-party identifier]. This may
|
||||
/// happen if the server only permits, for example, email addresses from
|
||||
/// a particular domain.
|
||||
///
|
||||
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
|
||||
ThreepidDenied,
|
||||
|
||||
/// `M_THREEPID_IN_USE`
|
||||
///
|
||||
/// The [third-party identifier] is already in use by another user.
|
||||
///
|
||||
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
|
||||
ThreepidInUse,
|
||||
|
||||
/// `M_THREEPID_MEDIUM_NOT_SUPPORTED`
|
||||
///
|
||||
/// The homeserver does not support adding a [third-party identifier] of the
|
||||
/// given medium.
|
||||
///
|
||||
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
|
||||
ThreepidMediumNotSupported,
|
||||
|
||||
/// `M_THREEPID_NOT_FOUND`
|
||||
///
|
||||
/// No account matching the given [third-party identifier] could be found.
|
||||
///
|
||||
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
|
||||
ThreepidNotFound,
|
||||
|
||||
/// `M_TOO_LARGE`
|
||||
///
|
||||
/// The request or entity was too large.
|
||||
TooLarge,
|
||||
|
||||
/// `M_UNABLE_TO_AUTHORISE_JOIN`
|
||||
///
|
||||
/// The room is [restricted] and none of the conditions can be validated by
|
||||
/// the homeserver. This can happen if the homeserver does not know
|
||||
/// about any of the rooms listed as conditions, for example.
|
||||
///
|
||||
/// [restricted]: https://spec.matrix.org/latest/client-server-api/#restricted-rooms
|
||||
UnableToAuthorizeJoin,
|
||||
|
||||
/// `M_UNABLE_TO_GRANT_JOIN`
|
||||
///
|
||||
/// A different server should be attempted for the join. This is typically
|
||||
/// because the resident server can see that the joining user satisfies
|
||||
/// one or more conditions, such as in the case of [restricted rooms],
|
||||
/// but the resident server would be unable to meet the authorization
|
||||
/// rules.
|
||||
///
|
||||
/// [restricted rooms]: https://spec.matrix.org/latest/client-server-api/#restricted-rooms
|
||||
UnableToGrantJoin,
|
||||
|
||||
/// `M_UNAUTHORIZED`
|
||||
///
|
||||
/// The request was not correctly authorized. Usually due to login failures.
|
||||
Unauthorized,
|
||||
|
||||
/// `M_UNKNOWN`
|
||||
///
|
||||
/// An unknown error has occurred.
|
||||
Unknown,
|
||||
|
||||
/// `M_UNKNOWN_TOKEN`
|
||||
///
|
||||
/// The [access or refresh token] specified was not recognized.
|
||||
///
|
||||
/// [access or refresh token]: https://spec.matrix.org/latest/client-server-api/#client-authentication
|
||||
UnknownToken {
|
||||
/// If this is `true`, the client is in a "[soft logout]" state, i.e.
|
||||
/// the server requires re-authentication but the session is not
|
||||
/// invalidated. The client can acquire a new access token by
|
||||
/// specifying the device ID it is already using to the login API.
|
||||
///
|
||||
/// [soft logout]: https://spec.matrix.org/latest/client-server-api/#soft-logout
|
||||
soft_logout: bool,
|
||||
},
|
||||
|
||||
/// `M_UNRECOGNIZED`
|
||||
///
|
||||
/// The server did not understand the request.
|
||||
///
|
||||
/// This is expected to be returned with a 404 HTTP status code if the
|
||||
/// endpoint is not implemented or a 405 HTTP status code if the
|
||||
/// endpoint is implemented, but the incorrect HTTP method is used.
|
||||
Unrecognized,
|
||||
|
||||
/// `M_UNSUPPORTED_ROOM_VERSION`
|
||||
///
|
||||
/// The request to [`create_room`] used a room version that the server does
|
||||
/// not support.
|
||||
///
|
||||
/// [`create_room`]: crate::room::create_room
|
||||
UnsupportedRoomVersion,
|
||||
|
||||
/// `M_URL_NOT_SET`
|
||||
///
|
||||
/// The application service doesn't have a URL configured.
|
||||
UrlNotSet,
|
||||
|
||||
/// `M_USER_DEACTIVATED`
|
||||
///
|
||||
/// The user ID associated with the request has been deactivated.
|
||||
UserDeactivated,
|
||||
|
||||
/// `M_USER_IN_USE`
|
||||
///
|
||||
/// The desired user ID is already taken.
|
||||
UserInUse,
|
||||
|
||||
/// `M_USER_LOCKED`
|
||||
///
|
||||
/// The account has been [locked] and cannot be used at this time.
|
||||
///
|
||||
/// [locked]: https://spec.matrix.org/latest/client-server-api/#account-locking
|
||||
UserLocked,
|
||||
|
||||
/// `M_USER_SUSPENDED`
|
||||
///
|
||||
/// The account has been [suspended] and can only be used for limited
|
||||
/// actions at this time.
|
||||
///
|
||||
/// [suspended]: https://spec.matrix.org/latest/client-server-api/#account-suspension
|
||||
UserSuspended,
|
||||
|
||||
/// `M_WEAK_PASSWORD`
|
||||
///
|
||||
/// The password was [rejected] by the server for being too weak.
|
||||
///
|
||||
/// [rejected]: https://spec.matrix.org/latest/client-server-api/#notes-on-password-management
|
||||
WeakPassword,
|
||||
|
||||
/// `M_WRONG_ROOM_KEYS_VERSION`
|
||||
///
|
||||
/// The version of the [room keys backup] provided in the request does not
|
||||
/// match the current backup version.
|
||||
///
|
||||
/// [room keys backup]: https://spec.matrix.org/latest/client-server-api/#server-side-key-backups
|
||||
WrongRoomKeysVersion {
|
||||
/// The currently active backup version.
|
||||
current_version: String,
|
||||
},
|
||||
|
||||
/// A custom API error.
|
||||
Custom { errcode: String },
|
||||
}
|
||||
|
||||
impl TryFrom<RumaApiErrorKind> for ErrorKind {
|
||||
type Error = NotYetImplemented;
|
||||
fn try_from(value: RumaApiErrorKind) -> Result<Self, Self::Error> {
|
||||
match &value {
|
||||
RumaApiErrorKind::BadAlias => Ok(ErrorKind::BadAlias),
|
||||
RumaApiErrorKind::BadJson => Ok(ErrorKind::BadJson),
|
||||
RumaApiErrorKind::BadState => Ok(ErrorKind::BadState),
|
||||
RumaApiErrorKind::BadStatus(bad_status) => Ok(ErrorKind::BadStatus {
|
||||
status: bad_status.status.map(|code| code.as_u16()),
|
||||
body: bad_status.body.clone(),
|
||||
}),
|
||||
RumaApiErrorKind::CannotLeaveServerNoticeRoom => {
|
||||
Ok(ErrorKind::CannotLeaveServerNoticeRoom)
|
||||
}
|
||||
RumaApiErrorKind::CannotOverwriteMedia => Ok(ErrorKind::CannotOverwriteMedia),
|
||||
RumaApiErrorKind::CaptchaInvalid => Ok(ErrorKind::CaptchaInvalid),
|
||||
RumaApiErrorKind::CaptchaNeeded => Ok(ErrorKind::CaptchaNeeded),
|
||||
RumaApiErrorKind::ConnectionFailed => Ok(ErrorKind::ConnectionFailed),
|
||||
RumaApiErrorKind::ConnectionTimeout => Ok(ErrorKind::ConnectionTimeout),
|
||||
RumaApiErrorKind::DuplicateAnnotation => Ok(ErrorKind::DuplicateAnnotation),
|
||||
RumaApiErrorKind::Exclusive => Ok(ErrorKind::Exclusive),
|
||||
RumaApiErrorKind::Forbidden => Ok(ErrorKind::Forbidden),
|
||||
RumaApiErrorKind::GuestAccessForbidden => Ok(ErrorKind::GuestAccessForbidden),
|
||||
RumaApiErrorKind::IncompatibleRoomVersion(incompatible_room_version) => {
|
||||
Ok(ErrorKind::IncompatibleRoomVersion {
|
||||
room_version: incompatible_room_version.room_version.to_string(),
|
||||
})
|
||||
}
|
||||
RumaApiErrorKind::InvalidParam => Ok(ErrorKind::InvalidParam),
|
||||
RumaApiErrorKind::InvalidRoomState => Ok(ErrorKind::InvalidRoomState),
|
||||
RumaApiErrorKind::InvalidUsername => Ok(ErrorKind::InvalidUsername),
|
||||
RumaApiErrorKind::LimitExceeded(limit_exceeded) => {
|
||||
let retry_after_ms = match &limit_exceeded.retry_after {
|
||||
Some(RetryAfter::Delay(duration)) => Some(duration.as_millis() as u64),
|
||||
Some(RetryAfter::DateTime(system_time)) => {
|
||||
let duration = MilliSecondsSinceUnixEpoch::now()
|
||||
.to_system_time()
|
||||
.and_then(|now| system_time.duration_since(now).ok());
|
||||
duration.map(|duration| duration.as_millis() as u64)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
Ok(ErrorKind::LimitExceeded { retry_after_ms })
|
||||
}
|
||||
RumaApiErrorKind::MissingParam => Ok(ErrorKind::MissingParam),
|
||||
RumaApiErrorKind::MissingToken => Ok(ErrorKind::MissingToken),
|
||||
RumaApiErrorKind::NotFound => Ok(ErrorKind::NotFound),
|
||||
RumaApiErrorKind::NotJson => Ok(ErrorKind::NotJson),
|
||||
RumaApiErrorKind::NotYetUploaded => Ok(ErrorKind::NotYetUploaded),
|
||||
RumaApiErrorKind::ResourceLimitExceeded(resource_limit_exceeded) => {
|
||||
Ok(ErrorKind::ResourceLimitExceeded {
|
||||
admin_contact: resource_limit_exceeded.admin_contact.clone(),
|
||||
})
|
||||
}
|
||||
RumaApiErrorKind::RoomInUse => Ok(ErrorKind::RoomInUse),
|
||||
RumaApiErrorKind::ServerNotTrusted => Ok(ErrorKind::ServerNotTrusted),
|
||||
RumaApiErrorKind::ThreepidAuthFailed => Ok(ErrorKind::ThreepidAuthFailed),
|
||||
RumaApiErrorKind::ThreepidDenied => Ok(ErrorKind::ThreepidDenied),
|
||||
RumaApiErrorKind::ThreepidInUse => Ok(ErrorKind::ThreepidInUse),
|
||||
RumaApiErrorKind::ThreepidMediumNotSupported => {
|
||||
Ok(ErrorKind::ThreepidMediumNotSupported)
|
||||
}
|
||||
RumaApiErrorKind::ThreepidNotFound => Ok(ErrorKind::ThreepidNotFound),
|
||||
RumaApiErrorKind::TooLarge => Ok(ErrorKind::TooLarge),
|
||||
RumaApiErrorKind::UnableToAuthorizeJoin => Ok(ErrorKind::UnableToAuthorizeJoin),
|
||||
RumaApiErrorKind::UnableToGrantJoin => Ok(ErrorKind::UnableToGrantJoin),
|
||||
RumaApiErrorKind::Unauthorized => Ok(ErrorKind::Unauthorized),
|
||||
RumaApiErrorKind::Unknown => Ok(ErrorKind::Unknown),
|
||||
RumaApiErrorKind::UnknownToken(unknown_token) => {
|
||||
Ok(ErrorKind::UnknownToken { soft_logout: unknown_token.soft_logout.to_owned() })
|
||||
}
|
||||
RumaApiErrorKind::Unrecognized => Ok(ErrorKind::Unrecognized),
|
||||
RumaApiErrorKind::UnsupportedRoomVersion => Ok(ErrorKind::UnsupportedRoomVersion),
|
||||
RumaApiErrorKind::UrlNotSet => Ok(ErrorKind::UrlNotSet),
|
||||
RumaApiErrorKind::UserDeactivated => Ok(ErrorKind::UserDeactivated),
|
||||
RumaApiErrorKind::UserInUse => Ok(ErrorKind::UserInUse),
|
||||
RumaApiErrorKind::UserLocked => Ok(ErrorKind::UserLocked),
|
||||
RumaApiErrorKind::UserSuspended => Ok(ErrorKind::UserSuspended),
|
||||
RumaApiErrorKind::WeakPassword => Ok(ErrorKind::WeakPassword),
|
||||
RumaApiErrorKind::WrongRoomKeysVersion(wrong_version) => {
|
||||
Ok(ErrorKind::WrongRoomKeysVersion {
|
||||
current_version: wrong_version.current_version.clone(),
|
||||
})
|
||||
}
|
||||
RumaApiErrorKind::_Custom(_) => {
|
||||
// There is no way to map the extra values since they're private, so we omit
|
||||
// them
|
||||
Ok(ErrorKind::Custom { errcode: value.errcode().to_string() })
|
||||
}
|
||||
// In any other case, return it as the mapping not being yet implemented
|
||||
_ => Err(NotYetImplemented),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,16 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use anyhow::{Context, bail};
|
||||
use matrix_sdk::IdParseError;
|
||||
use matrix_sdk_ui::timeline::TimelineEventItemId;
|
||||
use ruma::{
|
||||
EventId,
|
||||
events::{
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent,
|
||||
MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
|
||||
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
|
||||
TimelineEventType as RumaTimelineEventType,
|
||||
room::{
|
||||
encrypted,
|
||||
message::{MessageType as RumaMessageType, Relation},
|
||||
redaction::SyncRoomRedactionEvent,
|
||||
},
|
||||
},
|
||||
use anyhow::{bail, Context};
|
||||
use ruma::events::{
|
||||
room::message::Relation, AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
|
||||
AnyTimelineEvent, MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
|
||||
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
ClientError,
|
||||
room_member::MembershipState,
|
||||
ruma::{MessageType, RtcCallIntent, RtcNotificationType},
|
||||
utils::Timestamp,
|
||||
};
|
||||
use crate::{room_member::MembershipState, ruma::MessageType, ClientError};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct TimelineEvent(pub(crate) Box<AnySyncTimelineEvent>);
|
||||
pub struct TimelineEvent(pub(crate) AnySyncTimelineEvent);
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
impl TimelineEvent {
|
||||
pub fn event_id(&self) -> String {
|
||||
self.0.event_id().to_string()
|
||||
@@ -50,251 +20,31 @@ impl TimelineEvent {
|
||||
self.0.sender().to_string()
|
||||
}
|
||||
|
||||
pub fn timestamp(&self) -> Timestamp {
|
||||
self.0.origin_server_ts().into()
|
||||
pub fn timestamp(&self) -> u64 {
|
||||
self.0.origin_server_ts().0.into()
|
||||
}
|
||||
|
||||
pub fn content(&self) -> Result<TimelineEventContent, ClientError> {
|
||||
let content = match &*self.0 {
|
||||
pub fn event_type(&self) -> Result<TimelineEventType, ClientError> {
|
||||
let event_type = match &self.0 {
|
||||
AnySyncTimelineEvent::MessageLike(event) => {
|
||||
TimelineEventContent::MessageLike { content: event.clone().try_into()? }
|
||||
TimelineEventType::MessageLike { content: event.clone().try_into()? }
|
||||
}
|
||||
AnySyncTimelineEvent::State(event) => {
|
||||
TimelineEventContent::State { content: event.clone().try_into()? }
|
||||
TimelineEventType::State { content: event.clone().try_into()? }
|
||||
}
|
||||
};
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
/// Returns the thread root event id for the event, if it's part of a
|
||||
/// thread.
|
||||
pub fn thread_root_event_id(&self) -> Option<String> {
|
||||
match &*self.0 {
|
||||
AnySyncTimelineEvent::MessageLike(event) => {
|
||||
match event.original_content().and_then(|content| content.relation()) {
|
||||
Some(encrypted::Relation::Thread(thread)) => Some(thread.event_id.to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
AnySyncTimelineEvent::State(_) => None,
|
||||
}
|
||||
Ok(event_type)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AnyTimelineEvent> for TimelineEvent {
|
||||
fn from(event: AnyTimelineEvent) -> Self {
|
||||
Self(Box::new(event.into()))
|
||||
}
|
||||
}
|
||||
|
||||
/// The timeline event type.
|
||||
#[derive(Clone, uniffi::Enum, PartialEq, Eq, Hash)]
|
||||
#[uniffi::export(Eq, Hash)]
|
||||
pub enum TimelineEventType {
|
||||
/// The event is a message-like one and should be displayed as such.
|
||||
MessageLike { value: MessageLikeEventType },
|
||||
/// The event is a state event, and may or may not be displayed in the
|
||||
/// timeline.
|
||||
State { value: StateEventType },
|
||||
}
|
||||
|
||||
impl From<RumaTimelineEventType> for TimelineEventType {
|
||||
fn from(value: RumaTimelineEventType) -> Self {
|
||||
match value {
|
||||
RumaTimelineEventType::Audio => {
|
||||
Self::MessageLike { value: MessageLikeEventType::Audio }
|
||||
}
|
||||
RumaTimelineEventType::File => Self::MessageLike { value: MessageLikeEventType::File },
|
||||
RumaTimelineEventType::Image => {
|
||||
Self::MessageLike { value: MessageLikeEventType::Image }
|
||||
}
|
||||
RumaTimelineEventType::Video => {
|
||||
Self::MessageLike { value: MessageLikeEventType::Video }
|
||||
}
|
||||
RumaTimelineEventType::Voice => {
|
||||
Self::MessageLike { value: MessageLikeEventType::Voice }
|
||||
}
|
||||
RumaTimelineEventType::Emote => {
|
||||
Self::MessageLike { value: MessageLikeEventType::Emote }
|
||||
}
|
||||
RumaTimelineEventType::Encrypted => {
|
||||
Self::MessageLike { value: MessageLikeEventType::Encrypted }
|
||||
}
|
||||
RumaTimelineEventType::RoomMessage => {
|
||||
Self::MessageLike { value: MessageLikeEventType::RoomMessage }
|
||||
}
|
||||
RumaTimelineEventType::CallAnswer => {
|
||||
Self::MessageLike { value: MessageLikeEventType::CallAnswer }
|
||||
}
|
||||
RumaTimelineEventType::CallInvite => {
|
||||
Self::MessageLike { value: MessageLikeEventType::CallInvite }
|
||||
}
|
||||
RumaTimelineEventType::CallHangup => {
|
||||
Self::MessageLike { value: MessageLikeEventType::CallHangup }
|
||||
}
|
||||
RumaTimelineEventType::CallCandidates => {
|
||||
Self::MessageLike { value: MessageLikeEventType::CallCandidates }
|
||||
}
|
||||
RumaTimelineEventType::CallNegotiate => {
|
||||
Self::MessageLike { value: MessageLikeEventType::CallNegotiate }
|
||||
}
|
||||
RumaTimelineEventType::CallReject => {
|
||||
Self::MessageLike { value: MessageLikeEventType::CallReject }
|
||||
}
|
||||
RumaTimelineEventType::CallSdpStreamMetadataChanged => {
|
||||
Self::MessageLike { value: MessageLikeEventType::CallSdpStreamMetadataChanged }
|
||||
}
|
||||
RumaTimelineEventType::CallSelectAnswer => {
|
||||
Self::MessageLike { value: MessageLikeEventType::CallSelectAnswer }
|
||||
}
|
||||
RumaTimelineEventType::KeyVerificationReady => {
|
||||
Self::MessageLike { value: MessageLikeEventType::KeyVerificationReady }
|
||||
}
|
||||
RumaTimelineEventType::KeyVerificationStart => {
|
||||
Self::MessageLike { value: MessageLikeEventType::KeyVerificationStart }
|
||||
}
|
||||
RumaTimelineEventType::KeyVerificationCancel => {
|
||||
Self::MessageLike { value: MessageLikeEventType::KeyVerificationCancel }
|
||||
}
|
||||
RumaTimelineEventType::KeyVerificationAccept => {
|
||||
Self::MessageLike { value: MessageLikeEventType::KeyVerificationAccept }
|
||||
}
|
||||
RumaTimelineEventType::KeyVerificationKey => {
|
||||
Self::MessageLike { value: MessageLikeEventType::KeyVerificationKey }
|
||||
}
|
||||
RumaTimelineEventType::KeyVerificationMac => {
|
||||
Self::MessageLike { value: MessageLikeEventType::KeyVerificationMac }
|
||||
}
|
||||
RumaTimelineEventType::KeyVerificationDone => {
|
||||
Self::MessageLike { value: MessageLikeEventType::KeyVerificationDone }
|
||||
}
|
||||
RumaTimelineEventType::Location => {
|
||||
Self::MessageLike { value: MessageLikeEventType::Location }
|
||||
}
|
||||
RumaTimelineEventType::Message => {
|
||||
Self::MessageLike { value: MessageLikeEventType::Message }
|
||||
}
|
||||
RumaTimelineEventType::PollStart => {
|
||||
Self::MessageLike { value: MessageLikeEventType::PollStart }
|
||||
}
|
||||
RumaTimelineEventType::UnstablePollStart => {
|
||||
Self::MessageLike { value: MessageLikeEventType::UnstablePollStart }
|
||||
}
|
||||
RumaTimelineEventType::PollResponse => {
|
||||
Self::MessageLike { value: MessageLikeEventType::PollResponse }
|
||||
}
|
||||
RumaTimelineEventType::UnstablePollResponse => {
|
||||
Self::MessageLike { value: MessageLikeEventType::UnstablePollResponse }
|
||||
}
|
||||
RumaTimelineEventType::PollEnd => {
|
||||
Self::MessageLike { value: MessageLikeEventType::PollEnd }
|
||||
}
|
||||
RumaTimelineEventType::UnstablePollEnd => {
|
||||
Self::MessageLike { value: MessageLikeEventType::UnstablePollEnd }
|
||||
}
|
||||
RumaTimelineEventType::Beacon => {
|
||||
Self::MessageLike { value: MessageLikeEventType::Beacon }
|
||||
}
|
||||
RumaTimelineEventType::Reaction => {
|
||||
Self::MessageLike { value: MessageLikeEventType::Reaction }
|
||||
}
|
||||
RumaTimelineEventType::RoomEncrypted => {
|
||||
Self::MessageLike { value: MessageLikeEventType::RoomEncrypted }
|
||||
}
|
||||
RumaTimelineEventType::RoomRedaction => {
|
||||
Self::MessageLike { value: MessageLikeEventType::RoomRedaction }
|
||||
}
|
||||
RumaTimelineEventType::Sticker => {
|
||||
Self::MessageLike { value: MessageLikeEventType::Sticker }
|
||||
}
|
||||
RumaTimelineEventType::CallNotify => {
|
||||
Self::MessageLike { value: MessageLikeEventType::CallNotify }
|
||||
}
|
||||
RumaTimelineEventType::RtcNotification => {
|
||||
Self::MessageLike { value: MessageLikeEventType::RtcNotification }
|
||||
}
|
||||
RumaTimelineEventType::RtcDecline => {
|
||||
Self::MessageLike { value: MessageLikeEventType::RtcDecline }
|
||||
}
|
||||
RumaTimelineEventType::PolicyRuleRoom => {
|
||||
Self::State { value: StateEventType::PolicyRuleRoom }
|
||||
}
|
||||
RumaTimelineEventType::PolicyRuleServer => {
|
||||
Self::State { value: StateEventType::PolicyRuleServer }
|
||||
}
|
||||
RumaTimelineEventType::PolicyRuleUser => {
|
||||
Self::State { value: StateEventType::PolicyRuleUser }
|
||||
}
|
||||
RumaTimelineEventType::RoomAvatar => Self::State { value: StateEventType::RoomAvatar },
|
||||
RumaTimelineEventType::RoomCanonicalAlias => {
|
||||
Self::State { value: StateEventType::RoomCanonicalAlias }
|
||||
}
|
||||
RumaTimelineEventType::RoomCreate => Self::State { value: StateEventType::RoomCreate },
|
||||
RumaTimelineEventType::RoomEncryption => {
|
||||
Self::State { value: StateEventType::RoomEncryption }
|
||||
}
|
||||
RumaTimelineEventType::RoomGuestAccess => {
|
||||
Self::State { value: StateEventType::RoomGuestAccess }
|
||||
}
|
||||
RumaTimelineEventType::RoomHistoryVisibility => {
|
||||
Self::State { value: StateEventType::RoomHistoryVisibility }
|
||||
}
|
||||
RumaTimelineEventType::RoomJoinRules => {
|
||||
Self::State { value: StateEventType::RoomJoinRules }
|
||||
}
|
||||
RumaTimelineEventType::RoomMember => {
|
||||
Self::State { value: StateEventType::RoomMemberEvent }
|
||||
}
|
||||
RumaTimelineEventType::RoomLanguage => {
|
||||
Self::State { value: StateEventType::RoomLanguage }
|
||||
}
|
||||
RumaTimelineEventType::RoomName => Self::State { value: StateEventType::RoomName },
|
||||
RumaTimelineEventType::RoomImagePack => {
|
||||
Self::State { value: StateEventType::RoomImagePack }
|
||||
}
|
||||
RumaTimelineEventType::RoomPinnedEvents => {
|
||||
Self::State { value: StateEventType::RoomPinnedEvents }
|
||||
}
|
||||
RumaTimelineEventType::RoomPowerLevels => {
|
||||
Self::State { value: StateEventType::RoomPowerLevels }
|
||||
}
|
||||
RumaTimelineEventType::RoomServerAcl => {
|
||||
Self::State { value: StateEventType::RoomServerAcl }
|
||||
}
|
||||
RumaTimelineEventType::RoomThirdPartyInvite => {
|
||||
Self::State { value: StateEventType::RoomThirdPartyInvite }
|
||||
}
|
||||
RumaTimelineEventType::RoomTombstone => {
|
||||
Self::State { value: StateEventType::RoomTombstone }
|
||||
}
|
||||
RumaTimelineEventType::RoomTopic => Self::State { value: StateEventType::RoomTopic },
|
||||
RumaTimelineEventType::SpaceChild => Self::State { value: StateEventType::SpaceChild },
|
||||
RumaTimelineEventType::SpaceParent => {
|
||||
Self::State { value: StateEventType::SpaceParent }
|
||||
}
|
||||
RumaTimelineEventType::BeaconInfo => Self::State { value: StateEventType::BeaconInfo },
|
||||
RumaTimelineEventType::CallMember => Self::State { value: StateEventType::CallMember },
|
||||
RumaTimelineEventType::MemberHints => {
|
||||
Self::State { value: StateEventType::MemberHints }
|
||||
}
|
||||
RumaTimelineEventType::_Custom(_) => {
|
||||
Self::State { value: StateEventType::Custom { value: value.to_string() } }
|
||||
}
|
||||
_ => Self::MessageLike { value: MessageLikeEventType::Other(value.to_string()) },
|
||||
}
|
||||
Self(event.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
// A note about this `allow(clippy::large_enum_variant)`.
|
||||
// In order to reduce the size of `TimelineEventContent`, we would need to
|
||||
// put some parts in a `Box`, or an `Arc`. Sadly, it doesn't play well with
|
||||
// UniFFI. We would need to change the `uniffi::Record` of the subtypes into
|
||||
// `uniffi::Object`, which is a radical change. It would simplify the memory
|
||||
// usage, but it would slow down the performance around the FFI border. Thus,
|
||||
// let's consider this is a false-positive lint in this particular case.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum TimelineEventContent {
|
||||
pub enum TimelineEventType {
|
||||
MessageLike { content: MessageLikeEventContent },
|
||||
State { content: StateEventContent },
|
||||
}
|
||||
@@ -304,6 +54,7 @@ pub enum StateEventContent {
|
||||
PolicyRuleRoom,
|
||||
PolicyRuleServer,
|
||||
PolicyRuleUser,
|
||||
RoomAliases,
|
||||
RoomAvatar,
|
||||
RoomCanonicalAlias,
|
||||
RoomCreate,
|
||||
@@ -318,7 +69,7 @@ pub enum StateEventContent {
|
||||
RoomServerAcl,
|
||||
RoomThirdPartyInvite,
|
||||
RoomTombstone,
|
||||
RoomTopic { topic: String },
|
||||
RoomTopic,
|
||||
SpaceChild,
|
||||
SpaceParent,
|
||||
}
|
||||
@@ -331,6 +82,7 @@ impl TryFrom<AnySyncStateEvent> for StateEventContent {
|
||||
AnySyncStateEvent::PolicyRuleRoom(_) => StateEventContent::PolicyRuleRoom,
|
||||
AnySyncStateEvent::PolicyRuleServer(_) => StateEventContent::PolicyRuleServer,
|
||||
AnySyncStateEvent::PolicyRuleUser(_) => StateEventContent::PolicyRuleUser,
|
||||
AnySyncStateEvent::RoomAliases(_) => StateEventContent::RoomAliases,
|
||||
AnySyncStateEvent::RoomAvatar(_) => StateEventContent::RoomAvatar,
|
||||
AnySyncStateEvent::RoomCanonicalAlias(_) => StateEventContent::RoomCanonicalAlias,
|
||||
AnySyncStateEvent::RoomCreate(_) => StateEventContent::RoomCreate,
|
||||
@@ -343,7 +95,7 @@ impl TryFrom<AnySyncStateEvent> for StateEventContent {
|
||||
let original_content = get_state_event_original_content(content)?;
|
||||
StateEventContent::RoomMemberContent {
|
||||
user_id: state_key,
|
||||
membership_state: original_content.membership.try_into()?,
|
||||
membership_state: original_content.membership.into(),
|
||||
}
|
||||
}
|
||||
AnySyncStateEvent::RoomName(_) => StateEventContent::RoomName,
|
||||
@@ -352,38 +104,19 @@ impl TryFrom<AnySyncStateEvent> for StateEventContent {
|
||||
AnySyncStateEvent::RoomServerAcl(_) => StateEventContent::RoomServerAcl,
|
||||
AnySyncStateEvent::RoomThirdPartyInvite(_) => StateEventContent::RoomThirdPartyInvite,
|
||||
AnySyncStateEvent::RoomTombstone(_) => StateEventContent::RoomTombstone,
|
||||
AnySyncStateEvent::RoomTopic(content) => {
|
||||
let content = get_state_event_original_content(content)?;
|
||||
|
||||
StateEventContent::RoomTopic { topic: content.topic }
|
||||
}
|
||||
AnySyncStateEvent::RoomTopic(_) => StateEventContent::RoomTopic,
|
||||
AnySyncStateEvent::SpaceChild(_) => StateEventContent::SpaceChild,
|
||||
AnySyncStateEvent::SpaceParent(_) => StateEventContent::SpaceParent,
|
||||
_ => bail!("Unsupported state event: {:?}", value.event_type()),
|
||||
_ => bail!("Unsupported state event"),
|
||||
};
|
||||
Ok(event)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
// A note about this `allow(clippy::large_enum_variant)`.
|
||||
// In order to reduce the size of `MessageLineEventContent`, we would need to
|
||||
// put some parts in a `Box`, or an `Arc`. Sadly, it doesn't play well with
|
||||
// UniFFI. We would need to change the `uniffi::Record` of the subtypes into
|
||||
// `uniffi::Object`, which is a radical change. It would simplify the memory
|
||||
// usage, but it would slow down the performance around the FFI border. Thus,
|
||||
// let's consider this is a false-positive lint in this particular case.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum MessageLikeEventContent {
|
||||
CallAnswer,
|
||||
CallInvite,
|
||||
RtcNotification {
|
||||
notification_type: RtcNotificationType,
|
||||
/// The timestamp at which this notification is considered invalid.
|
||||
expiration_ts: Timestamp,
|
||||
/// Soft indication of whether it is an audio or video call.
|
||||
call_intent: Option<RtcCallIntent>,
|
||||
},
|
||||
CallHangup,
|
||||
CallCandidates,
|
||||
KeyVerificationReady,
|
||||
@@ -393,21 +126,11 @@ pub enum MessageLikeEventContent {
|
||||
KeyVerificationKey,
|
||||
KeyVerificationMac,
|
||||
KeyVerificationDone,
|
||||
Poll {
|
||||
question: String,
|
||||
},
|
||||
ReactionContent {
|
||||
related_event_id: String,
|
||||
},
|
||||
Poll { question: String },
|
||||
ReactionContent { related_event_id: String },
|
||||
RoomEncrypted,
|
||||
RoomMessage {
|
||||
message_type: MessageType,
|
||||
in_reply_to_event_id: Option<String>,
|
||||
},
|
||||
RoomRedaction {
|
||||
redacted_event_id: Option<String>,
|
||||
reason: Option<String>,
|
||||
},
|
||||
RoomMessage { message_type: MessageType, in_reply_to_event_id: Option<String> },
|
||||
RoomRedaction,
|
||||
Sticker,
|
||||
}
|
||||
|
||||
@@ -418,16 +141,6 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
|
||||
let content = match value {
|
||||
AnySyncMessageLikeEvent::CallAnswer(_) => MessageLikeEventContent::CallAnswer,
|
||||
AnySyncMessageLikeEvent::CallInvite(_) => MessageLikeEventContent::CallInvite,
|
||||
AnySyncMessageLikeEvent::RtcNotification(event) => {
|
||||
let origin_server_ts = event.origin_server_ts();
|
||||
let original_content = get_message_like_event_original_content(event)?;
|
||||
let expiration_ts = original_content.expiration_ts(origin_server_ts, None).into();
|
||||
MessageLikeEventContent::RtcNotification {
|
||||
notification_type: original_content.notification_type.into(),
|
||||
expiration_ts,
|
||||
call_intent: original_content.call_intent.map(|intent| intent.into()),
|
||||
}
|
||||
}
|
||||
AnySyncMessageLikeEvent::CallHangup(_) => MessageLikeEventContent::CallHangup,
|
||||
AnySyncMessageLikeEvent::CallCandidates(_) => MessageLikeEventContent::CallCandidates,
|
||||
AnySyncMessageLikeEvent::KeyVerificationReady(_) => {
|
||||
@@ -468,27 +181,17 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
|
||||
let original_content = get_message_like_event_original_content(content)?;
|
||||
let in_reply_to_event_id =
|
||||
original_content.relates_to.and_then(|relation| match relation {
|
||||
Relation::Reply(reply) => Some(reply.in_reply_to.event_id.to_string()),
|
||||
Relation::Reply { in_reply_to } => Some(in_reply_to.event_id.to_string()),
|
||||
_ => None,
|
||||
});
|
||||
MessageLikeEventContent::RoomMessage {
|
||||
message_type: original_content.msgtype.try_into()?,
|
||||
message_type: original_content.msgtype.into(),
|
||||
in_reply_to_event_id,
|
||||
}
|
||||
}
|
||||
AnySyncMessageLikeEvent::RoomRedaction(c) => {
|
||||
let (redacted_event_id, reason) = match c {
|
||||
SyncRoomRedactionEvent::Original(o) => {
|
||||
let id =
|
||||
if o.content.redacts.is_some() { o.content.redacts } else { o.redacts };
|
||||
(id.map(|id| id.to_string()), o.content.reason)
|
||||
}
|
||||
SyncRoomRedactionEvent::Redacted(_) => (None, None),
|
||||
};
|
||||
MessageLikeEventContent::RoomRedaction { redacted_event_id, reason }
|
||||
}
|
||||
AnySyncMessageLikeEvent::RoomRedaction(_) => MessageLikeEventContent::RoomRedaction,
|
||||
AnySyncMessageLikeEvent::Sticker(_) => MessageLikeEventContent::Sticker,
|
||||
_ => bail!("Unsupported Event Type: {:?}", value.event_type()),
|
||||
_ => bail!("Unsupported Event Type"),
|
||||
};
|
||||
Ok(content)
|
||||
}
|
||||
@@ -513,231 +216,3 @@ where
|
||||
event.as_original().context("Failed to get original content")?.content.clone();
|
||||
Ok(original_content)
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum, PartialEq, Eq, Hash)]
|
||||
pub enum StateEventType {
|
||||
BeaconInfo,
|
||||
CallMember,
|
||||
MemberHints,
|
||||
PolicyRuleRoom,
|
||||
PolicyRuleServer,
|
||||
PolicyRuleUser,
|
||||
RoomAvatar,
|
||||
RoomCanonicalAlias,
|
||||
RoomCreate,
|
||||
RoomEncryption,
|
||||
RoomGuestAccess,
|
||||
RoomHistoryVisibility,
|
||||
RoomImagePack,
|
||||
RoomJoinRules,
|
||||
RoomMemberEvent,
|
||||
RoomLanguage,
|
||||
RoomName,
|
||||
RoomPinnedEvents,
|
||||
RoomPowerLevels,
|
||||
RoomServerAcl,
|
||||
RoomThirdPartyInvite,
|
||||
RoomTombstone,
|
||||
RoomTopic,
|
||||
SpaceChild,
|
||||
SpaceParent,
|
||||
Custom { value: String },
|
||||
}
|
||||
|
||||
impl From<StateEventType> for ruma::events::StateEventType {
|
||||
fn from(val: StateEventType) -> Self {
|
||||
match val {
|
||||
StateEventType::BeaconInfo => Self::BeaconInfo,
|
||||
StateEventType::CallMember => Self::CallMember,
|
||||
StateEventType::MemberHints => Self::MemberHints,
|
||||
StateEventType::PolicyRuleRoom => Self::PolicyRuleRoom,
|
||||
StateEventType::PolicyRuleServer => Self::PolicyRuleServer,
|
||||
StateEventType::PolicyRuleUser => Self::PolicyRuleUser,
|
||||
StateEventType::RoomAvatar => Self::RoomAvatar,
|
||||
StateEventType::RoomCanonicalAlias => Self::RoomCanonicalAlias,
|
||||
StateEventType::RoomCreate => Self::RoomCreate,
|
||||
StateEventType::RoomEncryption => Self::RoomEncryption,
|
||||
StateEventType::RoomGuestAccess => Self::RoomGuestAccess,
|
||||
StateEventType::RoomHistoryVisibility => Self::RoomHistoryVisibility,
|
||||
StateEventType::RoomImagePack => Self::RoomImagePack,
|
||||
StateEventType::RoomJoinRules => Self::RoomJoinRules,
|
||||
StateEventType::RoomLanguage => Self::RoomLanguage,
|
||||
StateEventType::RoomMemberEvent => Self::RoomMember,
|
||||
StateEventType::RoomName => Self::RoomName,
|
||||
StateEventType::RoomPinnedEvents => Self::RoomPinnedEvents,
|
||||
StateEventType::RoomPowerLevels => Self::RoomPowerLevels,
|
||||
StateEventType::RoomServerAcl => Self::RoomServerAcl,
|
||||
StateEventType::RoomThirdPartyInvite => Self::RoomThirdPartyInvite,
|
||||
StateEventType::RoomTombstone => Self::RoomTombstone,
|
||||
StateEventType::RoomTopic => Self::RoomTopic,
|
||||
StateEventType::SpaceChild => Self::SpaceChild,
|
||||
StateEventType::SpaceParent => Self::SpaceParent,
|
||||
StateEventType::Custom { value } => value.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum, PartialEq, Eq, Hash)]
|
||||
pub enum MessageLikeEventType {
|
||||
Audio,
|
||||
Beacon,
|
||||
CallAnswer,
|
||||
CallCandidates,
|
||||
CallHangup,
|
||||
CallInvite,
|
||||
CallNegotiate,
|
||||
CallNotify,
|
||||
CallReject,
|
||||
CallSdpStreamMetadataChanged,
|
||||
CallSelectAnswer,
|
||||
Emote,
|
||||
Encrypted,
|
||||
File,
|
||||
Image,
|
||||
KeyVerificationAccept,
|
||||
KeyVerificationCancel,
|
||||
KeyVerificationDone,
|
||||
KeyVerificationKey,
|
||||
KeyVerificationMac,
|
||||
KeyVerificationReady,
|
||||
KeyVerificationStart,
|
||||
Location,
|
||||
Message,
|
||||
PollEnd,
|
||||
PollResponse,
|
||||
PollStart,
|
||||
Reaction,
|
||||
RoomEncrypted,
|
||||
RoomMessage,
|
||||
RoomRedaction,
|
||||
RtcDecline,
|
||||
RtcNotification,
|
||||
Sticker,
|
||||
UnstablePollEnd,
|
||||
UnstablePollResponse,
|
||||
UnstablePollStart,
|
||||
Video,
|
||||
Voice,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
|
||||
fn from(val: MessageLikeEventType) -> Self {
|
||||
match val {
|
||||
MessageLikeEventType::Audio => Self::Audio,
|
||||
MessageLikeEventType::File => Self::File,
|
||||
MessageLikeEventType::Image => Self::Image,
|
||||
MessageLikeEventType::Video => Self::Video,
|
||||
MessageLikeEventType::Voice => Self::Voice,
|
||||
MessageLikeEventType::Beacon => Self::Beacon,
|
||||
MessageLikeEventType::CallAnswer => Self::CallAnswer,
|
||||
MessageLikeEventType::CallCandidates => Self::CallCandidates,
|
||||
MessageLikeEventType::CallInvite => Self::CallInvite,
|
||||
MessageLikeEventType::CallHangup => Self::CallHangup,
|
||||
MessageLikeEventType::CallNegotiate => Self::CallNegotiate,
|
||||
MessageLikeEventType::CallNotify => Self::CallNotify,
|
||||
MessageLikeEventType::CallReject => Self::CallReject,
|
||||
MessageLikeEventType::CallSdpStreamMetadataChanged => {
|
||||
Self::CallSdpStreamMetadataChanged
|
||||
}
|
||||
MessageLikeEventType::CallSelectAnswer => Self::CallSelectAnswer,
|
||||
MessageLikeEventType::Emote => Self::Emote,
|
||||
MessageLikeEventType::Encrypted => Self::Encrypted,
|
||||
MessageLikeEventType::KeyVerificationReady => Self::KeyVerificationReady,
|
||||
MessageLikeEventType::KeyVerificationStart => Self::KeyVerificationStart,
|
||||
MessageLikeEventType::KeyVerificationCancel => Self::KeyVerificationCancel,
|
||||
MessageLikeEventType::KeyVerificationAccept => Self::KeyVerificationAccept,
|
||||
MessageLikeEventType::KeyVerificationKey => Self::KeyVerificationKey,
|
||||
MessageLikeEventType::KeyVerificationMac => Self::KeyVerificationMac,
|
||||
MessageLikeEventType::KeyVerificationDone => Self::KeyVerificationDone,
|
||||
MessageLikeEventType::Location => Self::Location,
|
||||
MessageLikeEventType::Message => Self::Message,
|
||||
MessageLikeEventType::Reaction => Self::Reaction,
|
||||
MessageLikeEventType::RoomEncrypted => Self::RoomEncrypted,
|
||||
MessageLikeEventType::RoomMessage => Self::RoomMessage,
|
||||
MessageLikeEventType::RoomRedaction => Self::RoomRedaction,
|
||||
MessageLikeEventType::RtcDecline => Self::RtcDecline,
|
||||
MessageLikeEventType::Sticker => Self::Sticker,
|
||||
MessageLikeEventType::PollEnd => Self::PollEnd,
|
||||
MessageLikeEventType::PollResponse => Self::PollResponse,
|
||||
MessageLikeEventType::PollStart => Self::PollStart,
|
||||
MessageLikeEventType::RtcNotification => Self::RtcNotification,
|
||||
MessageLikeEventType::UnstablePollEnd => Self::UnstablePollEnd,
|
||||
MessageLikeEventType::UnstablePollResponse => Self::UnstablePollResponse,
|
||||
MessageLikeEventType::UnstablePollStart => Self::UnstablePollStart,
|
||||
MessageLikeEventType::Other(msgtype) => Self::from(msgtype),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, uniffi::Enum)]
|
||||
pub enum RoomMessageEventMessageType {
|
||||
Audio,
|
||||
Emote,
|
||||
File,
|
||||
#[cfg(feature = "unstable-msc4274")]
|
||||
Gallery,
|
||||
Image,
|
||||
Location,
|
||||
Notice,
|
||||
ServerNotice,
|
||||
Text,
|
||||
Video,
|
||||
VerificationRequest,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl From<RumaMessageType> for RoomMessageEventMessageType {
|
||||
fn from(val: ruma::events::room::message::MessageType) -> Self {
|
||||
match val {
|
||||
RumaMessageType::Audio { .. } => Self::Audio,
|
||||
RumaMessageType::Emote { .. } => Self::Emote,
|
||||
RumaMessageType::File { .. } => Self::File,
|
||||
#[cfg(feature = "unstable-msc4274")]
|
||||
RumaMessageType::Gallery { .. } => Self::Gallery,
|
||||
RumaMessageType::Image { .. } => Self::Image,
|
||||
RumaMessageType::Location { .. } => Self::Location,
|
||||
RumaMessageType::Notice { .. } => Self::Notice,
|
||||
RumaMessageType::ServerNotice { .. } => Self::ServerNotice,
|
||||
RumaMessageType::Text { .. } => Self::Text,
|
||||
RumaMessageType::Video { .. } => Self::Video,
|
||||
RumaMessageType::VerificationRequest { .. } => Self::VerificationRequest,
|
||||
_ => Self::Other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains the 2 possible identifiers of an event, either it has a remote
|
||||
/// event id or a local transaction id, never both or none.
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum EventOrTransactionId {
|
||||
EventId { event_id: String },
|
||||
TransactionId { transaction_id: String },
|
||||
}
|
||||
|
||||
impl From<TimelineEventItemId> for EventOrTransactionId {
|
||||
fn from(value: TimelineEventItemId) -> Self {
|
||||
match value {
|
||||
TimelineEventItemId::EventId(event_id) => {
|
||||
EventOrTransactionId::EventId { event_id: event_id.to_string() }
|
||||
}
|
||||
TimelineEventItemId::TransactionId(transaction_id) => {
|
||||
EventOrTransactionId::TransactionId { transaction_id: transaction_id.to_string() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<EventOrTransactionId> for TimelineEventItemId {
|
||||
type Error = IdParseError;
|
||||
fn try_from(value: EventOrTransactionId) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
EventOrTransactionId::EventId { event_id } => {
|
||||
Ok(TimelineEventItemId::EventId(EventId::parse(event_id)?))
|
||||
}
|
||||
EventOrTransactionId::TransactionId { transaction_id } => {
|
||||
Ok(TimelineEventItemId::TransactionId(transaction_id.into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,3 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(crate) fn unwrap_or_clone_arc<T: Clone>(arc: Arc<T>) -> T {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk_base::crypto::IdentityState;
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct IdentityStatusChange {
|
||||
/// The user ID of the user whose identity status changed
|
||||
pub user_id: String,
|
||||
|
||||
/// The new state of the identity of the user.
|
||||
pub changed_to: IdentityState,
|
||||
}
|
||||
@@ -1,8 +1,26 @@
|
||||
#![allow(unused_qualifications, clippy::new_without_default)]
|
||||
// Needed because uniffi macros contain empty lines after docs.
|
||||
#![allow(clippy::empty_line_after_doc_comments)]
|
||||
// TODO: target-os conditional would be good.
|
||||
|
||||
mod authentication;
|
||||
#![allow(unused_qualifications, clippy::new_without_default)]
|
||||
|
||||
macro_rules! unwrap_or_clone_arc_into_variant {
|
||||
(
|
||||
$arc:ident $(, .$field:tt)?, $pat:pat => $body:expr
|
||||
) => {
|
||||
#[allow(unused_variables)]
|
||||
match &(*$arc)$(.$field)? {
|
||||
$pat => {
|
||||
#[warn(unused_variables)]
|
||||
match crate::helpers::unwrap_or_clone_arc($arc)$(.$field)? {
|
||||
$pat => Some($body),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
mod authentication_service;
|
||||
mod chunk_iterator;
|
||||
mod client;
|
||||
mod client_builder;
|
||||
@@ -10,43 +28,36 @@ mod encryption;
|
||||
mod error;
|
||||
mod event;
|
||||
mod helpers;
|
||||
mod identity_status_change;
|
||||
mod live_location_share;
|
||||
mod notification;
|
||||
mod notification_settings;
|
||||
mod platform;
|
||||
mod qr_code;
|
||||
mod room;
|
||||
mod room_alias;
|
||||
mod room_directory_search;
|
||||
mod room_info;
|
||||
mod room_list;
|
||||
mod room_member;
|
||||
mod room_preview;
|
||||
mod ruma;
|
||||
mod runtime;
|
||||
mod search;
|
||||
mod session_verification;
|
||||
mod spaces;
|
||||
mod store;
|
||||
mod sync_service;
|
||||
mod sync_v2;
|
||||
mod task_handle;
|
||||
mod timeline;
|
||||
mod utd;
|
||||
mod tracing;
|
||||
mod utils;
|
||||
mod widget;
|
||||
|
||||
use matrix_sdk::ruma::events::room::message::RoomMessageEventContentWithoutRelation;
|
||||
use async_compat::TOKIO1 as RUNTIME;
|
||||
use matrix_sdk::ruma::events::room::{
|
||||
message::RoomMessageEventContentWithoutRelation, MediaSource,
|
||||
};
|
||||
|
||||
use self::{
|
||||
error::ClientError,
|
||||
ruma::{Mentions, RoomMessageEventContentWithoutRelationExt},
|
||||
ruma::{MediaSourceExt, Mentions, RoomMessageEventContentWithoutRelationExt},
|
||||
task_handle::TaskHandle,
|
||||
};
|
||||
|
||||
uniffi::include_scaffolding!("api");
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
fn sdk_git_sha() -> String {
|
||||
env!("VERGEN_GIT_SHA").to_owned()
|
||||
}
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::StreamExt as _;
|
||||
use matrix_sdk::live_location_share::{
|
||||
LiveLocationShare as SdkLiveLocationShare, LiveLocationShares as SdkLiveLocationShares,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
|
||||
use crate::{ruma::LocationContent, runtime::get_runtime_handle, task_handle::TaskHandle};
|
||||
|
||||
/// Details of the last known location beacon.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct LastLocation {
|
||||
/// The most recent location content shared for this asset.
|
||||
pub location: LocationContent,
|
||||
/// The timestamp of when the location was updated.
|
||||
pub ts: u64,
|
||||
}
|
||||
|
||||
/// Details of a user's live location share.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct LiveLocationShare {
|
||||
/// The asset's last known location.
|
||||
pub last_location: Option<LastLocation>,
|
||||
/// The user ID of the person sharing their live location.
|
||||
pub user_id: String,
|
||||
/// The time when location sharing started.
|
||||
pub start_ts: u64,
|
||||
/// The duration that the location sharing will be live.
|
||||
/// Meaning that the location will stop being shared at ts + timeout.
|
||||
pub timeout: u64,
|
||||
}
|
||||
|
||||
/// An update to the list of active live location shares.
|
||||
///
|
||||
/// Corresponds to a [`VectorDiff`] on the underlying [`ObservableVector`].
|
||||
///
|
||||
/// [`ObservableVector`]: eyeball_im::ObservableVector
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum LiveLocationShareUpdate {
|
||||
Append { values: Vec<LiveLocationShare> },
|
||||
Clear,
|
||||
PushFront { value: LiveLocationShare },
|
||||
PushBack { value: LiveLocationShare },
|
||||
PopFront,
|
||||
PopBack,
|
||||
Insert { index: u32, value: LiveLocationShare },
|
||||
Set { index: u32, value: LiveLocationShare },
|
||||
Remove { index: u32 },
|
||||
Truncate { length: u32 },
|
||||
Reset { values: Vec<LiveLocationShare> },
|
||||
}
|
||||
|
||||
/// Listener for live location share updates.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait LiveLocationShareListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
/// Called with a batch of [`LiveLocationShareUpdate`]s whenever the list
|
||||
/// of active shares changes.
|
||||
fn on_update(&self, updates: Vec<LiveLocationShareUpdate>);
|
||||
}
|
||||
|
||||
/// Tracks active live location shares in a room.
|
||||
///
|
||||
/// Holds the SDK [`SdkLiveLocationShares`] which keeps the beacon and
|
||||
/// beacon_info event handlers registered for as long as this object is alive.
|
||||
/// Call [`LiveLocationShares::subscribe`] to start receiving updates.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct LiveLocationShares {
|
||||
inner: SdkLiveLocationShares,
|
||||
}
|
||||
|
||||
impl LiveLocationShares {
|
||||
pub fn new(inner: SdkLiveLocationShares) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl LiveLocationShares {
|
||||
/// Subscribe to changes in the list of active live location shares.
|
||||
///
|
||||
/// Immediately calls `listener` with a `Reset` update containing the
|
||||
/// current snapshot (if non-empty), then calls it again for every
|
||||
/// subsequent change that arrives from sync.
|
||||
///
|
||||
/// Returns a [`TaskHandle`] that, when dropped, stops the listener.
|
||||
/// The event handlers remain registered for as long as this
|
||||
/// [`LiveLocationShares`] object is alive.
|
||||
pub fn subscribe(&self, listener: Box<dyn LiveLocationShareListener>) -> Arc<TaskHandle> {
|
||||
let (initial_values, mut stream) = self.inner.subscribe();
|
||||
|
||||
if !initial_values.is_empty() {
|
||||
listener.on_update(vec![LiveLocationShareUpdate::Reset {
|
||||
values: initial_values.into_iter().map(Into::into).collect(),
|
||||
}]);
|
||||
}
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(diffs) = stream.next().await {
|
||||
listener.on_update(diffs.into_iter().map(Into::into).collect());
|
||||
}
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SdkLiveLocationShare> for LiveLocationShare {
|
||||
fn from(share: SdkLiveLocationShare) -> Self {
|
||||
let start_ts = share.beacon_info.ts.0.into();
|
||||
let timeout = share.beacon_info.timeout.as_millis() as u64;
|
||||
let asset = share.beacon_info.asset.type_.into();
|
||||
let last_location = share.last_location.map(|l| LastLocation {
|
||||
location: LocationContent {
|
||||
body: "".to_owned(),
|
||||
geo_uri: l.location.uri.to_string(),
|
||||
description: None,
|
||||
zoom_level: None,
|
||||
asset,
|
||||
},
|
||||
ts: l.ts.0.into(),
|
||||
});
|
||||
LiveLocationShare { user_id: share.user_id.to_string(), last_location, start_ts, timeout }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VectorDiff<SdkLiveLocationShare>> for LiveLocationShareUpdate {
|
||||
fn from(diff: VectorDiff<SdkLiveLocationShare>) -> Self {
|
||||
match diff {
|
||||
VectorDiff::Append { values } => {
|
||||
Self::Append { values: values.into_iter().map(Into::into).collect() }
|
||||
}
|
||||
VectorDiff::Clear => Self::Clear,
|
||||
VectorDiff::PushFront { value } => Self::PushFront { value: value.into() },
|
||||
VectorDiff::PushBack { value } => Self::PushBack { value: value.into() },
|
||||
VectorDiff::PopFront => Self::PopFront,
|
||||
VectorDiff::PopBack => Self::PopBack,
|
||||
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::Truncate { length } => Self::Truncate { length: length as u32 },
|
||||
VectorDiff::Reset { values } => {
|
||||
Self::Reset { values: values.into_iter().map(Into::into).collect() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,14 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk_ui::notification_client::{
|
||||
NotificationClient as SdkNotificationClient, NotificationEvent as SdkNotificationEvent,
|
||||
NotificationItem as SdkNotificationItem, NotificationStatus as SdkNotificationStatus,
|
||||
RawNotificationEvent as SdkRawNotificationEvent,
|
||||
NotificationClient as MatrixNotificationClient,
|
||||
NotificationClientBuilder as MatrixNotificationClientBuilder,
|
||||
NotificationItem as MatrixNotificationItem, NotificationProcessSetup,
|
||||
};
|
||||
use ruma::{EventId, OwnedEventId, OwnedRoomId, RoomId};
|
||||
use ruma::{EventId, RoomId};
|
||||
|
||||
use crate::{
|
||||
client::{Client, JoinRule},
|
||||
error::ClientError,
|
||||
event::TimelineEvent,
|
||||
room::Room,
|
||||
client::Client, error::ClientError, event::TimelineEvent, helpers::unwrap_or_clone_arc, RUNTIME,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
@@ -38,7 +21,6 @@ pub enum NotificationEvent {
|
||||
pub struct NotificationSenderInfo {
|
||||
pub display_name: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
pub is_name_ambiguous: bool,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
@@ -46,22 +28,15 @@ pub struct NotificationRoomInfo {
|
||||
pub display_name: String,
|
||||
pub avatar_url: Option<String>,
|
||||
pub canonical_alias: Option<String>,
|
||||
pub topic: Option<String>,
|
||||
pub join_rule: Option<JoinRule>,
|
||||
pub joined_members_count: u64,
|
||||
pub service_members: Vec<String>,
|
||||
pub is_encrypted: Option<bool>,
|
||||
pub is_direct: bool,
|
||||
pub is_space: bool,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct NotificationItem {
|
||||
pub event: NotificationEvent,
|
||||
|
||||
/// The raw JSON of the underlying event.
|
||||
pub raw_event: String,
|
||||
|
||||
pub sender_info: NotificationSenderInfo,
|
||||
pub room_info: NotificationRoomInfo,
|
||||
|
||||
@@ -70,202 +45,107 @@ pub struct NotificationItem {
|
||||
/// information to create a push context.
|
||||
pub is_noisy: Option<bool>,
|
||||
pub has_mention: Option<bool>,
|
||||
pub thread_id: Option<String>,
|
||||
|
||||
/// The push actions for this notification (notify, sound, highlight, etc.).
|
||||
pub actions: Option<Vec<crate::notification_settings::Action>>,
|
||||
}
|
||||
|
||||
impl NotificationItem {
|
||||
fn from_inner(item: SdkNotificationItem) -> Self {
|
||||
fn from_inner(item: MatrixNotificationItem) -> Self {
|
||||
let event = match item.event {
|
||||
SdkNotificationEvent::Timeline(event) => {
|
||||
matrix_sdk_ui::notification_client::NotificationEvent::Timeline(event) => {
|
||||
NotificationEvent::Timeline { event: Arc::new(TimelineEvent(event)) }
|
||||
}
|
||||
SdkNotificationEvent::Invite(event) => {
|
||||
matrix_sdk_ui::notification_client::NotificationEvent::Invite(event) => {
|
||||
NotificationEvent::Invite { sender: event.sender.to_string() }
|
||||
}
|
||||
};
|
||||
|
||||
let raw_event = match &item.raw_event {
|
||||
SdkRawNotificationEvent::Timeline(raw) => raw.json().get().to_owned(),
|
||||
SdkRawNotificationEvent::Invite(raw) => raw.json().get().to_owned(),
|
||||
};
|
||||
|
||||
Self {
|
||||
event,
|
||||
raw_event,
|
||||
sender_info: NotificationSenderInfo {
|
||||
display_name: item.sender_display_name,
|
||||
avatar_url: item.sender_avatar_url,
|
||||
is_name_ambiguous: item.is_sender_name_ambiguous,
|
||||
},
|
||||
room_info: NotificationRoomInfo {
|
||||
display_name: item.room_computed_display_name,
|
||||
display_name: item.room_display_name,
|
||||
avatar_url: item.room_avatar_url,
|
||||
canonical_alias: item.room_canonical_alias,
|
||||
topic: item.room_topic,
|
||||
join_rule: item.room_join_rule.map(TryInto::try_into).transpose().ok().flatten(),
|
||||
joined_members_count: item.joined_members_count,
|
||||
service_members: item.service_members,
|
||||
is_encrypted: item.is_room_encrypted,
|
||||
is_direct: item.is_direct_message_room,
|
||||
is_space: item.is_space,
|
||||
},
|
||||
is_noisy: item.is_noisy,
|
||||
has_mention: item.has_mention,
|
||||
thread_id: item.thread_id.map(|t| t.to_string()),
|
||||
actions: item
|
||||
.actions
|
||||
.map(|a| a.into_iter().filter_map(|action| action.try_into().ok()).collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum NotificationStatus {
|
||||
/// The event has been found and was not filtered out.
|
||||
Event { item: NotificationItem },
|
||||
/// The event couldn't be found in the network queries used to find it.
|
||||
EventNotFound,
|
||||
/// The event has been filtered out, either because of the user's push
|
||||
/// rules, or because the user which triggered it is ignored by the
|
||||
/// current user.
|
||||
EventFilteredOut,
|
||||
/// The event has been redacted.
|
||||
EventRedacted,
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct NotificationClientBuilder {
|
||||
client: Arc<Client>,
|
||||
builder: MatrixNotificationClientBuilder,
|
||||
}
|
||||
|
||||
impl From<SdkNotificationStatus> for NotificationStatus {
|
||||
fn from(item: SdkNotificationStatus) -> Self {
|
||||
match item {
|
||||
SdkNotificationStatus::Event(item) => {
|
||||
NotificationStatus::Event { item: NotificationItem::from_inner(*item) }
|
||||
}
|
||||
SdkNotificationStatus::EventNotFound => NotificationStatus::EventNotFound,
|
||||
SdkNotificationStatus::EventFilteredOut => NotificationStatus::EventFilteredOut,
|
||||
SdkNotificationStatus::EventRedacted => NotificationStatus::EventRedacted,
|
||||
}
|
||||
impl NotificationClientBuilder {
|
||||
pub(crate) fn new(
|
||||
client: Arc<Client>,
|
||||
process_setup: NotificationProcessSetup,
|
||||
) -> Result<Arc<Self>, ClientError> {
|
||||
let builder = RUNTIME.block_on(async {
|
||||
MatrixNotificationClient::builder((*client.inner).clone(), process_setup).await
|
||||
})?;
|
||||
Ok(Arc::new(Self { builder, client }))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum BatchNotificationResult {
|
||||
/// We have more detailed information about the notification.
|
||||
Ok { status: NotificationStatus },
|
||||
/// An error occurred while trying to fetch the notification.
|
||||
Error {
|
||||
/// The error message observed while handling a specific notification.
|
||||
message: String,
|
||||
},
|
||||
#[uniffi::export]
|
||||
impl NotificationClientBuilder {
|
||||
/// Filter out the notification event according to the push rules present in
|
||||
/// the event.
|
||||
pub fn filter_by_push_rules(self: Arc<Self>) -> Arc<Self> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
let builder = this.builder.filter_by_push_rules();
|
||||
Arc::new(Self { builder, client: this.client })
|
||||
}
|
||||
|
||||
pub fn finish(self: Arc<Self>) -> Arc<NotificationClient> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
Arc::new(NotificationClient { inner: this.builder.build(), _client: this.client })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct NotificationClient {
|
||||
pub(crate) inner: SdkNotificationClient,
|
||||
inner: MatrixNotificationClient,
|
||||
|
||||
/// A reference to the FFI client.
|
||||
///
|
||||
/// Note: we do this to make it so that the FFI `NotificationClient` keeps
|
||||
/// the FFI `Client` and thus the SDK `Client` alive. Otherwise, we
|
||||
/// would need to repeat the hack done in the FFI `Client::drop` method.
|
||||
pub(crate) client: Arc<Client>,
|
||||
_client: Arc<Client>,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
impl NotificationClient {
|
||||
/// Fetches a room by its ID using the in-memory state store backed client.
|
||||
///
|
||||
/// Useful to retrieve room information after running the limited
|
||||
/// notification client sliding sync loop.
|
||||
pub fn get_room(&self, room_id: String) -> Result<Option<Arc<Room>>, ClientError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
let sdk_room = self.inner.get_room(&room_id);
|
||||
let room = sdk_room
|
||||
.map(|room| Arc::new(Room::new(room, self.client.utd_hook_manager.get().cloned())));
|
||||
Ok(room)
|
||||
}
|
||||
|
||||
/// Fetches the content of a notification.
|
||||
///
|
||||
/// This will first try to get the notification using a short-lived sliding
|
||||
/// sync, and if the sliding-sync can't find the event, then it'll use a
|
||||
/// `/context` query to find the event with associated member information.
|
||||
///
|
||||
/// An error result means that we couldn't resolve the notification; in that
|
||||
/// case, a dummy notification may be displayed instead.
|
||||
pub async fn get_notification(
|
||||
/// See also documentation of
|
||||
/// `MatrixNotificationClient::get_notification`.
|
||||
pub fn get_notification(
|
||||
&self,
|
||||
room_id: String,
|
||||
event_id: String,
|
||||
) -> Result<NotificationStatus, ClientError> {
|
||||
) -> Result<Option<NotificationItem>, ClientError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
|
||||
let item =
|
||||
self.inner.get_notification(&room_id, &event_id).await.map_err(ClientError::from)?;
|
||||
|
||||
Ok(item.into())
|
||||
}
|
||||
|
||||
/// Get several notification items in a single batch.
|
||||
///
|
||||
/// Returns an error if the flow failed when preparing to fetch the
|
||||
/// notifications, and a [`HashMap`] containing either a
|
||||
/// [`BatchNotificationResult`], that indicates if the notification was
|
||||
/// successfully fetched (in which case, it's a [`NotificationStatus`]), or
|
||||
/// an error message if it couldn't be fetched.
|
||||
pub async fn get_notifications(
|
||||
&self,
|
||||
requests: Vec<NotificationItemsRequest>,
|
||||
) -> Result<HashMap<String, BatchNotificationResult>, ClientError> {
|
||||
let requests =
|
||||
requests.into_iter().map(TryInto::try_into).collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let items = self.inner.get_notifications(&requests).await?;
|
||||
|
||||
let mut batch_result = HashMap::new();
|
||||
for (key, value) in items.into_iter() {
|
||||
let result = match value {
|
||||
Ok(status) => BatchNotificationResult::Ok { status: status.into() },
|
||||
Err(error) => BatchNotificationResult::Error { message: error.to_string() },
|
||||
};
|
||||
batch_result.insert(key.to_string(), result);
|
||||
}
|
||||
|
||||
Ok(batch_result)
|
||||
}
|
||||
}
|
||||
|
||||
/// A request for notification items grouped by their room.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct NotificationItemsRequest {
|
||||
room_id: String,
|
||||
event_ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl NotificationItemsRequest {
|
||||
/// The parsed [`OwnedRoomId`] to use with the SDK crates.
|
||||
pub fn room_id(&self) -> Result<OwnedRoomId, ClientError> {
|
||||
RoomId::parse(&self.room_id).map_err(ClientError::from)
|
||||
}
|
||||
|
||||
/// The parsed [`OwnedEventId`] list to use with the SDK crates.
|
||||
pub fn event_ids(&self) -> Result<Vec<OwnedEventId>, ClientError> {
|
||||
self.event_ids
|
||||
.iter()
|
||||
.map(|id| EventId::parse(id).map_err(ClientError::from))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<NotificationItemsRequest>
|
||||
for matrix_sdk_ui::notification_client::NotificationItemsRequest
|
||||
{
|
||||
type Error = ClientError;
|
||||
fn try_from(value: NotificationItemsRequest) -> Result<Self, Self::Error> {
|
||||
Ok(Self { room_id: value.room_id()?, event_ids: value.event_ids()? })
|
||||
RUNTIME.block_on(async move {
|
||||
let item = self
|
||||
.inner
|
||||
.get_notification(&room_id, &event_id)
|
||||
.await
|
||||
.map_err(ClientError::from)?;
|
||||
if let Some(item) = item {
|
||||
Ok(Some(NotificationItem::from_inner(item)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,388 +1,22 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk::{
|
||||
Client as MatrixClient,
|
||||
event_handler::EventHandlerHandle,
|
||||
notification_settings::{
|
||||
NotificationSettings as SdkNotificationSettings,
|
||||
RoomNotificationMode as SdkRoomNotificationMode,
|
||||
},
|
||||
ruma::events::push_rules::PushRulesEvent,
|
||||
Client as MatrixClient,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::{
|
||||
Int, RoomId, UInt,
|
||||
events::push_rules::PushRulesEventContent,
|
||||
push::{
|
||||
Action as SdkAction, ComparisonOperator as SdkComparisonOperator, EventMatchConditionData,
|
||||
EventPropertyContainsConditionData, EventPropertyIsConditionData, HighlightTweakValue,
|
||||
PredefinedOverrideRuleId, PredefinedUnderrideRuleId, PushCondition as SdkPushCondition,
|
||||
RoomMemberCountConditionData, RoomMemberCountIs, RuleKind as SdkRuleKind,
|
||||
ScalarJsonValue as SdkJsonValue, SenderNotificationPermissionConditionData,
|
||||
Tweak as SdkTweak,
|
||||
},
|
||||
push::{PredefinedOverrideRuleId, PredefinedUnderrideRuleId, RuleKind},
|
||||
RoomId,
|
||||
};
|
||||
use tokio::sync::RwLock as AsyncRwLock;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::error::{ClientError, NotificationSettingsError};
|
||||
|
||||
#[derive(Clone, Default, uniffi::Enum)]
|
||||
pub enum ComparisonOperator {
|
||||
/// Equals
|
||||
#[default]
|
||||
Eq,
|
||||
|
||||
/// Less than
|
||||
Lt,
|
||||
|
||||
/// Greater than
|
||||
Gt,
|
||||
|
||||
/// Greater or equal
|
||||
Ge,
|
||||
|
||||
/// Less or equal
|
||||
Le,
|
||||
}
|
||||
|
||||
impl From<SdkComparisonOperator> for ComparisonOperator {
|
||||
fn from(value: SdkComparisonOperator) -> Self {
|
||||
match value {
|
||||
SdkComparisonOperator::Eq => Self::Eq,
|
||||
SdkComparisonOperator::Lt => Self::Lt,
|
||||
SdkComparisonOperator::Gt => Self::Gt,
|
||||
SdkComparisonOperator::Ge => Self::Ge,
|
||||
SdkComparisonOperator::Le => Self::Le,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ComparisonOperator> for SdkComparisonOperator {
|
||||
fn from(value: ComparisonOperator) -> Self {
|
||||
match value {
|
||||
ComparisonOperator::Eq => Self::Eq,
|
||||
ComparisonOperator::Lt => Self::Lt,
|
||||
ComparisonOperator::Gt => Self::Gt,
|
||||
ComparisonOperator::Ge => Self::Ge,
|
||||
ComparisonOperator::Le => Self::Le,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, uniffi::Enum)]
|
||||
pub enum JsonValue {
|
||||
/// Represents a `null` value.
|
||||
#[default]
|
||||
Null,
|
||||
|
||||
/// Represents a boolean.
|
||||
Bool { value: bool },
|
||||
|
||||
/// Represents an integer.
|
||||
Integer { value: i64 },
|
||||
|
||||
/// Represents a string.
|
||||
String { value: String },
|
||||
}
|
||||
|
||||
impl From<SdkJsonValue> for JsonValue {
|
||||
fn from(value: SdkJsonValue) -> Self {
|
||||
match value {
|
||||
SdkJsonValue::Null => Self::Null,
|
||||
SdkJsonValue::Bool(b) => Self::Bool { value: b },
|
||||
SdkJsonValue::Integer(i) => Self::Integer { value: i.into() },
|
||||
SdkJsonValue::String(s) => Self::String { value: s },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JsonValue> for SdkJsonValue {
|
||||
fn from(value: JsonValue) -> Self {
|
||||
match value {
|
||||
JsonValue::Null => Self::Null,
|
||||
JsonValue::Bool { value } => Self::Bool(value),
|
||||
JsonValue::Integer { value } => Self::Integer(Int::new(value).unwrap_or_default()),
|
||||
JsonValue::String { value } => Self::String(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum PushCondition {
|
||||
/// A glob pattern match on a field of the event.
|
||||
EventMatch {
|
||||
/// The [dot-separated path] of the property of the event to match.
|
||||
///
|
||||
/// [dot-separated path]: https://spec.matrix.org/latest/appendices/#dot-separated-property-paths
|
||||
key: String,
|
||||
|
||||
/// The glob-style pattern to match against.
|
||||
///
|
||||
/// Patterns with no special glob characters should be treated as having
|
||||
/// asterisks prepended and appended when testing the condition.
|
||||
pattern: String,
|
||||
},
|
||||
|
||||
/// Matches unencrypted messages where `content.body` contains the owner's
|
||||
/// display name in that room.
|
||||
ContainsDisplayName,
|
||||
|
||||
/// Matches the current number of members in the room.
|
||||
RoomMemberCount { prefix: ComparisonOperator, count: u64 },
|
||||
|
||||
/// Takes into account the current power levels in the room, ensuring the
|
||||
/// sender of the event has high enough power to trigger the
|
||||
/// notification.
|
||||
SenderNotificationPermission {
|
||||
/// The field in the power level event the user needs a minimum power
|
||||
/// level for.
|
||||
///
|
||||
/// Fields must be specified under the `notifications` property in the
|
||||
/// power level event's `content`.
|
||||
key: String,
|
||||
},
|
||||
|
||||
/// Exact value match on a property of the event.
|
||||
EventPropertyIs {
|
||||
/// The [dot-separated path] of the property of the event to match.
|
||||
///
|
||||
/// [dot-separated path]: https://spec.matrix.org/latest/appendices/#dot-separated-property-paths
|
||||
key: String,
|
||||
|
||||
/// The value to match against.
|
||||
value: JsonValue,
|
||||
},
|
||||
|
||||
/// Exact value match on a value in an array property of the event.
|
||||
EventPropertyContains {
|
||||
/// The [dot-separated path] of the property of the event to match.
|
||||
///
|
||||
/// [dot-separated path]: https://spec.matrix.org/latest/appendices/#dot-separated-property-paths
|
||||
key: String,
|
||||
|
||||
/// The value to match against.
|
||||
value: JsonValue,
|
||||
},
|
||||
}
|
||||
|
||||
impl TryFrom<SdkPushCondition> for PushCondition {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: SdkPushCondition) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
SdkPushCondition::EventMatch(data) => {
|
||||
Self::EventMatch { key: data.key, pattern: data.pattern }
|
||||
}
|
||||
#[allow(deprecated)]
|
||||
SdkPushCondition::ContainsDisplayName => Self::ContainsDisplayName,
|
||||
SdkPushCondition::RoomMemberCount(data) => {
|
||||
Self::RoomMemberCount { prefix: data.is.prefix.into(), count: data.is.count.into() }
|
||||
}
|
||||
SdkPushCondition::SenderNotificationPermission(data) => {
|
||||
Self::SenderNotificationPermission { key: data.key.to_string() }
|
||||
}
|
||||
SdkPushCondition::EventPropertyIs(data) => {
|
||||
Self::EventPropertyIs { key: data.key, value: data.value.into() }
|
||||
}
|
||||
SdkPushCondition::EventPropertyContains(data) => {
|
||||
Self::EventPropertyContains { key: data.key, value: data.value.into() }
|
||||
}
|
||||
_ => return Err("Unsupported condition type".to_owned()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PushCondition> for SdkPushCondition {
|
||||
fn from(value: PushCondition) -> Self {
|
||||
match value {
|
||||
PushCondition::EventMatch { key, pattern } => {
|
||||
Self::EventMatch(EventMatchConditionData::new(key, pattern))
|
||||
}
|
||||
#[allow(deprecated)]
|
||||
PushCondition::ContainsDisplayName => Self::ContainsDisplayName,
|
||||
PushCondition::RoomMemberCount { prefix, count } => {
|
||||
Self::RoomMemberCount(RoomMemberCountConditionData::new(RoomMemberCountIs {
|
||||
prefix: prefix.into(),
|
||||
count: UInt::new(count).unwrap_or_default(),
|
||||
}))
|
||||
}
|
||||
PushCondition::SenderNotificationPermission { key } => {
|
||||
Self::SenderNotificationPermission(SenderNotificationPermissionConditionData::new(
|
||||
key.into(),
|
||||
))
|
||||
}
|
||||
PushCondition::EventPropertyIs { key, value } => {
|
||||
Self::EventPropertyIs(EventPropertyIsConditionData::new(key, value.into()))
|
||||
}
|
||||
PushCondition::EventPropertyContains { key, value } => Self::EventPropertyContains(
|
||||
EventPropertyContainsConditionData::new(key, value.into()),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum RuleKind {
|
||||
/// User-configured rules that override all other kinds.
|
||||
Override,
|
||||
|
||||
/// Lowest priority user-defined rules.
|
||||
Underride,
|
||||
|
||||
/// Sender-specific rules.
|
||||
Sender,
|
||||
|
||||
/// Room-specific rules.
|
||||
Room,
|
||||
|
||||
/// Content-specific rules.
|
||||
Content,
|
||||
|
||||
Custom {
|
||||
value: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<SdkRuleKind> for RuleKind {
|
||||
fn from(value: SdkRuleKind) -> Self {
|
||||
match value {
|
||||
SdkRuleKind::Override => Self::Override,
|
||||
SdkRuleKind::Underride => Self::Underride,
|
||||
SdkRuleKind::Sender => Self::Sender,
|
||||
SdkRuleKind::Room => Self::Room,
|
||||
SdkRuleKind::Content => Self::Content,
|
||||
SdkRuleKind::_Custom(_) => Self::Custom { value: value.as_str().to_owned() },
|
||||
_ => Self::Custom { value: value.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RuleKind> for SdkRuleKind {
|
||||
fn from(value: RuleKind) -> Self {
|
||||
match value {
|
||||
RuleKind::Override => Self::Override,
|
||||
RuleKind::Underride => Self::Underride,
|
||||
RuleKind::Sender => Self::Sender,
|
||||
RuleKind::Room => Self::Room,
|
||||
RuleKind::Content => Self::Content,
|
||||
RuleKind::Custom { value } => SdkRuleKind::from(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
/// Enum representing the push notification tweaks for a rule.
|
||||
pub enum Tweak {
|
||||
/// A string representing the sound to be played when this notification
|
||||
/// arrives.
|
||||
///
|
||||
/// A value of "default" means to play a default sound. A device may choose
|
||||
/// to alert the user by some other means if appropriate, eg. vibration.
|
||||
Sound { value: String },
|
||||
|
||||
/// A boolean representing whether or not this message should be highlighted
|
||||
/// in the UI.
|
||||
Highlight { value: bool },
|
||||
|
||||
/// A custom tweak
|
||||
Custom {
|
||||
/// The name of the custom tweak (`set_tweak` field)
|
||||
name: String,
|
||||
|
||||
/// The value of the custom tweak as an encoded JSON string
|
||||
value: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl TryFrom<SdkTweak> for Tweak {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: SdkTweak) -> Result<Self, Self::Error> {
|
||||
Ok(match &value {
|
||||
SdkTweak::Sound(sound) => Self::Sound { value: sound.to_string() },
|
||||
SdkTweak::Highlight(highlight) => {
|
||||
Self::Highlight { value: matches!(highlight, HighlightTweakValue::Yes) }
|
||||
}
|
||||
_ => {
|
||||
let json_string = value
|
||||
.custom_value()
|
||||
.ok_or_else(|| "Unsupported tweak type".to_owned())?
|
||||
.to_string();
|
||||
|
||||
Self::Custom { name: value.set_tweak().to_owned(), value: json_string }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Tweak> for SdkTweak {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: Tweak) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
Tweak::Sound { value } => Self::Sound(value.into()),
|
||||
Tweak::Highlight { value } => Self::Highlight(value.into()),
|
||||
Tweak::Custom { name, value } => Self::new(
|
||||
name,
|
||||
Some(
|
||||
serde_json::value::RawValue::from_string(value)
|
||||
.map_err(|e| format!("Failed to convert JSON value: {e}"))?,
|
||||
),
|
||||
)
|
||||
.map_err(|e| format!("Failed to convert custom tweak: {e}"))?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
/// Enum representing the push notification actions for a rule.
|
||||
pub enum Action {
|
||||
/// Causes matching events to generate a notification.
|
||||
Notify,
|
||||
/// Sets an entry in the 'tweaks' dictionary sent to the push gateway.
|
||||
SetTweak { value: Tweak },
|
||||
}
|
||||
|
||||
impl TryFrom<SdkAction> for Action {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: SdkAction) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
SdkAction::Notify => Self::Notify,
|
||||
SdkAction::SetTweak(tweak) => Self::SetTweak {
|
||||
value: tweak.try_into().map_err(|e| format!("Failed to convert tweak: {e}"))?,
|
||||
},
|
||||
_ => return Err("Unsupported action type".to_owned()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Action> for SdkAction {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: Action) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
Action::Notify => Self::Notify,
|
||||
Action::SetTweak { value } => Self::SetTweak(
|
||||
value.try_into().map_err(|e| format!("Failed to convert tweak: {e}"))?,
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
use super::RUNTIME;
|
||||
use crate::error::NotificationSettingsError;
|
||||
|
||||
/// Enum representing the push notification modes for a room.
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
@@ -416,8 +50,8 @@ impl From<RoomNotificationMode> for SdkRoomNotificationMode {
|
||||
}
|
||||
|
||||
/// Delegate to notify of changes in push rules
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait NotificationSettingsDelegate: SyncOutsideWasm + SendOutsideWasm {
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait NotificationSettingsDelegate: Sync + Send {
|
||||
fn settings_did_change(&self);
|
||||
}
|
||||
|
||||
@@ -439,7 +73,7 @@ impl RoomNotificationSettings {
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct NotificationSettings {
|
||||
sdk_client: MatrixClient,
|
||||
sdk_notification_settings: Arc<AsyncRwLock<SdkNotificationSettings>>,
|
||||
sdk_notification_settings: Arc<RwLock<SdkNotificationSettings>>,
|
||||
pushrules_event_handler: Arc<RwLock<Option<EventHandlerHandle>>>,
|
||||
}
|
||||
|
||||
@@ -448,9 +82,10 @@ impl NotificationSettings {
|
||||
sdk_client: MatrixClient,
|
||||
sdk_notification_settings: SdkNotificationSettings,
|
||||
) -> Self {
|
||||
let sdk_notification_settings = Arc::new(RwLock::new(sdk_notification_settings));
|
||||
Self {
|
||||
sdk_client,
|
||||
sdk_notification_settings: Arc::new(AsyncRwLock::new(sdk_notification_settings)),
|
||||
sdk_notification_settings,
|
||||
pushrules_event_handler: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
@@ -459,13 +94,15 @@ impl NotificationSettings {
|
||||
impl Drop for NotificationSettings {
|
||||
fn drop(&mut self) {
|
||||
// Remove the event handler on the sdk_client.
|
||||
if let Some(event_handler) = self.pushrules_event_handler.read().unwrap().as_ref() {
|
||||
self.sdk_client.remove_event_handler(event_handler.clone());
|
||||
}
|
||||
RUNTIME.block_on(async move {
|
||||
if let Some(event_handler) = self.pushrules_event_handler.read().await.as_ref() {
|
||||
self.sdk_client.remove_event_handler(event_handler.clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl NotificationSettings {
|
||||
pub fn set_delegate(&self, delegate: Option<Box<dyn NotificationSettingsDelegate>>) {
|
||||
if let Some(delegate) = delegate {
|
||||
@@ -477,14 +114,18 @@ impl NotificationSettings {
|
||||
delegate.settings_did_change();
|
||||
});
|
||||
|
||||
*self.pushrules_event_handler.write().unwrap() = Some(event_handler);
|
||||
RUNTIME.block_on(async move {
|
||||
*self.pushrules_event_handler.write().await = Some(event_handler);
|
||||
});
|
||||
} else {
|
||||
// Remove the event handler if there is no delegate
|
||||
let event_handler = &mut *self.pushrules_event_handler.write().unwrap();
|
||||
if let Some(event_handler) = event_handler {
|
||||
self.sdk_client.remove_event_handler(event_handler.clone());
|
||||
}
|
||||
*event_handler = None;
|
||||
RUNTIME.block_on(async move {
|
||||
let event_handler = &mut *self.pushrules_event_handler.write().await;
|
||||
if let Some(event_handler) = event_handler {
|
||||
self.sdk_client.remove_event_handler(event_handler.clone());
|
||||
}
|
||||
*event_handler = None;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,11 +143,9 @@ impl NotificationSettings {
|
||||
is_encrypted: bool,
|
||||
is_one_to_one: bool,
|
||||
) -> Result<RoomNotificationSettings, NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
let parsed_room_id = RoomId::parse(&room_id)
|
||||
.map_err(|_e| NotificationSettingsError::InvalidRoomId { room_id })?;
|
||||
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
|
||||
// Get the current user defined mode for this room
|
||||
if let Some(mode) =
|
||||
notification_settings.get_user_defined_room_notification_mode(&parsed_room_id).await
|
||||
@@ -519,7 +158,6 @@ impl NotificationSettings {
|
||||
let mode = notification_settings
|
||||
.get_default_room_notification_mode(is_encrypted.into(), is_one_to_one.into())
|
||||
.await;
|
||||
|
||||
Ok(RoomNotificationSettings::new(mode.into(), true))
|
||||
}
|
||||
|
||||
@@ -529,15 +167,10 @@ impl NotificationSettings {
|
||||
room_id: String,
|
||||
mode: RoomNotificationMode,
|
||||
) -> Result<(), NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
let parsed_room_id = RoomId::parse(&room_id)
|
||||
.map_err(|_e| NotificationSettingsError::InvalidRoomId { room_id })?;
|
||||
|
||||
self.sdk_notification_settings
|
||||
.read()
|
||||
.await
|
||||
.set_room_notification_mode(&parsed_room_id, mode.into())
|
||||
.await?;
|
||||
|
||||
notification_settings.set_room_notification_mode(&parsed_room_id, mode.into()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -634,7 +267,7 @@ impl NotificationSettings {
|
||||
pub async fn is_room_mention_enabled(&self) -> Result<bool, NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
let enabled = notification_settings
|
||||
.is_push_rule_enabled(SdkRuleKind::Override, PredefinedOverrideRuleId::IsRoomMention)
|
||||
.is_push_rule_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention)
|
||||
.await?;
|
||||
Ok(enabled)
|
||||
}
|
||||
@@ -647,7 +280,7 @@ impl NotificationSettings {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
notification_settings
|
||||
.set_push_rule_enabled(
|
||||
SdkRuleKind::Override,
|
||||
RuleKind::Override,
|
||||
PredefinedOverrideRuleId::IsRoomMention,
|
||||
enabled,
|
||||
)
|
||||
@@ -659,38 +292,33 @@ impl NotificationSettings {
|
||||
pub async fn is_user_mention_enabled(&self) -> Result<bool, NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
let enabled = notification_settings
|
||||
.is_push_rule_enabled(SdkRuleKind::Override, PredefinedOverrideRuleId::IsUserMention)
|
||||
.is_push_rule_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention)
|
||||
.await?;
|
||||
Ok(enabled)
|
||||
}
|
||||
|
||||
/// Returns true if [MSC 4028 push rule][rule] is supported and enabled.
|
||||
/// Check if [MSC 4028 push rule][rule] is enabled.
|
||||
///
|
||||
/// [rule]: https://github.com/matrix-org/matrix-spec-proposals/blob/giomfo/push_encrypted_events/proposals/4028-push-all-encrypted-events-except-for-muted-rooms.md
|
||||
pub async fn can_push_encrypted_event_to_device(&self) -> bool {
|
||||
pub async fn can_homeserver_push_encrypted_event_to_device(&self) -> bool {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
// Check stable identifier
|
||||
if let Ok(enabled) = notification_settings
|
||||
.is_push_rule_enabled(SdkRuleKind::Override, ".m.rule.encrypted_event")
|
||||
.is_push_rule_enabled(RuleKind::Override, ".m.rule.encrypted_event")
|
||||
.await
|
||||
{
|
||||
enabled
|
||||
// Check unstable identifier
|
||||
} else if let Ok(enabled) = notification_settings
|
||||
.is_push_rule_enabled(RuleKind::Override, ".org.matrix.msc4028.encrypted_event")
|
||||
.await
|
||||
{
|
||||
enabled
|
||||
} else {
|
||||
// Check unstable identifier
|
||||
notification_settings
|
||||
.is_push_rule_enabled(SdkRuleKind::Override, ".org.matrix.msc4028.encrypted_event")
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether [MSC 4028 push rule][rule] is enabled on the homeserver.
|
||||
///
|
||||
/// [rule]: https://github.com/matrix-org/matrix-spec-proposals/blob/giomfo/push_encrypted_events/proposals/4028-push-all-encrypted-events-except-for-muted-rooms.md
|
||||
pub async fn can_homeserver_push_encrypted_event_to_device(&self) -> bool {
|
||||
self.sdk_client.can_homeserver_push_encrypted_event_to_device().await.unwrap()
|
||||
}
|
||||
|
||||
/// Set whether user mentions are enabled.
|
||||
pub async fn set_user_mention_enabled(
|
||||
&self,
|
||||
@@ -699,7 +327,7 @@ impl NotificationSettings {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
notification_settings
|
||||
.set_push_rule_enabled(
|
||||
SdkRuleKind::Override,
|
||||
RuleKind::Override,
|
||||
PredefinedOverrideRuleId::IsUserMention,
|
||||
enabled,
|
||||
)
|
||||
@@ -711,7 +339,7 @@ impl NotificationSettings {
|
||||
pub async fn is_call_enabled(&self) -> Result<bool, NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
let enabled = notification_settings
|
||||
.is_push_rule_enabled(SdkRuleKind::Underride, PredefinedUnderrideRuleId::Call)
|
||||
.is_push_rule_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::Call)
|
||||
.await?;
|
||||
Ok(enabled)
|
||||
}
|
||||
@@ -720,7 +348,7 @@ impl NotificationSettings {
|
||||
pub async fn set_call_enabled(&self, enabled: bool) -> Result<(), NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
notification_settings
|
||||
.set_push_rule_enabled(SdkRuleKind::Underride, PredefinedUnderrideRuleId::Call, enabled)
|
||||
.set_push_rule_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::Call, enabled)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -730,7 +358,7 @@ impl NotificationSettings {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
let enabled = notification_settings
|
||||
.is_push_rule_enabled(
|
||||
SdkRuleKind::Override,
|
||||
RuleKind::Override,
|
||||
PredefinedOverrideRuleId::InviteForMe.as_str(),
|
||||
)
|
||||
.await?;
|
||||
@@ -745,7 +373,7 @@ impl NotificationSettings {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
notification_settings
|
||||
.set_push_rule_enabled(
|
||||
SdkRuleKind::Override,
|
||||
RuleKind::Override,
|
||||
PredefinedOverrideRuleId::InviteForMe.as_str(),
|
||||
enabled,
|
||||
)
|
||||
@@ -753,30 +381,6 @@ impl NotificationSettings {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets a custom push rule with the given actions and conditions.
|
||||
pub async fn set_custom_push_rule(
|
||||
&self,
|
||||
rule_id: String,
|
||||
rule_kind: RuleKind,
|
||||
actions: Vec<Action>,
|
||||
conditions: Vec<PushCondition>,
|
||||
) -> Result<(), NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
let actions: Result<Vec<_>, _> =
|
||||
actions.into_iter().map(|action| action.try_into()).collect();
|
||||
let actions = actions.map_err(|e| NotificationSettingsError::Generic { msg: e })?;
|
||||
|
||||
notification_settings
|
||||
.create_custom_conditional_push_rule(
|
||||
rule_id,
|
||||
rule_kind.into(),
|
||||
actions,
|
||||
conditions.into_iter().map(|condition| condition.into()).collect(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unmute a room.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -799,11 +403,4 @@ impl NotificationSettings {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the raw push rules in JSON format.
|
||||
pub async fn get_raw_push_rules(&self) -> Result<Option<String>, ClientError> {
|
||||
let raw_push_rules =
|
||||
self.sdk_client.account().account_data::<PushRulesEventContent>().await?;
|
||||
Ok(raw_push_rules.map(|raw| serde_json::to_string(&raw)).transpose()?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
use std::{collections::HashMap, fmt::Debug, pin::Pin};
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||
use futures_core::future::BoxFuture;
|
||||
use opentelemetry::KeyValue;
|
||||
use opentelemetry_otlp::{Protocol, WithExportConfig};
|
||||
use opentelemetry_sdk::{runtime::RuntimeChannel, trace::Tracer, Resource};
|
||||
use tokio::runtime::Handle;
|
||||
use tracing_core::Subscriber;
|
||||
use tracing_subscriber::{
|
||||
fmt::{self, time::FormatTime, FormatEvent, FormatFields, FormattedFields},
|
||||
layer::SubscriberExt,
|
||||
registry::LookupSpan,
|
||||
util::SubscriberInitExt,
|
||||
EnvFilter, Layer,
|
||||
};
|
||||
|
||||
use crate::RUNTIME;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct TokioRuntime {
|
||||
runtime: Handle,
|
||||
}
|
||||
|
||||
impl opentelemetry_sdk::runtime::Runtime for TokioRuntime {
|
||||
type Interval = tokio_stream::wrappers::IntervalStream;
|
||||
type Delay = Pin<Box<tokio::time::Sleep>>;
|
||||
|
||||
fn interval(&self, period: std::time::Duration) -> Self::Interval {
|
||||
let _guard = self.runtime.enter();
|
||||
tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(period))
|
||||
}
|
||||
|
||||
fn spawn(&self, future: BoxFuture<'static, ()>) {
|
||||
#[allow(clippy::let_underscore_future)]
|
||||
let _ = self.runtime.spawn(future);
|
||||
}
|
||||
|
||||
fn delay(&self, duration: std::time::Duration) -> Self::Delay {
|
||||
let _guard = self.runtime.enter();
|
||||
Box::pin(tokio::time::sleep(duration))
|
||||
}
|
||||
}
|
||||
|
||||
impl RuntimeChannel for TokioRuntime {
|
||||
type Receiver<T: Debug + Send> = tokio_stream::wrappers::ReceiverStream<T>;
|
||||
type Sender<T: Debug + Send> = tokio::sync::mpsc::Sender<T>;
|
||||
|
||||
fn batch_message_channel<T: Debug + Send>(
|
||||
&self,
|
||||
capacity: usize,
|
||||
) -> (Self::Sender<T>, Self::Receiver<T>) {
|
||||
let (sender, receiver) = tokio::sync::mpsc::channel(capacity);
|
||||
(sender, tokio_stream::wrappers::ReceiverStream::new(receiver))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_otlp_tracer(
|
||||
user: String,
|
||||
password: String,
|
||||
otlp_endpoint: String,
|
||||
client_name: String,
|
||||
) -> anyhow::Result<Tracer> {
|
||||
let runtime = RUNTIME.handle().to_owned();
|
||||
|
||||
let auth = STANDARD.encode(format!("{user}:{password}"));
|
||||
let headers = HashMap::from([("Authorization".to_owned(), format!("Basic {auth}"))]);
|
||||
let http_client = matrix_sdk::reqwest::ClientBuilder::new().build()?;
|
||||
|
||||
let exporter = opentelemetry_otlp::new_exporter()
|
||||
.http()
|
||||
.with_http_client(http_client)
|
||||
.with_protocol(Protocol::HttpBinary)
|
||||
.with_endpoint(otlp_endpoint)
|
||||
.with_headers(headers);
|
||||
|
||||
let tracer_runtime = TokioRuntime { runtime: runtime.to_owned() };
|
||||
|
||||
let _guard = runtime.enter();
|
||||
let tracer = opentelemetry_otlp::new_pipeline()
|
||||
.tracing()
|
||||
.with_exporter(exporter)
|
||||
.with_trace_config(
|
||||
opentelemetry_sdk::trace::config()
|
||||
.with_resource(Resource::new(vec![KeyValue::new("service.name", client_name)])),
|
||||
)
|
||||
.install_batch(tracer_runtime)?;
|
||||
|
||||
Ok(tracer)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn log_panics() {
|
||||
std::env::set_var("RUST_BACKTRACE", "1");
|
||||
log_panics::init();
|
||||
}
|
||||
|
||||
fn text_layers<S>(config: TracingConfiguration) -> impl Layer<S>
|
||||
where
|
||||
S: Subscriber + for<'a> LookupSpan<'a>,
|
||||
{
|
||||
// Adjusted version of tracing_subscriber::fmt::Format
|
||||
struct EventFormatter {
|
||||
display_timestamp: bool,
|
||||
display_level: bool,
|
||||
}
|
||||
|
||||
impl EventFormatter {
|
||||
fn new() -> Self {
|
||||
Self { display_timestamp: true, display_level: true }
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn for_logcat() -> Self {
|
||||
// Level and time are already captured by logcat separately
|
||||
Self { display_timestamp: false, display_level: false }
|
||||
}
|
||||
|
||||
fn format_timestamp(&self, writer: &mut fmt::format::Writer<'_>) -> std::fmt::Result {
|
||||
if fmt::time::SystemTime.format_time(writer).is_err() {
|
||||
writer.write_str("<unknown time>")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_filename(
|
||||
&self,
|
||||
writer: &mut fmt::format::Writer<'_>,
|
||||
filename: &str,
|
||||
) -> std::fmt::Result {
|
||||
const CRATES_IO_PATH_MATCHER: &str = ".cargo/registry/src/index.crates.io";
|
||||
let crates_io_filename = filename
|
||||
.split_once(CRATES_IO_PATH_MATCHER)
|
||||
.and_then(|(_, rest)| rest.split_once('/').map(|(_, rest)| rest));
|
||||
|
||||
if let Some(filename) = crates_io_filename {
|
||||
writer.write_str("<crates.io>/")?;
|
||||
writer.write_str(filename)
|
||||
} else {
|
||||
writer.write_str(filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, N> FormatEvent<S, N> for EventFormatter
|
||||
where
|
||||
S: Subscriber + for<'a> LookupSpan<'a>,
|
||||
N: for<'a> FormatFields<'a> + 'static,
|
||||
{
|
||||
fn format_event(
|
||||
&self,
|
||||
ctx: &fmt::FmtContext<'_, S, N>,
|
||||
mut writer: fmt::format::Writer<'_>,
|
||||
event: &tracing_core::Event<'_>,
|
||||
) -> std::fmt::Result {
|
||||
let meta = event.metadata();
|
||||
|
||||
if self.display_timestamp {
|
||||
self.format_timestamp(&mut writer)?;
|
||||
writer.write_char(' ')?;
|
||||
}
|
||||
|
||||
if self.display_level {
|
||||
// For info and warn, add a padding space to the left
|
||||
write!(writer, "{:>5} ", meta.level())?;
|
||||
}
|
||||
|
||||
write!(writer, "{}: ", meta.target())?;
|
||||
|
||||
ctx.format_fields(writer.by_ref(), event)?;
|
||||
|
||||
if let Some(filename) = meta.file() {
|
||||
writer.write_str(" | ")?;
|
||||
self.write_filename(&mut writer, filename)?;
|
||||
if let Some(line_number) = meta.line() {
|
||||
write!(writer, ":{line_number}")?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(scope) = ctx.event_scope() {
|
||||
writer.write_str(" | spans: ")?;
|
||||
let mut first = true;
|
||||
|
||||
for span in scope.from_root() {
|
||||
if !first {
|
||||
writer.write_str(" > ")?;
|
||||
}
|
||||
first = false;
|
||||
write!(writer, "{}", span.metadata().name())?;
|
||||
|
||||
let ext = span.extensions();
|
||||
if let Some(fields) = &ext.get::<FormattedFields<N>>() {
|
||||
if !fields.is_empty() {
|
||||
write!(writer, "{{{fields}}}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(writer)
|
||||
}
|
||||
}
|
||||
|
||||
let file_layer = config.write_to_files.map(|c| {
|
||||
fmt::layer()
|
||||
.event_format(EventFormatter::new())
|
||||
// EventFormatter doesn't support ANSI colors anyways, but the
|
||||
// default field formatter does, which is unhelpful for iOS +
|
||||
// Android logs, but enabled by default.
|
||||
.with_ansi(false)
|
||||
.with_writer(tracing_appender::rolling::hourly(c.path, c.file_prefix))
|
||||
});
|
||||
|
||||
Layer::and_then(
|
||||
file_layer,
|
||||
config.write_to_stdout_or_system.then(|| {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
return fmt::layer()
|
||||
.event_format(EventFormatter::new())
|
||||
// See comment above.
|
||||
.with_ansi(false)
|
||||
.with_writer(std::io::stderr);
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
return fmt::layer()
|
||||
.event_format(EventFormatter::for_logcat())
|
||||
// See comment above.
|
||||
.with_ansi(false)
|
||||
.with_writer(paranoid_android::AndroidLogMakeWriter::new(
|
||||
"org.matrix.rust.sdk".to_owned(),
|
||||
));
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct TracingFileConfiguration {
|
||||
path: String,
|
||||
file_prefix: String,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct TracingConfiguration {
|
||||
filter: String,
|
||||
/// Controls whether to print to stdout or, equivalent, the system logs on
|
||||
/// Android.
|
||||
write_to_stdout_or_system: bool,
|
||||
write_to_files: Option<TracingFileConfiguration>,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
pub fn setup_tracing(config: TracingConfiguration) {
|
||||
#[cfg(target_os = "android")]
|
||||
log_panics();
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(EnvFilter::new(&config.filter))
|
||||
.with(text_layers(config))
|
||||
.init();
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct OtlpTracingConfiguration {
|
||||
client_name: String,
|
||||
user: String,
|
||||
password: String,
|
||||
otlp_endpoint: String,
|
||||
filter: String,
|
||||
/// Controls whether to print to stdout or, equivalent, the system logs on
|
||||
/// Android.
|
||||
write_to_stdout_or_system: bool,
|
||||
write_to_files: Option<TracingFileConfiguration>,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
pub fn setup_otlp_tracing(config: OtlpTracingConfiguration) {
|
||||
#[cfg(target_os = "android")]
|
||||
log_panics();
|
||||
|
||||
let otlp_tracer =
|
||||
create_otlp_tracer(config.user, config.password, config.otlp_endpoint, config.client_name)
|
||||
.expect("Couldn't configure the OpenTelemetry tracer");
|
||||
let otlp_layer = tracing_opentelemetry::layer().with_tracer(otlp_tracer);
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(EnvFilter::new(&config.filter))
|
||||
.with(text_layers(TracingConfiguration {
|
||||
filter: config.filter,
|
||||
write_to_stdout_or_system: config.write_to_stdout_or_system,
|
||||
write_to_files: config.write_to_files,
|
||||
}))
|
||||
.with(otlp_layer)
|
||||
.init();
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
use std::{error::Error, mem::MaybeUninit};
|
||||
|
||||
use jni::{
|
||||
errors::JniError,
|
||||
sys::{JNI_OK, JavaVM as RawJavaVM},
|
||||
};
|
||||
use tracing::debug;
|
||||
|
||||
static ANDROID_JVM: once_cell::sync::OnceCell<jni::JavaVM> = once_cell::sync::OnceCell::new();
|
||||
|
||||
/// Initialize the platform support for Android targets.
|
||||
///
|
||||
/// This includes setting up `rustls-platform-verifier`.
|
||||
pub(crate) fn init() {
|
||||
debug!("Initializing Android platform support");
|
||||
|
||||
ANDROID_JVM.get_or_init(|| {
|
||||
match get_java_vm() {
|
||||
Ok(jvm) => {
|
||||
// Initialize rustls platform verifier
|
||||
let mut env =
|
||||
jvm.attach_current_thread_permanently().expect("Failed to attach thread");
|
||||
init_rustls_platform_verifier(&mut env)
|
||||
.expect("Failed to initialize rustls platform verifier");
|
||||
|
||||
debug!("Android platform support initialized successfully");
|
||||
|
||||
jvm
|
||||
}
|
||||
Err(e) => {
|
||||
panic!("Failed to initialize Android platform support: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn get_java_vm() -> Result<jni::JavaVM, Box<dyn Error>> {
|
||||
debug!("Getting a JVM pointer");
|
||||
#[allow(non_snake_case)]
|
||||
let JNI_GetCreatedJavaVMs = unsafe {
|
||||
jvm_getter::find_jni_get_created_java_vms().expect("Failed to find JNI_GetCreatedJavaVMs")
|
||||
};
|
||||
|
||||
let mut vm: MaybeUninit<*mut RawJavaVM> = MaybeUninit::uninit();
|
||||
let status = unsafe { JNI_GetCreatedJavaVMs(vm.as_mut_ptr(), 1, &mut 0) };
|
||||
if status != JNI_OK {
|
||||
panic!("no JavaVM was found by JNI_GetCreatedJavaVMs");
|
||||
}
|
||||
|
||||
unsafe { jni::JavaVM::from_raw(vm.assume_init()).map_err(|e| e.into()) }
|
||||
}
|
||||
|
||||
fn init_rustls_platform_verifier(env: &mut jni::JNIEnv<'_>) -> jni::errors::Result<()> {
|
||||
// Get the current activity thread
|
||||
let activity_thread = env
|
||||
.call_static_method(
|
||||
"android/app/ActivityThread",
|
||||
"currentActivityThread",
|
||||
"()Landroid/app/ActivityThread;",
|
||||
&[],
|
||||
)?
|
||||
.l()?;
|
||||
|
||||
// Then get the application context
|
||||
let context = env
|
||||
.call_method(activity_thread, "getApplication", "()Landroid/app/Application;", &[])?
|
||||
.l()?;
|
||||
|
||||
Ok(rustls_platform_verifier::android::init_hosted(env, context)?)
|
||||
}
|
||||
|
||||
/// Attach the current thread to a JVM one.
|
||||
pub(crate) fn android_attach_current_thread_permanently()
|
||||
-> jni::errors::Result<jni::JNIEnv<'static>> {
|
||||
ANDROID_JVM
|
||||
.get()
|
||||
.ok_or_else(|| jni::errors::Error::JniCall(JniError::Unknown))?
|
||||
.attach_current_thread_permanently()
|
||||
}
|
||||
@@ -1,931 +0,0 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
#[cfg(feature = "sentry")]
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
|
||||
use ::tracing::info;
|
||||
#[cfg(feature = "sentry")]
|
||||
use ::tracing::warn;
|
||||
use tracing_appender::rolling::Rotation;
|
||||
#[cfg(feature = "sentry")]
|
||||
use tracing_core::Level;
|
||||
use tracing_core::Subscriber;
|
||||
use tracing_subscriber::{
|
||||
EnvFilter, Layer, Registry,
|
||||
field::RecordFields,
|
||||
fmt::{
|
||||
self, FormatEvent, FormatFields, FormattedFields,
|
||||
format::{DefaultFields, Writer},
|
||||
time::FormatTime,
|
||||
},
|
||||
layer::{Layered, SubscriberExt as _},
|
||||
registry::LookupSpan,
|
||||
reload::{self, Handle},
|
||||
util::SubscriberInitExt as _,
|
||||
};
|
||||
|
||||
use crate::error::ClientError;
|
||||
|
||||
/// Default maximum total size of all log files combined (10MB).
|
||||
const DEFAULT_MAX_TOTAL_SIZE_BYTES: u64 = 10 * 1024 * 1024;
|
||||
|
||||
/// Default maximum age of log files in seconds (1 week).
|
||||
const DEFAULT_MAX_AGE_SECONDS: u64 = 7 * 24 * 60 * 60;
|
||||
|
||||
mod rolling_writer;
|
||||
pub mod tracing;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
mod android_platform;
|
||||
|
||||
use rolling_writer::SizeAndDateRollingWriter;
|
||||
|
||||
#[cfg(feature = "sentry")]
|
||||
use self::tracing::BRIDGE_SPAN_NAME;
|
||||
use self::tracing::LogLevel;
|
||||
|
||||
// Adjusted version of tracing_subscriber::fmt::Format
|
||||
struct EventFormatter {
|
||||
display_timestamp: bool,
|
||||
display_level: bool,
|
||||
}
|
||||
|
||||
impl EventFormatter {
|
||||
fn new() -> Self {
|
||||
Self { display_timestamp: true, display_level: true }
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn for_logcat() -> Self {
|
||||
// Level and time are already captured by logcat separately
|
||||
Self { display_timestamp: false, display_level: false }
|
||||
}
|
||||
|
||||
fn format_timestamp(&self, writer: &mut fmt::format::Writer<'_>) -> std::fmt::Result {
|
||||
if fmt::time::SystemTime.format_time(writer).is_err() {
|
||||
writer.write_str("<unknown time>")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_filename(
|
||||
&self,
|
||||
writer: &mut fmt::format::Writer<'_>,
|
||||
filename: &str,
|
||||
) -> std::fmt::Result {
|
||||
const CRATES_IO_PATH_MATCHER: &str = ".cargo/registry/src/index.crates.io";
|
||||
let crates_io_filename = filename
|
||||
.split_once(CRATES_IO_PATH_MATCHER)
|
||||
.and_then(|(_, rest)| rest.split_once('/').map(|(_, rest)| rest));
|
||||
|
||||
if let Some(filename) = crates_io_filename {
|
||||
writer.write_str("<crates.io>/")?;
|
||||
writer.write_str(filename)
|
||||
} else {
|
||||
writer.write_str(filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, N> FormatEvent<S, N> for EventFormatter
|
||||
where
|
||||
S: Subscriber + for<'a> LookupSpan<'a>,
|
||||
N: for<'a> FormatFields<'a> + 'static,
|
||||
{
|
||||
fn format_event(
|
||||
&self,
|
||||
ctx: &fmt::FmtContext<'_, S, N>,
|
||||
mut writer: fmt::format::Writer<'_>,
|
||||
event: &tracing_core::Event<'_>,
|
||||
) -> std::fmt::Result {
|
||||
let meta = event.metadata();
|
||||
|
||||
if self.display_timestamp {
|
||||
self.format_timestamp(&mut writer)?;
|
||||
writer.write_char(' ')?;
|
||||
}
|
||||
|
||||
if self.display_level {
|
||||
// For info and warn, add a padding space to the left
|
||||
write!(writer, "{:>5} ", meta.level())?;
|
||||
}
|
||||
|
||||
write!(writer, "{}: ", meta.target())?;
|
||||
|
||||
ctx.format_fields(writer.by_ref(), event)?;
|
||||
|
||||
if let Some(filename) = meta.file() {
|
||||
writer.write_str(" | ")?;
|
||||
self.write_filename(&mut writer, filename)?;
|
||||
if let Some(line_number) = meta.line() {
|
||||
write!(writer, ":{line_number}")?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(scope) = ctx.event_scope() {
|
||||
writer.write_str(" | spans: ")?;
|
||||
|
||||
let mut first = true;
|
||||
|
||||
for span in scope.from_root() {
|
||||
if !first {
|
||||
writer.write_str(" > ")?;
|
||||
}
|
||||
|
||||
first = false;
|
||||
|
||||
write!(writer, "{}", span.name())?;
|
||||
|
||||
if let Some(fields) = &span.extensions().get::<FormattedFields<N>>()
|
||||
&& !fields.is_empty()
|
||||
{
|
||||
write!(writer, "{{{fields}}}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(writer)
|
||||
}
|
||||
}
|
||||
|
||||
// Another fields formatter is necessary because of this bug
|
||||
// https://github.com/tokio-rs/tracing/issues/1372. Using a new
|
||||
// formatter for the fields forces to record them in different span
|
||||
// extensions, and thus remove the duplicated fields in the span.
|
||||
#[derive(Default)]
|
||||
struct FieldsFormatterForFiles(DefaultFields);
|
||||
|
||||
impl<'writer> FormatFields<'writer> for FieldsFormatterForFiles {
|
||||
fn format_fields<R: RecordFields>(
|
||||
&self,
|
||||
writer: Writer<'writer>,
|
||||
fields: R,
|
||||
) -> std::fmt::Result {
|
||||
self.0.format_fields(writer, fields)
|
||||
}
|
||||
}
|
||||
|
||||
type ReloadHandle = Handle<
|
||||
tracing_subscriber::fmt::Layer<
|
||||
Layered<EnvFilter, Registry>,
|
||||
FieldsFormatterForFiles,
|
||||
EventFormatter,
|
||||
SizeAndDateRollingWriter,
|
||||
>,
|
||||
Layered<EnvFilter, Registry>,
|
||||
>;
|
||||
|
||||
fn text_layers(
|
||||
config: TracingConfiguration,
|
||||
) -> (impl Layer<Layered<EnvFilter, Registry>>, Option<ReloadHandle>) {
|
||||
let (file_layer, reload_handle) = config
|
||||
.write_to_files
|
||||
.map(|c| {
|
||||
let layer = make_file_layer(c);
|
||||
reload::Layer::new(layer)
|
||||
})
|
||||
.unzip();
|
||||
|
||||
let layers = Layer::and_then(
|
||||
file_layer,
|
||||
config.write_to_stdout_or_system.then(|| {
|
||||
// Another fields formatter is necessary because of this bug
|
||||
// https://github.com/tokio-rs/tracing/issues/1372. Using a new
|
||||
// formatter for the fields forces to record them in different span
|
||||
// extensions, and thus remove the duplicated fields in the span.
|
||||
#[derive(Default)]
|
||||
struct FieldsFormatterFormStdoutOrSystem(DefaultFields);
|
||||
|
||||
impl<'writer> FormatFields<'writer> for FieldsFormatterFormStdoutOrSystem {
|
||||
fn format_fields<R: RecordFields>(
|
||||
&self,
|
||||
writer: Writer<'writer>,
|
||||
fields: R,
|
||||
) -> std::fmt::Result {
|
||||
self.0.format_fields(writer, fields)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
return fmt::layer()
|
||||
.fmt_fields(FieldsFormatterFormStdoutOrSystem::default())
|
||||
.event_format(EventFormatter::new())
|
||||
// See comment above.
|
||||
.with_ansi(false)
|
||||
.with_writer(std::io::stderr);
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
return fmt::layer()
|
||||
.fmt_fields(FieldsFormatterFormStdoutOrSystem::default())
|
||||
.event_format(EventFormatter::for_logcat())
|
||||
// See comment above.
|
||||
.with_ansi(false)
|
||||
.with_writer(paranoid_android::AndroidLogMakeWriter::new(
|
||||
"org.matrix.rust.sdk".to_owned(),
|
||||
));
|
||||
}),
|
||||
);
|
||||
|
||||
(layers, reload_handle)
|
||||
}
|
||||
|
||||
fn make_file_layer(
|
||||
file_configuration: TracingFileConfiguration,
|
||||
) -> tracing_subscriber::fmt::Layer<
|
||||
Layered<EnvFilter, Registry, Registry>,
|
||||
FieldsFormatterForFiles,
|
||||
EventFormatter,
|
||||
SizeAndDateRollingWriter,
|
||||
> {
|
||||
let writer = SizeAndDateRollingWriter::new(
|
||||
&file_configuration.path,
|
||||
file_configuration.file_prefix,
|
||||
file_configuration.file_suffix.unwrap_or_else(|| String::from(".log")),
|
||||
Rotation::HOURLY,
|
||||
file_configuration.max_total_size_bytes.unwrap_or(DEFAULT_MAX_TOTAL_SIZE_BYTES),
|
||||
file_configuration.max_age_seconds.unwrap_or(DEFAULT_MAX_AGE_SECONDS),
|
||||
)
|
||||
.expect("Failed to create a rolling file appender.");
|
||||
|
||||
fmt::layer()
|
||||
.fmt_fields(FieldsFormatterForFiles::default())
|
||||
.event_format(EventFormatter::new())
|
||||
// EventFormatter doesn't support ANSI colors anyways, but the
|
||||
// default field formatter does, which is unhelpful for iOS +
|
||||
// Android logs, but enabled by default.
|
||||
.with_ansi(false)
|
||||
.with_writer(writer)
|
||||
}
|
||||
|
||||
/// Configuration to save logs to (rotated) log-files.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct TracingFileConfiguration {
|
||||
/// Base location for all the log files.
|
||||
path: String,
|
||||
|
||||
/// Prefix for the log files' names.
|
||||
file_prefix: String,
|
||||
|
||||
/// Optional suffix for the log file's names.
|
||||
///
|
||||
/// Default is ".log" if not specified.
|
||||
file_suffix: Option<String>,
|
||||
|
||||
/// Maximum total size of all log files combined in bytes.
|
||||
///
|
||||
/// When the total size of all log files with the configured prefix and
|
||||
/// suffix exceeds this limit, the oldest files will be removed until
|
||||
/// the total is below the limit.
|
||||
///
|
||||
/// This is useful to prevent log files from consuming too much disk space
|
||||
/// over time, even with multiple rotated files.
|
||||
///
|
||||
/// Default: 10MB (10 * 1024 * 1024 bytes) if not specified.
|
||||
max_total_size_bytes: Option<u64>,
|
||||
|
||||
/// Maximum age of log files in seconds.
|
||||
///
|
||||
/// Log files older than this age will be automatically removed during
|
||||
/// cleanup. This is checked when the writer is created and during
|
||||
/// rotation operations.
|
||||
///
|
||||
/// Default: 1 week (7 * 24 * 60 * 60 seconds) if not specified.
|
||||
max_age_seconds: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, PartialOrd)]
|
||||
enum LogTarget {
|
||||
// External crates.
|
||||
Hyper,
|
||||
|
||||
// FFI modules.
|
||||
MatrixSdkFfi,
|
||||
|
||||
// SDK base modules.
|
||||
MatrixSdkBaseEventCache,
|
||||
MatrixSdkBaseSlidingSync,
|
||||
MatrixSdkBaseStoreAmbiguityMap,
|
||||
MatrixSdkBaseResponseProcessors,
|
||||
|
||||
// SDK common modules.
|
||||
MatrixSdkCommonCrossProcessLock,
|
||||
MatrixSdkCommonDeserializedResponses,
|
||||
|
||||
// SDK modules.
|
||||
MatrixSdk,
|
||||
MatrixSdkClient,
|
||||
MatrixSdkCrypto,
|
||||
MatrixSdkCryptoAccount,
|
||||
MatrixSdkEventCache,
|
||||
MatrixSdkEventCacheStore,
|
||||
MatrixSdkHttpClient,
|
||||
MatrixSdkLatestEvents,
|
||||
MatrixSdkOidc,
|
||||
MatrixSdkSendQueue,
|
||||
MatrixSdkSlidingSync,
|
||||
|
||||
// SDK UI modules.
|
||||
MatrixSdkUiTimeline,
|
||||
MatrixSdkUiNotificationClient,
|
||||
}
|
||||
|
||||
impl LogTarget {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
LogTarget::Hyper => "hyper",
|
||||
LogTarget::MatrixSdkFfi => "matrix_sdk_ffi",
|
||||
LogTarget::MatrixSdkBaseEventCache => "matrix_sdk_base::event_cache",
|
||||
LogTarget::MatrixSdkBaseSlidingSync => "matrix_sdk_base::sliding_sync",
|
||||
LogTarget::MatrixSdkBaseStoreAmbiguityMap => "matrix_sdk_base::store::ambiguity_map",
|
||||
LogTarget::MatrixSdkBaseResponseProcessors => "matrix_sdk_base::response_processors",
|
||||
LogTarget::MatrixSdkCommonCrossProcessLock => "matrix_sdk_common::cross_process_lock",
|
||||
LogTarget::MatrixSdkCommonDeserializedResponses => {
|
||||
"matrix_sdk_common::deserialized_responses"
|
||||
}
|
||||
LogTarget::MatrixSdk => "matrix_sdk",
|
||||
LogTarget::MatrixSdkClient => "matrix_sdk::client",
|
||||
LogTarget::MatrixSdkCrypto => "matrix_sdk_crypto",
|
||||
LogTarget::MatrixSdkCryptoAccount => "matrix_sdk_crypto::olm::account",
|
||||
LogTarget::MatrixSdkOidc => "matrix_sdk::oidc",
|
||||
LogTarget::MatrixSdkHttpClient => "matrix_sdk::http_client",
|
||||
LogTarget::MatrixSdkSlidingSync => "matrix_sdk::sliding_sync",
|
||||
LogTarget::MatrixSdkEventCache => "matrix_sdk::event_cache",
|
||||
LogTarget::MatrixSdkLatestEvents => "matrix_sdk::latest_events",
|
||||
LogTarget::MatrixSdkSendQueue => "matrix_sdk::send_queue",
|
||||
LogTarget::MatrixSdkEventCacheStore => "matrix_sdk_sqlite::event_cache_store",
|
||||
LogTarget::MatrixSdkUiTimeline => "matrix_sdk_ui::timeline",
|
||||
LogTarget::MatrixSdkUiNotificationClient => "matrix_sdk_ui::notification_client",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_TARGET_LOG_LEVELS: &[(LogTarget, LogLevel)] = &[
|
||||
(LogTarget::Hyper, LogLevel::Warn),
|
||||
(LogTarget::MatrixSdkFfi, LogLevel::Info),
|
||||
(LogTarget::MatrixSdk, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkClient, LogLevel::Trace),
|
||||
(LogTarget::MatrixSdkCrypto, LogLevel::Debug),
|
||||
(LogTarget::MatrixSdkCryptoAccount, LogLevel::Trace),
|
||||
(LogTarget::MatrixSdkOidc, LogLevel::Trace),
|
||||
(LogTarget::MatrixSdkHttpClient, LogLevel::Debug),
|
||||
(LogTarget::MatrixSdkSlidingSync, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkBaseSlidingSync, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkUiTimeline, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkSendQueue, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkEventCache, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkLatestEvents, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkBaseEventCache, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkEventCacheStore, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkCommonCrossProcessLock, LogLevel::Warn),
|
||||
(LogTarget::MatrixSdkCommonDeserializedResponses, LogLevel::Warn),
|
||||
(LogTarget::MatrixSdkBaseStoreAmbiguityMap, LogLevel::Warn),
|
||||
(LogTarget::MatrixSdkUiNotificationClient, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkBaseResponseProcessors, LogLevel::Debug),
|
||||
];
|
||||
|
||||
const IMMUTABLE_LOG_TARGETS: &[LogTarget] = &[
|
||||
LogTarget::Hyper, // Too verbose
|
||||
LogTarget::MatrixSdk, // Too generic
|
||||
LogTarget::MatrixSdkFfi, // Too verbose
|
||||
LogTarget::MatrixSdkCommonCrossProcessLock, // Too verbose
|
||||
LogTarget::MatrixSdkBaseStoreAmbiguityMap, // Too verbose
|
||||
];
|
||||
|
||||
/// A log pack can be used to set the trace log level for a group of multiple
|
||||
/// log targets at once, for debugging purposes.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum TraceLogPacks {
|
||||
/// Enables all the logs relevant to the event cache.
|
||||
EventCache,
|
||||
/// Enables all the logs relevant to the send queue.
|
||||
SendQueue,
|
||||
/// Enables all the logs relevant to the timeline.
|
||||
Timeline,
|
||||
/// Enables all the logs relevant to the notification client.
|
||||
NotificationClient,
|
||||
/// Enables all the logs relevant to sync profiling.
|
||||
SyncProfiling,
|
||||
/// Enables all the logs relevant to the latest events.
|
||||
LatestEvents,
|
||||
}
|
||||
|
||||
impl TraceLogPacks {
|
||||
// Note: all the log targets returned here must be part of
|
||||
// `DEFAULT_TARGET_LOG_LEVELS`.
|
||||
fn targets(&self) -> &[LogTarget] {
|
||||
match self {
|
||||
TraceLogPacks::EventCache => &[
|
||||
LogTarget::MatrixSdkEventCache,
|
||||
LogTarget::MatrixSdkBaseEventCache,
|
||||
LogTarget::MatrixSdkEventCacheStore,
|
||||
LogTarget::MatrixSdkCommonCrossProcessLock,
|
||||
LogTarget::MatrixSdkCommonDeserializedResponses,
|
||||
],
|
||||
TraceLogPacks::SendQueue => &[LogTarget::MatrixSdkSendQueue],
|
||||
TraceLogPacks::Timeline => {
|
||||
&[LogTarget::MatrixSdkUiTimeline, LogTarget::MatrixSdkCommonDeserializedResponses]
|
||||
}
|
||||
TraceLogPacks::NotificationClient => &[LogTarget::MatrixSdkUiNotificationClient],
|
||||
TraceLogPacks::SyncProfiling => &[
|
||||
LogTarget::MatrixSdkSlidingSync,
|
||||
LogTarget::MatrixSdkBaseSlidingSync,
|
||||
LogTarget::MatrixSdkBaseResponseProcessors,
|
||||
LogTarget::MatrixSdkCrypto,
|
||||
LogTarget::MatrixSdkCommonCrossProcessLock,
|
||||
LogTarget::MatrixSdkCommonDeserializedResponses,
|
||||
],
|
||||
TraceLogPacks::LatestEvents => &[
|
||||
LogTarget::MatrixSdkLatestEvents,
|
||||
LogTarget::MatrixSdkSendQueue,
|
||||
LogTarget::MatrixSdkEventCache,
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sentry")]
|
||||
struct SentryLoggingCtx {
|
||||
/// The Sentry client guard, which keeps the Sentry context alive.
|
||||
_guard: sentry::ClientInitGuard,
|
||||
|
||||
/// Whether the Sentry layer is enabled or not, at a global level.
|
||||
enabled: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
struct LoggingCtx {
|
||||
reload_handle: Option<ReloadHandle>,
|
||||
#[cfg(feature = "sentry")]
|
||||
sentry: Option<SentryLoggingCtx>,
|
||||
}
|
||||
|
||||
static LOGGING: OnceLock<LoggingCtx> = OnceLock::new();
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct TracingConfiguration {
|
||||
/// The desired log level.
|
||||
log_level: LogLevel,
|
||||
|
||||
/// All the log packs, that will be set to `TRACE` when they're enabled.
|
||||
trace_log_packs: Vec<TraceLogPacks>,
|
||||
|
||||
/// Additional targets that the FFI client would like to use.
|
||||
///
|
||||
/// This can include, for instance, the target names for created
|
||||
/// [`crate::tracing::Span`]. These targets will use the global log level by
|
||||
/// default.
|
||||
extra_targets: Vec<String>,
|
||||
|
||||
/// Whether to log to stdout, or in the logcat on Android.
|
||||
write_to_stdout_or_system: bool,
|
||||
|
||||
/// If set, configures rotated log files where to write additional logs.
|
||||
write_to_files: Option<TracingFileConfiguration>,
|
||||
|
||||
/// If set, the Sentry configuration to use for error reporting.
|
||||
#[cfg(feature = "sentry")]
|
||||
sentry_config: Option<SentryConfig>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "sentry")]
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct SentryConfig {
|
||||
dsn: String,
|
||||
app_version: String,
|
||||
app_platform: String,
|
||||
}
|
||||
|
||||
impl TracingConfiguration {
|
||||
/// Sets up the tracing configuration and return a [`Logger`] instance
|
||||
/// holding onto it.
|
||||
#[cfg_attr(not(feature = "sentry"), allow(unused_mut))]
|
||||
fn build(mut self) -> LoggingCtx {
|
||||
// Show full backtraces, if we run into panics.
|
||||
//
|
||||
// FIXME: Use safe API for this once stable. Tracking issue:
|
||||
// https://github.com/rust-lang/rust/issues/93346
|
||||
unsafe {
|
||||
std::env::set_var("RUST_BACKTRACE", "1");
|
||||
}
|
||||
|
||||
// Log panics.
|
||||
log_panics::init();
|
||||
|
||||
let env_filter = build_tracing_filter(&self);
|
||||
|
||||
let logging_ctx;
|
||||
#[cfg(feature = "sentry")]
|
||||
{
|
||||
// Prepare the Sentry layer, if a DSN is provided.
|
||||
let (sentry_layer, sentry_logging_ctx) =
|
||||
if let Some(sentry_config) = self.sentry_config.take() {
|
||||
// Initialize the Sentry client with the given options.
|
||||
let sentry_guard = sentry::init((
|
||||
sentry_config.dsn,
|
||||
sentry::ClientOptions {
|
||||
traces_sampler: Some(Arc::new(|ctx| {
|
||||
// Make sure bridge spans are always uploaded
|
||||
if ctx.name() == BRIDGE_SPAN_NAME { 1.0 } else { 0.0 }
|
||||
})),
|
||||
attach_stacktrace: true,
|
||||
release: Some(env!("VERGEN_GIT_SHA").into()),
|
||||
..sentry::ClientOptions::default()
|
||||
},
|
||||
));
|
||||
|
||||
sentry::configure_scope(|scope| {
|
||||
scope.set_tag("app_version", sentry_config.app_version);
|
||||
scope.set_tag("app_platform", sentry_config.app_platform);
|
||||
});
|
||||
|
||||
let sentry_enabled = Arc::new(AtomicBool::new(true));
|
||||
|
||||
// Add a Sentry layer to the tracing subscriber.
|
||||
//
|
||||
// Pass custom event and span filters, which will ignore anything, if the Sentry
|
||||
// support has been globally disabled, or if the statement doesn't include a
|
||||
// `sentry` field set to `true`.
|
||||
let sentry_layer = sentry_tracing::layer()
|
||||
.event_filter({
|
||||
let enabled = sentry_enabled.clone();
|
||||
|
||||
move |metadata| {
|
||||
if enabled.load(std::sync::atomic::Ordering::SeqCst)
|
||||
&& metadata.fields().field("sentry").is_some()
|
||||
{
|
||||
sentry_tracing::default_event_filter(metadata)
|
||||
} else {
|
||||
// Ignore the event.
|
||||
sentry_tracing::EventFilter::Ignore
|
||||
}
|
||||
}
|
||||
})
|
||||
.span_filter({
|
||||
let enabled = sentry_enabled.clone();
|
||||
|
||||
move |metadata| {
|
||||
if enabled.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
matches!(
|
||||
metadata.level(),
|
||||
&Level::ERROR | &Level::WARN | &Level::INFO | &Level::DEBUG
|
||||
)
|
||||
} else {
|
||||
// Ignore, if sentry is globally disabled.
|
||||
false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
(
|
||||
Some(sentry_layer),
|
||||
Some(SentryLoggingCtx { _guard: sentry_guard, enabled: sentry_enabled }),
|
||||
)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
let (text_layers, reload_handle) = crate::platform::text_layers(self);
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::EnvFilter::new(&env_filter))
|
||||
.with(text_layers)
|
||||
.with(sentry_layer)
|
||||
.init();
|
||||
logging_ctx = LoggingCtx { reload_handle, sentry: sentry_logging_ctx };
|
||||
}
|
||||
#[cfg(not(feature = "sentry"))]
|
||||
{
|
||||
let (text_layers, reload_handle) = crate::platform::text_layers(self);
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::EnvFilter::new(&env_filter))
|
||||
.with(text_layers)
|
||||
.init();
|
||||
logging_ctx = LoggingCtx { reload_handle };
|
||||
}
|
||||
|
||||
// Log the log levels 🧠.
|
||||
info!(env_filter, "Logging has been set up");
|
||||
|
||||
logging_ctx
|
||||
}
|
||||
}
|
||||
|
||||
fn build_tracing_filter(config: &TracingConfiguration) -> String {
|
||||
// We are intentionally not setting a global log level because we don't want to
|
||||
// risk third party crates logging sensitive information.
|
||||
// As such we need to make sure that panics will be properly logged.
|
||||
// On 2025-01-08, `log_panics` uses the `panic` target, at the error log level.
|
||||
let mut filters = vec!["panic=error".to_owned()];
|
||||
|
||||
let global_level = config.log_level;
|
||||
|
||||
DEFAULT_TARGET_LOG_LEVELS.iter().for_each(|(target, default_level)| {
|
||||
let level = if IMMUTABLE_LOG_TARGETS.contains(target) {
|
||||
// If the target is immutable, keep the log level.
|
||||
*default_level
|
||||
} else if config.trace_log_packs.iter().any(|pack| pack.targets().contains(target)) {
|
||||
// If a log pack includes that target, set the associated log level to TRACE.
|
||||
LogLevel::Trace
|
||||
} else if *default_level > global_level {
|
||||
// If the default level is more verbose than the global level, keep the default.
|
||||
*default_level
|
||||
} else {
|
||||
// Otherwise, use the global level.
|
||||
global_level
|
||||
};
|
||||
|
||||
filters.push(format!("{}={}", target.as_str(), level.as_str()));
|
||||
});
|
||||
|
||||
// Finally append the extra targets requested by the client.
|
||||
for target in &config.extra_targets {
|
||||
filters.push(format!("{}={}", target, config.log_level.as_str()));
|
||||
}
|
||||
|
||||
filters.join(",")
|
||||
}
|
||||
|
||||
/// Sets up logs and the tokio runtime for the current application.
|
||||
///
|
||||
/// If `use_lightweight_tokio_runtime` is set to true, this will set up a
|
||||
/// lightweight tokio runtime, for processes that have memory limitations (like
|
||||
/// the NSE process on iOS). Otherwise, this can remain false, in which case a
|
||||
/// multithreaded tokio runtime will be set up.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn init_platform(
|
||||
config: TracingConfiguration,
|
||||
use_lightweight_tokio_runtime: bool,
|
||||
) -> Result<(), ClientError> {
|
||||
#[cfg(all(feature = "js", target_family = "wasm"))]
|
||||
{
|
||||
console_error_panic_hook::set_once();
|
||||
}
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
LOGGING.set(config.build()).map_err(|_| ClientError::Generic {
|
||||
msg: "logger already initialized".to_owned(),
|
||||
details: None,
|
||||
})?;
|
||||
|
||||
if use_lightweight_tokio_runtime {
|
||||
setup_lightweight_tokio_runtime();
|
||||
} else {
|
||||
setup_multithreaded_tokio_runtime();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
android_platform::init();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the global enablement level for the Sentry layer (after the logs have
|
||||
/// been set up).
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[cfg(feature = "sentry")]
|
||||
pub fn enable_sentry_logging(enabled: bool) {
|
||||
if let Some(ctx) = LOGGING.get() {
|
||||
if let Some(sentry_ctx) = &ctx.sentry {
|
||||
sentry_ctx.enabled.store(enabled, std::sync::atomic::Ordering::SeqCst);
|
||||
} else {
|
||||
warn!("Sentry logging is not enabled");
|
||||
}
|
||||
} else {
|
||||
// Can't use log statements here, since logging hasn't been enabled yet 🧠
|
||||
eprintln!("Logging hasn't been enabled yet");
|
||||
};
|
||||
}
|
||||
|
||||
/// Updates the tracing subscriber with a new file writer based on the provided
|
||||
/// configuration.
|
||||
///
|
||||
/// This method will throw if `init_platform` hasn't been called, or if it was
|
||||
/// called with `write_to_files` set to `None`.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn reload_tracing_file_writer(
|
||||
configuration: TracingFileConfiguration,
|
||||
) -> Result<(), ClientError> {
|
||||
let Some(logging_context) = LOGGING.get() else {
|
||||
return Err(ClientError::Generic {
|
||||
msg: "Logging hasn't been initialized yet".to_owned(),
|
||||
details: None,
|
||||
});
|
||||
};
|
||||
|
||||
let Some(reload_handle) = logging_context.reload_handle.as_ref() else {
|
||||
return Err(ClientError::Generic {
|
||||
msg: "Logging wasn't initialized with a file config".to_owned(),
|
||||
details: None,
|
||||
});
|
||||
};
|
||||
|
||||
let layer = make_file_layer(configuration);
|
||||
reload_handle.reload(layer).map_err(|error| ClientError::Generic {
|
||||
msg: format!("Failed to reload file config: {error}"),
|
||||
details: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn setup_multithreaded_tokio_runtime() {
|
||||
async_compat::set_runtime_builder(Box::new(|| {
|
||||
eprintln!("spawning a multithreaded tokio runtime");
|
||||
|
||||
let mut builder = tokio::runtime::Builder::new_multi_thread();
|
||||
builder.enable_all();
|
||||
#[cfg(target_os = "android")]
|
||||
builder.on_thread_start(|| {
|
||||
_ = android_platform::android_attach_current_thread_permanently();
|
||||
});
|
||||
builder
|
||||
}));
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn setup_lightweight_tokio_runtime() {
|
||||
async_compat::set_runtime_builder(Box::new(|| {
|
||||
eprintln!("spawning a lightweight tokio runtime");
|
||||
|
||||
// Get the number of available cores through the system, if possible.
|
||||
let num_available_cores =
|
||||
std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
|
||||
|
||||
// The number of worker threads will be either that or 4, whichever is smaller.
|
||||
let num_worker_threads = num_available_cores.min(4);
|
||||
|
||||
// Chosen by a fair dice roll.
|
||||
let num_blocking_threads = 2;
|
||||
|
||||
// 1 MiB of memory per worker thread. Should be enough for everyone™.
|
||||
let max_memory_bytes = 1024 * 1024;
|
||||
|
||||
let mut builder = tokio::runtime::Builder::new_multi_thread();
|
||||
|
||||
builder
|
||||
.enable_all()
|
||||
.worker_threads(num_worker_threads)
|
||||
.thread_stack_size(max_memory_bytes)
|
||||
.max_blocking_threads(num_blocking_threads);
|
||||
|
||||
builder
|
||||
}));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use similar_asserts::assert_eq;
|
||||
|
||||
use super::build_tracing_filter;
|
||||
use crate::platform::TraceLogPacks;
|
||||
|
||||
#[test]
|
||||
fn test_default_tracing_filter() {
|
||||
let config = super::TracingConfiguration {
|
||||
log_level: super::LogLevel::Error,
|
||||
trace_log_packs: Vec::new(),
|
||||
extra_targets: vec!["super_duper_app".to_owned()],
|
||||
write_to_stdout_or_system: true,
|
||||
write_to_files: None,
|
||||
#[cfg(feature = "sentry")]
|
||||
sentry_config: None,
|
||||
};
|
||||
|
||||
let filter = build_tracing_filter(&config);
|
||||
|
||||
assert_eq!(
|
||||
filter,
|
||||
r#"panic=error,
|
||||
hyper=warn,
|
||||
matrix_sdk_ffi=info,
|
||||
matrix_sdk=info,
|
||||
matrix_sdk::client=trace,
|
||||
matrix_sdk_crypto=debug,
|
||||
matrix_sdk_crypto::olm::account=trace,
|
||||
matrix_sdk::oidc=trace,
|
||||
matrix_sdk::http_client=debug,
|
||||
matrix_sdk::sliding_sync=info,
|
||||
matrix_sdk_base::sliding_sync=info,
|
||||
matrix_sdk_ui::timeline=info,
|
||||
matrix_sdk::send_queue=info,
|
||||
matrix_sdk::event_cache=info,
|
||||
matrix_sdk::latest_events=info,
|
||||
matrix_sdk_base::event_cache=info,
|
||||
matrix_sdk_sqlite::event_cache_store=info,
|
||||
matrix_sdk_common::cross_process_lock=warn,
|
||||
matrix_sdk_common::deserialized_responses=warn,
|
||||
matrix_sdk_base::store::ambiguity_map=warn,
|
||||
matrix_sdk_ui::notification_client=info,
|
||||
matrix_sdk_base::response_processors=debug,
|
||||
super_duper_app=error"#
|
||||
.split('\n')
|
||||
.map(|s| s.trim())
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trace_tracing_filter() {
|
||||
let config = super::TracingConfiguration {
|
||||
log_level: super::LogLevel::Trace,
|
||||
trace_log_packs: Vec::new(),
|
||||
extra_targets: vec!["super_duper_app".to_owned(), "some_other_span".to_owned()],
|
||||
write_to_stdout_or_system: true,
|
||||
write_to_files: None,
|
||||
#[cfg(feature = "sentry")]
|
||||
sentry_config: None,
|
||||
};
|
||||
|
||||
let filter = build_tracing_filter(&config);
|
||||
|
||||
assert_eq!(
|
||||
filter,
|
||||
r#"panic=error,
|
||||
hyper=warn,
|
||||
matrix_sdk_ffi=info,
|
||||
matrix_sdk=info,
|
||||
matrix_sdk::client=trace,
|
||||
matrix_sdk_crypto=trace,
|
||||
matrix_sdk_crypto::olm::account=trace,
|
||||
matrix_sdk::oidc=trace,
|
||||
matrix_sdk::http_client=trace,
|
||||
matrix_sdk::sliding_sync=trace,
|
||||
matrix_sdk_base::sliding_sync=trace,
|
||||
matrix_sdk_ui::timeline=trace,
|
||||
matrix_sdk::send_queue=trace,
|
||||
matrix_sdk::event_cache=trace,
|
||||
matrix_sdk::latest_events=trace,
|
||||
matrix_sdk_base::event_cache=trace,
|
||||
matrix_sdk_sqlite::event_cache_store=trace,
|
||||
matrix_sdk_common::cross_process_lock=warn,
|
||||
matrix_sdk_common::deserialized_responses=trace,
|
||||
matrix_sdk_base::store::ambiguity_map=warn,
|
||||
matrix_sdk_ui::notification_client=trace,
|
||||
matrix_sdk_base::response_processors=trace,
|
||||
super_duper_app=trace,
|
||||
some_other_span=trace"#
|
||||
.split('\n')
|
||||
.map(|s| s.trim())
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trace_log_packs() {
|
||||
let config = super::TracingConfiguration {
|
||||
log_level: super::LogLevel::Info,
|
||||
trace_log_packs: vec![TraceLogPacks::EventCache, TraceLogPacks::SendQueue],
|
||||
extra_targets: vec!["super_duper_app".to_owned()],
|
||||
write_to_stdout_or_system: true,
|
||||
write_to_files: None,
|
||||
#[cfg(feature = "sentry")]
|
||||
sentry_config: None,
|
||||
};
|
||||
|
||||
let filter = build_tracing_filter(&config);
|
||||
|
||||
assert_eq!(
|
||||
filter,
|
||||
r#"panic=error,
|
||||
hyper=warn,
|
||||
matrix_sdk_ffi=info,
|
||||
matrix_sdk=info,
|
||||
matrix_sdk::client=trace,
|
||||
matrix_sdk_crypto=debug,
|
||||
matrix_sdk_crypto::olm::account=trace,
|
||||
matrix_sdk::oidc=trace,
|
||||
matrix_sdk::http_client=debug,
|
||||
matrix_sdk::sliding_sync=info,
|
||||
matrix_sdk_base::sliding_sync=info,
|
||||
matrix_sdk_ui::timeline=info,
|
||||
matrix_sdk::send_queue=trace,
|
||||
matrix_sdk::event_cache=trace,
|
||||
matrix_sdk::latest_events=info,
|
||||
matrix_sdk_base::event_cache=trace,
|
||||
matrix_sdk_sqlite::event_cache_store=trace,
|
||||
matrix_sdk_common::cross_process_lock=warn,
|
||||
matrix_sdk_common::deserialized_responses=trace,
|
||||
matrix_sdk_base::store::ambiguity_map=warn,
|
||||
matrix_sdk_ui::notification_client=info,
|
||||
matrix_sdk_base::response_processors=debug,
|
||||
super_duper_app=info"#
|
||||
.split('\n')
|
||||
.map(|s| s.trim())
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,914 +0,0 @@
|
||||
// Copyright 2026 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
fs::{self, File, OpenOptions},
|
||||
io::{self, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::Mutex,
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use tracing_appender::rolling::Rotation;
|
||||
|
||||
/// Custom rolling file appender that supports time-based rotation and
|
||||
/// size-based cleanup.
|
||||
///
|
||||
/// This writer automatically manages log files with the following behavior:
|
||||
///
|
||||
/// # File Naming
|
||||
///
|
||||
/// Log files are named using the pattern: `{prefix}.{timestamp}.{suffix}`
|
||||
/// where the timestamp format depends on the rotation period:
|
||||
/// - `MINUTELY`: `YYYY-MM-DD-HH-MM`
|
||||
/// - `HOURLY`: `YYYY-MM-DD-HH`
|
||||
/// - `DAILY`: `YYYY-MM-DD`
|
||||
/// - `WEEKLY` or `NEVER`: `YYYY-Www` (ISO week number, e.g., `2024-W03`)
|
||||
///
|
||||
/// # Automatic Rotation
|
||||
///
|
||||
/// Files are rotated (a new file is created) when the configured time period
|
||||
/// changes. For example, with hourly rotation, a new file is created when the
|
||||
/// hour changes. Rotation is checked:
|
||||
/// - During writer initialization (creates/opens file for current period)
|
||||
/// - Before each write operation (only rotates if time period has changed)
|
||||
///
|
||||
/// If a log file already exists for the current time period, it will be
|
||||
/// reopened and appended to rather than creating a new file.
|
||||
///
|
||||
/// # Automatic Cleanup
|
||||
///
|
||||
/// The writer performs cleanup operations during initialization and rotation:
|
||||
/// - **Size limit enforcement**: When total size of all log files exceeds
|
||||
/// `max_total_size_bytes`, the oldest files are removed until under the limit
|
||||
/// - **Age-based cleanup**: Files older than `max_age_seconds` (based on
|
||||
/// filesystem modification time) are automatically removed
|
||||
/// - **File filtering**: Only files matching both the configured prefix and
|
||||
/// suffix are managed; other files in the directory are left untouched
|
||||
///
|
||||
/// # Side Effects on Creation
|
||||
///
|
||||
/// When `new()` is called, the following side effects occur:
|
||||
/// 1. Creates the log directory if it doesn't exist (including parent
|
||||
/// directories)
|
||||
/// 2. Creates or opens a log file for the current time period (appends if
|
||||
/// exists)
|
||||
/// 3. Performs cleanup of old files based on age (by filesystem mtime)
|
||||
/// 4. Enforces the total size limit by removing oldest files if needed
|
||||
///
|
||||
/// # Thread Safety
|
||||
///
|
||||
/// This writer is safe to use from multiple threads. Internal state is
|
||||
/// protected by a mutex, ensuring that file operations and rotations are
|
||||
/// properly synchronized.
|
||||
pub(super) struct SizeAndDateRollingWriter {
|
||||
config: WriterConfig,
|
||||
state: Mutex<Option<WriterState>>,
|
||||
}
|
||||
|
||||
/// Immutable configuration for the writer - shared without locks.
|
||||
///
|
||||
/// This struct contains all configuration parameters that remain constant
|
||||
/// throughout the writer's lifetime. Since these values never change, they
|
||||
/// can be safely shared across threads without synchronization.
|
||||
struct WriterConfig {
|
||||
/// Directory where log files are created
|
||||
base_path: PathBuf,
|
||||
/// Prefix for log file names (e.g., "app" results in "app.2024-01-15.log")
|
||||
file_prefix: String,
|
||||
/// Suffix for log file names (typically ".log")
|
||||
file_suffix: String,
|
||||
/// Time period for automatic rotation (MINUTELY, HOURLY, DAILY, WEEKLY, or
|
||||
/// NEVER which is treated as WEEKLY)
|
||||
rotation: Rotation,
|
||||
/// Maximum total size in bytes of all log files before cleanup
|
||||
max_total_size_bytes: u64,
|
||||
/// Maximum age in seconds before a log file is removed during cleanup
|
||||
max_age_seconds: u64,
|
||||
}
|
||||
|
||||
/// Mutable state that requires synchronization.
|
||||
///
|
||||
/// This struct contains the current log file handle and its path. These values
|
||||
/// change when rotation occurs, so access must be protected by a mutex to
|
||||
/// ensure thread safety.
|
||||
///
|
||||
/// The state is wrapped in `Option` because it needs to be temporarily taken
|
||||
/// during rotation operations to allow mutation while holding the lock.
|
||||
struct WriterState {
|
||||
/// The currently open log file handle for writing
|
||||
current_file: File,
|
||||
/// Path to the current log file (used to identify it during cleanup)
|
||||
current_path: PathBuf,
|
||||
}
|
||||
|
||||
impl SizeAndDateRollingWriter {
|
||||
/// Creates a new rolling writer with the specified configuration.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Directory where log files will be created. Will be created if
|
||||
/// it doesn't exist.
|
||||
/// * `file_prefix` - Prefix for log file names (e.g., "app")
|
||||
/// * `file_suffix` - Suffix for log file names (e.g., ".log")
|
||||
/// * `rotation` - Time period for rotation (MINUTELY, HOURLY, DAILY,
|
||||
/// WEEKLY, or NEVER which is treated as WEEKLY)
|
||||
/// * `max_total_size_bytes` - Maximum total size of all log files. When
|
||||
/// exceeded, oldest files are removed.
|
||||
/// * `max_age_seconds` - Maximum age of log files in seconds. Files older
|
||||
/// than this (by filesystem mtime) are removed during cleanup.
|
||||
///
|
||||
/// # Side Effects
|
||||
///
|
||||
/// This method performs several file system operations in order:
|
||||
/// 1. Creates the directory at `path` if it doesn't exist
|
||||
/// 2. Creates or reopens a log file for the current time period (appends if
|
||||
/// exists)
|
||||
/// 3. Scans the directory for existing log files matching the prefix/suffix
|
||||
/// 4. Removes files older than `max_age_seconds` (by filesystem mtime)
|
||||
/// 5. Removes oldest files if total size exceeds `max_total_size_bytes`
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if:
|
||||
/// - The directory cannot be created
|
||||
/// - The directory cannot be read
|
||||
/// - The log file cannot be created or opened
|
||||
/// - File metadata cannot be read during cleanup
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```ignore
|
||||
/// let writer = SizeAndDateRollingWriter::new(
|
||||
/// "/var/log/myapp",
|
||||
/// "app".to_owned(),
|
||||
/// ".log".to_owned(),
|
||||
/// Rotation::HOURLY,
|
||||
/// 100 * 1024 * 1024, // 100 MB
|
||||
/// 7 * 24 * 60 * 60, // 7 days
|
||||
/// )?;
|
||||
/// ```
|
||||
pub(super) fn new(
|
||||
path: impl AsRef<Path>,
|
||||
file_prefix: String,
|
||||
file_suffix: String,
|
||||
rotation: Rotation,
|
||||
max_total_size_bytes: u64,
|
||||
max_age_seconds: u64,
|
||||
) -> io::Result<Self> {
|
||||
let base_path = path.as_ref().to_path_buf();
|
||||
fs::create_dir_all(&base_path)?;
|
||||
|
||||
let config = WriterConfig {
|
||||
base_path,
|
||||
file_prefix,
|
||||
file_suffix,
|
||||
rotation,
|
||||
max_total_size_bytes,
|
||||
max_age_seconds,
|
||||
};
|
||||
|
||||
// Create initial state with first rotation
|
||||
let mut state = None;
|
||||
Self::rotate_internal(&config, &mut state, false)?;
|
||||
|
||||
Ok(Self { config, state: Mutex::new(state) })
|
||||
}
|
||||
|
||||
/// Extract the timestamp from the current filename.
|
||||
fn extract_timestamp_from_path(config: &WriterConfig, current_path: &Path) -> Option<String> {
|
||||
let filename = current_path.file_name()?.to_str()?;
|
||||
|
||||
// Strip prefix and suffix to get the timestamp
|
||||
// Format: "prefix.timestamp.suffix"
|
||||
let without_prefix = filename.strip_prefix(&format!("{}.", config.file_prefix))?;
|
||||
let timestamp = without_prefix.strip_suffix(&config.file_suffix)?;
|
||||
|
||||
Some(timestamp.to_owned())
|
||||
}
|
||||
|
||||
/// Check if rotation is needed based on time period change.
|
||||
fn should_rotate_by_time(config: &WriterConfig, current_path: &Path) -> bool {
|
||||
let current_time = Self::format_rotation_timestamp(config);
|
||||
let last_rotation_time = Self::extract_timestamp_from_path(config, current_path);
|
||||
|
||||
// If we can't extract the timestamp, assume rotation is needed
|
||||
match last_rotation_time {
|
||||
Some(last_time) => current_time != last_time,
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the current time as a timestamp string for the rotation period.
|
||||
///
|
||||
/// Returns a timestamp string based on the configured rotation period
|
||||
/// (e.g., "2024-01-15-14" for hourly rotation).
|
||||
fn format_rotation_timestamp(config: &WriterConfig) -> String {
|
||||
let now = chrono::Local::now();
|
||||
match config.rotation {
|
||||
Rotation::MINUTELY => now.format("%Y-%m-%d-%H-%M").to_string(),
|
||||
Rotation::HOURLY => now.format("%Y-%m-%d-%H").to_string(),
|
||||
Rotation::DAILY => now.format("%Y-%m-%d").to_string(),
|
||||
Rotation::WEEKLY | Rotation::NEVER => now.format("%Y-W%W").to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Rotate the log file, creating a new file with a timestamp-based name.
|
||||
///
|
||||
/// If `check_conditions` is true, rotation only happens if time or size
|
||||
/// thresholds are met. Otherwise, rotation is forced.
|
||||
///
|
||||
/// This method also handles initial state creation when called with None
|
||||
/// state.
|
||||
fn rotate_internal(
|
||||
config: &WriterConfig,
|
||||
state: &mut Option<WriterState>,
|
||||
check_conditions: bool,
|
||||
) -> io::Result<()> {
|
||||
// Check if rotation is needed (skip for uninitialized state)
|
||||
if check_conditions
|
||||
&& let Some(state) = state.as_ref()
|
||||
&& !Self::should_rotate_by_time(config, &state.current_path)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let time_str = Self::format_rotation_timestamp(config);
|
||||
|
||||
// Generate filename with timestamp
|
||||
let filename = format!("{}.{}{}", config.file_prefix, time_str, config.file_suffix);
|
||||
let new_path = config.base_path.join(filename);
|
||||
|
||||
// Open or create file in append mode
|
||||
let new_file = OpenOptions::new().create(true).append(true).open(&new_path)?;
|
||||
|
||||
let new_state = WriterState { current_file: new_file, current_path: new_path };
|
||||
|
||||
// Clean up logs older than configured max age
|
||||
Self::trim_old_logs_internal(config, &new_state)?;
|
||||
|
||||
// Enforce total size limit by removing oldest files if needed
|
||||
Self::enforce_total_size_limit_internal(config, &new_state)?;
|
||||
|
||||
*state = Some(new_state);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all log files matching our prefix and suffix, sorted by modification
|
||||
/// time.
|
||||
///
|
||||
/// Returns a vector of (path, modification_time) tuples, oldest first.
|
||||
fn get_matching_log_files(config: &WriterConfig) -> io::Result<Vec<(PathBuf, SystemTime)>> {
|
||||
let mut files: Vec<_> = fs::read_dir(&config.base_path)?
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
let filename = path.file_name()?.to_str()?;
|
||||
// Only process files matching our prefix and suffix
|
||||
if filename.starts_with(&config.file_prefix)
|
||||
&& filename.ends_with(&config.file_suffix)
|
||||
{
|
||||
let metadata = fs::metadata(&path).ok()?;
|
||||
let modified = metadata.modified().ok()?;
|
||||
return Some((path, modified));
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Sort by modification time (oldest first)
|
||||
files.sort_by_key(|(_, modified)| *modified);
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
/// Remove all log files older than the configured max age.
|
||||
///
|
||||
/// Only files matching the configured prefix and suffix are removed.
|
||||
/// Other files in the directory are left alone.
|
||||
fn trim_old_logs_internal(config: &WriterConfig, state: &WriterState) -> io::Result<()> {
|
||||
let now = SystemTime::now();
|
||||
let files = Self::get_matching_log_files(config)?;
|
||||
|
||||
for (path, modified) in files {
|
||||
// Skip the current file
|
||||
if path == state.current_path {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if file is older than max age
|
||||
if let Ok(duration) = now.duration_since(modified)
|
||||
&& duration.as_secs() > config.max_age_seconds
|
||||
{
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enforce total size limit across all log files.
|
||||
///
|
||||
/// If the total size of all matching log files exceeds
|
||||
/// max_total_size_bytes, remove the oldest files until the total is
|
||||
/// below the limit.
|
||||
fn enforce_total_size_limit_internal(
|
||||
config: &WriterConfig,
|
||||
state: &WriterState,
|
||||
) -> io::Result<()> {
|
||||
let max_total = config.max_total_size_bytes;
|
||||
|
||||
let files = Self::get_matching_log_files(config)?;
|
||||
|
||||
// Calculate total size of all log files
|
||||
let mut total_size: u64 = 0;
|
||||
for (path, _) in &files {
|
||||
if let Ok(metadata) = fs::metadata(path) {
|
||||
total_size += metadata.len();
|
||||
}
|
||||
}
|
||||
|
||||
// If under limit, nothing to do
|
||||
if total_size <= max_total {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Remove oldest files until we're under the limit
|
||||
// Files are already sorted by modification time (oldest first)
|
||||
for (path, _) in files {
|
||||
// Don't remove the current file
|
||||
if path == state.current_path {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok(metadata) = fs::metadata(&path) {
|
||||
let file_size = metadata.len();
|
||||
let _ = fs::remove_file(&path);
|
||||
total_size = total_size.saturating_sub(file_size);
|
||||
|
||||
// Check if we're now under the limit
|
||||
if total_size <= max_total {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for SizeAndDateRollingWriter {
|
||||
type Writer = SizeAndDateRollingWriterHandle<'a>;
|
||||
|
||||
fn make_writer(&'a self) -> Self::Writer {
|
||||
SizeAndDateRollingWriterHandle {
|
||||
config: &self.config,
|
||||
state: &self.state,
|
||||
_phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct SizeAndDateRollingWriterHandle<'a> {
|
||||
config: &'a WriterConfig,
|
||||
state: &'a Mutex<Option<WriterState>>,
|
||||
_phantom: std::marker::PhantomData<&'a ()>,
|
||||
}
|
||||
|
||||
impl<'a> Write for SizeAndDateRollingWriterHandle<'a> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
|
||||
// Check if rotation is needed
|
||||
SizeAndDateRollingWriter::rotate_internal(self.config, &mut state, true)?;
|
||||
|
||||
// Write to file (state must be initialized after rotation)
|
||||
state.as_mut().unwrap().current_file.write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
if let Some(s) = state.as_mut() { s.current_file.flush() } else { Ok(()) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl SizeAndDateRollingWriter {
|
||||
/// Manually trigger log rotation if conditions are met.
|
||||
///
|
||||
/// This will rotate the current log file if the time period has changed.
|
||||
fn roll(&self) -> io::Result<()> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
Self::rotate_internal(&self.config, &mut state, true)
|
||||
}
|
||||
|
||||
/// Manually trigger cleanup of old log files.
|
||||
///
|
||||
/// This removes all log files older than the configured max age, keeping
|
||||
/// only files that match the configured prefix and suffix.
|
||||
fn trim(&self) -> io::Result<()> {
|
||||
let state = self.state.lock().unwrap();
|
||||
if let Some(ref state) = *state {
|
||||
Self::trim_old_logs_internal(&self.config, state)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Write;
|
||||
|
||||
use tempfile::tempdir;
|
||||
use tracing_subscriber::fmt::MakeWriter;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_rotation_file_naming() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
let _writer = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"app".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::HOURLY,
|
||||
10 * 1024 * 1024, // 10MB total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Check file naming pattern
|
||||
let log_files: Vec<_> = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
let filename = path.file_name()?.to_str()?.to_owned();
|
||||
if filename.starts_with("app") && filename.ends_with(".log") {
|
||||
Some(filename)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Should have one file with proper naming
|
||||
assert_eq!(log_files.len(), 1, "Expected exactly one log file");
|
||||
|
||||
// Check that files follow the expected naming pattern
|
||||
for filename in &log_files {
|
||||
assert!(
|
||||
filename.starts_with("app."),
|
||||
"Filename should start with prefix: {}",
|
||||
filename
|
||||
);
|
||||
assert!(filename.ends_with(".log"), "Filename should end with suffix: {}", filename);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prefix_suffix_filtering() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
// Create some unrelated files
|
||||
std::fs::write(log_path.join("other.txt"), "unrelated").unwrap();
|
||||
std::fs::write(log_path.join("app.txt"), "wrong suffix").unwrap();
|
||||
std::fs::write(log_path.join("test.log"), "wrong prefix").unwrap();
|
||||
|
||||
let writer = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"app".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::HOURLY,
|
||||
10 * 1024 * 1024, // 10MB total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut handle = writer.make_writer();
|
||||
handle.write_all(b"test message\n").unwrap();
|
||||
handle.flush().unwrap();
|
||||
|
||||
// Trigger manual trim
|
||||
writer.trim().unwrap();
|
||||
|
||||
// Check that unrelated files still exist
|
||||
assert!(log_path.join("other.txt").exists(), "Unrelated file should not be removed");
|
||||
assert!(log_path.join("app.txt").exists(), "File with wrong suffix should not be removed");
|
||||
assert!(log_path.join("test.log").exists(), "File with wrong prefix should not be removed");
|
||||
|
||||
// App log file should still exist
|
||||
let app_logs: Vec<_> = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let filename = entry.file_name().to_str()?.to_owned();
|
||||
if filename.starts_with("app.") && filename.ends_with(".log") {
|
||||
Some(filename)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(app_logs.len(), 1, "Expected one app log file");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manual_roll_and_trim() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
let writer = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"manual".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::DAILY,
|
||||
10 * 1024 * 1024, // 10MB total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Manual roll should work
|
||||
assert!(writer.roll().is_ok(), "Manual roll should succeed");
|
||||
|
||||
// Manual trim should work
|
||||
assert!(writer.trim().is_ok(), "Manual trim should succeed");
|
||||
|
||||
// Should still have log file
|
||||
let log_files: Vec<_> = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.file_name()?.to_str()?.starts_with("manual") {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
assert!(!log_files.is_empty(), "Should have at least one log file");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_total_size_limit_removes_oldest_files() {
|
||||
// This test verifies that when the total size of all log files exceeds
|
||||
// max_total_size_bytes, the oldest files are removed
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
// Manually create several log files with different timestamps (alphabetically
|
||||
// sorted = oldest first) Total of 240 bytes
|
||||
std::fs::write(log_path.join("total.2024-01-01-10-00.log"), "x".repeat(80)).unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
std::fs::write(log_path.join("total.2024-01-01-10-01.log"), "y".repeat(80)).unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
std::fs::write(log_path.join("total.2024-01-01-10-02.log"), "z".repeat(80)).unwrap();
|
||||
|
||||
let count_files = || {
|
||||
std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.file_name()?.to_str()?.starts_with("total") {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.count()
|
||||
};
|
||||
|
||||
assert_eq!(count_files(), 3, "Should have 3 log files");
|
||||
|
||||
// Now create a new writer with 200 byte total limit
|
||||
// Current total is 240 bytes. The writer will:
|
||||
// 1. Create a new file with current timestamp
|
||||
// 2. See total exceeds 200 bytes
|
||||
// 3. Remove oldest file(s) until under limit
|
||||
let _writer = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"total".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::MINUTELY,
|
||||
200, // 200 byte total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// After writer creation, we should still have 3 files:
|
||||
// - Oldest file (10-00) was removed
|
||||
// - Two middle files (10-01, 10-02) remain
|
||||
// - New current file was created
|
||||
let remaining_files = count_files();
|
||||
assert_eq!(
|
||||
remaining_files, 3,
|
||||
"Should have 3 files: 2 old files + 1 new current file, but have {}",
|
||||
remaining_files
|
||||
);
|
||||
|
||||
// Calculate total size of remaining files
|
||||
let total_size: u64 = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.file_name()?.to_str()?.starts_with("total") {
|
||||
Some(std::fs::metadata(&path).ok()?.len())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sum();
|
||||
|
||||
// Total should be 160 bytes (80 + 80 + 0 for new file)
|
||||
assert!(total_size <= 200, "Total size should be under 200 bytes, but is {}", total_size);
|
||||
|
||||
// Verify the oldest file was removed by checking filenames
|
||||
let filenames: Vec<String> = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() { path.file_name()?.to_str().map(|s| s.to_owned()) } else { None }
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
!filenames.contains(&"total.2024-01-01-10-00.log".to_owned()),
|
||||
"Oldest file should have been removed"
|
||||
);
|
||||
assert!(
|
||||
filenames.iter().any(|f| f.starts_with("total.2024-01-01-10-01")),
|
||||
"Second file should still exist"
|
||||
);
|
||||
assert!(
|
||||
filenames.iter().any(|f| f.starts_with("total.2024-01-01-10-02")),
|
||||
"Third file should still exist"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_based_rotation_logic() {
|
||||
// Test that the rotation logic correctly identifies when rotation is needed
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
let writer = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"time".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::DAILY,
|
||||
10 * 1024 * 1024, // 10MB total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Write some data
|
||||
let mut handle = writer.make_writer();
|
||||
handle.write_all(b"initial log entry\n").unwrap();
|
||||
handle.flush().unwrap();
|
||||
|
||||
// Verify initial state - should have created one file
|
||||
let initial_files: Vec<_> = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.file_name()?.to_str()?.starts_with("time") {
|
||||
Some(path.file_name()?.to_str()?.to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(initial_files.len(), 1, "Should have one log file initially");
|
||||
|
||||
// Verify the file contains our data
|
||||
let file_content = std::fs::read_to_string(log_path.join(&initial_files[0])).unwrap();
|
||||
assert!(file_content.contains("initial log entry"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_old_files_cleanup() {
|
||||
// Test that files older than one week are removed
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
// Create files with timestamps more than a week old
|
||||
// We can't easily manipulate file mtimes without external crates,
|
||||
// but we can verify the cleanup logic doesn't fail
|
||||
std::fs::write(log_path.join("old.2020-01-01-10-00.log"), "old data").unwrap();
|
||||
|
||||
// Create a writer which will trigger cleanup
|
||||
let writer = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"old".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::HOURLY,
|
||||
10 * 1024 * 1024, // 10MB total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// The old file should still exist because we can't manipulate mtime easily
|
||||
// But we verify that cleanup doesn't crash
|
||||
writer.trim().unwrap();
|
||||
|
||||
// Verify we can still write
|
||||
let mut handle = writer.make_writer();
|
||||
handle.write_all(b"new data").unwrap();
|
||||
handle.flush().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_operations() {
|
||||
// Test that writing works correctly across multiple writes
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
let writer = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"write".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::HOURLY,
|
||||
10 * 1024 * 1024, // 10MB total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Write multiple times
|
||||
for i in 0..5 {
|
||||
let mut handle = writer.make_writer();
|
||||
let message = format!("Log entry {}\n", i);
|
||||
handle.write_all(message.as_bytes()).unwrap();
|
||||
handle.flush().unwrap();
|
||||
}
|
||||
|
||||
// Verify all writes went to the same file (same hour)
|
||||
let log_files: Vec<_> = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.file_name()?.to_str()?.starts_with("write") {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(log_files.len(), 1, "Should have one log file for same time period");
|
||||
|
||||
// Verify content
|
||||
let content = std::fs::read_to_string(&log_files[0]).unwrap();
|
||||
for i in 0..5 {
|
||||
assert!(content.contains(&format!("Log entry {}", i)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reopening_existing_file() {
|
||||
// Test that reopening appends to existing file within same time period
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
// Create first writer and write
|
||||
let writer1 = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"reopen".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::DAILY,
|
||||
10 * 1024 * 1024, // 10MB total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut handle1 = writer1.make_writer();
|
||||
handle1.write_all(b"first write\n").unwrap();
|
||||
handle1.flush().unwrap();
|
||||
drop(writer1);
|
||||
|
||||
// Create second writer (simulating restart within same day)
|
||||
let writer2 = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"reopen".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::DAILY,
|
||||
10 * 1024 * 1024, // 10MB total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut handle2 = writer2.make_writer();
|
||||
handle2.write_all(b"second write\n").unwrap();
|
||||
handle2.flush().unwrap();
|
||||
|
||||
// Should still have only one file
|
||||
let log_files: Vec<_> = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.file_name()?.to_str()?.starts_with("reopen") {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(log_files.len(), 1, "Should have one log file");
|
||||
|
||||
// Verify both writes are in the file
|
||||
let content = std::fs::read_to_string(&log_files[0]).unwrap();
|
||||
assert!(content.contains("first write"));
|
||||
assert!(content.contains("second write"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_total_size_limit_with_multiple_old_files() {
|
||||
// Test that multiple old files are removed until under limit
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
// Create 5 files, each 50 bytes (total 250 bytes)
|
||||
for i in 0..5 {
|
||||
let filename = format!("multi.2024-01-01-10-0{}.log", i);
|
||||
std::fs::write(log_path.join(filename), "x".repeat(50)).unwrap();
|
||||
}
|
||||
|
||||
let count_files = || {
|
||||
std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter(|entry| {
|
||||
if let Ok(entry) = entry
|
||||
&& let Some(name) = entry.file_name().to_str()
|
||||
{
|
||||
return name.starts_with("multi");
|
||||
}
|
||||
false
|
||||
})
|
||||
.count()
|
||||
};
|
||||
|
||||
assert_eq!(count_files(), 5, "Should start with 5 files");
|
||||
|
||||
// Create writer with 100 byte limit (should keep only 2 old files + new
|
||||
// current)
|
||||
let _writer = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"multi".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::MINUTELY,
|
||||
100, // 100 byte total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Should have removed oldest files
|
||||
let remaining = count_files();
|
||||
assert!(remaining <= 3, "Should have removed multiple old files to stay under limit");
|
||||
|
||||
// Verify total size is under limit
|
||||
let total_size: u64 = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.file_name()?.to_str()?.starts_with("multi") {
|
||||
Some(std::fs::metadata(&path).ok()?.len())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sum();
|
||||
|
||||
assert!(total_size <= 100, "Total size should be under 100 bytes, but is {}", total_size);
|
||||
}
|
||||
}
|
||||
@@ -1,723 +0,0 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk::authentication::oauth::{
|
||||
OAuth,
|
||||
qrcode::{
|
||||
self, CheckCodeSender as SdkCheckCodeSender, CheckCodeSenderError,
|
||||
DeviceCodeErrorResponseType, GeneratedQrProgress, LoginFailureReason, QrProgress,
|
||||
},
|
||||
};
|
||||
use matrix_sdk_base::crypto::types::qr_login::{self, QrCodeIntent};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm, stream::StreamExt};
|
||||
|
||||
use crate::{
|
||||
authentication::OidcConfiguration, runtime::get_runtime_handle, task_handle::TaskHandle,
|
||||
};
|
||||
|
||||
/// Handler for logging in with a QR code.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct LoginWithQrCodeHandler {
|
||||
oauth: OAuth,
|
||||
oidc_configuration: OidcConfiguration,
|
||||
}
|
||||
|
||||
impl LoginWithQrCodeHandler {
|
||||
pub(crate) fn new(oauth: OAuth, oidc_configuration: OidcConfiguration) -> Self {
|
||||
Self { oauth, oidc_configuration }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl LoginWithQrCodeHandler {
|
||||
/// This method allows you to log in with a scanned QR code.
|
||||
///
|
||||
/// The existing device needs to display the QR code which this device can
|
||||
/// scan, call this method and handle its progress updates to log in.
|
||||
///
|
||||
/// For the login to succeed, the [`Client`] associated with the
|
||||
/// [`LoginWithQrCodeHandler`] must have been built with
|
||||
/// [`QrCodeData::server_name`] as the server name.
|
||||
///
|
||||
/// This method uses the login mechanism described in [MSC4108]. As such,
|
||||
/// it requires OAuth 2.0 support.
|
||||
///
|
||||
/// For the reverse flow where this device generates the QR code for the
|
||||
/// existing device to scan, use [`LoginWithQrCodeHandler::generate`].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `qr_code_data` - The [`QrCodeData`] scanned from the QR code.
|
||||
/// * `progress_listener` - A progress listener that must also be used to
|
||||
/// transfer the [`CheckCode`] to the existing device.
|
||||
///
|
||||
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
|
||||
pub async fn scan(
|
||||
self: Arc<Self>,
|
||||
qr_code_data: &QrCodeData,
|
||||
progress_listener: Box<dyn QrLoginProgressListener>,
|
||||
) -> Result<(), HumanQrLoginError> {
|
||||
let registration_data = self
|
||||
.oidc_configuration
|
||||
.registration_data()
|
||||
.map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
|
||||
|
||||
let login =
|
||||
self.oauth.login_with_qr_code(Some(®istration_data)).scan(&qr_code_data.inner);
|
||||
|
||||
let mut progress = login.subscribe_to_progress();
|
||||
|
||||
// We create this task, which will get cancelled once it's dropped, just in case
|
||||
// the progress stream doesn't end.
|
||||
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(state) = progress.next().await {
|
||||
progress_listener.on_update(state.into());
|
||||
}
|
||||
}));
|
||||
|
||||
login.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This method allows you to log in by generating a QR code.
|
||||
///
|
||||
/// This device needs to call this method and handle its progress updates to
|
||||
/// generate a QR code which the existing device can scan and grant the
|
||||
/// log in.
|
||||
///
|
||||
/// This method uses the login mechanism described in [MSC4108]. As such,
|
||||
/// it requires OAuth 2.0 support.
|
||||
///
|
||||
/// For the reverse flow where the existing device generates the QR code
|
||||
/// for this device to scan, use [`LoginWithQrCodeHandler::scan`].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `progress_listener` - A progress listener that must also be used to
|
||||
/// obtain the [`QrCodeData`] and collect the [`CheckCode`] from the user.
|
||||
///
|
||||
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
|
||||
pub async fn generate(
|
||||
self: Arc<Self>,
|
||||
progress_listener: Box<dyn GeneratedQrLoginProgressListener>,
|
||||
) -> Result<(), HumanQrLoginError> {
|
||||
let registration_data = self
|
||||
.oidc_configuration
|
||||
.registration_data()
|
||||
.map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
|
||||
|
||||
let login = self.oauth.login_with_qr_code(Some(®istration_data)).generate();
|
||||
|
||||
let mut progress = login.subscribe_to_progress();
|
||||
|
||||
// We create this task, which will get cancelled once it's dropped, just in case
|
||||
// the progress stream doesn't end.
|
||||
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(state) = progress.next().await {
|
||||
progress_listener.on_update(state.into());
|
||||
}
|
||||
}));
|
||||
|
||||
login.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for granting login in with a QR code.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct GrantLoginWithQrCodeHandler {
|
||||
oauth: OAuth,
|
||||
}
|
||||
|
||||
impl GrantLoginWithQrCodeHandler {
|
||||
pub(crate) fn new(oauth: OAuth) -> Self {
|
||||
Self { oauth }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl GrantLoginWithQrCodeHandler {
|
||||
/// This method allows you to grant login with a scanned QR code.
|
||||
///
|
||||
/// The new device needs to display the QR code which this device can
|
||||
/// scan, call this method and handle its progress updates to grant the
|
||||
/// login.
|
||||
///
|
||||
/// This method uses the login mechanism described in [MSC4108]. As such,
|
||||
/// it requires OAuth 2.0 support.
|
||||
///
|
||||
/// For the reverse flow where this device generates the QR code for the
|
||||
/// existing device to scan, use [`GrantLoginWithQrCodeHandler::generate`].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `qr_code_data` - The [`QrCodeData`] scanned from the QR code.
|
||||
/// * `progress_listener` - A progress listener that must also be used to
|
||||
/// transfer the [`CheckCode`] to the new device.
|
||||
///
|
||||
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
|
||||
pub async fn scan(
|
||||
self: Arc<Self>,
|
||||
qr_code_data: &QrCodeData,
|
||||
progress_listener: Box<dyn GrantQrLoginProgressListener>,
|
||||
) -> Result<(), HumanQrGrantLoginError> {
|
||||
let grant = self.oauth.grant_login_with_qr_code().scan(&qr_code_data.inner);
|
||||
|
||||
let mut progress = grant.subscribe_to_progress();
|
||||
|
||||
// We create this task, which will get cancelled once it's dropped, just in case
|
||||
// the progress stream doesn't end.
|
||||
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(state) = progress.next().await {
|
||||
progress_listener.on_update(state.into());
|
||||
}
|
||||
}));
|
||||
|
||||
grant.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This method allows you to grant login by generating a QR code.
|
||||
///
|
||||
/// This device needs to call this method and handle its progress updates to
|
||||
/// generate a QR code which the new device can scan to log in.
|
||||
///
|
||||
/// This method uses the login mechanism described in [MSC4108]. As such,
|
||||
/// it requires OAuth 2.0 support.
|
||||
///
|
||||
/// For the reverse flow where the existing device generates the QR code
|
||||
/// for this device to scan, use [`GrantLoginWithQrCodeHandler::scan`].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `progress_listener` - A progress listener that must also be used to
|
||||
/// obtain the [`QrCodeData`] and collect the [`CheckCode`] from the user.
|
||||
///
|
||||
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
|
||||
pub async fn generate(
|
||||
self: Arc<Self>,
|
||||
progress_listener: Box<dyn GrantGeneratedQrLoginProgressListener>,
|
||||
) -> Result<(), HumanQrGrantLoginError> {
|
||||
let grant = self.oauth.grant_login_with_qr_code().generate();
|
||||
|
||||
let mut progress = grant.subscribe_to_progress();
|
||||
|
||||
// We create this task, which will get cancelled once it's dropped, just in case
|
||||
// the progress stream doesn't end.
|
||||
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(state) = progress.next().await {
|
||||
progress_listener.on_update(state.into());
|
||||
}
|
||||
}));
|
||||
|
||||
grant.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Data for the QR code login mechanism.
|
||||
///
|
||||
/// The [`QrCodeData`] can be serialized and encoded as a QR code or it can be
|
||||
/// decoded from a QR code.
|
||||
#[derive(Debug, uniffi::Object)]
|
||||
pub struct QrCodeData {
|
||||
pub(crate) inner: qrcode::QrCodeData,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl QrCodeData {
|
||||
/// Attempt to decode a slice of bytes into a [`QrCodeData`] object.
|
||||
///
|
||||
/// The slice of bytes would generally be returned by a QR code decoder.
|
||||
#[uniffi::constructor]
|
||||
pub fn from_bytes(bytes: Vec<u8>) -> Result<Arc<Self>, QrCodeDecodeError> {
|
||||
Ok(Self { inner: qrcode::QrCodeData::from_bytes(&bytes)? }.into())
|
||||
}
|
||||
|
||||
/// Serialize the [`QrCodeData`] into a byte vector for encoding as a QR
|
||||
/// code.
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
self.inner.to_bytes()
|
||||
}
|
||||
|
||||
/// The server name contained within the scanned QR code data.
|
||||
///
|
||||
/// Note: This value is only present when scanning a QR code that belongs to
|
||||
/// a logged in client. The mode where the new client shows the QR code
|
||||
/// will return `None`.
|
||||
pub fn server_name(&self) -> Option<String> {
|
||||
match &self.inner.intent_data() {
|
||||
qr_login::QrCodeIntentData::Msc4108 { data, .. } => match data {
|
||||
qrcode::Msc4108IntentData::Login => None,
|
||||
qrcode::Msc4108IntentData::Reciprocate { server_name } => {
|
||||
Some(server_name.to_owned())
|
||||
}
|
||||
},
|
||||
qr_login::QrCodeIntentData::Msc4388 { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The base URL of the homeserver contained within the scanned QR code
|
||||
/// data.
|
||||
///
|
||||
/// Note: This value is only present when scanning a QR code conforming to
|
||||
/// MSC4388.
|
||||
pub fn base_url(&self) -> Option<String> {
|
||||
match self.inner.intent_data() {
|
||||
qrcode::QrCodeIntentData::Msc4108 { .. } => None,
|
||||
qrcode::QrCodeIntentData::Msc4388 { base_url, .. } => Some(base_url.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the [`QrCodeIntent`] of this [`QrCodeData`] object.
|
||||
///
|
||||
/// This tells us if the creator of the QR code wants to log in or if they
|
||||
/// want to log another device in.
|
||||
pub fn intent(&self) -> QrCodeIntent {
|
||||
self.inner.intent()
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for the decoding of the [`QrCodeData`].
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum QrCodeDecodeError {
|
||||
#[error("Error decoding QR code: {error:?}")]
|
||||
Crypto {
|
||||
#[from]
|
||||
error: qrcode::LoginQrCodeDecodeError,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
pub enum HumanQrLoginError {
|
||||
#[error("Linking with this device is not supported.")]
|
||||
LinkingNotSupported,
|
||||
#[error("The sign in was cancelled.")]
|
||||
Cancelled,
|
||||
#[error("The sign in was not completed in the required time.")]
|
||||
Expired,
|
||||
#[error("A secure connection could not have been established between the two devices.")]
|
||||
ConnectionInsecure,
|
||||
#[error("The sign in was declined.")]
|
||||
Declined,
|
||||
#[error("An unknown error has happened.")]
|
||||
Unknown,
|
||||
#[error("The homeserver doesn't provide sliding sync in its configuration.")]
|
||||
SlidingSyncNotAvailable,
|
||||
#[error("Unable to use OIDC as the supplied client metadata is invalid.")]
|
||||
OidcMetadataInvalid,
|
||||
#[error("The other device is not signed in and as such can't sign in other devices.")]
|
||||
OtherDeviceNotSignedIn,
|
||||
#[error("The check code was already sent.")]
|
||||
CheckCodeAlreadySent,
|
||||
#[error("The check code could not be sent.")]
|
||||
CheckCodeCannotBeSent,
|
||||
#[error("The rendezvous session was not found and might have expired")]
|
||||
NotFound,
|
||||
#[error("The QR code specifies an unsupported protocol version")]
|
||||
UnsupportedQrCodeType,
|
||||
}
|
||||
|
||||
impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
|
||||
fn from(value: qrcode::QRCodeLoginError) -> Self {
|
||||
use qrcode::{QRCodeLoginError, SecureChannelError};
|
||||
|
||||
match value {
|
||||
QRCodeLoginError::LoginFailure { reason, .. } => match reason {
|
||||
LoginFailureReason::UnsupportedProtocol => HumanQrLoginError::LinkingNotSupported,
|
||||
LoginFailureReason::AuthorizationExpired => HumanQrLoginError::Expired,
|
||||
LoginFailureReason::UserCancelled => HumanQrLoginError::Cancelled,
|
||||
_ => HumanQrLoginError::Unknown,
|
||||
},
|
||||
|
||||
QRCodeLoginError::OAuth(e) => {
|
||||
if let Some(e) = e.as_request_token_error() {
|
||||
match e {
|
||||
DeviceCodeErrorResponseType::AccessDenied => HumanQrLoginError::Declined,
|
||||
DeviceCodeErrorResponseType::ExpiredToken => HumanQrLoginError::Expired,
|
||||
_ => HumanQrLoginError::Unknown,
|
||||
}
|
||||
} else {
|
||||
HumanQrLoginError::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
QRCodeLoginError::SecureChannel(e) => match e {
|
||||
SecureChannelError::Utf8(_)
|
||||
| SecureChannelError::MessageDecode(_)
|
||||
| SecureChannelError::Json(_)
|
||||
| SecureChannelError::RendezvousChannel(_) => HumanQrLoginError::Unknown,
|
||||
SecureChannelError::UnsupportedQrCodeType => {
|
||||
HumanQrLoginError::UnsupportedQrCodeType
|
||||
}
|
||||
SecureChannelError::SecureChannelMessage { .. }
|
||||
| SecureChannelError::Ecies(_)
|
||||
| SecureChannelError::InvalidCheckCode
|
||||
| SecureChannelError::CannotReceiveCheckCode => {
|
||||
HumanQrLoginError::ConnectionInsecure
|
||||
}
|
||||
SecureChannelError::InvalidIntent => HumanQrLoginError::OtherDeviceNotSignedIn,
|
||||
},
|
||||
|
||||
QRCodeLoginError::UnexpectedMessage { .. }
|
||||
| QRCodeLoginError::CrossProcessRefreshLock(_)
|
||||
| QRCodeLoginError::DeviceKeyUpload(_)
|
||||
| QRCodeLoginError::SessionTokens(_)
|
||||
| QRCodeLoginError::UserIdDiscovery(_)
|
||||
| QRCodeLoginError::SecretImport(_)
|
||||
| QRCodeLoginError::ServerReset(_) => HumanQrLoginError::Unknown,
|
||||
|
||||
QRCodeLoginError::NotFound => HumanQrLoginError::NotFound,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CheckCodeSenderError> for HumanQrLoginError {
|
||||
fn from(value: CheckCodeSenderError) -> Self {
|
||||
match value {
|
||||
CheckCodeSenderError::AlreadySent => HumanQrLoginError::CheckCodeAlreadySent,
|
||||
CheckCodeSenderError::CannotSend => HumanQrLoginError::CheckCodeCannotBeSent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum HumanQrGrantLoginError {
|
||||
/// The requested device ID is already in use.
|
||||
#[error("The requested device ID is already in use.")]
|
||||
DeviceIDAlreadyInUse,
|
||||
|
||||
/// The check code was incorrect.
|
||||
#[error("The check code was incorrect.")]
|
||||
InvalidCheckCode,
|
||||
|
||||
/// The other client proposed an unsupported protocol.
|
||||
#[error("Unsupported protocol: {0}")]
|
||||
UnsupportedProtocol(String),
|
||||
|
||||
/// Secrets backup not set up properly.
|
||||
#[error("Secrets backup not set up: {0}")]
|
||||
MissingSecretsBackup(String),
|
||||
|
||||
/// The rendezvous session was not found and might have expired.
|
||||
#[error("The rendezvous session was not found and might have expired")]
|
||||
NotFound,
|
||||
|
||||
/// An unknown error has happened.
|
||||
#[error("An unknown error has happened.")]
|
||||
Unknown(String),
|
||||
|
||||
/// The requested device was not returned by the homeserver.
|
||||
#[error("The requested device was not returned by the homeserver.")]
|
||||
DeviceNotFound,
|
||||
|
||||
/// The other device is already signed in and so does not need to sign in.
|
||||
#[error("The other device is already signed and so does not need to sign in.")]
|
||||
OtherDeviceAlreadySignedIn,
|
||||
|
||||
/// The sign in was cancelled.
|
||||
#[error("The sign in was cancelled.")]
|
||||
Cancelled,
|
||||
|
||||
/// The sign in was not completed in the required time.
|
||||
#[error("The sign in was not completed in the required time.")]
|
||||
Expired,
|
||||
|
||||
/// A secure connection could not have been established between the two
|
||||
/// devices.
|
||||
#[error("A secure connection could not have been established between the two devices.")]
|
||||
ConnectionInsecure,
|
||||
|
||||
/// The QR code specifies an unsupported protocol version.
|
||||
#[error("The QR code specifies an unsupported protocol version")]
|
||||
UnsupportedQrCodeType,
|
||||
}
|
||||
|
||||
impl From<qrcode::QRCodeGrantLoginError> for HumanQrGrantLoginError {
|
||||
fn from(value: qrcode::QRCodeGrantLoginError) -> Self {
|
||||
use qrcode::{QRCodeGrantLoginError, SecureChannelError};
|
||||
|
||||
match value {
|
||||
QRCodeGrantLoginError::DeviceIDAlreadyInUse => Self::DeviceIDAlreadyInUse,
|
||||
QRCodeGrantLoginError::DeviceNotFound => Self::DeviceNotFound,
|
||||
QRCodeGrantLoginError::InvalidCheckCode => Self::InvalidCheckCode,
|
||||
QRCodeGrantLoginError::UnsupportedProtocol(protocol) => {
|
||||
Self::UnsupportedProtocol(protocol.to_string())
|
||||
}
|
||||
QRCodeGrantLoginError::MissingSecretsBackup(error) => {
|
||||
Self::MissingSecretsBackup(error.map_or("other".to_owned(), |e| e.to_string()))
|
||||
}
|
||||
QRCodeGrantLoginError::NotFound => Self::NotFound,
|
||||
QRCodeGrantLoginError::SecureChannel(e) => match e {
|
||||
SecureChannelError::Utf8(_)
|
||||
| SecureChannelError::MessageDecode(_)
|
||||
| SecureChannelError::Json(_)
|
||||
| SecureChannelError::RendezvousChannel(_) => Self::Unknown(e.to_string()),
|
||||
SecureChannelError::UnsupportedQrCodeType => Self::UnsupportedQrCodeType,
|
||||
SecureChannelError::SecureChannelMessage { .. }
|
||||
| SecureChannelError::Ecies(_)
|
||||
| SecureChannelError::InvalidCheckCode
|
||||
| SecureChannelError::CannotReceiveCheckCode => Self::ConnectionInsecure,
|
||||
SecureChannelError::InvalidIntent => Self::OtherDeviceAlreadySignedIn,
|
||||
},
|
||||
QRCodeGrantLoginError::UnexpectedMessage { .. } => Self::Unknown(value.to_string()),
|
||||
QRCodeGrantLoginError::Unknown(string) => Self::Unknown(string),
|
||||
QRCodeGrantLoginError::LoginFailure { reason, .. } => match reason {
|
||||
LoginFailureReason::UnsupportedProtocol => Self::UnsupportedProtocol(
|
||||
"Other device does not support any of our protocols".to_owned(),
|
||||
),
|
||||
LoginFailureReason::AuthorizationExpired => Self::Expired,
|
||||
LoginFailureReason::UserCancelled => Self::Cancelled,
|
||||
_ => Self::Unknown(reason.to_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum describing the progress of logging in by scanning a QR code that was
|
||||
/// generated on an existing device.
|
||||
#[derive(Debug, Default, Clone, uniffi::Enum)]
|
||||
pub enum QrLoginProgress {
|
||||
/// The login process is starting.
|
||||
#[default]
|
||||
Starting,
|
||||
/// We established a secure channel with the other device.
|
||||
EstablishingSecureChannel {
|
||||
/// The check code that the device should display so the other device
|
||||
/// can confirm that the channel is secure as well.
|
||||
check_code: u8,
|
||||
/// The string representation of the check code, will be guaranteed to
|
||||
/// be 2 characters long, preserving the leading zero if the
|
||||
/// first digit is a zero.
|
||||
check_code_string: String,
|
||||
},
|
||||
/// We are waiting for the login and for the OAuth 2.0 authorization server
|
||||
/// to give us an access token.
|
||||
WaitingForToken { user_code: String },
|
||||
/// We are syncing secrets.
|
||||
SyncingSecrets,
|
||||
/// The login has successfully finished.
|
||||
Done,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait QrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, state: QrLoginProgress);
|
||||
}
|
||||
|
||||
impl From<qrcode::LoginProgress<QrProgress>> for QrLoginProgress {
|
||||
fn from(value: qrcode::LoginProgress<QrProgress>) -> Self {
|
||||
use qrcode::LoginProgress;
|
||||
|
||||
match value {
|
||||
LoginProgress::Starting => Self::Starting,
|
||||
LoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => {
|
||||
let check_code = check_code.to_digit();
|
||||
|
||||
Self::EstablishingSecureChannel {
|
||||
check_code,
|
||||
check_code_string: format!("{check_code:02}"),
|
||||
}
|
||||
}
|
||||
LoginProgress::WaitingForToken { user_code } => Self::WaitingForToken { user_code },
|
||||
LoginProgress::SyncingSecrets => Self::SyncingSecrets,
|
||||
LoginProgress::Done => Self::Done,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum describing the progress of logging in by generating a QR code and
|
||||
/// having an existing device scan it.
|
||||
#[derive(Debug, Default, Clone, uniffi::Enum)]
|
||||
pub enum GeneratedQrLoginProgress {
|
||||
/// The login process is starting.
|
||||
#[default]
|
||||
Starting,
|
||||
/// We have established the secure channel and now need to display the
|
||||
/// QR code so that the existing device can scan it.
|
||||
QrReady { qr_code: Arc<QrCodeData> },
|
||||
/// The existing device has scanned the QR code and is displaying the
|
||||
/// checkcode. We now need to ask the user to enter the checkcode so that
|
||||
/// we can verify that the channel is indeed secure.
|
||||
QrScanned { check_code_sender: Arc<CheckCodeSender> },
|
||||
/// We are waiting for the login and for the OAuth 2.0 authorization server
|
||||
/// to give us an access token.
|
||||
WaitingForToken { user_code: String },
|
||||
/// We are syncing secrets.
|
||||
SyncingSecrets,
|
||||
/// The login has successfully finished.
|
||||
Done,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait GeneratedQrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, state: GeneratedQrLoginProgress);
|
||||
}
|
||||
|
||||
impl From<qrcode::LoginProgress<GeneratedQrProgress>> for GeneratedQrLoginProgress {
|
||||
fn from(value: qrcode::LoginProgress<GeneratedQrProgress>) -> Self {
|
||||
use qrcode::LoginProgress;
|
||||
|
||||
match value {
|
||||
LoginProgress::Starting => Self::Starting,
|
||||
LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrReady(inner)) => {
|
||||
Self::QrReady { qr_code: Arc::new(QrCodeData { inner }) }
|
||||
}
|
||||
LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrScanned(inner)) => {
|
||||
Self::QrScanned { check_code_sender: Arc::new(CheckCodeSender { inner }) }
|
||||
}
|
||||
LoginProgress::WaitingForToken { user_code } => Self::WaitingForToken { user_code },
|
||||
LoginProgress::SyncingSecrets => Self::SyncingSecrets,
|
||||
LoginProgress::Done => Self::Done,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum describing the progress of granting login in by scanning a QR code that
|
||||
/// was generated on a new device.
|
||||
#[derive(Debug, Default, Clone, uniffi::Enum)]
|
||||
pub enum GrantQrLoginProgress {
|
||||
/// The login process is starting.
|
||||
#[default]
|
||||
Starting,
|
||||
/// We established a secure channel with the other device.
|
||||
EstablishingSecureChannel {
|
||||
/// The check code that the device should display so the other device
|
||||
/// can confirm that the channel is secure as well.
|
||||
check_code: u8,
|
||||
/// The string representation of the check code, will be guaranteed to
|
||||
/// be 2 characters long, preserving the leading zero if the
|
||||
/// first digit is a zero.
|
||||
check_code_string: String,
|
||||
},
|
||||
/// The secure channel has been confirmed using the [`CheckCode`] and this
|
||||
/// device is waiting for the authorization to complete.
|
||||
WaitingForAuth {
|
||||
/// A URI to open in a (secure) system browser to verify the new login.
|
||||
verification_uri: String,
|
||||
},
|
||||
/// We are syncing secrets.
|
||||
SyncingSecrets,
|
||||
/// The login has successfully finished.
|
||||
Done,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait GrantQrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, state: GrantQrLoginProgress);
|
||||
}
|
||||
|
||||
impl From<qrcode::GrantLoginProgress<QrProgress>> for GrantQrLoginProgress {
|
||||
fn from(value: qrcode::GrantLoginProgress<QrProgress>) -> Self {
|
||||
use qrcode::GrantLoginProgress;
|
||||
|
||||
match value {
|
||||
GrantLoginProgress::Starting => Self::Starting,
|
||||
GrantLoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => {
|
||||
let check_code = check_code.to_digit();
|
||||
|
||||
Self::EstablishingSecureChannel {
|
||||
check_code,
|
||||
check_code_string: format!("{check_code:02}"),
|
||||
}
|
||||
}
|
||||
GrantLoginProgress::WaitingForAuth { verification_uri } => {
|
||||
Self::WaitingForAuth { verification_uri: verification_uri.into() }
|
||||
}
|
||||
GrantLoginProgress::SyncingSecrets => Self::SyncingSecrets,
|
||||
GrantLoginProgress::Done => Self::Done,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum describing the progress of granting login by generating a QR code to
|
||||
/// be scanned on the new device.
|
||||
#[derive(Debug, Default, Clone, uniffi::Enum)]
|
||||
pub enum GrantGeneratedQrLoginProgress {
|
||||
/// The login process is starting.
|
||||
#[default]
|
||||
Starting,
|
||||
/// We have established the secure channel and now need to display the
|
||||
/// QR code so that the existing device can scan it.
|
||||
QrReady { qr_code: Arc<QrCodeData> },
|
||||
/// The existing device has scanned the QR code and is displaying the
|
||||
/// checkcode. We now need to ask the user to enter the checkcode so that
|
||||
/// we can verify that the channel is indeed secure.
|
||||
QrScanned { check_code_sender: Arc<CheckCodeSender> },
|
||||
/// The secure channel has been confirmed using the [`CheckCode`] and this
|
||||
/// device is waiting for the authorization to complete.
|
||||
WaitingForAuth {
|
||||
/// A URI to open in a (secure) system browser to verify the new login.
|
||||
verification_uri: String,
|
||||
},
|
||||
/// We are syncing secrets.
|
||||
SyncingSecrets,
|
||||
/// The login has successfully finished.
|
||||
Done,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait GrantGeneratedQrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, state: GrantGeneratedQrLoginProgress);
|
||||
}
|
||||
|
||||
impl From<qrcode::GrantLoginProgress<GeneratedQrProgress>> for GrantGeneratedQrLoginProgress {
|
||||
fn from(value: qrcode::GrantLoginProgress<GeneratedQrProgress>) -> Self {
|
||||
use qrcode::GrantLoginProgress;
|
||||
|
||||
match value {
|
||||
GrantLoginProgress::Starting => Self::Starting,
|
||||
GrantLoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrReady(inner)) => {
|
||||
Self::QrReady { qr_code: Arc::new(QrCodeData { inner }) }
|
||||
}
|
||||
GrantLoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrScanned(
|
||||
inner,
|
||||
)) => Self::QrScanned { check_code_sender: Arc::new(CheckCodeSender { inner }) },
|
||||
GrantLoginProgress::WaitingForAuth { verification_uri } => {
|
||||
Self::WaitingForAuth { verification_uri: verification_uri.into() }
|
||||
}
|
||||
GrantLoginProgress::SyncingSecrets => Self::SyncingSecrets,
|
||||
GrantLoginProgress::Done => Self::Done,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, uniffi::Object)]
|
||||
/// Used to pass back the [`CheckCode`] entered by the user to verify that the
|
||||
/// secure channel is indeed secure.
|
||||
pub struct CheckCodeSender {
|
||||
inner: SdkCheckCodeSender,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl CheckCodeSender {
|
||||
/// Send the [`CheckCode`].
|
||||
///
|
||||
/// Calling this method more than once will result in an error.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `check_code` - The check code in digits representation.
|
||||
pub async fn send(&self, code: u8) -> Result<(), HumanQrLoginError> {
|
||||
self.inner.send(code).await.map_err(HumanQrLoginError::from)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,551 @@
|
||||
use std::{convert::TryFrom, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use matrix_sdk::{room::Room as SdkRoom, RoomMemberships, RoomState};
|
||||
use matrix_sdk_ui::timeline::RoomExt;
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
api::client::room::report_content,
|
||||
assign,
|
||||
events::room::{avatar::ImageInfo as RumaAvatarImageInfo, MediaSource},
|
||||
EventId, UserId,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::error;
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::{
|
||||
chunk_iterator::ChunkIterator,
|
||||
error::{ClientError, MediaInfoError, RoomError},
|
||||
room_info::RoomInfo,
|
||||
room_member::{MessageLikeEventType, RoomMember, StateEventType},
|
||||
ruma::ImageInfo,
|
||||
timeline::{EventTimelineItem, Timeline},
|
||||
utils::u64_to_uint,
|
||||
TaskHandle,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum Membership {
|
||||
Invited,
|
||||
Joined,
|
||||
Left,
|
||||
}
|
||||
|
||||
impl From<RoomState> for Membership {
|
||||
fn from(value: RoomState) -> Self {
|
||||
match value {
|
||||
RoomState::Invited => Membership::Invited,
|
||||
RoomState::Joined => Membership::Joined,
|
||||
RoomState::Left => Membership::Left,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type TimelineLock = Arc<RwLock<Option<Arc<Timeline>>>>;
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Room {
|
||||
pub(super) inner: SdkRoom,
|
||||
timeline: TimelineLock,
|
||||
}
|
||||
|
||||
impl Room {
|
||||
pub(crate) fn new(inner: SdkRoom) -> Self {
|
||||
Room { inner, timeline: Default::default() }
|
||||
}
|
||||
|
||||
pub(crate) fn with_timeline(inner: SdkRoom, timeline: TimelineLock) -> Self {
|
||||
Room { inner, timeline }
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl Room {
|
||||
pub fn id(&self) -> String {
|
||||
self.inner.room_id().to_string()
|
||||
}
|
||||
|
||||
pub fn name(&self) -> Option<String> {
|
||||
self.inner.name()
|
||||
}
|
||||
|
||||
pub fn topic(&self) -> Option<String> {
|
||||
self.inner.topic()
|
||||
}
|
||||
|
||||
pub fn avatar_url(&self) -> Option<String> {
|
||||
self.inner.avatar_url().map(|m| m.to_string())
|
||||
}
|
||||
|
||||
pub fn is_direct(&self) -> bool {
|
||||
RUNTIME.block_on(async move { self.inner.is_direct().await.unwrap_or(false) })
|
||||
}
|
||||
|
||||
pub fn is_public(&self) -> bool {
|
||||
self.inner.is_public()
|
||||
}
|
||||
|
||||
pub fn is_space(&self) -> bool {
|
||||
self.inner.is_space()
|
||||
}
|
||||
|
||||
pub fn is_tombstoned(&self) -> bool {
|
||||
self.inner.is_tombstoned()
|
||||
}
|
||||
|
||||
pub fn canonical_alias(&self) -> Option<String> {
|
||||
self.inner.canonical_alias().map(|a| a.to_string())
|
||||
}
|
||||
|
||||
pub fn alternative_aliases(&self) -> Vec<String> {
|
||||
self.inner.alt_aliases().iter().map(|a| a.to_string()).collect()
|
||||
}
|
||||
|
||||
pub fn membership(&self) -> Membership {
|
||||
self.inner.state().into()
|
||||
}
|
||||
|
||||
/// Is there a non expired membership with application "m.call" and scope
|
||||
/// "m.room" in this room.
|
||||
pub fn has_active_room_call(&self) -> bool {
|
||||
self.inner.has_active_room_call()
|
||||
}
|
||||
|
||||
/// Returns a Vec of userId's that participate in the room call.
|
||||
///
|
||||
/// matrix_rtc memberships with application "m.call" and scope "m.room" are
|
||||
/// considered. A user can occur twice if they join with two devices.
|
||||
/// convert to a set depending if the different users are required or the
|
||||
/// amount of sessions.
|
||||
///
|
||||
/// The vector is ordered by oldest membership user to newest.
|
||||
pub fn active_room_call_participants(&self) -> Vec<String> {
|
||||
self.inner.active_room_call_participants().iter().map(|u| u.to_string()).collect()
|
||||
}
|
||||
|
||||
pub fn inviter(&self) -> Option<Arc<RoomMember>> {
|
||||
if self.inner.state() == RoomState::Invited {
|
||||
RUNTIME.block_on(async move {
|
||||
self.inner
|
||||
.invite_details()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|a| a.inviter)
|
||||
.map(|m| Arc::new(RoomMember::new(m)))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn timeline(&self) -> Arc<Timeline> {
|
||||
let mut write_guard = self.timeline.write().await;
|
||||
if let Some(timeline) = &*write_guard {
|
||||
timeline.clone()
|
||||
} else {
|
||||
let timeline = Timeline::new(self.inner.timeline().await);
|
||||
*write_guard = Some(timeline.clone());
|
||||
timeline
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn poll_history(&self) -> Arc<Timeline> {
|
||||
Timeline::new(self.inner.poll_history().await)
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> Result<String, ClientError> {
|
||||
let r = self.inner.clone();
|
||||
RUNTIME.block_on(async move { Ok(r.display_name().await?.to_string()) })
|
||||
}
|
||||
|
||||
pub fn is_encrypted(&self) -> Result<bool, ClientError> {
|
||||
let room = self.inner.clone();
|
||||
RUNTIME.block_on(async move {
|
||||
let is_encrypted = room.is_encrypted().await?;
|
||||
Ok(is_encrypted)
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn members(&self) -> Result<Arc<RoomMembersIterator>, ClientError> {
|
||||
Ok(Arc::new(RoomMembersIterator::new(self.inner.members(RoomMemberships::empty()).await?)))
|
||||
}
|
||||
|
||||
pub async fn member(&self, user_id: String) -> Result<Arc<RoomMember>, ClientError> {
|
||||
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
|
||||
let member = self.inner.get_member(&user_id).await?.context("No user found")?;
|
||||
Ok(Arc::new(RoomMember::new(member)))
|
||||
}
|
||||
|
||||
pub fn member_avatar_url(&self, user_id: String) -> Result<Option<String>, ClientError> {
|
||||
let room = self.inner.clone();
|
||||
RUNTIME.block_on(async move {
|
||||
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
|
||||
let member = room.get_member(&user_id).await?.context("No user found")?;
|
||||
let avatar_url_string = member.avatar_url().map(|m| m.to_string());
|
||||
Ok(avatar_url_string)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn member_display_name(&self, user_id: String) -> Result<Option<String>, ClientError> {
|
||||
let room = self.inner.clone();
|
||||
RUNTIME.block_on(async move {
|
||||
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
|
||||
let member = room.get_member(&user_id).await?.context("No user found")?;
|
||||
let avatar_url_string = member.display_name().map(|m| m.to_owned());
|
||||
Ok(avatar_url_string)
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn room_info(&self) -> Result<RoomInfo, ClientError> {
|
||||
let avatar_url = self.inner.avatar_url();
|
||||
|
||||
// Look for a local event in the `Timeline`.
|
||||
//
|
||||
// First off, let's see if a `Timeline` exists…
|
||||
if let Some(timeline) = self.timeline.read().await.clone() {
|
||||
// If it contains a `latest_event`…
|
||||
if let Some(timeline_last_event) = timeline.inner.latest_event().await {
|
||||
// If it's a local echo…
|
||||
if timeline_last_event.is_local_echo() {
|
||||
return Ok(RoomInfo::new(
|
||||
&self.inner,
|
||||
avatar_url,
|
||||
Some(Arc::new(EventTimelineItem(timeline_last_event))),
|
||||
)
|
||||
.await?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, fallback to the classical path.
|
||||
let latest_event = match self.inner.latest_event() {
|
||||
Some(latest_event) => matrix_sdk_ui::timeline::EventTimelineItem::from_latest_event(
|
||||
self.inner.client(),
|
||||
self.inner.room_id(),
|
||||
latest_event,
|
||||
)
|
||||
.await
|
||||
.map(EventTimelineItem)
|
||||
.map(Arc::new),
|
||||
None => None,
|
||||
};
|
||||
Ok(RoomInfo::new(&self.inner, avatar_url, latest_event).await?)
|
||||
}
|
||||
|
||||
pub fn subscribe_to_room_info_updates(
|
||||
self: Arc<Self>,
|
||||
listener: Box<dyn RoomInfoListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let mut subscriber = self.inner.subscribe_info();
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
while subscriber.next().await.is_some() {
|
||||
match self.room_info().await {
|
||||
Ok(room_info) => listener.call(room_info),
|
||||
Err(e) => {
|
||||
error!("Failed to compute new RoomInfo: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Redacts an event from the room.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event_id` - The ID of the event to redact
|
||||
///
|
||||
/// * `reason` - The reason for the event being redacted (optional).
|
||||
/// its transaction ID (optional). If not given one is created.
|
||||
pub fn redact(&self, event_id: String, reason: Option<String>) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
self.inner.redact(&event_id, reason.as_deref(), None).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn active_members_count(&self) -> u64 {
|
||||
self.inner.active_members_count()
|
||||
}
|
||||
|
||||
pub fn invited_members_count(&self) -> u64 {
|
||||
self.inner.invited_members_count()
|
||||
}
|
||||
|
||||
pub fn joined_members_count(&self) -> u64 {
|
||||
self.inner.joined_members_count()
|
||||
}
|
||||
|
||||
/// Reports an event from the room.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event_id` - The ID of the event to report
|
||||
///
|
||||
/// * `reason` - The reason for the event being reported (optional).
|
||||
///
|
||||
/// * `score` - The score to rate this content as where -100 is most
|
||||
/// offensive and 0 is inoffensive (optional).
|
||||
pub fn report_content(
|
||||
&self,
|
||||
event_id: String,
|
||||
score: Option<i32>,
|
||||
reason: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let int_score = score.map(|value| value.into());
|
||||
RUNTIME.block_on(async move {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
self.inner
|
||||
.client()
|
||||
.send(
|
||||
report_content::v3::Request::new(
|
||||
self.inner.room_id().into(),
|
||||
event_id,
|
||||
int_score,
|
||||
reason,
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Ignores a user.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event_id` - The ID of the user to ignore.
|
||||
pub fn ignore_user(&self, user_id: String) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
self.inner.client().account().ignore_user(&user_id).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Leave this room.
|
||||
///
|
||||
/// Only invited and joined rooms can be left.
|
||||
pub fn leave(&self) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async {
|
||||
self.inner.leave().await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Join this room.
|
||||
///
|
||||
/// Only invited and left rooms can be joined via this method.
|
||||
pub fn join(&self) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async {
|
||||
self.inner.join().await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets a new name to the room.
|
||||
pub fn set_name(&self, name: String) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
self.inner.set_name(name).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets a new topic in the room.
|
||||
pub fn set_topic(&self, topic: String) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
self.inner.set_room_topic(&topic).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Upload and set the room's avatar.
|
||||
///
|
||||
/// This will upload the data produced by the reader to the homeserver's
|
||||
/// content repository, and set the room's avatar to the MXC URI for the
|
||||
/// uploaded file.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `mime_type` - The mime description of the avatar, for example
|
||||
/// image/jpeg
|
||||
/// * `data` - The raw data that will be uploaded to the homeserver's
|
||||
/// content repository
|
||||
/// * `media_info` - The media info used as avatar image info.
|
||||
pub fn upload_avatar(
|
||||
&self,
|
||||
mime_type: String,
|
||||
data: Vec<u8>,
|
||||
media_info: Option<ImageInfo>,
|
||||
) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let mime: Mime = mime_type.parse()?;
|
||||
self.inner
|
||||
.upload_avatar(
|
||||
&mime,
|
||||
data,
|
||||
media_info
|
||||
.map(TryInto::try_into)
|
||||
.transpose()
|
||||
.map_err(|_| RoomError::InvalidMediaInfo)?,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Removes the current room avatar
|
||||
pub fn remove_avatar(&self) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
self.inner.remove_avatar().await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn invite_user_by_id(&self, user_id: String) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let user = <&UserId>::try_from(user_id.as_str())
|
||||
.context("Could not create user from string")?;
|
||||
self.inner.invite_user_by_id(user).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn can_user_redact(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_redact(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_ban(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_ban(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn ban_user(
|
||||
&self,
|
||||
user_id: String,
|
||||
reason: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.ban_user(&user_id, reason.as_deref()).await?)
|
||||
}
|
||||
|
||||
pub async fn unban_user(
|
||||
&self,
|
||||
user_id: String,
|
||||
reason: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.unban_user(&user_id, reason.as_deref()).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_invite(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_invite(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_kick(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_kick(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn kick_user(
|
||||
&self,
|
||||
user_id: String,
|
||||
reason: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.kick_user(&user_id, reason.as_deref()).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_send_state(
|
||||
&self,
|
||||
user_id: String,
|
||||
state_event: StateEventType,
|
||||
) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_send_state(&user_id, state_event.into()).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_send_message(
|
||||
&self,
|
||||
user_id: String,
|
||||
message: MessageLikeEventType,
|
||||
) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_send_message(&user_id, message.into()).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_trigger_room_notification(
|
||||
&self,
|
||||
user_id: String,
|
||||
) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_trigger_room_notification(&user_id).await?)
|
||||
}
|
||||
|
||||
pub fn own_user_id(&self) -> String {
|
||||
self.inner.own_user_id().to_string()
|
||||
}
|
||||
|
||||
pub async fn typing_notice(&self, is_typing: bool) -> Result<(), ClientError> {
|
||||
Ok(self.inner.typing_notice(is_typing).await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait RoomInfoListener: Sync + Send {
|
||||
fn call(&self, room_info: RoomInfo);
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomMembersIterator {
|
||||
chunk_iterator: ChunkIterator<matrix_sdk::room::RoomMember>,
|
||||
}
|
||||
|
||||
impl RoomMembersIterator {
|
||||
fn new(members: Vec<matrix_sdk::room::RoomMember>) -> Self {
|
||||
Self { chunk_iterator: ChunkIterator::new(members) }
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl RoomMembersIterator {
|
||||
fn len(&self) -> u32 {
|
||||
self.chunk_iterator.len()
|
||||
}
|
||||
|
||||
fn next_chunk(&self, chunk_size: u32) -> Option<Vec<Arc<RoomMember>>> {
|
||||
self.chunk_iterator
|
||||
.next(chunk_size)
|
||||
.map(|members| members.into_iter().map(RoomMember::new).map(Arc::new).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ImageInfo> for RumaAvatarImageInfo {
|
||||
type Error = MediaInfoError;
|
||||
|
||||
fn try_from(value: ImageInfo) -> Result<Self, MediaInfoError> {
|
||||
let thumbnail_url = if let Some(media_source) = value.thumbnail_source {
|
||||
match media_source.as_ref() {
|
||||
MediaSource::Plain(mxc_uri) => Some(mxc_uri.clone()),
|
||||
MediaSource::Encrypted(_) => return Err(MediaInfoError::InvalidField),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(assign!(RumaAvatarImageInfo::new(), {
|
||||
height: value.height.map(u64_to_uint),
|
||||
width: value.width.map(u64_to_uint),
|
||||
mimetype: value.mimetype,
|
||||
size: value.size.map(u64_to_uint),
|
||||
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
|
||||
thumbnail_url: thumbnail_url,
|
||||
blurhash: value.blurhash,
|
||||
}))
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,254 +0,0 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use ruma::{
|
||||
OwnedUserId, UserId,
|
||||
events::{TimelineEventType, room::power_levels::RoomPowerLevels as RumaPowerLevels},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::ClientError,
|
||||
event::{MessageLikeEventType, StateEventType},
|
||||
};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomPowerLevels {
|
||||
inner: RumaPowerLevels,
|
||||
own_user_id: OwnedUserId,
|
||||
}
|
||||
|
||||
impl RoomPowerLevels {
|
||||
pub fn new(value: RumaPowerLevels, own_user_id: OwnedUserId) -> Self {
|
||||
Self { inner: value, own_user_id }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomPowerLevels {
|
||||
fn values(&self) -> RoomPowerLevelsValues {
|
||||
self.inner.clone().into()
|
||||
}
|
||||
|
||||
fn events(&self) -> HashMap<crate::event::TimelineEventType, i64> {
|
||||
self.inner.events.iter().map(|(key, value)| (key.clone().into(), (*value).into())).collect()
|
||||
}
|
||||
|
||||
/// Gets a map with the `UserId` of users with power levels other than `0`
|
||||
/// and their power level.
|
||||
pub fn user_power_levels(&self) -> HashMap<String, i64> {
|
||||
let mut user_power_levels = HashMap::<String, i64>::new();
|
||||
|
||||
for (id, level) in self.inner.users.iter() {
|
||||
user_power_levels.insert(id.to_string(), (*level).into());
|
||||
}
|
||||
|
||||
user_power_levels
|
||||
}
|
||||
|
||||
/// Returns true if the current user is able to ban in the room.
|
||||
pub fn can_own_user_ban(&self) -> bool {
|
||||
self.inner.user_can_ban(&self.own_user_id)
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to ban in the
|
||||
/// room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_ban(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_ban(&user_id))
|
||||
}
|
||||
|
||||
/// Returns true if the current user is able to redact their own messages in
|
||||
/// the room.
|
||||
pub fn can_own_user_redact_own(&self) -> bool {
|
||||
self.inner.user_can_redact_own_event(&self.own_user_id)
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to redact
|
||||
/// their own messages in the room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_redact_own(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_redact_own_event(&user_id))
|
||||
}
|
||||
|
||||
/// Returns true if the current user user is able to redact messages of
|
||||
/// other users in the room.
|
||||
pub fn can_own_user_redact_other(&self) -> bool {
|
||||
self.inner.user_can_redact_event_of_other(&self.own_user_id)
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to redact
|
||||
/// messages of other users in the room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_redact_other(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_redact_event_of_other(&user_id))
|
||||
}
|
||||
|
||||
/// Returns true if the current user is able to invite in the room.
|
||||
pub fn can_own_user_invite(&self) -> bool {
|
||||
self.inner.user_can_invite(&self.own_user_id)
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to invite in the
|
||||
/// room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_invite(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_invite(&user_id))
|
||||
}
|
||||
|
||||
/// Returns true if the current user is able to kick in the room.
|
||||
pub fn can_own_user_kick(&self) -> bool {
|
||||
self.inner.user_can_kick(&self.own_user_id)
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to kick in the
|
||||
/// room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_kick(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_kick(&user_id))
|
||||
}
|
||||
|
||||
/// Returns true if the current user is able to send a specific state event
|
||||
/// type in the room.
|
||||
pub fn can_own_user_send_state(&self, state_event: StateEventType) -> bool {
|
||||
self.inner.user_can_send_state(&self.own_user_id, state_event.into())
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to send a
|
||||
/// specific state event type in the room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_send_state(
|
||||
&self,
|
||||
user_id: String,
|
||||
state_event: StateEventType,
|
||||
) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_send_state(&user_id, state_event.into()))
|
||||
}
|
||||
|
||||
/// Returns true if the current user is able to send a specific message type
|
||||
/// in the room.
|
||||
pub fn can_own_user_send_message(&self, message: MessageLikeEventType) -> bool {
|
||||
self.inner.user_can_send_message(&self.own_user_id, message.into())
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to send a
|
||||
/// specific message type in the room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_send_message(
|
||||
&self,
|
||||
user_id: String,
|
||||
message: MessageLikeEventType,
|
||||
) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_send_message(&user_id, message.into()))
|
||||
}
|
||||
|
||||
/// Returns true if the current user is able to pin or unpin events in the
|
||||
/// room.
|
||||
pub fn can_own_user_pin_unpin(&self) -> bool {
|
||||
self.inner.user_can_send_state(&self.own_user_id, StateEventType::RoomPinnedEvents.into())
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to pin or unpin
|
||||
/// events in the room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_pin_unpin(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_send_state(&user_id, StateEventType::RoomPinnedEvents.into()))
|
||||
}
|
||||
|
||||
/// Returns true if the current user is able to trigger a notification in
|
||||
/// the room.
|
||||
pub fn can_own_user_trigger_room_notification(&self) -> bool {
|
||||
self.inner.user_can_trigger_room_notification(&self.own_user_id)
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to trigger a
|
||||
/// notification in the room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_trigger_room_notification(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_trigger_room_notification(&user_id))
|
||||
}
|
||||
}
|
||||
|
||||
/// This intermediary struct is used to expose the power levels values through
|
||||
/// FFI and work around it not exposing public exported object fields.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomPowerLevelsValues {
|
||||
/// The level required to ban a user.
|
||||
pub ban: i64,
|
||||
/// The level required to invite a user.
|
||||
pub invite: i64,
|
||||
/// The level required to kick a user.
|
||||
pub kick: i64,
|
||||
/// The level required to redact an event.
|
||||
pub redact: i64,
|
||||
/// The default level required to send message events.
|
||||
pub events_default: i64,
|
||||
/// The default level required to send state events.
|
||||
pub state_default: i64,
|
||||
/// The default power level for every user in the room.
|
||||
pub users_default: i64,
|
||||
/// The level required to change the room's name.
|
||||
pub room_name: i64,
|
||||
/// The level required to change the room's avatar.
|
||||
pub room_avatar: i64,
|
||||
/// The level required to change the room's topic.
|
||||
pub room_topic: i64,
|
||||
/// The level required to change the space's children.
|
||||
pub space_child: i64,
|
||||
}
|
||||
|
||||
impl From<RumaPowerLevels> for RoomPowerLevelsValues {
|
||||
fn from(value: RumaPowerLevels) -> Self {
|
||||
fn state_event_level_for(
|
||||
power_levels: &RumaPowerLevels,
|
||||
event_type: &TimelineEventType,
|
||||
) -> i64 {
|
||||
let default_state: i64 = power_levels.state_default.into();
|
||||
power_levels.events.get(event_type).map_or(default_state, |&level| level.into())
|
||||
}
|
||||
Self {
|
||||
ban: value.ban.into(),
|
||||
invite: value.invite.into(),
|
||||
kick: value.kick.into(),
|
||||
redact: value.redact.into(),
|
||||
events_default: value.events_default.into(),
|
||||
state_default: value.state_default.into(),
|
||||
users_default: value.users_default.into(),
|
||||
room_name: state_event_level_for(&value, &TimelineEventType::RoomName),
|
||||
room_avatar: state_event_level_for(&value, &TimelineEventType::RoomAvatar),
|
||||
room_topic: state_event_level_for(&value, &TimelineEventType::RoomTopic),
|
||||
space_child: state_event_level_for(&value, &TimelineEventType::SpaceChild),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk::{CallIntentConsensus, EncryptionState, RoomState};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
client::JoinRule,
|
||||
error::ClientError,
|
||||
notification_settings::RoomNotificationMode,
|
||||
room::{
|
||||
Membership, RoomHero, RoomHistoryVisibility, SuccessorRoom, power_levels::RoomPowerLevels,
|
||||
},
|
||||
room_member::RoomMember,
|
||||
ruma::RtcCallIntent,
|
||||
};
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum RtcCallIntentConsensus {
|
||||
Full(RtcCallIntent),
|
||||
Partial { intent: RtcCallIntent, agreeing_count: u64, total_count: u64 },
|
||||
None,
|
||||
}
|
||||
|
||||
impl From<CallIntentConsensus> for RtcCallIntentConsensus {
|
||||
fn from(value: CallIntentConsensus) -> Self {
|
||||
match value {
|
||||
CallIntentConsensus::Full(intent) => RtcCallIntentConsensus::Full(intent.into()),
|
||||
CallIntentConsensus::Partial { intent, agreeing_count, total_count } => {
|
||||
RtcCallIntentConsensus::Partial {
|
||||
intent: intent.into(),
|
||||
agreeing_count,
|
||||
total_count,
|
||||
}
|
||||
}
|
||||
CallIntentConsensus::None => RtcCallIntentConsensus::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomInfo {
|
||||
id: String,
|
||||
encryption_state: EncryptionState,
|
||||
creators: Option<Vec<String>>,
|
||||
/// The room's name from the room state event if received from sync, or one
|
||||
/// that's been computed otherwise.
|
||||
display_name: Option<String>,
|
||||
/// Room name as defined by the room state event only.
|
||||
raw_name: Option<String>,
|
||||
topic: Option<String>,
|
||||
avatar_url: Option<String>,
|
||||
is_direct: bool,
|
||||
/// Whether the room is public or not, based on the join rules.
|
||||
///
|
||||
/// Can be `None` if the join rules state event is not available for this
|
||||
/// room.
|
||||
is_public: Option<bool>,
|
||||
is_space: bool,
|
||||
/// If present, it means the room has been archived/upgraded.
|
||||
successor_room: Option<SuccessorRoom>,
|
||||
is_favourite: bool,
|
||||
is_low_priority: bool,
|
||||
canonical_alias: Option<String>,
|
||||
alternative_aliases: Vec<String>,
|
||||
membership: Membership,
|
||||
/// Member who invited the current user to a room that's in the invited
|
||||
/// state.
|
||||
///
|
||||
/// Can be missing if the room membership invite event is missing from the
|
||||
/// store.
|
||||
inviter: Option<RoomMember>,
|
||||
heroes: Vec<RoomHero>,
|
||||
active_members_count: u64,
|
||||
invited_members_count: u64,
|
||||
joined_members_count: u64,
|
||||
service_members: Vec<String>,
|
||||
highlight_count: u64,
|
||||
notification_count: u64,
|
||||
cached_user_defined_notification_mode: Option<RoomNotificationMode>,
|
||||
has_room_call: bool,
|
||||
active_room_call_participants: Vec<String>,
|
||||
active_room_call_consensus_intent: RtcCallIntentConsensus,
|
||||
/// Whether this room has been explicitly marked as unread
|
||||
is_marked_unread: bool,
|
||||
/// "Interesting" messages received in that room, independently of the
|
||||
/// notification settings.
|
||||
num_unread_messages: u64,
|
||||
/// Events that will notify the user, according to their
|
||||
/// notification settings.
|
||||
num_unread_notifications: u64,
|
||||
/// Events causing mentions/highlights for the user, according to their
|
||||
/// notification settings.
|
||||
num_unread_mentions: u64,
|
||||
/// The currently pinned event ids.
|
||||
pinned_event_ids: Vec<String>,
|
||||
/// The join rule for this room, if known.
|
||||
join_rule: Option<JoinRule>,
|
||||
/// The history visibility for this room, if known.
|
||||
history_visibility: RoomHistoryVisibility,
|
||||
/// This room's current power levels.
|
||||
///
|
||||
/// Can be missing if the room power levels event is missing from the store.
|
||||
power_levels: Option<Arc<RoomPowerLevels>>,
|
||||
/// This room's version.
|
||||
room_version: Option<String>,
|
||||
/// Whether creators are privileged over every other user (have infinite
|
||||
/// power level).
|
||||
privileged_creators_role: bool,
|
||||
}
|
||||
|
||||
impl RoomInfo {
|
||||
pub(crate) async fn new(room: &matrix_sdk::Room) -> Result<Self, ClientError> {
|
||||
let unread_notification_counts = room.unread_notification_counts();
|
||||
|
||||
let pinned_event_ids =
|
||||
room.pinned_event_ids().unwrap_or_default().iter().map(|id| id.to_string()).collect();
|
||||
|
||||
let join_rule = room
|
||||
.join_rule()
|
||||
.map(TryInto::try_into)
|
||||
.transpose()
|
||||
.inspect_err(|err| {
|
||||
warn!("Failed to parse join rule: {err}");
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
let power_levels = room
|
||||
.power_levels()
|
||||
.await
|
||||
.ok()
|
||||
.map(|p| RoomPowerLevels::new(p, room.own_user_id().to_owned()));
|
||||
|
||||
Ok(Self {
|
||||
id: room.room_id().to_string(),
|
||||
encryption_state: room.encryption_state(),
|
||||
creators: room
|
||||
.creators()
|
||||
.map(|creators| creators.into_iter().map(Into::into).collect()),
|
||||
display_name: room.cached_display_name().map(|name| name.to_string()),
|
||||
raw_name: room.name(),
|
||||
topic: room.topic(),
|
||||
avatar_url: room.avatar_url().map(Into::into),
|
||||
is_direct: room.is_direct().await?,
|
||||
is_public: room.is_public(),
|
||||
is_space: room.is_space(),
|
||||
successor_room: room.successor_room().map(Into::into),
|
||||
is_favourite: room.is_favourite(),
|
||||
is_low_priority: room.is_low_priority(),
|
||||
canonical_alias: room.canonical_alias().map(Into::into),
|
||||
alternative_aliases: room.alt_aliases().into_iter().map(Into::into).collect(),
|
||||
membership: room.state().into(),
|
||||
inviter: match room.state() {
|
||||
RoomState::Invited => room
|
||||
.invite_details()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|details| details.inviter)
|
||||
.map(TryInto::try_into)
|
||||
.transpose()
|
||||
.ok()
|
||||
.flatten(),
|
||||
_ => None,
|
||||
},
|
||||
heroes: room.heroes().into_iter().map(Into::into).collect(),
|
||||
active_members_count: room.active_members_count(),
|
||||
invited_members_count: room.invited_members_count(),
|
||||
joined_members_count: room.joined_members_count(),
|
||||
service_members: room
|
||||
.service_members()
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|m| m.to_string())
|
||||
.collect(),
|
||||
highlight_count: unread_notification_counts.highlight_count,
|
||||
notification_count: unread_notification_counts.notification_count,
|
||||
cached_user_defined_notification_mode: room
|
||||
.cached_user_defined_notification_mode()
|
||||
.map(Into::into),
|
||||
has_room_call: room.has_active_room_call(),
|
||||
active_room_call_participants: room
|
||||
.active_room_call_participants()
|
||||
.iter()
|
||||
.map(|u| u.to_string())
|
||||
.collect(),
|
||||
active_room_call_consensus_intent: room.active_room_call_consensus_intent().into(),
|
||||
is_marked_unread: room.is_marked_unread(),
|
||||
num_unread_messages: room.num_unread_messages(),
|
||||
num_unread_notifications: room.num_unread_notifications(),
|
||||
num_unread_mentions: room.num_unread_mentions(),
|
||||
pinned_event_ids,
|
||||
join_rule,
|
||||
history_visibility: room.history_visibility_or_default().try_into()?,
|
||||
power_levels: power_levels.map(Arc::new),
|
||||
room_version: room.version().map(|version| version.to_string()),
|
||||
privileged_creators_role: room
|
||||
.version()
|
||||
.and_then(|version| version.rules())
|
||||
.map(|rules| rules.authorization.explicitly_privilege_room_creators)
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk::RoomDisplayName;
|
||||
|
||||
/// Verifies the passed `String` matches the expected room alias format:
|
||||
///
|
||||
/// This means it's lowercase, with no whitespace chars, has a single leading
|
||||
/// `#` char and a single `:` separator between the local and domain parts, and
|
||||
/// the local part only contains characters that can't be percent encoded.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
fn is_room_alias_format_valid(alias: String) -> bool {
|
||||
matrix_sdk::utils::is_room_alias_format_valid(alias)
|
||||
}
|
||||
|
||||
/// Transforms a Room's display name into a valid room alias name.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
fn room_alias_name_from_room_display_name(room_name: String) -> String {
|
||||
RoomDisplayName::Named(room_name).to_room_alias_name()
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::room_directory_search::RoomDirectorySearch as SdkRoomDirectorySearch;
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::ServerName;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{error::ClientError, runtime::get_runtime_handle, task_handle::TaskHandle};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum PublicRoomJoinRule {
|
||||
Public,
|
||||
Knock,
|
||||
Restricted,
|
||||
KnockRestricted,
|
||||
Invite,
|
||||
}
|
||||
|
||||
impl TryFrom<ruma::room::JoinRuleKind> for PublicRoomJoinRule {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: ruma::room::JoinRuleKind) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
ruma::room::JoinRuleKind::Public => Ok(Self::Public),
|
||||
ruma::room::JoinRuleKind::Knock => Ok(Self::Knock),
|
||||
ruma::room::JoinRuleKind::Restricted => Ok(Self::Restricted),
|
||||
ruma::room::JoinRuleKind::KnockRestricted => Ok(Self::KnockRestricted),
|
||||
ruma::room::JoinRuleKind::Invite => Ok(Self::Invite),
|
||||
rule => Err(format!("unsupported join rule: {rule:?}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomDescription {
|
||||
pub room_id: String,
|
||||
pub name: Option<String>,
|
||||
pub topic: Option<String>,
|
||||
pub alias: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
pub join_rule: Option<PublicRoomJoinRule>,
|
||||
pub is_world_readable: bool,
|
||||
pub joined_members: u64,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::room_directory_search::RoomDescription> for RoomDescription {
|
||||
fn from(value: matrix_sdk::room_directory_search::RoomDescription) -> Self {
|
||||
Self {
|
||||
room_id: value.room_id.to_string(),
|
||||
name: value.name,
|
||||
topic: value.topic,
|
||||
alias: value.alias.map(|alias| alias.to_string()),
|
||||
avatar_url: value.avatar_url.map(|url| url.to_string()),
|
||||
join_rule: value.join_rule.try_into().ok(),
|
||||
is_world_readable: value.is_world_readable,
|
||||
joined_members: value.joined_members,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper for performing room searches in the room directory.
|
||||
/// The way this is intended to be used is:
|
||||
///
|
||||
/// 1. Register a callback using [`RoomDirectorySearch::results`].
|
||||
/// 2. Start the room search with [`RoomDirectorySearch::search`].
|
||||
/// 3. To get more results, use [`RoomDirectorySearch::next_page`].
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomDirectorySearch {
|
||||
pub(crate) inner: RwLock<SdkRoomDirectorySearch>,
|
||||
}
|
||||
|
||||
impl RoomDirectorySearch {
|
||||
pub fn new(inner: SdkRoomDirectorySearch) -> Self {
|
||||
Self { inner: RwLock::new(inner) }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomDirectorySearch {
|
||||
/// Asks the server for the next page of the current search.
|
||||
pub async fn next_page(&self) -> Result<(), ClientError> {
|
||||
let mut inner = self.inner.write().await;
|
||||
inner.next_page().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Starts a filtered search for the server.
|
||||
///
|
||||
/// If the `filter` is not provided it will search for all the rooms.
|
||||
/// You can specify a `batch_size` to control the number of rooms to fetch
|
||||
/// per request.
|
||||
///
|
||||
/// If the `via_server` is not provided it will search in the current
|
||||
/// homeserver by default.
|
||||
///
|
||||
/// This method will clear the current search results and start a new one.
|
||||
pub async fn search(
|
||||
&self,
|
||||
filter: Option<String>,
|
||||
batch_size: u32,
|
||||
via_server_name: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let server = via_server_name.map(ServerName::parse).transpose()?;
|
||||
let mut inner = self.inner.write().await;
|
||||
inner.search(filter, batch_size, server).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the number of pages that have been loaded so far.
|
||||
pub async fn loaded_pages(&self) -> Result<u32, ClientError> {
|
||||
let inner = self.inner.read().await;
|
||||
Ok(inner.loaded_pages() as u32)
|
||||
}
|
||||
|
||||
/// Get whether the search is at the last page.
|
||||
pub async fn is_at_last_page(&self) -> Result<bool, ClientError> {
|
||||
let inner = self.inner.read().await;
|
||||
Ok(inner.is_at_last_page())
|
||||
}
|
||||
|
||||
/// Registers a callback to receive new search results when starting a
|
||||
/// search or getting new paginated results.
|
||||
pub async fn results(
|
||||
&self,
|
||||
listener: Box<dyn RoomDirectorySearchEntriesListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let (initial_values, mut stream) = self.inner.read().await.results();
|
||||
|
||||
listener.on_update(vec![RoomDirectorySearchEntryUpdate::Reset {
|
||||
values: initial_values.into_iter().map(Into::into).collect(),
|
||||
}]);
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(diffs) = stream.next().await {
|
||||
listener.on_update(diffs.into_iter().map(|diff| diff.into()).collect());
|
||||
}
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RoomDirectorySearchEntryUpdate {
|
||||
Append { values: Vec<RoomDescription> },
|
||||
Clear,
|
||||
PushFront { value: RoomDescription },
|
||||
PushBack { value: RoomDescription },
|
||||
PopFront,
|
||||
PopBack,
|
||||
Insert { index: u32, value: RoomDescription },
|
||||
Set { index: u32, value: RoomDescription },
|
||||
Remove { index: u32 },
|
||||
Truncate { length: u32 },
|
||||
Reset { values: Vec<RoomDescription> },
|
||||
}
|
||||
|
||||
impl From<VectorDiff<matrix_sdk::room_directory_search::RoomDescription>>
|
||||
for RoomDirectorySearchEntryUpdate
|
||||
{
|
||||
fn from(diff: VectorDiff<matrix_sdk::room_directory_search::RoomDescription>) -> Self {
|
||||
match diff {
|
||||
VectorDiff::Append { values } => {
|
||||
Self::Append { values: values.into_iter().map(|v| v.into()).collect() }
|
||||
}
|
||||
VectorDiff::Clear => Self::Clear,
|
||||
VectorDiff::PushFront { value } => Self::PushFront { value: value.into() },
|
||||
VectorDiff::PushBack { value } => Self::PushBack { value: value.into() },
|
||||
VectorDiff::PopFront => Self::PopFront,
|
||||
VectorDiff::PopBack => Self::PopBack,
|
||||
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::Truncate { length } => Self::Truncate { length: length as u32 },
|
||||
VectorDiff::Reset { values } => {
|
||||
Self::Reset { values: values.into_iter().map(|v| v.into()).collect() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomDirectorySearchEntriesListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
fn on_update(&self, room_entries_update: Vec<RoomDirectorySearchEntryUpdate>);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk::RoomState;
|
||||
use ruma::OwnedMxcUri;
|
||||
|
||||
use crate::{
|
||||
notification_settings::RoomNotificationMode, room::Membership, room_member::RoomMember,
|
||||
timeline::EventTimelineItem,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomInfo {
|
||||
id: String,
|
||||
name: Option<String>,
|
||||
topic: Option<String>,
|
||||
avatar_url: Option<String>,
|
||||
is_direct: bool,
|
||||
is_public: bool,
|
||||
is_space: bool,
|
||||
is_tombstoned: bool,
|
||||
canonical_alias: Option<String>,
|
||||
alternative_aliases: Vec<String>,
|
||||
membership: Membership,
|
||||
latest_event: Option<Arc<EventTimelineItem>>,
|
||||
inviter: Option<Arc<RoomMember>>,
|
||||
active_members_count: u64,
|
||||
invited_members_count: u64,
|
||||
joined_members_count: u64,
|
||||
highlight_count: u64,
|
||||
notification_count: u64,
|
||||
user_defined_notification_mode: Option<RoomNotificationMode>,
|
||||
has_room_call: bool,
|
||||
active_room_call_participants: Vec<String>,
|
||||
/// "Interesting" messages received in that room, independently of the
|
||||
/// notification settings.
|
||||
num_unread_messages: u64,
|
||||
/// Events that will notify the user, according to their
|
||||
/// notification settings.
|
||||
num_unread_notifications: u64,
|
||||
/// Events causing mentions/highlights for the user, according to their
|
||||
/// notification settings.
|
||||
num_unread_mentions: u64,
|
||||
}
|
||||
|
||||
impl RoomInfo {
|
||||
pub(crate) async fn new(
|
||||
room: &matrix_sdk::Room,
|
||||
avatar_url: Option<OwnedMxcUri>,
|
||||
latest_event: Option<Arc<EventTimelineItem>>,
|
||||
) -> matrix_sdk::Result<Self> {
|
||||
let unread_notification_counts = room.unread_notification_counts();
|
||||
|
||||
Ok(Self {
|
||||
id: room.room_id().to_string(),
|
||||
name: room.name(),
|
||||
topic: room.topic(),
|
||||
avatar_url: avatar_url.map(Into::into),
|
||||
is_direct: room.is_direct().await?,
|
||||
is_public: room.is_public(),
|
||||
is_space: room.is_space(),
|
||||
is_tombstoned: room.is_tombstoned(),
|
||||
canonical_alias: room.canonical_alias().map(Into::into),
|
||||
alternative_aliases: room.alt_aliases().into_iter().map(Into::into).collect(),
|
||||
membership: room.state().into(),
|
||||
latest_event,
|
||||
inviter: match room.state() {
|
||||
RoomState::Invited => {
|
||||
room.invite_details().await?.inviter.map(|inner| Arc::new(RoomMember { inner }))
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
active_members_count: room.active_members_count(),
|
||||
invited_members_count: room.invited_members_count(),
|
||||
joined_members_count: room.joined_members_count(),
|
||||
highlight_count: unread_notification_counts.highlight_count,
|
||||
notification_count: unread_notification_counts.notification_count,
|
||||
user_defined_notification_mode: room
|
||||
.user_defined_notification_mode()
|
||||
.await
|
||||
.map(Into::into),
|
||||
has_room_call: room.has_active_room_call(),
|
||||
active_room_call_participants: room
|
||||
.active_room_call_participants()
|
||||
.iter()
|
||||
.map(|u| u.to_string())
|
||||
.collect(),
|
||||
num_unread_messages: room.num_unread_messages(),
|
||||
num_unread_notifications: room.num_unread_notifications(),
|
||||
num_unread_mentions: room.num_unread_mentions(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,29 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#![allow(deprecated)]
|
||||
|
||||
use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Duration};
|
||||
use std::{fmt::Debug, sync::Arc, time::Duration};
|
||||
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{StreamExt, pin_mut};
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use matrix_sdk::{
|
||||
Room as SdkRoom,
|
||||
ruma::{
|
||||
RoomId,
|
||||
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
api::client::sync::sync_events::{
|
||||
v4::RoomSubscription as RumaRoomSubscription,
|
||||
UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
},
|
||||
assign, RoomId,
|
||||
},
|
||||
RoomListEntry as MatrixRoomListEntry,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::{
|
||||
room_list_service::filters::{
|
||||
BoxedFilterFn, RoomCategory, new_filter_all, new_filter_any, new_filter_category,
|
||||
new_filter_deduplicate_versions, new_filter_favourite, new_filter_fuzzy_match_room_name,
|
||||
new_filter_identifiers, new_filter_invite, new_filter_joined, new_filter_low_priority,
|
||||
new_filter_non_left, new_filter_none, new_filter_normalized_match_room_name,
|
||||
new_filter_not, new_filter_space, new_filter_unread,
|
||||
},
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
use matrix_sdk_ui::room_list_service::filters::{
|
||||
new_filter_all, new_filter_all_non_left, new_filter_fuzzy_match_room_name, new_filter_none,
|
||||
new_filter_normalized_match_room_name,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
TaskHandle,
|
||||
room::{Membership, Room},
|
||||
runtime::get_runtime_handle,
|
||||
error::ClientError,
|
||||
room::Room,
|
||||
room_info::RoomInfo,
|
||||
timeline::{EventTimelineItem, Timeline},
|
||||
TaskHandle, RUNTIME,
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
@@ -55,13 +38,6 @@ pub enum RoomListError {
|
||||
RoomNotFound { room_name: String },
|
||||
#[error("invalid room ID: {error}")]
|
||||
InvalidRoomId { error: String },
|
||||
#[error("Event cache ran into an error: {error}")]
|
||||
EventCache { error: String },
|
||||
#[error(
|
||||
"The requested room doesn't match the membership requirements {expected:?}, \
|
||||
observed {actual:?}"
|
||||
)]
|
||||
IncorrectRoomMembership { expected: Vec<Membership>, actual: Membership },
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_ui::room_list_service::Error> for RoomListError {
|
||||
@@ -71,8 +47,8 @@ impl From<matrix_sdk_ui::room_list_service::Error> for RoomListError {
|
||||
match value {
|
||||
SlidingSync(error) => Self::SlidingSync { error: error.to_string() },
|
||||
UnknownList(list_name) => Self::UnknownList { list_name },
|
||||
InputCannotBeApplied(_) => Self::InputCannotBeApplied,
|
||||
RoomNotFound(room_id) => Self::RoomNotFound { room_name: room_id.to_string() },
|
||||
EventCache(error) => Self::EventCache { error: error.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,18 +59,38 @@ impl From<ruma::IdParseError> for RoomListError {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomListRange {
|
||||
pub start: u32,
|
||||
pub end_inclusive: u32,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RoomListInput {
|
||||
Viewport { ranges: Vec<RoomListRange> },
|
||||
}
|
||||
|
||||
impl From<RoomListInput> for matrix_sdk_ui::room_list_service::Input {
|
||||
fn from(value: RoomListInput) -> Self {
|
||||
match value {
|
||||
RoomListInput::Viewport { ranges } => Self::Viewport(
|
||||
ranges.iter().map(|range| range.start..=range.end_inclusive).collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomListService {
|
||||
pub(crate) inner: Arc<matrix_sdk_ui::RoomListService>,
|
||||
pub(crate) utd_hook: Option<Arc<UtdHookManager>>,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl RoomListService {
|
||||
fn state(&self, listener: Box<dyn RoomListServiceStateListener>) -> Arc<TaskHandle> {
|
||||
let state_stream = self.inner.state();
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
pin_mut!(state_stream);
|
||||
|
||||
while let Some(state) = state_stream.next().await {
|
||||
@@ -103,10 +99,12 @@ impl RoomListService {
|
||||
})))
|
||||
}
|
||||
|
||||
fn room(&self, room_id: String) -> Result<Arc<Room>, RoomListError> {
|
||||
fn room(&self, room_id: String) -> Result<Arc<RoomListItem>, RoomListError> {
|
||||
let room_id = <&RoomId>::try_from(room_id.as_str()).map_err(RoomListError::from)?;
|
||||
|
||||
Ok(Arc::new(Room::new(self.inner.room(room_id)?, self.utd_hook.clone())))
|
||||
Ok(Arc::new(RoomListItem {
|
||||
inner: Arc::new(RUNTIME.block_on(async { self.inner.room(room_id).await })?),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn all_rooms(self: Arc<Self>) -> Result<Arc<RoomList>, RoomListError> {
|
||||
@@ -116,6 +114,17 @@ impl RoomListService {
|
||||
}))
|
||||
}
|
||||
|
||||
async fn invites(self: Arc<Self>) -> Result<Arc<RoomList>, RoomListError> {
|
||||
Ok(Arc::new(RoomList {
|
||||
room_list_service: self.clone(),
|
||||
inner: Arc::new(self.inner.invites().await.map_err(RoomListError::from)?),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn apply_input(&self, input: RoomListInput) -> Result<(), RoomListError> {
|
||||
self.inner.apply_input(input.into()).await.map(|_| ()).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn sync_indicator(
|
||||
&self,
|
||||
delay_before_showing_in_ms: u32,
|
||||
@@ -127,7 +136,7 @@ impl RoomListService {
|
||||
Duration::from_millis(delay_before_hiding_in_ms.into()),
|
||||
);
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
pin_mut!(sync_indicator_stream);
|
||||
|
||||
while let Some(sync_indicator) = sync_indicator_stream.next().await {
|
||||
@@ -135,21 +144,6 @@ impl RoomListService {
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
async fn subscribe_to_rooms(&self, room_ids: Vec<String>) -> Result<(), RoomListError> {
|
||||
let room_ids = room_ids
|
||||
.into_iter()
|
||||
.map(|room_id| {
|
||||
RoomId::parse(&room_id).map_err(|_| RoomListError::InvalidRoomId { error: room_id })
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
self.inner
|
||||
.subscribe_to_rooms(&room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
@@ -158,7 +152,7 @@ pub struct RoomList {
|
||||
inner: Arc<matrix_sdk_ui::room_list_service::RoomList>,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
impl RoomList {
|
||||
fn loading_state(
|
||||
&self,
|
||||
@@ -168,7 +162,7 @@ impl RoomList {
|
||||
|
||||
Ok(RoomListLoadingStateResult {
|
||||
state: loading_state.get().into(),
|
||||
state_stream: Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
state_stream: Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
pin_mut!(loading_state);
|
||||
|
||||
while let Some(loading_state) = loading_state.next().await {
|
||||
@@ -178,126 +172,59 @@ impl RoomList {
|
||||
})
|
||||
}
|
||||
|
||||
fn entries_with_dynamic_adapters(
|
||||
self: Arc<Self>,
|
||||
page_size: u32,
|
||||
listener: Box<dyn RoomListEntriesListener>,
|
||||
) -> Arc<RoomListEntriesWithDynamicAdaptersResult> {
|
||||
let this = self;
|
||||
fn entries(&self, listener: Box<dyn RoomListEntriesListener>) -> RoomListEntriesResult {
|
||||
let (entries, entries_stream) = self.inner.entries();
|
||||
|
||||
// The following code deserves a bit of explanation.
|
||||
// `matrix_sdk_ui::room_list_service::RoomList::entries_with_dynamic_adapters`
|
||||
// returns a `Stream` with a lifetime bounds to its `self` (`RoomList`). This is
|
||||
// problematic here as this `Stream` is returned as part of
|
||||
// `RoomListEntriesWithDynamicAdaptersResult` but it is not possible to store
|
||||
// `RoomList` with it inside the `Future` that is run inside the `TaskHandle`
|
||||
// that consumes this `Stream`. We have a lifetime issue: `RoomList` doesn't
|
||||
// live long enough!
|
||||
//
|
||||
// To solve this issue, the trick is to store the `RoomList` inside the
|
||||
// `RoomListEntriesWithDynamicAdaptersResult`. Alright, but then we have another
|
||||
// lifetime issue! `RoomList` cannot move inside this struct because it is
|
||||
// borrowed by `entries_with_dynamic_adapters`. Indeed, the struct is built
|
||||
// after the `Stream` is obtained.
|
||||
//
|
||||
// To solve this issue, we need to build the struct field by field, starting
|
||||
// with `this`, and use a reference to `this` to call
|
||||
// `entries_with_dynamic_adapters`. This is unsafe because a couple of
|
||||
// invariants must hold, but all this is legal and correct if the invariants are
|
||||
// properly fulfilled.
|
||||
RoomListEntriesResult {
|
||||
entries: entries.into_iter().map(Into::into).collect(),
|
||||
entries_stream: Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
pin_mut!(entries_stream);
|
||||
|
||||
// Create the struct result with uninitialized fields.
|
||||
let mut result = MaybeUninit::<RoomListEntriesWithDynamicAdaptersResult>::uninit();
|
||||
let ptr = result.as_mut_ptr();
|
||||
|
||||
// Initialize the first field `this`.
|
||||
//
|
||||
// SAFETY: `ptr` is correctly aligned, this is guaranteed by `MaybeUninit`.
|
||||
unsafe {
|
||||
addr_of_mut!((*ptr).this).write(this);
|
||||
while let Some(diff) = entries_stream.next().await {
|
||||
listener.on_update(diff.into_iter().map(Into::into).collect());
|
||||
}
|
||||
}))),
|
||||
}
|
||||
|
||||
// Get a reference to `this`. It is only borrowed, it's not moved.
|
||||
let this =
|
||||
// SAFETY: `ptr` is correctly aligned, the `this` field is correctly aligned,
|
||||
// is dereferenceable and points to a correctly initialized value as done
|
||||
// in the previous line.
|
||||
unsafe { addr_of_mut!((*ptr).this).as_ref() }
|
||||
// SAFETY: `this` contains a non null value.
|
||||
.unwrap();
|
||||
|
||||
// Now we can create `entries_stream` and `dynamic_entries_controller` by
|
||||
// borrowing `this`, which is going to live long enough since it will live as
|
||||
// long as `entries_stream` and `dynamic_entries_controller`.
|
||||
let (entries_stream, dynamic_entries_controller) =
|
||||
this.inner.entries_with_dynamic_adapters(page_size.try_into().unwrap());
|
||||
|
||||
// FFI dance to make those values consumable by foreign language, nothing fancy
|
||||
// here, that's the real code for this method.
|
||||
let dynamic_entries_controller =
|
||||
Arc::new(RoomListDynamicEntriesController::new(dynamic_entries_controller));
|
||||
|
||||
let utd_hook = this.room_list_service.utd_hook.clone();
|
||||
let entries_stream = Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(entries_stream);
|
||||
|
||||
while let Some(diffs) = entries_stream.next().await {
|
||||
listener.on_update(
|
||||
diffs
|
||||
.into_iter()
|
||||
.map(|diff| {
|
||||
RoomListEntriesUpdate::from(
|
||||
utd_hook.clone(),
|
||||
diff.map(|room| room.into_inner()),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
})));
|
||||
|
||||
// Initialize the second field `controller`.
|
||||
//
|
||||
// SAFETY: `ptr` is correctly aligned.
|
||||
unsafe {
|
||||
addr_of_mut!((*ptr).controller).write(dynamic_entries_controller);
|
||||
}
|
||||
|
||||
// Initialize the third and last field `entries_stream`.
|
||||
//
|
||||
// SAFETY: `ptr` is correctly aligned.
|
||||
unsafe {
|
||||
addr_of_mut!((*ptr).entries_stream).write(entries_stream);
|
||||
}
|
||||
|
||||
// The result is complete, let's return it!
|
||||
//
|
||||
// SAFETY: `result` is fully initialized, all its fields have received a valid
|
||||
// value.
|
||||
Arc::new(unsafe { result.assume_init() })
|
||||
}
|
||||
|
||||
fn room(&self, room_id: String) -> Result<Arc<Room>, RoomListError> {
|
||||
fn entries_with_dynamic_adapters(
|
||||
&self,
|
||||
page_size: u32,
|
||||
listener: Box<dyn RoomListEntriesListener>,
|
||||
) -> RoomListEntriesWithDynamicAdaptersResult {
|
||||
let (entries_stream, dynamic_entries_controller) =
|
||||
self.inner.entries_with_dynamic_adapters(page_size.try_into().unwrap());
|
||||
|
||||
RoomListEntriesWithDynamicAdaptersResult {
|
||||
controller: Arc::new(RoomListDynamicEntriesController::new(
|
||||
dynamic_entries_controller,
|
||||
self.room_list_service.inner.client(),
|
||||
)),
|
||||
entries_stream: Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
pin_mut!(entries_stream);
|
||||
|
||||
while let Some(diff) = entries_stream.next().await {
|
||||
listener.on_update(diff.into_iter().map(Into::into).collect());
|
||||
}
|
||||
}))),
|
||||
}
|
||||
}
|
||||
|
||||
fn room(&self, room_id: String) -> Result<Arc<RoomListItem>, RoomListError> {
|
||||
self.room_list_service.room(room_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomListEntriesWithDynamicAdaptersResult {
|
||||
this: Arc<RoomList>,
|
||||
controller: Arc<RoomListDynamicEntriesController>,
|
||||
entries_stream: Arc<TaskHandle>,
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomListEntriesResult {
|
||||
pub entries: Vec<RoomListEntry>,
|
||||
pub entries_stream: Arc<TaskHandle>,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomListEntriesWithDynamicAdaptersResult {
|
||||
fn controller(&self) -> Arc<RoomListDynamicEntriesController> {
|
||||
self.controller.clone()
|
||||
}
|
||||
|
||||
fn entries_stream(&self) -> Arc<TaskHandle> {
|
||||
self.entries_stream.clone()
|
||||
}
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomListEntriesWithDynamicAdaptersResult {
|
||||
pub controller: Arc<RoomListDynamicEntriesController>,
|
||||
pub entries_stream: Arc<TaskHandle>,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
@@ -367,98 +294,100 @@ impl From<matrix_sdk_ui::room_list_service::RoomListLoadingState> for RoomListLo
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomListServiceStateListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait RoomListServiceStateListener: Send + Sync + Debug {
|
||||
fn on_update(&self, state: RoomListServiceState);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomListLoadingStateListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait RoomListLoadingStateListener: Send + Sync + Debug {
|
||||
fn on_update(&self, state: RoomListLoadingState);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomListServiceSyncIndicatorListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait RoomListServiceSyncIndicatorListener: Send + Sync + Debug {
|
||||
fn on_update(&self, sync_indicator: RoomListServiceSyncIndicator);
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RoomListEntriesUpdate {
|
||||
Append { values: Vec<Arc<Room>> },
|
||||
Append { values: Vec<RoomListEntry> },
|
||||
Clear,
|
||||
PushFront { value: Arc<Room> },
|
||||
PushBack { value: Arc<Room> },
|
||||
PushFront { value: RoomListEntry },
|
||||
PushBack { value: RoomListEntry },
|
||||
PopFront,
|
||||
PopBack,
|
||||
Insert { index: u32, value: Arc<Room> },
|
||||
Set { index: u32, value: Arc<Room> },
|
||||
Insert { index: u32, value: RoomListEntry },
|
||||
Set { index: u32, value: RoomListEntry },
|
||||
Remove { index: u32 },
|
||||
Truncate { length: u32 },
|
||||
Reset { values: Vec<Arc<Room>> },
|
||||
Reset { values: Vec<RoomListEntry> },
|
||||
}
|
||||
|
||||
impl RoomListEntriesUpdate {
|
||||
fn from(utd_hook: Option<Arc<UtdHookManager>>, vector_diff: VectorDiff<SdkRoom>) -> Self {
|
||||
match vector_diff {
|
||||
VectorDiff::Append { values } => Self::Append {
|
||||
values: values
|
||||
.into_iter()
|
||||
.map(|value| Arc::new(Room::new(value, utd_hook.clone())))
|
||||
.collect(),
|
||||
},
|
||||
impl From<VectorDiff<matrix_sdk::RoomListEntry>> for RoomListEntriesUpdate {
|
||||
fn from(other: VectorDiff<matrix_sdk::RoomListEntry>) -> Self {
|
||||
match other {
|
||||
VectorDiff::Append { values } => {
|
||||
Self::Append { values: values.into_iter().map(Into::into).collect() }
|
||||
}
|
||||
VectorDiff::Clear => Self::Clear,
|
||||
VectorDiff::PushFront { value } => {
|
||||
Self::PushFront { value: Arc::new(Room::new(value, utd_hook)) }
|
||||
}
|
||||
VectorDiff::PushBack { value } => {
|
||||
Self::PushBack { value: Arc::new(Room::new(value, utd_hook)) }
|
||||
}
|
||||
VectorDiff::PushFront { value } => Self::PushFront { value: value.into() },
|
||||
VectorDiff::PushBack { value } => Self::PushBack { value: value.into() },
|
||||
VectorDiff::PopFront => Self::PopFront,
|
||||
VectorDiff::PopBack => Self::PopBack,
|
||||
VectorDiff::Insert { index, value } => Self::Insert {
|
||||
index: u32::try_from(index).unwrap(),
|
||||
value: Arc::new(Room::new(value, utd_hook)),
|
||||
},
|
||||
VectorDiff::Set { index, value } => Self::Set {
|
||||
index: u32::try_from(index).unwrap(),
|
||||
value: Arc::new(Room::new(value, utd_hook)),
|
||||
},
|
||||
VectorDiff::Insert { index, value } => {
|
||||
Self::Insert { index: u32::try_from(index).unwrap(), value: value.into() }
|
||||
}
|
||||
VectorDiff::Set { index, value } => {
|
||||
Self::Set { index: u32::try_from(index).unwrap(), value: value.into() }
|
||||
}
|
||||
VectorDiff::Remove { index } => Self::Remove { index: u32::try_from(index).unwrap() },
|
||||
VectorDiff::Truncate { length } => {
|
||||
Self::Truncate { length: u32::try_from(length).unwrap() }
|
||||
}
|
||||
VectorDiff::Reset { values } => Self::Reset {
|
||||
values: values
|
||||
.into_iter()
|
||||
.map(|value| Arc::new(Room::new(value, utd_hook.clone())))
|
||||
.collect(),
|
||||
},
|
||||
VectorDiff::Reset { values } => {
|
||||
Self::Reset { values: values.into_iter().map(Into::into).collect() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomListEntriesListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait RoomListEntriesListener: Send + Sync + Debug {
|
||||
fn on_update(&self, room_entries_update: Vec<RoomListEntriesUpdate>);
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomListDynamicEntriesController {
|
||||
inner: matrix_sdk_ui::room_list_service::RoomListDynamicEntriesController,
|
||||
client: matrix_sdk::Client,
|
||||
}
|
||||
|
||||
impl RoomListDynamicEntriesController {
|
||||
fn new(
|
||||
dynamic_entries_controller: matrix_sdk_ui::room_list_service::RoomListDynamicEntriesController,
|
||||
client: &matrix_sdk::Client,
|
||||
) -> Self {
|
||||
Self { inner: dynamic_entries_controller }
|
||||
Self { inner: dynamic_entries_controller, client: client.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
impl RoomListDynamicEntriesController {
|
||||
fn set_filter(&self, kind: RoomListEntriesDynamicFilterKind) -> bool {
|
||||
self.inner.set_filter(kind.into())
|
||||
use RoomListEntriesDynamicFilterKind as Kind;
|
||||
|
||||
match kind {
|
||||
Kind::All => self.inner.set_filter(new_filter_all()),
|
||||
Kind::AllNonLeft => self.inner.set_filter(new_filter_all_non_left(&self.client)),
|
||||
Kind::None => self.inner.set_filter(new_filter_none()),
|
||||
Kind::NormalizedMatchRoomName { pattern } => {
|
||||
self.inner.set_filter(new_filter_normalized_match_room_name(&self.client, &pattern))
|
||||
}
|
||||
Kind::FuzzyMatchRoomName { pattern } => {
|
||||
self.inner.set_filter(new_filter_fuzzy_match_room_name(&self.client, &pattern))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_one_page(&self) {
|
||||
@@ -472,77 +401,128 @@ impl RoomListDynamicEntriesController {
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RoomListEntriesDynamicFilterKind {
|
||||
All { filters: Vec<RoomListEntriesDynamicFilterKind> },
|
||||
Any { filters: Vec<RoomListEntriesDynamicFilterKind> },
|
||||
Identifiers { identifiers: Vec<String> },
|
||||
NonSpace,
|
||||
Space,
|
||||
NonLeft,
|
||||
// Not { filter: RoomListEntriesDynamicFilterKind } - requires recursive enum
|
||||
// support in uniffi https://github.com/mozilla/uniffi-rs/issues/396
|
||||
Joined,
|
||||
Unread,
|
||||
Favourite,
|
||||
LowPriority,
|
||||
NonLowPriority,
|
||||
NonFavorite,
|
||||
Invite,
|
||||
Category { expect: RoomListFilterCategory },
|
||||
All,
|
||||
AllNonLeft,
|
||||
None,
|
||||
NormalizedMatchRoomName { pattern: String },
|
||||
FuzzyMatchRoomName { pattern: String },
|
||||
DeduplicateVersions,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RoomListFilterCategory {
|
||||
Group,
|
||||
People,
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomListItem {
|
||||
inner: Arc<matrix_sdk_ui::room_list_service::Room>,
|
||||
}
|
||||
|
||||
impl From<RoomListFilterCategory> for RoomCategory {
|
||||
fn from(value: RoomListFilterCategory) -> Self {
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl RoomListItem {
|
||||
fn id(&self) -> String {
|
||||
self.inner.id().to_string()
|
||||
}
|
||||
|
||||
fn name(&self) -> Option<String> {
|
||||
RUNTIME.block_on(async { self.inner.name().await })
|
||||
}
|
||||
|
||||
fn avatar_url(&self) -> Option<String> {
|
||||
self.inner.avatar_url().map(|uri| uri.to_string())
|
||||
}
|
||||
|
||||
fn is_direct(&self) -> bool {
|
||||
RUNTIME.block_on(async { self.inner.inner_room().is_direct().await.unwrap_or(false) })
|
||||
}
|
||||
|
||||
fn canonical_alias(&self) -> Option<String> {
|
||||
self.inner.inner_room().canonical_alias().map(|alias| alias.to_string())
|
||||
}
|
||||
|
||||
pub async fn room_info(&self) -> Result<RoomInfo, ClientError> {
|
||||
let avatar_url = self.inner.avatar_url();
|
||||
let latest_event = self.inner.latest_event().await.map(EventTimelineItem).map(Arc::new);
|
||||
Ok(RoomInfo::new(self.inner.inner_room(), avatar_url, latest_event).await?)
|
||||
}
|
||||
|
||||
/// Building a `Room`.
|
||||
///
|
||||
/// Be careful that building a `Room` builds its entire `Timeline` at the
|
||||
/// same time.
|
||||
async fn full_room(&self) -> Arc<Room> {
|
||||
Arc::new(Room::with_timeline(
|
||||
self.inner.inner_room().clone(),
|
||||
Arc::new(RwLock::new(Some(Timeline::from_arc(self.inner.timeline().await)))),
|
||||
))
|
||||
}
|
||||
|
||||
// Temporary workaround for coroutine leaks on Kotlin.
|
||||
fn full_room_blocking(&self) -> Arc<Room> {
|
||||
RUNTIME.block_on(async move { self.full_room().await })
|
||||
}
|
||||
|
||||
fn subscribe(&self, settings: Option<RoomSubscription>) {
|
||||
self.inner.subscribe(settings.map(Into::into));
|
||||
}
|
||||
|
||||
fn unsubscribe(&self) {
|
||||
self.inner.unsubscribe();
|
||||
}
|
||||
|
||||
async fn latest_event(&self) -> Option<Arc<EventTimelineItem>> {
|
||||
self.inner.latest_event().await.map(EventTimelineItem).map(Arc::new)
|
||||
}
|
||||
|
||||
fn has_unread_notifications(&self) -> bool {
|
||||
self.inner.has_unread_notifications()
|
||||
}
|
||||
|
||||
fn unread_notifications(&self) -> Arc<UnreadNotificationsCount> {
|
||||
Arc::new(self.inner.unread_notifications().into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, uniffi::Enum)]
|
||||
pub enum RoomListEntry {
|
||||
Empty,
|
||||
Invalidated { room_id: String },
|
||||
Filled { room_id: String },
|
||||
}
|
||||
|
||||
impl From<MatrixRoomListEntry> for RoomListEntry {
|
||||
fn from(value: MatrixRoomListEntry) -> Self {
|
||||
(&value).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&MatrixRoomListEntry> for RoomListEntry {
|
||||
fn from(value: &MatrixRoomListEntry) -> Self {
|
||||
match value {
|
||||
RoomListFilterCategory::Group => Self::Group,
|
||||
RoomListFilterCategory::People => Self::People,
|
||||
MatrixRoomListEntry::Empty => Self::Empty,
|
||||
MatrixRoomListEntry::Filled(room_id) => Self::Filled { room_id: room_id.to_string() },
|
||||
MatrixRoomListEntry::Invalidated(room_id) => {
|
||||
Self::Invalidated { room_id: room_id.to_string() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomListEntriesDynamicFilterKind> for BoxedFilterFn {
|
||||
fn from(value: RoomListEntriesDynamicFilterKind) -> Self {
|
||||
use RoomListEntriesDynamicFilterKind as Kind;
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RequiredState {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
match value {
|
||||
Kind::All { filters } => Box::new(new_filter_all(
|
||||
filters.into_iter().map(|filter| BoxedFilterFn::from(filter)).collect(),
|
||||
)),
|
||||
Kind::Any { filters } => Box::new(new_filter_any(
|
||||
filters.into_iter().map(|filter| BoxedFilterFn::from(filter)).collect(),
|
||||
)),
|
||||
Kind::Identifiers { identifiers } => Box::new(new_filter_identifiers(
|
||||
identifiers.into_iter().map(|id| RoomId::parse(id).unwrap()).collect(),
|
||||
)),
|
||||
Kind::NonSpace => Box::new(new_filter_not(Box::new(new_filter_space()))),
|
||||
Kind::Space => Box::new(new_filter_space()),
|
||||
Kind::NonLeft => Box::new(new_filter_non_left()),
|
||||
Kind::Joined => Box::new(new_filter_joined()),
|
||||
Kind::Unread => Box::new(new_filter_unread()),
|
||||
Kind::Favourite => Box::new(new_filter_favourite()),
|
||||
Kind::LowPriority => Box::new(new_filter_low_priority()),
|
||||
Kind::NonLowPriority => Box::new(new_filter_not(Box::new(new_filter_low_priority()))),
|
||||
Kind::NonFavorite => Box::new(new_filter_not(Box::new(new_filter_favourite()))),
|
||||
Kind::Invite => Box::new(new_filter_invite()),
|
||||
Kind::Category { expect } => Box::new(new_filter_category(expect.into())),
|
||||
Kind::None => Box::new(new_filter_none()),
|
||||
Kind::NormalizedMatchRoomName { pattern } => {
|
||||
Box::new(new_filter_normalized_match_room_name(&pattern))
|
||||
}
|
||||
Kind::FuzzyMatchRoomName { pattern } => {
|
||||
Box::new(new_filter_fuzzy_match_room_name(&pattern))
|
||||
}
|
||||
Kind::DeduplicateVersions => Box::new(new_filter_deduplicate_versions()),
|
||||
}
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomSubscription {
|
||||
pub required_state: Option<Vec<RequiredState>>,
|
||||
pub timeline_limit: Option<u32>,
|
||||
}
|
||||
|
||||
impl From<RoomSubscription> for RumaRoomSubscription {
|
||||
fn from(val: RoomSubscription) -> Self {
|
||||
assign!(RumaRoomSubscription::default(), {
|
||||
required_state: val.required_state.map(|r|
|
||||
r.into_iter().map(|s| (s.key.into(), s.value)).collect()
|
||||
).unwrap_or_default(),
|
||||
timeline_limit: val.timeline_limit.map(|u| u.into())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,7 +532,7 @@ pub struct UnreadNotificationsCount {
|
||||
notification_count: u32,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
impl UnreadNotificationsCount {
|
||||
fn highlight_count(&self) -> u32 {
|
||||
self.highlight_count
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use matrix_sdk::room::{RoomMember as SdkRoomMember, RoomMemberRole};
|
||||
use ruma::{UserId, events::room::power_levels::UserPowerLevel};
|
||||
use matrix_sdk::room::RoomMember as SdkRoomMember;
|
||||
|
||||
use crate::error::{ClientError, NotYetImplemented};
|
||||
use super::RUNTIME;
|
||||
use crate::ClientError;
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum MembershipState {
|
||||
@@ -19,160 +19,219 @@ pub enum MembershipState {
|
||||
|
||||
/// The user has left.
|
||||
Leave,
|
||||
|
||||
/// A custom membership state value.
|
||||
Custom { value: String },
|
||||
}
|
||||
|
||||
impl TryFrom<matrix_sdk::ruma::events::room::member::MembershipState> for MembershipState {
|
||||
type Error = NotYetImplemented;
|
||||
|
||||
fn try_from(
|
||||
m: matrix_sdk::ruma::events::room::member::MembershipState,
|
||||
) -> Result<Self, Self::Error> {
|
||||
impl From<matrix_sdk::ruma::events::room::member::MembershipState> for MembershipState {
|
||||
fn from(m: matrix_sdk::ruma::events::room::member::MembershipState) -> Self {
|
||||
match m {
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Ban => {
|
||||
Ok(MembershipState::Ban)
|
||||
}
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Ban => MembershipState::Ban,
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Invite => {
|
||||
Ok(MembershipState::Invite)
|
||||
}
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Join => {
|
||||
Ok(MembershipState::Join)
|
||||
MembershipState::Invite
|
||||
}
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Join => MembershipState::Join,
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Knock => {
|
||||
Ok(MembershipState::Knock)
|
||||
MembershipState::Knock
|
||||
}
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Leave => {
|
||||
Ok(MembershipState::Leave)
|
||||
}
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::_Custom(_) => {
|
||||
Ok(MembershipState::Custom { value: m.to_string() })
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("Other membership state change not yet implemented");
|
||||
Err(NotYetImplemented)
|
||||
MembershipState::Leave
|
||||
}
|
||||
_ => todo!(
|
||||
"Handle Custom case: https://github.com/matrix-org/matrix-rust-sdk/issues/1254"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the suggested role for the given power level.
|
||||
///
|
||||
/// Returns an error if the value of the power level is out of range for numbers
|
||||
/// accepted in canonical JSON.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn suggested_role_for_power_level(
|
||||
power_level: PowerLevel,
|
||||
) -> Result<RoomMemberRole, ClientError> {
|
||||
// It's not possible to expose the constructor on the Enum through Uniffi ☹️
|
||||
Ok(RoomMemberRole::suggested_role_for_power_level(power_level.try_into()?))
|
||||
}
|
||||
|
||||
/// Get the suggested power level for the given role.
|
||||
///
|
||||
/// Returns an error if the value of the power level is unsupported.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn suggested_power_level_for_role(role: RoomMemberRole) -> Result<PowerLevel, ClientError> {
|
||||
// It's not possible to expose methods on an Enum through Uniffi ☹️
|
||||
Ok(role.suggested_power_level().try_into()?)
|
||||
}
|
||||
|
||||
/// Generates a `matrix.to` permalink to the given userID.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn matrix_to_user_permalink(user_id: String) -> Result<String, ClientError> {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
Ok(user_id.matrix_to_uri().to_string())
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomMember {
|
||||
pub user_id: String,
|
||||
pub display_name: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
pub membership: MembershipState,
|
||||
pub is_name_ambiguous: bool,
|
||||
pub power_level: PowerLevel,
|
||||
pub is_ignored: bool,
|
||||
pub suggested_role_for_power_level: RoomMemberRole,
|
||||
pub membership_change_reason: Option<String>,
|
||||
pub(crate) inner: SdkRoomMember,
|
||||
}
|
||||
|
||||
impl TryFrom<SdkRoomMember> for RoomMember {
|
||||
type Error = NotYetImplemented;
|
||||
#[uniffi::export]
|
||||
impl RoomMember {
|
||||
pub fn user_id(&self) -> String {
|
||||
self.inner.user_id().to_string()
|
||||
}
|
||||
|
||||
fn try_from(m: SdkRoomMember) -> Result<Self, Self::Error> {
|
||||
Ok(RoomMember {
|
||||
user_id: m.user_id().to_string(),
|
||||
display_name: m.display_name().map(|s| s.to_owned()),
|
||||
avatar_url: m.avatar_url().map(|a| a.to_string()),
|
||||
membership: m.membership().clone().try_into()?,
|
||||
is_name_ambiguous: m.name_ambiguous(),
|
||||
power_level: m.power_level().try_into()?,
|
||||
is_ignored: m.is_ignored(),
|
||||
suggested_role_for_power_level: m.suggested_role_for_power_level(),
|
||||
membership_change_reason: m.event().reason().map(|s| s.to_owned()),
|
||||
pub fn display_name(&self) -> Option<String> {
|
||||
self.inner.display_name().map(|d| d.to_owned())
|
||||
}
|
||||
|
||||
pub fn avatar_url(&self) -> Option<String> {
|
||||
self.inner.avatar_url().map(ToString::to_string)
|
||||
}
|
||||
|
||||
pub fn membership(&self) -> MembershipState {
|
||||
self.inner.membership().to_owned().into()
|
||||
}
|
||||
|
||||
pub fn is_name_ambiguous(&self) -> bool {
|
||||
self.inner.name_ambiguous()
|
||||
}
|
||||
|
||||
pub fn power_level(&self) -> i64 {
|
||||
self.inner.power_level()
|
||||
}
|
||||
|
||||
pub fn normalized_power_level(&self) -> i64 {
|
||||
self.inner.normalized_power_level()
|
||||
}
|
||||
|
||||
pub fn is_ignored(&self) -> bool {
|
||||
self.inner.is_ignored()
|
||||
}
|
||||
|
||||
pub fn is_account_user(&self) -> bool {
|
||||
self.inner.is_account_user()
|
||||
}
|
||||
|
||||
/// Adds the room member to the current account data's ignore list
|
||||
/// which will ignore the user across all rooms.
|
||||
pub fn ignore(&self) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
self.inner.ignore().await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Removes the room member from the current account data's ignore list
|
||||
/// which will unignore the user across all rooms.
|
||||
pub fn unignore(&self) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
self.inner.unignore().await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn can_ban(&self) -> bool {
|
||||
self.inner.can_ban()
|
||||
}
|
||||
|
||||
pub fn can_invite(&self) -> bool {
|
||||
self.inner.can_invite()
|
||||
}
|
||||
|
||||
pub fn can_kick(&self) -> bool {
|
||||
self.inner.can_kick()
|
||||
}
|
||||
|
||||
pub fn can_redact(&self) -> bool {
|
||||
self.inner.can_redact()
|
||||
}
|
||||
|
||||
pub fn can_send_state(&self, state_event: StateEventType) -> bool {
|
||||
self.inner.can_send_state(state_event.into())
|
||||
}
|
||||
|
||||
pub fn can_send_message(&self, event: MessageLikeEventType) -> bool {
|
||||
self.inner.can_send_message(event.into())
|
||||
}
|
||||
|
||||
pub fn can_trigger_room_notification(&self) -> bool {
|
||||
self.inner.can_trigger_room_notification()
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains the current user's room member info and the optional room member
|
||||
/// info of the sender of the `m.room.member` event that this info represents.
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct RoomMemberWithSenderInfo {
|
||||
/// The room member.
|
||||
room_member: RoomMember,
|
||||
/// The info of the sender of the event `room_member` is based on, if
|
||||
/// available.
|
||||
sender_info: Option<RoomMember>,
|
||||
}
|
||||
|
||||
impl TryFrom<matrix_sdk::room::RoomMemberWithSenderInfo> for RoomMemberWithSenderInfo {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: matrix_sdk::room::RoomMemberWithSenderInfo) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
room_member: value.room_member.try_into()?,
|
||||
sender_info: value.sender_info.map(|member| member.try_into()).transpose()?,
|
||||
})
|
||||
impl RoomMember {
|
||||
pub fn new(room_member: SdkRoomMember) -> Self {
|
||||
RoomMember { inner: room_member }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum PowerLevel {
|
||||
/// The user is a room creator and has infinite power level.
|
||||
///
|
||||
/// This power level was introduced in room version 12.
|
||||
Infinite,
|
||||
|
||||
/// The user has the given power level.
|
||||
Value { value: i64 },
|
||||
pub enum StateEventType {
|
||||
CallMember,
|
||||
PolicyRuleRoom,
|
||||
PolicyRuleServer,
|
||||
PolicyRuleUser,
|
||||
RoomAliases,
|
||||
RoomAvatar,
|
||||
RoomCanonicalAlias,
|
||||
RoomCreate,
|
||||
RoomEncryption,
|
||||
RoomGuestAccess,
|
||||
RoomHistoryVisibility,
|
||||
RoomJoinRules,
|
||||
RoomMemberEvent,
|
||||
RoomName,
|
||||
RoomPinnedEvents,
|
||||
RoomPowerLevels,
|
||||
RoomServerAcl,
|
||||
RoomThirdPartyInvite,
|
||||
RoomTombstone,
|
||||
RoomTopic,
|
||||
SpaceChild,
|
||||
SpaceParent,
|
||||
}
|
||||
|
||||
impl TryFrom<UserPowerLevel> for PowerLevel {
|
||||
type Error = NotYetImplemented;
|
||||
|
||||
fn try_from(value: UserPowerLevel) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
UserPowerLevel::Infinite => Ok(Self::Infinite),
|
||||
UserPowerLevel::Int(value) => Ok(Self::Value { value: value.into() }),
|
||||
_ => Err(NotYetImplemented),
|
||||
impl From<StateEventType> for ruma::events::StateEventType {
|
||||
fn from(val: StateEventType) -> Self {
|
||||
match val {
|
||||
StateEventType::CallMember => Self::CallMember,
|
||||
StateEventType::PolicyRuleRoom => Self::PolicyRuleRoom,
|
||||
StateEventType::PolicyRuleServer => Self::PolicyRuleServer,
|
||||
StateEventType::PolicyRuleUser => Self::PolicyRuleUser,
|
||||
StateEventType::RoomAliases => Self::RoomAliases,
|
||||
StateEventType::RoomAvatar => Self::RoomAvatar,
|
||||
StateEventType::RoomCanonicalAlias => Self::RoomCanonicalAlias,
|
||||
StateEventType::RoomCreate => Self::RoomCreate,
|
||||
StateEventType::RoomEncryption => Self::RoomEncryption,
|
||||
StateEventType::RoomGuestAccess => Self::RoomGuestAccess,
|
||||
StateEventType::RoomHistoryVisibility => Self::RoomHistoryVisibility,
|
||||
StateEventType::RoomJoinRules => Self::RoomJoinRules,
|
||||
StateEventType::RoomMemberEvent => Self::RoomMember,
|
||||
StateEventType::RoomName => Self::RoomName,
|
||||
StateEventType::RoomPinnedEvents => Self::RoomPinnedEvents,
|
||||
StateEventType::RoomPowerLevels => Self::RoomPowerLevels,
|
||||
StateEventType::RoomServerAcl => Self::RoomServerAcl,
|
||||
StateEventType::RoomThirdPartyInvite => Self::RoomThirdPartyInvite,
|
||||
StateEventType::RoomTombstone => Self::RoomTombstone,
|
||||
StateEventType::RoomTopic => Self::RoomTopic,
|
||||
StateEventType::SpaceChild => Self::SpaceChild,
|
||||
StateEventType::SpaceParent => Self::SpaceParent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<PowerLevel> for UserPowerLevel {
|
||||
type Error = ClientError;
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum MessageLikeEventType {
|
||||
CallAnswer,
|
||||
CallInvite,
|
||||
CallHangup,
|
||||
CallCandidates,
|
||||
KeyVerificationReady,
|
||||
KeyVerificationStart,
|
||||
KeyVerificationCancel,
|
||||
KeyVerificationAccept,
|
||||
KeyVerificationKey,
|
||||
KeyVerificationMac,
|
||||
KeyVerificationDone,
|
||||
ReactionSent,
|
||||
RoomEncrypted,
|
||||
RoomMessage,
|
||||
RoomRedaction,
|
||||
Sticker,
|
||||
}
|
||||
|
||||
fn try_from(value: PowerLevel) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
PowerLevel::Infinite => Self::Infinite,
|
||||
PowerLevel::Value { value } => {
|
||||
Self::Int(value.try_into().map_err(|err| ClientError::Generic {
|
||||
msg: "Power level is out of range".to_owned(),
|
||||
details: Some(format!("{err:?}")),
|
||||
})?)
|
||||
}
|
||||
})
|
||||
impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
|
||||
fn from(val: MessageLikeEventType) -> Self {
|
||||
match val {
|
||||
MessageLikeEventType::CallAnswer => Self::CallAnswer,
|
||||
MessageLikeEventType::CallInvite => Self::CallInvite,
|
||||
MessageLikeEventType::CallHangup => Self::CallHangup,
|
||||
MessageLikeEventType::CallCandidates => Self::CallCandidates,
|
||||
MessageLikeEventType::KeyVerificationReady => Self::KeyVerificationReady,
|
||||
MessageLikeEventType::KeyVerificationStart => Self::KeyVerificationStart,
|
||||
MessageLikeEventType::KeyVerificationCancel => Self::KeyVerificationCancel,
|
||||
MessageLikeEventType::KeyVerificationAccept => Self::KeyVerificationAccept,
|
||||
MessageLikeEventType::KeyVerificationKey => Self::KeyVerificationKey,
|
||||
MessageLikeEventType::KeyVerificationMac => Self::KeyVerificationMac,
|
||||
MessageLikeEventType::KeyVerificationDone => Self::KeyVerificationDone,
|
||||
MessageLikeEventType::ReactionSent => Self::Reaction,
|
||||
MessageLikeEventType::RoomEncrypted => Self::RoomEncrypted,
|
||||
MessageLikeEventType::RoomMessage => Self::RoomMessage,
|
||||
MessageLikeEventType::RoomRedaction => Self::RoomRedaction,
|
||||
MessageLikeEventType::Sticker => Self::Sticker,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use anyhow::Context as _;
|
||||
use matrix_sdk::{Client, room_preview::RoomPreview as SdkRoomPreview};
|
||||
use ruma::room::{JoinRuleSummary, RoomType as RumaRoomType};
|
||||
|
||||
use crate::{
|
||||
client::{AllowRule, JoinRule},
|
||||
error::ClientError,
|
||||
room::{Membership, RoomHero},
|
||||
room_member::{RoomMember, RoomMemberWithSenderInfo},
|
||||
utils::AsyncRuntimeDropped,
|
||||
};
|
||||
|
||||
/// A room preview for a room. It's intended to be used to represent rooms that
|
||||
/// aren't joined yet.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomPreview {
|
||||
inner: SdkRoomPreview,
|
||||
client: AsyncRuntimeDropped<Client>,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomPreview {
|
||||
/// Returns the room info the preview contains.
|
||||
pub fn info(&self) -> RoomPreviewInfo {
|
||||
let info = &self.inner;
|
||||
RoomPreviewInfo {
|
||||
room_id: info.room_id.to_string(),
|
||||
canonical_alias: info.canonical_alias.as_ref().map(|alias| alias.to_string()),
|
||||
name: info.name.clone(),
|
||||
topic: info.topic.clone(),
|
||||
avatar_url: info.avatar_url.as_ref().map(|url| url.to_string()),
|
||||
num_joined_members: info.num_joined_members,
|
||||
num_active_members: info.num_active_members,
|
||||
room_type: info.room_type.clone().into(),
|
||||
is_history_world_readable: info.is_world_readable,
|
||||
membership: info.state.map(|state| state.into()),
|
||||
join_rule: info.join_rule.clone().map(Into::into),
|
||||
is_direct: info.is_direct,
|
||||
heroes: info
|
||||
.heroes
|
||||
.as_ref()
|
||||
.map(|heroes| heroes.iter().map(|h| h.to_owned().into()).collect()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Leave the room if the room preview state is either joined, invited or
|
||||
/// knocked.
|
||||
///
|
||||
/// If rejecting an invite then also forget it as an extra layer of
|
||||
/// protection against spam attacks.
|
||||
///
|
||||
/// Will return an error otherwise.
|
||||
pub async fn leave(&self) -> Result<(), ClientError> {
|
||||
let room =
|
||||
self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?;
|
||||
|
||||
Ok(room.leave().await?)
|
||||
}
|
||||
|
||||
/// Get the user who created the invite, if any.
|
||||
pub async fn inviter(&self) -> Option<RoomMember> {
|
||||
let room = self.client.get_room(&self.inner.room_id)?;
|
||||
let invite_details = room.invite_details().await.ok()?;
|
||||
invite_details.inviter.and_then(|m| m.try_into().ok())
|
||||
}
|
||||
|
||||
/// Forget the room if we had access to it, and it was left or banned.
|
||||
pub async fn forget(&self) -> Result<(), ClientError> {
|
||||
let room =
|
||||
self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?;
|
||||
room.forget().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the membership details for the current user.
|
||||
pub async fn own_membership_details(&self) -> Option<RoomMemberWithSenderInfo> {
|
||||
let room = self.client.get_room(&self.inner.room_id)?;
|
||||
room.member_with_sender_info(self.client.user_id()?).await.ok()?.try_into().ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl RoomPreview {
|
||||
pub(crate) fn new(client: AsyncRuntimeDropped<Client>, inner: SdkRoomPreview) -> Self {
|
||||
Self { client, inner }
|
||||
}
|
||||
}
|
||||
|
||||
/// The preview of a room, be it invited/joined/left, or not.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomPreviewInfo {
|
||||
/// The room id for this room.
|
||||
pub room_id: String,
|
||||
/// The canonical alias for the room.
|
||||
pub canonical_alias: Option<String>,
|
||||
/// The room's name, if set.
|
||||
pub name: Option<String>,
|
||||
/// The room's topic, if set.
|
||||
pub topic: Option<String>,
|
||||
/// The MXC URI to the room's avatar, if set.
|
||||
pub avatar_url: Option<String>,
|
||||
/// The number of joined members.
|
||||
pub num_joined_members: u64,
|
||||
/// The number of active members, if known (joined + invited).
|
||||
pub num_active_members: Option<u64>,
|
||||
/// The room type (space, custom) or nothing, if it's a regular room.
|
||||
pub room_type: RoomType,
|
||||
/// Is the history world-readable for this room?
|
||||
pub is_history_world_readable: Option<bool>,
|
||||
/// The membership state for the current user, if known.
|
||||
pub membership: Option<Membership>,
|
||||
/// The join rule for this room (private, public, knock, etc.).
|
||||
pub join_rule: Option<JoinRule>,
|
||||
/// Whether the room is direct or not, if known.
|
||||
pub is_direct: Option<bool>,
|
||||
/// Room heroes.
|
||||
pub heroes: Option<Vec<RoomHero>>,
|
||||
}
|
||||
|
||||
impl From<JoinRuleSummary> for JoinRule {
|
||||
fn from(join_rule: JoinRuleSummary) -> Self {
|
||||
match join_rule {
|
||||
JoinRuleSummary::Invite => JoinRule::Invite,
|
||||
JoinRuleSummary::Knock => JoinRule::Knock,
|
||||
JoinRuleSummary::Private => JoinRule::Private,
|
||||
JoinRuleSummary::Restricted(summary) => JoinRule::Restricted {
|
||||
rules: summary
|
||||
.allowed_room_ids
|
||||
.iter()
|
||||
.map(|room_id| AllowRule::RoomMembership { room_id: room_id.to_string() })
|
||||
.collect(),
|
||||
},
|
||||
JoinRuleSummary::KnockRestricted(summary) => JoinRule::KnockRestricted {
|
||||
rules: summary
|
||||
.allowed_room_ids
|
||||
.iter()
|
||||
.map(|room_id| AllowRule::RoomMembership { room_id: room_id.to_string() })
|
||||
.collect(),
|
||||
},
|
||||
JoinRuleSummary::Public => JoinRule::Public,
|
||||
_ => JoinRule::Custom { repr: join_rule.as_str().to_owned() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of room for a [`RoomPreviewInfo`].
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum RoomType {
|
||||
/// It's a plain chat room.
|
||||
Room,
|
||||
/// It's a space that can group several rooms.
|
||||
Space,
|
||||
/// It's a custom implementation.
|
||||
Custom { value: String },
|
||||
}
|
||||
|
||||
impl From<Option<RumaRoomType>> for RoomType {
|
||||
fn from(value: Option<RumaRoomType>) -> Self {
|
||||
match value {
|
||||
Some(RumaRoomType::Space) => RoomType::Space,
|
||||
Some(RumaRoomType::_Custom(_)) => RoomType::Custom {
|
||||
// SAFETY: this was checked in the match branch above
|
||||
value: value.unwrap().to_string(),
|
||||
},
|
||||
_ => RoomType::Room,
|
||||
}
|
||||
}
|
||||
}
|
||||
+132
-1317
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user