Compare commits

..

2 Commits

Author SHA1 Message Date
Damir Jelić c99f665766 chore: Remove an unused import 2024-05-13 12:42:31 +02:00
Damir Jelić 8a975c8985 chore: Fix the formatting 2024-05-13 12:35:52 +02:00
872 changed files with 54732 additions and 225791 deletions
+46
View File
@@ -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,5 +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-applies-to-host = true
-9
View File
@@ -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
View File
@@ -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." },
]
[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",
]
exceptions = [
{ allow = ["Unicode-DFS-2016"], crate = "unicode-ident" },
]
[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",
]
-1
View File
@@ -1,2 +1 @@
* @matrix-org/rust
/crates/matrix-sdk-crypto @matrix-org/rust @matrix-org/rust-crypto-reviewers
-7
View File
@@ -1,7 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
# Check for updates to GitHub Actions every week
interval: "weekly"
+13
View File
@@ -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 }}
+40 -26
View File
@@ -1,40 +1,54 @@
name: Benchmarks
on:
push:
branches:
- "main"
pull_request:
workflow_dispatch:
jobs:
benchmarks:
name: Run Benchmarks
runs-on: ubuntu-latest
strategy:
matrix:
benchmark:
- crypto_bench
- event_cache
- linked_chunk
- store_bench
- timeline
environment: matrix-rust-bot
if: github.event_name == 'push' || !github.event.pull_request.draft
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- name: Checkout the repo
uses: actions/checkout@v3
- name: Setup rust toolchain, cache and cargo-codspeed binary
uses: moonrepo/setup-rust@ede6de059f8046a5e236c94046823e2af11ca670
with:
channel: stable
cache-target: release
bins: cargo-codspeed
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2023-11-08
components: rustfmt
- name: Build the benchmark target(s)
run: cargo codspeed build -p benchmarks ${{ matrix.benchmark }} --features codspeed
- name: Run Benchmarks
run: cargo bench | tee benchmark-output.txt
- name: Run the benchmarks
uses: CodSpeedHQ/action@76578c2a7ddd928664caa737f0e962e3085d4e7c
with:
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}
- 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
- 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'
+6 -119
View File
@@ -31,7 +31,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v3
- name: Install protoc
uses: taiki-e/install-action@v2
@@ -52,7 +52,7 @@ jobs:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Get xtask
uses: actions/cache/restore@v4
uses: actions/cache/restore@v3
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -61,82 +61,15 @@ 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@v5
- name: Checkout Kotlin Rust Components project
uses: actions/checkout@v5
with:
repository: matrix-org/matrix-rust-components-kotlin
path: rust-components-kotlin
ref: main
- name: Use JDK 17
uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
- name: Install android sdk
uses: malinskiy/action-android/install-sdk@release/0.1.7
- name: Install android ndk
uses: nttld/setup-ndk@v1
id: install-ndk
with:
ndk-version: r27
- name: Install Rust
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
- name: Delete cargo config
run: rm .cargo/config.toml
- name: Load cache
uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Get xtask
uses: actions/cache/restore@v4
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
fail-on-cache-miss: true
- name: 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@v5
uses: actions/checkout@v3
# install protoc in case we end up rebuilding opentelemetry-proto
- name: Install protoc
@@ -161,7 +94,7 @@ jobs:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Get xtask
uses: actions/cache/restore@v4
uses: actions/cache/restore@v3
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-macos }}"
@@ -175,50 +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@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@v5
# install protoc in case we end up rebuilding opentelemetry-proto
- name: Install protoc
uses: taiki-e/install-action@v2
with:
tool: protoc@3.20.3
- name: Install Rust
uses: dtolnay/rust-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@v2
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
+96 -93
View File
@@ -6,6 +6,11 @@ on:
branches: [main]
pull_request:
branches: [main]
types:
- opened
- reopened
- synchronize
- ready_for_review
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -13,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:
@@ -24,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:
@@ -34,25 +37,18 @@ 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@v5
uses: actions/checkout@v3
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install libsqlite
run: |
sudo apt-get update
sudo apt-get install libsqlite3-dev
- name: Load cache
uses: Swatinem/rust-cache@v2
with:
@@ -68,7 +64,7 @@ jobs:
uses: taiki-e/install-action@nextest
- name: Get xtask
uses: actions/cache/restore@v4
uses: actions/cache/restore@v3
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -82,10 +78,11 @@ 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@v5
uses: actions/checkout@v3
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -99,7 +96,7 @@ jobs:
uses: taiki-e/install-action@nextest
- name: Get xtask
uses: actions/cache/restore@v4
uses: actions/cache/restore@v3
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -113,15 +110,11 @@ 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@v5
- 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@stable
@@ -135,7 +128,7 @@ jobs:
uses: taiki-e/install-action@nextest
- name: Get xtask
uses: actions/cache/restore@v4
uses: actions/cache/restore@v3
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -147,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:
@@ -167,19 +161,13 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v3
- name: Install protoc
uses: taiki-e/install-action@v2
with:
tool: protoc@3.20.3
- name: Install libsqlite
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install libsqlite3-dev
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
@@ -205,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
@@ -221,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
@@ -239,7 +227,7 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v5
uses: actions/checkout@v3
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -248,10 +236,9 @@ jobs:
components: clippy
- name: Install wasm-pack
uses: qmaru/wasm-pack-action@v0.5.1
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@v2
@@ -268,7 +255,7 @@ jobs:
uses: taiki-e/install-action@nextest
- name: Get xtask
uses: actions/cache/restore@v4
uses: actions/cache/restore@v3
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -279,29 +266,49 @@ 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@v5
uses: actions/checkout@v3
- name: Check the spelling of the files in our repo
uses: crate-ci/typos@v1.35.7
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@v5
uses: actions/checkout@v3
- name: Install protoc
uses: taiki-e/install-action@v2
@@ -311,8 +318,8 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2025-08-08
components: clippy, rustfmt
toolchain: nightly-2023-11-08
components: clippy
- name: Load cache
uses: Swatinem/rust-cache@v2
@@ -320,52 +327,67 @@ jobs:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Get xtask
uses: actions/cache/restore@v4
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@v5
- 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@stable
@@ -380,28 +402,9 @@ jobs:
- 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 -p matrix-sdk-integration-testing --features "${{ matrix.feature }}"
compile-bench:
name: 🚄 Compile benchmarks
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
uses: actions/checkout@v5
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Load cache
uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Compile benchmarks (no run)
run: |
cargo bench --profile dev --no-run
cargo nextest run -p matrix-sdk-integration-testing
+61 -122
View File
@@ -1,10 +1,15 @@
name: Code Coverage
name: Code coverage
on:
push:
branches: [main]
pull_request:
branches: [main]
types:
- opened
- reopened
- synchronize
- ready_for_review
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -17,21 +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"
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
@@ -39,75 +70,11 @@ jobs:
- 8008:8008
steps:
# This CI workflow can run into space issue, so we're cleaning up some
# space here.
- name: Create some more space
run: |
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@v5
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- 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@stable
@@ -119,64 +86,36 @@ jobs:
- name: Load cache
uses: Swatinem/rust-cache@v2
with:
prefix-key: "coverage"
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Get xtask
uses: actions/cache/restore@v4
- name: Install tarpaulin
uses: taiki-e/install-action@v2
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
fail-on-cache-miss: true
tool: cargo-tarpaulin
- name: Check total disk space before running
run: |
df -h
# set up backend for integration tests
- uses: actions/setup-python@v4
with:
python-version: 3.8
- name: Create the coverage report
- name: Run tarpaulin
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"
# Copied with minimal adjustments, source:
# https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/test.yml
- name: Get PR number and commit SHA
run: |
echo "Storing PR number ${{ github.event.number }}"
echo "${{ github.event.number }}" > pr_number.txt
echo "Storing commit SHA ${{ github.event.pull_request.head.sha }}"
echo "${{ github.event.pull_request.head.sha }}" > commit_sha.txt
- name: Move the JUnit file into the root directory
shell: bash
run: |
mv target/nextest/ci/junit.xml ./junit.xml
# This stores the coverage report and metadata in artifacts.
# The actual upload to Codecov is executed by a different workflow `upload_coverage.yml`.
# The reason for this split is because `on.pull_request` workflows don't have access to secrets.
- name: Store coverage report in artifacts
uses: actions/upload-artifact@v4
- name: Upload to codecov.io
uses: codecov/codecov-action@v3
with:
name: codecov_report
path: |
coverage.xml
junit.xml
pr_number.txt
commit_sha.txt
if-no-files-found: error
- run: |
echo 'The coverage report was stored in Github artifacts.'
echo 'It will be uploaded to Codecov using `upload_coverage.yml` workflow shortly.'
# 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
-14
View File
@@ -1,14 +0,0 @@
name: Lint dependencies (for licences, allowed sources, banned dependencies, vulnerabilities)
on:
pull_request:
paths:
- '**/Cargo.toml'
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
jobs:
cargo-deny:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: EmbarkStudios/cargo-deny-action@v2
-35
View File
@@ -1,35 +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]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
long-path:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v46.0.5
- name: Detect long path
env:
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} # ignore the deleted files
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,12 +0,0 @@
name: Detects unused dependencies
on:
pull_request: { branches: "*" }
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Machete
uses: bnjbvr/cargo-machete@v0.9.1
+14 -6
View File
@@ -4,6 +4,11 @@ on:
push:
branches: [main]
pull_request:
types:
- opened
- reopened
- synchronize
- ready_for_review
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -18,10 +23,11 @@ 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@v5
uses: actions/checkout@v3
- name: Install protoc
uses: taiki-e/install-action@v2
@@ -31,10 +37,10 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2025-08-08
toolchain: nightly-2023-11-08
- name: Install Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: 20
@@ -46,17 +52,19 @@ jobs:
# Keep in sync with xtask docs
- name: Build documentation
env:
# Work around https://github.com/rust-lang/cargo/issues/10744
CARGO_TARGET_APPLIES_TO_HOST: "true"
RUSTDOCFLAGS: "--enable-index-page -Zunstable-options --cfg docsrs -Dwarnings"
run:
cargo doc --no-deps --workspace --features docsrs --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@v4
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@v4
uses: actions/deploy-pages@v2
-12
View File
@@ -1,12 +0,0 @@
name: Git Checks
on: [pull_request]
jobs:
block-fixup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Block Fixup Commit Merge
uses: 13rac1/block-fixup-merge-action@v2.0.0
-16
View File
@@ -1,16 +0,0 @@
name: Rust version
on:
workflow_dispatch:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
msrv:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: taiki-e/install-action@cargo-hack
- run: cargo hack check --rust-version --workspace --all-targets --ignore-private
-96
View File
@@ -1,96 +0,0 @@
# Copied with minimal adjustments, source:
# https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/coverage-report.yml
name: Upload code coverage
on:
# This workflow is triggered after every successful execution
# of `coverage` workflow.
workflow_run:
workflows: ["Code Coverage"]
types:
- completed
jobs:
coverage:
name: Upload coverage report
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success'
steps:
- name: 'Fetch coverage report from artifacts'
id: prepare_report
uses: actions/github-script@v7
with:
script: |
var fs = require('fs');
// List artifacts of the workflow run that triggered this workflow
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let codecovReport = artifacts.data.artifacts.filter((artifact) => {
return artifact.name == "codecov_report";
});
if (codecovReport.length != 1) {
throw new Error("Unexpected number of {codecov_report} artifacts: " + codecovReport.length);
}
var download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: codecovReport[0].id,
archive_format: 'zip',
});
fs.writeFileSync('codecov_report.zip', Buffer.from(download.data));
- id: parse_previous_artifacts
run: |
unzip codecov_report.zip
echo "Detected PR is: $(<pr_number.txt)"
echo "Detected commit_sha is: $(<commit_sha.txt)"
# Make the params available as step output
echo "override_pr=$(<pr_number.txt)" >> "$GITHUB_OUTPUT"
echo "override_commit=$(<commit_sha.txt)" >> "$GITHUB_OUTPUT"
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
path: repo_root
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
fail_ci_if_error: true
# Manual overrides for these parameters are needed because automatic detection
# in codecov-action does not work for non-`pull_request` workflows.
# In `main` branch push, these default to empty strings since we want to run
# the analysis on HEAD.
override_commit: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
override_pr: ${{ steps.parse_previous_artifacts.outputs.override_pr || '' }}
working-directory: ${{ github.workspace }}/repo_root
# Location where coverage report files are searched for
directory: ${{ github.workspace }}
- name: Upload test results to Codecov
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
fail_ci_if_error: true
# Manual overrides for these parameters are needed because automatic detection
# in codecov-action does not work for non-`pull_request` workflows.
# In `main` branch push, these default to empty strings since we want to run
# the analysis on HEAD.
override_commit: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
override_pr: ${{ steps.parse_previous_artifacts.outputs.override_pr || '' }}
working-directory: ${{ github.workspace }}/repo_root
# Location where coverage report files are searched for
directory: ${{ github.workspace }}
+4 -4
View File
@@ -35,7 +35,7 @@ jobs:
os-name: 🐧
cachekey-id: linux
- os: macos-15
- os: macos-12
os-name: 🍏
cachekey-id: macos
@@ -43,7 +43,7 @@ jobs:
steps:
- name: Checkout repo
uses: actions/checkout@v5
uses: actions/checkout@v3
- name: Calculate cache key
id: cachekey
@@ -53,11 +53,11 @@ jobs:
echo "cachekey-${{ matrix.cachekey-id }}=xtask-${{ matrix.cachekey-id }}-${{ hashFiles('Cargo.toml', 'xtask/**') }}" >> $GITHUB_OUTPUT
- name: Check xtask cache
uses: actions/cache@v4
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)] }}
-3
View File
@@ -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

