Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c99f665766 | |||
| 8a975c8985 |
@@ -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
|
||||
|
||||
-61
@@ -1,61 +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",
|
||||
"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",
|
||||
# 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",
|
||||
]
|
||||
@@ -1,2 +1 @@
|
||||
* @matrix-org/rust
|
||||
/crates/matrix-sdk-crypto @matrix-org/rust @matrix-org/rust-crypto-reviewers
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
# Check for updates to GitHub Actions every week
|
||||
interval: "weekly"
|
||||
@@ -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 }}
|
||||
@@ -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-06-25
|
||||
toolchain: nightly-2023-11-08
|
||||
components: rustfmt
|
||||
|
||||
- name: Run Benchmarks
|
||||
|
||||
@@ -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.4
|
||||
|
||||
- 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-14
|
||||
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=dev
|
||||
|
||||
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-14
|
||||
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
|
||||
|
||||
+46
-44
@@ -26,6 +26,7 @@ jobs:
|
||||
test-matrix-sdk-features:
|
||||
name: 🐧 [m], ${{ matrix.name }}
|
||||
needs: xtask
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
@@ -43,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:
|
||||
@@ -68,7 +64,7 @@ jobs:
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -82,10 +78,11 @@ jobs:
|
||||
name: 🐧 [m]-examples
|
||||
needs: xtask
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -99,7 +96,7 @@ jobs:
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -113,15 +110,11 @@ jobs:
|
||||
name: 🐧 [m]-crypto
|
||||
needs: xtask
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@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
|
||||
@@ -135,7 +128,7 @@ jobs:
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -147,6 +140,7 @@ jobs:
|
||||
|
||||
test-all-crates:
|
||||
name: ${{ matrix.name }}
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
@@ -167,19 +161,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@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:
|
||||
@@ -205,6 +193,7 @@ jobs:
|
||||
test-wasm:
|
||||
name: 🕸️ ${{ matrix.name }}
|
||||
needs: xtask
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -221,8 +210,11 @@ jobs:
|
||||
- name: '[m]-common'
|
||||
cmd: matrix-sdk-common
|
||||
|
||||
- name: '[m]-indexeddb'
|
||||
cmd: indexeddb
|
||||
- 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
|
||||
@@ -235,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
|
||||
@@ -244,7 +236,7 @@ jobs:
|
||||
components: clippy
|
||||
|
||||
- name: Install wasm-pack
|
||||
uses: qmaru/wasm-pack-action@v0.5.0
|
||||
uses: jetli/wasm-pack-action@v0.4.0
|
||||
with:
|
||||
version: v0.10.3
|
||||
|
||||
@@ -263,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 }}"
|
||||
@@ -280,15 +272,16 @@ jobs:
|
||||
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@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-06-25
|
||||
toolchain: nightly-2023-11-08
|
||||
components: rustfmt
|
||||
|
||||
- name: Cargo fmt
|
||||
@@ -298,22 +291,24 @@ jobs:
|
||||
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.27.3
|
||||
uses: crate-ci/typos@v1.17.0
|
||||
|
||||
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
|
||||
@@ -323,7 +318,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-06-25
|
||||
toolchain: nightly-2023-11-08
|
||||
components: clippy
|
||||
|
||||
- name: Load cache
|
||||
@@ -332,7 +327,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 }}"
|
||||
@@ -344,13 +339,14 @@ jobs:
|
||||
|
||||
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
|
||||
@@ -368,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
|
||||
@@ -380,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
|
||||
@@ -400,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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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-06-25
|
||||
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
|
||||
|
||||
- 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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
os-name: 🐧
|
||||
cachekey-id: linux
|
||||
|
||||
- os: macos-14
|
||||
- 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)] }}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
-187
@@ -29,187 +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.
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
We aim to maintain clear and informative changelogs that accurately reflect the
|
||||
changes in our project. This guide will help you write useful changelog entries
|
||||
using git-cliff, which fetches changelog entries from commit messages.
|
||||
|
||||
## 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).
|
||||
|
||||
### Changelog trailer
|
||||
|
||||
In addition to the Conventional Commit format, you can use the `Changelog` git
|
||||
trailer to specify the changelog message explicitly. When that trailer is
|
||||
present, its value will be used as the changelog entry instead of the commit's
|
||||
leading line. The `Breaking-Change` git trailer can be used in a similar manner
|
||||
if the changelog entry should be marked as a breaking change.
|
||||
|
||||
|
||||
#### Example commit message
|
||||
|
||||
```
|
||||
feat: Add a method to encode Ed25519 public keys to Base64
|
||||
|
||||
This patch adds the `Ed25519PublicKey::to_base64()` method, which allows us to
|
||||
stringify Ed25519 and thus present them to users. It's also commonly used when
|
||||
Ed25519 keys need to be inserted into JSON.
|
||||
|
||||
Changelog: Add the `Ed25519PublicKey::to_base64()` method which can be used to
|
||||
stringify the Ed25519 public key.
|
||||
```
|
||||
|
||||
In this commit message, the content specified in the `Changelog` trailer will be
|
||||
used for the changelog entry.
|
||||
|
||||
Be careful to add at least one whitespace after new lines to create a paragraph.
|
||||
|
||||
### Security fixes
|
||||
|
||||
Commits addressing security vulnerabilities must include specific trailers for
|
||||
vulnerability metadata. These commits are required to include at least the
|
||||
`Security-Impact` trailer to indicate that the commit is a security fix.
|
||||
|
||||
Security issues have some additional 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.
|
||||
|
||||
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
|
||||
|
||||
Changelog: Use a constant-time Base64 encoder for secret key material
|
||||
to mitigate side-channel attacks leaking secret key material.
|
||||
```
|
||||
|
||||
## Review process
|
||||
|
||||
To streamline the review process and make it easier for maintainers to review
|
||||
your contributions, follow these basic rules:
|
||||
|
||||
1. Do not force push after a review has started. This helps maintainers track
|
||||
incremental changes without confusion and makes it easier to follow the
|
||||
evolution of the code.
|
||||
|
||||
2. Do not mix moves and refactoring with functional changes. Keep these in
|
||||
separate commits for clarity. This ensures that the purpose of each commit is
|
||||
clear and easy to review.
|
||||
|
||||
3. Each commit must compile. If commits don’t compile, git bisect becomes
|
||||
unusable, which hampers the debugging process and makes it harder to identify
|
||||
the source of issues.
|
||||
|
||||
4. Commits should only introduce test failures if they are proving that a bug
|
||||
exists. New features should never introduce test failures. Test failures
|
||||
should only be used to demonstrate existing bugs, not as part of adding new
|
||||
functionality.
|
||||
|
||||
5. Keep PRs on topic and small. Large PRs are harder to review and more prone to
|
||||
delays. Create small, focused commits that address a single topic. Use a
|
||||
combination of [git add] -p or git checkout -p to split changes into logical
|
||||
units. This makes your work easier to review and reduces the chance of
|
||||
introducing unrelated changes.
|
||||
|
||||
[git add]: https://git-scm.com/docs/git-add#Documentation/git-add.txt---patch
|
||||
[git checkout]: https://git-scm.com/docs/git-checkout#Documentation/git-checkout.txt---patch
|
||||
|
||||
### Addressing review comments using fixup commits
|
||||
|
||||
So you posted a PR and the maintainers aren't quite happy with it. Here are some
|
||||
guidelines to make the maintainers life easier and increase the chances that
|
||||
your PR will be reviewed swiftly.
|
||||
|
||||
1. Use [fixup] commits. When addressing reviewer feedback, you can create fixup
|
||||
commits. These commits mark your changes as corrections of specific previous
|
||||
commits in the PR.
|
||||
|
||||
Example:
|
||||
|
||||
```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
|
||||
@@ -256,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.
|
||||
@@ -266,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:
|
||||
|
||||

