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
673 changed files with 38025 additions and 141961 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
-67
View File
@@ -1,67 +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-2023-0071", reason = "We are not using RSA directly, nor do we depend on the RSA crate directly" },
{ id = "RUSTSEC-2024-0384", reason = "Unmaintained backoff crate, not critical. We'll migrate soon." },
]
[licenses]
version = 2
allow = [
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"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" },
{ allow = ["CDDL-1.0"], crate = "inferno" },
{ allow = ["LicenseRef-ring"], crate = "ring" },
]
[[licenses.clarify]]
name = "ring"
expression = "LicenseRef-ring"
license-files = [
{ path = "LICENSE", hash = 0xbd0eed23 },
]
[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/element-hq/tracing.git",
# Sam as for the tracing dependency.
"https://github.com/element-hq/paranoid-android.git",
# Well, it's Ruma.
"https://github.com/ruma/ruma",
# A patch override for the bindings: https://github.com/rodrimati1992/const_panic/pull/10
"https://github.com/jplatte/const_panic",
# A patch override for the bindings: https://github.com/smol-rs/async-compat/pull/22
"https://github.com/jplatte/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 }}
+3 -3
View File
@@ -8,16 +8,16 @@ jobs:
name: Run Benchmarks
runs-on: ubuntu-latest
environment: matrix-rust-bot
if: github.event_name == 'push'
if: github.event_name == 'push' || !github.event.pull_request.draft
steps:
- name: Checkout the repo
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2024-11-26
toolchain: nightly-2023-11-08
components: rustfmt
- name: Run Benchmarks
+6 -119
View File
@@ -31,7 +31,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
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@v4
- name: Checkout Kotlin Rust Components project
uses: actions/checkout@v4
with:
repository: matrix-org/matrix-rust-components-kotlin
path: rust-components-kotlin
ref: main
- name: Use JDK 17
uses: actions/setup-java@v4
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@v4
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=reldbg
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@v4
# 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
+67 -60
View File
@@ -18,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:
@@ -29,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:
@@ -46,16 +44,11 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
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:
@@ -71,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 }}"
@@ -85,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@v4
uses: actions/checkout@v3
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -102,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 }}"
@@ -116,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@v4
- 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
@@ -138,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 }}"
@@ -150,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:
@@ -170,19 +161,13 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
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:
@@ -208,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
@@ -224,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
@@ -242,7 +227,7 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -251,8 +236,7 @@ jobs:
components: clippy
- name: Install wasm-pack
uses: qmaru/wasm-pack-action@v0.5.0
if: '!matrix.check_only'
uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.10.3
@@ -271,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 }}"
@@ -282,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@v4
uses: actions/checkout@v3
- name: Check the spelling of the files in our repo
uses: crate-ci/typos@v1.29.5
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@v4
uses: actions/checkout@v3
- name: Install protoc
uses: taiki-e/install-action@v2
@@ -314,8 +318,8 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2024-11-26
components: clippy, rustfmt
toolchain: nightly-2023-11-08
components: clippy
- name: Load cache
uses: Swatinem/rust-cache@v2
@@ -323,29 +327,26 @@ 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
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 'postgres'
# maps to the postgres container, etc.
services:
# synapse needs a postgres container
# sliding sync needs a postgres container
postgres:
# Docker Hub image
image: postgres
@@ -363,10 +364,21 @@ jobs:
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
@@ -375,12 +387,7 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v4
- 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
@@ -395,7 +402,7 @@ 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"
+26 -35
View File
@@ -1,4 +1,4 @@
name: Code Coverage
name: Code coverage
on:
push:
@@ -25,10 +25,12 @@ jobs:
code_coverage:
name: Code Coverage
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 'postgres'
# maps to the postgres container, etc.
services:
# sliding sync needs a postgres container
postgres:
# Docker Hub image
image: postgres
@@ -46,10 +48,21 @@ jobs:
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
@@ -58,15 +71,10 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
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
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -86,7 +94,7 @@ jobs:
tool: cargo-tarpaulin
# set up backend for integration tests
- uses: actions/setup-python@v5
- uses: actions/setup-python@v4
with:
python-version: 3.8
@@ -97,34 +105,17 @@ jobs:
--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
# 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: |
cobertura.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@v4
- uses: EmbarkStudios/cargo-deny-action@v2
@@ -1,12 +0,0 @@
name: Detects unused dependencies
on:
pull_request: { branches: "*" }
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Machete
uses: bnjbvr/cargo-machete@main
+9 -6
View File
@@ -23,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@v4
uses: actions/checkout@v3
- name: Install protoc
uses: taiki-e/install-action@v2
@@ -36,10 +37,10 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2024-11-26
toolchain: nightly-2023-11-08
- name: Install Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: 20
@@ -51,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@v3
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@v4
- name: Block Fixup Commit Merge
uses: 13rac1/block-fixup-merge-action@v2.0.0
-21
View File
@@ -1,21 +0,0 @@
name: Rust version
on:
workflow_dispatch:
push:
branches: [main]
pull_request:
branches: [main]
types:
- opened
- reopened
- synchronize
- ready_for_review
jobs:
msrv:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: taiki-e/install-action@cargo-hack
- run: cargo hack check --rust-version --workspace --all-targets --ignore-private
-79
View File
@@ -1,79 +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@v4
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 }}
+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@v4
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)] }}
-2
View File
@@ -7,8 +7,6 @@ emsdk-*
.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 -223
View File
@@ -29,223 +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
```
## 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
@@ -292,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.
@@ -302,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
+1789 -2158
View File
File diff suppressed because it is too large Load Diff
+40 -125
View File
@@ -6,112 +6,64 @@ members = [
"crates/*",
"testing/*",
"examples/*",
"labs/*",
"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.83"
rust-version = "1.70"
[workspace.dependencies]
anyhow = "1.0.95"
aquamarine = "0.6.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"
assert_matches2 = "0.1.1"
async-rx = "0.1.3"
async-stream = "0.3.5"
async-trait = "0.1.85"
async-stream = "0.3.3"
async-trait = "0.1.60"
as_variant = "1.2.0"
base64 = "0.22.1"
byteorder = "1.5.0"
chrono = "0.4.39"
eyeball = { version = "0.8.8", features = ["tracing"] }
eyeball-im = { version = "0.6.0", features = ["tracing"] }
eyeball-im-util = "0.8.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.2.0"
imbl = "4.0.1"
indexmap = "2.7.1"
insta = { version = "1.42.1", features = ["json"] }
itertools = "0.14.0"
js-sys = "0.3.69"
mime = "0.3.17"
once_cell = "1.20.2"
pbkdf2 = { version = "0.12.2" }
pin-project-lite = "0.2.16"
proptest = { version = "1.6.0", default-features = false, features = ["std"] }
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.12", default-features = false }
rmp-serde = "1.3.0"
# Be careful to use commits from the https://github.com/ruma/ruma/tree/ruma-0.12
# branch until a proper release with breaking changes happens.
ruma = { version = "0.12.1", features = [
"client-api-c",
"compat-upload-signatures",
"compat-user-id",
"compat-arbitrary-length-ids",
"compat-tag-info",
"compat-encrypted-stickers",
"unstable-msc3401",
"unstable-msc3266",
"unstable-msc3488",
"unstable-msc3489",
"unstable-msc4075",
"unstable-msc4140",
"unstable-msc4171",
] }
ruma-common = { version = "0.15.1" }
serde = "1.0.217"
serde_html_form = "0.2.7"
serde_json = "1.0.138"
serde = "1.0.151"
serde_html_form = "0.2.0"
serde_json = "1.0.91"
sha2 = "0.10.8"
similar-asserts = "1.6.1"
stream_assert = "0.1.1"
tempfile = "3.16.0"
thiserror = "2.0.11"
tokio = { version = "1.43.0", default-features = false, features = ["sync"] }
tokio-stream = "0.1.17"
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"
tracing-subscriber = "0.3.18"
unicode-normalization = "0.1.24"
uniffi = { version = "0.28.0" }
uniffi_bindgen = { version = "0.28.0" }
url = "2.5.4"
uuid = "1.12.1"
vodozemac = { version = "0.9.0", features = ["insecure-pk-encryption"] }
wasm-bindgen = "0.2.84"
wasm-bindgen-test = "0.3.33"
web-sys = "0.3.69"
wiremock = "0.6.2"
zeroize = "1.8.1"
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.10.0", default-features = false }
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.10.0" }
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.10.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.10.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.10.0", default-features = false }
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.10.0" }
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.10.0", default-features = false }
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.10.0" }
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.10.0" }
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.10.0", default-features = false }
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 }
# Default release profile, select with `--release`
[profile.release]
@@ -128,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]
@@ -146,37 +95,3 @@ opt-level = 3
[patch.crates-io]
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/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
tracing-core = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
tracing-subscriber = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
tracing-appender = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
paranoid-android = { git = "https://github.com/element-hq/paranoid-android.git", rev = "69388ac5b4afeed7be4401c70ce17f6d9a2cf19b" }
[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"
mut_mut = "warn"
needless_borrow = "warn"
nonstandard_macro_braces = "warn"
str_to_string = "warn"
todo = "warn"
unused_async = "warn"
redundant_clone = "warn"
+8 -2
View File
@@ -24,11 +24,17 @@ The rust-sdk consists of multiple crates that can be picked at your convenience:
- **matrix-sdk-crypto** - No (network) IO encryption state machine that can be
used to add Matrix E2EE support to your client or client library.
## Minimum Supported Rust Version (MSRV)
These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.70`.
## Status
The library is 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) and [Fractal](https://gitlab.gnome.org/World/fractal). 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.
Development of the SDK has been primarily sponsored by Element though accepts contributions from all.
If you are interested in using the matrix-sdk now is the time to try it out and
provide feedback.
## Bindings
-47
View File
@@ -1,47 +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
+3 -13
View File
@@ -13,17 +13,14 @@ 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 }
matrix-sdk = { workspace = true, features = ["native-tls", "e2e-encryption", "sqlite"] }
matrix-sdk = { workspace = true }
ruma = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tempfile = "3.3.0"
tokio = { workspace = true, default-features = false, features = ["rt-multi-thread"] }
wiremock = { workspace = true }
tokio = { version = "1.24.2", default-features = false, features = ["rt-multi-thread"] }
[target.'cfg(target_os = "linux")'.dependencies]
pprof = { version = "0.14.0", features = ["flamegraph", "criterion"] }
pprof = { version = "0.13.0", features = ["flamegraph", "criterion"] }
[[bench]]
name = "crypto_bench"
@@ -32,10 +29,3 @@ harness = false
[[bench]]
name = "store_bench"
harness = false
[[bench]]
name = "room_bench"
harness = false
[package.metadata.release]
release = false
+25 -19
View File
@@ -3,11 +3,14 @@ use std::{ops::Deref, sync::Arc};
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::{
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, DeviceId, OwnedUserId, TransactionId, UserId,
};
@@ -25,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) {
@@ -67,9 +76,8 @@ pub fn keys_query(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();
group.bench_with_input(BenchmarkId::new("sqlite store", &name), &response, |b, response| {
b.to_async(&runtime)
@@ -126,7 +134,7 @@ pub fn keys_claiming(c: &mut Criterion) {
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))
.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))
@@ -186,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();
})
});
@@ -195,9 +203,8 @@ 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();
@@ -218,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();
})
});
@@ -260,9 +267,8 @@ 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();
-250
View File
@@ -1,250 +0,0 @@
use std::{sync::Arc, time::Duration};
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server};
use matrix_sdk_base::{
store::StoreConfig, BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore,
};
use matrix_sdk_sqlite::SqliteStateStore;
use matrix_sdk_test::{
event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder,
};
use matrix_sdk_ui::{timeline::TimelineFocus, Timeline};
use ruma::{
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, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId,
};
use serde::Serialize;
use serde_json::json;
use tokio::runtime::Builder;
use wiremock::{
matchers::{header, method, path, path_regex, query_param, query_param_is_missing},
Mock, MockServer, 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_{}:matrix.org", i)).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::with_store_config(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
.state_store(sqlite_store),
);
runtime
.block_on(base_client.set_session_meta(
SessionMeta {
user_id: user_id!("@somebody:example.com").to_owned(),
device_id: device_id!("DEVICE_ID").to_owned(),
},
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("receive_members", 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 (client, server) = runtime.block_on(logged_in_client_with_server());
let mut sync_response_builder = SyncResponseBuilder::new();
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 response_json =
sync_response_builder.add_joined_room(joined_room_builder).build_json_sync_response();
runtime.block_on(mock_sync(&server, response_json, None));
let sync_settings = SyncSettings::default();
runtime.block_on(client.sync_once(sync_settings)).expect("Could not sync");
runtime.block_on(server.reset());
runtime.block_on(
Mock::given(method("GET"))
.and(path_regex(r"/_matrix/client/r0/rooms/.*/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(&server),
);
let room = client.get_room(&room_id).expect("Room not found");
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("Test");
group.throughput(Throughput::Elements(count as u64));
group.sample_size(10);
let client = Arc::new(client);
{
let client = client.clone();
runtime.spawn_blocking(move || {
client.event_cache().subscribe().unwrap();
});
}
group.bench_function(BenchmarkId::new("load_pinned_events", 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().empty_immutable_cache().await;
let timeline = Timeline::builder(&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();
runtime.block_on(server.reset());
drop(server);
}
group.finish();
}
async fn mock_sync(server: &MockServer, response_body: impl Serialize, since: Option<String>) {
let mut mock_builder = Mock::given(method("GET"))
.and(path("/_matrix/client/r0/sync"))
.and(header("authorization", "Bearer 1234"));
if let Some(since) = since {
mock_builder = mock_builder.and(query_param("since", since));
} else {
mock_builder = mock_builder.and(query_param_is_missing("since"));
}
mock_builder
.respond_with(ResponseTemplate::new(200).set_body_json(response_body))
.mount(server)
.await;
}
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 = room;
config = criterion();
targets = receive_all_members_benchmark, load_pinned_events_benchmark,
}
criterion_main!(room);
+3 -9
View File
@@ -2,8 +2,8 @@ use std::sync::Arc;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use matrix_sdk::{
authentication::matrix::{MatrixSession, MatrixSessionTokens},
config::StoreConfig,
matrix_auth::{MatrixSession, MatrixSessionTokens},
Client, RoomInfo, RoomState, StateChanges,
};
use matrix_sdk_base::{store::MemoryStore, SessionMeta, StateStore as _};
@@ -69,10 +69,7 @@ pub fn restore_session(c: &mut Criterion) {
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");
@@ -99,10 +96,7 @@ pub fn restore_session(c: &mut Criterion) {
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");
+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
+7 -18
View File
@@ -12,21 +12,16 @@ publish = 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"]
[dependencies]
anyhow = { workspace = true }
futures-util = { workspace = true }
futures-util = "0.3.28"
hmac = "0.12.1"
http = { workspace = true }
matrix-sdk-common = { workspace = true, features = ["uniffi"] }
matrix-sdk-ffi-macros = { workspace = true }
matrix-sdk-common = { workspace = true }
pbkdf2 = "0.12.2"
rand = { workspace = true }
ruma = { workspace = true }
@@ -34,9 +29,9 @@ serde = { workspace = true }
serde_json = { workspace = true }
sha2 = { workspace = true }
thiserror = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
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"] }
uniffi = { workspace = true }
vodozemac = { workspace = true }
zeroize = { workspace = true, features = ["zeroize_derive"] }
@@ -46,7 +41,7 @@ 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
@@ -54,19 +49,13 @@ features = ["crypto-store"]
[dependencies.tokio]
version = "1.33.0"
default-features = false
default_features = false
features = ["rt-multi-thread"]
[build-dependencies]
vergen = { version = "8.2.5", features = ["build", "git", "gitcl"] }
uniffi = { workspace = true, features = ["build"] }
vergen = { version = "8.2.5", features = ["build", "git", "gitcl"] }
[dev-dependencies]
tempfile = "3.8.0"
assert_matches2 = { workspace = true }
[lints]
workspace = true
[package.metadata.release]
release = false
+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)
+18 -44
View File
@@ -1,63 +1,37 @@
use std::{
env,
error::Error,
path::{Path, PathBuf},
process::Command,
};
use std::{env, error::Error};
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")?;
EmitBuilder::builder().git_sha(true).git_describe(true, false, None).emit()?;
@@ -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,25 +1,19 @@
use std::{mem::ManuallyDrop, sync::Arc};
use matrix_sdk_crypto::{
dehydrated_devices::{
DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices,
RehydratedDevice as InnerRehydratedDevice,
},
store::DehydratedDeviceKey as InnerDehydratedDeviceKey,
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 tokio::runtime::Handle;
use crate::{CryptoStoreError, DehydratedDeviceKey};
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)]
@@ -28,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 {
@@ -37,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)
}
}
}
}
@@ -67,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())?;
@@ -80,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(),
@@ -99,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)]
@@ -152,7 +107,7 @@ impl Drop for RehydratedDevice {
}
}
#[matrix_sdk_ffi_macros::export]
#[uniffi::export]
impl RehydratedDevice {
pub fn receive_events(&self, events: String) -> Result<(), crate::CryptoStoreError> {
let events: Vec<Raw<AnyToDeviceEvent>> = serde_json::from_str(&events)?;
@@ -178,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())
}
}
@@ -220,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(),
}
}
}
+3 -6
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)]
@@ -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));
+54 -198
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,17 +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::{
Changes, CryptoStore, DehydratedDeviceKey as InnerDehydratedDeviceKey, PendingChanges,
RoomSettings as RustRoomSettings,
},
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::{
@@ -52,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;
@@ -65,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 {
@@ -139,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`.
@@ -196,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,
@@ -254,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)?;
@@ -359,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,
@@ -430,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,
@@ -471,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;
@@ -502,7 +466,6 @@ 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,
@@ -535,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,
@@ -563,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
///
@@ -673,27 +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(),
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,
}
}
}
@@ -738,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 },
}
}
}
@@ -804,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> {
@@ -832,39 +779,6 @@ impl TryFrom<matrix_sdk_crypto::store::BackupKeys> for BackupKeys {
}
}
/// Dehydrated device key
#[derive(uniffi::Record, Clone)]
pub struct DehydratedDeviceKey {
pub(crate) inner: Vec<u8>,
}
impl DehydratedDeviceKey {
/// Generates a new random pickle key.
pub fn new() -> Result<Self, DehydrationError> {
let inner = InnerDehydratedDeviceKey::new()?;
Ok(inner.into())
}
/// Creates a new dehydration pickle key from the given slice.
///
/// Fail if the slice length is not 32.
pub fn from_slice(slice: &[u8]) -> Result<Self, DehydrationError> {
let inner = InnerDehydratedDeviceKey::from_slice(slice)?;
Ok(inner.into())
}
/// Export the [`DehydratedDeviceKey`] as a base64 encoded string.
pub fn to_base64(&self) -> String {
let inner = InnerDehydratedDeviceKey::from_slice(&self.inner).unwrap();
inner.to_base64()
}
}
impl From<InnerDehydratedDeviceKey> for DehydratedDeviceKey {
fn from(pickle_key: InnerDehydratedDeviceKey) -> Self {
DehydratedDeviceKey { inner: pickle_key.into() }
}
}
impl From<matrix_sdk_crypto::store::RoomKeyCounts> for RoomKeyCounts {
fn from(count: matrix_sdk_crypto::store::RoomKeyCounts) -> Self {
Self { total: count.total as i64, backed_up: count.backed_up as i64 }
@@ -925,7 +839,6 @@ impl From<RoomSettings> for RustRoomSettings {
Self {
algorithm: value.algorithm.into(),
only_allow_trusted_devices: value.only_allow_trusted_devices,
..RustRoomSettings::default()
}
}
}
@@ -934,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(),
@@ -958,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 {
@@ -1045,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":[
@@ -1153,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();
+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)) };
+80 -162
View File
@@ -17,8 +17,7 @@ use matrix_sdk_crypto::{
decrypt_room_key_export, encrypt_room_key_export,
olm::ExportedRoomKey,
store::{BackupDecryptionKey, Changes},
types::requests::ToDeviceRequest,
DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, UserIdentity as SdkUserIdentity,
LocalTrust, OlmMachine as InnerMachine, UserIdentities,
};
use ruma::{
api::{
@@ -38,12 +37,10 @@ use ruma::{
},
events::{
key::verification::VerificationMethod, room::message::MessageType, AnyMessageLikeEvent,
AnySyncMessageLikeEvent, MessageLikeEvent,
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};
@@ -179,7 +176,7 @@ impl From<RustSignatureCheckResult> for SignatureVerification {
}
}
#[matrix_sdk_ffi_macros::export]
#[uniffi::export]
impl OlmMachine {
/// Create a new `OlmMachine`
///
@@ -210,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 }))
}
@@ -253,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,
@@ -287,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
},
@@ -315,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 {
@@ -332,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,
@@ -415,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,
@@ -516,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(
@@ -529,11 +525,11 @@ impl OlmMachine {
) -> 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"),
@@ -541,8 +537,8 @@ 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 {
@@ -607,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>,
@@ -728,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(
@@ -804,53 +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,
) -> 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))?;
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
@@ -862,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
@@ -885,14 +832,10 @@ 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(e) = decrypted.event.deserialize() {
if let Ok(AnyTimelineEvent::MessageLike(e)) = decrypted.event.deserialize() {
match &e {
AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original(
original_event,
@@ -910,7 +853,8 @@ 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())?;
@@ -939,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(
@@ -964,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)?;
@@ -990,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,
@@ -999,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.
@@ -1009,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
@@ -1062,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(())
}
@@ -1105,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![];
@@ -1126,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(
@@ -1146,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,
@@ -1162,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
@@ -1175,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(
@@ -1205,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() }
@@ -1221,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,
@@ -1242,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 {
@@ -1295,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>> {
@@ -1315,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.
///
@@ -1538,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",
};
+13 -15
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::{
@@ -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(),
}
}
}
@@ -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(),
}
}
})
@@ -15,7 +15,7 @@ use crate::{CryptoStoreError, OutgoingVerificationRequest, SignatureUploadReques
/// 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,7 +78,7 @@ 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.
@@ -112,7 +108,7 @@ pub struct Sas {
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
///
@@ -328,7 +324,7 @@ pub struct QrCode {
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,10 +659,10 @@ 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: sas, runtime: self.runtime.clone() }),
@@ -681,11 +677,11 @@ 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
@@ -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()?;
@@ -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/
-65
View File
@@ -1,65 +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! { #[uniffi::export(async_runtime = "tokio", #attr2)] },
false => quote! { #[uniffi::export(#attr2)] },
},
Err(e) => e.into_compile_error(),
};
quote! {
#res
#item2
}
.into()
}
-40
View File
@@ -1,40 +0,0 @@
# unreleased
Breaking changes:
- 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.
Additions:
- Add `Encryption::get_user_identity` which returns `UserIdentity`
- Add `ClientBuilder::room_key_recipient_strategy`
- Add `Room::send_raw`
- Expose `withdraw_verification` to `UserIdentity`
+15 -14
View File
@@ -24,30 +24,37 @@ vergen = { version = "8.1.3", features = ["build", "git", "gitcl"] }
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 }
log-panics = { version = "2", features = ["with-backtrace"] }
matrix-sdk-ffi-macros = { workspace = true }
matrix-sdk-ui = { workspace = true, features = ["uniffi"] }
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-subscriber = { workspace = true, features = ["env-filter"] }
tracing-opentelemetry = "0.22.0"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-appender = { version = "0.2.2" }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
tokio-stream = { workspace = true, features = ["time"] }
uniffi = { workspace = true, features = ["tokio"] }
url = { workspace = true }
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]
@@ -56,12 +63,12 @@ features = [
"anyhow",
"e2e-encryption",
"experimental-oidc",
"experimental-sliding-sync",
"experimental-widgets",
"markdown",
"rustls-tls", # note: differ from block below
"socks",
"sqlite",
"uniffi",
]
[target.'cfg(not(target_os = "android"))'.dependencies.matrix-sdk]
@@ -70,16 +77,10 @@ features = [
"anyhow",
"e2e-encryption",
"experimental-oidc",
"experimental-sliding-sync",
"experimental-widgets",
"markdown",
"native-tls", # note: differ from block above
"socks",
"sqlite",
"uniffi",
]
[lints]
workspace = true
[package.metadata.release]
release = false
+8
View File
@@ -2,6 +2,14 @@
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.
# OpenTelemetry support
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
### Swift/iOS sync
+17 -44
View File
@@ -1,61 +1,34 @@
use std::{
env,
error::Error,
path::{Path, PathBuf},
process::Command,
};
use std::{env, error::Error};
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");
+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,243 +0,0 @@
use std::{
collections::HashMap,
fmt::{self, Debug},
sync::Arc,
};
use matrix_sdk::{
authentication::oidc::{
registrations::OidcRegistrationsError,
types::{
iana::oauth::OAuthClientAuthenticationMethod,
oidc::ApplicationType,
registration::{ClientMetadata, Localized, VerifiedClientMetadata},
requests::GrantType,
},
OidcError as SdkOidcError,
},
Error,
};
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_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
}
/// 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: 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>,
/// A file path where any dynamic registrations should be stored.
///
/// Suggested value: `{base_path}/oidc/registrations.json`
pub dynamic_registrations_file: String,
}
impl TryInto<VerifiedClientMetadata> for &OidcConfiguration {
type Error = OidcError;
fn try_into(self) -> Result<VerifiedClientMetadata, Self::Error> {
let redirect_uri =
Url::parse(&self.redirect_uri).map_err(|_| OidcError::CallbackUrlInvalid)?;
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 contacts = self.contacts.clone();
ClientMetadata {
application_type: Some(ApplicationType::Native),
redirect_uris: Some(vec![redirect_uri]),
grant_types: Some(vec![
GrantType::RefreshToken,
GrantType::AuthorizationCode,
GrantType::DeviceCode,
]),
// 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(|_| OidcError::MetadataInvalid)
}
}
#[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("Failed to use the supplied registrations file path.")]
RegistrationsPathInvalid,
#[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<SdkOidcError> for OidcError {
fn from(e: SdkOidcError) -> OidcError {
match e {
SdkOidcError::MissingAuthenticationIssuer => OidcError::NotSupported,
SdkOidcError::MissingRedirectUri => OidcError::MetadataInvalid,
SdkOidcError::InvalidCallbackUrl => OidcError::CallbackUrlInvalid,
SdkOidcError::InvalidState => OidcError::CallbackUrlInvalid,
SdkOidcError::CancelledAuthorization => OidcError::Cancelled,
_ => OidcError::Generic { message: e.to_string() },
}
}
}
impl From<OidcRegistrationsError> for OidcError {
fn from(e: OidcRegistrationsError) -> OidcError {
match e {
OidcRegistrationsError::InvalidFilePath => OidcError::RegistrationsPathInvalid,
_ => OidcError::Generic { message: e.to_string() },
}
}
}
impl From<Error> for OidcError {
fn from(e: Error) -> OidcError {
match e {
Error::Oidc(e) => e.into(),
_ => OidcError::Generic { message: e.to_string() },
}
}
}
/* Helpers */
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>>, OidcError>;
}
impl OptionExt for Option<String> {
fn localized_url(&self) -> Result<Option<Localized<Url>>, OidcError> {
self.as_deref()
.map(|uri| -> Result<Localized<Url>, OidcError> {
Ok(Localized::new(Url::parse(uri).map_err(|_| OidcError::MetadataInvalid)?, []))
})
.transpose()
}
}
@@ -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
+130 -608
View File
@@ -1,345 +1,57 @@
use std::{fs, num::NonZeroUsize, path::Path, sync::Arc, time::Duration};
use std::{fs, path::PathBuf, sync::Arc};
use futures_util::StreamExt;
use matrix_sdk::{
authentication::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason},
crypto::{
types::qr_login::{LoginQrCodeDecodeError, QrCodeModeData},
CollectStrategy, TrustRequirement,
},
encryption::{BackupDownloadStrategy, EncryptionSettings},
event_cache::EventCacheError,
reqwest::Certificate,
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,
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, RUNTIME};
use crate::{
authentication::OidcConfiguration, client::ClientSessionDelegate, error::ClientError,
helpers::unwrap_or_clone_arc, task_handle::TaskHandle,
};
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),
}
/// Data for the QR code login mechanism.
///
/// The [`QrCodeData`] can be serialized and encoded as a QR code or it can be
/// decoded from a QR code.
#[derive(Debug, uniffi::Object)]
pub struct QrCodeData {
inner: qrcode::QrCodeData,
}
#[matrix_sdk_ffi_macros::export]
impl QrCodeData {
/// Attempt to decode a slice of bytes into a [`QrCodeData`] object.
///
/// The slice of bytes would generally be returned by a QR code decoder.
#[uniffi::constructor]
pub fn from_bytes(bytes: Vec<u8>) -> Result<Arc<Self>, QrCodeDecodeError> {
Ok(Self { inner: qrcode::QrCodeData::from_bytes(&bytes)? }.into())
}
}
/// 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::Oidc(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 OIDC provider 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: Sync + Send {
fn on_update(&self, state: QrLoginProgress);
}
impl From<qrcode::LoginProgress> for QrLoginProgress {
fn from(value: qrcode::LoginProgress) -> Self {
use qrcode::LoginProgress;
match value {
LoginProgress::Starting => Self::Starting,
LoginProgress::EstablishingSecureChannel { check_code } => {
let check_code = check_code.to_digit();
Self::EstablishingSecureChannel {
check_code,
check_code_string: format!("{check_code:02}"),
}
}
LoginProgress::WaitingForToken { user_code } => Self::WaitingForToken { user_code },
LoginProgress::Done => Self::Done,
}
}
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum ClientBuildError {
#[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>,
base_path: Option<String>,
username: Option<String>,
homeserver_cfg: Option<HomeserverConfig>,
server_name: Option<(String, UrlScheme)>,
homeserver_url: Option<String>,
server_versions: Option<Vec<String>>,
passphrase: Zeroizing<Option<String>>,
user_agent: Option<String>,
sliding_sync_version_builder: SlidingSyncVersionBuilder,
sliding_sync_proxy: Option<String>,
proxy: Option<String>,
disable_ssl_verification: bool,
disable_automatic_token_refresh: bool,
cross_process_store_locks_holder_name: Option<String>,
enable_oidc_refresh_lock: bool,
inner: MatrixClientBuilder,
cross_process_refresh_lock_id: Option<String>,
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
additional_root_certificates: Vec<Vec<u8>>,
disable_built_in_root_certificates: bool,
encryption_settings: EncryptionSettings,
room_key_recipient_strategy: CollectStrategy,
decryption_trust_requirement: TrustRequirement,
request_config: Option<RequestConfig>,
/// Whether to enable use of the event cache store, for reloading events
/// when building timelines et al.
use_event_cache_persistent_storage: bool,
}
#[matrix_sdk_ffi_macros::export]
#[uniffi::export]
impl ClientBuilder {
#[uniffi::constructor]
pub fn new() -> Arc<Self> {
Arc::new(Self {
session_paths: None,
username: None,
homeserver_cfg: None,
passphrase: Zeroizing::new(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_trust_requirement: TrustRequirement::Untrusted,
request_config: Default::default(),
use_event_cache_persistent_storage: false,
})
Arc::new(Self::default())
}
/// Whether to use the event cache persistent storage or not.
///
/// This is a temporary feature flag, for testing the event cache's
/// persistent storage. Follow new developments in https://github.com/matrix-org/matrix-rust-sdk/issues/3280.
///
/// This is disabled by default. When disabled, a one-time cleanup is
/// performed when creating the client, and it will clear all the events
/// previously stored in the event cache.
///
/// When enabled, it will attempt to store events in the event cache as
/// they're received, and reuse them when reconstructing timelines.
pub fn use_event_cache_persistent_storage(self: Arc<Self>, value: bool) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.use_event_cache_persistent_storage = value;
Arc::new(builder)
}
pub fn cross_process_store_locks_holder_name(
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(
@@ -351,15 +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 });
builder.base_path = Some(path);
Arc::new(builder)
}
@@ -369,21 +75,22 @@ 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));
Arc::new(builder)
}
pub fn server_name_or_homeserver_url(self: Arc<Self>, server_name_or_url: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.homeserver_cfg = Some(HomeserverConfig::ServerNameOrUrl(server_name_or_url));
builder.homeserver_url = Some(url);
Arc::new(builder)
}
@@ -399,12 +106,9 @@ impl ClientBuilder {
Arc::new(builder)
}
pub fn sliding_sync_version_builder(
self: Arc<Self>,
version_builder: SlidingSyncVersionBuilder,
) -> 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.sliding_sync_version_builder = version_builder;
builder.sliding_sync_proxy = sliding_sync_proxy;
Arc::new(builder)
}
@@ -426,156 +130,70 @@ impl ClientBuilder {
Arc::new(builder)
}
pub fn add_root_certificates(
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> {
let mut builder = unwrap_or_clone_arc(self);
builder.disable_built_in_root_certificates = true;
Arc::new(builder)
}
pub fn auto_enable_cross_signing(
pub(crate) fn set_session_delegate_inner(
self: Arc<Self>,
auto_enable_cross_signing: bool,
session_delegate: Arc<dyn ClientSessionDelegate>,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.encryption_settings.auto_enable_cross_signing = auto_enable_cross_signing;
builder.session_delegate = Some(session_delegate);
Arc::new(builder)
}
/// Select a strategy to download room keys from the backup. By default
/// we download after a decryption failure.
///
/// Take a look at the [`BackupDownloadStrategy`] enum for more options.
pub fn backup_download_strategy(
pub(crate) fn server_name_with_protocol(
self: Arc<Self>,
backup_download_strategy: BackupDownloadStrategy,
server_name: String,
protocol: UrlScheme,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.encryption_settings.backup_download_strategy = backup_download_strategy;
builder.server_name = Some((server_name, protocol));
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 room_decryption_trust_requirement(
self: Arc<Self>,
trust_requirement: TrustRequirement,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.decryption_trust_requirement = trust_requirement;
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)
}
pub async fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientBuildError> {
pub(crate) fn build_inner(self: Arc<Self>) -> anyhow::Result<Arc<Client>> {
let builder = unwrap_or_clone_arc(self);
let mut inner_builder = MatrixClient::builder();
let mut inner_builder = builder.inner;
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());
}
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)?;
if let Some(session_paths) = &builder.session_paths {
let data_path = Path::new(&session_paths.data_path);
let cache_path = Path::new(&session_paths.cache_path);
debug!(
data_path = %data_path.to_string_lossy(),
cache_path = %cache_path.to_string_lossy(),
"Creating directories for data and cache stores.",
);
fs::create_dir_all(data_path)?;
fs::create_dir_all(cache_path)?;
inner_builder = inner_builder.sqlite_store_with_cache_path(
data_path,
cache_path,
builder.passphrase.as_deref(),
);
} else {
debug!("Not using a store path.");
inner_builder = inner_builder.sqlite_store(&data_path, builder.passphrase.as_deref());
}
// Determine server either from URL, server name or user ID.
inner_builder = match builder.homeserver_cfg {
Some(HomeserverConfig::Url(url)) => inner_builder.homeserver_url(url),
Some(HomeserverConfig::ServerName(server_name)) => {
let server_name = ServerName::parse(server_name)?;
inner_builder.server_name(&server_name)
}
Some(HomeserverConfig::ServerNameOrUrl(server_name_or_url)) => {
inner_builder.server_name_or_homeserver_url(server_name_or_url)
}
None => {
if let Some(username) = builder.username {
let user = UserId::parse(username)?;
inner_builder.server_name(user.server_name())
} else {
return Err(ClientBuildError::Generic {
message: "Failed to build: One of homeserver_url, server_name, server_name_or_homeserver_url or username must be called.".to_owned(),
});
}
}
};
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(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 {
@@ -594,162 +212,66 @@ impl ClientBuilder {
inner_builder = inner_builder.user_agent(user_agent);
}
inner_builder = inner_builder
.with_encryption_settings(builder.encryption_settings)
.with_room_key_recipient_strategy(builder.room_key_recipient_strategy)
.with_decryption_trust_requirement(builder.decryption_trust_requirement);
match builder.sliding_sync_version_builder {
SlidingSyncVersionBuilder::None => {
inner_builder = inner_builder
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::None)
}
SlidingSyncVersionBuilder::Proxy { url } => {
inner_builder = inner_builder.sliding_sync_version_builder(
MatrixSlidingSyncVersionBuilder::Proxy {
url: Url::parse(&url)
.map_err(|e| ClientBuildError::Generic { message: e.to_string() })?,
},
)
}
SlidingSyncVersionBuilder::Native => {
inner_builder = inner_builder
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::Native)
}
SlidingSyncVersionBuilder::DiscoverProxy => {
inner_builder = inner_builder
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::DiscoverProxy)
}
SlidingSyncVersionBuilder::DiscoverNative => {
inner_builder = inner_builder
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::DiscoverNative)
}
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>>()?,
);
}
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);
}
if let Some(timeout) = config.timeout {
updated_config = updated_config.timeout(Duration::from_millis(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(retry_timeout) = config.retry_timeout {
updated_config = updated_config.retry_timeout(Duration::from_millis(retry_timeout));
}
inner_builder = inner_builder.request_config(updated_config);
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)?));
}
let sdk_client = inner_builder.build().await?;
if builder.use_event_cache_persistent_storage {
// Enable the persistent storage \o/
sdk_client.event_cache().enable_storage()?;
} else {
// Get rid of all the previous events, if any.
let store = sdk_client
.event_cache_store()
.lock()
.await
.map_err(EventCacheError::LockingStorage)?;
store.clear_all_rooms_chunks().await.map_err(EventCacheError::Storage)?;
}
Ok(Arc::new(
Client::new(sdk_client, builder.enable_oidc_refresh_lock, builder.session_delegate)
.await?,
))
Ok(Client::new(
sdk_client,
builder.cross_process_refresh_lock_id,
builder.session_delegate,
)?)
}
}
/// Finish the building of the client and attempt to log in using the
/// provided [`QrCodeData`].
///
/// This method will build the client and immediately attempt to log the
/// client in using the provided [`QrCodeData`] using the login
/// mechanism described in [MSC4108]. As such this methods requires OIDC
/// support as well as sliding sync support.
///
/// The usage of the progress_listener is required to transfer the
/// [`CheckCode`] to the existing client.
///
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
pub async fn build_with_qr_code(
self: Arc<Self>,
qr_code_data: &QrCodeData,
oidc_configuration: &OidcConfiguration,
progress_listener: Box<dyn QrLoginProgressListener>,
) -> Result<Arc<Client>, HumanQrLoginError> {
let QrCodeModeData::Reciprocate { server_name } = &qr_code_data.inner.mode_data else {
return Err(HumanQrLoginError::OtherDeviceNotSignedIn);
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);
let builder = self.server_name_or_homeserver_url(server_name.to_owned());
let client = builder.build().await.map_err(|e| match e {
ClientBuildError::SlidingSync(_) => HumanQrLoginError::SlidingSyncNotAvailable,
_ => {
error!("Couldn't build the client {e:?}");
HumanQrLoginError::Unknown
}
})?;
let client_metadata =
oidc_configuration.try_into().map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
let oidc = client.inner.oidc();
let login = oidc.login_with_qr_code(&qr_code_data.inner, client_metadata);
let mut progress = login.subscribe_to_progress();
// We create this task, which will get cancelled once it's dropped, just in case
// the progress stream doesn't end.
let _progress_task = TaskHandle::new(RUNTIME.spawn(async move {
while let Some(state) = progress.next().await {
progress_listener.on_update(state.into());
}
}));
login.await?;
Ok(client)
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,
}
}
}
#[derive(Clone)]
/// The store paths the client will use when built.
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,
}
#[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.
retry_timeout: Option<u64>,
}
#[derive(Clone, uniffi::Enum)]
pub enum SlidingSyncVersionBuilder {
None,
Proxy { url: String },
Native,
DiscoverProxy,
DiscoverNative,
}
-22
View File
@@ -1,22 +0,0 @@
use serde::Deserialize;
use crate::ClientError;
/// Well-known settings specific to ElementCall
#[derive(Deserialize, uniffi::Record)]
pub struct ElementCallWellKnown {
widget_url: String,
}
/// Element specific well-known settings
#[derive(Deserialize, uniffi::Record)]
pub struct ElementWellKnown {
call: Option<ElementCallWellKnown>,
registration_helper_url: Option<String>,
}
/// Helper function to parse a string into a ElementWellKnown struct
#[matrix_sdk_ffi_macros::export]
pub fn make_element_well_known(string: String) -> Result<ElementWellKnown, ClientError> {
serde_json::from_str(&string).map_err(ClientError::new)
}
+16 -262
View File
@@ -1,49 +1,39 @@
use std::sync::Arc;
use futures_util::StreamExt;
use matrix_sdk::{
encryption,
encryption::{backups, recovery},
};
use matrix_sdk::encryption::{backups, recovery};
use thiserror::Error;
use tracing::{error, info};
use zeroize::Zeroize;
use super::RUNTIME;
use crate::{client::Client, error::ClientError, ruma::AuthData, task_handle::TaskHandle};
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)]
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)]
#[uniffi::export(callback_interface)]
pub trait BackupSteadyStateListener: Sync + Send {
fn on_update(&self, status: BackupUploadState);
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
#[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: Sync + Send {
fn on_update(&self, status: VerificationState);
}
#[derive(uniffi::Enum)]
pub enum BackupUploadState {
Waiting,
@@ -163,7 +153,7 @@ impl From<recovery::RecoveryState> for RecoveryState {
}
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
#[uniffi::export(callback_interface)]
pub trait EnableRecoveryProgressListener: Sync + Send {
fn on_update(&self, status: EnableRecoveryProgress);
}
@@ -196,37 +186,8 @@ 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();
@@ -254,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 {
@@ -281,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(
@@ -316,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();
@@ -327,12 +287,6 @@ 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 = RUNTIME.spawn(async move {
@@ -345,7 +299,6 @@ impl Encryption {
let ret = enable.await?;
task.abort();
passphrase.zeroize();
Ok(ret)
}
@@ -366,22 +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(|e| ClientError::Generic { msg: e.to_string() })?
{
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;
@@ -389,187 +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(RUNTIME.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?)
}
/// 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?)
}
/// 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()
}
}
#[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(|e| ClientError::Generic { msg: e.to_string() })
} else {
self.inner.reset(None).await.map_err(|e| ClientError::Generic { msg: e.to_string() })
}
}
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::Oidc(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::OidcCrossSigningResetInfo> for OidcCrossSigningResetInfo {
fn from(value: &matrix_sdk::encryption::OidcCrossSigningResetInfo) -> Self {
Self { approval_url: value.approval_url.to_string() }
}
}
+6 -607
View File
@@ -1,27 +1,20 @@
use std::{collections::HashMap, fmt, fmt::Display, time::SystemTime};
use std::fmt::Display;
use matrix_sdk::{
authentication::oidc::OidcError, encryption::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};
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 },
#[error("api error {code}: {msg}")]
MatrixApi { kind: ErrorKind, code: String, msg: String },
}
impl ClientError {
pub(crate) fn new<E: Display>(error: E) -> Self {
fn new<E: Display>(error: E) -> Self {
Self::Generic { msg: error.to_string() }
}
}
@@ -32,12 +25,6 @@ impl From<anyhow::Error> for ClientError {
}
}
impl From<reqwest::Error> for ClientError {
fn from(e: reqwest::Error) -> Self {
Self::new(e)
}
}
impl From<UnexpectedUniFFICallbackError> for ClientError {
fn from(e: UnexpectedUniFFICallbackError) -> Self {
Self::new(e)
@@ -46,22 +33,7 @@ impl From<UnexpectedUniFFICallbackError> for ClientError {
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 Self::Generic { msg: message.to_string() };
};
return Self::MatrixApi { kind, code, msg: message.to_owned() };
}
}
Self::Generic { msg: http_error.to_string() }
}
_ => Self::Generic { msg: e.to_string() },
}
Self::new(e)
}
}
@@ -119,12 +91,6 @@ impl From<timeline::Error> for ClientError {
}
}
impl From<timeline::UnsupportedEditItem> for ClientError {
fn from(e: timeline::UnsupportedEditItem) -> Self {
Self::new(e)
}
}
impl From<notification_client::Error> for ClientError {
fn from(e: notification_client::Error) -> Self {
Self::new(e)
@@ -149,132 +115,6 @@ impl From<RoomError> for ClientError {
}
}
impl From<RoomListError> for ClientError {
fn from(e: RoomListError) -> Self {
Self::new(e)
}
}
impl From<EventCacheError> for ClientError {
fn from(e: EventCacheError) -> Self {
Self::new(e)
}
}
impl From<EditError> for ClientError {
fn from(e: EditError) -> Self {
Self::new(e)
}
}
impl From<RoomSendQueueError> for ClientError {
fn from(e: RoomSendQueueError) -> Self {
Self::new(e)
}
}
impl From<NotYetImplemented> for ClientError {
fn from(_: NotYetImplemented) -> Self {
Self::new("This functionality is not implemented yet.")
}
}
impl From<FocusEventError> for ClientError {
fn from(e: FocusEventError) -> Self {
Self::new(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 },
}
}
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum RoomError {
@@ -346,444 +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 = system_time.duration_since(SystemTime::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),
}
}
}
+12 -223
View File
@@ -1,30 +1,16 @@
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) AnySyncTimelineEvent);
#[matrix_sdk_ffi_macros::export]
#[uniffi::export]
impl TimelineEvent {
pub fn event_id(&self) -> String {
self.0.event_id().to_string()
@@ -34,8 +20,8 @@ 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> {
@@ -109,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,
@@ -131,7 +117,6 @@ impl TryFrom<AnySyncStateEvent> for StateEventContent {
pub enum MessageLikeEventContent {
CallAnswer,
CallInvite,
CallNotify { notify_type: NotifyType },
CallHangup,
CallCandidates,
KeyVerificationReady,
@@ -145,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,
}
@@ -156,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(_) => {
@@ -206,21 +185,11 @@ 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"),
};
@@ -247,183 +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,
Image,
Location,
Notice,
ServerNotice,
Text,
Video,
VerificationRequest,
Other,
}
impl From<RumaMessageType> for RoomMessageEventMessageType {
fn from(val: ruma::events::room::message::MessageType) -> Self {
match val {
RumaMessageType::Audio { .. } => Self::Audio,
RumaMessageType::Emote { .. } => Self::Emote,
RumaMessageType::File { .. } => Self::File,
RumaMessageType::Image { .. } => Self::Image,
RumaMessageType::Location { .. } => Self::Location,
RumaMessageType::Notice { .. } => Self::Notice,
RumaMessageType::ServerNotice { .. } => Self::ServerNotice,
RumaMessageType::Text { .. } => Self::Text,
RumaMessageType::Video { .. } => Self::Video,
RumaMessageType::VerificationRequest { .. } => Self::VerificationRequest,
_ => Self::Other,
}
}
}
/// Contains the 2 possible identifiers of an event, either it has a remote
/// event id or a local transaction id, never both or none.
#[derive(Clone, uniffi::Enum)]
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()))
}
}
}
}
+24 -13
View File
@@ -1,52 +1,63 @@
// TODO: target-os conditional would be good.
#![allow(unused_qualifications, clippy::new_without_default)]
#![allow(clippy::empty_line_after_doc_comments)] // Needed because uniffi macros contain empty
// lines after docs.
mod authentication;
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;
mod element;
mod encryption;
mod error;
mod event;
mod helpers;
mod identity_status_change;
mod live_location_share;
mod notification;
mod notification_settings;
mod platform;
mod room;
mod room_alias;
mod room_directory_search;
mod room_info;
mod room_list;
mod room_member;
mod room_preview;
mod ruma;
mod session_verification;
mod sync_service;
mod task_handle;
mod timeline;
mod timeline_event_filter;
mod tracing;
mod utils;
mod widget;
use async_compat::TOKIO1 as RUNTIME;
use matrix_sdk::ruma::events::room::message::RoomMessageEventContentWithoutRelation;
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 -18
View File
@@ -1,11 +1,15 @@
use std::sync::Arc;
use matrix_sdk_ui::notification_client::{
NotificationClient as MatrixNotificationClient, NotificationItem as MatrixNotificationItem,
NotificationClient as MatrixNotificationClient,
NotificationClientBuilder as MatrixNotificationClientBuilder,
NotificationItem as MatrixNotificationItem, NotificationProcessSetup,
};
use ruma::{EventId, RoomId};
use crate::{client::Client, error::ClientError, event::TimelineEvent};
use crate::{
client::Client, error::ClientError, event::TimelineEvent, helpers::unwrap_or_clone_arc, RUNTIME,
};
#[derive(uniffi::Enum)]
pub enum NotificationEvent {
@@ -17,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)]
@@ -60,10 +63,9 @@ impl NotificationItem {
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,
joined_members_count: item.joined_members_count,
@@ -76,37 +78,74 @@ impl NotificationItem {
}
}
#[derive(Clone, uniffi::Object)]
pub struct NotificationClientBuilder {
client: Arc<Client>,
builder: MatrixNotificationClientBuilder,
}
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 }))
}
}
#[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: MatrixNotificationClient,
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 {
/// See also documentation of
/// `MatrixNotificationClient::get_notification`.
pub async fn get_notification(
pub fn get_notification(
&self,
room_id: String,
event_id: String,
) -> 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)?;
if let Some(item) = item {
Ok(Some(NotificationItem::from_inner(item)))
} else {
Ok(None)
}
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,
@@ -13,8 +13,9 @@ use ruma::{
push::{PredefinedOverrideRuleId, PredefinedUnderrideRuleId, RuleKind},
RoomId,
};
use tokio::sync::RwLock as AsyncRwLock;
use tokio::sync::RwLock;
use super::RUNTIME;
use crate::error::NotificationSettingsError;
/// Enum representing the push notification modes for a room.
@@ -49,7 +50,7 @@ impl From<RoomNotificationMode> for SdkRoomNotificationMode {
}
/// Delegate to notify of changes in push rules
#[matrix_sdk_ffi_macros::export(callback_interface)]
#[uniffi::export(callback_interface)]
pub trait NotificationSettingsDelegate: Sync + Send {
fn settings_did_change(&self);
}
@@ -72,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>>>,
}
@@ -81,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)),
}
}
@@ -92,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 {
@@ -110,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;
});
}
}
@@ -135,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
@@ -152,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))
}
@@ -162,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(())
}
@@ -297,10 +297,10 @@ impl NotificationSettings {
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
@@ -308,22 +308,17 @@ impl NotificationSettings {
.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(RuleKind::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,
+125 -250
View File
@@ -1,24 +1,97 @@
use tracing_appender::rolling::{RollingFileAppender, Rotation};
use std::{collections::HashMap, fmt::Debug, pin::Pin};
use base64::{engine::general_purpose::STANDARD, Engine};
use futures_core::future::BoxFuture;
use opentelemetry::KeyValue;
use opentelemetry_otlp::{Protocol, WithExportConfig};
use opentelemetry_sdk::{runtime::RuntimeChannel, trace::Tracer, Resource};
use tokio::runtime::Handle;
use tracing_core::Subscriber;
use tracing_subscriber::{
field::RecordFields,
fmt::{
self,
format::{DefaultFields, Writer},
time::FormatTime,
FormatEvent, FormatFields, FormattedFields,
},
fmt::{self, time::FormatTime, FormatEvent, FormatFields, FormattedFields},
layer::SubscriberExt,
registry::LookupSpan,
util::SubscriberInitExt,
EnvFilter, Layer,
};
use crate::tracing::LogLevel;
use crate::RUNTIME;
#[derive(Clone, Debug)]
struct TokioRuntime {
runtime: Handle,
}
impl opentelemetry_sdk::runtime::Runtime for TokioRuntime {
type Interval = tokio_stream::wrappers::IntervalStream;
type Delay = Pin<Box<tokio::time::Sleep>>;
fn interval(&self, period: std::time::Duration) -> Self::Interval {
let _guard = self.runtime.enter();
tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(period))
}
fn spawn(&self, future: BoxFuture<'static, ()>) {
#[allow(clippy::let_underscore_future)]
let _ = self.runtime.spawn(future);
}
fn delay(&self, duration: std::time::Duration) -> Self::Delay {
let _guard = self.runtime.enter();
Box::pin(tokio::time::sleep(duration))
}
}
impl RuntimeChannel for TokioRuntime {
type Receiver<T: Debug + Send> = tokio_stream::wrappers::ReceiverStream<T>;
type Sender<T: Debug + Send> = tokio::sync::mpsc::Sender<T>;
fn batch_message_channel<T: Debug + Send>(
&self,
capacity: usize,
) -> (Self::Sender<T>, Self::Receiver<T>) {
let (sender, receiver) = tokio::sync::mpsc::channel(capacity);
(sender, tokio_stream::wrappers::ReceiverStream::new(receiver))
}
}
pub fn create_otlp_tracer(
user: String,
password: String,
otlp_endpoint: String,
client_name: String,
) -> anyhow::Result<Tracer> {
let runtime = RUNTIME.handle().to_owned();
let auth = STANDARD.encode(format!("{user}:{password}"));
let headers = HashMap::from([("Authorization".to_owned(), format!("Basic {auth}"))]);
let http_client = matrix_sdk::reqwest::ClientBuilder::new().build()?;
let exporter = opentelemetry_otlp::new_exporter()
.http()
.with_http_client(http_client)
.with_protocol(Protocol::HttpBinary)
.with_endpoint(otlp_endpoint)
.with_headers(headers);
let tracer_runtime = TokioRuntime { runtime: runtime.to_owned() };
let _guard = runtime.enter();
let tracer = opentelemetry_otlp::new_pipeline()
.tracing()
.with_exporter(exporter)
.with_trace_config(
opentelemetry_sdk::trace::config()
.with_resource(Resource::new(vec![KeyValue::new("service.name", client_name)])),
)
.install_batch(tracer_runtime)?;
Ok(tracer)
}
#[cfg(target_os = "android")]
pub fn log_panics() {
std::env::set_var("RUST_BACKTRACE", "1");
log_panics::init();
}
@@ -106,19 +179,17 @@ where
if let Some(scope) = ctx.event_scope() {
writer.write_str(" | spans: ")?;
let mut first = true;
for span in scope.from_root() {
if !first {
writer.write_str(" > ")?;
}
first = false;
write!(writer, "{}", span.metadata().name())?;
write!(writer, "{}", span.name())?;
if let Some(fields) = &span.extensions().get::<FormattedFields<N>>() {
let ext = span.extensions();
if let Some(fields) = &ext.get::<FormattedFields<N>>() {
if !fields.is_empty() {
write!(writer, "{{{fields}}}")?;
}
@@ -131,69 +202,20 @@ where
}
let file_layer = config.write_to_files.map(|c| {
let mut builder = RollingFileAppender::builder()
.rotation(Rotation::HOURLY)
.filename_prefix(&c.file_prefix);
if let Some(max_files) = c.max_files {
builder = builder.max_log_files(max_files as usize)
};
if let Some(file_suffix) = c.file_suffix {
builder = builder.filename_suffix(file_suffix)
}
let writer = builder.build(&c.path).expect("Failed to create a rolling file appender.");
// Another fields formatter is necessary because of this bug
// https://github.com/tokio-rs/tracing/issues/1372. Using a new
// formatter for the fields forces to record them in different span
// extensions, and thus remove the duplicated fields in the span.
#[derive(Default)]
struct FieldsFormatterForFiles(DefaultFields);
impl<'writer> FormatFields<'writer> for FieldsFormatterForFiles {
fn format_fields<R: RecordFields>(
&self,
writer: Writer<'writer>,
fields: R,
) -> std::fmt::Result {
self.0.format_fields(writer, fields)
}
}
fmt::layer()
.fmt_fields(FieldsFormatterForFiles::default())
.event_format(EventFormatter::new())
// EventFormatter doesn't support ANSI colors anyways, but the
// default field formatter does, which is unhelpful for iOS +
// Android logs, but enabled by default.
.with_ansi(false)
.with_writer(writer)
.with_writer(tracing_appender::rolling::hourly(c.path, c.file_prefix))
});
Layer::and_then(
file_layer,
config.write_to_stdout_or_system.then(|| {
// Another fields formatter is necessary because of this bug
// https://github.com/tokio-rs/tracing/issues/1372. Using a new
// formatter for the fields forces to record them in different span
// extensions, and thus remove the duplicated fields in the span.
#[derive(Default)]
struct FieldsFormatterFormStdoutOrSystem(DefaultFields);
impl<'writer> FormatFields<'writer> for FieldsFormatterFormStdoutOrSystem {
fn format_fields<R: RecordFields>(
&self,
writer: Writer<'writer>,
fields: R,
) -> std::fmt::Result {
self.0.format_fields(writer, fields)
}
}
#[cfg(not(target_os = "android"))]
return fmt::layer()
.fmt_fields(FieldsFormatterFormStdoutOrSystem::default())
.event_format(EventFormatter::new())
// See comment above.
.with_ansi(false)
@@ -201,7 +223,6 @@ where
#[cfg(target_os = "android")]
return fmt::layer()
.fmt_fields(FieldsFormatterFormStdoutOrSystem::default())
.event_format(EventFormatter::for_logcat())
// See comment above.
.with_ansi(false)
@@ -212,208 +233,62 @@ where
)
}
/// Configuration to save logs to (rotated) log-files.
#[derive(uniffi::Record)]
pub struct TracingFileConfiguration {
/// Base location for all the log files.
path: String,
/// Prefix for the log files' names.
file_prefix: String,
/// Optional suffix for the log file's names.
file_suffix: Option<String>,
/// Maximum number of rotated files.
///
/// If not set, there's no max limit, i.e. the number of log files is
/// unlimited.
max_files: Option<u64>,
}
#[derive(PartialEq, PartialOrd)]
enum LogTarget {
Hyper,
MatrixSdkFfi,
MatrixSdk,
MatrixSdkClient,
MatrixSdkCrypto,
MatrixSdkCryptoAccount,
MatrixSdkOidc,
MatrixSdkHttpClient,
MatrixSdkSlidingSync,
MatrixSdkBaseSlidingSync,
MatrixSdkUiTimeline,
MatrixSdkEventCache,
MatrixSdkBaseEventCache,
MatrixSdkEventCacheStore,
}
impl LogTarget {
fn as_str(&self) -> &'static str {
match self {
LogTarget::Hyper => "hyper",
LogTarget::MatrixSdkFfi => "matrix_sdk_ffi",
LogTarget::MatrixSdk => "matrix_sdk",
LogTarget::MatrixSdkClient => "matrix_sdk::client",
LogTarget::MatrixSdkCrypto => "matrix_sdk_crypto",
LogTarget::MatrixSdkCryptoAccount => "matrix_sdk_crypto::olm::account",
LogTarget::MatrixSdkOidc => "matrix_sdk::oidc",
LogTarget::MatrixSdkHttpClient => "matrix_sdk::http_client",
LogTarget::MatrixSdkSlidingSync => "matrix_sdk::sliding_sync",
LogTarget::MatrixSdkBaseSlidingSync => "matrix_sdk_base::sliding_sync",
LogTarget::MatrixSdkUiTimeline => "matrix_sdk_ui::timeline",
LogTarget::MatrixSdkEventCache => "matrix_sdk::event_cache",
LogTarget::MatrixSdkBaseEventCache => "matrix_sdk_base::event_cache",
LogTarget::MatrixSdkEventCacheStore => "matrix_sdk_sqlite::event_cache_store",
}
}
}
const DEFAULT_TARGET_LOG_LEVELS: &[(LogTarget, LogLevel)] = &[
(LogTarget::Hyper, LogLevel::Warn),
(LogTarget::MatrixSdkFfi, LogLevel::Info),
(LogTarget::MatrixSdk, LogLevel::Info),
(LogTarget::MatrixSdkClient, LogLevel::Trace),
(LogTarget::MatrixSdkCrypto, LogLevel::Debug),
(LogTarget::MatrixSdkCryptoAccount, LogLevel::Trace),
(LogTarget::MatrixSdkOidc, LogLevel::Trace),
(LogTarget::MatrixSdkHttpClient, LogLevel::Debug),
(LogTarget::MatrixSdkSlidingSync, LogLevel::Info),
(LogTarget::MatrixSdkBaseSlidingSync, LogLevel::Info),
(LogTarget::MatrixSdkUiTimeline, LogLevel::Info),
(LogTarget::MatrixSdkEventCache, LogLevel::Info),
(LogTarget::MatrixSdkBaseEventCache, LogLevel::Info),
(LogTarget::MatrixSdkEventCacheStore, LogLevel::Info),
];
const IMMUTABLE_TARGET_LOG_LEVELS: &[LogTarget] = &[
LogTarget::Hyper, // Too verbose
LogTarget::MatrixSdk, // Too generic
LogTarget::MatrixSdkFfi, // Too verbose
];
#[derive(uniffi::Record)]
pub struct TracingConfiguration {
/// The desired log level
log_level: LogLevel,
/// Additional targets that the FFI client would like to use e.g.
/// the target names for created [`crate::tracing::Span`]
extra_targets: Option<Vec<String>>,
/// Whether to log to stdout, or in the logcat on Android.
filter: String,
/// Controls whether to print to stdout or, equivalent, the system logs on
/// Android.
write_to_stdout_or_system: bool,
/// If set, configures rotated log files where to write additional logs.
write_to_files: Option<TracingFileConfiguration>,
}
fn build_tracing_filter(config: &TracingConfiguration) -> String {
// We are intentionally not setting a global log level because we don't want to
// risk third party crates logging sensitive information.
// As such we need to make sure that panics will be properly logged.
// On 2025-01-08, `log_panics` uses the `panic` target, at the error log level.
let mut filters = vec!["panic=error".to_owned()];
DEFAULT_TARGET_LOG_LEVELS.iter().for_each(|(target, level)| {
// Use the default if the log level shouldn't be changed for this target or
// if it's already logging more than requested
let level = if IMMUTABLE_TARGET_LOG_LEVELS.contains(target) || level > &config.log_level {
level.as_str()
} else {
config.log_level.as_str()
};
filters.push(format!("{}={}", target.as_str(), level));
});
// Finally append the extra targets requested by the client
if let Some(extra_targets) = &config.extra_targets {
for target in extra_targets {
filters.push(format!("{}={}", target, config.log_level.as_str()));
}
}
filters.join(",")
}
#[matrix_sdk_ffi_macros::export]
#[uniffi::export]
pub fn setup_tracing(config: TracingConfiguration) {
#[cfg(target_os = "android")]
log_panics();
tracing_subscriber::registry()
.with(EnvFilter::new(build_tracing_filter(&config)))
.with(EnvFilter::new(&config.filter))
.with(text_layers(config))
.init();
}
#[cfg(test)]
mod tests {
use super::build_tracing_filter;
#[test]
fn test_default_tracing_filter() {
let config = super::TracingConfiguration {
log_level: super::LogLevel::Error,
extra_targets: Some(vec!["super_duper_app".to_owned()]),
write_to_stdout_or_system: true,
write_to_files: None,
};
let filter = build_tracing_filter(&config);
assert_eq!(
filter,
"panic=error,\
hyper=warn,\
matrix_sdk_ffi=info,\
matrix_sdk=info,\
matrix_sdk::client=trace,\
matrix_sdk_crypto=debug,\
matrix_sdk_crypto::olm::account=trace,\
matrix_sdk::oidc=trace,\
matrix_sdk::http_client=debug,\
matrix_sdk::sliding_sync=info,\
matrix_sdk_base::sliding_sync=info,\
matrix_sdk_ui::timeline=info,\
matrix_sdk::event_cache=info,\
matrix_sdk_base::event_cache=info,\
matrix_sdk_sqlite::event_cache_store=info,\
super_duper_app=error"
);
}
#[test]
fn test_trace_tracing_filter() {
let config = super::TracingConfiguration {
log_level: super::LogLevel::Trace,
extra_targets: Some(vec!["super_duper_app".to_owned(), "some_other_span".to_owned()]),
write_to_stdout_or_system: true,
write_to_files: None,
};
let filter = build_tracing_filter(&config);
assert_eq!(
filter,
"panic=error,\
hyper=warn,\
matrix_sdk_ffi=info,\
matrix_sdk=info,\
matrix_sdk::client=trace,\
matrix_sdk_crypto=trace,\
matrix_sdk_crypto::olm::account=trace,\
matrix_sdk::oidc=trace,\
matrix_sdk::http_client=trace,\
matrix_sdk::sliding_sync=trace,\
matrix_sdk_base::sliding_sync=trace,\
matrix_sdk_ui::timeline=trace,\
matrix_sdk::event_cache=trace,\
matrix_sdk_base::event_cache=trace,\
matrix_sdk_sqlite::event_cache_store=trace,\
super_duper_app=trace,\
some_other_span=trace"
);
}
#[derive(uniffi::Record)]
pub struct OtlpTracingConfiguration {
client_name: String,
user: String,
password: String,
otlp_endpoint: String,
filter: String,
/// Controls whether to print to stdout or, equivalent, the system logs on
/// Android.
write_to_stdout_or_system: bool,
write_to_files: Option<TracingFileConfiguration>,
}
#[uniffi::export]
pub fn setup_otlp_tracing(config: OtlpTracingConfiguration) {
#[cfg(target_os = "android")]
log_panics();
let otlp_tracer =
create_otlp_tracer(config.user, config.password, config.otlp_endpoint, config.client_name)
.expect("Couldn't configure the OpenTelemetry tracer");
let otlp_layer = tracing_opentelemetry::layer().with_tracer(otlp_tracer);
tracing_subscriber::registry()
.with(EnvFilter::new(&config.filter))
.with(text_layers(TracingConfiguration {
filter: config.filter,
write_to_stdout_or_system: config.write_to_stdout_or_system,
write_to_files: config.write_to_files,
}))
.with(otlp_layer)
.init();
}
File diff suppressed because it is too large Load Diff
-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,203 +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 ruma::ServerName;
use tokio::sync::RwLock;
use super::RUNTIME;
use crate::{error::ClientError, task_handle::TaskHandle};
#[derive(uniffi::Enum)]
pub enum PublicRoomJoinRule {
Public,
Knock,
}
impl TryFrom<ruma::directory::PublicRoomJoinRule> for PublicRoomJoinRule {
type Error = String;
fn try_from(value: ruma::directory::PublicRoomJoinRule) -> Result<Self, Self::Error> {
match value {
ruma::directory::PublicRoomJoinRule::Public => Ok(Self::Public),
ruma::directory::PublicRoomJoinRule::Knock => Ok(Self::Knock),
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();
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
listener.on_update(vec![RoomDirectorySearchEntryUpdate::Reset {
values: initial_values.into_iter().map(Into::into).collect(),
}]);
while let Some(diffs) = stream.next().await {
listener.on_update(diffs.into_iter().map(|diff| diff.into()).collect());
}
})))
}
}
#[derive(uniffi::Record)]
pub struct RoomDirectorySearchEntriesResult {
pub entries_stream: Arc<TaskHandle>,
}
#[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: Send + Sync + Debug {
fn on_update(&self, room_entries_update: Vec<RoomDirectorySearchEntryUpdate>);
}
+22 -67
View File
@@ -1,53 +1,36 @@
use std::collections::HashMap;
use std::sync::Arc;
use matrix_sdk::RoomState;
use tracing::warn;
use ruma::OwnedMxcUri;
use crate::{
client::JoinRule,
error::ClientError,
notification_settings::RoomNotificationMode,
room::{Membership, RoomHero, RoomHistoryVisibility},
room_member::RoomMember,
notification_settings::RoomNotificationMode, room::Membership, room_member::RoomMember,
timeline::EventTimelineItem,
};
#[derive(uniffi::Record)]
pub struct RoomInfo {
id: String,
creator: Option<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>,
name: Option<String>,
topic: Option<String>,
avatar_url: Option<String>,
is_direct: bool,
is_public: bool,
is_space: bool,
is_tombstoned: bool,
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>,
latest_event: Option<Arc<EventTimelineItem>>,
inviter: Option<Arc<RoomMember>>,
active_members_count: u64,
invited_members_count: u64,
joined_members_count: u64,
user_power_levels: HashMap<String, i64>,
highlight_count: u64,
notification_count: u64,
cached_user_defined_notification_mode: Option<RoomNotificationMode>,
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,
@@ -57,67 +40,43 @@ pub struct RoomInfo {
/// 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,
}
impl RoomInfo {
pub(crate) async fn new(room: &matrix_sdk::Room) -> Result<Self, ClientError> {
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();
let power_levels_map = room.users_with_power_levels().await;
let mut user_power_levels = HashMap::<String, i64>::new();
for (id, level) in power_levels_map.iter() {
user_power_levels.insert(id.to_string(), *level);
}
let pinned_event_ids =
room.pinned_event_ids().unwrap_or_default().iter().map(|id| id.to_string()).collect();
let join_rule = room.join_rule().try_into();
if let Err(e) = &join_rule {
warn!("Failed to parse join rule: {:?}", e);
}
Ok(Self {
id: room.room_id().to_string(),
creator: room.creator().as_ref().map(ToString::to_string),
display_name: room.cached_display_name().map(|name| name.to_string()),
raw_name: room.name(),
name: room.name(),
topic: room.topic(),
avatar_url: room.avatar_url().map(Into::into),
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(),
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(),
latest_event,
inviter: match room.state() {
RoomState::Invited => room
.invite_details()
.await
.ok()
.and_then(|details| details.inviter)
.map(TryInto::try_into)
.transpose()
.ok()
.flatten(),
RoomState::Invited => {
room.invite_details().await?.inviter.map(|inner| Arc::new(RoomMember { inner }))
}
_ => 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(),
user_power_levels,
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()
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
@@ -125,13 +84,9 @@ impl RoomInfo {
.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: join_rule.ok(),
history_visibility: room.history_visibility_or_default().try_into()?,
})
}
}
+212 -405
View File
@@ -1,34 +1,28 @@
#![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, TryFutureExt};
use matrix_sdk::ruma::{
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
RoomId,
};
use matrix_sdk_ui::{
room_list_service::filters::{
new_filter_all, new_filter_any, new_filter_category, new_filter_favourite,
new_filter_fuzzy_match_room_name, new_filter_invite, new_filter_joined,
new_filter_non_left, new_filter_none, new_filter_normalized_match_room_name,
new_filter_unread, BoxedFilterFn, RoomCategory,
use futures_util::{pin_mut, StreamExt};
use matrix_sdk::{
ruma::{
api::client::sync::sync_events::{
v4::RoomSubscription as RumaRoomSubscription,
UnreadNotificationsCount as RumaUnreadNotificationsCount,
},
assign, RoomId,
},
timeline::default_event_filter,
unable_to_decrypt_hook::UtdHookManager,
RoomListEntry as MatrixRoomListEntry,
};
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 ruma::{OwnedRoomOrAliasId, OwnedServerName, ServerName};
use tokio::sync::RwLock;
use crate::{
error::ClientError,
room::{Membership, Room},
room::Room,
room_info::RoomInfo,
room_preview::RoomPreview,
timeline::{EventTimelineItem, Timeline},
timeline_event_filter::TimelineEventTypeFilter,
utils::AsyncRuntimeDropped,
TaskHandle, RUNTIME,
};
@@ -44,16 +38,6 @@ pub enum RoomListError {
RoomNotFound { room_name: String },
#[error("invalid room ID: {error}")]
InvalidRoomId { error: String },
#[error("A timeline instance already exists for room {room_name}")]
TimelineAlreadyExists { room_name: String },
#[error("A timeline instance hasn't been initialized for room {room_name}")]
TimelineNotInitialized { room_name: String },
#[error("Timeline couldn't be initialized: {error}")]
InitializingTimeline { 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 {
@@ -63,14 +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() },
TimelineAlreadyExists(room_id) => {
Self::TimelineAlreadyExists { room_name: room_id.to_string() }
}
InitializingTimeline(source) => {
Self::InitializingTimeline { error: source.to_string() }
}
EventCache(error) => Self::EventCache { error: error.to_string() },
}
}
}
@@ -81,13 +59,33 @@ 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();
@@ -105,8 +103,7 @@ impl RoomListService {
let room_id = <&RoomId>::try_from(room_id.as_str()).map_err(RoomListError::from)?;
Ok(Arc::new(RoomListItem {
inner: Arc::new(self.inner.room(room_id)?),
utd_hook: self.utd_hook.clone(),
inner: Arc::new(RUNTIME.block_on(async { self.inner.room(room_id).await })?),
}))
}
@@ -117,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,
@@ -136,19 +144,6 @@ impl RoomListService {
}
})))
}
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<_>>());
Ok(())
}
}
#[derive(uniffi::Object)]
@@ -157,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,
@@ -177,98 +172,42 @@ impl RoomList {
})
}
fn entries(&self, listener: Box<dyn RoomListEntriesListener>) -> RoomListEntriesResult {
let (entries, entries_stream) = self.inner.entries();
RoomListEntriesResult {
entries: entries.into_iter().map(Into::into).collect(),
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 entries_with_dynamic_adapters(
self: Arc<Self>,
&self,
page_size: u32,
listener: Box<dyn RoomListEntriesListener>,
) -> Arc<RoomListEntriesWithDynamicAdaptersResult> {
let this = self.clone();
let utd_hook = self.room_list_service.utd_hook.clone();
// 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.
// 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);
}
// 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`.
) -> RoomListEntriesWithDynamicAdaptersResult {
let (entries_stream, dynamic_entries_controller) =
this.inner.entries_with_dynamic_adapters(page_size.try_into().unwrap());
self.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));
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);
let entries_stream = Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
pin_mut!(entries_stream);
while let Some(diffs) = entries_stream.next().await {
listener.on_update(
diffs
.into_iter()
.map(|diff| RoomListEntriesUpdate::from(diff, utd_hook.clone()))
.collect(),
);
}
})));
// Initialize the second field `controller`.
//
// SAFETY: `ptr` is correctly aligned.
unsafe {
addr_of_mut!((*ptr).controller).write(dynamic_entries_controller);
while let Some(diff) = entries_stream.next().await {
listener.on_update(diff.into_iter().map(Into::into).collect());
}
}))),
}
// 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<RoomListItem>, RoomListError> {
@@ -276,22 +215,16 @@ impl RoomList {
}
}
#[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)]
@@ -361,80 +294,65 @@ impl From<matrix_sdk_ui::room_list_service::RoomListLoadingState> for RoomListLo
}
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
#[uniffi::export(callback_interface)]
pub trait RoomListServiceStateListener: Send + Sync + Debug {
fn on_update(&self, state: RoomListServiceState);
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
#[uniffi::export(callback_interface)]
pub trait RoomListLoadingStateListener: Send + Sync + Debug {
fn on_update(&self, state: RoomListLoadingState);
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
#[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<RoomListItem>> },
Append { values: Vec<RoomListEntry> },
Clear,
PushFront { value: Arc<RoomListItem> },
PushBack { value: Arc<RoomListItem> },
PushFront { value: RoomListEntry },
PushBack { value: RoomListEntry },
PopFront,
PopBack,
Insert { index: u32, value: Arc<RoomListItem> },
Set { index: u32, value: Arc<RoomListItem> },
Insert { index: u32, value: RoomListEntry },
Set { index: u32, value: RoomListEntry },
Remove { index: u32 },
Truncate { length: u32 },
Reset { values: Vec<Arc<RoomListItem>> },
Reset { values: Vec<RoomListEntry> },
}
impl RoomListEntriesUpdate {
fn from(
vector_diff: VectorDiff<matrix_sdk_ui::room_list_service::Room>,
utd_hook: Option<Arc<UtdHookManager>>,
) -> Self {
match vector_diff {
VectorDiff::Append { values } => Self::Append {
values: values
.into_iter()
.map(|value| Arc::new(RoomListItem::from(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(RoomListItem::from(value, utd_hook)) }
}
VectorDiff::PushBack { value } => {
Self::PushBack { value: Arc::new(RoomListItem::from(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(RoomListItem::from(value, utd_hook)),
},
VectorDiff::Set { index, value } => Self::Set {
index: u32::try_from(index).unwrap(),
value: Arc::new(RoomListItem::from(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(RoomListItem::from(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)]
#[uniffi::export(callback_interface)]
pub trait RoomListEntriesListener: Send + Sync + Debug {
fn on_update(&self, room_entries_update: Vec<RoomListEntriesUpdate>);
}
@@ -442,20 +360,34 @@ pub trait RoomListEntriesListener: Send + Sync + Debug {
#[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) {
@@ -469,88 +401,26 @@ impl RoomListDynamicEntriesController {
#[derive(uniffi::Enum)]
pub enum RoomListEntriesDynamicFilterKind {
All { filters: Vec<RoomListEntriesDynamicFilterKind> },
Any { filters: Vec<RoomListEntriesDynamicFilterKind> },
NonLeft,
Joined,
Unread,
Favourite,
Invite,
Category { expect: RoomListFilterCategory },
All,
AllNonLeft,
None,
NormalizedMatchRoomName { pattern: String },
FuzzyMatchRoomName { pattern: String },
}
#[derive(uniffi::Enum)]
pub enum RoomListFilterCategory {
Group,
People,
}
impl From<RoomListFilterCategory> for RoomCategory {
fn from(value: RoomListFilterCategory) -> Self {
match value {
RoomListFilterCategory::Group => Self::Group,
RoomListFilterCategory::People => Self::People,
}
}
}
impl From<RoomListEntriesDynamicFilterKind> for BoxedFilterFn {
fn from(value: RoomListEntriesDynamicFilterKind) -> Self {
use RoomListEntriesDynamicFilterKind as Kind;
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::Joined => Box::new(new_filter_joined()),
Kind::Unread => Box::new(new_filter_unread()),
Kind::Favourite => Box::new(new_filter_favourite()),
Kind::Invite => Box::new(new_filter_invite()),
Kind::Category { expect } => Box::new(new_filter_category(expect.into())),
Kind::None => Box::new(new_filter_none()),
Kind::NormalizedMatchRoomName { pattern } => {
Box::new(new_filter_normalized_match_room_name(&pattern))
}
Kind::FuzzyMatchRoomName { pattern } => {
Box::new(new_filter_fuzzy_match_room_name(&pattern))
}
}
}
}
#[derive(uniffi::Object)]
pub struct RoomListItem {
inner: Arc<matrix_sdk_ui::room_list_service::Room>,
utd_hook: Option<Arc<UtdHookManager>>,
}
impl RoomListItem {
fn from(
value: matrix_sdk_ui::room_list_service::Room,
utd_hook: Option<Arc<UtdHookManager>>,
) -> Self {
Self { inner: Arc::new(value), utd_hook }
}
}
#[matrix_sdk_ffi_macros::export]
#[uniffi::export(async_runtime = "tokio")]
impl RoomListItem {
fn id(&self) -> String {
self.inner.id().to_string()
}
/// Returns the room's name from the state event if available, otherwise
/// compute a room name based on the room's nature (DM or not) and number of
/// members.
fn display_name(&self) -> Option<String> {
self.inner.cached_display_name()
fn name(&self) -> Option<String> {
RUNTIME.block_on(async { self.inner.name().await })
}
fn avatar_url(&self) -> Option<String> {
@@ -558,164 +428,101 @@ impl RoomListItem {
}
fn is_direct(&self) -> bool {
RUNTIME.block_on(self.inner.inner_room().is_direct()).unwrap_or(false)
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())
}
async fn room_info(&self) -> Result<RoomInfo, ClientError> {
RoomInfo::new(self.inner.inner_room()).await
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?)
}
/// The room's current membership state.
fn membership(&self) -> Membership {
self.inner.inner_room().state().into()
/// 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)))),
))
}
/// Builds a `Room` FFI from an invited room without initializing its
/// internal timeline.
///
/// An error will be returned if the room is a state different than invited.
///
/// ⚠️ Holding on to this room instance after it has been joined is not
/// safe. Use `full_room` instead.
#[deprecated(note = "Please use `preview_room` instead.")]
fn invited_room(&self) -> Result<Arc<Room>, RoomListError> {
if !matches!(self.membership(), Membership::Invited) {
return Err(RoomListError::IncorrectRoomMembership {
expected: vec![Membership::Invited],
actual: self.membership(),
});
}
Ok(Arc::new(Room::new(self.inner.inner_room().clone())))
// Temporary workaround for coroutine leaks on Kotlin.
fn full_room_blocking(&self) -> Arc<Room> {
RUNTIME.block_on(async move { self.full_room().await })
}
/// Builds a `RoomPreview` from a room list item. This is intended for
/// invited or knocked rooms.
///
/// An error will be returned if the room is in a state other than invited
/// or knocked.
async fn preview_room(&self, via: Vec<String>) -> Result<Arc<RoomPreview>, ClientError> {
// Validate parameters first.
let server_names: Vec<OwnedServerName> = via
.into_iter()
.map(|server| ServerName::parse(server).map_err(ClientError::from))
.collect::<Result<_, ClientError>>()?;
fn subscribe(&self, settings: Option<RoomSubscription>) {
self.inner.subscribe(settings.map(Into::into));
}
// Validate internal room state.
let membership = self.membership();
if !matches!(membership, Membership::Invited | Membership::Knocked) {
return Err(RoomListError::IncorrectRoomMembership {
expected: vec![Membership::Invited, Membership::Knocked],
actual: membership,
}
.into());
}
fn unsubscribe(&self) {
self.inner.unsubscribe();
}
// Do the thing.
let client = self.inner.client();
let (room_or_alias_id, mut server_names) = if let Some(alias) = self.inner.canonical_alias()
{
let room_or_alias_id: OwnedRoomOrAliasId = alias.into();
(room_or_alias_id, Vec::new())
} else {
let room_or_alias_id: OwnedRoomOrAliasId = self.inner.id().to_owned().into();
(room_or_alias_id, server_names)
};
async fn latest_event(&self) -> Option<Arc<EventTimelineItem>> {
self.inner.latest_event().await.map(EventTimelineItem).map(Arc::new)
}
// If no server names are provided and the room's membership is invited,
// add the server name from the sender's user id as a fallback value
if server_names.is_empty() {
if let Ok(invite_details) = self.inner.invite_details().await {
if let Some(inviter) = invite_details.inviter {
server_names.push(inviter.user_id().server_name().to_owned());
}
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 {
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() }
}
}
let room_preview = client.get_room_preview(&room_or_alias_id, server_names).await?;
Ok(Arc::new(RoomPreview::new(AsyncRuntimeDropped::new(client), room_preview)))
}
}
/// Build a full `Room` FFI object, filling its associated timeline.
///
/// An error will be returned if the room is a state different than joined
/// or if its internal timeline hasn't been initialized.
fn full_room(&self) -> Result<Arc<Room>, RoomListError> {
if !matches!(self.membership(), Membership::Joined) {
return Err(RoomListError::IncorrectRoomMembership {
expected: vec![Membership::Joined],
actual: self.membership(),
});
}
#[derive(uniffi::Record)]
pub struct RequiredState {
pub key: String,
pub value: String,
}
if let Some(timeline) = self.inner.timeline() {
Ok(Arc::new(Room::with_timeline(
self.inner.inner_room().clone(),
Arc::new(RwLock::new(Some(Timeline::from_arc(timeline)))),
)))
} else {
Err(RoomListError::TimelineNotInitialized {
room_name: self.inner.inner_room().room_id().to_string(),
})
}
}
#[derive(uniffi::Record)]
pub struct RoomSubscription {
pub required_state: Option<Vec<RequiredState>>,
pub timeline_limit: Option<u32>,
}
/// Checks whether the Room's timeline has been initialized before.
fn is_timeline_initialized(&self) -> bool {
self.inner.is_timeline_initialized()
}
/// Initializes the timeline for this room using the provided parameters.
///
/// * `event_type_filter` - An optional [`TimelineEventTypeFilter`] to be
/// used to filter timeline events besides the default timeline filter. If
/// `None` is passed, only the default timeline filter will be used.
/// * `internal_id_prefix` - An optional String that will be prepended to
/// all the timeline item's internal IDs, making it possible to
/// distinguish different timeline instances from each other.
async fn init_timeline(
&self,
event_type_filter: Option<Arc<TimelineEventTypeFilter>>,
internal_id_prefix: Option<String>,
) -> Result<(), RoomListError> {
let mut timeline_builder = self
.inner
.default_room_timeline_builder()
.await
.map_err(|err| RoomListError::InitializingTimeline { error: err.to_string() })?;
if let Some(event_type_filter) = event_type_filter {
timeline_builder = timeline_builder.event_filter(move |event, room_version_id| {
// Always perform the default filter first
default_event_filter(event, room_version_id) && event_type_filter.filter(event)
});
}
if let Some(internal_id_prefix) = internal_id_prefix {
timeline_builder = timeline_builder.with_internal_id_prefix(internal_id_prefix);
}
if let Some(utd_hook) = self.utd_hook.clone() {
timeline_builder = timeline_builder.with_unable_to_decrypt_hook(utd_hook);
}
self.inner.init_timeline_with_builder(timeline_builder).map_err(RoomListError::from).await
}
/// Checks whether the room is encrypted or not.
///
/// **Note**: this info may not be reliable if you don't set up
/// `m.room.encryption` as required state.
async fn is_encrypted(&self) -> bool {
self.inner.is_encrypted().await.unwrap_or(false)
}
async fn latest_event(&self) -> Option<EventTimelineItem> {
self.inner.latest_event().await.map(Into::into)
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())
})
}
}
@@ -725,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
+199 -72
View File
@@ -1,7 +1,7 @@
use matrix_sdk::room::{RoomMember as SdkRoomMember, RoomMemberRole};
use ruma::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,92 +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"
),
}
}
}
#[matrix_sdk_ffi_macros::export]
pub fn suggested_role_for_power_level(power_level: i64) -> RoomMemberRole {
// It's not possible to expose the constructor on the Enum through Uniffi ☹️
RoomMemberRole::suggested_role_for_power_level(power_level)
}
#[matrix_sdk_ffi_macros::export]
pub fn suggested_power_level_for_role(role: RoomMemberRole) -> i64 {
// It's not possible to expose methods on an Enum through Uniffi ☹️
role.suggested_power_level()
}
/// 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: i64,
pub normalized_power_level: i64,
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(),
normalized_power_level: m.normalized_power_level(),
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()
}
}
impl RoomMember {
pub fn new(room_member: SdkRoomMember) -> Self {
RoomMember { inner: room_member }
}
}
#[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,
CallInvite,
CallHangup,
CallCandidates,
KeyVerificationReady,
KeyVerificationStart,
KeyVerificationCancel,
KeyVerificationAccept,
KeyVerificationKey,
KeyVerificationMac,
KeyVerificationDone,
ReactionSent,
RoomEncrypted,
RoomMessage,
RoomRedaction,
Sticker,
}
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,
}
}
}
-182
View File
@@ -1,182 +0,0 @@
use anyhow::Context as _;
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, Client};
use ruma::{room::RoomType as RumaRoomType, space::SpaceRoomJoinRule};
use tracing::warn;
use crate::{
client::JoinRule,
error::ClientError,
room::{Membership, RoomHero},
room_member::RoomMember,
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) -> Result<RoomPreviewInfo, ClientError> {
let info = &self.inner;
Ok(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.as_ref().into(),
is_history_world_readable: info.is_world_readable,
membership: info.state.map(|state| state.into()),
join_rule: info
.join_rule
.clone()
.try_into()
.map_err(|_| anyhow::anyhow!("unhandled SpaceRoomJoinRule kind"))?,
is_direct: info.is_direct,
heroes: info
.heroes
.as_ref()
.map(|heroes| heroes.iter().map(|h| h.to_owned().into()).collect()),
})
}
/// Leave the room if the room preview state is either joined, invited or
/// knocked.
///
/// 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")?;
room.leave().await.map_err(Into::into)
}
/// 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<RoomMembershipDetails> {
let room = self.client.get_room(&self.inner.room_id)?;
let (own_member, sender_member) = match room.own_membership_details().await {
Ok(memberships) => memberships,
Err(error) => {
warn!("Couldn't get membership info: {error}");
return None;
}
};
Some(RoomMembershipDetails {
own_room_member: own_member.try_into().ok()?,
sender_room_member: sender_member.and_then(|member| member.try_into().ok()),
})
}
}
/// 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(uniffi::Record)]
pub struct RoomMembershipDetails {
pub own_room_member: RoomMember,
pub sender_room_member: Option<RoomMember>,
}
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: JoinRule,
/// Whether the room is direct or not, if known.
pub is_direct: Option<bool>,
/// Room heroes.
pub heroes: Option<Vec<RoomHero>>,
}
impl TryFrom<SpaceRoomJoinRule> for JoinRule {
type Error = ();
fn try_from(join_rule: SpaceRoomJoinRule) -> Result<Self, ()> {
Ok(match join_rule {
SpaceRoomJoinRule::Invite => JoinRule::Invite,
SpaceRoomJoinRule::Knock => JoinRule::Knock,
SpaceRoomJoinRule::Private => JoinRule::Private,
SpaceRoomJoinRule::Restricted => JoinRule::Restricted { rules: Vec::new() },
SpaceRoomJoinRule::KnockRestricted => JoinRule::KnockRestricted { rules: Vec::new() },
SpaceRoomJoinRule::Public => JoinRule::Public,
SpaceRoomJoinRule::_Custom(_) => JoinRule::Custom { repr: join_rule.to_string() },
_ => {
warn!("unhandled SpaceRoomJoinRule: {join_rule}");
return Err(());
}
})
}
}
/// 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,
}
}
}
+94 -352
View File
@@ -15,11 +15,12 @@
use std::{collections::BTreeSet, sync::Arc, time::Duration};
use extension_trait::extension_trait;
use matrix_sdk::attachment::{BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo};
use matrix_sdk::attachment::{
BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo,
};
use ruma::{
assign,
events::{
call::notify::NotifyType as RumaNotifyType,
location::AssetType as RumaAssetType,
poll::start::PollKind as RumaPollKind,
room::{
@@ -40,141 +41,47 @@ use ruma::{
VideoInfo as RumaVideoInfo,
VideoMessageEventContent as RumaVideoMessageEventContent,
},
ImageInfo as RumaImageInfo, MediaSource as RumaMediaSource,
ThumbnailInfo as RumaThumbnailInfo,
ImageInfo as RumaImageInfo, MediaSource, ThumbnailInfo as RumaThumbnailInfo,
},
},
matrix_uri::MatrixId as RumaMatrixId,
serde::JsonObject,
MatrixToUri, MatrixUri as RumaMatrixUri, OwnedUserId, UInt, UserId,
OwnedUserId, UInt, UserId,
};
use tracing::info;
use crate::{
error::{ClientError, MediaInfoError},
helpers::unwrap_or_clone_arc,
timeline::MessageContent,
utils::u64_to_uint,
};
#[derive(uniffi::Enum)]
pub enum AuthData {
/// Password-based authentication (`m.login.password`).
Password { password_details: AuthDataPasswordDetails },
#[uniffi::export]
pub fn media_source_from_url(url: String) -> Arc<MediaSource> {
Arc::new(MediaSource::Plain(url.into()))
}
#[derive(uniffi::Record)]
pub struct AuthDataPasswordDetails {
/// One of the user's identifiers.
identifier: String,
/// The plaintext password.
password: String,
}
impl From<AuthData> for ruma::api::client::uiaa::AuthData {
fn from(value: AuthData) -> ruma::api::client::uiaa::AuthData {
match value {
AuthData::Password { password_details } => {
let user_id = ruma::UserId::parse(password_details.identifier).unwrap();
ruma::api::client::uiaa::AuthData::Password(ruma::api::client::uiaa::Password::new(
user_id.into(),
password_details.password,
))
}
}
}
}
/// Parse a matrix entity from a given URI, be it either
/// a `matrix.to` link or a `matrix:` URI
#[matrix_sdk_ffi_macros::export]
pub fn parse_matrix_entity_from(uri: String) -> Option<MatrixEntity> {
if let Ok(matrix_uri) = RumaMatrixUri::parse(&uri) {
return Some(MatrixEntity {
id: matrix_uri.id().into(),
via: matrix_uri.via().iter().map(|via| via.to_string()).collect(),
});
}
if let Ok(matrix_to_uri) = MatrixToUri::parse(&uri) {
return Some(MatrixEntity {
id: matrix_to_uri.id().into(),
via: matrix_to_uri.via().iter().map(|via| via.to_string()).collect(),
});
}
None
}
/// A Matrix entity that can be a room, room alias, user, or event, and a list
/// of via servers.
#[derive(uniffi::Record)]
pub struct MatrixEntity {
id: MatrixId,
via: Vec<String>,
}
/// A Matrix ID that can be a room, room alias, user, or event.
#[derive(Clone, uniffi::Enum)]
pub enum MatrixId {
Room { id: String },
RoomAlias { alias: String },
User { id: String },
EventOnRoomId { room_id: String, event_id: String },
EventOnRoomAlias { alias: String, event_id: String },
}
impl From<&RumaMatrixId> for MatrixId {
fn from(value: &RumaMatrixId) -> Self {
match value {
RumaMatrixId::User(id) => MatrixId::User { id: id.to_string() },
RumaMatrixId::Room(id) => MatrixId::Room { id: id.to_string() },
RumaMatrixId::RoomAlias(id) => MatrixId::RoomAlias { alias: id.to_string() },
RumaMatrixId::Event(room_id_or_alias, event_id) => {
if room_id_or_alias.is_room_id() {
MatrixId::EventOnRoomId {
room_id: room_id_or_alias.to_string(),
event_id: event_id.to_string(),
}
} else if room_id_or_alias.is_room_alias_id() {
MatrixId::EventOnRoomAlias {
alias: room_id_or_alias.to_string(),
event_id: event_id.to_string(),
}
} else {
panic!("Unexpected MatrixId type: {:?}", room_id_or_alias)
}
}
_ => panic!("Unexpected MatrixId type: {:?}", value),
}
}
}
#[matrix_sdk_ffi_macros::export]
#[uniffi::export]
pub fn message_event_content_new(
msgtype: MessageType,
) -> Result<Arc<RoomMessageEventContentWithoutRelation>, ClientError> {
Ok(Arc::new(RoomMessageEventContentWithoutRelation::new(msgtype.try_into()?)))
}
#[matrix_sdk_ffi_macros::export]
#[uniffi::export]
pub fn message_event_content_from_markdown(
md: String,
) -> Arc<RoomMessageEventContentWithoutRelation> {
Arc::new(RoomMessageEventContentWithoutRelation::new(RumaMessageType::text_markdown(md)))
}
#[matrix_sdk_ffi_macros::export]
#[uniffi::export]
pub fn message_event_content_from_markdown_as_emote(
md: String,
) -> Arc<RoomMessageEventContentWithoutRelation> {
Arc::new(RoomMessageEventContentWithoutRelation::new(RumaMessageType::emote_markdown(md)))
}
#[matrix_sdk_ffi_macros::export]
#[uniffi::export]
pub fn message_event_content_from_html(
body: String,
html_body: String,
@@ -184,7 +91,7 @@ pub fn message_event_content_from_html(
)))
}
#[matrix_sdk_ffi_macros::export]
#[uniffi::export]
pub fn message_event_content_from_html_as_emote(
body: String,
html_body: String,
@@ -194,84 +101,21 @@ pub fn message_event_content_from_html_as_emote(
)))
}
#[derive(Clone, uniffi::Object)]
pub struct MediaSource {
pub(crate) media_source: RumaMediaSource,
}
#[matrix_sdk_ffi_macros::export]
impl MediaSource {
#[uniffi::constructor]
pub fn from_url(url: String) -> Result<Arc<MediaSource>, ClientError> {
let media_source = RumaMediaSource::Plain(url.into());
media_source.verify()?;
Ok(Arc::new(MediaSource { media_source }))
}
pub fn url(&self) -> String {
self.media_source.url()
}
// Used on Element X Android
#[uniffi::constructor]
pub fn from_json(json: String) -> Result<Arc<Self>, ClientError> {
let media_source: RumaMediaSource = serde_json::from_str(&json)?;
media_source.verify()?;
Ok(Arc::new(MediaSource { media_source }))
}
// Used on Element X Android
pub fn to_json(&self) -> String {
serde_json::to_string(&self.media_source)
.expect("Media source should always be serializable ")
}
}
impl TryFrom<RumaMediaSource> for MediaSource {
type Error = ClientError;
fn try_from(value: RumaMediaSource) -> Result<Self, Self::Error> {
value.verify()?;
Ok(Self { media_source: value })
}
}
impl TryFrom<&RumaMediaSource> for MediaSource {
type Error = ClientError;
fn try_from(value: &RumaMediaSource) -> Result<Self, Self::Error> {
value.verify()?;
Ok(Self { media_source: value.clone() })
}
}
impl From<MediaSource> for RumaMediaSource {
fn from(value: MediaSource) -> Self {
value.media_source
}
}
#[extension_trait]
pub(crate) impl MediaSourceExt for RumaMediaSource {
fn verify(&self) -> Result<(), ClientError> {
match self {
RumaMediaSource::Plain(url) => {
url.validate().map_err(|e| ClientError::Generic { msg: e.to_string() })?;
}
RumaMediaSource::Encrypted(file) => {
file.url.validate().map_err(|e| ClientError::Generic { msg: e.to_string() })?;
}
}
pub impl MediaSourceExt for MediaSource {
fn from_json(json: String) -> Result<MediaSource, ClientError> {
let res = serde_json::from_str(&json)?;
Ok(res)
}
Ok(())
fn to_json(&self) -> String {
serde_json::to_string(self).expect("Media source should always be serializable ")
}
fn url(&self) -> String {
match self {
RumaMediaSource::Plain(url) => url.to_string(),
RumaMediaSource::Encrypted(file) => file.url.to_string(),
MediaSource::Plain(url) => url.to_string(),
MediaSource::Encrypted(file) => file.url.to_string(),
}
}
}
@@ -285,7 +129,6 @@ pub impl RoomMessageEventContentWithoutRelationExt for RoomMessageEventContentWi
}
}
#[derive(Clone)]
pub struct Mentions {
pub user_ids: Vec<String>,
pub room: bool,
@@ -319,25 +162,8 @@ pub enum MessageType {
Other { msgtype: String, body: String },
}
/// From MSC2530: https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2530-body-as-caption.md
/// If the filename field is present in a media message, clients should treat
/// body as a caption instead of a file name. Otherwise, the body is the
/// file name.
///
/// So:
/// - if a media has a filename and a caption, the body is the caption, filename
/// is its own field.
/// - if a media only has a filename, then body is the filename.
fn get_body_and_filename(filename: String, caption: Option<String>) -> (String, Option<String>) {
if let Some(caption) = caption {
(caption, Some(filename))
} else {
(filename, None)
}
}
impl TryFrom<MessageType> for RumaMessageType {
type Error = ClientError;
type Error = serde_json::Error;
fn try_from(value: MessageType) -> Result<Self, Self::Error> {
Ok(match value {
@@ -346,42 +172,23 @@ impl TryFrom<MessageType> for RumaMessageType {
formatted: content.formatted.map(Into::into),
}))
}
MessageType::Image { content } => {
let (body, filename) = get_body_and_filename(content.filename, content.caption);
let mut event_content =
RumaImageMessageEventContent::new(body, (*content.source).clone().into())
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted_caption.map(Into::into);
event_content.filename = filename;
Self::Image(event_content)
}
MessageType::Audio { content } => {
let (body, filename) = get_body_and_filename(content.filename, content.caption);
let mut event_content =
RumaAudioMessageEventContent::new(body, (*content.source).clone().into())
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted_caption.map(Into::into);
event_content.filename = filename;
Self::Audio(event_content)
}
MessageType::Video { content } => {
let (body, filename) = get_body_and_filename(content.filename, content.caption);
let mut event_content =
RumaVideoMessageEventContent::new(body, (*content.source).clone().into())
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted_caption.map(Into::into);
event_content.filename = filename;
Self::Video(event_content)
}
MessageType::File { content } => {
let (body, filename) = get_body_and_filename(content.filename, content.caption);
let mut event_content =
RumaFileMessageEventContent::new(body, (*content.source).clone().into())
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted_caption.map(Into::into);
event_content.filename = filename;
Self::File(event_content)
}
MessageType::Image { content } => Self::Image(
RumaImageMessageEventContent::new(content.body, (*content.source).clone())
.info(content.info.map(Into::into).map(Box::new)),
),
MessageType::Audio { content } => Self::Audio(
RumaAudioMessageEventContent::new(content.body, (*content.source).clone())
.info(content.info.map(Into::into).map(Box::new)),
),
MessageType::Video { content } => Self::Video(
RumaVideoMessageEventContent::new(content.body, (*content.source).clone())
.info(content.info.map(Into::into).map(Box::new)),
),
MessageType::File { content } => Self::File(
RumaFileMessageEventContent::new(content.body, (*content.source).clone())
.filename(content.filename)
.info(content.info.map(Into::into).map(Box::new)),
),
MessageType::Notice { content } => {
Self::Notice(assign!(RumaNoticeMessageEventContent::plain(content.body), {
formatted: content.formatted.map(Into::into),
@@ -402,11 +209,9 @@ impl TryFrom<MessageType> for RumaMessageType {
}
}
impl TryFrom<RumaMessageType> for MessageType {
type Error = ClientError;
fn try_from(value: RumaMessageType) -> Result<Self, Self::Error> {
Ok(match value {
impl From<RumaMessageType> for MessageType {
fn from(value: RumaMessageType) -> Self {
match value {
RumaMessageType::Emote(c) => MessageType::Emote {
content: EmoteMessageContent {
body: c.body.clone(),
@@ -415,20 +220,15 @@ impl TryFrom<RumaMessageType> for MessageType {
},
RumaMessageType::Image(c) => MessageType::Image {
content: ImageMessageContent {
filename: c.filename().to_owned(),
caption: c.caption().map(ToString::to_string),
formatted_caption: c.formatted_caption().map(Into::into),
source: Arc::new(c.source.try_into()?),
info: c.info.as_deref().map(TryInto::try_into).transpose()?,
body: c.body.clone(),
source: Arc::new(c.source.clone()),
info: c.info.as_deref().map(Into::into),
},
},
RumaMessageType::Audio(c) => MessageType::Audio {
content: AudioMessageContent {
filename: c.filename().to_owned(),
caption: c.caption().map(ToString::to_string),
formatted_caption: c.formatted_caption().map(Into::into),
source: Arc::new(c.source.try_into()?),
body: c.body.clone(),
source: Arc::new(c.source.clone()),
info: c.info.as_deref().map(Into::into),
audio: c.audio.map(Into::into),
voice: c.voice.map(Into::into),
@@ -436,20 +236,17 @@ impl TryFrom<RumaMessageType> for MessageType {
},
RumaMessageType::Video(c) => MessageType::Video {
content: VideoMessageContent {
filename: c.filename().to_owned(),
caption: c.caption().map(ToString::to_string),
formatted_caption: c.formatted_caption().map(Into::into),
source: Arc::new(c.source.try_into()?),
info: c.info.as_deref().map(TryInto::try_into).transpose()?,
body: c.body.clone(),
source: Arc::new(c.source.clone()),
info: c.info.as_deref().map(Into::into),
},
},
RumaMessageType::File(c) => MessageType::File {
content: FileMessageContent {
filename: c.filename().to_owned(),
caption: c.caption().map(ToString::to_string),
formatted_caption: c.formatted_caption().map(Into::into),
source: Arc::new(c.source.try_into()?),
info: c.info.as_deref().map(TryInto::try_into).transpose()?,
body: c.body.clone(),
filename: c.filename.clone(),
source: Arc::new(c.source.clone()),
info: c.info.as_deref().map(Into::into),
},
},
RumaMessageType::Notice(c) => MessageType::Notice {
@@ -485,30 +282,6 @@ impl TryFrom<RumaMessageType> for MessageType {
msgtype: value.msgtype().to_owned(),
body: value.body().to_owned(),
},
})
}
}
#[derive(Clone, uniffi::Enum)]
pub enum NotifyType {
Ring,
Notify,
}
impl From<RumaNotifyType> for NotifyType {
fn from(val: RumaNotifyType) -> Self {
match val {
RumaNotifyType::Ring => Self::Ring,
_ => Self::Notify,
}
}
}
impl From<NotifyType> for RumaNotifyType {
fn from(value: NotifyType) -> Self {
match value {
NotifyType::Ring => RumaNotifyType::Ring,
NotifyType::Notify => RumaNotifyType::Notify,
}
}
}
@@ -521,20 +294,14 @@ pub struct EmoteMessageContent {
#[derive(Clone, uniffi::Record)]
pub struct ImageMessageContent {
/// The computed filename, for use in a client.
pub filename: String,
pub caption: Option<String>,
pub formatted_caption: Option<FormattedBody>,
pub body: String,
pub source: Arc<MediaSource>,
pub info: Option<ImageInfo>,
}
#[derive(Clone, uniffi::Record)]
pub struct AudioMessageContent {
/// The computed filename, for use in a client.
pub filename: String,
pub caption: Option<String>,
pub formatted_caption: Option<FormattedBody>,
pub body: String,
pub source: Arc<MediaSource>,
pub info: Option<AudioInfo>,
pub audio: Option<UnstableAudioDetailsContent>,
@@ -543,20 +310,15 @@ pub struct AudioMessageContent {
#[derive(Clone, uniffi::Record)]
pub struct VideoMessageContent {
/// The computed filename, for use in a client.
pub filename: String,
pub caption: Option<String>,
pub formatted_caption: Option<FormattedBody>,
pub body: String,
pub source: Arc<MediaSource>,
pub info: Option<VideoInfo>,
}
#[derive(Clone, uniffi::Record)]
pub struct FileMessageContent {
/// The computed filename, for use in a client.
pub filename: String,
pub caption: Option<String>,
pub formatted_caption: Option<FormattedBody>,
pub body: String,
pub filename: Option<String>,
pub source: Arc<MediaSource>,
pub info: Option<FileInfo>,
}
@@ -570,7 +332,6 @@ pub struct ImageInfo {
pub thumbnail_info: Option<ThumbnailInfo>,
pub thumbnail_source: Option<Arc<MediaSource>>,
pub blurhash: Option<String>,
pub is_animated: Option<bool>,
}
impl From<ImageInfo> for RumaImageInfo {
@@ -581,9 +342,8 @@ impl From<ImageInfo> for RumaImageInfo {
mimetype: value.mimetype,
size: value.size.map(u64_to_uint),
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()),
blurhash: value.blurhash,
is_animated: value.is_animated,
})
}
}
@@ -605,7 +365,6 @@ impl TryFrom<&ImageInfo> for BaseImageInfo {
width: Some(width),
size: Some(size),
blurhash: Some(blurhash),
is_animated: value.is_animated,
})
}
}
@@ -688,7 +447,7 @@ impl From<VideoInfo> for RumaVideoInfo {
mimetype: value.mimetype,
size: value.size.map(u64_to_uint),
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()),
blurhash: value.blurhash,
})
}
@@ -731,7 +490,7 @@ impl From<FileInfo> for RumaFileInfo {
mimetype: value.mimetype,
size: value.size.map(u64_to_uint),
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()),
})
}
}
@@ -766,6 +525,21 @@ impl From<ThumbnailInfo> for RumaThumbnailInfo {
}
}
impl TryFrom<&ThumbnailInfo> for BaseThumbnailInfo {
type Error = MediaInfoError;
fn try_from(value: &ThumbnailInfo) -> Result<Self, MediaInfoError> {
let height = UInt::try_from(value.height.ok_or(MediaInfoError::MissingField)?)
.map_err(|_| MediaInfoError::InvalidField)?;
let width = UInt::try_from(value.width.ok_or(MediaInfoError::MissingField)?)
.map_err(|_| MediaInfoError::InvalidField)?;
let size = UInt::try_from(value.size.ok_or(MediaInfoError::MissingField)?)
.map_err(|_| MediaInfoError::InvalidField)?;
Ok(BaseThumbnailInfo { height: Some(height), width: Some(width), size: Some(size) })
}
}
#[derive(Clone, uniffi::Record)]
pub struct NoticeMessageContent {
pub body: String,
@@ -838,10 +612,8 @@ pub enum MessageFormat {
Unknown { format: String },
}
impl TryFrom<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
type Error = ClientError;
fn try_from(info: &matrix_sdk::ruma::events::room::ImageInfo) -> Result<Self, Self::Error> {
impl From<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
fn from(info: &matrix_sdk::ruma::events::room::ImageInfo) -> Self {
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
height: info.height.map(Into::into),
width: info.width.map(Into::into),
@@ -849,21 +621,15 @@ impl TryFrom<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
size: info.size.map(Into::into),
});
Ok(Self {
Self {
height: info.height.map(Into::into),
width: info.width.map(Into::into),
mimetype: info.mimetype.clone(),
size: info.size.map(Into::into),
thumbnail_info,
thumbnail_source: info
.thumbnail_source
.as_ref()
.map(TryInto::try_into)
.transpose()?
.map(Arc::new),
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
blurhash: info.blurhash.clone(),
is_animated: info.is_animated,
})
}
}
}
@@ -877,10 +643,8 @@ impl From<&RumaAudioInfo> for AudioInfo {
}
}
impl TryFrom<&RumaVideoInfo> for VideoInfo {
type Error = ClientError;
fn try_from(info: &RumaVideoInfo) -> Result<Self, Self::Error> {
impl From<&RumaVideoInfo> for VideoInfo {
fn from(info: &RumaVideoInfo) -> Self {
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
height: info.height.map(Into::into),
width: info.width.map(Into::into),
@@ -888,28 +652,21 @@ impl TryFrom<&RumaVideoInfo> for VideoInfo {
size: info.size.map(Into::into),
});
Ok(Self {
Self {
duration: info.duration,
height: info.height.map(Into::into),
width: info.width.map(Into::into),
mimetype: info.mimetype.clone(),
size: info.size.map(Into::into),
thumbnail_info,
thumbnail_source: info
.thumbnail_source
.as_ref()
.map(TryInto::try_into)
.transpose()?
.map(Arc::new),
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
blurhash: info.blurhash.clone(),
})
}
}
}
impl TryFrom<&RumaFileInfo> for FileInfo {
type Error = ClientError;
fn try_from(info: &RumaFileInfo) -> Result<Self, Self::Error> {
impl From<&RumaFileInfo> for FileInfo {
fn from(info: &RumaFileInfo) -> Self {
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
height: info.height.map(Into::into),
width: info.width.map(Into::into),
@@ -917,21 +674,16 @@ impl TryFrom<&RumaFileInfo> for FileInfo {
size: info.size.map(Into::into),
});
Ok(Self {
Self {
mimetype: info.mimetype.clone(),
size: info.size.map(Into::into),
thumbnail_info,
thumbnail_source: info
.thumbnail_source
.as_ref()
.map(TryInto::try_into)
.transpose()?
.map(Arc::new),
})
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
}
}
}
#[derive(Clone, uniffi::Enum)]
#[derive(uniffi::Enum)]
pub enum PollKind {
Disclosed,
Undisclosed,
@@ -958,13 +710,3 @@ impl From<RumaPollKind> for PollKind {
}
}
}
/// Creates a [`RoomMessageEventContentWithoutRelation`] given a
/// [`MessageContent`] value.
#[matrix_sdk_ffi_macros::export]
pub fn content_without_relation_from_message(
message: MessageContent,
) -> Result<Arc<RoomMessageEventContentWithoutRelation>, ClientError> {
let msg_type = message.msg_type.try_into()?;
Ok(Arc::new(RoomMessageEventContentWithoutRelation::new(msg_type)))
}
@@ -1,19 +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, AnyToDeviceEvent},
};
use ruma::UserId;
use tracing::{error, info};
use super::RUNTIME;
use crate::{error::ClientError, utils::Timestamp};
use crate::error::ClientError;
#[derive(uniffi::Object)]
pub struct SessionVerificationEmoji {
@@ -21,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()
@@ -38,20 +37,8 @@ pub enum SessionVerificationData {
Decimals { values: Vec<u16> },
}
/// Details about the incoming verification request
#[derive(Debug, uniffi::Record)]
pub struct SessionVerificationRequestDetails {
sender_id: String,
flow_id: String,
device_id: String,
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)]
#[uniffi::export(callback_interface)]
pub trait SessionVerificationControllerDelegate: Sync + Send {
fn did_receive_verification_request(&self, details: SessionVerificationRequestDetails);
fn did_accept_verification_request(&self);
fn did_start_sas_verification(&self);
fn did_receive_verification_data(&self, data: SessionVerificationData);
@@ -71,53 +58,19 @@ pub struct SessionVerificationController {
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())?;
let verification_request = self
.encryption
.get_verification_request(&sender_id, flow_id)
.await
.ok_or(ClientError::new("Unknown session verification request"))?;
*self.verification_request.write().unwrap() = Some(verification_request.clone());
RUNTIME.spawn(Self::listen_to_verification_request_changes(
verification_request,
self.sas_verification.clone(),
self.delegate.clone(),
));
Ok(())
}
/// 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?;
}
Ok(())
}
/// Request verification for the current device
pub async fn request_verification(&self) -> Result<(), ClientError> {
let methods = vec![VerificationMethod::SasV1];
let verification_request = self
@@ -125,41 +78,30 @@ impl SessionVerificationController {
.request_verification_with_methods(methods)
.await
.map_err(anyhow::Error::from)?;
*self.verification_request.write().unwrap() = Some(verification_request.clone());
RUNTIME.spawn(Self::listen_to_verification_request_changes(
verification_request,
self.sas_verification.clone(),
self.delegate.clone(),
));
*self.verification_request.write().unwrap() = Some(verification_request);
Ok(())
}
/// 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::new("Verification request missing."));
};
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();
RUNTIME.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()
}
}
}
}
@@ -167,37 +109,31 @@ 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::new("SAS verification missing"));
};
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::new("SAS verification missing"));
};
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::new("Verification request missing."));
};
Ok(verification_request.cancel().await?)
Ok(())
}
}
@@ -213,88 +149,58 @@ impl SessionVerificationController {
}
pub(crate) async fn process_to_device_message(&self, event: AnyToDeviceEvent) {
if let AnyToDeviceEvent::KeyVerificationRequest(event) = event {
info!("Received verification request: {:}", event.sender);
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(&event.sender, &event.content.transaction_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()
}
if !request.is_self_verification() {
info!("Received non-self verification request. Ignoring.");
return;
}
let VerificationRequestState::Requested { other_device_data, .. } = request.state()
else {
error!("Received key verification event but the request is in the wrong state.");
return;
};
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_receive_verification_request(SessionVerificationRequestDetails {
sender_id: request.other_user_id().into(),
flow_id: request.flow_id().into(),
device_id: other_device_data.device_id().into(),
display_name: other_device_data.display_name().map(str::to_string),
first_seen_timestamp: other_device_data.first_time_seen_ts().into(),
});
}
}
}
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();
RUNTIME.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 {
@@ -340,10 +246,7 @@ impl SessionVerificationController {
}
break;
}
SasState::Created { .. }
| SasState::Started { .. }
| SasState::Accepted { .. }
| SasState::Confirmed => (),
SasState::Started { .. } | SasState::Accepted { .. } | SasState::Confirmed => (),
}
}
}
+17 -152
View File
@@ -12,20 +12,14 @@
// See the License for that specific language governing permissions and
// limitations under the License.
use std::{fmt::Debug, sync::Arc, time::Duration};
use std::{fmt::Debug, sync::Arc};
use futures_util::pin_mut;
use matrix_sdk::{crypto::types::events::UtdCause, Client};
use matrix_sdk_ui::{
sync_service::{
State as MatrixSyncServiceState, SyncService as MatrixSyncService,
SyncServiceBuilder as MatrixSyncServiceBuilder,
},
unable_to_decrypt_hook::{
UnableToDecryptHook, UnableToDecryptInfo as SdkUnableToDecryptInfo, UtdHookManager,
},
use matrix_sdk::Client;
use matrix_sdk_ui::sync_service::{
State as MatrixSyncServiceState, SyncService as MatrixSyncService,
SyncServiceBuilder as MatrixSyncServiceBuilder,
};
use tracing::error;
use crate::{
error::ClientError, helpers::unwrap_or_clone_arc, room_list::RoomListService, TaskHandle,
@@ -38,7 +32,6 @@ pub enum SyncServiceState {
Running,
Terminated,
Error,
Offline,
}
impl From<MatrixSyncServiceState> for SyncServiceState {
@@ -48,12 +41,11 @@ 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)]
#[uniffi::export(callback_interface)]
pub trait SyncServiceStateObserver: Send + Sync + Debug {
fn on_update(&self, state: SyncServiceState);
}
@@ -61,24 +53,20 @@ pub trait SyncServiceStateObserver: Send + Sync + Debug {
#[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> {
@@ -96,148 +84,25 @@ impl SyncService {
#[derive(Clone, uniffi::Object)]
pub struct SyncServiceBuilder {
client: Client,
builder: MatrixSyncServiceBuilder,
utd_hook: Option<Arc<UtdHookManager>>,
}
impl SyncServiceBuilder {
pub(crate) fn new(client: Client) -> Arc<Self> {
Arc::new(Self {
client: client.clone(),
builder: MatrixSyncService::builder(client),
utd_hook: None,
})
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 { client: this.client, builder, utd_hook: this.utd_hook })
}
/// 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 { client: this.client, builder, utd_hook: this.utd_hook })
}
pub async fn with_utd_hook(
self: Arc<Self>,
delegate: Box<dyn UnableToDecryptDelegate>,
) -> Arc<Self> {
// UTDs detected before this duration may be reclassified as "late decryption"
// events (or discarded, if they get decrypted fast enough).
const UTD_HOOK_GRACE_PERIOD: Duration = Duration::from_secs(60);
let this = unwrap_or_clone_arc(self);
let mut utd_hook = UtdHookManager::new(Arc::new(UtdHook { delegate }), this.client.clone())
.with_max_delay(UTD_HOOK_GRACE_PERIOD);
if let Err(e) = utd_hook.reload_from_store().await {
error!("Unable to reload UTD hook data from data store: {}", e);
// Carry on with the setup anyway; we shouldn't fail setup just
// because the UTD hook failed to load its data.
}
Arc::new(Self {
client: this.client,
builder: this.builder,
utd_hook: Some(Arc::new(utd_hook)),
})
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,
}))
}
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait UnableToDecryptDelegate: Sync + Send {
fn on_utd(&self, info: UnableToDecryptInfo);
}
struct UtdHook {
delegate: Box<dyn UnableToDecryptDelegate>,
}
impl std::fmt::Debug for UtdHook {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UtdHook").finish_non_exhaustive()
}
}
impl UnableToDecryptHook for UtdHook {
fn on_utd(&self, info: SdkUnableToDecryptInfo) {
const IGNORE_UTD_PERIOD: Duration = Duration::from_secs(4);
// UTDs that have been decrypted in the `IGNORE_UTD_PERIOD` are just ignored and
// not considered UTDs.
if let Some(duration) = &info.time_to_decrypt {
if *duration < IGNORE_UTD_PERIOD {
return;
}
}
// Report the UTD to the client.
self.delegate.on_utd(info.into());
}
}
#[derive(uniffi::Record)]
pub struct UnableToDecryptInfo {
/// The identifier of the event that couldn't get decrypted.
event_id: String,
/// If the event could be decrypted late (that is, the event was encrypted
/// at first, but could be decrypted later on), then this indicates the
/// time it took to decrypt the event. If it is not set, this is
/// considered a definite UTD.
///
/// If set, this is in milliseconds.
pub time_to_decrypt_ms: Option<u64>,
/// What we know about what caused this UTD. E.g. was this event sent when
/// we were not a member of this room?
pub cause: UtdCause,
/// The difference between the event creation time (`origin_server_ts`) and
/// the time our device was created. If negative, this event was sent
/// *before* our device was created.
pub event_local_age_millis: i64,
/// Whether the user had verified their own identity at the point they
/// received the UTD event.
pub user_trusts_own_identity: bool,
/// The homeserver of the user that sent the undecryptable event.
pub sender_homeserver: String,
/// Our local user's own homeserver, or `None` if the client is not logged
/// in.
pub own_homeserver: Option<String>,
}
impl From<SdkUnableToDecryptInfo> for UnableToDecryptInfo {
fn from(value: SdkUnableToDecryptInfo) -> Self {
Self {
event_id: value.event_id.to_string(),
time_to_decrypt_ms: value.time_to_decrypt.map(|ttd| ttd.as_millis() as u64),
cause: value.cause,
event_local_age_millis: value.event_local_age_millis,
user_trusts_own_identity: value.user_trusts_own_identity,
sender_homeserver: value.sender_homeserver.to_string(),
own_homeserver: value.own_homeserver.map(String::from),
}
Ok(Arc::new(SyncService { inner: Arc::new(this.builder.build().await?) }))
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ impl TaskHandle {
}
}
#[matrix_sdk_ffi_macros::export]
#[uniffi::export]
impl TaskHandle {
// Cancel a task handle.
pub fn cancel(&self) {
@@ -1,85 +0,0 @@
use ruma::EventId;
use super::FocusEventError;
use crate::{error::ClientError, event::RoomMessageEventMessageType};
#[derive(uniffi::Enum)]
pub enum TimelineFocus {
Live,
Event { event_id: String, num_context_events: u16 },
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 => Ok(Self::Live),
TimelineFocus::Event { event_id, num_context_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 })
}
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 AllowedMessageTypes {
All,
Only { types: Vec<RoomMessageEventMessageType> },
}
/// Various options used to configure the timeline's behavior.
///
/// # Arguments
///
/// * `internal_id_prefix` -
///
/// * `allowed_message_types` -
///
/// * `date_divider_mode` -
#[derive(uniffi::Record)]
pub struct TimelineConfiguration {
/// What should the timeline focus on?
pub focus: TimelineFocus,
/// A list of [`RoomMessageEventMessageType`] that will be allowed to appear
/// in the timeline
pub allowed_message_types: AllowedMessageTypes,
/// 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,
}
+78 -168
View File
@@ -14,84 +14,39 @@
use std::{collections::HashMap, sync::Arc};
use matrix_sdk::{crypto::types::events::UtdCause, room::power_levels::power_level_user_changes};
use matrix_sdk_ui::timeline::{PollResult, RoomPinnedEventsChange, TimelineDetails};
use ruma::events::{room::MediaSource as RumaMediaSource, EventContent, FullStateEventContent};
use matrix_sdk_ui::timeline::{PollResult, TimelineDetails};
use tracing::warn;
use super::ProfileDetails;
use crate::{
error::ClientError,
ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind},
utils::Timestamp,
};
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::Message(message) => {
let msgtype = message.msgtype().msgtype().to_owned();
match TryInto::<MessageContent>::try_into(message) {
Ok(message) => TimelineItemContent::Message { content: message },
Err(error) => TimelineItemContent::FailedToParseMessageLike {
event_type: msgtype,
error: error.to_string(),
},
}
}
Content::RedactedMessage => TimelineItemContent::RedactedMessage,
match &self.0 {
Content::Message(_) => TimelineItemContentKind::Message,
Content::RedactedMessage => TimelineItemContentKind::RedactedMessage,
Content::Sticker(sticker) => {
let content = sticker.content();
let media_source = RumaMediaSource::from(content.source.clone());
if let Err(error) = media_source.verify() {
return TimelineItemContent::FailedToParseMessageLike {
event_type: sticker.content().event_type().to_string(),
error: error.to_string(),
};
}
match TryInto::<ImageInfo>::try_into(&content.info) {
Ok(info) => TimelineItemContent::Sticker {
body: content.body.clone(),
info,
source: Arc::new(MediaSource { media_source }),
},
Err(error) => TimelineItemContent::FailedToParseMessageLike {
event_type: sticker.content().event_type().to_string(),
error: error.to_string(),
},
TimelineItemContentKind::Sticker {
body: content.body.clone(),
info: (&content.info).into(),
url: content.url.to_string(),
}
}
Content::Poll(poll_state) => TimelineItemContent::from(poll_state.results()),
Content::CallInvite => TimelineItemContent::CallInvite,
Content::CallNotify => TimelineItemContent::CallNotify,
Content::Poll(poll_state) => TimelineItemContentKind::from(poll_state.results()),
Content::UnableToDecrypt(msg) => {
TimelineItemContent::UnableToDecrypt { msg: EncryptedMessage::new(&msg) }
TimelineItemContentKind::UnableToDecrypt { msg: EncryptedMessage::new(msg) }
}
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,
}
}
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()
@@ -106,81 +61,47 @@ 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(),
}
}
}
}
}
#[derive(Clone, uniffi::Record)]
pub struct MessageContent {
pub msg_type: MessageType,
pub body: String,
pub in_reply_to: Option<Arc<InReplyToDetails>>,
pub thread_root: Option<String>,
pub is_edited: bool,
pub mentions: Option<Mentions>,
}
impl TryFrom<matrix_sdk_ui::timeline::Message> for MessageContent {
type Error = ClientError;
fn try_from(value: matrix_sdk_ui::timeline::Message) -> Result<Self, Self::Error> {
Ok(Self {
msg_type: value.msgtype().clone().try_into()?,
body: value.body().to_owned(),
in_reply_to: value.in_reply_to().map(|r| Arc::new(r.clone().into())),
is_edited: value.is_edited(),
thread_root: value.thread_root().map(|id| id.to_string()),
mentions: value.mentions().cloned().map(|m| m.into()),
})
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)))
}
}
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 TimelineItemContent {
Message {
content: MessageContent,
},
#[derive(uniffi::Enum)]
pub enum TimelineItemContentKind {
Message,
RedactedMessage,
Sticker {
body: String,
info: ImageInfo,
source: Arc<MediaSource>,
url: String,
},
Poll {
question: String,
@@ -188,19 +109,15 @@ pub enum TimelineItemContent {
max_selections: u64,
answers: Vec<PollAnswer>,
votes: HashMap<String, Vec<String>>,
end_time: Option<Timestamp>,
end_time: Option<u64>,
has_been_edited: bool,
},
CallInvite,
CallNotify,
UnableToDecrypt {
msg: EncryptedMessage,
},
RoomMembership {
user_id: String,
user_display_name: Option<String>,
change: Option<MembershipChange>,
reason: Option<String>,
},
ProfileChange {
display_name: Option<String>,
@@ -224,36 +141,45 @@ 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 InReplyToDetails {
pub(crate) fn new(event_id: String, event: RepliedToEventDetails) -> 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) -> RepliedToEventDetails {
self.event.clone()
}
}
impl From<matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
fn from(inner: matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
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: event.content().clone().into(),
content: Arc::new(TimelineItemContent(event.content().to_owned())),
sender: event.sender().to_string(),
sender_profile: event.sender_profile().into(),
},
@@ -266,11 +192,11 @@ impl From<matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
}
}
#[derive(Clone, uniffi::Enum)]
#[derive(uniffi::Enum)]
pub enum RepliedToEventDetails {
Unavailable,
Pending,
Ready { content: TimelineItemContent, sender: String, sender_profile: ProfileDetails },
Ready { content: Arc<TimelineItemContent>, sender: String, sender_profile: ProfileDetails },
Error { message: String },
}
@@ -285,10 +211,6 @@ pub enum EncryptedMessage {
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,
}
@@ -302,9 +224,9 @@ impl EncryptedMessage {
let sender_key = sender_key.clone();
Self::OlmV1Curve25519AesSha2 { sender_key }
}
Message::MegolmV1AesSha2 { session_id, cause, .. } => {
Message::MegolmV1AesSha2 { session_id, .. } => {
let session_id = session_id.clone();
Self::MegolmV1AesSha2 { session_id, cause: *cause }
Self::MegolmV1AesSha2 { session_id }
}
Message::Unknown => Self::Unknown,
}
@@ -314,13 +236,14 @@ impl EncryptedMessage {
#[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)]
@@ -383,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,
@@ -426,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 {
@@ -464,15 +374,15 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
}
}
#[derive(Clone, uniffi::Record)]
#[derive(uniffi::Record)]
pub struct PollAnswer {
pub id: String,
pub text: String,
}
impl From<PollResult> for TimelineItemContent {
impl From<PollResult> for TimelineItemContentKind {
fn from(value: PollResult) -> Self {
TimelineItemContent::Poll {
TimelineItemContentKind::Poll {
question: value.question,
kind: PollKind::from(value.kind),
max_selections: value.max_selections,
@@ -482,7 +392,7 @@ impl From<PollResult> for TimelineItemContent {
.map(|i| PollAnswer { id: i.id, text: i.text })
.collect(),
votes: value.votes,
end_time: value.end_time.map(|t| t.into()),
end_time: value.end_time,
has_been_edited: value.has_been_edited,
}
}
File diff suppressed because it is too large Load Diff
@@ -1,55 +0,0 @@
use std::sync::Arc;
use matrix_sdk_ui::timeline::event_type_filter::TimelineEventTypeFilter as InnerTimelineEventTypeFilter;
use ruma::events::{AnySyncTimelineEvent, TimelineEventType};
use crate::event::{MessageLikeEventType, 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()
}
}
}
}
+6 -16
View File
@@ -19,7 +19,7 @@ use tracing_core::{identify_callsite, metadata::Kind as MetadataKind};
/// level + target) it is called with. Please make sure that the number of
/// different combinations of those parameters this can be called with is
/// constant in the final executable.
#[matrix_sdk_ffi_macros::export]
#[uniffi::export]
fn log_event(file: String, line: Option<u32>, level: LogLevel, target: String, message: String) {
static CALLSITES: Mutex<BTreeMap<MetadataId, &'static DefaultCallsite>> =
Mutex::new(BTreeMap::new());
@@ -96,7 +96,7 @@ fn span_or_event_enabled(callsite: &'static DefaultCallsite) -> bool {
#[derive(uniffi::Object)]
pub struct Span(tracing::Span);
#[matrix_sdk_ffi_macros::export]
#[uniffi::export]
impl Span {
/// Create a span originating at the given callsite (file, line and column).
///
@@ -107,10 +107,10 @@ impl Span {
/// target passed for second and following creation of a span with the same
/// callsite will be ignored.
///
/// This function leaks a little bit of memory for each unique (file +
/// line + level + target + name) it is called with. Please make sure that
/// the number of different combinations of those parameters this can be
/// called with is constant in the final executable.
/// This function leaks a little bit of memory for each unique (file + line
/// + level + target + name) it is called with. Please make sure that the
/// number of different combinations of those parameters this can be called
/// with is constant in the final executable.
///
/// For a span to have an effect, you must `.enter()` it at the start of a
/// logical unit of work and `.exit()` it at the end of the same (including
@@ -185,16 +185,6 @@ impl LogLevel {
LogLevel::Trace => tracing::Level::TRACE,
}
}
pub(crate) fn as_str(&self) -> &'static str {
match self {
LogLevel::Error => "error",
LogLevel::Warn => "warn",
LogLevel::Info => "info",
LogLevel::Debug => "debug",
LogLevel::Trace => "trace",
}
}
}
#[derive(PartialEq, Eq, PartialOrd, Ord)]
+1 -57
View File
@@ -12,68 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{mem::ManuallyDrop, ops::Deref};
use async_compat::TOKIO1 as RUNTIME;
use ruma::{MilliSecondsSinceUnixEpoch, UInt};
use ruma::UInt;
use tracing::warn;
#[derive(Debug, Clone)]
pub struct Timestamp(u64);
impl From<MilliSecondsSinceUnixEpoch> for Timestamp {
fn from(date: MilliSecondsSinceUnixEpoch) -> Self {
Self(date.0.into())
}
}
uniffi::custom_newtype!(Timestamp, u64);
pub(crate) fn u64_to_uint(u: u64) -> UInt {
UInt::new(u).unwrap_or_else(|| {
warn!("u64 -> UInt conversion overflowed, falling back to UInt::MAX");
UInt::MAX
})
}
/// Tiny wrappers for data types that must be dropped in the context of an async
/// runtime.
///
/// This is useful whenever such a data type may transitively call some
/// runtime's `block_on` function in their `Drop` impl (since we lack async drop
/// at the moment), like done in some `deadpool` drop impls.
pub(crate) struct AsyncRuntimeDropped<T>(ManuallyDrop<T>);
impl<T> AsyncRuntimeDropped<T> {
/// Create a new wrapper for this type that will be dropped under an async
/// runtime.
pub fn new(val: T) -> Self {
Self(ManuallyDrop::new(val))
}
}
impl<T> Drop for AsyncRuntimeDropped<T> {
fn drop(&mut self) {
let _guard = RUNTIME.enter();
// SAFETY: self.inner is never used again, which is the only requirement
// for ManuallyDrop::drop to be used safely.
unsafe {
ManuallyDrop::drop(&mut self.0);
}
}
}
// What is an `AsyncRuntimeDropped<T>`, if not a `T` in disguise?
impl<T> Deref for AsyncRuntimeDropped<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T: Clone> Clone for AsyncRuntimeDropped<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
+23 -138
View File
@@ -5,7 +5,6 @@ use matrix_sdk::{
async_trait,
widget::{MessageLikeEventFilter, StateEventFilter},
};
use ruma::events::MessageLikeEventType;
use tracing::error;
use crate::{room::Room, RUNTIME};
@@ -16,7 +15,7 @@ pub struct WidgetDriverAndHandle {
pub handle: Arc<WidgetDriverHandle>,
}
#[matrix_sdk_ffi_macros::export]
#[uniffi::export]
pub fn make_widget_driver(settings: WidgetSettings) -> Result<WidgetDriverAndHandle, ParseError> {
let (driver, handle) = matrix_sdk::widget::WidgetDriver::new(settings.try_into()?);
Ok(WidgetDriverAndHandle {
@@ -30,7 +29,7 @@ pub fn make_widget_driver(settings: WidgetSettings) -> Result<WidgetDriverAndHan
#[derive(uniffi::Object)]
pub struct WidgetDriver(Mutex<Option<matrix_sdk::widget::WidgetDriver>>);
#[matrix_sdk_ffi_macros::export]
#[uniffi::export(async_runtime = "tokio")]
impl WidgetDriver {
pub async fn run(
&self,
@@ -97,7 +96,7 @@ impl From<matrix_sdk::widget::WidgetSettings> for WidgetSettings {
/// * `room` - A matrix room which is used to query the logged in username
/// * `props` - Properties from the client that can be used by a widget to adapt
/// to the client. e.g. language, font-scale...
#[matrix_sdk_ffi_macros::export]
#[uniffi::export(async_runtime = "tokio")]
pub async fn generate_webview_url(
widget_settings: WidgetSettings,
room: Arc<Room>,
@@ -237,12 +236,10 @@ impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElemen
/// This function returns a `WidgetSettings` object which can be used
/// to setup a widget using `run_client_widget_api`
/// and to generate the correct url for the widget.
///
/// # Arguments
///
/// * `props` - A struct containing the configuration parameters for a element
/// # Arguments
/// * - `props` A struct containing the configuration parameters for a element
/// call widget.
#[matrix_sdk_ffi_macros::export]
#[uniffi::export]
pub fn new_virtual_element_call_widget(
props: VirtualElementCallWidgetOptions,
) -> Result<WidgetSettings, ParseError> {
@@ -262,84 +259,31 @@ pub fn new_virtual_element_call_widget(
/// Editing and extending the capabilities from this function is also possible,
/// but should only be done as temporal workarounds until this function is
/// adjusted
#[matrix_sdk_ffi_macros::export]
pub fn get_element_call_required_permissions(
own_user_id: String,
own_device_id: String,
) -> WidgetCapabilities {
#[uniffi::export]
pub fn get_element_call_required_permissions() -> WidgetCapabilities {
use ruma::events::StateEventType;
let read_send = vec![
// To read and send rageshake requests from other room members
WidgetEventFilter::MessageLikeWithType {
event_type: "org.matrix.rageshake_request".to_owned(),
},
// To read and send encryption keys
// TODO change this to the appropriate to-device version once ready
WidgetEventFilter::MessageLikeWithType {
event_type: "io.element.call.encryption_keys".to_owned(),
},
// To read and send custom EC reactions. They are different to normal `m.reaction`
// because they can be send multiple times to the same event.
WidgetEventFilter::MessageLikeWithType {
event_type: "io.element.call.reaction".to_owned(),
},
// This allows send raise hand reactions.
WidgetEventFilter::MessageLikeWithType {
event_type: MessageLikeEventType::Reaction.to_string(),
},
// This allows to detect if someone does not raise their hand anymore.
WidgetEventFilter::MessageLikeWithType {
event_type: MessageLikeEventType::RoomRedaction.to_string(),
},
];
WidgetCapabilities {
read: vec![
// To compute the current state of the matrixRTC session.
WidgetEventFilter::StateWithType { event_type: StateEventType::CallMember.to_string() },
// To detect leaving/kicked room members during a call.
WidgetEventFilter::StateWithType { event_type: StateEventType::RoomMember.to_string() },
// To decide whether to encrypt the call streams based on the room encryption setting.
WidgetEventFilter::StateWithType {
event_type: StateEventType::RoomEncryption.to_string(),
WidgetEventFilter::MessageLikeWithType {
event_type: "org.matrix.rageshake_request".to_owned(),
},
// This allows the widget to check the room version, so it can know about
// version-specific auth rules (namely MSC3779).
WidgetEventFilter::StateWithType { event_type: StateEventType::RoomCreate.to_string() },
]
.into_iter()
.chain(read_send.clone())
.collect(),
WidgetEventFilter::MessageLikeWithType {
event_type: "io.element.call.encryption_keys".to_owned(),
},
],
send: vec![
// To send the call participation state event (main MatrixRTC event).
// This is required for legacy state events (using only one event for all devices with
// a membership array). TODO: remove once legacy call member events are
// sunset.
WidgetEventFilter::StateWithTypeAndStateKey {
event_type: StateEventType::CallMember.to_string(),
state_key: own_user_id.clone(),
WidgetEventFilter::StateWithType { event_type: StateEventType::CallMember.to_string() },
WidgetEventFilter::StateWithType {
event_type: "org.matrix.rageshake_request".to_owned(),
},
// `delayed_event`` version for session memberhips
// [MSC3779](https://github.com/matrix-org/matrix-spec-proposals/pull/3779), with no leading underscore.
WidgetEventFilter::StateWithTypeAndStateKey {
event_type: StateEventType::CallMember.to_string(),
state_key: format!("{own_user_id}_{own_device_id}"),
WidgetEventFilter::StateWithType {
event_type: "io.element.call.encryption_keys".to_owned(),
},
// The same as above but with an underscore.
// To work around the issue that state events starting with `@` have to be matrix id's
// but we use mxId+deviceId.
WidgetEventFilter::StateWithTypeAndStateKey {
event_type: StateEventType::CallMember.to_string(),
state_key: format!("_{own_user_id}_{own_device_id}"),
},
]
.into_iter()
.chain(read_send)
.collect(),
],
requires_client: true,
update_delayed_event: true,
send_delayed_event: true,
}
}
@@ -369,7 +313,7 @@ impl From<ClientProperties> for matrix_sdk::widget::ClientProperties {
#[derive(uniffi::Object)]
pub struct WidgetDriverHandle(matrix_sdk::widget::WidgetDriverHandle);
#[matrix_sdk_ffi_macros::export]
#[uniffi::export(async_runtime = "tokio")]
impl WidgetDriverHandle {
/// Receive a message from the widget driver.
///
@@ -401,10 +345,6 @@ pub struct WidgetCapabilities {
/// This means clients should not offer to open the widget in a separate
/// browser/tab/webview that is not connected to the postmessage widget-api.
pub requires_client: bool,
/// This allows the widget to ask the client to update delayed events.
pub update_delayed_event: bool,
/// This allows the widget to send events with a delay.
pub send_delayed_event: bool,
}
impl From<WidgetCapabilities> for matrix_sdk::widget::Capabilities {
@@ -413,8 +353,6 @@ impl From<WidgetCapabilities> for matrix_sdk::widget::Capabilities {
read: value.read.into_iter().map(Into::into).collect(),
send: value.send.into_iter().map(Into::into).collect(),
requires_client: value.requires_client,
update_delayed_event: value.update_delayed_event,
send_delayed_event: value.send_delayed_event,
}
}
}
@@ -425,14 +363,12 @@ impl From<matrix_sdk::widget::Capabilities> for WidgetCapabilities {
read: value.read.into_iter().map(Into::into).collect(),
send: value.send.into_iter().map(Into::into).collect(),
requires_client: value.requires_client,
update_delayed_event: value.update_delayed_event,
send_delayed_event: value.send_delayed_event,
}
}
}
/// Different kinds of filters that could be applied to the timeline events.
#[derive(uniffi::Enum, Clone)]
#[derive(uniffi::Enum)]
pub enum WidgetEventFilter {
/// Matches message-like events with the given `type`.
MessageLikeWithType { event_type: String },
@@ -484,7 +420,7 @@ impl From<matrix_sdk::widget::EventFilter> for WidgetEventFilter {
}
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
#[uniffi::export(callback_interface)]
pub trait WidgetCapabilitiesProvider: Send + Sync {
fn acquire_capabilities(&self, capabilities: WidgetCapabilities) -> WidgetCapabilities;
}
@@ -555,54 +491,3 @@ impl From<url::ParseError> for ParseError {
}
}
}
#[cfg(test)]
mod tests {
use matrix_sdk::widget::Capabilities;
use super::get_element_call_required_permissions;
#[test]
fn element_call_permissions_are_correct() {
let widget_cap = get_element_call_required_permissions(
"@my_user:my_domain.org".to_owned(),
"ABCDEFGHI".to_owned(),
);
// We test two things:
// Converting the WidgetCapability (ffi struct) to Capabilities (rust sdk
// struct)
let cap = Into::<Capabilities>::into(widget_cap);
// Converting Capabilities (rust sdk struct) to a json list.
let cap_json_repr = serde_json::to_string(&cap).unwrap();
// Converting to a Vec<String> allows to check if the required elements exist
// without breaking the test each time the order of permissions might
// change.
let permission_array: Vec<String> = serde_json::from_str(&cap_json_repr).unwrap();
let cap_assert = |capability: &str| {
assert!(
permission_array.contains(&capability.to_owned()),
"The \"{}\" capability was missing from the element call capability list.",
capability
);
};
cap_assert("io.element.requires_client");
cap_assert("org.matrix.msc4157.update_delayed_event");
cap_assert("org.matrix.msc4157.send.delayed_event");
cap_assert("org.matrix.msc2762.receive.state_event:org.matrix.msc3401.call.member");
cap_assert("org.matrix.msc2762.receive.state_event:m.room.member");
cap_assert("org.matrix.msc2762.receive.state_event:m.room.encryption");
cap_assert("org.matrix.msc2762.receive.event:org.matrix.rageshake_request");
cap_assert("org.matrix.msc2762.receive.event:io.element.call.encryption_keys");
cap_assert("org.matrix.msc2762.receive.state_event:m.room.create");
cap_assert("org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org");
cap_assert("org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org_ABCDEFGHI");
cap_assert("org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#_@my_user:my_domain.org_ABCDEFGHI");
cap_assert("org.matrix.msc2762.send.event:org.matrix.rageshake_request");
cap_assert("org.matrix.msc2762.send.event:io.element.call.encryption_keys");
}
}
+1 -2
View File
@@ -1,4 +1,3 @@
[bindings.kotlin]
package_name = "org.matrix.rustcomponents.sdk"
cdylib_name = "matrix_sdk_ffi"
android_cleaner = true
cdylib_name = "matrix_sdk_ffi"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

@@ -1,4 +1,4 @@
This models the room key forwarding algorithm as a decision tree and provides
This models the room key sharing algorithm as a decision tree and provides
tooling to render it as a PDF or PNG.
# Usage
@@ -1,5 +1,5 @@
digraph {
label="Matrix room key forwarding algorithm"
label="Matrix room key sharing algorithm"
fontname="Fira Sans"
ratio=0.5
@@ -16,10 +16,10 @@ digraph {
/* End states */
allow_verified [label="Share the entire session from the earliest known index.\n\nOk(None)", color=4, fillcolor=3]
allow_limited [label="Share a limited session starting from index i, which is the index we previously shared at.\n\nOk(Some(i))", color=4, fillcolor=3]
refuse_device_key_changed [label="Sender key changed, refuse to forward.\n\nErr(KeyForwardDecision::ChangedSenderKey)", color=6, fillcolor=5]
refuse_not_shared [label="Session was never shared with this device, refuse to forward.\n\nErr(KeyForwardDecision::OutboundSessionNotShared)", color=6, fillcolor=5]
refuse_untrusted_own_device [label="Our own device, but it is untrusted and we haven't previously shared with it. Refuse to forward.\n\nErr(KeyForwardDecision::UntrustedDevice)", color=6, fillcolor=5]
refuse_missing_outbound_session [label="Not our device and haven't previously shared with it. Refuse to forward.\n\nErr(KeyForwardDecision::MissingOutboundSession)", color=6, fillcolor=5]
refuse_device_key_changed [label="Sender key changed, refuse to share.\n\nErr(KeyForwardDecision::ChangedSenderKey)", color=6, fillcolor=5]
refuse_not_shared [label="Session was never shared with this device, refuse to share.\n\nErr(KeyForwardDecision::OutboundSessionNotShared)", color=6, fillcolor=5]
refuse_untrusted_own_device [label="Our own device, but it is untrusted and we haven't previously shared with it. Refuse to share.\n\nErr(KeyForwardDecision::UntrustedDevice)", color=6, fillcolor=5]
refuse_missing_outbound_session [label="Not our device and haven't previously shared with it. Refuse to share.\n\nErr(KeyForwardDecision::MissingOutboundSession)", color=6, fillcolor=5]
/* Checks */
Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

-132
View File
@@ -1,135 +1,3 @@
# Changelog
All notable changes to this project will be documented in this file.
<!-- next-header -->
## [Unreleased] - ReleaseDate
## [0.10.0] - 2025-02-04
### Features
- [**breaking**] `EventCacheStore` allows to control which media content is
allowed in the media cache, and how long it should be kept, with a
`MediaRetentionPolicy`:
- `EventCacheStore::add_media_content()` has an extra argument,
`ignore_policy`, which decides whether a media content should ignore the
`MediaRetentionPolicy`. It should be stored alongside the media content.
- `EventCacheStore` has four new methods: `media_retention_policy()`,
`set_media_retention_policy()`, `set_ignore_media_retention_policy()` and
`clean_up_media_cache()`.
- `EventCacheStore` implementations should delegate media cache methods to the
methods of the same name of `MediaService` to use the `MediaRetentionPolicy`.
They need to implement the `EventCacheStoreMedia` trait that can be tested
with the `event_cache_store_media_integration_tests!` macro.
([#4571](https://github.com/matrix-org/matrix-rust-sdk/pull/4571))
### Refactor
- [**breaking**] Replaced `Room::compute_display_name` with the reintroduced
`Room::display_name()`. The new method computes a display name, or return a
cached value from the previous successful computation. If you need a sync
variant, consider using `Room::cached_display_name()`.
([#4470](https://github.com/matrix-org/matrix-rust-sdk/pull/4470))
- [**breaking**]: The reexported types `SyncTimelineEvent` and `TimelineEvent`
have been fused into a single type `TimelineEvent`, and its field
`push_actions` has been made `Option`al (it is set to `None` when we couldn't
compute the push actions, because we lacked some information).
([#4568](https://github.com/matrix-org/matrix-rust-sdk/pull/4568))
## [0.9.0] - 2024-12-18
### Features
- Introduced support for
[MSC4171](https://github.com/matrix-org/matrix-rust-sdk/pull/4335), enabling
the designation of certain users as service members. These flagged users are
excluded from the room display name calculation.
([#4335](https://github.com/matrix-org/matrix-rust-sdk/pull/4335))
### Bug Fixes
- Fix an off-by-one error in the `ObservableMap` when the `remove()` method is
called. Previously, items following the removed item were not shifted left by
one position, leaving them at incorrect indices.
([#4346](https://github.com/matrix-org/matrix-rust-sdk/pull/4346))
## [0.8.0] - 2024-11-19
### Bug Fixes
- Add more invalid characters for room aliases.
- Use the `DisplayName` struct to protect against homoglyph attacks.
### Features
- Add `BaseClient::room_key_recipient_strategy` field
- `AmbiguityCache` contains the room member's user ID.
- [**breaking**] `Media::get_thumbnail` and `MediaFormat::Thumbnail` allow to
request an animated thumbnail They both take a `MediaThumbnailSettings`
instead of `MediaThumbnailSize`.
- Consider knocked members to be part of the room for display name
disambiguation.
- `Client::cross_process_store_locks_holder_name` is used everywhere:
- `StoreConfig::new()` now takes a
`cross_process_store_locks_holder_name` argument.
- `StoreConfig` no longer implements `Default`.
- `BaseClient::new()` has been removed.
- `BaseClient::clone_with_in_memory_state_store()` now takes a
`cross_process_store_locks_holder_name` argument.
- `BaseClient` no longer implements `Default`.
- `EventCacheStoreLock::new()` no longer takes a `key` argument.
- `BuilderStoreConfig` no longer has
`cross_process_store_locks_holder_name` field for `Sqlite` and
`IndexedDb`.
- Make `ObservableMap::stream` works on `wasm32-unknown-unknown`.
- Allow aborting media uploads.
- Replace the `Notification` type from Ruma in `SyncResponse` and `StateChanges`
by a custom one.
- Introduce a `DisplayName` struct which normalizes and sanitizes
display names.
### Refactor
- [**breaking**] Rename `DisplayName` to `RoomDisplayName`.
- Rename `AmbiguityMap` to `DisplayNameUsers`.
- Move `event_cache_store/` to `event_cache/store/` in `matrix-sdk-base`.
- Move `linked_chunk` from `matrix-sdk` to `matrix-sdk-common`.
- Move `Event` and `Gap` into `matrix_sdk_base::event_cache`.
- The ambiguity maps in `SyncResponse` are moved to `JoinedRoom` and `LeftRoom`.
- `Store::get_rooms` and `Store::get_rooms_filtered` are way faster because they
don't acquire the lock for every room they read.
- `Store::get_rooms`, `Store::get_rooms_filtered` and `Store::get_room` are
renamed `Store::rooms`, `Store::rooms_filtered` and `Store::room`.
- [**breaking**] `Client::get_rooms` and `Client::get_rooms_filtered` are renamed
`Client::rooms` and `Client::rooms_filtered`.
- [**breaking**] `Client::get_stripped_rooms` has finally been removed.
- [**breaking**] The `StateStore` methods to access data in the media cache
where moved to a separate `EventCacheStore` trait.
- [**breaking**] The `instant` module was removed, use the `ruma::time` module instead.
# 0.7.0
- Rename `RoomType` to `RoomState`
+7 -33
View File
@@ -9,7 +9,7 @@ name = "matrix-sdk-base"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.10.0"
version = "0.7.0"
[package.metadata.docs.rs]
all-features = true
@@ -21,18 +21,8 @@ e2e-encryption = ["dep:matrix-sdk-crypto"]
js = ["matrix-sdk-common/js", "matrix-sdk-crypto?/js", "ruma/js", "matrix-sdk-store-encryption/js"]
qrcode = ["matrix-sdk-crypto?/qrcode"]
automatic-room-key-forwarding = ["matrix-sdk-crypto?/automatic-room-key-forwarding"]
uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"]
# Private feature, see
# https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823 for the gory
# details.
test-send-sync = [
"matrix-sdk-common/test-send-sync",
"matrix-sdk-crypto?/test-send-sync",
]
# "message-ids" feature doesn't do anything and is deprecated.
message-ids = []
message-ids = ["matrix-sdk-crypto?/message-ids"]
experimental-sliding-sync = ["ruma/unstable-msc3575"]
# helpers for testing features build upon this
testing = [
@@ -48,34 +38,22 @@ as_variant = { workspace = true }
assert_matches = { workspace = true, optional = true }
assert_matches2 = { workspace = true, optional = true }
async-trait = { workspace = true }
bitflags = { version = "2.8.0", features = ["serde"] }
decancer = "3.2.8"
eyeball = { workspace = true, features = ["async-lock"] }
bitflags = "2.1.0"
eyeball = { workspace = true }
eyeball-im = { workspace = true }
futures-util = { workspace = true }
growable-bloom-filter = { workspace = true }
http = { workspace = true, optional = true }
matrix-sdk-common = { workspace = true }
matrix-sdk-crypto = { workspace = true, optional = true }
matrix-sdk-store-encryption = { workspace = true }
matrix-sdk-test = { workspace = true, optional = true }
once_cell = { workspace = true }
regex = "1.11.1"
ruma = { workspace = true, features = [
"canonical-json",
"unstable-msc2867",
"unstable-msc3381",
"unstable-msc3575",
"unstable-msc4186",
"rand",
] }
unicode-normalization = { workspace = true }
ruma = { workspace = true, features = ["canonical-json", "unstable-msc3381"] }
serde = { workspace = true, features = ["rc"] }
serde_json = { workspace = true }
tokio = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
uniffi = { workspace = true, optional = true }
[dev-dependencies]
assert_matches = { workspace = true }
@@ -85,13 +63,9 @@ futures-executor = { workspace = true }
http = { workspace = true }
matrix-sdk-test = { workspace = true }
stream_assert = { workspace = true }
similar-asserts = { workspace = true }
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen-test = { workspace = true }
[lints]
workspace = true
wasm-bindgen-test = "0.3.33"
File diff suppressed because it is too large Load Diff

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