+3 -9
View File
@@ -21,19 +21,13 @@ WeeChat = "WeeChat"
sing = "sign"
singed = "signed"
singing = "signing"
ratatui = "ratatui"
# 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",
]
-119
View File
@@ -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).
+11 -240
View File
@@ -29,240 +29,16 @@ 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:
```
curl -LsSf https://insta.rs/install.sh | sh
```
Windows:
```
powershell -c "irm https://insta.rs/install.ps1 | iex"
```
Usual flow is to first run the test, then review them.
```
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:
```
<type>(<scope>): <short summary>
```
The type of changes which will be included in changelogs is one of the following:
* `feat`: A new feature
* `fix`: A bug fix
* `doc`: Documentation changes
* `refactor`: Code refactoring
* `perf`: Performance improvements
* `ci`: Changes to CI configuration files and scripts
The scope is optional and can specify the area of the codebase affected (e.g.,
olm, cipher).
### 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 of the fields that are available.
Example:
```
fix(crypto): Use a constant-time Base64 encoder for secret key material
This patch fixes a security issue around a side-channel vulnerability[1]
when decoding secret key material using Base64.
In some circumstances an attacker can obtain information about secret
secret key material via a controlled-channel and side-channel attack.
This patch avoids the side-channel by switching to the base64ct crate
for the encoding, and more importantly, the decoding of secret key
material.
Security-Impact: Low
CVE: CVE-2024-40640
GitHub-Advisory: GHSA-j8cm-g7r6-hfpq
```
## 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 dont 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:
```bash
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.
```bash
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:
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:
```
Developer Certificate of Origin
@@ -309,6 +85,11 @@ include the line in your commit or pull request comment:
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.
@@ -319,13 +100,3 @@ on Git 2.17+ you can mass signoff using rebase:
```
git rebase --signoff origin/main
```
## Tips for working on the `matrix-rust-sdk` with specific IDEs
* [RustRover](https://www.jetbrains.com/rust/) will attempt to sync the project
with all features enabled, causing an error in `matrix-sdk` ("only one of the
features 'native-tls' or 'rustls-tls' can be enabled"). To work around this,
open `crates/matrix-sdk/Cargo.toml` in RustRover and uncheck one of the
`native-tls` or `rustls-tls` feature definitions:
![Screenshot of RustRover](.img/rustrover-disable-feature.png)
Generated
+2337 -2937
View File
File diff suppressed because it is too large Load Diff
+49 -157
View File
@@ -4,159 +4,70 @@ members = [
"bindings/matrix-sdk-crypto-ffi",
"bindings/matrix-sdk-ffi",
"crates/*",
"examples/*",
"labs/*",
"testing/*",
"examples/*",
"uniffi-bindgen",
"xtask",
]
exclude = ["testing/data"]
# xtask, testing and the bindings should only be built when invoked explicitly.
default-members = ["benchmarks", "crates/*", "labs/*"]
default-members = ["benchmarks", "crates/*"]
resolver = "2"
[workspace.package]
rust-version = "1.88"
rust-version = "1.70"
[workspace.dependencies]
anyhow = "1.0.99"
aquamarine = "0.6.0"
as_variant = "1.3.0"
assert-json-diff = "2.0.2"
anyhow = "1.0.68"
assert-json-diff = "2"
assert_matches = "1.5.0"
assert_matches2 = "0.1.2"
async-compat = "0.2.5"
assert_matches2 = "0.1.1"
async-rx = "0.1.3"
# Bumping this to 0.3.6 produces a test failure because the semantic between the
# versions changed subtly.
async-stream = "0.3.5"
async-trait = "0.1.89"
base64 = "0.22.1"
bitflags = "2.9.3"
byteorder = "1.5.0"
cfg-if = "1.0.3"
clap = "4.5.46"
chrono = "0.4.41"
dirs = "6.0.0"
eyeball = { version = "0.8.8", features = ["tracing"] }
eyeball-im = { version = "0.7.0", features = ["tracing"] }
eyeball-im-util = "0.9.0"
futures-core = "0.3.31"
futures-executor = "0.3.31"
futures-util = "0.3.31"
getrandom = { version = "0.2.15", default-features = false }
gloo-timers = "0.3.0"
growable-bloom-filter = "2.1.1"
hkdf = "0.12.4"
hmac = "0.12.1"
http = "1.3.1"
imbl = "5.0.0"
indexmap = "2.11.0"
insta = { version = "1.43.1", features = ["json", "redactions"] }
itertools = "0.14.0"
js-sys = "0.3.77"
mime = "0.3.17"
oauth2 = { version = "5.0.0", default-features = false, features = ["reqwest", "timing-resistant-secret-traits"] }
once_cell = "1.21.3"
pbkdf2 = { version = "0.12.2" }
pin-project-lite = "0.2.16"
proptest = { version = "1.6.0", default-features = false, features = ["std"] }
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"
reqwest = { version = "0.12.23", default-features = false }
rmp-serde = "1.3.0"
ruma = { version = "0.13.0", features = [
"client-api-c",
"compat-upload-signatures",
"compat-arbitrary-length-ids",
"compat-tag-info",
"compat-encrypted-stickers",
"compat-lax-room-create-deser",
"compat-lax-room-topic-deser",
"unstable-msc3401",
"unstable-msc3488",
"unstable-msc3489",
"unstable-msc4075",
"unstable-msc4140",
"unstable-msc4143",
"unstable-msc4171",
"unstable-msc4222",
"unstable-msc4278",
"unstable-msc4286",
"unstable-msc4306",
"unstable-msc4308"
] }
sentry = { version = "0.42.0", default-features = false }
sentry-tracing = "0.42.0"
serde = { version = "1.0.219", features = ["rc"] }
serde_html_form = "0.2.7"
serde_json = "1.0.143"
sha2 = "0.10.9"
similar-asserts = "1.7.0"
serde = "1.0.151"
serde_html_form = "0.2.0"
serde_json = "1.0.91"
sha2 = "0.10.8"
stream_assert = "0.1.1"
tempfile = "3.21.0"
thiserror = "2.0.16"
tokio = { version = "1.47.1", default-features = false, features = ["sync"] }
tokio-stream = "0.1.17"
tracing = { version = "0.1.41", default-features = false, features = ["std"] }
tracing-appender = "0.2.3"
tracing-core = "0.1.34"
tracing-subscriber = "0.3.20"
unicode-normalization = "0.1.24"
uniffi = { version = "0.28.0" }
uniffi_bindgen = { version = "0.28.0" }
url = "2.5.7"
uuid = "1.18.0"
vergen-gitcl = "1.0.8"
vodozemac = { version = "0.9.0", features = ["insecure-pk-encryption"] }
wasm-bindgen = "0.2.84"
wasm-bindgen-test = "0.3.50"
web-sys = "0.3.69"
wiremock = "0.6.5"
zeroize = "1.8.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.14.0", default-features = false }
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.14.0" }
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.14.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.14.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.14.0", default-features = false }
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.14.0" }
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.14.0", default-features = false }
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.14.0" }
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.14.0" }
matrix-sdk-test-utils = { path = "testing/matrix-sdk-test-utils", version = "0.14.0" }
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.14.0", default-features = false }
matrix-sdk-search = { path = "crates/matrix-sdk-search", version = "0.14.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`
@@ -169,9 +80,6 @@ debug = 0
# 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]
@@ -184,22 +92,6 @@ debug = 2
inherits = "dbg"
opt-level = 3
[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" }
async-compat = { git = "https://github.com/jplatte/async-compat", rev = "16dc8597ec09a6102d58d4e7b67714a35dd0ecb8" }
const_panic = { git = "https://github.com/jplatte/const_panic", rev = "9024a4cb3eac45c1d2d980f17aaee287b17be498" }
# 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" }
+29 -72
View File
@@ -1,90 +1,47 @@
<h1 align="center">Matrix Rust SDK</h1>
![Build Status](https://img.shields.io/github/actions/workflow/status/matrix-org/matrix-rust-sdk/ci.yml?style=flat-square)
[![codecov](https://img.shields.io/codecov/c/github/matrix-org/matrix-rust-sdk/main.svg?style=flat-square)](https://codecov.io/gh/matrix-org/matrix-rust-sdk)
[![License](https://img.shields.io/badge/License-Apache%202.0-yellowgreen.svg?style=flat-square)](https://opensource.org/licenses/Apache-2.0)
[![#matrix-rust-sdk](https://img.shields.io/badge/matrix-%23matrix--rust--sdk-blue?style=flat-square)](https://matrix.to/#/#matrix-rust-sdk:matrix.org)
[![Docs - Main](https://img.shields.io/badge/docs-main-blue.svg?style=flat-square)](https://matrix-org.github.io/matrix-rust-sdk/matrix_sdk/)
[![Docs - Stable](https://img.shields.io/crates/v/matrix-sdk?color=blue&label=docs&style=flat-square)](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/
-48
View File
@@ -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
+121
View File
@@ -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 -36
View File
@@ -1,53 +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]
criterion = { version = "3.0.5", features = ["async", "async_tokio", "html_reports"], package = "codspeed-criterion-compat" }
matrix-sdk = { workspace = true, features = ["native-tls", "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
ruma.workspace = true
serde.workspace = true
serde_json.workspace = true
tempfile.workspace = true
tokio = { workspace = true, default-features = false, 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
+3 -10
View File
@@ -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
+93 -111
View File
@@ -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);
-353
View File
@@ -1,353 +0,0 @@
use std::{pin::Pin, sync::Arc};
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use matrix_sdk::{
RoomInfo, RoomState, SqliteEventCacheStore, StateStore,
store::StoreConfig,
sync::{JoinedRoomUpdate, RoomUpdates},
test_utils::client::MockClientBuilder,
};
use matrix_sdk_base::event_cache::store::{DynEventCacheStore, IntoEventCacheStore, MemoryStore};
use matrix_sdk_test::{ALICE, event_factory::EventFactory};
use ruma::{
EventId, RoomId, event_id,
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 {
let room_id = RoomId::parse(format!("!room{i}:example.com")).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_id = EventId::parse(format!("$ev{i}_{j}")).unwrap();
let event =
event_factory.text_msg(format!("Message {j}")).event_id(&event_id).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("cross-process-store-locks-holder-name".to_owned())
.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);
let room_id = room_id!("!room:ben.ch");
let other_room_id = room_id!("!other-room:ben.ch");
// 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_id = event_id!("$target");
let target_event =
event_factory.text_msg("hello world").event_id(target_event_id).into_event();
joined_room_update.timeline.events.push(target_event);
// Add the numerous edits.
for i in 0..num_related_events {
let event_id = EventId::parse(format!("$edit{i}")).unwrap();
let event = event_factory
.text_msg(format!("* edit {i}"))
.edit(
target_event_id,
RoomMessageEventContentWithoutRelation::text_plain(format!("edit {i}")),
)
.event_id(&event_id)
.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_id = EventId::parse(format!("$msg{i}")).unwrap();
let event =
event_factory.text_msg(format!("unrelated message {i}")).event_id(&event_id).into();
joined_room_update.timeline.events.push(event);
}
// Add other events, in the same room, related to other events.
let other_target_event_id = event_id!("$other_target");
let other_target_event =
event_factory.text_msg("hello world").event_id(other_target_event_id).into_event();
joined_room_update.timeline.events.push(other_target_event);
for i in 0..NUM_OTHER_EVENTS {
let event_id = EventId::parse(format!("$unrelated{i}")).unwrap();
let event =
event_factory.reaction(other_target_event_id, "👍").event_id(&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_id = EventId::parse(format!("$other_room{i}")).unwrap();
let event = event_factory.text_msg(format!("hi {i}")).event_id(&event_id).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("cross-process-store-locks-holder-name".to_owned())
.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();
assert_eq!(target.event_id().as_deref().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);
-274
View File
@@ -1,274 +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::{EventId, 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!("!foo: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("foo")
.event_id(&EventId::parse(format!("$ev{nth}")).unwrap())
.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 {
prev_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!("!foo: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("foo")
.event_id(&EventId::parse(format!("$ev{nth}")).unwrap())
.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 { prev_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);
-216
View File
@@ -1,216 +0,0 @@
use std::time::Duration;
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use matrix_sdk::{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, StateTestEvent, event_factory::EventFactory};
use matrix_sdk_ui::timeline::{TimelineBuilder, TimelineFocus};
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId,
api::client::membership::get_member_events,
device_id,
events::room::member::{MembershipState, RoomMemberEvent},
mxc_uri, owned_room_id, owned_user_id,
serde::Raw,
user_id,
};
use serde_json::json;
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!("!room: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("cross-process-store-locks-holder-name".to_owned())
.state_store(sqlite_store),
ThreadingSupport::Disabled,
);
runtime
.block_on(base_client.activate(
SessionMeta {
user_id: user_id!("@somebody:example.com").to_owned(),
device_id: device_id!("DEVICE_ID").to_owned(),
},
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!("!room: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(StateTestEvent::Encryption);
let pinned_event_ids: Vec<OwnedEventId> = (0..PINNED_EVENTS_COUNT)
.map(|i| EventId::parse(format!("${i}")).expect("Invalid event id"))
.collect();
joined_room_builder = joined_room_builder.add_state_event(StateTestEvent::Custom(json!(
{
"content": {
"pinned": pinned_event_ids
},
"event_id": "$15139375513VdeRF:localhost",
"origin_server_ts": 151393755,
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.pinned_events",
"unsigned": {
"age": 703422
}
}
)));
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()
.clear_all_linked_chunks()
.await
.unwrap();
let timeline = TimelineBuilder::new(&room)
.with_focus(TimelineFocus::PinnedEvents {
max_events_to_load: 100,
max_concurrent_requests: 10,
})
.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);
+28 -18
View File
@@ -1,15 +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,
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 ruma::{RoomId, device_id, 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;
@@ -17,7 +31,7 @@ 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();
@@ -37,7 +51,7 @@ pub fn restore_session(c: &mut Criterion) {
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.
@@ -45,18 +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("cross-process-store-locks-holder-name".to_owned())
.state_store(store.clone()),
)
.store_config(StoreConfig::new().state_store(store.clone()))
.build()
.await
.expect("Can't build client");
@@ -77,16 +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("cross-process-store-locks-holder-name".to_owned())
.state_store(store.clone()),
)
.store_config(StoreConfig::new().state_store(store.clone()))
.build()
.await
.expect("Can't build client");
@@ -109,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);
-125
View File
@@ -1,125 +0,0 @@
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use matrix_sdk::test_utils::mocks::MatrixMockServer;
use matrix_sdk_test::{JoinedRoomBuilder, StateTestEvent, event_factory::EventFactory};
use matrix_sdk_ui::timeline::TimelineBuilder;
use ruma::{
EventId, 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!("!room: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 event_id = EventId::parse(format!("$event{i}")).unwrap();
let j = i % 10;
if j < 6 {
// Messages.
events.push(
f.text_msg(format!("Message {i}"))
.sender(sender)
.event_id(&event_id)
.into_raw_sync(),
);
} else if j < 8 {
// Reactions.
let prev_event = EventId::parse(format!("$event{}", i - 2)).unwrap();
events.push(
f.reaction(&prev_event, "👍").sender(sender).event_id(&event_id).into_raw_sync(),
);
} else if j == 8 {
// Edit.
// Note: (i-3)%3 is the same as i%3 -> same sender!
let prev_event = EventId::parse(format!("$event{}", i - 3)).unwrap();
events.push(
f.text_msg(format!("* Message {}v2", i - 3))
.edit(
&prev_event,
RoomMessageEventContentWithoutRelation::text_plain(format!(
"Message {}v2",
i - 3
)),
)
.sender(sender)
.event_id(&event_id)
.into_raw_sync(),
);
} else if j == 9 {
// Redaction.
// Note: (i-6)%3 is the same as i%6 -> same sender!
let prev_event = EventId::parse(format!("$event{}", i - 6)).unwrap();
events
.push(f.redaction(&prev_event).sender(sender).event_id(&event_id).into_raw_sync());
}
}
let builder = JoinedRoomBuilder::new(&room_id)
.add_state_event(StateTestEvent::Encryption)
.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()
.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
View File
@@ -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
View File
@@ -13,7 +13,6 @@ let package = Package(
],
products: [
.library(name: "MatrixRustSDK",
type: .dynamic,
targets: ["MatrixRustSDK"]),
],
targets: [
+4 -4
View File
@@ -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']
+4 -4
View File
@@ -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"
@@ -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")
+39 -72
View File
@@ -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
+22 -39
View File
@@ -3,50 +3,36 @@ name = "matrix-sdk-crypto-ffi"
version = "0.1.0"
authors = ["Damir Jelić <poljar@termina.org.uk>"]
edition = "2021"
rust-version.workspace = true
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]
@@ -55,24 +41,21 @@ 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
default-features = false
version = "1.33.0"
default_features = false
features = ["rt-multi-thread"]
[build-dependencies]
uniffi = { workspace = true, features = ["build"] }
vergen-gitcl = { 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 }
+4
View File
@@ -71,6 +71,10 @@ $ 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)
+20 -47
View File
@@ -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,7 +3,7 @@ use std::{collections::HashMap, iter, ops::DerefMut, sync::Arc};
use hmac::Hmac;
use matrix_sdk_crypto::{
backups::DecryptionError,
store::{types::BackupDecryptionKey, CryptoStoreError as InnerStoreError},
store::{BackupDecryptionKey, CryptoStoreError as InnerStoreError},
};
use pbkdf2::pbkdf2;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
@@ -69,7 +69,7 @@ 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)]
@@ -1,26 +1,19 @@
use std::{mem::ManuallyDrop, sync::Arc};
use matrix_sdk_common::executor::Handle;
use matrix_sdk_crypto::{
dehydrated_devices::{
DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices,
RehydratedDevice as InnerRehydratedDevice,
},
store::types::DehydratedDeviceKey as InnerDehydratedDeviceKey,
DecryptionSettings,
use matrix_sdk_crypto::dehydrated_devices::{
DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices,
RehydratedDevice as InnerRehydratedDevice,
};
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)]
@@ -29,8 +22,6 @@ pub enum DehydrationError {
Store(#[from] matrix_sdk_crypto::CryptoStoreError),
#[error("The pickle key has an invalid length, expected 32 bytes, got {0}")]
PickleKeyLength(usize),
#[error(transparent)]
Rand(#[from] rand::Error),
}
impl From<matrix_sdk_crypto::dehydrated_devices::DehydrationError> for DehydrationError {
@@ -38,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)
}
}
}
}
@@ -68,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())?;
@@ -81,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(),
@@ -100,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)]
@@ -153,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(())
}
@@ -183,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())
}
}
@@ -225,36 +177,15 @@ impl From<dehydrated_device::put_dehydrated_device::unstable::Request>
}
}
#[cfg(test)]
mod tests {
use crate::{dehydrated_devices::DehydrationError, DehydratedDeviceKey};
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 result = DehydratedDeviceKey::new();
assert!(result.is_ok());
let dehydrated_device_key = result.unwrap();
let base_64 = dehydrated_device_key.to_base64();
let inner_bytes = dehydrated_device_key.inner;
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))
}
}
+2 -5
View File
@@ -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(),
}
}
}
+6 -9
View File
@@ -1,9 +1,8 @@
#![allow(missing_docs)]
use matrix_sdk_crypto::{
store::{CryptoStoreError as InnerStoreError, DehydrationError as InnerDehydrationError},
KeyExportError, MegolmError, OlmError, SecretImportError as RustSecretImportError,
SignatureError as InnerSignatureError,
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)]
@@ -78,10 +75,10 @@ pub enum DecryptionError {
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() },
}
@@ -115,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));
+60 -226
View File
@@ -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,
};
@@ -33,20 +29,13 @@ pub use error::{
use js_int::UInt;
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::{
olm::{IdentityKeys, InboundGroupSession, SenderData, Session},
store::{
types::{
Changes, DehydratedDeviceKey as InnerDehydratedDeviceKey, PendingChanges,
RoomSettings as RustRoomSettings,
},
CryptoStore,
},
types::{
DeviceKey, DeviceKeys, EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey,
},
CollectStrategy, EncryptionSettings as RustEncryptionSettings,
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::{
@@ -55,8 +44,8 @@ pub use responses::{
};
use ruma::{
events::room::history_visibility::HistoryVisibility as RustHistoryVisibility,
DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId,
RoomId, SecondsSinceUnixEpoch, UserId,
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,12 +466,10 @@ fn collect_sessions(
Ok((algorithm, key))
})
.collect::<anyhow::Result<_>>()?,
sender_data: SenderData::legacy(),
room_id: RoomId::parse(session.room_id)?,
imported: session.imported,
backed_up: session.backed_up,
history_visibility: None,
shared_history: false,
algorithm: RustEventEncryptionAlgorithm::MegolmV1AesSha2,
};
@@ -539,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,
@@ -567,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
///
@@ -665,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,
@@ -680,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,
}
}
}
@@ -747,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 },
}
}
}
@@ -813,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> {
@@ -826,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(())?,
@@ -841,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.
pub fn new() -> Result<Self, DehydrationError> {
let inner = InnerDehydratedDeviceKey::new()?;
Ok(inner.into())
}
/// Creates a new dehydration pickle key from the given slice.
///
/// Fail if the slice length is not 32.
pub fn from_slice(slice: &[u8]) -> Result<Self, DehydrationError> {
let inner = InnerDehydratedDeviceKey::from_slice(slice)?;
Ok(inner.into())
}
/// Export the [`DehydratedDeviceKey`] as a base64 encoded string.
pub fn to_base64(&self) -> String {
let inner = InnerDehydratedDeviceKey::from_slice(&self.inner).unwrap();
inner.to_base64()
}
}
impl From<InnerDehydratedDeviceKey> for DehydratedDeviceKey {
fn from(pickle_key: InnerDehydratedDeviceKey) -> Self {
DehydratedDeviceKey { inner: pickle_key.into() }
}
}
impl From<matrix_sdk_crypto::store::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 }
}
}
@@ -915,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,
@@ -929,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 })
}
}
@@ -943,7 +839,6 @@ impl From<RoomSettings> for RustRoomSettings {
Self {
algorithm: value.algorithm.into(),
only_allow_trusted_devices: value.only_allow_trusted_devices,
..RustRoomSettings::default()
}
}
}
@@ -952,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(),
@@ -976,74 +871,17 @@ 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) -> PkMessage {
use vodozemac::base64_encode;
let message = self.inner.encrypt(plaintext.as_ref());
let vodozemac::pk_encryption::Message { ciphertext, mac, ephemeral_key } = message;
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 {
@@ -1063,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":[
@@ -1171,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();
@@ -1187,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
@@ -1198,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
+2 -2
View File
@@ -7,7 +7,7 @@ use tracing_subscriber::{fmt::MakeWriter, EnvFilter};
/// Trait that can be used to forward Rust logs over FFI to a language specific
/// logger.
#[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)) };
+108 -206
View File
@@ -16,10 +16,8 @@ use matrix_sdk_crypto::{
},
decrypt_room_key_export, encrypt_room_key_export,
olm::ExportedRoomKey,
store::types::{BackupDecryptionKey, Changes},
types::requests::ToDeviceRequest,
CollectStrategy, DecryptionSettings, LocalTrust, OlmMachine as InnerMachine,
UserIdentity as SdkUserIdentity,
store::{BackupDecryptionKey, Changes},
LocalTrust, OlmMachine as InnerMachine, UserIdentities,
};
use ruma::{
api::{
@@ -42,9 +40,7 @@ use ruma::{
AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
},
serde::Raw,
to_device::DeviceIdOrAllDevices,
DeviceKeyAlgorithm, EventId, OneTimeKeyAlgorithm, OwnedTransactionId, OwnedUserId, RoomId,
UserId,
DeviceKeyAlgorithm, EventId, OwnedTransactionId, OwnedUserId, RoomId, UserId,
};
use serde::{Deserialize, Serialize};
use serde_json::{value::RawValue, Value};
@@ -97,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(),
@@ -180,7 +176,7 @@ impl From<RustSignatureCheckResult> for SignatureVerification {
}
}
#[matrix_sdk_ffi_macros::export]
#[uniffi::export]
impl OlmMachine {
/// Create a new `OlmMachine`
///
@@ -211,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 }))
}
@@ -254,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,
@@ -288,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
},
@@ -316,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 {
@@ -333,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,
@@ -355,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.
@@ -416,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,
@@ -517,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(
@@ -527,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"),
@@ -543,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 })
@@ -613,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>,
@@ -734,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(
@@ -810,57 +800,6 @@ impl OlmMachine {
Ok(serde_json::to_string(&encrypted_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)
}
}
/// Decrypt the given event that was sent in the given room.
///
/// # Arguments
@@ -872,14 +811,12 @@ impl OlmMachine {
/// * `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
@@ -895,11 +832,7 @@ 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 {
if let Ok(AnyTimelineEvent::MessageLike(e)) = decrypted.event.deserialize() {
@@ -920,30 +853,26 @@ impl OlmMachine {
}
}
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()
},
}
}
})
}
@@ -954,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(
@@ -979,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)?;
@@ -1005,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,
@@ -1014,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.
@@ -1024,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
@@ -1077,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(())
}
@@ -1120,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![];
@@ -1141,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(
@@ -1161,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,
@@ -1177,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
@@ -1190,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(
@@ -1220,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() }
@@ -1236,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,
@@ -1257,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 {
@@ -1310,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>> {
@@ -1330,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.
///
@@ -1350,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 {
@@ -1554,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",
};
+16 -18
View File
@@ -4,12 +4,9 @@ use std::collections::HashMap;
use http::Response;
use matrix_sdk_crypto::{
types::requests::{
AnyIncomingResponse, KeysBackupRequest, OutgoingRequest,
OutgoingVerificationRequest as SdkVerificationRequest, RoomMessageRequest, ToDeviceRequest,
UploadSigningKeysRequest as RustUploadSigningKeysRequest,
},
CrossSigningBootstrapRequests,
CrossSigningBootstrapRequests, IncomingResponse, KeysBackupRequest, OutgoingRequest,
OutgoingVerificationRequest as SdkVerificationRequest, RoomMessageRequest, ToDeviceRequest,
UploadSigningKeysRequest as RustUploadSigningKeysRequest,
};
use ruma::{
api::client::{
@@ -27,7 +24,7 @@ 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),
}
}
}
+5 -11
View File
@@ -1,8 +1,8 @@
use matrix_sdk_crypto::{types::CrossSigningKey, UserIdentity as SdkUserIdentity};
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,7 +1,6 @@
use std::sync::Arc;
use futures_util::{Stream, StreamExt};
use matrix_sdk_common::executor::Handle;
use matrix_sdk_crypto::{
matrix_sdk_qrcode::QrVerificationData, CancelInfo as RustCancelInfo, QrVerification as InnerQr,
QrVerificationState, Sas as InnerSas, SasState as RustSasState,
@@ -9,13 +8,14 @@ use matrix_sdk_crypto::{
VerificationRequestState as RustVerificationRequestState,
};
use ruma::events::key::verification::VerificationMethod;
use tokio::runtime::Handle;
use vodozemac::{base64_decode, base64_encode};
use crate::{CryptoStoreError, OutgoingVerificationRequest, SignatureUploadRequest};
/// 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
///
@@ -28,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.
@@ -61,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 {
@@ -82,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
}
@@ -98,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
}
@@ -108,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 {
@@ -169,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> {
@@ -276,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
///
@@ -324,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 {
@@ -391,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> {
@@ -522,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
///
@@ -562,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.
@@ -640,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())
@@ -663,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(),
}))
}
@@ -681,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
@@ -701,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()?;
@@ -717,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 {
@@ -752,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(),
@@ -791,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()
}
-28
View File
@@ -1,28 +0,0 @@
[package]
description = "Helper macros to write FFI bindings"
edition = "2021"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
keywords = ["matrix", "chat", "messaging", "ruma"]
license = "Apache-2.0"
name = "matrix-sdk-ffi-macros"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version.workspace = true
version = "0.7.0"
publish = false
[lib]
proc-macro = true
test = false
doctest = false
[dependencies]
proc-macro2 = "1.0.86"
quote = "1.0.18"
syn = { version = "2.0.43", features = ["full", "extra-traits"] }
[lints]
workspace = true
[package.metadata.release]
release = false
-14
View File
@@ -1,14 +0,0 @@
[![Build Status](https://img.shields.io/travis/matrix-org/matrix-rust-sdk.svg?style=flat-square)](https://travis-ci.org/matrix-org/matrix-rust-sdk)
[![codecov](https://img.shields.io/codecov/c/github/matrix-org/matrix-rust-sdk/main.svg?style=flat-square)](https://codecov.io/gh/matrix-org/matrix-rust-sdk)
[![License](https://img.shields.io/badge/License-Apache%202.0-yellowgreen.svg?style=flat-square)](https://opensource.org/licenses/Apache-2.0)
[![#matrix-rust-sdk](https://img.shields.io/badge/matrix-%23matrix--rust--sdk-blue?style=flat-square)](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/
-70
View File
@@ -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 {
if fun.sig.asyncness.is_some() {
return true;
}
}
}
} else if let Item::Trait(blk) = &item {
for item in &blk.items {
if let TraitItem::Fn(fun) = item {
if fun.sig.asyncness.is_some() {
return true;
}
}
}
}
false
};
let attr2 = proc_macro2::TokenStream::from(attr);
let item2 = proc_macro2::TokenStream::from(item.clone());
let res = match syn::parse(item) {
Ok(item) => match has_async_fn(item) {
true => {
quote! {
#[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()
}
-274
View File
@@ -1,274 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
<!-- next-header -->
## [Unreleased] - ReleaseDate
## [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.
+68 -85
View File
@@ -1,103 +1,86 @@
[package]
name = "matrix-sdk-ffi"
version = "0.14.0"
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"]
default = ["bundled-sqlite"]
bundled-sqlite = ["matrix-sdk/bundled-sqlite"]
unstable-msc4274 = ["matrix-sdk-ui/unstable-msc4274"]
# Required when targeting a Javascript environment, like Wasm in a browser.
js = ["matrix-sdk-ui/js"]
# Use the TLS implementation provided by the host system, necessary on iOS and Wasm platforms.
native-tls = ["matrix-sdk/native-tls", "sentry?/native-tls"]
# Use Rustls as the TLS implementation, necessary on Android platforms.
rustls-tls = ["matrix-sdk/rustls-tls", "sentry?/rustls"]
# Enable sentry error monitoring, not compatible with Wasm platforms.
sentry = ["dep:sentry", "dep:sentry-tracing"]
[dependencies]
anyhow.workspace = true
extension-trait = "1.0.1"
eyeball-im.workspace = true
futures-util.workspace = true
language-tags = "0.3.2"
log-panics = { version = "2", features = ["with-backtrace"] }
matrix-sdk = { workspace = true, features = [
"anyhow",
"e2e-encryption",
"experimental-widgets",
"markdown",
"socks",
"sqlite",
"uniffi",
] }
matrix-sdk-common.workspace = true
matrix-sdk-ffi-macros.workspace = true
matrix-sdk-ui = { workspace = true, features = ["uniffi"] }
mime = "0.3.16"
once_cell.workspace = true
ruma = { workspace = true, features = ["html", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat", "unstable-msc4278", "unstable-hydra"] }
serde.workspace = true
serde_json.workspace = true
sentry = { workspace = true, optional = true, default-features = false, features = [
# Most default features enabled otherwise.
"backtrace",
"contexts",
"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", features = ["v4"] }
zeroize.workspace = true
oauth2.workspace = true
[target.'cfg(target_family = "wasm")'.dependencies]
console_error_panic_hook = "0.1.7"
tokio = { workspace = true, features = ["sync", "macros"] }
uniffi.workspace = true
[target.'cfg(not(target_family = "wasm"))'.dependencies]
async-compat.workspace = true
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
uniffi = { workspace = true, features = ["tokio"] }
[target.'cfg(target_os = "android")'.dependencies]
paranoid-android = "0.2.1"
[dev-dependencies]
similar-asserts.workspace = true
[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",
]
+6 -18
View File
@@ -2,28 +2,16 @@
This uses [`uniffi`](https://mozilla.github.io/uniffi-rs/Overview.html) to build the matrix bindings for native support and wasm-bindgen for web-browser assembly support. Please refer to the specific section to figure out how to build and use the bindings for your platform.
## Features
Given the number of platforms targeted, we have broken out a number of features
# OpenTelemetry support
### Platform specific
- `rustls-tls`: Use Rustls as the TLS implementation, necessary on Android platforms.
- `native-tls`: Use the TLS implementation provided by the host system, necessary on iOS and Wasm platforms.
### Functionality
- `sentry`: Enable error monitoring using Sentry, not supports on Wasm platforms.
- `bundled-sqlite`: Use an embedded version of sqlite instead of the system provided one.
### Unstable specs
- `unstable-msc4274`: Adds support for gallery message types, which contain multiple media elements.
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 select the relevant TLS system. Here are some suggested feature flags for the major platforms:
- Android: `"bundled-sqlite,unstable-msc4274,rustls-tls,sentry"`
- iOS: `"bundled-sqlite,unstable-msc4274,native-tls,sentry"`
- Javascript/Wasm: `"unstable-msc4274,native-tls"`
### Swift/iOS sync
+19 -49
View File
@@ -1,67 +1,37 @@
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-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");
}
}
/// 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/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(())
}
+12
View File
@@ -8,3 +8,15 @@ dictionary Mentions {
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,261 +0,0 @@
use std::{
collections::HashMap,
fmt::{self, Debug},
sync::Arc,
};
use matrix_sdk::{
authentication::oauth::{
error::OAuthAuthorizationCodeError,
registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType},
ClientId, ClientRegistrationData, OAuthError as SdkOAuthError,
},
Error,
};
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).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()
}
}
File diff suppressed because it is too large Load Diff
+179 -575
View File
@@ -1,195 +1,57 @@
use std::{fs, num::NonZeroUsize, path::Path, sync::Arc, time::Duration};
use std::{fs, path::PathBuf, sync::Arc};
#[cfg(not(target_family = "wasm"))]
use matrix_sdk::reqwest::Certificate;
use matrix_sdk::{
crypto::{CollectStrategy, DecryptionSettings, TrustRequirement},
encryption::{BackupDownloadStrategy, EncryptionSettings},
event_cache::EventCacheError,
ruma::{ServerName, UserId},
sliding_sync::{
Error as MatrixSlidingSyncError, VersionBuilder as MatrixSlidingSyncVersionBuilder,
VersionBuilderError,
ruma::{
api::{error::UnknownVersionError, MatrixVersion},
ServerName, UserId,
},
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
RumaApiError, SqliteStoreConfig, ThreadingSupport,
Client as MatrixClient, ClientBuilder as MatrixClientBuilder,
};
use ruma::api::error::{DeserializationError, FromHttpResponseError};
use tracing::{debug, error};
use sanitize_filename_reader_friendly::sanitize;
use url::Url;
use zeroize::Zeroizing;
use super::client::Client;
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 {
session_paths: Option<SessionPaths>,
session_passphrase: Zeroizing<Option<String>>,
session_pool_max_size: Option<usize>,
session_cache_size: Option<u32>,
session_journal_size_limit: Option<u32>,
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_store_locks_holder_name: Option<String>,
enable_oidc_refresh_lock: bool,
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>,
#[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 isnt 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 {
session_paths: None,
session_passphrase: Zeroizing::new(None),
session_pool_max_size: None,
session_cache_size: None,
session_journal_size_limit: None,
system_is_memory_constrained: false,
username: None,
homeserver_cfg: None,
user_agent: None,
sliding_sync_version_builder: SlidingSyncVersionBuilder::None,
proxy: None,
disable_ssl_verification: false,
disable_automatic_token_refresh: false,
cross_process_store_locks_holder_name: None,
enable_oidc_refresh_lock: false,
session_delegate: None,
additional_root_certificates: Default::default(),
disable_built_in_root_certificates: false,
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,
})
Arc::new(Self::default())
}
pub fn cross_process_store_locks_holder_name(
pub fn enable_cross_process_refresh_lock(
self: Arc<Self>,
holder_name: String,
process_id: String,
session_delegate: Box<dyn ClientSessionDelegate>,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.cross_process_store_locks_holder_name = Some(holder_name);
Arc::new(builder)
}
pub fn enable_oidc_refresh_lock(self: Arc<Self>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.enable_oidc_refresh_lock = true;
Arc::new(builder)
self.enable_cross_process_refresh_lock_inner(process_id, session_delegate.into())
}
pub fn set_session_delegate(
@@ -201,83 +63,9 @@ impl ClientBuilder {
Arc::new(builder)
}
/// Sets the paths that the client will use to store its data and caches.
/// 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.
///
/// Leaving this unset tells the client to use an in-memory data store.
pub fn session_paths(self: Arc<Self>, data_path: String, cache_path: String) -> Arc<Self> {
pub fn base_path(self: Arc<Self>, path: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.session_paths = Some(SessionPaths { data_path, cache_path });
Arc::new(builder)
}
/// Set the passphrase for the stores given to
/// [`ClientBuilder::session_paths`].
pub fn session_passphrase(self: Arc<Self>, passphrase: Option<String>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.session_passphrase = Zeroizing::new(passphrase);
Arc::new(builder)
}
/// Set the pool max size for the SQLite stores given to
/// [`ClientBuilder::session_paths`].
///
/// Each store exposes an async pool of connections. This method controls
/// the size of the pool. The larger the pool is, the more memory is
/// consumed, but also the more the app is reactive because it doesn't need
/// to wait on a pool to be available to run queries.
///
/// See [`SqliteStoreConfig::pool_max_size`] to learn more.
pub fn session_pool_max_size(self: Arc<Self>, pool_max_size: Option<u32>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.session_pool_max_size = pool_max_size
.map(|size| size.try_into().expect("`pool_max_size` is too large to fit in `usize`"));
Arc::new(builder)
}
/// Set the cache size for the SQLite stores given to
/// [`ClientBuilder::session_paths`].
///
/// Each store exposes a SQLite connection. This method controls the cache
/// size, in **bytes (!)**.
///
/// The cache represents data SQLite holds in memory at once per open
/// database file. The default cache implementation does not allocate the
/// full amount of cache memory all at once. Cache memory is allocated
/// in smaller chunks on an as-needed basis.
///
/// See [`SqliteStoreConfig::cache_size`] to learn more.
pub fn session_cache_size(self: Arc<Self>, cache_size: Option<u32>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.session_cache_size = cache_size;
Arc::new(builder)
}
/// Set the size limit for the SQLite WAL files of stores given to
/// [`ClientBuilder::session_paths`].
///
/// Each store uses the WAL journal mode. This method controls the size
/// limit of the WAL files, in **bytes (!)**.
///
/// See [`SqliteStoreConfig::journal_size_limit`] to learn more.
pub fn session_journal_size_limit(self: Arc<Self>, limit: Option<u32>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.session_journal_size_limit = limit;
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
/// [`SqliteStoreConfig`], so one might not need to call
/// [`ClientBuilder::session_cache_size`] and siblings for example. Please
/// check [`SqliteStoreConfig::with_low_memory_config`].
pub fn system_is_memory_constrained(self: Arc<Self>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.system_is_memory_constrained = true;
builder.base_path = Some(path);
Arc::new(builder)
}
@@ -287,323 +75,43 @@ 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 disable_automatic_token_refresh(self: Arc<Self>) -> Arc<Self> {
pub fn sliding_sync_proxy(self: Arc<Self>, sliding_sync_proxy: Option<String>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.disable_automatic_token_refresh = true;
builder.sliding_sync_proxy = sliding_sync_proxy;
Arc::new(builder)
}
pub fn auto_enable_cross_signing(
self: Arc<Self>,
auto_enable_cross_signing: bool,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.encryption_settings.auto_enable_cross_signing = auto_enable_cross_signing;
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(
self: Arc<Self>,
backup_download_strategy: BackupDownloadStrategy,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.encryption_settings.backup_download_strategy = backup_download_strategy;
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(
self: Arc<Self>,
decryption_settings: DecryptionSettings,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.decryption_settings = decryption_settings;
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)
}
pub async fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientBuildError> {
let builder = unwrap_or_clone_arc(self);
let mut inner_builder = MatrixClient::builder();
if let Some(holder_name) = &builder.cross_process_store_locks_holder_name {
inner_builder =
inner_builder.cross_process_store_locks_holder_name(holder_name.clone());
}
let store_path = if let Some(session_paths) = &builder.session_paths {
// This is the path where both the state store and the crypto store will live.
let data_path = Path::new(&session_paths.data_path);
// This is the path where the event cache store will live.
let cache_path = Path::new(&session_paths.cache_path);
debug!(
data_path = %data_path.to_string_lossy(),
event_cache_path = %cache_path.to_string_lossy(),
"Creating directories for data (state and crypto) and cache stores.",
);
fs::create_dir_all(data_path)?;
fs::create_dir_all(cache_path)?;
let mut sqlite_store_config = if builder.system_is_memory_constrained {
SqliteStoreConfig::with_low_memory_config(data_path)
} else {
SqliteStoreConfig::new(data_path)
};
sqlite_store_config =
sqlite_store_config.passphrase(builder.session_passphrase.as_deref());
if let Some(size) = builder.session_pool_max_size {
sqlite_store_config = sqlite_store_config.pool_max_size(size);
}
if let Some(size) = builder.session_cache_size {
sqlite_store_config = sqlite_store_config.cache_size(size);
}
if let Some(limit) = builder.session_journal_size_limit {
sqlite_store_config = sqlite_store_config.journal_size_limit(limit);
}
inner_builder = inner_builder
.sqlite_store_with_config_and_cache_path(sqlite_store_config, Some(cache_path));
Some(data_path.to_owned())
} else {
debug!("Not using a store path.");
None
};
// 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(),
});
}
}
};
#[cfg(not(target_family = "wasm"))]
{
let mut certificates = Vec::new();
for certificate in builder.additional_root_certificates {
// We don't really know what type of certificate we may get here, so let's try
// first one type, then the other.
match Certificate::from_der(&certificate) {
Ok(cert) => {
certificates.push(cert);
}
Err(der_error) => {
let cert = Certificate::from_pem(&certificate).map_err(|pem_error| {
ClientBuildError::Generic {
message: format!("Failed to add a root certificate as DER ({der_error:?}) or PEM ({pem_error:?})"),
}
})?;
certificates.push(cert);
}
}
}
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_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(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 {
if 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);
}
inner_builder = inner_builder.with_threading_support(builder.threading_support);
let sdk_client = inner_builder.build().await?;
// Disable retries for this request to prevent it from being retried
// indefinitely
let config = sdk_client.request_config().disable_retry();
// Log server version information at info level.
if let Ok(server_info) = sdk_client.server_vendor_info(Some(config)).await {
tracing::info!(
server_name = %server_info.server_name,
version = %server_info.version,
"Connected to Matrix server"
);
} else {
tracing::warn!("Could not retrieve server version information");
}
Ok(Arc::new(
Client::new(
sdk_client,
builder.enable_oidc_refresh_lock,
builder.session_delegate,
store_path,
)
.await?,
))
}
}
#[cfg(not(target_family = "wasm"))]
#[matrix_sdk_ffi_macros::export]
impl ClientBuilder {
pub fn proxy(self: Arc<Self>, url: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.proxy = Some(url);
@@ -616,58 +124,154 @@ impl ClientBuilder {
Arc::new(builder)
}
pub fn add_root_certificates(
pub fn disable_automatic_token_refresh(self: Arc<Self>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.disable_automatic_token_refresh = true;
Arc::new(builder)
}
pub fn 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>,
certificates: Vec<CertificateBytes>,
process_id: String,
session_delegate: Arc<dyn ClientSessionDelegate>,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.additional_root_certificates = certificates;
builder.cross_process_refresh_lock_id = Some(process_id);
builder.session_delegate = Some(session_delegate);
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> {
pub(crate) fn set_session_delegate_inner(
self: Arc<Self>,
session_delegate: Arc<dyn ClientSessionDelegate>,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.disable_built_in_root_certificates = true;
builder.session_delegate = Some(session_delegate);
Arc::new(builder)
}
pub fn user_agent(self: Arc<Self>, user_agent: String) -> Arc<Self> {
pub(crate) fn server_name_with_protocol(
self: Arc<Self>,
server_name: String,
protocol: UrlScheme,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.user_agent = Some(user_agent);
builder.server_name = Some((server_name, protocol));
Arc::new(builder)
}
pub(crate) fn build_inner(self: Arc<Self>) -> anyhow::Result<Arc<Client>> {
let builder = unwrap_or_clone_arc(self);
let mut inner_builder = builder.inner;
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)?;
inner_builder = inner_builder.sqlite_store(&data_path, builder.passphrase.as_deref());
}
// Determine server either from URL, server name or user ID.
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."
);
}
if let Some(proxy) = builder.proxy {
inner_builder = inner_builder.proxy(proxy);
}
if builder.disable_ssl_verification {
inner_builder = inner_builder.disable_ssl_verification();
}
if !builder.disable_automatic_token_refresh {
inner_builder = inner_builder.handle_refresh_tokens();
}
if let Some(user_agent) = builder.user_agent {
inner_builder = inner_builder.user_agent(user_agent);
}
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>>()?,
);
}
let sdk_client = RUNTIME.block_on(async move { 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(Client::new(
sdk_client,
builder.cross_process_refresh_lock_id,
builder.session_delegate,
)?)
}
}
/// The store paths the client will use when built.
#[derive(Clone)]
struct SessionPaths {
/// The path that the client will use to store its data.
data_path: String,
/// The path that the client will use to store its caches. This path can be
/// the same as the data path if you prefer to keep everything in one place.
cache_path: String,
}
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);
#[derive(Clone, uniffi::Record)]
/// The config to use for HTTP requests by default in this client.
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,
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,
}
}
}
+25 -280
View File
@@ -1,52 +1,39 @@
use std::sync::Arc;
use futures_util::StreamExt;
use matrix_sdk::{
encryption,
encryption::{backups, recovery},
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
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,
@@ -166,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);
}
@@ -199,41 +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,
}
}
}
#[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());
@@ -257,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 {
@@ -270,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());
}
@@ -284,7 +242,7 @@ impl Encryption {
}
pub async fn is_last_device(&self) -> Result<bool> {
Ok(self.inner.recovery().is_last_device().await?)
Ok(self.inner.recovery().are_we_the_last_man_standing().await?)
}
pub async fn wait_for_backup_upload_steady_state(
@@ -297,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());
@@ -319,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();
@@ -330,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());
@@ -348,7 +299,6 @@ impl Encryption {
let ret = enable.await?;
task.abort();
passphrase.zeroize();
Ok(ret)
}
@@ -369,18 +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)
}
pub async fn recover(&self, mut recovery_key: String) -> Result<()> {
let result = self.inner.recovery().recover(&recovery_key).await;
@@ -388,197 +326,4 @@ impl Encryption {
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.
///
/// # Arguments
///
/// * `user_id` - The ID of the user that the identity belongs to.
///
/// Returns a `UserIdentity` if one is found. Returns an error if there
/// was an issue with the crypto store or with the request to the
/// homeserver.
///
/// This will always return `None` if the client hasn't been logged in.
pub async fn user_identity(
&self,
user_id: String,
) -> Result<Option<Arc<UserIdentity>>, ClientError> {
match self.inner.get_user_identity(user_id.as_str().try_into()?).await {
Ok(Some(identity)) => {
return Ok(Some(Arc::new(UserIdentity { inner: identity })));
}
Ok(None) => {
info!("No identity found in the store.");
}
Err(error) => {
error!("Failed fetching identity from the store: {error}");
}
}
info!("Requesting identity from the server.");
let identity = self.inner.request_user_identity(user_id.as_str().try_into()?).await?;
Ok(identity.map(|identity| Arc::new(UserIdentity { inner: identity })))
}
}
/// 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> {
if let Some(auth) = auth {
self.inner.reset(Some(auth.into())).await.map_err(ClientError::from_err)
} else {
self.inner.reset(None).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() }
}
}
+25 -656
View File
@@ -1,303 +1,117 @@
use std::{collections::HashMap, error::Error, fmt, fmt::Display};
use std::fmt::Display;
use matrix_sdk::{
authentication::oauth::OAuthError,
encryption::{identities::RequestVerificationError, CryptoStoreError},
event_cache::EventCacheError,
reqwest,
room::edit::EditError,
send_queue::RoomSendQueueError,
HttpError, IdParseError, NotificationSettingsError as SdkNotificationSettingsError,
QueueWedgeError as SdkQueueWedgeError, StoreError,
self, encryption::CryptoStoreError, oidc::OidcError, HttpError, IdParseError,
NotificationSettingsError as SdkNotificationSettingsError, StoreError,
};
use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline};
use ruma::{
api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter},
MilliSecondsSinceUnixEpoch,
};
use tracing::warn;
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 {
warn!("Error: {error}");
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() {
if let ErrorBody::Standard { 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<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)
}
}
/// 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)
}
}
@@ -314,8 +128,6 @@ pub enum RoomError {
TimelineUnavailable,
#[error("Invalid thumbnail data")]
InvalidThumbnailData,
#[error("Invalid replied to event ID")]
InvalidRepliedToEventId,
#[error("Failed sending attachment")]
FailedSendingAttachment,
}
@@ -374,446 +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: Option<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 { status, body } => Ok(ErrorKind::BadStatus {
status: status.map(|code| code.clone().as_u16()),
body: 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 { room_version } => {
Ok(ErrorKind::IncompatibleRoomVersion { room_version: room_version.to_string() })
}
RumaApiErrorKind::InvalidParam => Ok(ErrorKind::InvalidParam),
RumaApiErrorKind::InvalidRoomState => Ok(ErrorKind::InvalidRoomState),
RumaApiErrorKind::InvalidUsername => Ok(ErrorKind::InvalidUsername),
RumaApiErrorKind::LimitExceeded { retry_after } => {
let retry_after_ms = match 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 { admin_contact } => {
Ok(ErrorKind::ResourceLimitExceeded { admin_contact: admin_contact.to_owned() })
}
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 { soft_logout } => {
Ok(ErrorKind::UnknownToken { soft_logout: 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 { current_version } => {
Ok(ErrorKind::WrongRoomKeysVersion { current_version: current_version.to_owned() })
}
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),
}
}
}
+19 -256
View File
@@ -1,32 +1,16 @@
use std::ops::Deref;
use anyhow::{bail, Context};
use matrix_sdk::IdParseError;
use matrix_sdk_ui::timeline::TimelineEventItemId;
use ruma::{
events::{
room::{
message::{MessageType as RumaMessageType, Relation},
redaction::SyncRoomRedactionEvent,
},
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent,
MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
},
EventId,
use ruma::events::{
room::message::Relation, AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
AnyTimelineEvent, MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
};
use crate::{
room_member::MembershipState,
ruma::{MessageType, NotifyType},
utils::Timestamp,
ClientError,
};
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()
@@ -36,12 +20,12 @@ 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 event_type(&self) -> Result<TimelineEventType, ClientError> {
let event_type = match self.0.deref() {
let event_type = match &self.0 {
AnySyncTimelineEvent::MessageLike(event) => {
TimelineEventType::MessageLike { content: event.clone().try_into()? }
}
@@ -55,19 +39,11 @@ impl TimelineEvent {
impl From<AnyTimelineEvent> for TimelineEvent {
fn from(event: AnyTimelineEvent) -> Self {
Self(Box::new(event.into()))
Self(event.into())
}
}
#[derive(uniffi::Enum)]
// A note about this `allow(clippy::large_enum_variant)`.
// In order to reduce the size of `TimelineEventType`, 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 TimelineEventType {
MessageLike { content: MessageLikeEventContent },
State { content: StateEventContent },
@@ -93,7 +69,7 @@ pub enum StateEventContent {
RoomServerAcl,
RoomThirdPartyInvite,
RoomTombstone,
RoomTopic { topic: String },
RoomTopic,
SpaceChild,
SpaceParent,
}
@@ -119,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,
@@ -128,32 +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,
CallNotify { notify_type: NotifyType },
CallHangup,
CallCandidates,
KeyVerificationReady,
@@ -167,7 +130,7 @@ pub enum MessageLikeEventContent {
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> },
RoomRedaction,
Sticker,
}
@@ -178,12 +141,6 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
let content = match value {
AnySyncMessageLikeEvent::CallAnswer(_) => MessageLikeEventContent::CallAnswer,
AnySyncMessageLikeEvent::CallInvite(_) => MessageLikeEventContent::CallInvite,
AnySyncMessageLikeEvent::CallNotify(content) => {
let original_content = get_message_like_event_original_content(content)?;
MessageLikeEventContent::CallNotify {
notify_type: original_content.notify_type.into(),
}
}
AnySyncMessageLikeEvent::CallHangup(_) => MessageLikeEventContent::CallHangup,
AnySyncMessageLikeEvent::CallCandidates(_) => MessageLikeEventContent::CallCandidates,
AnySyncMessageLikeEvent::KeyVerificationReady(_) => {
@@ -228,23 +185,13 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
_ => 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)
}
@@ -269,187 +216,3 @@ where
event.as_original().context("Failed to get original content")?.content.clone();
Ok(original_content)
}
#[derive(Clone, uniffi::Enum)]
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 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,
}
}
}
#[derive(Clone, uniffi::Enum)]
pub enum MessageLikeEventType {
CallAnswer,
CallCandidates,
CallHangup,
CallInvite,
CallNotify,
KeyVerificationAccept,
KeyVerificationCancel,
KeyVerificationDone,
KeyVerificationKey,
KeyVerificationMac,
KeyVerificationReady,
KeyVerificationStart,
PollEnd,
PollResponse,
PollStart,
Reaction,
RoomEncrypted,
RoomMessage,
RoomRedaction,
Sticker,
UnstablePollEnd,
UnstablePollResponse,
UnstablePollStart,
}
impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
fn from(val: MessageLikeEventType) -> Self {
match val {
MessageLikeEventType::CallAnswer => Self::CallAnswer,
MessageLikeEventType::CallInvite => Self::CallInvite,
MessageLikeEventType::CallNotify => Self::CallNotify,
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::Reaction => Self::Reaction,
MessageLikeEventType::RoomEncrypted => Self::RoomEncrypted,
MessageLikeEventType::RoomMessage => Self::RoomMessage,
MessageLikeEventType::RoomRedaction => Self::RoomRedaction,
MessageLikeEventType::Sticker => Self::Sticker,
MessageLikeEventType::PollEnd => Self::PollEnd,
MessageLikeEventType::PollResponse => Self::PollResponse,
MessageLikeEventType::PollStart => Self::PollStart,
MessageLikeEventType::UnstablePollEnd => Self::UnstablePollEnd,
MessageLikeEventType::UnstablePollResponse => Self::UnstablePollResponse,
MessageLikeEventType::UnstablePollStart => Self::UnstablePollStart,
}
}
}
#[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,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::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,
}
+29 -16
View File
@@ -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,41 +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 session_verification;
mod spaces;
mod sync_service;
mod task_handle;
mod timeline;
mod tracing;
mod utd;
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,32 +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 crate::ruma::LocationContent;
#[derive(uniffi::Record)]
pub struct LastLocation {
/// The most recent location content of the user.
pub location: LocationContent,
/// A timestamp in milliseconds since Unix Epoch on that day in local
/// time.
pub ts: u64,
}
/// Details of a users live location share.
#[derive(uniffi::Record)]
pub struct LiveLocationShare {
/// The user's last known location.
pub last_location: LastLocation,
/// The live status of the live location share.
pub(crate) is_live: bool,
/// The user ID of the person sharing their live location.
pub user_id: String,
}
+57 -139
View File
@@ -1,16 +1,14 @@
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,
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)]
@@ -23,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)]
@@ -31,8 +28,6 @@ pub struct NotificationRoomInfo {
pub display_name: String,
pub avatar_url: Option<String>,
pub canonical_alias: Option<String>,
pub topic: Option<String>,
pub join_rule: Option<JoinRule>,
pub joined_members_count: u64,
pub is_encrypted: Option<bool>,
pub is_direct: bool,
@@ -50,184 +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>,
}
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() }
}
};
Self {
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,
is_encrypted: item.is_room_encrypted,
is_direct: item.is_direct_message_room,
},
is_noisy: item.is_noisy,
has_mention: item.has_mention,
thread_id: item.thread_id.map(|t| t.to_string()),
}
}
}
#[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,
#[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,
}
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,4 +1,4 @@
use std::sync::{Arc, RwLock};
use std::sync::Arc;
use matrix_sdk::{
event_handler::EventHandlerHandle,
@@ -9,352 +9,15 @@ use matrix_sdk::{
ruma::events::push_rules::PushRulesEvent,
Client as MatrixClient,
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use ruma::{
push::{
Action as SdkAction, ComparisonOperator as SdkComparisonOperator, PredefinedOverrideRuleId,
PredefinedUnderrideRuleId, PushCondition as SdkPushCondition, RoomMemberCountIs,
RuleKind as SdkRuleKind, ScalarJsonValue as SdkJsonValue, Tweak as SdkTweak,
},
Int, RoomId, UInt,
push::{PredefinedOverrideRuleId, PredefinedUnderrideRuleId, RuleKind},
RoomId,
};
use tokio::sync::RwLock as AsyncRwLock;
use tokio::sync::RwLock;
use super::RUNTIME;
use crate::error::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 { key, pattern } => Self::EventMatch { key, pattern },
SdkPushCondition::ContainsDisplayName => Self::ContainsDisplayName,
SdkPushCondition::RoomMemberCount { is } => {
Self::RoomMemberCount { prefix: is.prefix.into(), count: is.count.into() }
}
SdkPushCondition::SenderNotificationPermission { key } => {
Self::SenderNotificationPermission { key: key.to_string() }
}
SdkPushCondition::EventPropertyIs { key, value } => {
Self::EventPropertyIs { key, value: value.into() }
}
SdkPushCondition::EventPropertyContains { key, value } => {
Self::EventPropertyContains { key, value: 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 { key, pattern },
PushCondition::ContainsDisplayName => Self::ContainsDisplayName,
PushCondition::RoomMemberCount { prefix, count } => Self::RoomMemberCount {
is: RoomMemberCountIs {
prefix: prefix.into(),
count: UInt::new(count).unwrap_or_default(),
},
},
PushCondition::SenderNotificationPermission { key } => {
Self::SenderNotificationPermission { key: key.into() }
}
PushCondition::EventPropertyIs { key, value } => {
Self::EventPropertyIs { key, value: value.into() }
}
PushCondition::EventPropertyContains { key, value } => {
Self::EventPropertyContains { key, value: 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 },
SdkTweak::Highlight(highlight) => Self::Highlight { value: highlight },
SdkTweak::Custom { name, value } => {
let json_string = serde_json::to_string(&value)
.map_err(|e| format!("Failed to serialize custom tweak value: {e}"))?;
Self::Custom { name, value: json_string }
}
_ => return Err("Unsupported tweak type".to_owned()),
})
}
}
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),
Tweak::Highlight { value } => Self::Highlight(value),
Tweak::Custom { name, value } => {
let json_value: serde_json::Value = serde_json::from_str(&value)
.map_err(|e| format!("Failed to deserialize custom tweak value: {e}"))?;
let value = serde_json::from_value(json_value)
.map_err(|e| format!("Failed to convert JSON value: {e}"))?;
Self::Custom { name, value }
}
})
}
}
#[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}"))?,
),
})
}
}
/// Enum representing the push notification modes for a room.
#[derive(Clone, uniffi::Enum)]
pub enum RoomNotificationMode {
@@ -387,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);
}
@@ -410,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>>>,
}
@@ -419,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)),
}
}
@@ -430,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 {
@@ -448,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;
});
}
}
@@ -473,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
@@ -490,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))
}
@@ -500,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(())
}
@@ -605,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)
}
@@ -618,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,
)
@@ -630,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,
@@ -670,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,
)
@@ -682,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)
}
@@ -691,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(())
}
@@ -701,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?;
@@ -716,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,
)
@@ -724,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
File diff suppressed because it is too large Load Diff
-166
View File
@@ -1,166 +0,0 @@
use std::sync::Arc;
use matrix_sdk::{
authentication::oauth::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason},
crypto::types::qr_login::{LoginQrCodeDecodeError, QrCodeModeData},
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use tracing::error;
/// Data for the QR code login mechanism.
///
/// The [`QrCodeData`] can be serialized and encoded as a QR code or it can be
/// decoded from a QR code.
#[derive(Debug, uniffi::Object)]
pub struct QrCodeData {
pub(crate) inner: qrcode::QrCodeData,
}
#[matrix_sdk_ffi_macros::export]
impl QrCodeData {
/// Attempt to decode a slice of bytes into a [`QrCodeData`] object.
///
/// The slice of bytes would generally be returned by a QR code decoder.
#[uniffi::constructor]
pub fn from_bytes(bytes: Vec<u8>) -> Result<Arc<Self>, QrCodeDecodeError> {
Ok(Self { inner: qrcode::QrCodeData::from_bytes(&bytes)? }.into())
}
/// The server name contained within the scanned QR code data.
///
/// Note: This value is only present when scanning a QR code the belongs to
/// a logged in client. The mode where the new client shows the QR code
/// will return `None`.
pub fn server_name(&self) -> Option<String> {
match &self.inner.mode_data {
QrCodeModeData::Reciprocate { server_name } => Some(server_name.to_owned()),
QrCodeModeData::Login => None,
}
}
}
/// Error type for the decoding of the [`QrCodeData`].
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum QrCodeDecodeError {
#[error("Error decoding QR code: {error:?}")]
Crypto {
#[from]
error: LoginQrCodeDecodeError,
},
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum HumanQrLoginError {
#[error("Linking with this device is not supported.")]
LinkingNotSupported,
#[error("The sign in was cancelled.")]
Cancelled,
#[error("The sign in was not completed in the required time.")]
Expired,
#[error("A secure connection could not have been established between the two devices.")]
ConnectionInsecure,
#[error("The sign in was declined.")]
Declined,
#[error("An unknown error has happened.")]
Unknown,
#[error("The homeserver doesn't provide sliding sync in its configuration.")]
SlidingSyncNotAvailable,
#[error("Unable to use OIDC as the supplied client metadata is invalid.")]
OidcMetadataInvalid,
#[error("The other device is not signed in and as such can't sign in other devices.")]
OtherDeviceNotSignedIn,
}
impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
fn from(value: qrcode::QRCodeLoginError) -> Self {
use qrcode::{QRCodeLoginError, SecureChannelError};
match value {
QRCodeLoginError::LoginFailure { reason, .. } => match reason {
LoginFailureReason::UnsupportedProtocol => HumanQrLoginError::LinkingNotSupported,
LoginFailureReason::AuthorizationExpired => HumanQrLoginError::Expired,
LoginFailureReason::UserCancelled => HumanQrLoginError::Cancelled,
_ => HumanQrLoginError::Unknown,
},
QRCodeLoginError::OAuth(e) => {
if let Some(e) = e.as_request_token_error() {
match e {
DeviceCodeErrorResponseType::AccessDenied => HumanQrLoginError::Declined,
DeviceCodeErrorResponseType::ExpiredToken => HumanQrLoginError::Expired,
_ => HumanQrLoginError::Unknown,
}
} else {
HumanQrLoginError::Unknown
}
}
QRCodeLoginError::SecureChannel(e) => match e {
SecureChannelError::Utf8(_)
| SecureChannelError::MessageDecode(_)
| SecureChannelError::Json(_)
| SecureChannelError::RendezvousChannel(_) => HumanQrLoginError::Unknown,
SecureChannelError::SecureChannelMessage { .. }
| SecureChannelError::Ecies(_)
| SecureChannelError::InvalidCheckCode => HumanQrLoginError::ConnectionInsecure,
SecureChannelError::InvalidIntent => HumanQrLoginError::OtherDeviceNotSignedIn,
},
QRCodeLoginError::UnexpectedMessage { .. }
| QRCodeLoginError::CrossProcessRefreshLock(_)
| QRCodeLoginError::DeviceKeyUpload(_)
| QRCodeLoginError::SessionTokens(_)
| QRCodeLoginError::UserIdDiscovery(_)
| QRCodeLoginError::SecretImport(_) => HumanQrLoginError::Unknown,
}
}
}
/// Enum describing the progress of the QR-code login.
#[derive(Debug, Default, Clone, uniffi::Enum)]
pub enum QrLoginProgress {
/// The login process is starting.
#[default]
Starting,
/// We established a secure channel with the other device.
EstablishingSecureChannel {
/// The check code that the device should display so the other device
/// can confirm that the channel is secure as well.
check_code: u8,
/// The string representation of the check code, will be guaranteed to
/// be 2 characters long, preserving the leading zero if the
/// first digit is a zero.
check_code_string: String,
},
/// We are waiting for the login and for the OAuth 2.0 authorization server
/// to give us an access token.
WaitingForToken { user_code: String },
/// The login has successfully finished.
Done,
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait QrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm {
fn on_update(&self, state: QrLoginProgress);
}
impl From<qrcode::LoginProgress> for QrLoginProgress {
fn from(value: qrcode::LoginProgress) -> Self {
use qrcode::LoginProgress;
match value {
LoginProgress::Starting => Self::Starting,
LoginProgress::EstablishingSecureChannel { check_code } => {
let check_code = check_code.to_digit();
Self::EstablishingSecureChannel {
check_code,
check_code_string: format!("{check_code:02}"),
}
}
LoginProgress::WaitingForToken { user_code } => Self::WaitingForToken { user_code },
LoginProgress::Done => Self::Done,
}
}
}
+551
View File
@@ -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,233 +0,0 @@
use std::collections::HashMap;
use anyhow::Result;
use ruma::{
events::{room::power_levels::RoomPowerLevels as RumaPowerLevels, TimelineEventType},
OwnedUserId, UserId,
};
use crate::{
error::ClientError,
event::{MessageLikeEventType, StateEventType},
};
#[derive(uniffi::Object)]
pub struct RoomPowerLevels {
inner: RumaPowerLevels,
own_user_id: OwnedUserId,
}
impl RoomPowerLevels {
pub fn new(value: RumaPowerLevels, own_user_id: OwnedUserId) -> Self {
Self { inner: value, own_user_id }
}
}
#[matrix_sdk_ffi_macros::export]
impl RoomPowerLevels {
fn values(&self) -> RoomPowerLevelsValues {
self.inner.clone().into()
}
/// Gets a map with the `UserId` of users with power levels other than `0`
/// and their power level.
pub fn user_power_levels(&self) -> HashMap<String, i64> {
let mut user_power_levels = HashMap::<String, i64>::new();
for (id, level) in self.inner.users.iter() {
user_power_levels.insert(id.to_string(), (*level).into());
}
user_power_levels
}
/// Returns true if the current user is able to ban in the room.
pub fn can_own_user_ban(&self) -> bool {
self.inner.user_can_ban(&self.own_user_id)
}
/// Returns true if the user with the given user_id is able to ban in the
/// room.
///
/// The call may fail if there is an error in getting the power levels.
pub fn can_user_ban(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.user_can_ban(&user_id))
}
/// Returns true if the current user is able to redact their own messages in
/// the room.
pub fn can_own_user_redact_own(&self) -> bool {
self.inner.user_can_redact_own_event(&self.own_user_id)
}
/// Returns true if the user with the given user_id is able to redact
/// their own messages in the room.
///
/// The call may fail if there is an error in getting the power levels.
pub fn can_user_redact_own(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.user_can_redact_own_event(&user_id))
}
/// Returns true if the current user user is able to redact messages of
/// other users in the room.
pub fn can_own_user_redact_other(&self) -> bool {
self.inner.user_can_redact_event_of_other(&self.own_user_id)
}
/// Returns true if the user with the given user_id is able to redact
/// messages of other users in the room.
///
/// The call may fail if there is an error in getting the power levels.
pub fn can_user_redact_other(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.user_can_redact_event_of_other(&user_id))
}
/// Returns true if the current user is able to invite in the room.
pub fn can_own_user_invite(&self) -> bool {
self.inner.user_can_invite(&self.own_user_id)
}
/// Returns true if the user with the given user_id is able to invite in the
/// room.
///
/// The call may fail if there is an error in getting the power levels.
pub fn can_user_invite(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.user_can_invite(&user_id))
}
/// Returns true if the current user is able to kick in the room.
pub fn can_own_user_kick(&self) -> bool {
self.inner.user_can_kick(&self.own_user_id)
}
/// Returns true if the user with the given user_id is able to kick in the
/// room.
///
/// The call may fail if there is an error in getting the power levels.
pub fn can_user_kick(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.user_can_kick(&user_id))
}
/// Returns true if the current user is able to send a specific state event
/// type in the room.
pub fn can_own_user_send_state(&self, state_event: StateEventType) -> bool {
self.inner.user_can_send_state(&self.own_user_id, state_event.into())
}
/// Returns true if the user with the given user_id is able to send a
/// specific state event type in the room.
///
/// The call may fail if there is an error in getting the power levels.
pub fn can_user_send_state(
&self,
user_id: String,
state_event: StateEventType,
) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.user_can_send_state(&user_id, state_event.into()))
}
/// Returns true if the current user is able to send a specific message type
/// in the room.
pub fn can_own_user_send_message(&self, message: MessageLikeEventType) -> bool {
self.inner.user_can_send_message(&self.own_user_id, message.into())
}
/// Returns true if the user with the given user_id is able to send a
/// specific message type in the room.
///
/// The call may fail if there is an error in getting the power levels.
pub fn can_user_send_message(
&self,
user_id: String,
message: MessageLikeEventType,
) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.user_can_send_message(&user_id, message.into()))
}
/// Returns true if the current user is able to pin or unpin events in the
/// room.
pub fn can_own_user_pin_unpin(&self) -> bool {
self.inner.user_can_send_state(&self.own_user_id, StateEventType::RoomPinnedEvents.into())
}
/// Returns true if the user with the given user_id is able to pin or unpin
/// events in the room.
///
/// The call may fail if there is an error in getting the power levels.
pub fn can_user_pin_unpin(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.user_can_send_state(&user_id, StateEventType::RoomPinnedEvents.into()))
}
/// Returns true if the current user is able to trigger a notification in
/// the room.
pub fn can_own_user_trigger_room_notification(&self) -> bool {
self.inner.user_can_trigger_room_notification(&self.own_user_id)
}
/// Returns true if the user with the given user_id is able to trigger a
/// notification in the room.
///
/// The call may fail if there is an error in getting the power levels.
pub fn can_user_trigger_room_notification(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.user_can_trigger_room_notification(&user_id))
}
}
/// This intermediary struct is used to expose the power levels values through
/// FFI and work around it not exposing public exported object fields.
#[derive(uniffi::Record)]
pub struct RoomPowerLevelsValues {
/// The level required to ban a user.
pub ban: i64,
/// The level required to invite a user.
pub invite: i64,
/// The level required to kick a user.
pub kick: i64,
/// The level required to redact an event.
pub redact: i64,
/// The default level required to send message events.
pub events_default: i64,
/// The default level required to send state events.
pub state_default: i64,
/// The default power level for every user in the room.
pub users_default: i64,
/// The level required to change the room's name.
pub room_name: i64,
/// The level required to change the room's avatar.
pub room_avatar: i64,
/// The level required to change the room's topic.
pub room_topic: i64,
}
impl From<RumaPowerLevels> for RoomPowerLevelsValues {
fn from(value: RumaPowerLevels) -> Self {
fn state_event_level_for(
power_levels: &RumaPowerLevels,
event_type: &TimelineEventType,
) -> i64 {
let default_state: i64 = power_levels.state_default.into();
power_levels.events.get(event_type).map_or(default_state, |&level| level.into())
}
Self {
ban: value.ban.into(),
invite: value.invite.into(),
kick: value.kick.into(),
redact: value.redact.into(),
events_default: value.events_default.into(),
state_default: value.state_default.into(),
users_default: value.users_default.into(),
room_name: state_event_level_for(&value, &TimelineEventType::RoomName),
room_avatar: state_event_level_for(&value, &TimelineEventType::RoomAvatar),
room_topic: state_event_level_for(&value, &TimelineEventType::RoomTopic),
}
}
}
@@ -1,168 +0,0 @@
use std::sync::Arc;
use matrix_sdk::{EncryptionState, RoomState};
use tracing::warn;
use crate::{
client::JoinRule,
error::ClientError,
notification_settings::RoomNotificationMode,
room::{
power_levels::RoomPowerLevels, Membership, RoomHero, RoomHistoryVisibility, SuccessorRoom,
},
room_member::RoomMember,
};
#[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,
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,
highlight_count: u64,
notification_count: u64,
cached_user_defined_notification_mode: Option<RoomNotificationMode>,
has_room_call: bool,
active_room_call_participants: Vec<String>,
/// 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(),
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(),
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(),
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(),
})
}
}
-17
View File
@@ -1,17 +0,0 @@
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,204 +0,0 @@
// Copyright 2024 Mauro Romito
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{fmt::Debug, 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>);
}
+92
View File
@@ -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(),
})
}
}
+258 -251
View File
@@ -1,32 +1,29 @@
#![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::{pin_mut, StreamExt};
use matrix_sdk::{
ruma::{
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
RoomId,
api::client::sync::sync_events::{
v4::RoomSubscription as RumaRoomSubscription,
UnreadNotificationsCount as RumaUnreadNotificationsCount,
},
assign, RoomId,
},
Room as SdkRoom,
RoomListEntry as MatrixRoomListEntry,
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use matrix_sdk_ui::{
room_list_service::filters::{
new_filter_all, new_filter_any, new_filter_category, new_filter_deduplicate_versions,
new_filter_favourite, new_filter_fuzzy_match_room_name, 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,
BoxedFilterFn, RoomCategory,
},
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::{
room::{Membership, Room},
runtime::get_runtime_handle,
TaskHandle,
error::ClientError,
room::Room,
room_info::RoomInfo,
timeline::{EventTimelineItem, Timeline},
TaskHandle, RUNTIME,
};
#[derive(Debug, thiserror::Error, uniffi::Error)]
@@ -41,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 {
@@ -57,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() },
}
}
}
@@ -69,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 {
@@ -89,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> {
@@ -102,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,
@@ -113,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 {
@@ -121,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)]
@@ -144,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,
@@ -154,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 {
@@ -164,121 +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 correct 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(|room| RoomListEntriesUpdate::from(utd_hook.clone(), room))
.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)]
@@ -348,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) {
@@ -453,69 +401,128 @@ impl RoomListDynamicEntriesController {
#[derive(uniffi::Enum)]
pub enum RoomListEntriesDynamicFilterKind {
All { filters: Vec<RoomListEntriesDynamicFilterKind> },
Any { filters: Vec<RoomListEntriesDynamicFilterKind> },
NonSpace,
NonLeft,
// Not { filter: RoomListEntriesDynamicFilterKind } - requires recursive enum
// support in uniffi https://github.com/mozilla/uniffi-rs/issues/396
Joined,
Unread,
Favourite,
LowPriority,
NonLowPriority,
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::NonLeft => Box::new(new_filter_non_left()),
Kind::NonSpace => Box::new(new_filter_not(Box::new(new_filter_space()))),
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::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())
})
}
}
@@ -525,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
+185 -128
View File
@@ -1,7 +1,7 @@
use matrix_sdk::room::{RoomMember as SdkRoomMember, RoomMemberRole};
use ruma::{events::room::power_levels::UserPowerLevel, UserId};
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,162 +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 normalized_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()?,
normalized_power_level: m.normalized_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,
}
}
}
-167
View File
@@ -1,167 +0,0 @@
use anyhow::Context as _;
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, Client};
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,
}
}
}
File diff suppressed because it is too large Load Diff
-110
View File
@@ -1,110 +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 the specific language governing permissions and
// limitations under the License.
//! Runtime abstractions for cross-platform async execution. This provides
//! a stand-in for tokio's `get_runtime_handle` method that will work on
//! both Wasm and non-Wasm platforms. It also provides corresponding types
//! that can be used in place of tokio's `Handle` and `Runtime` types.
#[cfg(not(target_family = "wasm"))]
mod sys {
pub use tokio::runtime::Handle;
/// Get a runtime handle appropriate for the current target platform.
///
/// This function returns a unified `Handle` type that works across both
/// Wasm and non-Wasm platforms, allowing code to be written that is
/// agnostic to the platform-specific runtime implementation.
///
/// Returns:
/// - A `tokio::runtime::Handle` on non-Wasm platforms
/// - A `WasmRuntimeHandle` on Wasm platforms
pub fn get_runtime_handle() -> Handle {
async_compat::get_runtime_handle()
}
}
#[cfg(target_family = "wasm")]
mod sys {
use std::future::Future;
use matrix_sdk_common::executor::{spawn, JoinHandle};
/// A dummy guard that does nothing when dropped.
/// This is used for the Wasm implementation to match
/// tokio::runtime::EnterGuard.
#[derive(Debug)]
pub struct RuntimeGuard;
/// A runtime handle implementation for WebAssembly targets.
///
/// This implements a minimal subset of the tokio::runtime::Handle API
/// that is needed for the matrix-rust-sdk to function on Wasm.
#[derive(Default, Debug)]
pub struct Handle;
pub type Runtime = Handle;
impl Handle {
/// Spawns a future in the wasm32 bindgen runtime.
#[track_caller]
pub fn spawn<F>(&self, future: F) -> JoinHandle<F::Output>
where
F: Future + 'static,
F::Output: 'static,
{
spawn(future)
}
/// Runs the provided function on an executor dedicated to blocking
/// operations.
#[track_caller]
pub fn spawn_blocking<F, R>(&self, func: F) -> JoinHandle<R>
where
F: FnOnce() -> R + 'static,
R: 'static,
{
spawn(async move { func() })
}
/// Runs a future to completion on the current thread.
pub fn block_on<F, T>(&self, future: F) -> T
where
F: Future<Output = T>,
{
futures_executor::block_on(future)
}
/// Enters the runtime context.
///
/// For WebAssembly, this is a no-op that returns a dummy guard.
pub fn enter(&self) -> RuntimeGuard {
RuntimeGuard
}
}
/// Get a runtime handle appropriate for the current target platform.
///
/// This function returns a unified `Handle` type that works across both
/// Wasm and non-Wasm platforms, allowing code to be written that is
/// agnostic to the platform-specific runtime implementation.
///
/// Returns:
/// - A `tokio::runtime::Handle` on non-Wasm platforms
/// - A `WasmRuntimeHandle` on Wasm platforms
pub fn get_runtime_handle() -> Handle {
Handle
}
}
pub use sys::*;
@@ -1,22 +1,18 @@
use std::sync::{Arc, RwLock};
use anyhow::Context as _;
use futures_util::StreamExt;
use matrix_sdk::{
encryption::{
identities::UserIdentity,
verification::{SasState, SasVerification, VerificationRequest, VerificationRequestState},
verification::{SasState, SasVerification, VerificationRequest},
Encryption,
},
ruma::events::key::verification::VerificationMethod,
Account,
ruma::events::{key::verification::VerificationMethod, AnyToDeviceEvent},
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use ruma::UserId;
use tracing::{error, warn};
use crate::{
client::UserProfile, error::ClientError, runtime::get_runtime_handle, utils::Timestamp,
};
use super::RUNTIME;
use crate::error::ClientError;
#[derive(uniffi::Object)]
pub struct SessionVerificationEmoji {
@@ -24,7 +20,7 @@ pub struct SessionVerificationEmoji {
description: String,
}
#[matrix_sdk_ffi_macros::export]
#[uniffi::export]
impl SessionVerificationEmoji {
pub fn symbol(&self) -> String {
self.symbol.clone()
@@ -41,20 +37,8 @@ pub enum SessionVerificationData {
Decimals { values: Vec<u16> },
}
/// Details about the incoming verification request
#[derive(uniffi::Record)]
pub struct SessionVerificationRequestDetails {
sender_profile: UserProfile,
flow_id: String,
device_id: String,
device_display_name: Option<String>,
/// First time this device was seen in milliseconds since epoch.
first_seen_timestamp: Timestamp,
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SessionVerificationControllerDelegate: SyncOutsideWasm + SendOutsideWasm {
fn did_receive_verification_request(&self, details: SessionVerificationRequestDetails);
#[uniffi::export(callback_interface)]
pub trait SessionVerificationControllerDelegate: Sync + Send {
fn did_accept_verification_request(&self);
fn did_start_sas_verification(&self);
fn did_receive_verification_data(&self, data: SessionVerificationData);
@@ -69,104 +53,55 @@ pub type Delegate = Arc<RwLock<Option<Box<dyn SessionVerificationControllerDeleg
pub struct SessionVerificationController {
encryption: Encryption,
user_identity: UserIdentity,
account: Account,
delegate: Delegate,
verification_request: Arc<RwLock<Option<VerificationRequest>>>,
sas_verification: Arc<RwLock<Option<SasVerification>>>,
}
#[matrix_sdk_ffi_macros::export]
#[uniffi::export(async_runtime = "tokio")]
impl SessionVerificationController {
pub async fn is_verified(&self) -> Result<bool, ClientError> {
let device =
self.encryption.get_own_device().await?.context("Our own device is missing")?;
Ok(device.is_cross_signed_by_owner())
}
pub fn set_delegate(&self, delegate: Option<Box<dyn SessionVerificationControllerDelegate>>) {
*self.delegate.write().unwrap() = delegate;
}
/// Set this particular request as the currently active one and register for
/// events pertaining it.
/// * `sender_id` - The user requesting verification.
/// * `flow_id` - - The ID that uniquely identifies the verification flow.
pub async fn acknowledge_verification_request(
&self,
sender_id: String,
flow_id: String,
) -> Result<(), ClientError> {
let sender_id = UserId::parse(sender_id.clone())?;
pub async fn request_verification(&self) -> Result<(), ClientError> {
let methods = vec![VerificationMethod::SasV1];
let verification_request = self
.encryption
.get_verification_request(&sender_id, flow_id)
.user_identity
.request_verification_with_methods(methods)
.await
.ok_or(ClientError::from_str("Unknown session verification request", None))?;
self.set_ongoing_verification_request(verification_request)
}
/// Accept the previously acknowledged verification request
pub async fn accept_verification_request(&self) -> Result<(), ClientError> {
let verification_request = self.verification_request.read().unwrap().clone();
if let Some(verification_request) = verification_request {
let methods = vec![VerificationMethod::SasV1];
verification_request.accept_with_methods(methods).await?;
}
.map_err(anyhow::Error::from)?;
*self.verification_request.write().unwrap() = Some(verification_request);
Ok(())
}
/// Request verification for the current device
pub async fn request_device_verification(&self) -> Result<(), ClientError> {
let methods = vec![VerificationMethod::SasV1];
let verification_request =
self.user_identity.request_verification_with_methods(methods).await?;
self.set_ongoing_verification_request(verification_request)
}
/// Request verification for the given user
pub async fn request_user_verification(&self, user_id: String) -> Result<(), ClientError> {
let user_id = UserId::parse(user_id)?;
let user_identity = self
.encryption
.get_user_identity(&user_id)
.await?
.ok_or(ClientError::from_str("Unknown user identity", None))?;
if user_identity.is_verified() {
return Err(ClientError::from_str("User is already verified", None));
}
let methods = vec![VerificationMethod::SasV1];
let verification_request = user_identity.request_verification_with_methods(methods).await?;
self.set_ongoing_verification_request(verification_request)
}
/// Transition the current verification request into a SAS verification
/// flow.
pub async fn start_sas_verification(&self) -> Result<(), ClientError> {
let verification_request = self.verification_request.read().unwrap().clone();
let Some(verification_request) = verification_request else {
return Err(ClientError::from_str("Verification request missing.", None));
};
if let Some(verification) = verification_request {
match verification.start_sas().await {
Ok(Some(verification)) => {
*self.sas_verification.write().unwrap() = Some(verification.clone());
match verification_request.start_sas().await {
Ok(Some(verification)) => {
*self.sas_verification.write().unwrap() = Some(verification.clone());
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_start_sas_verification()
}
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_start_sas_verification()
let delegate = self.delegate.clone();
RUNTIME.spawn(Self::listen_to_changes(delegate, verification));
}
let delegate = self.delegate.clone();
get_runtime_handle()
.spawn(Self::listen_to_sas_verification_changes(verification, delegate));
}
_ => {
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_fail()
_ => {
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_fail()
}
}
}
}
@@ -174,177 +109,98 @@ impl SessionVerificationController {
Ok(())
}
/// Confirm that the short auth strings match on both sides.
pub async fn approve_verification(&self) -> Result<(), ClientError> {
let sas_verification = self.sas_verification.read().unwrap().clone();
if let Some(sas_verification) = sas_verification {
sas_verification.confirm().await?;
}
let Some(sas_verification) = sas_verification else {
return Err(ClientError::from_str("SAS verification missing", None));
};
Ok(sas_verification.confirm().await?)
Ok(())
}
/// Reject the short auth string
pub async fn decline_verification(&self) -> Result<(), ClientError> {
let sas_verification = self.sas_verification.read().unwrap().clone();
if let Some(sas_verification) = sas_verification {
sas_verification.mismatch().await?;
}
let Some(sas_verification) = sas_verification else {
return Err(ClientError::from_str("SAS verification missing", None));
};
Ok(sas_verification.mismatch().await?)
Ok(())
}
/// Cancel the current verification request
pub async fn cancel_verification(&self) -> Result<(), ClientError> {
let verification_request = self.verification_request.read().unwrap().clone();
if let Some(verification) = verification_request {
verification.cancel().await?;
}
let Some(verification_request) = verification_request else {
return Err(ClientError::from_str("Verification request missing.", None));
};
Ok(verification_request.cancel().await?)
Ok(())
}
}
impl SessionVerificationController {
pub(crate) fn new(
encryption: Encryption,
user_identity: UserIdentity,
account: Account,
) -> Self {
pub(crate) fn new(encryption: Encryption, user_identity: UserIdentity) -> Self {
SessionVerificationController {
encryption,
user_identity,
account,
delegate: Arc::new(RwLock::new(None)),
verification_request: Arc::new(RwLock::new(None)),
sas_verification: Arc::new(RwLock::new(None)),
}
}
/// Ask the controller to process an incoming request based on the sender
/// and flow identifier. It will fetch the request, verify that it's in the
/// correct state and then and notify the delegate.
pub(crate) async fn process_incoming_verification_request(
&self,
sender: &UserId,
flow_id: impl AsRef<str>,
) {
if sender != self.user_identity.user_id() {
if let Some(status) = self.encryption.cross_signing_status().await {
if !status.is_complete() {
warn!(
"Cannot verify other users until our own device's cross-signing status \
is complete: {status:?}"
);
pub(crate) async fn process_to_device_message(&self, event: AnyToDeviceEvent) {
match event {
// TODO: Use the changes stream for this as well once we expose
// VerificationRequest::changes() in the main crate.
AnyToDeviceEvent::KeyVerificationStart(event) => {
if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) {
return;
}
}
}
if let Some(verification) = self
.encryption
.get_verification(
self.user_identity.user_id(),
event.content.transaction_id.as_str(),
)
.await
{
if let Some(sas_verification) = verification.sas() {
*self.sas_verification.write().unwrap() = Some(sas_verification.clone());
let Some(request) = self.encryption.get_verification_request(sender, flow_id).await else {
error!("Failed retrieving verification request");
return;
};
if sas_verification.accept().await.is_ok() {
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_start_sas_verification()
}
let VerificationRequestState::Requested { other_device_data, .. } = request.state() else {
error!("Received verification request event but the request is in the wrong state.");
return;
};
let Ok(sender_profile) = UserProfile::fetch(&self.account, sender).await else {
error!("Failed fetching user profile for verification request");
return;
};
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_receive_verification_request(SessionVerificationRequestDetails {
sender_profile,
flow_id: request.flow_id().into(),
device_id: other_device_data.device_id().into(),
device_display_name: other_device_data.display_name().map(str::to_string),
first_seen_timestamp: other_device_data.first_time_seen_ts().into(),
});
}
}
fn set_ongoing_verification_request(
&self,
verification_request: VerificationRequest,
) -> Result<(), ClientError> {
if let Some(ongoing_verification_request) =
self.verification_request.read().unwrap().clone()
{
if !ongoing_verification_request.is_done()
&& !ongoing_verification_request.is_cancelled()
{
return Err(ClientError::from_str(
"There is another verification flow ongoing.",
None,
));
}
}
*self.verification_request.write().unwrap() = Some(verification_request.clone());
get_runtime_handle().spawn(Self::listen_to_verification_request_changes(
verification_request,
self.sas_verification.clone(),
self.delegate.clone(),
));
Ok(())
}
async fn listen_to_verification_request_changes(
verification_request: VerificationRequest,
sas_verification: Arc<RwLock<Option<SasVerification>>>,
delegate: Delegate,
) {
let mut stream = verification_request.changes();
while let Some(state) = stream.next().await {
match state {
VerificationRequestState::Transitioned { verification } => {
let Some(verification) = verification.sas() else {
error!("Invalid, non-sas verification flow. Returning.");
return;
};
*sas_verification.write().unwrap() = Some(verification.clone());
if verification.accept().await.is_ok() {
if let Some(delegate) = &*delegate.read().unwrap() {
delegate.did_start_sas_verification()
let delegate = self.delegate.clone();
RUNTIME.spawn(Self::listen_to_changes(delegate, sas_verification));
} else if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_fail()
}
let delegate = delegate.clone();
get_runtime_handle().spawn(Self::listen_to_sas_verification_changes(
verification,
delegate,
));
} else if let Some(delegate) = &*delegate.read().unwrap() {
delegate.did_fail()
}
}
VerificationRequestState::Ready { .. } => {
if let Some(delegate) = &*delegate.read().unwrap() {
delegate.did_accept_verification_request()
}
}
VerificationRequestState::Cancelled(..) => {
if let Some(delegate) = &*delegate.read().unwrap() {
delegate.did_cancel();
}
}
_ => {}
}
AnyToDeviceEvent::KeyVerificationReady(event) => {
if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) {
return;
}
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_accept_verification_request()
}
}
_ => (),
}
}
async fn listen_to_sas_verification_changes(sas: SasVerification, delegate: Delegate) {
fn is_transaction_id_valid(&self, transaction_id: String) -> bool {
match &*self.verification_request.read().unwrap() {
Some(verification) => verification.flow_id() == transaction_id,
None => false,
}
}
async fn listen_to_changes(delegate: Delegate, sas: SasVerification) {
let mut stream = sas.changes();
while let Some(state) = stream.next().await {
@@ -390,10 +246,7 @@ impl SessionVerificationController {
}
break;
}
SasState::Created { .. }
| SasState::Started { .. }
| SasState::Accepted { .. }
| SasState::Confirmed => (),
SasState::Started { .. } | SasState::Accepted { .. } | SasState::Confirmed => (),
}
}
}
-276
View File
@@ -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 the specific language governing permissions and
// limitations under the License.
use std::{fmt::Debug, sync::Arc};
use eyeball_im::VectorDiff;
use futures_util::{pin_mut, StreamExt};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use matrix_sdk_ui::spaces::{
room_list::SpaceRoomListPaginationState, SpaceRoom as UISpaceRoom,
SpaceRoomList as UISpaceRoomList, SpaceService as UISpaceService,
};
use ruma::RoomId;
use crate::{
client::JoinRule,
error::ClientError,
room::{Membership, RoomHero},
room_preview::RoomType,
runtime::get_runtime_handle,
TaskHandle,
};
/// The main entry point into the Spaces facilities.
///
/// The spaces service is responsible for retrieving one's joined rooms,
/// building a graph out of their `m.space.parent` and `m.space.child` state
/// events, and providing access to the top-level spaces and their children.
#[derive(uniffi::Object)]
pub struct SpaceService {
inner: UISpaceService,
}
impl SpaceService {
/// Creates a new `SpaceService` instance.
pub(crate) fn new(inner: UISpaceService) -> Self {
Self { inner }
}
}
#[matrix_sdk_ffi_macros::export]
impl SpaceService {
/// Returns a list of all the top-level joined spaces. It will eagerly
/// compute the latest version and also notify subscribers if there were
/// any changes.
pub async fn joined_spaces(&self) -> Vec<SpaceRoom> {
self.inner.joined_spaces().await.into_iter().map(Into::into).collect()
}
/// Subscribes to updates on the joined spaces list. If space rooms are
/// joined or left, the stream will yield diffs that reflect the changes.
pub async fn subscribe_to_joined_spaces(
&self,
listener: Box<dyn SpaceServiceJoinedSpacesListener>,
) -> Arc<TaskHandle> {
let (initial_values, mut stream) = self.inner.subscribe_to_joined_spaces().await;
listener.on_update(vec![SpaceListUpdate::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());
}
})))
}
/// Returns a `SpaceRoomList` for the given space ID.
#[allow(clippy::unused_async)]
// This method doesn't need to be async but if its not the FFI layer panics
// with "there is no no reactor running, must be called from the context
// of a Tokio 1.x runtime" error because the underlying constructor spawns
// an async task.
pub async fn space_room_list(
&self,
space_id: String,
) -> Result<Arc<SpaceRoomList>, ClientError> {
let space_id = RoomId::parse(space_id)?;
Ok(Arc::new(SpaceRoomList::new(self.inner.space_room_list(space_id))))
}
}
/// The `SpaceRoomList`represents a paginated list of direct rooms
/// that belong to a particular space.
///
/// It can be used to paginate through the list (and have live updates on the
/// pagination state) as well as subscribe to changes as rooms are joined or
/// left.
///
/// The `SpaceRoomList` also automatically subscribes to client room changes
/// and updates the list accordingly as rooms are joined or left.
#[derive(uniffi::Object)]
pub struct SpaceRoomList {
inner: UISpaceRoomList,
}
impl SpaceRoomList {
/// Creates a new `SpaceRoomList` for the underlying UI crate room list.
fn new(inner: UISpaceRoomList) -> Self {
Self { inner }
}
}
#[matrix_sdk_ffi_macros::export]
impl SpaceRoomList {
/// Returns if the room list is currently paginating or not.
pub fn pagination_state(&self) -> SpaceRoomListPaginationState {
self.inner.pagination_state()
}
/// Subscribe to pagination updates.
pub fn subscribe_to_pagination_state_updates(
&self,
listener: Box<dyn SpaceRoomListPaginationStateListener>,
) -> Arc<TaskHandle> {
let pagination_state = self.inner.subscribe_to_pagination_state_updates();
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
pin_mut!(pagination_state);
while let Some(state) = pagination_state.next().await {
listener.on_update(state);
}
})))
}
/// Return the current list of rooms.
pub fn rooms(&self) -> Vec<SpaceRoom> {
self.inner.rooms().into_iter().map(Into::into).collect()
}
/// Subscribes to room list updates.
pub fn subscribe_to_room_update(
&self,
listener: Box<dyn SpaceRoomListEntriesListener>,
) -> Arc<TaskHandle> {
let (initial_values, mut stream) = self.inner.subscribe_to_room_updates();
listener.on_update(vec![SpaceListUpdate::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());
}
})))
}
/// Ask the list to retrieve the next page if the end hasn't been reached
/// yet. Otherwise it no-ops.
pub async fn paginate(&self) -> Result<(), ClientError> {
self.inner.paginate().await.map_err(ClientError::from)
}
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SpaceRoomListPaginationStateListener: SendOutsideWasm + SyncOutsideWasm + Debug {
fn on_update(&self, pagination_state: SpaceRoomListPaginationState);
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SpaceRoomListEntriesListener: SendOutsideWasm + SyncOutsideWasm + Debug {
fn on_update(&self, rooms: Vec<SpaceListUpdate>);
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SpaceServiceJoinedSpacesListener: SendOutsideWasm + SyncOutsideWasm + Debug {
fn on_update(&self, room_updates: Vec<SpaceListUpdate>);
}
/// Structure representing a room in a space and aggregated information
/// relevant to the UI layer.
#[derive(uniffi::Record)]
pub struct SpaceRoom {
/// The ID of the room.
pub room_id: String,
/// The canonical alias of the room, if any.
pub canonical_alias: Option<String>,
/// The name of the room, if any.
pub name: Option<String>,
/// The topic of the room, if any.
pub topic: Option<String>,
/// The URL for the room's avatar, if one is set.
pub avatar_url: Option<String>,
/// The type of room from `m.room.create`, if any.
pub room_type: RoomType,
/// The number of members joined to the room.
pub num_joined_members: u64,
/// The join rule of the room.
pub join_rule: Option<JoinRule>,
/// Whether the room may be viewed by users without joining.
pub world_readable: Option<bool>,
/// Whether guest users may join the room and participate in it.
pub guest_can_join: bool,
/// The number of children room this has, if a space.
pub children_count: u64,
/// Whether this room is joined, left etc.
pub state: Option<Membership>,
/// A list of room members considered to be heroes.
pub heroes: Option<Vec<RoomHero>>,
}
impl From<UISpaceRoom> for SpaceRoom {
fn from(room: UISpaceRoom) -> Self {
Self {
room_id: room.room_id.into(),
canonical_alias: room.canonical_alias.map(|alias| alias.into()),
name: room.name,
topic: room.topic,
avatar_url: room.avatar_url.map(|url| url.into()),
room_type: room.room_type.into(),
num_joined_members: room.num_joined_members,
join_rule: room.join_rule.map(Into::into),
world_readable: room.world_readable,
guest_can_join: room.guest_can_join,
children_count: room.children_count,
state: room.state.map(Into::into),
heroes: room.heroes.map(|heroes| heroes.into_iter().map(Into::into).collect()),
}
}
}
#[derive(uniffi::Enum)]
pub enum SpaceListUpdate {
Append { values: Vec<SpaceRoom> },
Clear,
PushFront { value: SpaceRoom },
PushBack { value: SpaceRoom },
PopFront,
PopBack,
Insert { index: u32, value: SpaceRoom },
Set { index: u32, value: SpaceRoom },
Remove { index: u32 },
Truncate { length: u32 },
Reset { values: Vec<SpaceRoom> },
}
impl From<VectorDiff<UISpaceRoom>> for SpaceListUpdate {
fn from(diff: VectorDiff<UISpaceRoom>) -> 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() }
}
}
}
}
+20 -56
View File
@@ -16,18 +16,14 @@ use std::{fmt::Debug, sync::Arc};
use futures_util::pin_mut;
use matrix_sdk::Client;
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use matrix_sdk_ui::{
sync_service::{
State as MatrixSyncServiceState, SyncService as MatrixSyncService,
SyncServiceBuilder as MatrixSyncServiceBuilder,
},
unable_to_decrypt_hook::UtdHookManager,
use matrix_sdk_ui::sync_service::{
State as MatrixSyncServiceState, SyncService as MatrixSyncService,
SyncServiceBuilder as MatrixSyncServiceBuilder,
};
use crate::{
error::ClientError, helpers::unwrap_or_clone_arc, room_list::RoomListService,
runtime::get_runtime_handle, TaskHandle,
error::ClientError, helpers::unwrap_or_clone_arc, room_list::RoomListService, TaskHandle,
RUNTIME,
};
#[derive(uniffi::Enum)]
@@ -36,7 +32,6 @@ pub enum SyncServiceState {
Running,
Terminated,
Error,
Offline,
}
impl From<MatrixSyncServiceState> for SyncServiceState {
@@ -46,43 +41,38 @@ impl From<MatrixSyncServiceState> for SyncServiceState {
MatrixSyncServiceState::Running => Self::Running,
MatrixSyncServiceState::Terminated => Self::Terminated,
MatrixSyncServiceState::Error => Self::Error,
MatrixSyncServiceState::Offline => Self::Offline,
}
}
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SyncServiceStateObserver: SendOutsideWasm + SyncOutsideWasm + Debug {
#[uniffi::export(callback_interface)]
pub trait SyncServiceStateObserver: Send + Sync + Debug {
fn on_update(&self, state: SyncServiceState);
}
#[derive(uniffi::Object)]
pub struct SyncService {
pub(crate) inner: Arc<MatrixSyncService>,
utd_hook: Option<Arc<UtdHookManager>>,
}
#[matrix_sdk_ffi_macros::export]
#[uniffi::export(async_runtime = "tokio")]
impl SyncService {
pub fn room_list_service(&self) -> Arc<RoomListService> {
Arc::new(RoomListService {
inner: self.inner.room_list_service(),
utd_hook: self.utd_hook.clone(),
})
Arc::new(RoomListService { inner: self.inner.room_list_service() })
}
pub async fn start(&self) {
self.inner.start().await
self.inner.start().await;
}
pub async fn stop(&self) {
self.inner.stop().await
pub async fn stop(&self) -> Result<(), ClientError> {
Ok(self.inner.stop().await?)
}
pub fn state(&self, listener: Box<dyn SyncServiceStateObserver>) -> 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 {
@@ -90,55 +80,29 @@ impl SyncService {
}
})))
}
/// Force expiring both sliding sync sessions.
///
/// This ensures that the sync service is stopped before expiring both
/// sessions. It should be used sparingly, as it will cause a restart of
/// the sessions on the server as well.
pub async fn expire_sessions(&self) {
self.inner.expire_sessions().await;
}
}
#[derive(Clone, uniffi::Object)]
pub struct SyncServiceBuilder {
builder: MatrixSyncServiceBuilder,
utd_hook: Option<Arc<UtdHookManager>>,
}
impl SyncServiceBuilder {
pub(crate) fn new(client: Client, utd_hook: Option<Arc<UtdHookManager>>) -> Arc<Self> {
Arc::new(Self { builder: MatrixSyncService::builder(client), utd_hook })
pub(crate) fn new(client: Client) -> Arc<Self> {
Arc::new(Self { builder: MatrixSyncService::builder(client) })
}
}
#[matrix_sdk_ffi_macros::export]
#[uniffi::export(async_runtime = "tokio")]
impl SyncServiceBuilder {
pub fn with_cross_process_lock(self: Arc<Self>) -> Arc<Self> {
pub fn with_cross_process_lock(self: Arc<Self>, app_identifier: Option<String>) -> Arc<Self> {
let this = unwrap_or_clone_arc(self);
let builder = this.builder.with_cross_process_lock();
Arc::new(Self { builder, ..this })
}
/// Enable the "offline" mode for the [`SyncService`].
pub fn with_offline_mode(self: Arc<Self>) -> Arc<Self> {
let this = unwrap_or_clone_arc(self);
let builder = this.builder.with_offline_mode();
Arc::new(Self { builder, ..this })
}
pub fn with_share_pos(self: Arc<Self>, enable: bool) -> Arc<Self> {
let this = unwrap_or_clone_arc(self);
let builder = this.builder.with_share_pos(enable);
Arc::new(Self { builder, ..this })
let builder = this.builder.with_cross_process_lock(app_identifier);
Arc::new(Self { builder })
}
pub async fn finish(self: Arc<Self>) -> Result<Arc<SyncService>, ClientError> {
let this = unwrap_or_clone_arc(self);
Ok(Arc::new(SyncService {
inner: Arc::new(this.builder.build().await?),
utd_hook: this.utd_hook,
}))
Ok(Arc::new(SyncService { inner: Arc::new(this.builder.build().await?) }))
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
use matrix_sdk_common::executor::JoinHandle;
use tokio::task::JoinHandle;
use tracing::debug;
/// A task handle is a way to keep the handle a task running by itself in
@@ -17,7 +17,7 @@ impl TaskHandle {
}
}
#[matrix_sdk_ffi_macros::export]
#[uniffi::export]
impl TaskHandle {
// Cancel a task handle.
pub fn cancel(&self) {
@@ -1,185 +0,0 @@
use std::sync::Arc;
use matrix_sdk_ui::timeline::event_type_filter::TimelineEventTypeFilter as InnerTimelineEventTypeFilter;
use ruma::{
events::{AnySyncTimelineEvent, TimelineEventType},
EventId,
};
use super::FocusEventError;
use crate::{
error::ClientError,
event::{MessageLikeEventType, RoomMessageEventMessageType, StateEventType},
};
#[derive(uniffi::Object)]
pub struct TimelineEventTypeFilter {
inner: InnerTimelineEventTypeFilter,
}
#[matrix_sdk_ffi_macros::export]
impl TimelineEventTypeFilter {
#[uniffi::constructor]
pub fn include(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
let event_types: Vec<TimelineEventType> =
event_types.iter().map(|t| t.clone().into()).collect();
Arc::new(Self { inner: InnerTimelineEventTypeFilter::Include(event_types) })
}
#[uniffi::constructor]
pub fn exclude(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
let event_types: Vec<TimelineEventType> =
event_types.iter().map(|t| t.clone().into()).collect();
Arc::new(Self { inner: InnerTimelineEventTypeFilter::Exclude(event_types) })
}
}
impl TimelineEventTypeFilter {
/// Filters an [`event`] to decide whether it should be part of the timeline
/// based on [`AnySyncTimelineEvent::event_type()`].
pub(crate) fn filter(&self, event: &AnySyncTimelineEvent) -> bool {
self.inner.filter(event)
}
}
#[derive(uniffi::Enum, Clone)]
pub enum FilterTimelineEventType {
MessageLike { event_type: MessageLikeEventType },
State { event_type: StateEventType },
}
impl From<FilterTimelineEventType> for TimelineEventType {
fn from(value: FilterTimelineEventType) -> TimelineEventType {
match value {
FilterTimelineEventType::MessageLike { event_type } => {
ruma::events::MessageLikeEventType::from(event_type).into()
}
FilterTimelineEventType::State { event_type } => {
ruma::events::StateEventType::from(event_type).into()
}
}
}
}
#[derive(uniffi::Enum)]
pub enum TimelineFocus {
Live {
/// Whether to hide in-thread replies from the live timeline.
hide_threaded_events: bool,
},
Event {
/// The initial event to focus on. This is usually the target of a
/// permalink.
event_id: String,
/// The number of context events to load around the focused event.
num_context_events: u16,
/// Whether to hide in-thread replies from the live timeline.
hide_threaded_events: bool,
},
Thread {
/// The thread root event ID to focus on.
root_event_id: String,
},
PinnedEvents {
max_events_to_load: u16,
max_concurrent_requests: u16,
},
}
impl TryFrom<TimelineFocus> for matrix_sdk_ui::timeline::TimelineFocus {
type Error = ClientError;
fn try_from(
value: TimelineFocus,
) -> Result<matrix_sdk_ui::timeline::TimelineFocus, Self::Error> {
match value {
TimelineFocus::Live { hide_threaded_events } => Ok(Self::Live { hide_threaded_events }),
TimelineFocus::Event { event_id, num_context_events, hide_threaded_events } => {
let parsed_event_id =
EventId::parse(&event_id).map_err(|err| FocusEventError::InvalidEventId {
event_id: event_id.clone(),
err: err.to_string(),
})?;
Ok(Self::Event {
target: parsed_event_id,
num_context_events,
hide_threaded_events,
})
}
TimelineFocus::Thread { root_event_id } => {
let parsed_root_event_id = EventId::parse(&root_event_id).map_err(|err| {
FocusEventError::InvalidEventId {
event_id: root_event_id.clone(),
err: err.to_string(),
}
})?;
Ok(Self::Thread { root_event_id: parsed_root_event_id })
}
TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests } => {
Ok(Self::PinnedEvents { max_events_to_load, max_concurrent_requests })
}
}
}
}
/// Changes how date dividers get inserted, either in between each day or in
/// between each month
#[derive(uniffi::Enum)]
pub enum DateDividerMode {
Daily,
Monthly,
}
impl From<DateDividerMode> for matrix_sdk_ui::timeline::DateDividerMode {
fn from(value: DateDividerMode) -> Self {
match value {
DateDividerMode::Daily => Self::Daily,
DateDividerMode::Monthly => Self::Monthly,
}
}
}
#[derive(uniffi::Enum)]
pub enum TimelineFilter {
/// Show all the events in the timeline, independent of their type.
All,
/// Show only `m.room.messages` of the given room message types.
OnlyMessage {
/// A list of [`RoomMessageEventMessageType`] that will be allowed to
/// appear in the timeline.
types: Vec<RoomMessageEventMessageType>,
},
/// Show only events which match this filter.
EventTypeFilter { filter: Arc<TimelineEventTypeFilter> },
}
/// Various options used to configure the timeline's behavior.
#[derive(uniffi::Record)]
pub struct TimelineConfiguration {
/// What should the timeline focus on?
pub focus: TimelineFocus,
/// How should we filter out events from the timeline?
pub filter: TimelineFilter,
/// An optional String that will be prepended to
/// all the timeline item's internal IDs, making it possible to
/// distinguish different timeline instances from each other.
pub internal_id_prefix: Option<String>,
/// How often to insert date dividers
pub date_divider_mode: DateDividerMode,
/// Should the read receipts and read markers be tracked for the timeline
/// items in this instance?
///
/// As this has a non negligible performance impact, make sure to enable it
/// only when you need it.
pub track_read_receipts: bool,
/// Whether this timeline instance should report UTDs through the client's
/// delegate.
pub report_utds: bool,
}
+181 -73
View File
@@ -12,44 +12,41 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc};
use matrix_sdk::room::power_levels::power_level_user_changes;
use matrix_sdk_ui::timeline::RoomPinnedEventsChange;
use ruma::events::FullStateEventContent;
use matrix_sdk_ui::timeline::{PollResult, TimelineDetails};
use tracing::warn;
use crate::{timeline::msg_like::MsgLikeContent, utils::Timestamp};
use super::ProfileDetails;
use crate::ruma::{ImageInfo, MessageType, PollKind};
impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent {
fn from(value: matrix_sdk_ui::timeline::TimelineItemContent) -> Self {
#[derive(Clone, uniffi::Object)]
pub struct TimelineItemContent(pub(crate) matrix_sdk_ui::timeline::TimelineItemContent);
#[uniffi::export]
impl TimelineItemContent {
pub fn kind(&self) -> TimelineItemContentKind {
use matrix_sdk_ui::timeline::TimelineItemContent as Content;
match value {
Content::MsgLike(msg_like) => match msg_like.try_into() {
Ok(content) => TimelineItemContent::MsgLike { content },
Err((error, event_type)) => TimelineItemContent::FailedToParseMessageLike {
event_type,
error: error.to_string(),
},
},
Content::CallInvite => TimelineItemContent::CallInvite,
Content::CallNotify => TimelineItemContent::CallNotify,
Content::MembershipChange(membership) => {
let reason = match membership.content() {
FullStateEventContent::Original { content, .. } => content.reason.clone(),
_ => None,
};
TimelineItemContent::RoomMembership {
user_id: membership.user_id().to_string(),
user_display_name: membership.display_name(),
change: membership.change().map(Into::into),
reason,
match &self.0 {
Content::Message(_) => TimelineItemContentKind::Message,
Content::RedactedMessage => TimelineItemContentKind::RedactedMessage,
Content::Sticker(sticker) => {
let content = sticker.content();
TimelineItemContentKind::Sticker {
body: content.body.clone(),
info: (&content.info).into(),
url: content.url.to_string(),
}
}
Content::Poll(poll_state) => TimelineItemContentKind::from(poll_state.results()),
Content::UnableToDecrypt(msg) => {
TimelineItemContentKind::UnableToDecrypt { msg: EncryptedMessage::new(msg) }
}
Content::MembershipChange(membership) => TimelineItemContentKind::RoomMembership {
user_id: membership.user_id().to_string(),
change: membership.change().map(Into::into),
},
Content::ProfileChange(profile) => {
let (display_name, prev_display_name) = profile
.displayname_change()
@@ -64,57 +61,63 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
)
})
.unzip();
TimelineItemContent::ProfileChange {
TimelineItemContentKind::ProfileChange {
display_name: display_name.flatten(),
prev_display_name: prev_display_name.flatten(),
avatar_url: avatar_url.flatten(),
prev_avatar_url: prev_avatar_url.flatten(),
}
}
Content::OtherState(state) => TimelineItemContent::State {
Content::OtherState(state) => TimelineItemContentKind::State {
state_key: state.state_key().to_owned(),
content: state.content().into(),
},
Content::FailedToParseMessageLike { event_type, error } => {
TimelineItemContent::FailedToParseMessageLike {
TimelineItemContentKind::FailedToParseMessageLike {
event_type: event_type.to_string(),
error: error.to_string(),
}
}
Content::FailedToParseState { event_type, state_key, error } => {
TimelineItemContent::FailedToParseState {
TimelineItemContentKind::FailedToParseState {
event_type: event_type.to_string(),
state_key,
state_key: state_key.to_string(),
error: error.to_string(),
}
}
}
}
pub fn as_message(self: Arc<Self>) -> Option<Arc<Message>> {
use matrix_sdk_ui::timeline::TimelineItemContent as Content;
unwrap_or_clone_arc_into_variant!(self, .0, Content::Message(msg) => Arc::new(Message(msg)))
}
}
#[derive(Clone, uniffi::Enum)]
// A note about this `allow(clippy::large_enum_variant)`.
// In order to reduce the size of `TimelineItemContent`, 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 TimelineItemContent {
MsgLike {
content: MsgLikeContent,
#[derive(uniffi::Enum)]
pub enum TimelineItemContentKind {
Message,
RedactedMessage,
Sticker {
body: String,
info: ImageInfo,
url: String,
},
Poll {
question: String,
kind: PollKind,
max_selections: u64,
answers: Vec<PollAnswer>,
votes: HashMap<String, Vec<String>>,
end_time: Option<u64>,
has_been_edited: bool,
},
UnableToDecrypt {
msg: EncryptedMessage,
},
CallInvite,
CallNotify,
RoomMembership {
user_id: String,
user_display_name: Option<String>,
change: Option<MembershipChange>,
reason: Option<String>,
},
ProfileChange {
display_name: Option<String>,
@@ -137,16 +140,110 @@ pub enum TimelineItemContent {
},
}
#[derive(Clone, uniffi::Object)]
pub struct Message(matrix_sdk_ui::timeline::Message);
#[uniffi::export]
impl Message {
pub fn msgtype(&self) -> MessageType {
self.0.msgtype().clone().into()
}
pub fn body(&self) -> String {
self.0.msgtype().body().to_owned()
}
pub fn in_reply_to(&self) -> Option<InReplyToDetails> {
self.0.in_reply_to().map(InReplyToDetails::from)
}
pub fn is_threaded(&self) -> bool {
self.0.is_threaded()
}
pub fn is_edited(&self) -> bool {
self.0.is_edited()
}
}
#[derive(uniffi::Record)]
pub struct InReplyToDetails {
event_id: String,
event: RepliedToEventDetails,
}
impl From<&matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
fn from(inner: &matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
let event_id = inner.event_id.to_string();
let event = match &inner.event {
TimelineDetails::Unavailable => RepliedToEventDetails::Unavailable,
TimelineDetails::Pending => RepliedToEventDetails::Pending,
TimelineDetails::Ready(event) => RepliedToEventDetails::Ready {
content: Arc::new(TimelineItemContent(event.content().to_owned())),
sender: event.sender().to_string(),
sender_profile: event.sender_profile().into(),
},
TimelineDetails::Error(err) => {
RepliedToEventDetails::Error { message: err.to_string() }
}
};
Self { event_id, event }
}
}
#[derive(uniffi::Enum)]
pub enum RepliedToEventDetails {
Unavailable,
Pending,
Ready { content: Arc<TimelineItemContent>, sender: String, sender_profile: ProfileDetails },
Error { message: String },
}
#[derive(Clone, uniffi::Enum)]
pub enum EncryptedMessage {
OlmV1Curve25519AesSha2 {
/// The Curve25519 key of the sender.
sender_key: String,
},
// Other fields not included because UniFFI doesn't have the concept of
// deprecated fields right now.
MegolmV1AesSha2 {
/// The ID of the session used to encrypt the message.
session_id: String,
},
Unknown,
}
impl EncryptedMessage {
fn new(msg: &matrix_sdk_ui::timeline::EncryptedMessage) -> Self {
use matrix_sdk_ui::timeline::EncryptedMessage as Message;
match msg {
Message::OlmV1Curve25519AesSha2 { sender_key } => {
let sender_key = sender_key.clone();
Self::OlmV1Curve25519AesSha2 { sender_key }
}
Message::MegolmV1AesSha2 { session_id, .. } => {
let session_id = session_id.clone();
Self::MegolmV1AesSha2 { session_id }
}
Message::Unknown => Self::Unknown,
}
}
}
#[derive(Clone, uniffi::Record)]
pub struct Reaction {
pub key: String,
pub count: u64,
pub senders: Vec<ReactionSenderData>,
}
#[derive(Clone, uniffi::Record)]
pub struct ReactionSenderData {
pub sender_id: String,
pub timestamp: Timestamp,
pub timestamp: u64,
}
#[derive(Clone, uniffi::Enum)]
@@ -209,8 +306,8 @@ pub enum OtherState {
RoomHistoryVisibility,
RoomJoinRules,
RoomName { name: Option<String> },
RoomPinnedEvents { change: RoomPinnedEventsChange },
RoomPowerLevels { users: HashMap<String, i64>, previous: Option<HashMap<String, i64>> },
RoomPinnedEvents,
RoomPowerLevels,
RoomServerAcl,
RoomThirdPartyInvite { display_name: Option<String> },
RoomTombstone,
@@ -252,21 +349,8 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
};
Self::RoomName { name }
}
Content::RoomPinnedEvents(c) => Self::RoomPinnedEvents { change: c.into() },
Content::RoomPowerLevels(c) => match c {
FullContent::Original { content, prev_content } => Self::RoomPowerLevels {
users: power_level_user_changes(content, prev_content)
.iter()
.map(|(k, v)| (k.to_string(), *v))
.collect(),
previous: prev_content.as_ref().map(|prev_content| {
prev_content.users.iter().map(|(k, &v)| (k.to_string(), v.into())).collect()
}),
},
FullContent::Redacted(_) => {
Self::RoomPowerLevels { users: Default::default(), previous: None }
}
},
Content::RoomPinnedEvents(_) => Self::RoomPinnedEvents,
Content::RoomPowerLevels(_) => Self::RoomPowerLevels,
Content::RoomServerAcl(_) => Self::RoomServerAcl,
Content::RoomThirdPartyInvite(c) => {
let display_name = match c {
@@ -289,3 +373,27 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
}
}
}
#[derive(uniffi::Record)]
pub struct PollAnswer {
pub id: String,
pub text: String,
}
impl From<PollResult> for TimelineItemContentKind {
fn from(value: PollResult) -> Self {
TimelineItemContentKind::Poll {
question: value.question,
kind: PollKind::from(value.kind),
max_selections: value.max_selections,
answers: value
.answers
.into_iter()
.map(|i| PollAnswer { id: i.id, text: i.text })
.collect(),
votes: value.votes,
end_time: value.end_time,
has_been_edited: value.has_been_edited,
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,265 +0,0 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{collections::HashMap, sync::Arc};
use matrix_sdk::crypto::types::events::UtdCause;
use ruma::events::{room::MediaSource as RumaMediaSource, MessageLikeEventContent};
use super::{
content::Reaction,
reply::{EmbeddedEventDetails, InReplyToDetails},
};
use crate::{
error::ClientError,
ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind},
timeline::content::ReactionSenderData,
utils::Timestamp,
};
#[derive(Clone, uniffi::Enum)]
pub enum MsgLikeKind {
/// An `m.room.message` event or extensible event, including edits.
Message { content: MessageContent },
/// An `m.sticker` event.
Sticker { body: String, info: ImageInfo, source: Arc<MediaSource> },
/// An `m.poll.start` event.
Poll {
question: String,
kind: PollKind,
max_selections: u64,
answers: Vec<PollAnswer>,
votes: HashMap<String, Vec<String>>,
end_time: Option<Timestamp>,
has_been_edited: bool,
},
/// A redacted message.
Redacted,
/// An `m.room.encrypted` event that could not be decrypted.
UnableToDecrypt { msg: EncryptedMessage },
}
/// A special kind of [`super::TimelineItemContent`] that groups together
/// different room message types with their respective reactions and thread
/// information.
#[derive(Clone, uniffi::Record)]
pub struct MsgLikeContent {
pub kind: MsgLikeKind,
pub reactions: Vec<Reaction>,
/// The event this message is replying to, if any.
pub in_reply_to: Option<Arc<InReplyToDetails>>,
/// Event ID of the thread root, if this is a message in a thread.
pub thread_root: Option<String>,
/// Details about the thread this message is the root of.
pub thread_summary: Option<Arc<ThreadSummary>>,
}
#[derive(Clone, uniffi::Record)]
pub struct MessageContent {
pub msg_type: MessageType,
pub body: String,
pub is_edited: bool,
pub mentions: Option<Mentions>,
}
impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
type Error = (ClientError, String);
fn try_from(value: matrix_sdk_ui::timeline::MsgLikeContent) -> Result<Self, Self::Error> {
use matrix_sdk_ui::timeline::MsgLikeKind as Kind;
let reactions = value
.reactions
.iter()
.map(|(k, v)| Reaction {
key: k.to_owned(),
senders: v
.into_iter()
.map(|(sender_id, info)| ReactionSenderData {
sender_id: sender_id.to_string(),
timestamp: info.timestamp.into(),
})
.collect(),
})
.collect();
let in_reply_to = value.in_reply_to.map(|r| Arc::new(r.into()));
let thread_root = value.thread_root.map(|id| id.to_string());
let thread_summary = value.thread_summary.map(|t| Arc::new(t.into()));
Ok(match value.kind {
Kind::Message(message) => {
let msg_type = TryInto::<MessageType>::try_into(message.msgtype().clone())
.map_err(|e| (e, message.msgtype().msgtype().to_owned()))?;
Self {
kind: MsgLikeKind::Message {
content: MessageContent {
msg_type,
body: message.body().to_owned(),
is_edited: message.is_edited(),
mentions: message.mentions().cloned().map(|m| m.into()),
},
},
reactions,
in_reply_to,
thread_root,
thread_summary,
}
}
Kind::Sticker(sticker) => {
let content = sticker.content();
let media_source = RumaMediaSource::from(content.source.clone());
media_source
.verify()
.map_err(|e| (e, sticker.content().event_type().to_string()))?;
let image_info = TryInto::<ImageInfo>::try_into(&content.info)
.map_err(|e| (e, sticker.content().event_type().to_string()))?;
Self {
kind: MsgLikeKind::Sticker {
body: content.body.clone(),
info: image_info,
source: Arc::new(MediaSource { media_source }),
},
reactions,
in_reply_to,
thread_root,
thread_summary,
}
}
Kind::Poll(poll_state) => {
let results = poll_state.results();
Self {
kind: MsgLikeKind::Poll {
question: results.question,
kind: PollKind::from(results.kind),
max_selections: results.max_selections,
answers: results
.answers
.into_iter()
.map(|i| PollAnswer { id: i.id, text: i.text })
.collect(),
votes: results.votes,
end_time: results.end_time.map(|t| t.into()),
has_been_edited: results.has_been_edited,
},
reactions,
in_reply_to,
thread_root,
thread_summary,
}
}
Kind::Redacted => Self {
kind: MsgLikeKind::Redacted,
reactions,
in_reply_to,
thread_root,
thread_summary,
},
Kind::UnableToDecrypt(msg) => Self {
kind: MsgLikeKind::UnableToDecrypt { msg: EncryptedMessage::new(&msg) },
reactions,
in_reply_to,
thread_root,
thread_summary,
},
})
}
}
impl From<ruma::events::Mentions> for Mentions {
fn from(value: ruma::events::Mentions) -> Self {
Self {
user_ids: value.user_ids.iter().map(|id| id.to_string()).collect(),
room: value.room,
}
}
}
#[derive(Clone, uniffi::Enum)]
pub enum EncryptedMessage {
OlmV1Curve25519AesSha2 {
/// The Curve25519 key of the sender.
sender_key: String,
},
// Other fields not included because UniFFI doesn't have the concept of
// deprecated fields right now.
MegolmV1AesSha2 {
/// The ID of the session used to encrypt the message.
session_id: String,
/// What we know about what caused this UTD. E.g. was this event sent
/// when we were not a member of this room?
cause: UtdCause,
},
Unknown,
}
impl EncryptedMessage {
pub(crate) fn new(msg: &matrix_sdk_ui::timeline::EncryptedMessage) -> Self {
use matrix_sdk_ui::timeline::EncryptedMessage as Message;
match msg {
Message::OlmV1Curve25519AesSha2 { sender_key } => {
let sender_key = sender_key.clone();
Self::OlmV1Curve25519AesSha2 { sender_key }
}
Message::MegolmV1AesSha2 { session_id, cause, .. } => {
let session_id = session_id.clone();
Self::MegolmV1AesSha2 { session_id, cause: *cause }
}
Message::Unknown => Self::Unknown,
}
}
}
#[derive(Clone, uniffi::Record)]
pub struct PollAnswer {
pub id: String,
pub text: String,
}
#[derive(Clone, uniffi::Object)]
pub struct ThreadSummary {
pub latest_event: EmbeddedEventDetails,
pub num_replies: u32,
}
#[matrix_sdk_ffi_macros::export]
impl ThreadSummary {
pub fn latest_event(&self) -> EmbeddedEventDetails {
self.latest_event.clone()
}
pub fn num_replies(&self) -> u64 {
self.num_replies as u64
}
}
impl From<matrix_sdk_ui::timeline::ThreadSummary> for ThreadSummary {
fn from(value: matrix_sdk_ui::timeline::ThreadSummary) -> Self {
Self {
latest_event: EmbeddedEventDetails::from(value.latest_event),
num_replies: value.num_replies,
}
}
}
@@ -1,81 +0,0 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use matrix_sdk_ui::timeline::{EmbeddedEvent, TimelineDetails};
use super::{content::TimelineItemContent, ProfileDetails};
use crate::{event::EventOrTransactionId, utils::Timestamp};
#[derive(Clone, uniffi::Object)]
pub struct InReplyToDetails {
event_id: String,
event: EmbeddedEventDetails,
}
impl InReplyToDetails {
pub(crate) fn new(event_id: String, event: EmbeddedEventDetails) -> Self {
Self { event_id, event }
}
}
#[matrix_sdk_ffi_macros::export]
impl InReplyToDetails {
pub fn event_id(&self) -> String {
self.event_id.clone()
}
pub fn event(&self) -> EmbeddedEventDetails {
self.event.clone()
}
}
impl From<matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
fn from(inner: matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
Self { event_id: inner.event_id.to_string(), event: inner.event.into() }
}
}
#[derive(Clone, uniffi::Enum)]
#[allow(clippy::large_enum_variant)]
pub enum EmbeddedEventDetails {
Unavailable,
Pending,
Ready {
content: TimelineItemContent,
sender: String,
sender_profile: ProfileDetails,
timestamp: Timestamp,
event_or_transaction_id: EventOrTransactionId,
},
Error {
message: String,
},
}
impl From<TimelineDetails<Box<EmbeddedEvent>>> for EmbeddedEventDetails {
fn from(event: TimelineDetails<Box<EmbeddedEvent>>) -> Self {
match event {
TimelineDetails::Unavailable => EmbeddedEventDetails::Unavailable,
TimelineDetails::Pending => EmbeddedEventDetails::Pending,
TimelineDetails::Ready(event) => EmbeddedEventDetails::Ready {
content: event.content.into(),
sender: event.sender.to_string(),
sender_profile: event.sender_profile.into(),
timestamp: event.timestamp.into(),
event_or_transaction_id: event.identifier.into(),
},
TimelineDetails::Error(err) => EmbeddedEventDetails::Error { message: err.to_string() },
}
}
}

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