|
||||
|
||||
Generated
+1679
-1570
File diff suppressed because it is too large
Load Diff
+23
-80
@@ -6,19 +6,15 @@ 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.76"
|
||||
rust-version = "1.70"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.68"
|
||||
@@ -29,67 +25,45 @@ async-rx = "0.1.3"
|
||||
async-stream = "0.3.3"
|
||||
async-trait = "0.1.60"
|
||||
as_variant = "1.2.0"
|
||||
base64 = "0.22.0"
|
||||
base64 = "0.21.0"
|
||||
byteorder = "1.4.3"
|
||||
eyeball = { version = "0.8.8", features = ["tracing"] }
|
||||
eyeball-im = { version = "0.5.1", features = ["tracing"] }
|
||||
eyeball-im-util = "0.7.0"
|
||||
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 = "0.3.26"
|
||||
growable-bloom-filter = "2.1.0"
|
||||
http = "1.1.0"
|
||||
imbl = "3.0.0"
|
||||
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"
|
||||
pin-project-lite = "0.2.9"
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.12.4", default-features = false }
|
||||
ruma = { version = "0.11.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",
|
||||
] }
|
||||
ruma-common = "0.14.1"
|
||||
serde = "1.0.151"
|
||||
serde_html_form = "0.2.0"
|
||||
serde_json = "1.0.91"
|
||||
sha2 = "0.10.8"
|
||||
similar-asserts = "1.5.0"
|
||||
stream_assert = "0.1.1"
|
||||
thiserror = "1.0.38"
|
||||
tokio = { version = "1.39.1", default-features = false, features = ["sync"] }
|
||||
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"
|
||||
uniffi = { version = "0.28.0" }
|
||||
uniffi_bindgen = { version = "0.28.0" }
|
||||
url = "2.5.0"
|
||||
vodozemac = { version = "0.8.0", features = ["insecure-pk-encryption"] }
|
||||
wiremock = "0.6.0"
|
||||
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.8.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.8.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.8.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.8.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.8.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.8.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.8.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.8.0" }
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.7.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.7.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.7.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.7.0" }
|
||||
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.7.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.7.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.7.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.7.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.7.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.8.0", default-features = false }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.7.0", default-features = false }
|
||||
|
||||
# Default release profile, select with `--release`
|
||||
[profile.release]
|
||||
@@ -121,34 +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)'] }
|
||||
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"
|
||||
|
||||
@@ -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`, prepend the `CHANGELOG.md`
|
||||
file using `git cliff`, 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
|
||||
+2
-12
@@ -13,14 +13,11 @@ 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.13.0", features = ["flamegraph", "criterion"] }
|
||||
@@ -32,10 +29,3 @@ harness = false
|
||||
[[bench]]
|
||||
name = "store_bench"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "room_bench"
|
||||
harness = false
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::{
|
||||
config::SyncSettings,
|
||||
test_utils::{events::EventFactory, logged_in_client_with_server},
|
||||
utils::IntoRawStateEventContent,
|
||||
};
|
||||
use matrix_sdk_base::{
|
||||
store::StoreConfig, BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore,
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteStateStore;
|
||||
use matrix_sdk_test::{EventBuilder, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder};
|
||||
use matrix_sdk_ui::{timeline::TimelineFocus, Timeline};
|
||||
use ruma::{
|
||||
api::client::membership::get_member_events,
|
||||
device_id,
|
||||
events::room::member::{RoomMemberEvent, RoomMemberEventContent},
|
||||
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 ev_builder = EventBuilder::new();
|
||||
let mut member_events: Vec<Raw<RoomMemberEvent>> = Vec::with_capacity(MEMBERS_IN_ROOM);
|
||||
let member_content_json = json!({
|
||||
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"displayname": "Alice Margatroid",
|
||||
"membership": "join",
|
||||
"reason": "Looking for support",
|
||||
});
|
||||
let member_content: Raw<RoomMemberEventContent> =
|
||||
member_content_json.into_raw_state_event_content().cast();
|
||||
for i in 0..MEMBERS_IN_ROOM {
|
||||
let user_id = OwnedUserId::try_from(format!("@user_{}:matrix.org", i)).unwrap();
|
||||
let state_key = user_id.to_string();
|
||||
let event: Raw<RoomMemberEvent> = ev_builder
|
||||
.make_state_event(
|
||||
&user_id,
|
||||
&room_id,
|
||||
&state_key,
|
||||
member_content.deserialize().unwrap(),
|
||||
None,
|
||||
)
|
||||
.cast();
|
||||
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);
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
Pod::Spec.new do |s|
|
||||
|
||||
s.name = "MatrixSDKCrypto"
|
||||
s.version = "0.4.1"
|
||||
s.version = "0.0.1"
|
||||
s.summary = "Uniffi based bindings for the Rust SDK crypto crate."
|
||||
s.homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
s.license = { :type => "Apache License, Version 2.0", :file => "LICENSE" }
|
||||
s.author = { "matrix.org" => "support@matrix.org" }
|
||||
|
||||
s.ios.deployment_target = "13.0"
|
||||
s.osx.deployment_target = "10.15"
|
||||
|
||||
s.ios.deployment_target = "11.0"
|
||||
s.osx.deployment_target = "10.10"
|
||||
|
||||
s.swift_versions = ['5.1', '5.2']
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
Pod::Spec.new do |s|
|
||||
|
||||
s.name = "MatrixSDKCrypto"
|
||||
s.version = "0.4.1" # Version is only incremented manually and locally before pushing to CocoaPods
|
||||
s.version = "0.0.1" # Version is only incremented manually and locally before pushing to CocoaPods
|
||||
s.summary = "Uniffi based bindings for the Rust SDK crypto crate."
|
||||
s.homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
s.license = { :type => "Apache License, Version 2.0", :file => "LICENSE" }
|
||||
s.author = { "matrix.org" => "support@matrix.org" }
|
||||
|
||||
s.ios.deployment_target = "13.0"
|
||||
s.osx.deployment_target = "10.15"
|
||||
s.ios.deployment_target = "11.0"
|
||||
s.osx.deployment_target = "10.10"
|
||||
|
||||
s.swift_versions = ['5.1', '5.2']
|
||||
s.swift_versions = ['5.0']
|
||||
|
||||
s.source = { :http => "https://github.com/matrix-org/matrix-rust-sdk/releases/download/matrix-sdk-crypto-ffi-#{s.version}/MatrixSDKCryptoFFI.zip" }
|
||||
s.vendored_frameworks = "MatrixSDKCryptoFFI.xcframework"
|
||||
|
||||
@@ -2,9 +2,9 @@ import XCTest
|
||||
@testable import MatrixRustSDK
|
||||
|
||||
final class ClientTests: XCTestCase {
|
||||
func testBuildingWithHomeserverURL() async {
|
||||
func testBuildingWithHomeserverURL() {
|
||||
do {
|
||||
_ = try await ClientBuilder()
|
||||
_ = try ClientBuilder()
|
||||
.homeserverUrl(url: "https://localhost:8008")
|
||||
.build()
|
||||
} catch {
|
||||
@@ -12,9 +12,9 @@ final class ClientTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testBuildingWithHomeserverURLAndUserAgent() async {
|
||||
func testBuildingWithHomeserverURLAndUserAgent() {
|
||||
do {
|
||||
_ = try await ClientBuilder()
|
||||
_ = try ClientBuilder()
|
||||
.homeserverUrl(url: "https://localhost:8008")
|
||||
.userAgent(userAgent: "golden-eye/007")
|
||||
.build()
|
||||
@@ -23,14 +23,24 @@ final class ClientTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testBuildingWithInvalidUsername() async {
|
||||
func testBuildingWithUsername() {
|
||||
do {
|
||||
_ = try await ClientBuilder()
|
||||
_ = try ClientBuilder()
|
||||
.username(username: "@test:matrix.org")
|
||||
.build()
|
||||
} catch {
|
||||
XCTFail("The client should build successfully when given a username.")
|
||||
}
|
||||
}
|
||||
|
||||
func testBuildingWithInvalidUsername() {
|
||||
do {
|
||||
_ = try ClientBuilder()
|
||||
.username(username: "@test:invalid")
|
||||
.build()
|
||||
|
||||
XCTFail("The client should not build when given an invalid username.")
|
||||
} catch ClientBuildError.ServerUnreachable(let message) {
|
||||
} catch ClientError.Generic(let message) {
|
||||
XCTAssertTrue(message.contains(".well-known"), "The client should fail to do the well-known lookup.")
|
||||
} catch {
|
||||
XCTFail("Not expecting any other kind of exception")
|
||||
|
||||
@@ -1,22 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eEu
|
||||
|
||||
helpFunction() {
|
||||
echo ""
|
||||
echo "Usage: $0 -only_ios"
|
||||
echo -e "\t-i Option to build only for iOS. Default will build for all targets."
|
||||
exit 1
|
||||
}
|
||||
|
||||
only_ios='false'
|
||||
|
||||
while getopts ':i' 'opt'; do
|
||||
case ${opt} in
|
||||
'i') only_ios='true' ;;
|
||||
?) helpFunction ;;
|
||||
esac
|
||||
done
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Path to the repo root
|
||||
@@ -38,63 +22,52 @@ TARGET_CRATE=matrix-sdk-crypto-ffi
|
||||
# Required by olm-sys crate
|
||||
export IOS_SDK_PATH=`xcrun --show-sdk-path --sdk iphoneos`
|
||||
|
||||
if ${only_ios}; then
|
||||
# iOS
|
||||
echo -e "Building only for iOS"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios"
|
||||
else
|
||||
# iOS
|
||||
echo -e "Building for iOS [1/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios"
|
||||
# iOS
|
||||
echo -e "Building for iOS [1/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios"
|
||||
|
||||
# MacOS
|
||||
echo -e "\nBuilding for macOS (Apple Silicon) [2/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-darwin"
|
||||
echo -e "\nBuilding for macOS (Intel) [3/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "x86_64-apple-darwin"
|
||||
# MacOS
|
||||
echo -e "\nBuilding for macOS (Apple Silicon) [2/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-darwin"
|
||||
echo -e "\nBuilding for macOS (Intel) [3/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "x86_64-apple-darwin"
|
||||
|
||||
# iOS Simulator
|
||||
echo -e "\nBuilding for iOS Simulator (Apple Silicon) [4/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios-sim"
|
||||
echo -e "\nBuilding for iOS Simulator (Intel) [5/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "x86_64-apple-ios"
|
||||
fi
|
||||
# iOS Simulator
|
||||
echo -e "\nBuilding for iOS Simulator (Apple Silicon) [4/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios-sim"
|
||||
echo -e "\nBuilding for iOS Simulator (Intel) [5/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "x86_64-apple-ios"
|
||||
|
||||
echo -e "\nCreating XCFramework"
|
||||
# Lipo together the libraries for the same platform
|
||||
|
||||
if ! ${only_ios}; then
|
||||
echo "Lipo together the libraries for the same platform"
|
||||
# MacOS
|
||||
lipo -create \
|
||||
"${TARGET_DIR}/x86_64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
"${TARGET_DIR}/aarch64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-output "${GENERATED_DIR}/macos/libmatrix_sdk_crypto_ffi.a"
|
||||
# MacOS
|
||||
lipo -create \
|
||||
"${TARGET_DIR}/x86_64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
"${TARGET_DIR}/aarch64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-output "${GENERATED_DIR}/macos/libmatrix_sdk_crypto_ffi.a"
|
||||
|
||||
# iOS Simulator
|
||||
lipo -create \
|
||||
"${TARGET_DIR}/x86_64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
"${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-output "${GENERATED_DIR}/simulator/libmatrix_sdk_crypto_ffi.a"
|
||||
fi
|
||||
# iOS Simulator
|
||||
lipo -create \
|
||||
"${TARGET_DIR}/x86_64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
"${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-output "${GENERATED_DIR}/simulator/libmatrix_sdk_crypto_ffi.a"
|
||||
|
||||
# Generate uniffi files
|
||||
cd ../matrix-sdk-crypto-ffi && cargo run --bin matrix_sdk_crypto_ffi generate \
|
||||
cargo uniffi-bindgen generate \
|
||||
--language swift \
|
||||
--library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
--out-dir ${GENERATED_DIR}
|
||||
--lib-file "${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
--config "${SRC_ROOT}/bindings/${TARGET_CRATE}/uniffi.toml" \
|
||||
--out-dir ${GENERATED_DIR} \
|
||||
"${SRC_ROOT}/bindings/${TARGET_CRATE}/src/olm.udl"
|
||||
|
||||
# Move headers to the right place
|
||||
HEADERS_DIR=${GENERATED_DIR}/headers
|
||||
mkdir -p ${HEADERS_DIR}
|
||||
mv ${GENERATED_DIR}/*.h ${HEADERS_DIR}
|
||||
|
||||
# Rename and merge the modulemap files into a single file to the right place
|
||||
for f in ${GENERATED_DIR}/*.modulemap
|
||||
do
|
||||
cat $f; echo;
|
||||
done > ${HEADERS_DIR}/module.modulemap
|
||||
rm ${GENERATED_DIR}/*.modulemap
|
||||
# Rename and move modulemap to the right place
|
||||
mv ${GENERATED_DIR}/*.modulemap ${HEADERS_DIR}/module.modulemap
|
||||
|
||||
# Move source files to the right place
|
||||
SWIFT_DIR="${GENERATED_DIR}/Sources"
|
||||
@@ -104,21 +77,15 @@ mv ${GENERATED_DIR}/*.swift ${SWIFT_DIR}
|
||||
# Build the xcframework
|
||||
|
||||
if [ -d "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework" ]; then rm -rf "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"; fi
|
||||
if ${only_ios}; then
|
||||
xcodebuild -create-xcframework \
|
||||
-library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-output "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"
|
||||
else
|
||||
xcodebuild -create-xcframework \
|
||||
-library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-library "${GENERATED_DIR}/macos/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-library "${GENERATED_DIR}/simulator/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-output "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"
|
||||
fi
|
||||
|
||||
xcodebuild -create-xcframework \
|
||||
-library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-library "${GENERATED_DIR}/macos/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-library "${GENERATED_DIR}/simulator/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-output "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"
|
||||
|
||||
# Cleanup
|
||||
if [ -d "${GENERATED_DIR}/macos" ]; then rm -rf "${GENERATED_DIR}/macos"; fi
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,9 +5,6 @@ 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 comes from: https://github.com/mozilla/application-services/pull/5442
|
||||
///
|
||||
/// IMPORTANT: if you modify this, make sure to modify
|
||||
/// [../matrix-sdk-ffi/build.rs] too!
|
||||
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");
|
||||
@@ -21,11 +18,11 @@ fn setup_x86_64_android_workaround() {
|
||||
"Unsupported OS. You must use either Linux, MacOS or Windows to build the crate."
|
||||
),
|
||||
};
|
||||
const DEFAULT_CLANG_VERSION: &str = "18";
|
||||
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/lib/clang/{clang_version}/lib/linux/"
|
||||
"toolchains/llvm/prebuilt/{build_os}-x86_64/lib64/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");
|
||||
@@ -34,6 +31,7 @@ fn setup_x86_64_android_workaround() {
|
||||
|
||||
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)]
|
||||
|
||||
@@ -53,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())?;
|
||||
@@ -107,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)?;
|
||||
@@ -133,7 +133,7 @@ impl Drop for DehydratedDevice {
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
impl DehydratedDevice {
|
||||
pub fn keys_for_upload(
|
||||
&self,
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,14 +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},
|
||||
backups::SignatureState,
|
||||
olm::{IdentityKeys, InboundGroupSession, Session},
|
||||
store::{Changes, CryptoStore, PendingChanges, RoomSettings as RustRoomSettings},
|
||||
types::{
|
||||
DeviceKey, DeviceKeys, EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey,
|
||||
},
|
||||
CollectStrategy, EncryptionSettings as RustEncryptionSettings,
|
||||
types::{EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey},
|
||||
EncryptionSettings as RustEncryptionSettings, LocalTrust,
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteCryptoStore;
|
||||
pub use responses::{
|
||||
@@ -49,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;
|
||||
@@ -134,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`.
|
||||
@@ -191,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,
|
||||
@@ -249,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)?;
|
||||
|
||||
@@ -354,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,
|
||||
@@ -425,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,
|
||||
@@ -466,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;
|
||||
@@ -497,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,
|
||||
@@ -530,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,
|
||||
@@ -558,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
|
||||
///
|
||||
@@ -668,9 +636,6 @@ 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 {
|
||||
@@ -680,10 +645,7 @@ impl From<EncryptionSettings> for RustEncryptionSettings {
|
||||
rotation_period: Duration::from_secs(v.rotation_period),
|
||||
rotation_period_msgs: v.rotation_period_msgs,
|
||||
history_visibility: v.history_visibility.into(),
|
||||
sharing_strategy: CollectStrategy::DeviceBasedStrategy {
|
||||
only_allow_trusted_devices: v.only_allow_trusted_devices,
|
||||
error_on_verified_user_problem: v.error_on_verified_user_problem,
|
||||
},
|
||||
only_allow_trusted_devices: v.only_allow_trusted_devices,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -728,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 },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -794,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> {
|
||||
@@ -882,7 +839,6 @@ impl From<RoomSettings> for RustRoomSettings {
|
||||
Self {
|
||||
algorithm: value.algorithm.into(),
|
||||
only_allow_trusted_devices: value.only_allow_trusted_devices,
|
||||
..RustRoomSettings::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -891,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(),
|
||||
@@ -915,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 {
|
||||
@@ -1002,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":[
|
||||
@@ -1110,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();
|
||||
|
||||
@@ -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)) };
|
||||
|
||||
|
||||
@@ -17,8 +17,7 @@ use matrix_sdk_crypto::{
|
||||
decrypt_room_key_export, encrypt_room_key_export,
|
||||
olm::ExportedRoomKey,
|
||||
store::{BackupDecryptionKey, Changes},
|
||||
DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, ToDeviceRequest,
|
||||
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",
|
||||
};
|
||||
@@ -164,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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
uniffi::uniffi_bindgen_main()
|
||||
}
|
||||
@@ -1,24 +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"
|
||||
|
||||
[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
|
||||
@@ -1,14 +0,0 @@
|
||||
[](https://travis-ci.org/matrix-org/matrix-rust-sdk)
|
||||
[](https://codecov.io/gh/matrix-org/matrix-rust-sdk)
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://matrix.to/#/#matrix-rust-sdk:matrix.org)
|
||||
|
||||
# matrix-sdk-ffi-macros
|
||||
|
||||
Internal macros used for the FFI layer (bindings) of the Rust Matrix SDK.
|
||||
|
||||
**NOTE:** These are just macros that help build the matrix-rust-sdk bindings, you're probably
|
||||
interested in the main [rust-sdk](https://github.com/matrix-org/matrix-rust-sdk/) crate.
|
||||
|
||||
[Matrix]: https://matrix.org/
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
@@ -1,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()
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
# unreleased
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- `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`
|
||||
@@ -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]
|
||||
@@ -62,7 +69,6 @@ features = [
|
||||
"rustls-tls", # note: differ from block below
|
||||
"socks",
|
||||
"sqlite",
|
||||
"uniffi",
|
||||
]
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies.matrix-sdk]
|
||||
@@ -77,11 +83,4 @@ features = [
|
||||
"native-tls", # note: differ from block above
|
||||
"socks",
|
||||
"sqlite",
|
||||
"uniffi",
|
||||
]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,9 +5,6 @@ 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 comes from: https://github.com/mozilla/application-services/pull/5442
|
||||
///
|
||||
/// IMPORTANT: if you modify this, make sure to modify
|
||||
/// [../matrix-sdk-crypto-ffi/build.rs] too!
|
||||
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");
|
||||
@@ -21,11 +18,11 @@ fn setup_x86_64_android_workaround() {
|
||||
"Unsupported OS. You must use either Linux, MacOS or Windows to build the crate."
|
||||
),
|
||||
};
|
||||
const DEFAULT_CLANG_VERSION: &str = "18";
|
||||
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/lib/clang/{clang_version}/lib/linux/"
|
||||
"toolchains/llvm/prebuilt/{build_os}-x86_64/lib64/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");
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::{self, Debug},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use matrix_sdk::{
|
||||
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
@@ -1,320 +1,57 @@
|
||||
use std::{fs, num::NonZeroUsize, path::PathBuf, 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},
|
||||
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("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>,
|
||||
}
|
||||
|
||||
#[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(),
|
||||
})
|
||||
Arc::new(Self::default())
|
||||
}
|
||||
|
||||
pub fn cross_process_store_locks_holder_name(
|
||||
pub fn enable_cross_process_refresh_lock(
|
||||
self: Arc<Self>,
|
||||
holder_name: String,
|
||||
process_id: String,
|
||||
session_delegate: Box<dyn ClientSessionDelegate>,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.cross_process_store_locks_holder_name = Some(holder_name);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn enable_oidc_refresh_lock(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.enable_oidc_refresh_lock = true;
|
||||
Arc::new(builder)
|
||||
self.enable_cross_process_refresh_lock_inner(process_id, session_delegate.into())
|
||||
}
|
||||
|
||||
pub fn set_session_delegate(
|
||||
@@ -326,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)
|
||||
}
|
||||
|
||||
@@ -344,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)
|
||||
}
|
||||
|
||||
@@ -374,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)
|
||||
}
|
||||
|
||||
@@ -401,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();
|
||||
|
||||
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(session_paths) = &builder.session_paths {
|
||||
let data_path = PathBuf::from(&session_paths.data_path);
|
||||
let cache_path = PathBuf::from(&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.",
|
||||
);
|
||||
let mut inner_builder = builder.inner;
|
||||
|
||||
if let (Some(base_path), Some(username)) = (builder.base_path, &builder.username) {
|
||||
// Determine store path
|
||||
let data_path = PathBuf::from(base_path).join(sanitize(username));
|
||||
fs::create_dir_all(&data_path)?;
|
||||
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 {
|
||||
@@ -569,149 +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?;
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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,178 +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?)
|
||||
}
|
||||
|
||||
/// 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() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
use std::{collections::HashMap, fmt, fmt::Display};
|
||||
use std::fmt::Display;
|
||||
|
||||
use matrix_sdk::{
|
||||
encryption::CryptoStoreError, event_cache::EventCacheError, oidc::OidcError, 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 uniffi::UnexpectedUniFFICallbackError;
|
||||
|
||||
use crate::room_list::RoomListError;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ClientError {
|
||||
#[error("client error: {msg}")]
|
||||
@@ -18,7 +14,7 @@ pub enum ClientError {
|
||||
}
|
||||
|
||||
impl ClientError {
|
||||
pub(crate) fn new<E: Display>(error: E) -> Self {
|
||||
fn new<E: Display>(error: E) -> Self {
|
||||
Self::Generic { msg: error.to_string() }
|
||||
}
|
||||
}
|
||||
@@ -29,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)
|
||||
@@ -101,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)
|
||||
@@ -131,120 +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)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -316,8 +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;
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
use anyhow::{bail, Context};
|
||||
use matrix_sdk::IdParseError;
|
||||
use matrix_sdk_ui::timeline::TimelineEventItemId;
|
||||
use ruma::{
|
||||
events::{
|
||||
room::{message::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},
|
||||
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()
|
||||
@@ -105,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,
|
||||
@@ -127,7 +117,6 @@ impl TryFrom<AnySyncStateEvent> for StateEventContent {
|
||||
pub enum MessageLikeEventContent {
|
||||
CallAnswer,
|
||||
CallInvite,
|
||||
CallNotify { notify_type: NotifyType },
|
||||
CallHangup,
|
||||
CallCandidates,
|
||||
KeyVerificationReady,
|
||||
@@ -141,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,
|
||||
}
|
||||
|
||||
@@ -152,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,17 +189,7 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
|
||||
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"),
|
||||
};
|
||||
@@ -243,150 +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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,32 +2,44 @@
|
||||
|
||||
#![allow(unused_qualifications, clippy::new_without_default)]
|
||||
|
||||
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 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;
|
||||
@@ -45,7 +57,7 @@ use self::{
|
||||
|
||||
uniffi::include_scaffolding!("api");
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
fn sdk_git_sha() -> String {
|
||||
env!("VERGEN_GIT_SHA").to_owned()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,19 +1,95 @@
|
||||
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::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();
|
||||
@@ -103,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}}}")?;
|
||||
}
|
||||
@@ -128,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)
|
||||
@@ -198,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)
|
||||
@@ -209,41 +233,24 @@ 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(uniffi::Record)]
|
||||
pub struct TracingConfiguration {
|
||||
/// A filter line following the [RUST_LOG format].
|
||||
///
|
||||
/// [RUST_LOG format]: https://rust-lang-nursery.github.io/rust-cookbook/development_tools/debugging/config_log.html
|
||||
filter: String,
|
||||
|
||||
/// Whether to log to stdout, or in the logcat on Android.
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
pub fn setup_tracing(config: TracingConfiguration) {
|
||||
#[cfg(target_os = "android")]
|
||||
log_panics();
|
||||
|
||||
tracing_subscriber::registry()
|
||||
@@ -251,3 +258,37 @@ pub fn setup_tracing(config: TracingConfiguration) {
|
||||
.with(text_layers(config))
|
||||
.init();
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct OtlpTracingConfiguration {
|
||||
client_name: String,
|
||||
user: String,
|
||||
password: String,
|
||||
otlp_endpoint: String,
|
||||
filter: String,
|
||||
/// Controls whether to print to stdout or, equivalent, the system logs on
|
||||
/// Android.
|
||||
write_to_stdout_or_system: bool,
|
||||
write_to_files: Option<TracingFileConfiguration>,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
pub fn setup_otlp_tracing(config: OtlpTracingConfiguration) {
|
||||
#[cfg(target_os = "android")]
|
||||
log_panics();
|
||||
|
||||
let otlp_tracer =
|
||||
create_otlp_tracer(config.user, config.password, config.otlp_endpoint, config.client_name)
|
||||
.expect("Couldn't configure the OpenTelemetry tracer");
|
||||
let otlp_layer = tracing_opentelemetry::layer().with_tracer(otlp_tracer);
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(EnvFilter::new(&config.filter))
|
||||
.with(text_layers(TracingConfiguration {
|
||||
filter: config.filter,
|
||||
write_to_stdout_or_system: config.write_to_stdout_or_system,
|
||||
write_to_files: config.write_to_files,
|
||||
}))
|
||||
.with(otlp_layer)
|
||||
.init();
|
||||
}
|
||||
|
||||
+174
-698
File diff suppressed because it is too large
Load Diff
@@ -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>);
|
||||
}
|
||||
@@ -1,50 +1,36 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk::RoomState;
|
||||
use ruma::OwnedMxcUri;
|
||||
|
||||
use crate::{
|
||||
notification_settings::RoomNotificationMode,
|
||||
room::{Membership, RoomHero},
|
||||
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,
|
||||
@@ -54,58 +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>,
|
||||
}
|
||||
|
||||
impl RoomInfo {
|
||||
pub(crate) async fn new(room: &matrix_sdk::Room) -> matrix_sdk::Result<Self> {
|
||||
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();
|
||||
|
||||
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
|
||||
@@ -113,11 +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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,153 +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> {
|
||||
Ok(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,
|
||||
fn unsubscribe(&self) {
|
||||
self.inner.unsubscribe();
|
||||
}
|
||||
|
||||
async fn latest_event(&self) -> Option<Arc<EventTimelineItem>> {
|
||||
self.inner.latest_event().await.map(EventTimelineItem).map(Arc::new)
|
||||
}
|
||||
|
||||
fn has_unread_notifications(&self) -> bool {
|
||||
self.inner.has_unread_notifications()
|
||||
}
|
||||
|
||||
fn unread_notifications(&self) -> Arc<UnreadNotificationsCount> {
|
||||
Arc::new(self.inner.unread_notifications().into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, uniffi::Enum)]
|
||||
pub enum RoomListEntry {
|
||||
Empty,
|
||||
Invalidated { room_id: String },
|
||||
Filled { room_id: String },
|
||||
}
|
||||
|
||||
impl From<MatrixRoomListEntry> for RoomListEntry {
|
||||
fn from(value: MatrixRoomListEntry) -> Self {
|
||||
(&value).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&MatrixRoomListEntry> for RoomListEntry {
|
||||
fn from(value: &MatrixRoomListEntry) -> Self {
|
||||
match value {
|
||||
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() }
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
// Do the thing.
|
||||
let client = self.inner.client();
|
||||
let (room_or_alias_id, 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)
|
||||
};
|
||||
|
||||
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(),
|
||||
});
|
||||
}
|
||||
|
||||
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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks whether the Room's timeline has been initialized before.
|
||||
fn is_timeline_initialized(&self) -> bool {
|
||||
self.inner.is_timeline_initialized()
|
||||
}
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RequiredState {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// 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() })?;
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomSubscription {
|
||||
pub required_state: Option<Vec<RequiredState>>,
|
||||
pub timeline_limit: Option<u32>,
|
||||
}
|
||||
|
||||
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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,7 +532,7 @@ pub struct UnreadNotificationsCount {
|
||||
notification_count: u32,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
impl UnreadNotificationsCount {
|
||||
fn highlight_count(&self) -> u32 {
|
||||
self.highlight_count
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use matrix_sdk::room::{RoomMember as SdkRoomMember, RoomMemberRole};
|
||||
use ruma::UserId;
|
||||
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,90 +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(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(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(),
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,139 +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, 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,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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())
|
||||
}
|
||||
}
|
||||
|
||||
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: 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>,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ use matrix_sdk::attachment::{
|
||||
use ruma::{
|
||||
assign,
|
||||
events::{
|
||||
call::notify::NotifyType as RumaNotifyType,
|
||||
location::AssetType as RumaAssetType,
|
||||
poll::start::PollKind as RumaPollKind,
|
||||
room::{
|
||||
@@ -45,142 +44,44 @@ use ruma::{
|
||||
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 },
|
||||
}
|
||||
|
||||
#[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 media_source_from_url(url: String) -> Arc<MediaSource> {
|
||||
Arc::new(MediaSource::Plain(url.into()))
|
||||
}
|
||||
|
||||
#[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,
|
||||
@@ -190,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,
|
||||
@@ -228,7 +129,6 @@ pub impl RoomMessageEventContentWithoutRelationExt for RoomMessageEventContentWi
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Mentions {
|
||||
pub user_ids: Vec<String>,
|
||||
pub room: bool,
|
||||
@@ -262,23 +162,6 @@ 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 = serde_json::Error;
|
||||
|
||||
@@ -289,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())
|
||||
.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())
|
||||
.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())
|
||||
.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())
|
||||
.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),
|
||||
@@ -356,18 +220,14 @@ impl From<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),
|
||||
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),
|
||||
body: c.body.clone(),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
audio: c.audio.map(Into::into),
|
||||
@@ -376,18 +236,15 @@ impl From<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),
|
||||
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),
|
||||
body: c.body.clone(),
|
||||
filename: c.filename.clone(),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
},
|
||||
@@ -429,30 +286,6 @@ impl From<RumaMessageType> for MessageType {
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct EmoteMessageContent {
|
||||
pub body: String,
|
||||
@@ -461,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>,
|
||||
@@ -483,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>,
|
||||
}
|
||||
@@ -861,7 +683,7 @@ impl From<&RumaFileInfo> for FileInfo {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum PollKind {
|
||||
Disclosed,
|
||||
Undisclosed,
|
||||
@@ -888,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,16 +1,15 @@
|
||||
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;
|
||||
@@ -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: u64,
|
||||
}
|
||||
|
||||
#[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().get().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 => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -51,7 +45,7 @@ impl From<MatrixSyncServiceState> for SyncServiceState {
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait SyncServiceStateObserver: Send + Sync + Debug {
|
||||
fn on_update(&self, state: SyncServiceState);
|
||||
}
|
||||
@@ -59,16 +53,12 @@ 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) {
|
||||
@@ -94,121 +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 })
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
Ok(Arc::new(SyncService { inner: Arc::new(this.builder.build().await?) }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ impl TaskHandle {
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
impl TaskHandle {
|
||||
// Cancel a task handle.
|
||||
pub fn cancel(&self) {
|
||||
|
||||
@@ -14,54 +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, FullStateEventContent};
|
||||
use matrix_sdk_ui::timeline::{PollResult, TimelineDetails};
|
||||
use tracing::warn;
|
||||
|
||||
use super::ProfileDetails;
|
||||
use crate::ruma::{ImageInfo, Mentions, MessageType, PollKind};
|
||||
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) => TimelineItemContent::Message { content: message.into() },
|
||||
|
||||
Content::RedactedMessage => TimelineItemContent::RedactedMessage,
|
||||
|
||||
match &self.0 {
|
||||
Content::Message(_) => TimelineItemContentKind::Message,
|
||||
Content::RedactedMessage => TimelineItemContentKind::RedactedMessage,
|
||||
Content::Sticker(sticker) => {
|
||||
let content = sticker.content();
|
||||
TimelineItemContent::Sticker {
|
||||
TimelineItemContentKind::Sticker {
|
||||
body: content.body.clone(),
|
||||
info: (&content.info).into(),
|
||||
source: Arc::new(MediaSource::from(content.source.clone())),
|
||||
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()
|
||||
@@ -76,79 +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 From<matrix_sdk_ui::timeline::Message> for MessageContent {
|
||||
fn from(value: matrix_sdk_ui::timeline::Message) -> Self {
|
||||
Self {
|
||||
msg_type: value.msgtype().clone().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,
|
||||
@@ -159,16 +112,12 @@ pub enum TimelineItemContent {
|
||||
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>,
|
||||
@@ -192,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(),
|
||||
},
|
||||
@@ -234,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 },
|
||||
}
|
||||
|
||||
@@ -253,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,
|
||||
}
|
||||
@@ -270,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,
|
||||
}
|
||||
@@ -282,6 +236,7 @@ impl EncryptedMessage {
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct Reaction {
|
||||
pub key: String,
|
||||
pub count: u64,
|
||||
pub senders: Vec<ReactionSenderData>,
|
||||
}
|
||||
|
||||
@@ -351,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,
|
||||
@@ -394,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 {
|
||||
@@ -432,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,
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -12,9 +12,6 @@
|
||||
// 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::UInt;
|
||||
use tracing::warn;
|
||||
|
||||
@@ -24,45 +21,3 @@ pub(crate) fn u64_to_uint(u: u64) -> UInt {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, 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,4 +1,3 @@
|
||||
[bindings.kotlin]
|
||||
package_name = "org.matrix.rustcomponents.sdk"
|
||||
cdylib_name = "matrix_sdk_ffi"
|
||||
android_cleaner = true
|
||||
cdylib_name = "matrix_sdk_ffi"
|
||||
@@ -1,47 +0,0 @@
|
||||
# This git-cliff configuration file is used to generate weekly reports for This
|
||||
# Week in Matrix amongst others.
|
||||
|
||||
[changelog]
|
||||
header = """
|
||||
# This Week in the Matrix Rust SDK ({{ now() | date(format="%Y-%m-%d") }})
|
||||
"""
|
||||
body = """
|
||||
{% for commit in commits %}
|
||||
{% set_global commit_message = commit.message -%}
|
||||
{% for footer in commit.footers -%}
|
||||
{% if footer.token | lower == "changelog" -%}
|
||||
{% set_global commit_message = footer.value -%}
|
||||
{% elif footer.token | lower == "breaking-change" -%}
|
||||
{% set_global commit_message = footer.value -%}
|
||||
{% endif -%}
|
||||
{% endfor -%}
|
||||
- {{ commit_message | upper_first }}
|
||||
{% endfor %}
|
||||
"""
|
||||
trim = true
|
||||
footer = ""
|
||||
|
||||
[git]
|
||||
conventional_commits = true
|
||||
filter_unconventional = true
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/pull/${2}))"},
|
||||
]
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "Features" },
|
||||
{ message = "^fix", group = "Bug Fixes" },
|
||||
{ message = "^doc", group = "Documentation" },
|
||||
{ message = "^perf", group = "Performance" },
|
||||
{ message = "^refactor", group = "Refactor", skip = true },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^chore", skip = true },
|
||||
{ message = "^style", group = "Styling", skip = true },
|
||||
{ message = "^test", skip = true },
|
||||
{ message = "^ci", skip = true },
|
||||
]
|
||||
filter_commits = true
|
||||
tag_pattern = "[0-9]*"
|
||||
skip_tags = ""
|
||||
ignore_tags = ""
|
||||
date_order = false
|
||||
sort_commits = "newest"
|
||||
-91
@@ -1,91 +0,0 @@
|
||||
# This git-cliff configuration file is used to generate release reports.
|
||||
|
||||
[changelog]
|
||||
# changelog header
|
||||
header = """
|
||||
# Changelog\n
|
||||
All notable changes to this project will be documented in this file.\n
|
||||
"""
|
||||
# template for the changelog body
|
||||
# https://keats.github.io/tera/docs/
|
||||
body = """
|
||||
{% if version %}\
|
||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
{% endif %}\
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | upper_first }}
|
||||
{% for commit in commits %}
|
||||
{% set_global commit_message = commit.message -%}
|
||||
{% set_global breaking = commit.breaking -%}
|
||||
{% for footer in commit.footers -%}
|
||||
{% if footer.token | lower == "changelog" -%}
|
||||
{% set_global commit_message = footer.value -%}
|
||||
{% elif footer.token | lower == "breaking-change" -%}
|
||||
{% set_global commit_message = footer.value -%}
|
||||
{% elif footer.token | lower == "security-impact" -%}
|
||||
{% set_global security_impact = footer.value -%}
|
||||
{% elif footer.token | lower == "cve" -%}
|
||||
{% set_global cve = footer.value -%}
|
||||
{% elif footer.token | lower == "github-advisory" -%}
|
||||
{% set_global github_advisory = footer.value -%}
|
||||
{% endif -%}
|
||||
{% endfor -%}
|
||||
- {% if breaking %}[**breaking**] {% endif %}{{ commit_message | upper_first }}
|
||||
{% if security_impact -%}
|
||||
(\
|
||||
*{{ security_impact | upper_first }}*\
|
||||
{% if cve -%}, [{{ cve | upper }}](https://www.cve.org/CVERecord?id={{ cve }}){% endif -%}\
|
||||
{% if github_advisory -%}, [{{ github_advisory | upper }}](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/{{ github_advisory }}){% endif -%}
|
||||
)
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
{% endfor %}\n
|
||||
"""
|
||||
# remove the leading and trailing whitespace from the template
|
||||
trim = true
|
||||
# changelog footer
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
"""
|
||||
|
||||
[git]
|
||||
# parse the commits based on https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# filter out the commits that are not conventional
|
||||
filter_unconventional = true
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/pull/${2}))"},
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ footer = "Security-Impact:", group = "Security" },
|
||||
{ footer = "CVE:", group = "Security" },
|
||||
{ footer = "GitHub-Advisory:", group = "Security" },
|
||||
{ message = "^feat", group = "Features" },
|
||||
{ message = "^fix", group = "Bug Fixes" },
|
||||
{ message = "^doc", group = "Documentation" },
|
||||
{ message = "^perf", group = "Performance" },
|
||||
{ message = "^refactor", group = "Refactor" },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^chore", skip = true },
|
||||
{ message = "^style", group = "Styling", skip = true },
|
||||
{ message = "^test", skip = true },
|
||||
{ message = "^ci", skip = true },
|
||||
]
|
||||
# forbid parsers from skipping breaking changes
|
||||
protect_breaking_commits = true
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
filter_commits = true
|
||||
# glob pattern for matching git tags
|
||||
tag_pattern = "[0-9]*"
|
||||
# regex for skipping tags
|
||||
skip_tags = ""
|
||||
# regex for ignoring tags
|
||||
ignore_tags = ""
|
||||
# sort the tags chronologically
|
||||
date_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
sort_commits = "oldest"
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 150 KiB |
+1
-1
@@ -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
|
||||
+5
-5
@@ -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 |
@@ -1,82 +1,3 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [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`
|
||||
|
||||
@@ -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.8.0"
|
||||
version = "0.7.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
@@ -21,19 +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"]
|
||||
experimental-sliding-sync = [
|
||||
"ruma/unstable-msc3575",
|
||||
"ruma/unstable-msc4186",
|
||||
]
|
||||
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 = []
|
||||
|
||||
# "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 = [
|
||||
@@ -49,27 +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.4.0", features = ["serde"] }
|
||||
decancer = "3.2.4"
|
||||
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.0"
|
||||
ruma = { workspace = true, features = ["canonical-json", "unstable-msc3381", "unstable-msc2867", "rand"] }
|
||||
unicode-normalization = "0.1.24"
|
||||
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 }
|
||||
@@ -79,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 = "0.3.33"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,12 +14,13 @@
|
||||
|
||||
//! Helpers for creating `std::fmt::Debug` implementations.
|
||||
|
||||
use std::fmt;
|
||||
use std::{collections::BTreeMap, fmt};
|
||||
|
||||
pub use matrix_sdk_common::debug::*;
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::v3::{InvitedRoom, KnockedRoom},
|
||||
api::client::{push::get_notifications::v3::Notification, sync::sync_events::v3::InvitedRoom},
|
||||
serde::Raw,
|
||||
OwnedRoomId,
|
||||
};
|
||||
|
||||
/// A wrapper around a slice of `Raw` events that implements `Debug` in a way
|
||||
@@ -35,6 +36,47 @@ impl<'a, T> fmt::Debug for DebugListOfRawEventsNoId<'a, T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around a notification map as found in `/sync` responses that
|
||||
/// implements `Debug` in a way that only prints the event ID and event type
|
||||
/// for the raw events contained in each notification.
|
||||
pub struct DebugNotificationMap<'a>(pub &'a BTreeMap<OwnedRoomId, Vec<Notification>>);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugNotificationMap<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut map = f.debug_map();
|
||||
map.entries(self.0.iter().map(|(room_id, raw)| (room_id, DebugNotificationList(raw))));
|
||||
map.finish()
|
||||
}
|
||||
}
|
||||
|
||||
struct DebugNotificationList<'a>(&'a [Notification]);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugNotificationList<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut list = f.debug_list();
|
||||
list.entries(self.0.iter().map(DebugNotification));
|
||||
list.finish()
|
||||
}
|
||||
}
|
||||
|
||||
struct DebugNotification<'a>(&'a Notification);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugNotification<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Notification")
|
||||
.field("actions", &self.0.actions)
|
||||
.field("event", &DebugRawEvent(&self.0.event))
|
||||
.field("profile_tag", &self.0.profile_tag)
|
||||
.field("read", &self.0.read)
|
||||
.field("room_id", &self.0.room_id)
|
||||
.field("ts", &self.0.ts)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around an invited room as found in `/sync` responses that
|
||||
/// implements `Debug` in a way that only prints the event ID and event type for
|
||||
/// the raw events contained in `invite_state`.
|
||||
@@ -49,20 +91,6 @@ impl<'a> fmt::Debug for DebugInvitedRoom<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around a knocked on room as found in `/sync` responses that
|
||||
/// implements `Debug` in a way that only prints the event ID and event type for
|
||||
/// the raw events contained in `knock_state`.
|
||||
pub struct DebugKnockedRoom<'a>(pub &'a KnockedRoom);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugKnockedRoom<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("KnockedRoom")
|
||||
.field("knock_state", &DebugListOfRawEvents(&self.0.knock_state.events))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DebugListOfRawEvents<'a, T>(pub &'a [Raw<T>]);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
|
||||
@@ -14,18 +14,16 @@
|
||||
|
||||
//! SDK-specific variations of response types from Ruma.
|
||||
|
||||
use std::{collections::BTreeMap, fmt, hash::Hash, iter};
|
||||
use std::{collections::BTreeMap, fmt};
|
||||
|
||||
pub use matrix_sdk_common::deserialized_responses::*;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use ruma::{
|
||||
events::{
|
||||
room::{
|
||||
member::{MembershipState, RoomMemberEvent, RoomMemberEventContent},
|
||||
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
||||
},
|
||||
AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, EventContentFromType,
|
||||
AnyStrippedStateEvent, AnySyncStateEvent, EventContentFromType,
|
||||
PossiblyRedactedStateEventContent, RedactContent, RedactedStateEventContent,
|
||||
StateEventContent, StaticStateEventContent, StrippedStateEvent, SyncStateEvent,
|
||||
},
|
||||
@@ -33,16 +31,12 @@ use ruma::{
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UserId,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use unicode_normalization::UnicodeNormalization;
|
||||
|
||||
/// A change in ambiguity of room members that an `m.room.member` event
|
||||
/// triggers.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct AmbiguityChange {
|
||||
/// The user ID of the member that is contained in the state key of the
|
||||
/// `m.room.member` event.
|
||||
pub member_id: OwnedUserId,
|
||||
/// Is the member that is contained in the state key of the `m.room.member`
|
||||
/// event itself ambiguous because of the event.
|
||||
pub member_ambiguous: bool,
|
||||
@@ -52,15 +46,6 @@ pub struct AmbiguityChange {
|
||||
pub ambiguated_member: Option<OwnedUserId>,
|
||||
}
|
||||
|
||||
impl AmbiguityChange {
|
||||
/// Get an iterator over the user IDs listed in this `AmbiguityChange`.
|
||||
pub fn user_ids(&self) -> impl Iterator<Item = &UserId> {
|
||||
iter::once(&*self.member_id)
|
||||
.chain(self.disambiguated_member.as_deref())
|
||||
.chain(self.ambiguated_member.as_deref())
|
||||
}
|
||||
}
|
||||
|
||||
/// Collection of ambiguity changes that room member events trigger.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[non_exhaustive]
|
||||
@@ -70,178 +55,6 @@ pub struct AmbiguityChanges {
|
||||
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
|
||||
}
|
||||
|
||||
static MXID_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(DisplayName::MXID_PATTERN)
|
||||
.expect("We should be able to create a regex from our static MXID pattern")
|
||||
});
|
||||
static LEFT_TO_RIGHT_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(DisplayName::LEFT_TO_RIGHT_PATTERN)
|
||||
.expect("We should be able to create a regex from our static left-to-right pattern")
|
||||
});
|
||||
static HIDDEN_CHARACTERS_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(DisplayName::HIDDEN_CHARACTERS_PATTERN)
|
||||
.expect("We should be able to create a regex from our static hidden characters pattern")
|
||||
});
|
||||
|
||||
/// Regex to match `i` characters.
|
||||
///
|
||||
/// This is used to replace an `i` with a lowercase `l`, i.e. to mark "Hello"
|
||||
/// and "HeIlo" as ambiguous. Decancer will lowercase an `I` for us.
|
||||
static I_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new("[i]").expect("We should be able to create a regex from our uppercase I pattern")
|
||||
});
|
||||
|
||||
/// Regex to match `0` characters.
|
||||
///
|
||||
/// This is used to replace an `0` with a lowercase `o`, i.e. to mark "HellO"
|
||||
/// and "Hell0" as ambiguous. Decancer will lowercase an `O` for us.
|
||||
static ZERO_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new("[0]").expect("We should be able to create a regex from our zero pattern")
|
||||
});
|
||||
|
||||
/// Regex to match a couple of dot-like characters, also matches an actual dot.
|
||||
///
|
||||
/// This is used to replace a `.` with a `:`, i.e. to mark "@mxid.domain.tld" as
|
||||
/// ambiguous.
|
||||
static DOT_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new("[.\u{1d16d}]").expect("We should be able to create a regex from our dot pattern")
|
||||
});
|
||||
|
||||
/// A high-level wrapper for strings representing display names.
|
||||
///
|
||||
/// This wrapper provides attempts to determine whether a display name
|
||||
/// contains characters that could make it ambiguous or easily confused
|
||||
/// with similar names.
|
||||
///
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use matrix_sdk_base::deserialized_responses::DisplayName;
|
||||
///
|
||||
/// let display_name = DisplayName::new("𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶");
|
||||
///
|
||||
/// // The normalized and sanitized string will be returned by DisplayName.as_normalized_str().
|
||||
/// assert_eq!(display_name.as_normalized_str(), Some("sahasrahla"));
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
/// # use matrix_sdk_base::deserialized_responses::DisplayName;
|
||||
/// let display_name = DisplayName::new("@alice:localhost");
|
||||
///
|
||||
/// // The display name looks like an MXID, which makes it ambiguous.
|
||||
/// assert!(display_name.is_inherently_ambiguous());
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Eq)]
|
||||
pub struct DisplayName {
|
||||
raw: String,
|
||||
decancered: Option<String>,
|
||||
}
|
||||
|
||||
impl Hash for DisplayName {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
if let Some(decancered) = &self.decancered {
|
||||
decancered.hash(state);
|
||||
} else {
|
||||
self.raw.hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for DisplayName {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self.decancered.as_deref(), other.decancered.as_deref()) {
|
||||
(None, None) => self.raw == other.raw,
|
||||
(None, Some(_)) | (Some(_), None) => false,
|
||||
(Some(this), Some(other)) => this == other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DisplayName {
|
||||
/// Regex pattern matching an MXID.
|
||||
const MXID_PATTERN: &str = "@.+[:.].+";
|
||||
|
||||
/// Regex pattern matching some left-to-right formatting marks:
|
||||
/// * LTR and RTL marks U+200E and U+200F
|
||||
/// * LTR/RTL and other directional formatting marks U+202A - U+202F
|
||||
const LEFT_TO_RIGHT_PATTERN: &str = "[\u{202a}-\u{202f}\u{200e}\u{200f}]";
|
||||
|
||||
/// Regex pattern matching bunch of unicode control characters and otherwise
|
||||
/// misleading/invisible characters.
|
||||
///
|
||||
/// This includes:
|
||||
/// * various width spaces U+2000 - U+200D
|
||||
/// * Combining characters U+0300 - U+036F
|
||||
/// * Blank/invisible characters (U2800, U2062-U2063)
|
||||
/// * Arabic Letter RTL mark U+061C
|
||||
/// * Zero width no-break space (BOM) U+FEFF
|
||||
const HIDDEN_CHARACTERS_PATTERN: &str =
|
||||
"[\u{2000}-\u{200D}\u{300}-\u{036f}\u{2062}-\u{2063}\u{2800}\u{061c}\u{feff}]";
|
||||
|
||||
/// Creates a new [`DisplayName`] from the given raw string.
|
||||
///
|
||||
/// The raw display name is transformed into a Unicode-normalized form, with
|
||||
/// common confusable characters removed to reduce ambiguity.
|
||||
///
|
||||
/// **Note**: If removing confusable characters fails,
|
||||
/// [`DisplayName::is_inherently_ambiguous`] will return `true`, and
|
||||
/// [`DisplayName::as_normalized_str()`] will return `None.
|
||||
pub fn new(raw: &str) -> Self {
|
||||
let normalized = raw.nfd().collect::<String>();
|
||||
let replaced = DOT_REGEX.replace_all(&normalized, ":");
|
||||
let replaced = HIDDEN_CHARACTERS_REGEX.replace_all(&replaced, "");
|
||||
|
||||
let decancered = decancer::cure!(&replaced).ok().map(|cured| {
|
||||
let removed_left_to_right = LEFT_TO_RIGHT_REGEX.replace_all(cured.as_ref(), "");
|
||||
let replaced = I_REGEX.replace_all(&removed_left_to_right, "l");
|
||||
// We re-run the dot replacement because decancer normalized a lot of weird
|
||||
// characets into a `.`, it just doesn't do that for /u{1d16d}.
|
||||
let replaced = DOT_REGEX.replace_all(&replaced, ":");
|
||||
let replaced = ZERO_REGEX.replace_all(&replaced, "o");
|
||||
|
||||
replaced.to_string()
|
||||
});
|
||||
|
||||
Self { raw: raw.to_owned(), decancered }
|
||||
}
|
||||
|
||||
/// Is this display name considered to be ambiguous?
|
||||
///
|
||||
/// If the display name has cancer (i.e. fails normalisation or has a
|
||||
/// different normalised form) or looks like an MXID, then it's ambiguous.
|
||||
pub fn is_inherently_ambiguous(&self) -> bool {
|
||||
// If we look like an MXID or have hidden characters then we're ambiguous.
|
||||
self.looks_like_an_mxid() || self.has_hidden_characters() || self.decancered.is_none()
|
||||
}
|
||||
|
||||
/// Returns the underlying raw and and unsanitized string of this
|
||||
/// [`DisplayName`].
|
||||
pub fn as_raw_str(&self) -> &str {
|
||||
&self.raw
|
||||
}
|
||||
|
||||
/// Returns the underlying normalized and and sanitized string of this
|
||||
/// [`DisplayName`].
|
||||
///
|
||||
/// Returns `None` if normalization failed during construction of this
|
||||
/// [`DisplayName`].
|
||||
pub fn as_normalized_str(&self) -> Option<&str> {
|
||||
self.decancered.as_deref()
|
||||
}
|
||||
|
||||
fn has_hidden_characters(&self) -> bool {
|
||||
HIDDEN_CHARACTERS_REGEX.is_match(&self.raw)
|
||||
}
|
||||
|
||||
fn looks_like_an_mxid(&self) -> bool {
|
||||
self.decancered
|
||||
.as_deref()
|
||||
.map(|d| MXID_REGEX.is_match(d))
|
||||
.unwrap_or_else(|| MXID_REGEX.is_match(&self.raw))
|
||||
}
|
||||
}
|
||||
|
||||
/// A deserialized response for the rooms members API call.
|
||||
///
|
||||
/// [`GET /_matrix/client/r0/rooms/{roomId}/members`](https://spec.matrix.org/v1.5/client-server-api/#get_matrixclientv3roomsroomidmembers)
|
||||
@@ -253,16 +66,6 @@ pub struct MembersResponse {
|
||||
pub ambiguity_changes: AmbiguityChanges,
|
||||
}
|
||||
|
||||
/// Wrapper around both versions of any event received via sync.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum RawAnySyncOrStrippedTimelineEvent {
|
||||
/// An event from a room in joined or left state.
|
||||
Sync(Raw<AnySyncTimelineEvent>),
|
||||
/// An event from a room in invited state.
|
||||
Stripped(Raw<AnyStrippedStateEvent>),
|
||||
}
|
||||
|
||||
/// Wrapper around both versions of any raw state event.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
@@ -469,12 +272,10 @@ impl MemberEvent {
|
||||
///
|
||||
/// It there is no `displayname` in the event's content, the localpart or
|
||||
/// the user ID is returned.
|
||||
pub fn display_name(&self) -> DisplayName {
|
||||
DisplayName::new(
|
||||
self.original_content()
|
||||
.and_then(|c| c.displayname.as_deref())
|
||||
.unwrap_or_else(|| self.user_id().localpart()),
|
||||
)
|
||||
pub fn display_name(&self) -> &str {
|
||||
self.original_content()
|
||||
.and_then(|c| c.displayname.as_deref())
|
||||
.unwrap_or_else(|| self.user_id().localpart())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,240 +288,3 @@ impl SyncOrStrippedState<RoomPowerLevelsEventContent> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
macro_rules! assert_display_name_eq {
|
||||
($left:expr, $right:expr $(, $desc:expr)?) => {{
|
||||
let left = crate::deserialized_responses::DisplayName::new($left);
|
||||
let right = crate::deserialized_responses::DisplayName::new($right);
|
||||
|
||||
similar_asserts::assert_eq!(
|
||||
left,
|
||||
right
|
||||
$(, $desc)?
|
||||
);
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! assert_display_name_ne {
|
||||
($left:expr, $right:expr $(, $desc:expr)?) => {{
|
||||
let left = crate::deserialized_responses::DisplayName::new($left);
|
||||
let right = crate::deserialized_responses::DisplayName::new($right);
|
||||
|
||||
assert_ne!(
|
||||
left,
|
||||
right
|
||||
$(, $desc)?
|
||||
);
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! assert_ambiguous {
|
||||
($name:expr) => {
|
||||
let name = crate::deserialized_responses::DisplayName::new($name);
|
||||
|
||||
assert!(
|
||||
name.is_inherently_ambiguous(),
|
||||
"The display {:?} should be considered amgibuous",
|
||||
name
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! assert_not_ambiguous {
|
||||
($name:expr) => {
|
||||
let name = crate::deserialized_responses::DisplayName::new($name);
|
||||
|
||||
assert!(
|
||||
!name.is_inherently_ambiguous(),
|
||||
"The display {:?} should not be considered amgibuous",
|
||||
name
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_inherently_ambiguous() {
|
||||
// These should not be inherently ambiguous, only if another similarly looking
|
||||
// display name appears should they be considered to be ambiguous.
|
||||
assert_not_ambiguous!("Alice");
|
||||
assert_not_ambiguous!("Carol");
|
||||
assert_not_ambiguous!("Car0l");
|
||||
assert_not_ambiguous!("Ivan");
|
||||
assert_not_ambiguous!("𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶");
|
||||
assert_not_ambiguous!("Ⓢⓐⓗⓐⓢⓡⓐⓗⓛⓐ");
|
||||
assert_not_ambiguous!("🅂🄰🄷🄰🅂🅁🄰🄷🄻🄰");
|
||||
assert_not_ambiguous!("Sahasrahla");
|
||||
// Left to right is fine, if it's the only one in the room.
|
||||
assert_not_ambiguous!("\u{202e}alharsahas");
|
||||
|
||||
// These on the other hand contain invisible chars.
|
||||
assert_ambiguous!("Sa̴hasrahla");
|
||||
assert_ambiguous!("Sahas\u{200D}rahla");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_capitalization() {
|
||||
// Display name with different capitalization
|
||||
assert_display_name_eq!("Alice", "alice");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_different_names() {
|
||||
// Different display names
|
||||
assert_display_name_ne!("Alice", "Carol");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_capital_l() {
|
||||
// Different display names
|
||||
assert_display_name_eq!("Hello", "HeIlo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_confusable_zero() {
|
||||
// Different display names
|
||||
assert_display_name_eq!("Carol", "Car0l");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_cyrilic() {
|
||||
// Display name with scritpure symbols
|
||||
assert_display_name_eq!("alice", "аlice");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_scriptures() {
|
||||
// Display name with scritpure symbols
|
||||
assert_display_name_eq!("Sahasrahla", "𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_frakturs() {
|
||||
// Display name with fraktur symbols
|
||||
assert_display_name_eq!("Sahasrahla", "𝔖𝔞𝔥𝔞𝔰𝔯𝔞𝔥𝔩𝔞");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_circled() {
|
||||
// Display name with circled symbols
|
||||
assert_display_name_eq!("Sahasrahla", "Ⓢⓐⓗⓐⓢⓡⓐⓗⓛⓐ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_squared() {
|
||||
// Display name with squared symbols
|
||||
assert_display_name_eq!("Sahasrahla", "🅂🄰🄷🄰🅂🅁🄰🄷🄻🄰");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_big_unicode() {
|
||||
// Display name with big unicode letters
|
||||
assert_display_name_eq!("Sahasrahla", "Sahasrahla");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_left_to_right() {
|
||||
// Display name with a left-to-right character
|
||||
assert_display_name_eq!("Sahasrahla", "\u{202e}alharsahas");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_diacritical() {
|
||||
// Display name with a diacritical mark.
|
||||
assert_display_name_eq!("Sahasrahla", "Sa̴hasrahla");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_zero_width_joiner() {
|
||||
// Display name with a zero-width joiner
|
||||
assert_display_name_eq!("Sahasrahla", "Sahas\u{200B}rahla");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_zero_width_space() {
|
||||
// Display name with zero-width space.
|
||||
assert_display_name_eq!("Sahasrahla", "Sahas\u{200D}rahla");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_ligatures() {
|
||||
// Display name with a ligature.
|
||||
assert_display_name_eq!("ff", "\u{FB00}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_confusable_mxid_colon() {
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0589}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{05c3}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0703}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0a83}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{16ec}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{205a}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{2236}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe13}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe52}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe30}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{ff1a}domain.tld");
|
||||
|
||||
// Additionally these should be considered to be ambiguous on their own.
|
||||
assert_ambiguous!("@mxid\u{0589}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{05c3}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{0703}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{0a83}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{16ec}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{205a}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{2236}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{fe13}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{fe52}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{fe30}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{ff1a}domain.tld");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_confusable_mxid_dot() {
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{0701}tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{0702}tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{2024}tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{fe52}tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{ff0e}tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{1d16d}tld");
|
||||
|
||||
// Additionally these should be considered to be ambiguous on their own.
|
||||
assert_ambiguous!("@mxid:domain\u{0701}tld");
|
||||
assert_ambiguous!("@mxid:domain\u{0702}tld");
|
||||
assert_ambiguous!("@mxid:domain\u{2024}tld");
|
||||
assert_ambiguous!("@mxid:domain\u{fe52}tld");
|
||||
assert_ambiguous!("@mxid:domain\u{ff0e}tld");
|
||||
assert_ambiguous!("@mxid:domain\u{1d16d}tld");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_confusable_mxid_replacing_a() {
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:dom\u{1d44e}in.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:dom\u{0430}in.tld");
|
||||
|
||||
// Additionally these should be considered to be ambiguous on their own.
|
||||
assert_ambiguous!("@mxid:dom\u{1d44e}in.tld");
|
||||
assert_ambiguous!("@mxid:dom\u{0430}in.tld");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_confusable_mxid_replacing_l() {
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain.tId");
|
||||
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{217c}d");
|
||||
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{ff4c}d");
|
||||
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{1d5f9}d");
|
||||
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{1d695}d");
|
||||
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{2223}d");
|
||||
|
||||
// Additionally these should be considered to be ambiguous on their own.
|
||||
assert_ambiguous!("@mxid:domain.tId");
|
||||
assert_ambiguous!("@mxid:domain.t\u{217c}d");
|
||||
assert_ambiguous!("@mxid:domain.t\u{ff4c}d");
|
||||
assert_ambiguous!("@mxid:domain.t\u{1d5f9}d");
|
||||
assert_ambiguous!("@mxid:domain.t\u{1d695}d");
|
||||
assert_ambiguous!("@mxid:domain.t\u{2223}d");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,17 +56,4 @@ pub enum Error {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[error(transparent)]
|
||||
MegolmError(#[from] MegolmError),
|
||||
|
||||
/// An error caused by calling the `BaseClient::receive_all_members`
|
||||
/// function with invalid parameters
|
||||
#[error("receive_all_members function was called with invalid parameters")]
|
||||
InvalidReceiveMembersParameters,
|
||||
|
||||
/// This request failed because the local data wasn't sufficient.
|
||||
#[error("Local cache doesn't contain all necessary data to perform the action.")]
|
||||
InsufficientData,
|
||||
|
||||
/// There was a [`serde_json`] deserialization error.
|
||||
#[error(transparent)]
|
||||
DeserializationError(#[from] serde_json::error::Error),
|
||||
}
|
||||
|
||||
@@ -1,30 +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.
|
||||
|
||||
//! Event cache store and common types shared with `matrix_sdk::event_cache`.
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
|
||||
pub mod store;
|
||||
|
||||
/// The kind of event the event storage holds.
|
||||
pub type Event = SyncTimelineEvent;
|
||||
|
||||
/// The kind of gap the event storage holds.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Gap {
|
||||
/// The token to use in the query, extracted from a previous "from" /
|
||||
/// "end" field of a `/messages` response.
|
||||
pub prev_token: String,
|
||||
}
|
||||
@@ -1,317 +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.
|
||||
|
||||
//! Trait and macro of integration tests for `EventCacheStore` implementations.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use ruma::{
|
||||
api::client::media::get_content_thumbnail::v3::Method, events::room::MediaSource, mxc_uri, uint,
|
||||
};
|
||||
|
||||
use super::DynEventCacheStore;
|
||||
use crate::media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings};
|
||||
|
||||
/// `EventCacheStore` integration tests.
|
||||
///
|
||||
/// This trait is not meant to be used directly, but will be used with the
|
||||
/// [`event_cache_store_integration_tests!`] macro.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait EventCacheStoreIntegrationTests {
|
||||
/// Test media content storage.
|
||||
async fn test_media_content(&self);
|
||||
|
||||
/// Test replacing a MXID.
|
||||
async fn test_replace_media_key(&self);
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
async fn test_media_content(&self) {
|
||||
let uri = mxc_uri!("mxc://localhost/media");
|
||||
let request_file = MediaRequestParameters {
|
||||
source: MediaSource::Plain(uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
let request_thumbnail = MediaRequestParameters {
|
||||
source: MediaSource::Plain(uri.to_owned()),
|
||||
format: MediaFormat::Thumbnail(MediaThumbnailSettings::with_method(
|
||||
Method::Crop,
|
||||
uint!(100),
|
||||
uint!(100),
|
||||
)),
|
||||
};
|
||||
|
||||
let other_uri = mxc_uri!("mxc://localhost/media-other");
|
||||
let request_other_file = MediaRequestParameters {
|
||||
source: MediaSource::Plain(other_uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
|
||||
let content: Vec<u8> = "hello".into();
|
||||
let thumbnail_content: Vec<u8> = "world".into();
|
||||
let other_content: Vec<u8> = "foo".into();
|
||||
|
||||
// Media isn't present in the cache.
|
||||
assert!(
|
||||
self.get_media_content(&request_file).await.unwrap().is_none(),
|
||||
"unexpected media found"
|
||||
);
|
||||
assert!(
|
||||
self.get_media_content(&request_thumbnail).await.unwrap().is_none(),
|
||||
"media not found"
|
||||
);
|
||||
|
||||
// Let's add the media.
|
||||
self.add_media_content(&request_file, content.clone()).await.expect("adding media failed");
|
||||
|
||||
// Media is present in the cache.
|
||||
assert_eq!(
|
||||
self.get_media_content(&request_file).await.unwrap().as_ref(),
|
||||
Some(&content),
|
||||
"media not found though added"
|
||||
);
|
||||
|
||||
// Let's remove the media.
|
||||
self.remove_media_content(&request_file).await.expect("removing media failed");
|
||||
|
||||
// Media isn't present in the cache.
|
||||
assert!(
|
||||
self.get_media_content(&request_file).await.unwrap().is_none(),
|
||||
"media still there after removing"
|
||||
);
|
||||
|
||||
// Let's add the media again.
|
||||
self.add_media_content(&request_file, content.clone())
|
||||
.await
|
||||
.expect("adding media again failed");
|
||||
|
||||
assert_eq!(
|
||||
self.get_media_content(&request_file).await.unwrap().as_ref(),
|
||||
Some(&content),
|
||||
"media not found after adding again"
|
||||
);
|
||||
|
||||
// Let's add the thumbnail media.
|
||||
self.add_media_content(&request_thumbnail, thumbnail_content.clone())
|
||||
.await
|
||||
.expect("adding thumbnail failed");
|
||||
|
||||
// Media's thumbnail is present.
|
||||
assert_eq!(
|
||||
self.get_media_content(&request_thumbnail).await.unwrap().as_ref(),
|
||||
Some(&thumbnail_content),
|
||||
"thumbnail not found"
|
||||
);
|
||||
|
||||
// Let's add another media with a different URI.
|
||||
self.add_media_content(&request_other_file, other_content.clone())
|
||||
.await
|
||||
.expect("adding other media failed");
|
||||
|
||||
// Other file is present.
|
||||
assert_eq!(
|
||||
self.get_media_content(&request_other_file).await.unwrap().as_ref(),
|
||||
Some(&other_content),
|
||||
"other file not found"
|
||||
);
|
||||
|
||||
// Let's remove media based on URI.
|
||||
self.remove_media_content_for_uri(uri).await.expect("removing all media for uri failed");
|
||||
|
||||
assert!(
|
||||
self.get_media_content(&request_file).await.unwrap().is_none(),
|
||||
"media wasn't removed"
|
||||
);
|
||||
assert!(
|
||||
self.get_media_content(&request_thumbnail).await.unwrap().is_none(),
|
||||
"thumbnail wasn't removed"
|
||||
);
|
||||
assert!(
|
||||
self.get_media_content(&request_other_file).await.unwrap().is_some(),
|
||||
"other media was removed"
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_replace_media_key(&self) {
|
||||
let uri = mxc_uri!("mxc://sendqueue.local/tr4n-s4ct-10n1-d");
|
||||
let req = MediaRequestParameters {
|
||||
source: MediaSource::Plain(uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
|
||||
let content = "hello".as_bytes().to_owned();
|
||||
|
||||
// Media isn't present in the cache.
|
||||
assert!(self.get_media_content(&req).await.unwrap().is_none(), "unexpected media found");
|
||||
|
||||
// Add the media.
|
||||
self.add_media_content(&req, content.clone()).await.expect("adding media failed");
|
||||
|
||||
// Sanity-check: media is found after adding it.
|
||||
assert_eq!(self.get_media_content(&req).await.unwrap().unwrap(), b"hello");
|
||||
|
||||
// Replacing a media request works.
|
||||
let new_uri = mxc_uri!("mxc://matrix.org/tr4n-s4ct-10n1-d");
|
||||
let new_req = MediaRequestParameters {
|
||||
source: MediaSource::Plain(new_uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
self.replace_media_key(&req, &new_req)
|
||||
.await
|
||||
.expect("replacing the media request key failed");
|
||||
|
||||
// Finding with the previous request doesn't work anymore.
|
||||
assert!(
|
||||
self.get_media_content(&req).await.unwrap().is_none(),
|
||||
"unexpected media found with the old key"
|
||||
);
|
||||
|
||||
// Finding with the new request does work.
|
||||
assert_eq!(self.get_media_content(&new_req).await.unwrap().unwrap(), b"hello");
|
||||
}
|
||||
}
|
||||
|
||||
/// Macro building to allow your `EventCacheStore` implementation to run the
|
||||
/// entire tests suite locally.
|
||||
///
|
||||
/// You need to provide a `async fn get_event_cache_store() ->
|
||||
/// EventCacheStoreResult<impl EventCacheStore>` providing a fresh event cache
|
||||
/// store on the same level you invoke the macro.
|
||||
///
|
||||
/// ## Usage Example:
|
||||
/// ```no_run
|
||||
/// # use matrix_sdk_base::event_cache::store::{
|
||||
/// # EventCacheStore,
|
||||
/// # MemoryStore as MyStore,
|
||||
/// # Result as EventCacheStoreResult,
|
||||
/// # };
|
||||
///
|
||||
/// #[cfg(test)]
|
||||
/// mod tests {
|
||||
/// use super::{EventCacheStore, EventCacheStoreResult, MyStore};
|
||||
///
|
||||
/// async fn get_event_cache_store(
|
||||
/// ) -> EventCacheStoreResult<impl EventCacheStore> {
|
||||
/// Ok(MyStore::new())
|
||||
/// }
|
||||
///
|
||||
/// event_cache_store_integration_tests!();
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(unused_macros, unused_extern_crates)]
|
||||
#[macro_export]
|
||||
macro_rules! event_cache_store_integration_tests {
|
||||
() => {
|
||||
mod event_cache_store_integration_tests {
|
||||
use matrix_sdk_test::async_test;
|
||||
use $crate::event_cache::store::{
|
||||
EventCacheStoreIntegrationTests, IntoEventCacheStore,
|
||||
};
|
||||
|
||||
use super::get_event_cache_store;
|
||||
|
||||
#[async_test]
|
||||
async fn test_media_content() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_media_content().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_replace_media_key() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_replace_media_key().await;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Macro generating tests for the event cache store, related to time (mostly
|
||||
/// for the cross-process lock).
|
||||
#[allow(unused_macros)]
|
||||
#[macro_export]
|
||||
macro_rules! event_cache_store_integration_tests_time {
|
||||
() => {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod event_cache_store_integration_tests_time {
|
||||
use std::time::Duration;
|
||||
|
||||
use matrix_sdk_test::async_test;
|
||||
use $crate::event_cache::store::IntoEventCacheStore;
|
||||
|
||||
use super::get_event_cache_store;
|
||||
|
||||
#[async_test]
|
||||
async fn test_lease_locks() {
|
||||
let store = get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
|
||||
let acquired0 = store.try_take_leased_lock(0, "key", "alice").await.unwrap();
|
||||
assert!(acquired0);
|
||||
|
||||
// Should extend the lease automatically (same holder).
|
||||
let acquired2 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
|
||||
assert!(acquired2);
|
||||
|
||||
// Should extend the lease automatically (same holder + time is ok).
|
||||
let acquired3 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
|
||||
assert!(acquired3);
|
||||
|
||||
// Another attempt at taking the lock should fail, because it's taken.
|
||||
let acquired4 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(!acquired4);
|
||||
|
||||
// Even if we insist.
|
||||
let acquired5 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(!acquired5);
|
||||
|
||||
// That's a nice test we got here, go take a little nap.
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
|
||||
// Still too early.
|
||||
let acquired55 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(!acquired55);
|
||||
|
||||
// Ok you can take another nap then.
|
||||
tokio::time::sleep(Duration::from_millis(250)).await;
|
||||
|
||||
// At some point, we do get the lock.
|
||||
let acquired6 = store.try_take_leased_lock(0, "key", "bob").await.unwrap();
|
||||
assert!(acquired6);
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(1)).await;
|
||||
|
||||
// The other gets it almost immediately too.
|
||||
let acquired7 = store.try_take_leased_lock(0, "key", "alice").await.unwrap();
|
||||
assert!(acquired7);
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(1)).await;
|
||||
|
||||
// But when we take a longer lease...
|
||||
let acquired8 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(acquired8);
|
||||
|
||||
// It blocks the other user.
|
||||
let acquired9 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
|
||||
assert!(!acquired9);
|
||||
|
||||
// We can hold onto our lease.
|
||||
let acquired10 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(acquired10);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,153 +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 std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock, time::Instant};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
ring_buffer::RingBuffer, store_locks::memory_store_helper::try_take_leased_lock,
|
||||
};
|
||||
use ruma::{MxcUri, OwnedMxcUri};
|
||||
|
||||
use super::{EventCacheStore, EventCacheStoreError, Result};
|
||||
use crate::media::{MediaRequestParameters, UniqueKey as _};
|
||||
|
||||
/// In-memory, non-persistent implementation of the `EventCacheStore`.
|
||||
///
|
||||
/// Default if no other is configured at startup.
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[derive(Debug)]
|
||||
pub struct MemoryStore {
|
||||
media: StdRwLock<RingBuffer<(OwnedMxcUri, String /* unique key */, Vec<u8>)>>,
|
||||
leases: StdRwLock<HashMap<String, (String, Instant)>>,
|
||||
}
|
||||
|
||||
// SAFETY: `new_unchecked` is safe because 20 is not zero.
|
||||
const NUMBER_OF_MEDIAS: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(20) };
|
||||
|
||||
impl Default for MemoryStore {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
media: StdRwLock::new(RingBuffer::new(NUMBER_OF_MEDIAS)),
|
||||
leases: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MemoryStore {
|
||||
/// Create a new empty MemoryStore
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl EventCacheStore for MemoryStore {
|
||||
type Error = EventCacheStoreError;
|
||||
|
||||
async fn try_take_leased_lock(
|
||||
&self,
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Result<bool, Self::Error> {
|
||||
Ok(try_take_leased_lock(&self.leases, lease_duration_ms, key, holder))
|
||||
}
|
||||
|
||||
async fn add_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
data: Vec<u8>,
|
||||
) -> Result<()> {
|
||||
// Avoid duplication. Let's try to remove it first.
|
||||
self.remove_media_content(request).await?;
|
||||
// Now, let's add it.
|
||||
self.media.write().unwrap().push((request.uri().to_owned(), request.unique_key(), data));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn replace_media_key(
|
||||
&self,
|
||||
from: &MediaRequestParameters,
|
||||
to: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error> {
|
||||
let expected_key = from.unique_key();
|
||||
|
||||
let mut medias = self.media.write().unwrap();
|
||||
if let Some((mxc, key, _)) = medias.iter_mut().find(|(_, key, _)| *key == expected_key) {
|
||||
*mxc = to.uri().to_owned();
|
||||
*key = to.unique_key();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_media_content(&self, request: &MediaRequestParameters) -> Result<Option<Vec<u8>>> {
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
let media = self.media.read().unwrap();
|
||||
Ok(media.iter().find_map(|(_media_uri, media_key, media_content)| {
|
||||
(media_key == &expected_key).then(|| media_content.to_owned())
|
||||
}))
|
||||
}
|
||||
|
||||
async fn remove_media_content(&self, request: &MediaRequestParameters) -> Result<()> {
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
let mut media = self.media.write().unwrap();
|
||||
let Some(index) = media
|
||||
.iter()
|
||||
.position(|(_media_uri, media_key, _media_content)| media_key == &expected_key)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
media.remove(index);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
|
||||
let mut media = self.media.write().unwrap();
|
||||
let expected_key = uri.to_owned();
|
||||
let positions = media
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(position, (media_uri, _media_key, _media_content))| {
|
||||
(media_uri == &expected_key).then_some(position)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Iterate in reverse-order so that positions stay valid after first removals.
|
||||
for position in positions.into_iter().rev() {
|
||||
media.remove(position);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{EventCacheStore, MemoryStore, Result};
|
||||
|
||||
async fn get_event_cache_store() -> Result<impl EventCacheStore> {
|
||||
Ok(MemoryStore::new())
|
||||
}
|
||||
|
||||
event_cache_store_integration_tests!();
|
||||
event_cache_store_integration_tests_time!();
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user