diff --git a/.github/workflows/bindings_ci.yml b/.github/workflows/bindings_ci.yml index a3cb09f8e..658142e17 100644 --- a/.github/workflows/bindings_ci.yml +++ b/.github/workflows/bindings_ci.yml @@ -133,6 +133,7 @@ jobs: test-apple: name: matrix-rust-components-swift runs-on: macos-12 + if: github.event_name == 'push' || !github.event.pull_request.draft steps: - name: Checkout @@ -161,7 +162,7 @@ jobs: args: uniffi_bindgen --version ^0.18 - name: Generate .xcframework - run: sh bindings/apple/debug_build_xcframework.sh ci + run: sh bindings/apple/debug_build_xcframework.sh x86_64 ci - name: Run XCTests run: | @@ -169,4 +170,4 @@ jobs: -project bindings/apple/MatrixRustSDK.xcodeproj \ -scheme MatrixRustSDK \ -sdk iphonesimulator \ - -destination 'platform=iOS Simulator,name=iPhone 13,OS=15.4' + -destination 'platform=iOS Simulator,name=iPhone 13' diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 7ce0a58ba..0e30d443f 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -13,6 +13,7 @@ jobs: code_coverage: name: Code Coverage runs-on: "ubuntu-latest" + if: github.event_name == 'push' || !github.event.pull_request.draft steps: - name: Checkout repository diff --git a/.github/workflows/prep-crypto-nodejs-release.yml b/.github/workflows/prep-crypto-nodejs-release.yml new file mode 100644 index 000000000..2edf2cdfc --- /dev/null +++ b/.github/workflows/prep-crypto-nodejs-release.yml @@ -0,0 +1,117 @@ +name: Prepare Crypto-Node.js Release +# +# This is a helper workflow to craft a new Node.js release, trigger this via +# the Github Workflow UI by dispatching it manually. Provide the version, the +# matrix-sdk-crypto-nodejs npm package should be set to, and a optionally the +# old version (as used in the git tag) this release should be compared to. +# +# This will then: +# 1. bump the npm version to the one you specified +# 2. commit that change together with the changelog (if it changed, see below) +# 3. create the appropriate tag on that commit +# 4. create the Github draft release, including the changes (if given, see below) +# 5. push these to a new branch, including tag, triggering the `release-crypto-nodejs` workflow +# 6. create a PR to merge these back into `main` +# +# Additionally, if you provide a tag to comapare this tag to, this will: +# 1. create a changelog between the two releases, used for the github release +# 2. update the Changelog.md and include it in the commit +# +# The remaining tasks are done by the release-crypto-nodejs workflow. + +on: + workflow_dispatch: + inputs: + version: + description: 'New Node.js SemVer version to create' + required: true + type: string + previous_version: + description: 'Create the changelog by comparing to this old SemVer Version (as used in the tag) ' + type: string + +env: + PKG_PATH: "bindings/matrix-sdk-crypto-nodejs" + TAG_PREFIX: "matrix-sdk-crypto-nodejs-v" + +jobs: + prepare-release: + name: "Preparing crypto-nodejs release tag" + runs-on: ubuntu-latest + outputs: + tag: "${{ env.TAG_PREFIX }}${{ inputs.version }}" + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + + # Generate changelog since last tag, if given + - name: Generate a changelog for upload + if: inputs.previous_version + uses: orhun/git-cliff-action@v1 + with: + config: "${{ env.PKG_PATH }}/cliff.toml" + args: --strip header "${{env.TAG_PREFIX}}${{ inputs.previous_version }}..HEAD" + env: + GIT_CLIFF_TAG: "Changes ${{ inputs.previous_version }} -> ${{ inputs.version }}" + GIT_CLIFF_OUTPUT: "${{ env.PKG_PATH }}/CHANGES-${{ inputs.version }}.md" + + # Update changelog since last tag, if given + - name: Update existing Changelog + if: inputs.previous_version + uses: orhun/git-cliff-action@v1 + with: + config: "${{ env.PKG_PATH }}/cliff.toml" + args: "${{ inputs.previous_version }}..HEAD" + env: + GIT_CLIFF_TAG: "${{ inputs.version }}" + GIT_CLIFF_PREPEND: "${{ env.PKG_PATH }}/CHANGELOG.md" + + - name: Set version + id: package_version + working-directory: ${{ env.PKG_PATH }} + run: npm version ${{ inputs.version }} + + - uses: EndBug/add-and-commit@v9 + with: + default_author: github_actions + message: "Tagging Crypto-Node.js for release" + tag: "${{env.TAG_PREFIX}}${{ inputs.version }}" + new_branch: "gh-action/release-${{ env.TAG_PREFIX }}${{ inputs.version }}" + push: true + add: | + ${{ env.PKG_PATH }}/package.json + ${{ env.PKG_PATH }}/CHANGELOG.md + + # if we have generated changes + - name: Update Github Release notes + if: inputs.previous_version + uses: softprops/action-gh-release@v1 + with: + draft: true + tag_name: ${{ env.TAG_PREFIX }}${{ inputs.version }} + body_path: "${{ env.PKG_PATH }}/CHANGES-${{ inputs.version }}.md" + + # no changes, use the default changelog for the body + - name: Update Github Release notes + if: ${{!inputs.previous_version}} + uses: softprops/action-gh-release@v1 + with: + draft: true + tag_name: ${{ env.TAG_PREFIX }}${{ inputs.version }} + body_path: "${{ env.PKG_PATH }}/CHANGELOG.md" + + # let's create a PR for all this, too + - name: Create Pull Request + uses: peter-evans/create-pull-request@v4 + with: + title: "Preparing Release ${{ env.TAG_PREFIX }}${{ inputs.version }}" + body: | + Automatic Pull-Request to merge release ${{ env.TAG_PREFIX }}${{ inputs.version }} + + trigger-release: + # and trigger the tagging release workflow + uses: matrix-org/matrix-rust-sdk/.github/workflows/release-crypto-nodejs.yml@main + needs: ['prepare-release'] + name: "Trigger release Workflow" + with: + tag: ${{needs.prepare-release.outputs.tag}} diff --git a/.github/workflows/release-crypto-nodejs.yml b/.github/workflows/release-crypto-nodejs.yml new file mode 100644 index 000000000..19e6fcd73 --- /dev/null +++ b/.github/workflows/release-crypto-nodejs.yml @@ -0,0 +1,139 @@ +name: Release Crypto-Node.js +# +# This workflow releases the crypto-bindings for nodejs +# +# It is triggered when seeing a tag prefixed matching `matrix-sdk-crypto-nodejs-v[0-9]+.*`, +# which then build the native bindings for linux, mac and windows via the CI and uploads +# them to the corresponding Github Release tag. Once they are finished, this workflow will +# package the npm tar.gz and uploads that to the Github Release tag as well, before publishing +# it to npmjs.com automatically. +# +# The usual way to trigger this is by manually triggering the `prep-crypto-nodejs-release` +# workflow. See its documentation for instructions how to use it. + +env: + PKG_PATH: "bindings/matrix-sdk-crypto-nodejs" + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: 'aarch64-linux-gnu-gcc' + CARGO_TARGET_I686_UNKNOWN_LINUX_GNU_LINKER: 'i686-linux-gnu-gcc' + CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_LINKER: 'arm-linux-gnueabihf-gcc' + +on: + push: + tags: + - matrix-sdk-crypto-nodejs-v[0-9]+.* + workflow_call: + inputs: + tag: + description: "The tag to build with" + required: true + type: string +jobs: + upload-assets: + name: "Upload prebuilt libraries" + strategy: + fail-fast: false + matrix: + include: + # ----------------------------------- Linux + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + - target: i686-unknown-linux-gnu + apt_install: gcc-i686-linux-gnu g++-i686-linux-gnu + os: ubuntu-latest + - target: aarch64-unknown-linux-gnu + os: ubuntu-latest + apt_install: gcc-aarch64-linux-gnu g++-aarch64-linux-gnu + - target: arm-unknown-linux-gnueabihf + os: ubuntu-latest + apt_install: gcc-arm-linux-gnueabihf + - target: x86_64-unknown-linux-musl + os: ubuntu-latest + # ----------------------------------- macOS + - target: aarch64-apple-darwin + os: macos-latest + - target: x86_64-apple-darwin + os: macos-latest + # ----------------------------------- Windows + - target: x86_64-pc-windows-msvc + os: windows-latest + - target: i686-pc-windows-msvc + os: windows-latest + - target: aarch64-pc-windows-msvc + os: windows-latest + runs-on: ${{ matrix.os }} + steps: + # use the given tag + - uses: actions/checkout@v3 + name: "Checking out ${{ inputs.tag }}" + if: "${{ inputs.tag }}" + with: + ref: ${{ inputs.tag }} + # use the default + - uses: actions/checkout@v3 + if: "${{ !inputs.tag }}" + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + profile: minimal + target: ${{ matrix.target }} + override: true + - name: Install Node.js + uses: actions/setup-node@v3 + - name: Load cache + uses: Swatinem/rust-cache@v1 + - if: ${{ matrix.apt_install }} + run: | + sudo apt-get update + sudo apt-get install -y ${{ matrix.apt_install }} + - name: Build lib + working-directory: ${{env.PKG_PATH}} + run: | + npm install --ignore-scripts + npx napi build --platform --release --strip --target ${{ matrix.target }} + - name: Upload artifacts to release + uses: softprops/action-gh-release@v1 + with: + draft: true + files: ${{env.PKG_PATH}}/*.node + + publish-nodejs-package: + name: "Package nodejs package" + runs-on: ubuntu-latest + needs: + - upload-assets + steps: + # use the given tag + - uses: actions/checkout@v3 + name: "Checking out ${{ inputs.tag }}" + if: "${{ inputs.tag }}" + with: + ref: ${{ inputs.tag }} + # use the default + - uses: actions/checkout@v3 + if: "${{ !inputs.tag }}" + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + profile: minimal + override: true + - name: Install Node.js + uses: actions/setup-node@v3 + - name: Build lib + working-directory: ${{env.PKG_PATH}} + run: | + npm install --ignore-scripts + npm run build + npm pack + - name: Upload npm package to release + uses: softprops/action-gh-release@v1 + with: + draft: true + files: ${{env.PKG_PATH}}/*tgz + - name: Publish to npmjs.com + uses: JS-DevTools/npm-publish@v1 + with: + package: ${{env.PKG_PATH}}/package.json + access: public + token: ${{ secrets.NPM_TOKEN }} diff --git a/benchmarks/Cargo.toml b/benchmarks/Cargo.toml index df6e400a8..f9325a724 100644 --- a/benchmarks/Cargo.toml +++ b/benchmarks/Cargo.toml @@ -12,7 +12,7 @@ criterion = { version = "0.3.5", features = ["async", "async_tokio", "html_repor matrix-sdk-crypto = { path = "../crates/matrix-sdk-crypto", version = "0.5.0" } matrix-sdk-sled = { path = "../crates/matrix-sdk-sled", version = "0.1.0", default-features = false, features = ["crypto-store"] } matrix-sdk-test = { path = "../crates/matrix-sdk-test", version = "0.5.0" } -ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f" } +ruma = { git = "https://github.com/ruma/ruma", rev = "ca8c66c885241a7ba3805399604eda4a38979f6b" } serde_json = "1.0.79" tempfile = "3.3.0" tokio = { version = "1.17.0", default-features = false, features = ["rt-multi-thread"] } diff --git a/bindings/apple/MatrixRustSDK.xcodeproj/project.pbxproj b/bindings/apple/MatrixRustSDK.xcodeproj/project.pbxproj index 38cc51514..026ad1cf2 100644 --- a/bindings/apple/MatrixRustSDK.xcodeproj/project.pbxproj +++ b/bindings/apple/MatrixRustSDK.xcodeproj/project.pbxproj @@ -44,7 +44,6 @@ 181AA19927B52AA60005F102 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 181AA19A27B52AB40005F102 /* MatrixSDKFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MatrixSDKFFI.xcframework; path = ../../generated/MatrixSDKFFI.xcframework; sourceTree = ""; }; 189A89B927B40BBF0048B0A5 /* sdk.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = sdk.swift; path = ../../../generated/swift/sdk.swift; sourceTree = ""; }; - 189A89C327B417CA0048B0A5 /* MatrixRustSDK-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MatrixRustSDK-Bridging-Header.h"; sourceTree = ""; }; 18CE89D427B2939900CA89E1 /* MatrixRustSDK.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MatrixRustSDK.app; sourceTree = BUILT_PRODUCTS_DIR; }; 18CE89D727B2939900CA89E1 /* MatrixRustSDKApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixRustSDKApp.swift; sourceTree = ""; }; 18CE89D927B2939900CA89E1 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -117,7 +116,6 @@ 18CE89D927B2939900CA89E1 /* ContentView.swift */, 18CE8A0127B293A900CA89E1 /* MatrixRustSDK.entitlements */, 18CE89DB27B2939A00CA89E1 /* Assets.xcassets */, - 189A89C327B417CA0048B0A5 /* MatrixRustSDK-Bridging-Header.h */, ); path = MatrixRustSDK; sourceTree = ""; @@ -307,7 +305,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -362,7 +361,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -397,9 +397,8 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.matrix.MatrixRustSDK; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTS_MACCATALYST = YES; + SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OBJC_BRIDGING_HEADER = "MatrixRustSDK/MatrixRustSDK-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -431,9 +430,8 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.matrix.MatrixRustSDK; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTS_MACCATALYST = YES; + SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OBJC_BRIDGING_HEADER = "MatrixRustSDK/MatrixRustSDK-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; diff --git a/bindings/apple/MatrixRustSDK.xcodeproj/xcshareddata/xcschemes/MatrixRustSDK.xcscheme b/bindings/apple/MatrixRustSDK.xcodeproj/xcshareddata/xcschemes/MatrixRustSDK.xcscheme index 6644a0efe..fe527d24f 100644 --- a/bindings/apple/MatrixRustSDK.xcodeproj/xcshareddata/xcschemes/MatrixRustSDK.xcscheme +++ b/bindings/apple/MatrixRustSDK.xcodeproj/xcshareddata/xcschemes/MatrixRustSDK.xcscheme @@ -1,10 +1,28 @@ + version = "1.7"> + + + + + + + + + + - - - - Copy generated files over to ../../../matrix-rust-components-swift - echo "$(printf "import MatrixSDKFFIWrapper\n\n"; cat "${SWIFT_DIR}/sdk.swift")" > "${SWIFT_DIR}/sdk.swift" - rsync -a --delete "${GENERATED_DIR}/MatrixSDKFFI.xcframework" "${SRC_ROOT}/../matrix-rust-components-swift/" rsync -a --delete "${GENERATED_DIR}/swift/" "${SRC_ROOT}/../matrix-rust-components-swift/Sources/MatrixRustSDK" fi diff --git a/bindings/matrix-sdk-crypto-ffi/Cargo.toml b/bindings/matrix-sdk-crypto-ffi/Cargo.toml index 6e18376c8..3b200e82a 100644 --- a/bindings/matrix-sdk-crypto-ffi/Cargo.toml +++ b/bindings/matrix-sdk-crypto-ffi/Cargo.toml @@ -20,7 +20,7 @@ hmac = "0.12.1" http = "0.2.6" pbkdf2 = "0.11.0" rand = "0.8.5" -ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c"] } +ruma = { git = "https://github.com/ruma/ruma", rev = "ca8c66c885241a7ba3805399604eda4a38979f6b", features = ["client-api-c"] } serde = "1.0.136" serde_json = "1.0.79" sha2 = "0.10.2" diff --git a/bindings/matrix-sdk-crypto-js/Cargo.toml b/bindings/matrix-sdk-crypto-js/Cargo.toml index 94d14468c..2b91685a9 100644 --- a/bindings/matrix-sdk-crypto-js/Cargo.toml +++ b/bindings/matrix-sdk-crypto-js/Cargo.toml @@ -22,18 +22,22 @@ wasm-opt = ['-Oz'] crate-type = ["cdylib"] [features] -default = [] +default = ["tracing"] qrcode = ["matrix-sdk-crypto/qrcode"] docsrs = [] +tracing = [] [dependencies] matrix-sdk-common = { version = "0.5.0", path = "../../crates/matrix-sdk-common" } matrix-sdk-crypto = { version = "0.5.0", path = "../../crates/matrix-sdk-crypto" } -ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c", "js", "rand", "unstable-msc2676", "unstable-msc2677"] } +ruma = { git = "https://github.com/ruma/ruma", rev = "ca8c66c885241a7ba3805399604eda4a38979f6b", features = ["client-api-c", "js", "rand", "unstable-msc2676", "unstable-msc2677"] } vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "2404f83f7d3a3779c1f518e4d949f7da9677c3dd", features = ["js"] } wasm-bindgen = "0.2.80" wasm-bindgen-futures = "0.4.30" js-sys = "0.3.49" +console_error_panic_hook = "0.1.7" serde_json = "1.0.79" http = "0.2.6" -anyhow = "1.0" +anyhow = "1.0.58" +tracing = { version = "0.1.35", default-features = false, features = ["attributes"] } +tracing-subscriber = { version = "0.3.14", default-features = false, features = ["registry", "std"] } diff --git a/bindings/matrix-sdk-crypto-js/package.json b/bindings/matrix-sdk-crypto-js/package.json index 3328f325d..45e7d85d2 100644 --- a/bindings/matrix-sdk-crypto-js/package.json +++ b/bindings/matrix-sdk-crypto-js/package.json @@ -1,5 +1,5 @@ { - "name": "matrix-sdk-crypto-js", + "name": "@matrix-org/matrix-sdk-crypto-js", "version": "0.5.0", "homepage": "https://github.com/matrix-org/matrix-rust-sdk", "description": "Matrix encryption library, for JavaScript", @@ -28,13 +28,14 @@ "devDependencies": { "wasm-pack": "^0.10.2", "jest": "^28.1.0", - "typedoc": "^0.22.17" + "typedoc": "^0.22.17", + "cross-env": "^7.0.3" }, "engines": { "node": ">= 10" }, "scripts": { - "build": "RUSTFLAGS='-C opt-level=z' wasm-pack build --release --target nodejs --out-name matrix_sdk_crypto --out-dir ./pkg", + "build": "cross-env RUSTFLAGS='-C opt-level=z' wasm-pack build --release --target nodejs --out-name matrix_sdk_crypto --out-dir ./pkg", "test": "jest --verbose", "doc": "typedoc --tsconfig ." } diff --git a/bindings/matrix-sdk-crypto-js/src/lib.rs b/bindings/matrix-sdk-crypto-js/src/lib.rs index a9b59426f..b927fedca 100644 --- a/bindings/matrix-sdk-crypto-js/src/lib.rs +++ b/bindings/matrix-sdk-crypto-js/src/lib.rs @@ -25,10 +25,21 @@ pub mod machine; pub mod requests; pub mod responses; pub mod sync_events; +mod tracing; use js_sys::{Object, Reflect}; use wasm_bindgen::{convert::RefFromWasmAbi, prelude::*}; +/// Run some stuff when the Wasm module is instantiated. +/// +/// Right now, it does the following: +/// +/// * Redirect Rust panics to JavaScript console. +#[wasm_bindgen(start)] +pub fn start() { + console_error_panic_hook::set_once(); +} + /// A really hacky and dirty code to downcast a `JsValue` to `T: /// RefFromWasmAbi`, inspired by /// https://github.com/rustwasm/wasm-bindgen/issues/2231#issuecomment-656293288. diff --git a/bindings/matrix-sdk-crypto-js/src/tracing.rs b/bindings/matrix-sdk-crypto-js/src/tracing.rs new file mode 100644 index 000000000..75a7801aa --- /dev/null +++ b/bindings/matrix-sdk-crypto-js/src/tracing.rs @@ -0,0 +1,290 @@ +use wasm_bindgen::prelude::*; + +/// Logger level. +#[wasm_bindgen] +#[derive(Debug, Clone)] +pub enum LoggerLevel { + /// `TRACE` level. + /// + /// Designate very low priority, often extremely verbose, + /// information. + Trace, + + /// `DEBUG` level. + /// + /// Designate lower priority information. + Debug, + + /// `INFO` level. + /// + /// Designate useful information. + Info, + + /// `WARN` level. + /// + /// Designate hazardous situations. + Warn, + + /// `ERROR` level. + /// + /// Designate very serious errors. + Error, +} + +#[cfg(feature = "tracing")] +mod inner { + use std::{ + fmt, + fmt::Write as _, + sync::{Arc, Once}, + }; + + use tracing::{ + field::{Field, Visit}, + metadata::LevelFilter, + Event, Level, Metadata, Subscriber, + }; + use tracing_subscriber::{ + layer::{Context, Layer as TracingLayer}, + prelude::*, + reload, Registry, + }; + + use super::*; + + type TracingInner = Arc>; + + /// Type to install and to manipulate the tracing layer. + #[wasm_bindgen] + pub struct Tracing { + handle: TracingInner, + } + + impl fmt::Debug for Tracing { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Tracing").finish_non_exhaustive() + } + } + + #[wasm_bindgen] + impl Tracing { + /// Check whether the `tracing` feature has been enabled. + #[wasm_bindgen(js_name = "isAvailable")] + pub fn is_available() -> bool { + true + } + + /// Install the tracing layer. + /// + /// `Tracing` is a singleton. Once it is installed, + /// consecutive calls to the constructor will construct a new + /// `Tracing` object but with the exact same inner + /// state. Calling the constructor with a new `min_level` will + /// just update the `min_level` parameter; in that regard, it + /// is similar to calling the `min_level` method on an + /// existing `Tracing` object. + #[wasm_bindgen(constructor)] + pub fn new(min_level: LoggerLevel) -> Result { + static mut INSTALL: Option = None; + static INSTALLED: Once = Once::new(); + + INSTALLED.call_once(|| { + let (filter, reload_handle) = reload::Layer::new(Layer::new(min_level.clone())); + + tracing_subscriber::registry().with(filter).init(); + + unsafe { INSTALL = Some(Arc::new(reload_handle)) }; + }); + + let tracing = Tracing { + handle: unsafe { INSTALL.as_ref() } + .cloned() + .expect("`Tracing` has not been installed correctly"), + }; + + // If it's not the first call to `install`, the + // `min_level` can be different. Let's update it. + tracing.min_level(min_level); + + Ok(tracing) + } + + /// Re-define the minimum logger level. + #[wasm_bindgen(setter, js_name = "minLevel")] + pub fn min_level(&self, min_level: LoggerLevel) { + let _ = self.handle.modify(|layer| layer.min_level = min_level.into()); + } + + /// Turn the logger on, i.e. it emits logs again if it was turned + /// off. + #[wasm_bindgen(js_name = "turnOn")] + pub fn turn_on(&self) { + let _ = self.handle.modify(|layer| layer.turn_on()); + } + + /// Turn the logger off, i.e. it no long emits logs. + #[wasm_bindgen(js_name = "turnOff")] + pub fn turn_off(&self) { + let _ = self.handle.modify(|layer| layer.turn_off()); + } + } + + impl From for Level { + fn from(value: LoggerLevel) -> Self { + use LoggerLevel::*; + + match value { + Trace => Self::TRACE, + Debug => Self::DEBUG, + Info => Self::INFO, + Warn => Self::WARN, + Error => Self::ERROR, + } + } + } + + #[wasm_bindgen] + extern "C" { + #[wasm_bindgen(js_namespace = console, js_name = "trace")] + fn log_trace(message: String); + + #[wasm_bindgen(js_namespace = console, js_name = "debug")] + fn log_debug(message: String); + + #[wasm_bindgen(js_namespace = console, js_name = "info")] + fn log_info(message: String); + + #[wasm_bindgen(js_namespace = console, js_name = "warn")] + fn log_warn(message: String); + + #[wasm_bindgen(js_namespace = console, js_name = "error")] + fn log_error(message: String); + } + + struct Layer { + min_level: Level, + enabled: bool, + } + + impl Layer { + fn new(min_level: L) -> Self + where + L: Into, + { + Self { min_level: min_level.into(), enabled: true } + } + + fn turn_on(&mut self) { + self.enabled = true; + } + + fn turn_off(&mut self) { + self.enabled = false; + } + } + + impl TracingLayer for Layer + where + S: Subscriber, + { + fn enabled(&self, metadata: &Metadata<'_>, _: Context<'_, S>) -> bool { + self.enabled && metadata.level() <= &self.min_level + } + + fn max_level_hint(&self) -> Option { + if !self.enabled { + Some(LevelFilter::OFF) + } else { + Some(LevelFilter::from_level(self.min_level)) + } + } + + fn on_event(&self, event: &Event<'_>, _: Context<'_, S>) { + let mut recorder = StringVisitor::new(); + event.record(&mut recorder); + let metadata = event.metadata(); + let level = metadata.level(); + + let origin = metadata + .file() + .and_then(|file| metadata.line().map(|ln| format!("{}:{}", file, ln))) + .unwrap_or_default(); + + let message = format!("{level} {origin}{recorder}"); + + match *level { + Level::TRACE => log_trace(message), + Level::DEBUG => log_debug(message), + Level::INFO => log_info(message), + Level::WARN => log_warn(message), + Level::ERROR => log_error(message), + } + } + } + + struct StringVisitor { + string: String, + } + + impl StringVisitor { + fn new() -> Self { + Self { string: String::new() } + } + } + + impl Visit for StringVisitor { + fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) { + match field.name() { + "message" => { + if !self.string.is_empty() { + self.string.push('\n'); + } + + let _ = write!(self.string, "{:?}", value); + } + + field_name => { + let _ = write!(self.string, "\n{} = {:?}", field_name, value); + } + } + } + } + + impl fmt::Display for StringVisitor { + fn fmt(&self, mut f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !self.string.is_empty() { + write!(&mut f, " {}", self.string) + } else { + Ok(()) + } + } + } +} + +#[cfg(not(feature = "tracing"))] +mod inner { + use super::*; + + /// Type to install and to manipulate the tracing layer. + #[wasm_bindgen] + #[derive(Debug)] + pub struct Tracing; + + #[wasm_bindgen] + impl Tracing { + /// Check whether the `tracing` feature has been enabled. + #[wasm_bindgen(js_name = "isAvailable")] + pub fn is_available() -> bool { + false + } + + /// The `tracing` feature is not enabled, so this constructor + /// will raise an error. + #[wasm_bindgen(constructor)] + pub fn new(_min_level: LoggerLevel) -> Result { + Err(JsError::new("The `tracing` feature is disabled. Check `Tracing.isAvailable` before constructing `Tracing`")) + } + } +} + +pub use inner::*; diff --git a/bindings/matrix-sdk-crypto-js/tests/machine.test.js b/bindings/matrix-sdk-crypto-js/tests/machine.test.js index 7a851dc4c..dd759f1b8 100644 --- a/bindings/matrix-sdk-crypto-js/tests/machine.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/machine.test.js @@ -299,7 +299,8 @@ describe(OlmMachine.name, () => { room, 'm.room.message', JSON.stringify({ - "hello": "world" + "msgtype": "m.text", + "body": "Hello, World!" }), )); @@ -328,7 +329,8 @@ describe(OlmMachine.name, () => { expect(decrypted).toBeInstanceOf(DecryptedRoomEvent); const event = JSON.parse(decrypted.event); - expect(event.content.hello).toStrictEqual("world"); + expect(event.content.msgtype).toStrictEqual("m.text"); + expect(event.content.body).toStrictEqual("Hello, World!"); expect(decrypted.sender.toString()).toStrictEqual(user.toString()); expect(decrypted.senderDevice.toString()).toStrictEqual(device.toString()); diff --git a/bindings/matrix-sdk-crypto-js/tests/requests.test.js b/bindings/matrix-sdk-crypto-js/tests/requests.test.js index 1f806f4ae..b23a2d31e 100644 --- a/bindings/matrix-sdk-crypto-js/tests/requests.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/requests.test.js @@ -12,7 +12,7 @@ describe('RequestType', () => { }); }); -for (const [request, request_type] of [ +for (const [request, requestType] of [ [KeysUploadRequest, RequestType.KeysUpload], [KeysQueryRequest, RequestType.KeysQuery], [KeysClaimRequest, RequestType.KeysClaim], @@ -28,7 +28,7 @@ for (const [request, request_type] of [ expect(r).toBeInstanceOf(request); expect(r.id).toStrictEqual('foo'); expect(r.body).toStrictEqual('{"bar": "baz"}'); - expect(r.type).toStrictEqual(request_type); + expect(r.type).toStrictEqual(requestType); }); }) diff --git a/bindings/matrix-sdk-crypto-js/tests/tracing.test.js b/bindings/matrix-sdk-crypto-js/tests/tracing.test.js new file mode 100644 index 000000000..bf27431ee --- /dev/null +++ b/bindings/matrix-sdk-crypto-js/tests/tracing.test.js @@ -0,0 +1,84 @@ +const { Tracing, LoggerLevel, OlmMachine, UserId, DeviceId } = require('../pkg/matrix_sdk_crypto'); + +describe('LoggerLevel', () => { + test('has the correct variant values', () => { + expect(LoggerLevel.Trace).toStrictEqual(0); + expect(LoggerLevel.Debug).toStrictEqual(1); + expect(LoggerLevel.Info).toStrictEqual(2); + expect(LoggerLevel.Warn).toStrictEqual(3); + expect(LoggerLevel.Error).toStrictEqual(4); + }); +}); + +describe(Tracing.name, () => { + if (Tracing.isAvailable()) { + let tracing = new Tracing(LoggerLevel.Debug); + + test('can installed several times', () => { + new Tracing(LoggerLevel.Debug); + new Tracing(LoggerLevel.Warn); + new Tracing(LoggerLevel.Debug); + }); + + const originalConsoleDebug = console.debug; + + for (const [testName, testPreState, testPostState, expectedGotcha] of [ + [ + 'can log something', + () => {}, + () => {}, + true, + ], + [ + 'can change the logger level', + () => { tracing.minLevel = LoggerLevel.Warn }, + () => { tracing.minLevel = LoggerLevel.Debug }, + false, + ], + [ + 'can be turned off', + () => { tracing.turnOff() }, + () => {}, + false, + ], + [ + 'can be turned on', + () => { tracing.turnOn() }, + () => {}, + true, + ], + + // This one *must* be the last. We are turning tracing off + // again for the other tests. + [ + 'can be turned off', + () => { tracing.turnOff() }, + () => {}, + false, + ], + ]) { + test(testName, async () => { + testPreState(); + + let gotcha = false; + + console.debug = (msg) => { + gotcha = true; + expect(msg).not.toHaveLength(0); + }; + + // Do something that emits a `DEBUG` log. + await new OlmMachine(new UserId('@alice:example.org'), new DeviceId('foo')); + + console.debug = originalConsoleDebug; + testPostState(); + + expect(gotcha).toStrictEqual(expectedGotcha); + }); + } + } else { + test('cannot be constructed', () => { + expect(() => { new Tracing(LoggerLevel.Error) }).toThrow(); + }); + } +}); diff --git a/bindings/matrix-sdk-crypto-nodejs/.gitignore b/bindings/matrix-sdk-crypto-nodejs/.gitignore index aece07f69..f1fbf26ec 100644 --- a/bindings/matrix-sdk-crypto-nodejs/.gitignore +++ b/bindings/matrix-sdk-crypto-nodejs/.gitignore @@ -4,3 +4,4 @@ /index.d.ts /matrix-sdk-crypto.*.node /docs/* +*.tgz \ No newline at end of file diff --git a/bindings/matrix-sdk-crypto-nodejs/.npmignore b/bindings/matrix-sdk-crypto-nodejs/.npmignore new file mode 100644 index 000000000..3e3d54025 --- /dev/null +++ b/bindings/matrix-sdk-crypto-nodejs/.npmignore @@ -0,0 +1,8 @@ +src/ +tests/ +Cargo.toml +build.rs +*.node +*.tgz +tsconfig.json +cliff.toml \ No newline at end of file diff --git a/bindings/matrix-sdk-crypto-nodejs/CHANGELOG.md b/bindings/matrix-sdk-crypto-nodejs/CHANGELOG.md new file mode 100644 index 000000000..2d83a8260 --- /dev/null +++ b/bindings/matrix-sdk-crypto-nodejs/CHANGELOG.md @@ -0,0 +1,32 @@ +# Matrix-Rust-SDK Node.js Bindings + +## 0.1.0-beta.1 - 2022-07-14 + +- Fixing broken download link, [#842](https://github.com/matrix-org/matrix-rust-sdk/issues/842) + +## 0.1.0-beta.0 - 2022-07-12 + +Welcome to the first release of `matrix-sdk-crypto-nodejs`. This is a +Node.js binding for the Rust `matrix-sdk-crypto` library. This is a +no-network-IO implementation of a state machine, named `OlmMachine`, +that handles E2EE (End-to-End Encryption) for Matrix clients. + +The goal of this binding is _not_ to cover the entirety of the +`matrix-sdk-crypto` API, but only what's required to build Matrix bots +or Matrix bridges (i.e. to connect different networks together via the +Matrix protocol). + +This project replaces and deprecates a previous project, with the same +name and same goals, inside [the `matrix-rust-sdk-bindings` +repository](https://github.com/matrix-org/matrix-rust-sdk-bindings), +with the NPM package name `@turt2live/matrix-sdk-crypto-nodejs`. The +The new official package name is +`@matrix-org/matrix-sdk-crypto-nodejs`. + +Note: All bindings are now part of [the `matrix-rust-sdk` +repository](https://github.com/matrix-org/matrix-rust-sdk) (see the +`bindings/` root directory). + +[A documentation is available inside the new +`matrix-sdk-crypto-nodejs` +project](https://github.com/matrix-org/matrix-rust-sdk/tree/0bde5ccf38f8cda3865297a2d12ddcdaf4b80ca7/bindings/matrix-sdk-crypto-nodejs). diff --git a/bindings/matrix-sdk-crypto-nodejs/Cargo.toml b/bindings/matrix-sdk-crypto-nodejs/Cargo.toml index ee16a92b0..82a51626a 100644 --- a/bindings/matrix-sdk-crypto-nodejs/Cargo.toml +++ b/bindings/matrix-sdk-crypto-nodejs/Cargo.toml @@ -22,16 +22,16 @@ crate-type = ["cdylib"] default = [] qrcode = ["matrix-sdk-crypto/qrcode"] docsrs = [] -tracing = ["tracing-subscriber"] +tracing = ["dep:tracing-subscriber"] [dependencies] matrix-sdk-crypto = { version = "0.5.0", path = "../../crates/matrix-sdk-crypto" } matrix-sdk-common = { version = "0.5.0", path = "../../crates/matrix-sdk-common" } matrix-sdk-sled = { version = "0.1.0", path = "../../crates/matrix-sdk-sled", default-features = false, features = ["crypto-store"] } -ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c", "rand", "unstable-msc2676", "unstable-msc2677"] } +ruma = { git = "https://github.com/ruma/ruma", rev = "ca8c66c885241a7ba3805399604eda4a38979f6b", features = ["client-api-c", "rand", "unstable-msc2676", "unstable-msc2677"] } vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "2404f83f7d3a3779c1f518e4d949f7da9677c3dd" } -napi = { version = "2.6.1", default-features = false, features = ["napi6", "tokio_rt"] } -napi-derive = "2.6.0" +napi = { git = "https://github.com/Hywan/napi-rs", branch = "fix-napi-strict-on-t-and-ref-t", default-features = false, features = ["napi6", "tokio_rt"] } +napi-derive = { git = "https://github.com/Hywan/napi-rs", branch = "fix-napi-strict-on-t-and-ref-t" } serde_json = "1.0.79" http = "0.2.6" zeroize = "1.3.0" diff --git a/bindings/matrix-sdk-crypto-nodejs/README.md b/bindings/matrix-sdk-crypto-nodejs/README.md index 10a6d321a..cb795dc1c 100644 --- a/bindings/matrix-sdk-crypto-nodejs/README.md +++ b/bindings/matrix-sdk-crypto-nodejs/README.md @@ -11,6 +11,77 @@ Encryption](https://en.wikipedia.org/wiki/End-to-end_encryption)) for ## Usage +Just add the latest release to your `package.json`: +```sh +$ npm install --save @matrix-org/matrix-sdk-crypto-nodejs +``` + +When installing, NPM will download the corresponding prebuilt Rust library for your current host system. The following are supported: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PlatformArchitectureTriplePrebuilt available
Linuxaarchaarch64-unknown-linux-gnu
arm-unknown-linux-gnueabihf
amdx86_64-unknown-linux-gnu
x86_64-unknown-linux-musl
i686-unknown-linux-gnu
macOSaarcharch64-apple-darwin
amdx86_64-apple-darwin
Windowsaarchaarch64-pc-windows-msvc
amdx86_64-pc-windows-msvc
i686-pc-windows-msvc
+ +## Development + This Node.js binding is written in [Rust]. To build this binding, you need to install the Rust compiler, see [the Install Rust Page](https://www.rust-lang.org/tools/install). Then, the workflow is @@ -31,7 +102,7 @@ Once the Rust compiler, Node.js and npm are installed, you can run the following commands: ```sh -$ npm install +$ npm install --ignore-scripts $ npm run build $ npm run test ``` diff --git a/bindings/matrix-sdk-crypto-nodejs/cliff.toml b/bindings/matrix-sdk-crypto-nodejs/cliff.toml new file mode 100644 index 000000000..e9fb2cf3b --- /dev/null +++ b/bindings/matrix-sdk-crypto-nodejs/cliff.toml @@ -0,0 +1,59 @@ +# configuration file for git-cliff (0.1.0) + +[changelog] +# changelog header +header = """ +# Matrix SDK Crypto Node.js Changelog\n +All notable changes to this project will be documented in this file.\n +""" +# template for the changelog body +# https://tera.netlify.app/docs/#introduction +body = """ +{% if version %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ + ## [unreleased] +{% endif %}\ +{% for group, commits in commits | filter(attribute="scope", value="crypto-nodejs") | group_by(attribute="group") %} + ### {{ group | upper_first }} + {% for commit in commits %} + - {{ commit.id | truncate(length=7, end="") }}{% if commit.breaking %} [**breaking**] {% endif %}: {{ commit.message | upper_first }}\ + {% endfor %} +{% endfor %}\n +""" +# remove the leading and trailing whitespace from the template +trim = true +# changelog footer +footer = """ +""" + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# regex for preprocessing the commit messages +commit_preprocessors = [ + { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/issues/${2}))"}, +] +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^feat", group = "Features"}, + { message = "^fix", group = "Bug Fixes"}, + { message = "^doc", group = "Documentation"}, + { message = "^perf", group = "Performance"}, + { message = "^test", group = "Testing"}, + { body = ".*security", group = "Security"}, +] +# filter out the commits that are not matched by commit parsers +filter_commits = true +# glob pattern for matching git tags +tag_pattern = "v[0-9]*" +# regex for skipping tags +skip_tags = "v0.1.0-beta.1" +# regex for ignoring tags +ignore_tags = "" +# sort the tags chronologically +date_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" diff --git a/bindings/matrix-sdk-crypto-nodejs/download-lib.js b/bindings/matrix-sdk-crypto-nodejs/download-lib.js new file mode 100644 index 000000000..ec7c157e3 --- /dev/null +++ b/bindings/matrix-sdk-crypto-nodejs/download-lib.js @@ -0,0 +1,113 @@ +const { DownloaderHelper } = require('node-downloader-helper'); +const { version } = require("./package.json"); +const { platform, arch } = process + +const DOWNLOADS_BASE_URL = "https://github.com/matrix-org/matrix-rust-sdk/releases/download"; +const CURRENT_VERSION = `matrix-sdk-crypto-nodejs-v${version}`; + +const byteHelper = function (value) { + if (value === 0) { + return '0 b'; + } + const units = ['b', 'kB', 'MB', 'GB', 'TB']; + const number = Math.floor(Math.log(value) / Math.log(1024)); + return (value / Math.pow(1024, Math.floor(number))).toFixed(1) + ' ' + + units[number]; +}; + +function download_lib(libname) { + let startTime = new Date(); + + const url = `${DOWNLOADS_BASE_URL}/${CURRENT_VERSION}/${libname}`; + console.info(`Downloading lib ${libname} from ${url}`); + const dl = new DownloaderHelper(url, __dirname, { + override: true, + }); + + dl.on('end', () => console.info('Download Completed')); + dl.on('error', (err) => console.info('Download Failed', err)); + dl.on('progress', stats => { + const progress = stats.progress.toFixed(1); + const speed = byteHelper(stats.speed); + const downloaded = byteHelper(stats.downloaded); + const total = byteHelper(stats.total); + + // print every one second (`progress.throttled` can be used instead) + const currentTime = new Date(); + const elaspsedTime = currentTime - startTime; + if (elaspsedTime > 1000) { + startTime = currentTime; + console.info(`${speed}/s - ${progress}% [${downloaded}/${total}]`); + } + }); + dl.start().catch(err => console.error(err)); +} + +function isMusl() { + // For Node 10 + if (!process.report || typeof process.report.getReport !== 'function') { + try { + return readFileSync('/usr/bin/ldd', 'utf8').includes('musl') + } catch (e) { + return true + } + } else { + const { glibcVersionRuntime } = process.report.getReport().header + return !glibcVersionRuntime + } +} + +switch (platform) { + case 'win32': + switch (arch) { + case 'x64': + download_lib('matrix-sdk-crypto.win32-x64-msvc.node') + break + case 'ia32': + download_lib('matrix-sdk-crypto.win32-ia32-msvc.node') + break + case 'arm64': + download_lib('matrix-sdk-crypto.win32-arm64-msvc.node') + break + default: + throw new Error(`Unsupported architecture on Windows: ${arch}`) + } + break + case 'darwin': + switch (arch) { + case 'x64': + download_lib('matrix-sdk-crypto.darwin-x64.node') + break + case 'arm64': + download_lib('matrix-sdk-crypto.darwin-arm64.node') + break + default: + throw new Error(`Unsupported architecture on macOS: ${arch}`) + } + break + case 'linux': + switch (arch) { + case 'x64': + if (isMusl()) { + download_lib('matrix-sdk-crypto.linux-x64-musl.node') + } else { + download_lib('matrix-sdk-crypto.linux-x64-gnu.node') + } + break + case 'arm64': + if (isMusl()) { + throw new Error('Linux for arm64 musl isn\'t support at the moment') + } else { + download_lib('matrix-sdk-crypto.linux-arm64-gnu.node') + } + break + case 'arm': + download_lib('matrix-sdk-crypto.linux-arm-gnueabihf.node') + break + default: + throw new Error(`Unsupported architecture on Linux: ${arch}`) + } + break + default: + throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`) +} diff --git a/bindings/matrix-sdk-crypto-nodejs/package.json b/bindings/matrix-sdk-crypto-nodejs/package.json index 46b18cbcd..9699a89c7 100644 --- a/bindings/matrix-sdk-crypto-nodejs/package.json +++ b/bindings/matrix-sdk-crypto-nodejs/package.json @@ -1,6 +1,6 @@ { - "name": "@matrix-org/matrix-sdk-crypto", - "version": "0.5.0", + "name": "@matrix-org/matrix-sdk-crypto-nodejs", + "version": "0.1.0-beta.1", "main": "index.js", "types": "index.d.ts", "napi": { @@ -23,7 +23,11 @@ "scripts": { "release-build": "napi build --platform --release --strip", "build": "napi build --platform", + "postinstall": "node download-lib.js", "test": "jest --verbose --testTimeout 10000", "doc": "typedoc --tsconfig ." + }, + "dependencies": { + "node-downloader-helper": "^2.1.1" } } diff --git a/bindings/matrix-sdk-crypto-nodejs/src/attachment.rs b/bindings/matrix-sdk-crypto-nodejs/src/attachment.rs index 9432c19b2..14b26b015 100644 --- a/bindings/matrix-sdk-crypto-nodejs/src/attachment.rs +++ b/bindings/matrix-sdk-crypto-nodejs/src/attachment.rs @@ -55,7 +55,7 @@ impl Attachment { None => { return Err(napi::Error::from_reason( "The media encryption info are absent from the given encrypted attachment" - .to_string(), + .to_owned(), )) } }; diff --git a/bindings/matrix-sdk-crypto-nodejs/src/identifiers.rs b/bindings/matrix-sdk-crypto-nodejs/src/identifiers.rs index eb45873b8..f5b6c4b55 100644 --- a/bindings/matrix-sdk-crypto-nodejs/src/identifiers.rs +++ b/bindings/matrix-sdk-crypto-nodejs/src/identifiers.rs @@ -24,7 +24,7 @@ impl From for UserId { #[napi] impl UserId { /// Parse/validate and create a new `UserId`. - #[napi(constructor)] + #[napi(constructor, strict)] pub fn new(id: String) -> napi::Result { Ok(Self::from(ruma::UserId::parse(id.as_str()).map_err(into_err)?)) } @@ -78,7 +78,7 @@ impl From for DeviceId { #[napi] impl DeviceId { /// Create a new `DeviceId`. - #[napi(constructor)] + #[napi(constructor, strict)] pub fn new(id: String) -> Self { Self::from(Into::::into(id)) } @@ -109,7 +109,7 @@ impl From for DeviceKeyId { #[napi] impl DeviceKeyId { /// Parse/validate and create a new `DeviceKeyId`. - #[napi(constructor)] + #[napi(constructor, strict)] pub fn new(id: String) -> napi::Result { Ok(Self::from(ruma::DeviceKeyId::parse(id.as_str()).map_err(into_err)?)) } @@ -212,7 +212,7 @@ impl From for RoomId { #[napi] impl RoomId { /// Parse/validate and create a new `RoomId`. - #[napi(constructor)] + #[napi(constructor, strict)] pub fn new(id: String) -> napi::Result { Ok(Self::from(ruma::RoomId::parse(id).map_err(into_err)?)) } @@ -252,7 +252,7 @@ pub struct ServerName { #[napi] impl ServerName { /// Parse/validate and create a new `ServerName`. - #[napi(constructor)] + #[napi(constructor, strict)] pub fn new(name: String) -> napi::Result { Ok(Self { inner: ruma::ServerName::parse(name).map_err(into_err)? }) } diff --git a/bindings/matrix-sdk-crypto-nodejs/src/machine.rs b/bindings/matrix-sdk-crypto-nodejs/src/machine.rs index 9128f2f4f..5348ad54a 100644 --- a/bindings/matrix-sdk-crypto-nodejs/src/machine.rs +++ b/bindings/matrix-sdk-crypto-nodejs/src/machine.rs @@ -56,7 +56,7 @@ impl OlmMachine { /// data at rest in the store. **Warning**, if no passphrase is given, the /// store and all its data will remain unencrypted. This argument is /// ignored if `store_path` is not set. - #[napi] + #[napi(strict)] pub async fn initialize( user_id: &identifiers::UserId, device_id: &identifiers::DeviceId, @@ -144,7 +144,7 @@ impl OlmMachine { /// response. /// * `one_time_keys_count`, the current one-time keys counts that the sync /// response returned. - #[napi] + #[napi(strict)] pub async fn receive_sync_changes( &self, to_device_events: String, @@ -227,7 +227,7 @@ impl OlmMachine { /// * `request_type`, the request type associated to the request ID. /// * `response`, the response that was received from the server after the /// outgoing request was sent out. - #[napi] + #[napi(strict)] pub async fn mark_request_as_sent( &self, request_id: String, @@ -272,7 +272,7 @@ impl OlmMachine { /// * `users`, the list of users that we should check if we lack a session /// with one of their devices. This can be an empty array or `null` when /// calling this method between sync requests. - #[napi] + #[napi(strict)] pub async fn get_missing_sessions( &self, users: Option>, @@ -312,7 +312,7 @@ impl OlmMachine { /// # Arguments /// /// * `users`, an array over user IDs that should be marked for tracking. - #[napi] + #[napi(strict)] pub async fn update_tracked_users(&self, users: Vec<&identifiers::UserId>) { let users = users.into_iter().map(|user| user.inner.clone()).collect::>(); @@ -326,7 +326,7 @@ impl OlmMachine { /// * `room_id`, the room ID of the room where the room key will be used. /// * `users`, the list of users that should receive the room key. /// * `encryption_settings`, the encryption settings. - #[napi] + #[napi(strict)] pub async fn share_room_key( &self, room_id: &identifiers::RoomId, @@ -357,7 +357,7 @@ impl OlmMachine { /// * `event_type`, the plaintext type of the event. /// * `content`, the JSON-encoded content of the message that should be /// encrypted. - #[napi] + #[napi(strict)] pub async fn encrypt_room_event( &self, room_id: &identifiers::RoomId, @@ -383,7 +383,7 @@ impl OlmMachine { /// /// * `event`, the event that should be decrypted. /// * `room_id`, the ID of the room where the event was sent to. - #[napi] + #[napi(strict)] pub async fn decrypt_room_event( &self, event: String, @@ -409,7 +409,7 @@ impl OlmMachine { /// Sign the given message using our device key and if available /// cross-signing master key. - #[napi] + #[napi(strict)] pub async fn sign(&self, message: String) -> types::Signatures { self.inner.sign(message.as_str()).await.into() } diff --git a/bindings/matrix-sdk-crypto-nodejs/src/sync_events.rs b/bindings/matrix-sdk-crypto-nodejs/src/sync_events.rs index 1f7f1619e..63e017021 100644 --- a/bindings/matrix-sdk-crypto-nodejs/src/sync_events.rs +++ b/bindings/matrix-sdk-crypto-nodejs/src/sync_events.rs @@ -13,7 +13,7 @@ pub struct DeviceLists { #[napi] impl DeviceLists { /// Create an empty `DeviceLists`. - #[napi(constructor)] + #[napi(constructor, strict)] pub fn new( changed: Option>, left: Option>, diff --git a/bindings/matrix-sdk-crypto-nodejs/src/types.rs b/bindings/matrix-sdk-crypto-nodejs/src/types.rs index ca68adab4..89d1027d5 100644 --- a/bindings/matrix-sdk-crypto-nodejs/src/types.rs +++ b/bindings/matrix-sdk-crypto-nodejs/src/types.rs @@ -29,7 +29,7 @@ impl Signatures { /// Add the given signature from the given signer and the given key ID to /// the collection. - #[napi] + #[napi(strict)] pub fn add_signature( &mut self, signer: &UserId, @@ -43,13 +43,13 @@ impl Signatures { /// Try to find an Ed25519 signature from the given signer with /// the given key ID. - #[napi] + #[napi(strict)] pub fn get_signature(&self, signer: &UserId, key_id: &DeviceKeyId) -> Option { self.inner.get_signature(signer.inner.as_ref(), key_id.inner.as_ref()).map(Into::into) } /// Get the map of signatures that belong to the given user. - #[napi] + #[napi(strict)] pub fn get(&self, signer: &UserId) -> Option> { self.inner.get(signer.inner.as_ref()).map(|map| { map.iter() diff --git a/bindings/matrix-sdk-crypto-nodejs/src/vodozemac.rs b/bindings/matrix-sdk-crypto-nodejs/src/vodozemac.rs index eacc15196..f2ecfc06b 100644 --- a/bindings/matrix-sdk-crypto-nodejs/src/vodozemac.rs +++ b/bindings/matrix-sdk-crypto-nodejs/src/vodozemac.rs @@ -42,7 +42,7 @@ impl From for Ed25519Signature { impl Ed25519Signature { /// Try to create an Ed25519 signature from an unpadded base64 /// representation. - #[napi(constructor)] + #[napi(constructor, strict)] pub fn new(signature: String) -> napi::Result { Ok(Self { inner: vodozemac::Ed25519Signature::from_base64(signature.as_str()) diff --git a/bindings/matrix-sdk-ffi/Cargo.toml b/bindings/matrix-sdk-ffi/Cargo.toml index bc2f81bb1..a686272a9 100644 --- a/bindings/matrix-sdk-ffi/Cargo.toml +++ b/bindings/matrix-sdk-ffi/Cargo.toml @@ -31,6 +31,7 @@ thiserror = "1.0.30" tokio = { version = "1", features = ["rt-multi-thread", "macros"] } tokio-stream = "0.1.8" tracing = "0.1.32" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } # keep in sync with uniffi dependency in matrix-sdk-crypto-ffi, and uniffi_bindgen in ffi CI job uniffi = "0.18.0" uniffi_macros = "0.18.0" diff --git a/bindings/matrix-sdk-ffi/src/api.udl b/bindings/matrix-sdk-ffi/src/api.udl index 80a24aa20..76d2ada9f 100644 --- a/bindings/matrix-sdk-ffi/src/api.udl +++ b/bindings/matrix-sdk-ffi/src/api.udl @@ -1,4 +1,6 @@ namespace sdk { + void setup_tracing(string configuration); + MediaSource media_source_from_url(string url); MessageEventContent message_event_content_from_markdown(string md); string gen_transaction_id(); @@ -156,24 +158,23 @@ interface MediaSource { [Error] enum AuthenticationError { - "ClientMissing", - "Generic", + "ClientMissing", + "Generic", +}; + +interface HomeserverLoginDetails { + string url(); + string? authentication_issuer(); + boolean supports_password_login(); }; interface AuthenticationService { constructor(string base_path); - [Throws=AuthenticationError] - string homeserver(); + HomeserverLoginDetails? homeserver_details(); [Throws=AuthenticationError] - string? authentication_issuer(); - - [Throws=AuthenticationError] - boolean supports_password_login(); - - [Throws=AuthenticationError] - void use_server(string server_name); + void configure_homeserver(string server_name); [Throws=AuthenticationError] Client login(string username, string password); diff --git a/bindings/matrix-sdk-ffi/src/authentication_service.rs b/bindings/matrix-sdk-ffi/src/authentication_service.rs index 7ed148d82..0c3df8f12 100644 --- a/bindings/matrix-sdk-ffi/src/authentication_service.rs +++ b/bindings/matrix-sdk-ffi/src/authentication_service.rs @@ -1,12 +1,14 @@ use std::sync::Arc; +use futures_util::future::join3; use parking_lot::RwLock; -use super::{client::Client, client_builder::ClientBuilder}; +use super::{client::Client, client_builder::ClientBuilder, RUNTIME}; pub struct AuthenticationService { base_path: String, client: RwLock>>, + homeserver_details: RwLock>>, } #[derive(Debug, thiserror::Error)] @@ -23,52 +25,67 @@ impl From for AuthenticationError { } } -impl AuthenticationService { - /// Creates a new service to authenticate a user with. - pub fn new(base_path: String) -> Self { - AuthenticationService { base_path, client: RwLock::new(None) } - } +pub struct HomeserverLoginDetails { + url: String, + authentication_issuer: Option, + supports_password_login: bool, +} - /// The currently configured homeserver. - pub fn homeserver(&self) -> Result { - self.client - .read() - .as_ref() - .ok_or(AuthenticationError::ClientMissing) - .map(|client| client.homeserver()) +impl HomeserverLoginDetails { + /// The URL of the currently configured homeserver. + pub fn url(&self) -> String { + self.url.clone() } /// The OIDC Provider that is trusted by the homeserver. `None` when /// not configured. - pub fn authentication_issuer(&self) -> Result, AuthenticationError> { - self.client - .read() - .as_ref() - .ok_or(AuthenticationError::ClientMissing) - .map(|client| client.authentication_issuer()) + pub fn authentication_issuer(&self) -> Option { + self.authentication_issuer.clone() } /// Whether the current homeserver supports the password login flow. - pub fn supports_password_login(&self) -> Result { - self.client - .read() - .as_ref() - .ok_or(AuthenticationError::ClientMissing) - .and_then(|client| client.supports_password_login().map_err(AuthenticationError::from)) + pub fn supports_password_login(&self) -> bool { + self.supports_password_login + } +} + +impl AuthenticationService { + /// Creates a new service to authenticate a user with. + pub fn new(base_path: String) -> Self { + AuthenticationService { + base_path, + client: RwLock::new(None), + homeserver_details: RwLock::new(None), + } } - /// Updates the server to authenticate with the specified homeserver. - pub fn use_server(&self, server_name: String) -> Result<(), AuthenticationError> { + pub fn homeserver_details(&self) -> Option> { + self.homeserver_details.read().clone() + } + + /// Updates the service to authenticate with the homeserver for the + /// specified address. + pub fn configure_homeserver(&self, server_name: String) -> Result<(), AuthenticationError> { // Construct a username as the builder currently requires one. let username = format!("@auth:{}", server_name); - let client = Arc::new(ClientBuilder::new()) - .base_path(self.base_path.clone()) - .username(username) - .build() - .map_err(AuthenticationError::from)?; - *self.client.write() = Some(client); - Ok(()) + let mut builder = + Arc::new(ClientBuilder::new()).base_path(self.base_path.clone()).username(username); + + if server_name.starts_with("http://") || server_name.starts_with("https://") { + builder = builder.homeserver_url(server_name) + } + + let client = builder.build().map_err(AuthenticationError::from)?; + + RUNTIME.block_on(async move { + let details = Arc::new(self.details_from_client(&client).await?); + + *self.client.write() = Some(client); + *self.homeserver_details.write() = Some(details); + + Ok(()) + }) } /// Performs a password login using the current homeserver. @@ -97,4 +114,23 @@ impl AuthenticationService { None => Err(AuthenticationError::ClientMissing), } } + + /// Get the homeserver login details from a client. + async fn details_from_client( + &self, + client: &Arc, + ) -> Result { + let login_details = join3( + client.async_homeserver(), + client.authentication_issuer(), + client.supports_password_login(), + ) + .await; + + let url = login_details.0; + let authentication_issuer = login_details.1; + let supports_password_login = login_details.2.map_err(AuthenticationError::from)?; + + Ok(HomeserverLoginDetails { url, authentication_issuer, supports_password_login }) + } } diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 0dda08d72..35538cb84 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -74,26 +74,27 @@ impl Client { /// The homeserver this client is configured to use. pub fn homeserver(&self) -> String { - RUNTIME.block_on(async move { self.client.homeserver().await.to_string() }) + RUNTIME.block_on(async move { self.async_homeserver().await }) + } + + pub async fn async_homeserver(&self) -> String { + self.client.homeserver().await.to_string() } /// The OIDC Provider that is trusted by the homeserver. `None` when /// not configured. - pub fn authentication_issuer(&self) -> Option { - RUNTIME.block_on(async move { - self.client.authentication_issuer().await.map(|server| server.to_string()) - }) + pub async fn authentication_issuer(&self) -> Option { + self.client.authentication_issuer().await.map(|server| server.to_string()) } /// Whether or not the client's homeserver supports the password login flow. - pub fn supports_password_login(&self) -> anyhow::Result { - RUNTIME.block_on(async move { - let login_types = self.client.get_login_types().await?; - let supports_password = login_types.flows.iter().any(|login_type| { - matches!(login_type, get_login_types::v3::LoginType::Password(_)) - }); - Ok(supports_password) - }) + pub async fn supports_password_login(&self) -> anyhow::Result { + let login_types = self.client.get_login_types().await?; + let supports_password = login_types + .flows + .iter() + .any(|login_type| matches!(login_type, get_login_types::v3::LoginType::Password(_))); + Ok(supports_password) } pub fn start_sync(&self) { diff --git a/bindings/matrix-sdk-ffi/src/lib.rs b/bindings/matrix-sdk-ffi/src/lib.rs index 7fb23d268..db2461303 100644 --- a/bindings/matrix-sdk-ffi/src/lib.rs +++ b/bindings/matrix-sdk-ffi/src/lib.rs @@ -17,6 +17,7 @@ use matrix_sdk::Session; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use tokio::runtime::Runtime; +use tracing_subscriber::{fmt, prelude::*, EnvFilter}; pub use uniffi_api::*; pub static RUNTIME: Lazy = @@ -55,3 +56,10 @@ impl From for ClientError { ClientError::Generic { msg: e.to_string() } } } + +fn setup_tracing(configuration: String) { + tracing_subscriber::registry() + .with(EnvFilter::new(configuration)) + .with(fmt::layer().with_ansi(false)) + .init(); +} diff --git a/crates/matrix-sdk-appservice/Cargo.toml b/crates/matrix-sdk-appservice/Cargo.toml index d3f43ba66..258591231 100644 --- a/crates/matrix-sdk-appservice/Cargo.toml +++ b/crates/matrix-sdk-appservice/Cargo.toml @@ -34,7 +34,7 @@ http = "0.2.6" matrix-sdk = { version = "0.5.0", path = "../matrix-sdk", default-features = false, features = ["appservice"] } percent-encoding = "2.1.0" regex = "1.5.5" -ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c", "appservice-api-s"] } +ruma = { git = "https://github.com/ruma/ruma", rev = "ca8c66c885241a7ba3805399604eda4a38979f6b", features = ["client-api-c", "appservice-api-s"] } serde = "1.0.136" serde_json = "1.0.79" serde_yaml = "0.8.23" diff --git a/crates/matrix-sdk-appservice/examples/appservice_autojoin.rs b/crates/matrix-sdk-appservice/examples/appservice_autojoin.rs index 0964ef528..8835449e4 100644 --- a/crates/matrix-sdk-appservice/examples/appservice_autojoin.rs +++ b/crates/matrix-sdk-appservice/examples/appservice_autojoin.rs @@ -27,11 +27,11 @@ pub async fn handle_room_member( trace!("not an appservice user: {}", event.state_key); } else if let MembershipState::Invite = event.content.membership { let user_id = UserId::parse(event.state_key.as_str())?; - if let Err(error) = appservice.register_virtual_user(user_id.localpart()).await { + if let Err(error) = appservice.register_virtual_user(user_id.localpart(), None).await { error_if_user_not_in_use(error)?; } - let client = appservice.virtual_user_client(user_id.localpart()).await?; + let client = appservice.virtual_user(Some(user_id.localpart())).await?; client.join_room_by_id(room.room_id()).await?; } @@ -61,7 +61,9 @@ pub async fn main() -> Result<(), Box> { let appservice = AppService::new(homeserver_url, server_name, registration).await?; appservice.register_user_query(Box::new(|_, _| Box::pin(async { true }))).await; appservice - .register_event_handler_context(appservice.clone())? + .virtual_user(None) + .await? + .register_event_handler_context(appservice.clone()) .register_event_handler( move |event: OriginalSyncRoomMemberEvent, room: Room, @@ -69,7 +71,7 @@ pub async fn main() -> Result<(), Box> { handle_room_member(appservice, room, event) }, ) - .await?; + .await; let (host, port) = appservice.registration().get_host_and_port()?; appservice.run(host, port).await?; diff --git a/crates/matrix-sdk-appservice/src/error.rs b/crates/matrix-sdk-appservice/src/error.rs index de3d15ed2..68a863ed5 100644 --- a/crates/matrix-sdk-appservice/src/error.rs +++ b/crates/matrix-sdk-appservice/src/error.rs @@ -41,41 +41,41 @@ pub enum Error { #[error("uri path is unknown")] UriPathUnknown, - #[error(transparent)] - HttpRequest(#[from] ruma::api::error::FromHttpRequestError), + #[error("HTTP request parsing error: {0}")] + FromHttpRequest(#[from] ruma::api::error::FromHttpRequestError), - #[error(transparent)] + #[error("identifier failed to parse: {0}")] Identifier(#[from] ruma::IdParseError), - #[error(transparent)] + #[error("HTTP error: {0}")] Http(#[from] http::Error), - #[error(transparent)] + #[error("url parse error: {0}")] Url(#[from] url::ParseError), - #[error(transparent)] + #[error("deserialization error: {0}")] Serde(#[from] serde::de::value::Error), - #[error(transparent)] + #[error("I/O error: {0}")] Io(#[from] std::io::Error), - #[error(transparent)] + #[error("http uri invalid error: {0}")] InvalidUri(#[from] http::uri::InvalidUri), #[error(transparent)] Matrix(#[from] matrix_sdk::Error), - #[error(transparent)] + #[error("regex error: {0}")] Regex(#[from] regex::Error), - #[error(transparent)] + #[error("serde yaml error: {0}")] SerdeYaml(#[from] serde_yaml::Error), - #[error(transparent)] + #[error("serde json error: {0}")] SerdeJson(#[from] serde_json::Error), - #[error(transparent)] - Utf8Error(#[from] std::str::Utf8Error), + #[error("utf8 error: {0}")] + Utf8(#[from] std::str::Utf8Error), #[error("warp rejection: {0}")] WarpRejection(String), diff --git a/crates/matrix-sdk-appservice/src/event_handler.rs b/crates/matrix-sdk-appservice/src/event_handler.rs index 0e1d1a73c..7d86a5331 100644 --- a/crates/matrix-sdk-appservice/src/event_handler.rs +++ b/crates/matrix-sdk-appservice/src/event_handler.rs @@ -1,3 +1,17 @@ +// Copyright 2022 Famedly GmbH +// +// 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::{future::Future, pin::Pin, sync::Arc}; use matrix_sdk::locks::Mutex; diff --git a/crates/matrix-sdk-appservice/src/lib.rs b/crates/matrix-sdk-appservice/src/lib.rs index 5060f2b8e..7fb2519c8 100644 --- a/crates/matrix-sdk-appservice/src/lib.rs +++ b/crates/matrix-sdk-appservice/src/lib.rs @@ -17,12 +17,13 @@ //! The appservice crate aims to provide a batteries-included experience by //! being a thin wrapper around the [`matrix_sdk`]. That means that we //! -//! * ship with functionality to configure your webserver crate or simply run -//! the webserver for you -//! * receive and validate requests from the homeserver correctly -//! * allow calling the homeserver with proper virtual user identity assertion -//! * have consistent room state by leveraging matrix-sdk's state store -//! * provide E2EE support by leveraging matrix-sdk's crypto store +//! - [x] ship with functionality to configure your webserver crate or simply +//! run the webserver for you +//! - [x] receive and validate requests from the homeserver correctly +//! - [x] allow calling the homeserver with proper virtual user identity +//! assertion +//! - [x] have consistent room state by leveraging matrix-sdk's state store +//! - [ ] provide E2EE support by leveraging matrix-sdk's crypto store //! //! # Status //! @@ -31,13 +32,10 @@ //! //! # Registration //! -//! The crate relies on the registration being always in sync with the actual -//! registration used by the homeserver. That's because it's required for the -//! access tokens and because membership states for virtual users are determined -//! based on the registered namespace. -//! -//! **Note:** Non-exclusive registration namespaces are not yet supported and -//! hence might lead to undefined behavior. +//! The crate relies on the appservice registration being always in sync with +//! the actual registration used by the homeserver. That's because it's required +//! for the access tokens and because membership states for virtual users are +//! determined based on the registered namespaces. //! //! # Quickstart //! @@ -65,9 +63,13 @@ //! ")?; //! //! let mut appservice = AppService::new(homeserver_url, server_name, registration).await?; -//! appservice.register_event_handler(|_ev: SyncRoomMemberEvent| async { -//! // do stuff -//! }); +//! appservice +//! .virtual_user(None) +//! .await? +//! .register_event_handler(|_ev: SyncRoomMemberEvent| async { +//! // do stuff +//! }) +//! .await; //! //! let (host, port) = appservice.registration().get_host_and_port()?; //! appservice.run(host, port).await?; @@ -82,283 +84,48 @@ //! [matrix-org/matrix-rust-sdk#228]: https://github.com/matrix-org/matrix-rust-sdk/issues/228 //! [examples directory]: https://github.com/matrix-org/matrix-rust-sdk/tree/main/crates/matrix-sdk-appservice/examples -use std::{ - convert::{TryFrom, TryInto}, - fs::File, - future::Future, - ops::Deref, - path::PathBuf, - sync::Arc, -}; +use std::{convert::TryInto, sync::Arc}; use dashmap::DashMap; pub use error::Error; use event_handler::AppserviceFn; -use http::Uri; pub use matrix_sdk; #[doc(no_inline)] pub use matrix_sdk::ruma; -use matrix_sdk::{ - bytes::Bytes, - config::RequestConfig, - event_handler::{EventHandler, EventHandlerResult, SyncEvent}, - reqwest::Url, - Client, ClientBuildError, ClientBuilder, Session, -}; -use regex::Regex; +use matrix_sdk::{reqwest::Url, Client, ClientBuilder}; use ruma::{ api::{ appservice::{ event::push_events, query::{query_room_alias::v1 as query_room, query_user_id::v1 as query_user}, - Registration, }, - client::{account::register, session::login, sync::sync_events, uiaa::UserIdentifier}, + client::{account::register, sync::sync_events}, }, assign, events::{room::member::MembershipState, AnyRoomEvent, AnyStateEvent}, - DeviceId, IdParseError, OwnedDeviceId, OwnedRoomId, OwnedServerName, UserId, + DeviceId, IdParseError, OwnedRoomId, OwnedServerName, }; -use serde::{de::DeserializeOwned, Deserialize}; +use serde::Deserialize; use tokio::task::JoinHandle; use tracing::{debug, info, warn}; mod error; pub mod event_handler; +pub mod registration; +pub mod virtual_user; mod webserver; +pub use registration::AppServiceRegistration; +use registration::NamespaceCache; +pub use virtual_user::VirtualUserBuilder; + pub type Result = std::result::Result; -pub type Host = String; -pub type Port = u16; const USER_KEY: &[u8] = b"appservice.users."; pub const USER_MEMBER: &[u8] = b"appservice.users.membership."; -/// Builder for a virtual user -#[derive(Debug)] -pub struct VirtualUserBuilder<'a> { - appservice: &'a AppService, - localpart: &'a str, - device_id: Option, - client_builder: ClientBuilder, - log_in: bool, - restored_session: Option, -} - -impl<'a> VirtualUserBuilder<'a> { - /// Create a new virtual user builder - /// # Arguments - /// - /// * `localpart` - The localpart of the virtual user - pub fn new(appservice: &'a AppService, localpart: &'a str) -> Self { - Self { - appservice, - localpart, - device_id: None, - client_builder: Client::builder(), - log_in: false, - restored_session: None, - } - } - - /// Set the device ID of the virtual user - pub fn device_id(mut self, device_id: Option) -> Self { - self.device_id = device_id; - self - } - - /// Sets the client builder to use for the virtual user - pub fn client_builder(mut self, client_builder: ClientBuilder) -> Self { - self.client_builder = client_builder; - self - } - - /// Log in as the virtual user - /// - /// In some cases it is necessary to log in as the virtual user, such as to - /// upload device keys - pub fn login(mut self) -> Self { - self.log_in = true; - self - } - - /// Restore a persisted session - /// - /// This is primarily useful if you enable - /// [`VirtualUserBuilder::login()`] and want to restore a session - /// from a previous run. - pub fn restored_session(mut self, session: Session) -> Self { - self.restored_session = Some(session); - self - } - - /// Build the virtual user - /// - /// # Errors - /// This function returns an error if an invalid localpart is provided. - pub async fn build(self) -> Result { - if let Some(client) = self.appservice.clients.get(self.localpart) { - return Ok(client.clone()); - } - - let user_id = UserId::parse_with_server_name(self.localpart, &self.appservice.server_name)?; - if !(self.appservice.user_id_is_in_namespace(&user_id) - || self.localpart == self.appservice.registration.sender_localpart) - { - warn!("Virtual client id '{user_id}' is not in the namespace") - } - - let mut builder = self.client_builder; - - if !self.log_in && self.localpart != self.appservice.registration.sender_localpart { - builder = builder.assert_identity(); - } - - let client = builder - .homeserver_url(self.appservice.homeserver_url.clone()) - .appservice_mode() - .build() - .await - .map_err(ClientBuildError::assert_valid_builder_args)?; - - let session = if let Some(session) = self.restored_session { - session - } else if self.log_in && self.localpart != self.appservice.registration.sender_localpart { - self.appservice - .create_session(self.localpart, self.device_id.as_ref().map(|v| v.as_ref()), None) - .await? - } else { - // Don’t log in - Session { - access_token: self.appservice.registration.as_token.clone(), - user_id: user_id.clone(), - device_id: self.device_id.unwrap_or_else(DeviceId::new), - } - }; - - client.restore_login(session).await?; - - self.appservice.clients.insert(self.localpart.to_owned(), client.clone()); - - Ok(client) - } -} - -/// AppService Registration -/// -/// Wrapper around [`Registration`] -#[derive(Debug, Clone)] -pub struct AppServiceRegistration { - inner: Registration, -} - -impl AppServiceRegistration { - /// Try to load registration from yaml string - /// - /// See the fields of [`Registration`] for the required format - pub fn try_from_yaml_str(value: impl AsRef) -> Result { - Ok(Self { inner: serde_yaml::from_str(value.as_ref())? }) - } - - /// Try to load registration from yaml file - /// - /// See the fields of [`Registration`] for the required format - pub fn try_from_yaml_file(path: impl Into) -> Result { - let file = File::open(path.into())?; - - Ok(Self { inner: serde_yaml::from_reader(file)? }) - } - - /// Get the host and port from the registration URL - /// - /// If no port is found it falls back to scheme defaults: 80 for http and - /// 443 for https - pub fn get_host_and_port(&self) -> Result<(Host, Port)> { - let uri = Uri::try_from(&self.inner.url)?; - - let host = uri.host().ok_or(Error::MissingRegistrationHost)?.to_owned(); - let port = match uri.port() { - Some(port) => Ok(port.as_u16()), - None => match uri.scheme_str() { - Some("http") => Ok(80), - Some("https") => Ok(443), - _ => Err(Error::MissingRegistrationPort), - }, - }?; - - Ok((host, port)) - } -} - -impl From for AppServiceRegistration { - fn from(value: Registration) -> Self { - Self { inner: value } - } -} - -impl Deref for AppServiceRegistration { - type Target = Registration; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -/// Cache data for the registration namespaces. -#[derive(Debug, Clone)] -pub struct NamespaceCache { - /// List of user regexes in our namespace - users: Vec, - /// List of alias regexes in our namespace - #[allow(dead_code)] - aliases: Vec, - /// List of room id regexes in our namespace - #[allow(dead_code)] - rooms: Vec, -} - -impl NamespaceCache { - /// Creates a new registration cache from a [`Registration`] value - pub fn from_registration(registration: &Registration) -> Result { - let users = registration - .namespaces - .users - .iter() - .map(|user| Regex::new(&user.regex)) - .collect::, _>>()?; - let aliases = registration - .namespaces - .aliases - .iter() - .map(|user| Regex::new(&user.regex)) - .collect::, _>>()?; - let rooms = registration - .namespaces - .rooms - .iter() - .map(|user| Regex::new(&user.regex)) - .collect::, _>>()?; - Ok(NamespaceCache { users, aliases, rooms }) - } -} - type Localpart = String; -/// The `localpart` of the user associated with the application service via -/// `sender_localpart` in [`AppServiceRegistration`]. -/// -/// Dummy type for shared documentation -#[allow(dead_code)] -pub type MainUser = (); - -/// The application service may specify the virtual user to act as through use -/// of a user_id query string parameter on the request. The user specified in -/// the query string must be covered by one of the [`AppServiceRegistration`]'s -/// `users` namespaces. -/// -/// Dummy type for shared documentation -pub type VirtualUser = (); - /// AppService #[derive(Debug, Clone)] pub struct AppService { @@ -371,11 +138,13 @@ pub struct AppService { } impl AppService { - /// Create new AppService + /// Create a new AppService. /// - /// Also creates and caches a [`Client`] for the [`MainUser`]. - /// A default [`ClientBuilder`] is used, if you want to customize it - /// use [`with_client_builder()`][Self::with_client_builder] instead. + /// This will also construct a [`virtual_user()`][Self::virtual_user] for + /// the `sender_localpart` of the given registration. This virtual user can + /// be used to register an event handler for all incoming events. Other + /// virtual users only receive events if they're known to be a member of a + /// room. /// /// # Arguments /// @@ -399,7 +168,7 @@ impl AppService { } /// Same as [`new()`][Self::new] but lets you provide a [`ClientBuilder`] - /// for the [`Client`] + /// for the virtual user that gets constructed for the `sender_localpart`. pub async fn with_client_builder( homeserver_url: impl TryInto, server_name: impl TryInto, @@ -423,141 +192,59 @@ impl AppService { event_handler, }; - // we create and cache the [`MainUser`] by default appservice.virtual_user_builder(&sender_localpart).client_builder(builder).build().await?; Ok(appservice) } - /// Create a [`Client`] for the given [`VirtualUser`]'s `localpart` + /// Create a virtual user client. /// - /// Will create and return a [`Client`] that's configured to [assert the - /// identity] on all outgoing homeserver requests if `localpart` is - /// given. + /// Will create and return a client that's configured to [assert the + /// identity] on outgoing homeserver requests that need authentication. /// /// This method is a singleton that saves the client internally for re-use - /// based on the `localpart`. The cached [`Client`] can be retrieved either - /// by calling this method again or by calling - /// [`get_cached_client()`][Self::get_cached_client] which is non-async - /// convenience wrapper. + /// based on the `localpart`. The cached client can be retrieved by calling + /// this method again. /// /// Note that if you want to do actions like joining rooms with a virtual - /// user it needs to be registered first. `Self::register_virtual_user()` - /// can be used for that purpose. + /// user it needs to be registered first. + /// [`register_virtual_user()`][Self::register_virtual_user] can be used + /// for that purpose. /// /// # Arguments /// - /// * `localpart` - The localpart of the user we want assert our identity to + /// * `localpart` - Used for constructing the virtual user accordingly. If + /// `None` is given it uses the `sender_localpart` from the registration. /// /// [registration]: https://matrix.org/docs/spec/application_service/r0.1.2#registration /// [assert the identity]: https://matrix.org/docs/spec/application_service/r0.1.2#identity-assertion - pub async fn virtual_user_client(&self, localpart: impl AsRef) -> Result { - self.virtual_user_builder(localpart.as_ref()).build().await + pub async fn virtual_user(&self, localpart: Option<&str>) -> Result { + let localpart = localpart.unwrap_or_else(|| self.registration.sender_localpart.as_ref()); + self.virtual_user_builder(localpart).build().await } - /// Same as [`virtual_user_client()`][Self::virtual_user_client] but with - /// the ability to pass in a [`ClientBuilder`] + /// Same as [`virtual_user()`][Self::virtual_user] but with + /// the ability to pass in a [`ClientBuilder`]. /// /// Since this method is a singleton follow-up calls with different /// [`ClientBuilder`]s will be ignored. - pub async fn virtual_user_client_with_client_builder( + pub async fn virtual_user_with_client_builder( &self, - localpart: impl AsRef, + localpart: Option<&str>, builder: ClientBuilder, ) -> Result { - self.virtual_user_builder(localpart.as_ref()).client_builder(builder).build().await + let localpart = localpart.unwrap_or_else(|| self.registration.sender_localpart.as_ref()); + self.virtual_user_builder(localpart).client_builder(builder).build().await } + /// Create a new virtual user builder for the given `localpart`. pub fn virtual_user_builder<'a>(&'a self, localpart: &'a str) -> VirtualUserBuilder<'a> { VirtualUserBuilder::new(self, localpart) } - /// Create a session using appservice login for a virtual user. - async fn create_session( - &self, - user: impl AsRef, - device_id: Option<&str>, - initial_device_display_name: Option<&str>, - ) -> Result { - let homeserver = self.homeserver_url.clone(); - info!(homeserver = homeserver.as_str(), user = user.as_ref(), "Logging in as virtual user"); - - let login_info = login::v3::LoginInfo::ApplicationService( - login::v3::ApplicationService::new(UserIdentifier::UserIdOrLocalpart(user.as_ref())), - ); - - let request = assign!(login::v3::Request::new(login_info), { - device_id: device_id.map(|d| d.into()), - initial_device_display_name - }); - - let response = self - .get_cached_client(None)? - .send(request, Some(RequestConfig::short_retry().force_auth())) - .await?; - - Ok(Session { - access_token: response.access_token, - user_id: response.user_id, - device_id: response.device_id, - }) - } - - /// Get cached [`Client`] - /// - /// Will return the client for the given `localpart` if previously - /// constructed with [`virtual_user_client()`][Self::virtual_user_client] or - /// [`virtual_user_client_with_config()`][Self:: - /// virtual_user_client_with_client_builder]. - /// - /// If no `localpart` is given it assumes the [`MainUser`]'s `localpart`. If - /// no client for `localpart` is found it will return an Error. - pub fn get_cached_client(&self, localpart: Option<&str>) -> Result { - let localpart = localpart.unwrap_or_else(|| self.registration.sender_localpart.as_ref()); - - let entry = self.clients.get(localpart).ok_or(Error::NoClientForLocalpart)?; - - Ok(entry.value().clone()) - } - - /// Convenience wrapper around [`Client::register_event_handler()`] that - /// attaches the event handler to the [`MainUser`]'s [`Client`] - /// - /// Note that the event handler in the [`AppService`] context only triggers - /// [`join` room `timeline` events], so no state events or events from the - /// `invite`, `knock` or `leave` scope. The rationale behind that is - /// that incoming AppService transactions from the homeserver are not - /// necessarily bound to a specific user but can cover a multitude of - /// namespaces, and as such the AppService basically only "observes - /// joined rooms". Also currently homeservers only push PDUs to appservices, - /// no EDUs. There's the open [MSC2409] regarding supporting EDUs in the - /// future, though it seems to be planned to put EDUs into a different - /// JSON key than `events` to stay backwards compatible. - /// - /// [`join` room `timeline` events]: https://spec.matrix.org/unstable/client-server-api/#get_matrixclientr0sync - /// [MSC2409]: https://github.com/matrix-org/matrix-doc/pull/2409 - pub async fn register_event_handler(&self, handler: H) -> Result<&Self> - where - Ev: SyncEvent + DeserializeOwned + Send + 'static, - H: EventHandler, - ::Output: EventHandlerResult, - { - let client = self.get_cached_client(None)?; - client.register_event_handler(handler).await; - - Ok(self) - } - - /// Convenience wrapper around [`Client::register_event_handler_context`] - /// attaches the event handler context to the [`MainUser`]'s [`Client`]. - pub fn register_event_handler_context(&self, ctx: T) -> Result<&Self> - where - T: Clone + Send + Sync + 'static, - { - let client = self.get_cached_client(None)?; - client.register_event_handler_context(ctx); - - Ok(self) + /// Get the map containing all constructed virtual user clients. + pub fn virtual_users(&self) -> Arc> { + self.clients.clone() } /// Register a responder for queries about the existence of a user with a @@ -605,35 +292,40 @@ impl AppService { } /// Register a virtual user by sending a [`register::v3::Request`] to the - /// homeserver + /// homeserver. /// /// # Arguments /// /// * `localpart` - The localpart of the user to register. Must be covered - /// by the namespaces in the [`Registration`] in order to succeed. + /// by the namespaces in the registration in order to succeed. /// /// # Returns /// This function may return a UIAA response, which should be checked for /// with [`Error::uiaa_response()`]. - pub async fn register_virtual_user(&self, localpart: impl AsRef) -> Result<()> { - if self.is_user_registered(localpart.as_ref()).await? { + pub async fn register_virtual_user<'a>( + &self, + localpart: &'a str, + device_id: Option<&'a DeviceId>, + ) -> Result<()> { + if self.is_user_registered(localpart).await? { return Ok(()); } let request = assign!(register::v3::Request::new(), { - username: Some(localpart.as_ref()), + username: Some(localpart), login_type: Some(®ister::LoginType::ApplicationService), + device_id, }); - let client = self.get_cached_client(None)?; + let client = self.virtual_user(None).await?; client.register(request).await?; - self.set_user_registered(localpart.as_ref()).await?; + self.set_user_registered(localpart).await?; Ok(()) } /// Add the given localpart to the database of registered localparts. async fn set_user_registered(&self, localpart: impl AsRef) -> Result<()> { - let client = self.get_cached_client(None)?; + let client = self.virtual_user(None).await?; client .store() .set_custom_value( @@ -646,7 +338,7 @@ impl AppService { /// Get whether a localpart is listed in the database as registered. async fn is_user_registered(&self, localpart: impl AsRef) -> Result { - let client = self.get_cached_client(None)?; + let client = self.virtual_user(None).await?; let key = [USER_KEY, localpart.as_ref().as_bytes()].concat(); let store = client.store().get_custom_value(&key).await?; let registered = @@ -654,14 +346,12 @@ impl AppService { Ok(registered) } - /// Get the AppService [registration] - /// - /// [registration]: https://matrix.org/docs/spec/application_service/r0.1.2#registration + /// Get the [`AppServiceRegistration`]. pub fn registration(&self) -> &AppServiceRegistration { &self.registration } - /// Compare the given `hs_token` against `registration.hs_token` + /// Compare the given `hs_token` against the registration's `hs_token`. /// /// Returns `true` if the tokens match, `false` otherwise. pub fn compare_hs_token(&self, hs_token: impl AsRef) -> bool { @@ -680,12 +370,16 @@ impl AppService { false } - /// Returns a [`warp::Filter`] to be used as [`warp::serve()`] route + /// Returns a [`warp::Filter`] to be used as [`warp::serve()`] route. /// /// Note that if you handle any of the [application-service-specific /// routes], including the legacy routes, you will break the appservice /// functionality. /// + /// Hint: [`warp::Filter`]s can be converted to an `hyper::Service` using + /// [`warp::service`], which allows using it with tower-compatible + /// frameworks such as axum. + /// /// [application-service-specific routes]: https://spec.matrix.org/unstable/application-service-api/#legacy-routes pub fn warp_filter(&self) -> warp::filters::BoxedFilter<(impl warp::Reply,)> { webserver::warp_filter(self.clone()) @@ -695,11 +389,11 @@ impl AppService { /// active virtual clients. /// /// [transaction]: https://spec.matrix.org/v1.2/application-service-api/#put_matrixappv1transactionstxnid - pub async fn receive_transaction( + async fn receive_transaction( &self, transaction: push_events::v1::IncomingRequest, ) -> Result<()> { - let client = self.get_cached_client(None)?; + let sender_localpart_client = self.virtual_user(None).await?; // Find membership events affecting members in our namespace, and update // membership accordingly @@ -712,7 +406,7 @@ impl AppService { continue; } let localpart = event.state_key().localpart(); - client + sender_localpart_client .store() .set_custom_value( &[USER_MEMBER, event.room_id().as_bytes(), b".", localpart.as_bytes()].concat(), @@ -730,24 +424,27 @@ impl AppService { // Spawn a task for each client that constructs and pushes a sync event let mut tasks: Vec> = Vec::new(); let transaction = Arc::new(transaction); - for virt_client in self.clients.iter() { - let client = client.clone(); - let virt_client = virt_client.clone(); + for virtual_user_client in self.clients.iter() { + let client = sender_localpart_client.clone(); + let virtual_user_client = virtual_user_client.clone(); let transaction = transaction.clone(); - let appserv_uid = self.registration.sender_localpart.clone(); + let sender_localpart = self.registration.sender_localpart.clone(); let task = tokio::spawn(async move { - let user_id = match virt_client.user_id() { + let virtual_user_localpart = match virtual_user_client.user_id() { Some(user_id) => user_id.localpart(), // The client is not logged in, skipping None => return Ok(()), }; let mut response = sync_events::v3::Response::new(transaction.txn_id.to_string()); - // Clients expect events to be grouped per room, where the group also denotes - // what the client's membership of the given room is. We take the - // all the events in the transaction and sort them into appropriate - // groups, falling back to a membership of "join" if it's unknown. + // Clients expect events to be grouped per room, where the + // group also denotes what the client's membership of the given + // room is. We take all the events in the transaction and sort + // them into appropriate groups. + // + // We special-case the `sender_localpart` user which receives all events and + // by falling back to a membership of "join" if it's unknown. for raw_event in &transaction.events { let room_id = match raw_event.deserialize_as::()?.room_id { Some(room_id) => room_id, @@ -756,11 +453,15 @@ impl AppService { continue; } }; - let key = &[USER_MEMBER, room_id.as_bytes(), b".", user_id.as_bytes()].concat(); + let key = + &[USER_MEMBER, room_id.as_bytes(), b".", virtual_user_localpart.as_bytes()] + .concat(); let membership = match client.store().get_custom_value(key).await? { Some(value) => String::from_utf8(value).ok().map(MembershipState::from), - // Assume the appservice is in every known room - None if user_id == appserv_uid => Some(MembershipState::Join), + // Assume the `sender_localpart` user is in every known room + None if virtual_user_localpart == sender_localpart => { + Some(MembershipState::Join) + } None => None, }; @@ -780,10 +481,10 @@ impl AppService { response.rooms.invite.entry(room_id).or_default(); } Some(unknown) => debug!("Unknown membership type: {unknown}"), - None => debug!("Assuming {user_id} is not in {room_id}"), + None => debug!("Assuming {virtual_user_localpart} is not in {room_id}"), } } - virt_client.receive_transaction(&transaction.txn_id, response).await?; + virtual_user_client.receive_transaction(&transaction.txn_id, response).await?; Ok::<_, Error>(()) }); @@ -797,11 +498,10 @@ impl AppService { Ok(()) } - /// Convenience method that runs an http server depending on the selected - /// server feature + /// Convenience method that runs an http server. /// /// This is a blocking call that tries to listen on the provided host and - /// port + /// port. pub async fn run(&self, host: impl Into, port: impl Into) -> Result<()> { let host = host.into(); let port = port.into(); @@ -812,66 +512,517 @@ impl AppService { } } -/// Ruma always expects the path to start with `/_matrix`, so we transform -/// accordingly. Handles [legacy routes] and appservice being located on a sub -/// path. -/// -/// [legacy routes]: https://matrix.org/docs/spec/application_service/r0.1.2#legacy-routes -// TODO: consider ruma PR -pub(crate) fn transform_request_path( - mut request: http::Request, -) -> Result> { - let uri = request.uri(); - // remove trailing slash from path - let path = uri.path().trim_end_matches('/').to_owned(); +#[cfg(test)] +mod tests { + use std::{ + future, + sync::{Arc, Mutex}, + }; - if !path.starts_with("/_matrix/app/v1/") { - let path = match path { - // special-case paths without value at the end - _ if path.ends_with("/_matrix/app/unstable/thirdparty/user") => { - "/_matrix/app/v1/thirdparty/user".to_owned() - } - _ if path.ends_with("/_matrix/app/unstable/thirdparty/location") => { - "/_matrix/app/v1/thirdparty/location".to_owned() - } - // regular paths with values at the end - _ => { - let mut path = path.split('/').into_iter().rev(); - let value = match path.next() { - Some(value) => value, - None => return Err(Error::UriEmptyPath), - }; + use matrix_sdk::{ + config::RequestConfig, + ruma::{api::appservice::Registration, events::room::member::OriginalSyncRoomMemberEvent}, + Client, + }; + use matrix_sdk_test::{appservice::TransactionBuilder, async_test, TimelineTestEvent}; + use ruma::{ + api::{appservice::event::push_events, MatrixVersion}, + events::AnyRoomEvent, + room_id, + serde::Raw, + }; + use serde_json::json; + use warp::{Filter, Reply}; + use wiremock::{ + matchers::{body_json, header, method, path}, + Mock, MockServer, ResponseTemplate, + }; - let mut path = match path.next() { - Some(path_segment) - if ["transactions", "users", "rooms"].contains(&path_segment) => - { - format!("/_matrix/app/v1/{}/{}", path_segment, value) - } - Some(path_segment) => match path.next() { - Some(path_segment2) if path_segment2 == "thirdparty" => { - format!("/_matrix/app/v1/thirdparty/{}/{}", path_segment, value) - } - _ => return Err(Error::UriPathUnknown), - }, - None => return Err(Error::UriEmptyPath), - }; + use super::*; - if let Some(query) = uri.query() { - path.push('?'); - path.push_str(query); - } - - path - } - }; - - let mut parts = uri.clone().into_parts(); - parts.path_and_query = Some(path.parse()?); - - let uri = parts.try_into().map_err(http::Error::from)?; - *request.uri_mut() = uri; + fn registration_string() -> String { + include_str!("../tests/registration.yaml").to_owned() } - Ok(request) + async fn appservice( + homeserver_url: Option, + registration: Option, + ) -> Result { + let _ = tracing_subscriber::fmt::try_init(); + + let registration = match registration { + Some(registration) => registration.into(), + None => AppServiceRegistration::try_from_yaml_str(registration_string()).unwrap(), + }; + + let homeserver_url = homeserver_url.unwrap_or_else(|| "http://localhost:1234".to_owned()); + let server_name = "localhost"; + + let client_builder = Client::builder() + .request_config(RequestConfig::default().disable_retry()) + .server_versions([MatrixVersion::V1_0]); + + AppService::with_client_builder( + homeserver_url.as_ref(), + server_name, + registration, + client_builder, + ) + .await + } + + #[async_test] + async fn test_register_virtual_user() -> Result<()> { + let server = MockServer::start().await; + let appservice = appservice(Some(server.uri()), None).await?; + + let localpart = "someone"; + Mock::given(method("POST")) + .and(path("/_matrix/client/r0/register")) + .and(header( + "authorization", + format!("Bearer {}", appservice.registration().as_token).as_str(), + )) + .and(body_json(json!({ + "username": localpart.to_owned(), + "type": "m.login.application_service" + }))) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "access_token": "abc123", + "device_id": "GHTYAJCE", + "user_id": format!("@{localpart}:localhost"), + }))) + .mount(&server) + .await; + + appservice.register_virtual_user(localpart, None).await?; + + Ok(()) + } + + #[async_test] + async fn test_put_transaction() -> Result<()> { + let uri = "/_matrix/app/v1/transactions/1?access_token=hs_token"; + + let mut transaction_builder = TransactionBuilder::new(); + transaction_builder.add_room_event(TimelineTestEvent::Member); + let transaction = transaction_builder.build_json_transaction(); + + let appservice = appservice(None, None).await?; + + let status = warp::test::request() + .method("PUT") + .path(uri) + .json(&transaction) + .filter(&appservice.warp_filter()) + .await + .unwrap() + .into_response() + .status(); + + assert_eq!(status, 200); + + Ok(()) + } + + #[async_test] + async fn test_put_transaction_with_repeating_txn_id() -> Result<()> { + let uri = "/_matrix/app/v1/transactions/1?access_token=hs_token"; + + let mut transaction_builder = TransactionBuilder::new(); + transaction_builder.add_room_event(TimelineTestEvent::Member); + let transaction = transaction_builder.build_json_transaction(); + + let appservice = appservice(None, None).await?; + + #[allow(clippy::mutex_atomic)] + let on_state_member = Arc::new(Mutex::new(false)); + appservice + .virtual_user(None) + .await? + .register_event_handler({ + let on_state_member = on_state_member.clone(); + move |_ev: OriginalSyncRoomMemberEvent| { + *on_state_member.lock().unwrap() = true; + future::ready(()) + } + }) + .await; + + let status = warp::test::request() + .method("PUT") + .path(uri) + .json(&transaction) + .filter(&appservice.warp_filter()) + .await + .unwrap() + .into_response() + .status(); + + assert_eq!(status, 200); + { + let on_room_member_called = *on_state_member.lock().unwrap(); + assert!(on_room_member_called); + } + + // Reset this to check that next time it doesnt get called + { + let mut on_room_member_called = on_state_member.lock().unwrap(); + *on_room_member_called = false; + } + + let status = warp::test::request() + .method("PUT") + .path(uri) + .json(&transaction) + .filter(&appservice.warp_filter()) + .await + .unwrap() + .into_response() + .status(); + + // According to https://spec.matrix.org/v1.2/application-service-api/#pushing-events + // This should noop and return 200. + assert_eq!(status, 200); + { + let on_room_member_called = *on_state_member.lock().unwrap(); + // This time we should not have called the event handler. + assert!(!on_room_member_called); + } + + Ok(()) + } + + #[async_test] + async fn test_get_user() -> Result<()> { + let appservice = appservice(None, None).await?; + appservice.register_user_query(Box::new(|_, _| Box::pin(async move { true }))).await; + + let uri = "/_matrix/app/v1/users/%40_botty_1%3Adev.famedly.local?access_token=hs_token"; + + let status = warp::test::request() + .method("GET") + .path(uri) + .filter(&appservice.warp_filter()) + .await + .unwrap() + .into_response() + .status(); + + assert_eq!(status, 200); + + Ok(()) + } + + #[async_test] + async fn test_get_room() -> Result<()> { + let appservice = appservice(None, None).await?; + appservice.register_room_query(Box::new(|_, _| Box::pin(async move { true }))).await; + + let uri = "/_matrix/app/v1/rooms/%23magicforest%3Aexample.com?access_token=hs_token"; + + let status = warp::test::request() + .method("GET") + .path(uri) + .filter(&appservice.warp_filter()) + .await + .unwrap() + .into_response() + .status(); + + assert_eq!(status, 200); + + Ok(()) + } + + #[async_test] + async fn test_invalid_access_token() -> Result<()> { + let uri = "/_matrix/app/v1/transactions/1?access_token=invalid_token"; + + let mut transaction_builder = TransactionBuilder::new(); + let transaction = + transaction_builder.add_room_event(TimelineTestEvent::Member).build_json_transaction(); + + let appservice = appservice(None, None).await?; + + let status = warp::test::request() + .method("PUT") + .path(uri) + .json(&transaction) + .filter(&appservice.warp_filter()) + .await + .unwrap() + .into_response() + .status(); + + assert_eq!(status, 401); + + Ok(()) + } + + #[async_test] + async fn test_no_access_token() -> Result<()> { + let uri = "/_matrix/app/v1/transactions/1"; + + let mut transaction_builder = TransactionBuilder::new(); + transaction_builder.add_room_event(TimelineTestEvent::Member); + let transaction = transaction_builder.build_json_transaction(); + + let appservice = appservice(None, None).await?; + + { + let status = warp::test::request() + .method("PUT") + .path(uri) + .json(&transaction) + .filter(&appservice.warp_filter()) + .await + .unwrap() + .into_response() + .status(); + + assert_eq!(status, 401); + } + + Ok(()) + } + + #[async_test] + async fn test_event_handler() -> Result<()> { + let appservice = appservice(None, None).await?; + + #[allow(clippy::mutex_atomic)] + let on_state_member = Arc::new(Mutex::new(false)); + appservice + .virtual_user(None) + .await? + .register_event_handler({ + let on_state_member = on_state_member.clone(); + move |_ev: OriginalSyncRoomMemberEvent| { + *on_state_member.lock().unwrap() = true; + future::ready(()) + } + }) + .await; + + let uri = "/_matrix/app/v1/transactions/1?access_token=hs_token"; + + let mut transaction_builder = TransactionBuilder::new(); + transaction_builder.add_room_event(TimelineTestEvent::Member); + let transaction = transaction_builder.build_json_transaction(); + + warp::test::request() + .method("PUT") + .path(uri) + .json(&transaction) + .filter(&appservice.warp_filter()) + .await + .unwrap(); + + let on_room_member_called = *on_state_member.lock().unwrap(); + assert!(on_room_member_called); + + Ok(()) + } + + #[async_test] + async fn test_unrelated_path() -> Result<()> { + let appservice = appservice(None, None).await?; + + let status = { + let consumer_filter = warp::any() + .and(appservice.warp_filter()) + .or(warp::get().and(warp::path("unrelated").map(warp::reply))); + + let response = warp::test::request() + .method("GET") + .path("/unrelated") + .filter(&consumer_filter) + .await? + .into_response(); + + response.status() + }; + + assert_eq!(status, 200); + + Ok(()) + } + + #[async_test] + async fn test_appservice_on_sub_path() -> Result<()> { + let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost"); + let uri_1 = "/sub_path/_matrix/app/v1/transactions/1?access_token=hs_token"; + let uri_2 = "/sub_path/_matrix/app/v1/transactions/2?access_token=hs_token"; + + let mut transaction_builder = TransactionBuilder::new(); + transaction_builder.add_room_event(TimelineTestEvent::Member); + let transaction_1 = transaction_builder.build_json_transaction(); + + let mut transaction_builder = TransactionBuilder::new(); + transaction_builder.add_room_event(TimelineTestEvent::MemberNameChange); + let transaction_2 = transaction_builder.build_json_transaction(); + + let appservice = appservice(None, None).await?; + + { + warp::test::request() + .method("PUT") + .path(uri_1) + .json(&transaction_1) + .filter(&warp::path("sub_path").and(appservice.warp_filter())) + .await?; + + warp::test::request() + .method("PUT") + .path(uri_2) + .json(&transaction_2) + .filter(&warp::path("sub_path").and(appservice.warp_filter())) + .await?; + }; + + let members = appservice + .virtual_user(None) + .await? + .get_room(room_id) + .expect("Expected room to be available") + .members_no_sync() + .await?; + + assert_eq!(members[0].display_name().unwrap(), "changed"); + + Ok(()) + } + + #[async_test] + async fn test_receive_transaction() -> Result<()> { + tracing_subscriber::fmt().try_init().ok(); + let json = vec![ + Raw::new(&json!({ + "content": { + "avatar_url": null, + "displayname": "Appservice", + "membership": "join" + }, + "event_id": "$151800140479rdvjg:localhost", + "membership": "join", + "origin_server_ts": 151800140, + "sender": "@_appservice:localhost", + "state_key": "@_appservice:localhost", + "type": "m.room.member", + "room_id": "!coolplace:localhost", + "unsigned": { + "age": 2970366 + } + }))? + .cast::(), + Raw::new(&json!({ + "content": { + "avatar_url": null, + "displayname": "Appservice", + "membership": "join" + }, + "event_id": "$151800140491rfbja:localhost", + "membership": "join", + "origin_server_ts": 151800140, + "sender": "@_appservice:localhost", + "state_key": "@_appservice:localhost", + "type": "m.room.member", + "room_id": "!boringplace:localhost", + "unsigned": { + "age": 2970366 + } + }))? + .cast::(), + Raw::new(&json!({ + "content": { + "avatar_url": null, + "displayname": "Alice", + "membership": "join" + }, + "event_id": "$151800140517rfvjc:localhost", + "membership": "join", + "origin_server_ts": 151800140, + "sender": "@_appservice_alice:localhost", + "state_key": "@_appservice_alice:localhost", + "type": "m.room.member", + "room_id": "!coolplace:localhost", + "unsigned": { + "age": 2970366 + } + }))? + .cast::(), + Raw::new(&json!({ + "content": { + "avatar_url": null, + "displayname": "Bob", + "membership": "invite" + }, + "event_id": "$151800140594rfvjc:localhost", + "membership": "invite", + "origin_server_ts": 151800174, + "sender": "@_appservice_bob:localhost", + "state_key": "@_appservice_bob:localhost", + "type": "m.room.member", + "room_id": "!boringplace:localhost", + "unsigned": { + "age": 2970366 + } + }))? + .cast::(), + ]; + let appservice = appservice(None, None).await?; + + let alice = appservice.virtual_user(Some("_appservice_alice")).await?; + let bob = appservice.virtual_user(Some("_appservice_bob")).await?; + appservice + .receive_transaction(push_events::v1::IncomingRequest::new("dontcare".into(), json)) + .await?; + let coolplace = room_id!("!coolplace:localhost"); + let boringplace = room_id!("!boringplace:localhost"); + assert!( + alice.get_joined_room(coolplace).is_some(), + "Alice's membership in coolplace should be join" + ); + assert!( + bob.get_invited_room(boringplace).is_some(), + "Bob's membership in boringplace should be invite" + ); + assert!(alice.get_room(boringplace).is_none(), "Alice should not know about boringplace"); + assert!(bob.get_room(coolplace).is_none(), "Bob should not know about coolplace"); + Ok(()) + } + + mod registration { + use super::*; + + #[test] + fn test_registration() -> Result<()> { + let registration: Registration = serde_yaml::from_str(®istration_string())?; + let registration: AppServiceRegistration = registration.into(); + + assert_eq!(registration.id, "appservice"); + + Ok(()) + } + + #[test] + fn test_registration_from_yaml_file() -> Result<()> { + let registration = + AppServiceRegistration::try_from_yaml_file("./tests/registration.yaml")?; + + assert_eq!(registration.id, "appservice"); + + Ok(()) + } + + #[test] + fn test_registration_from_yaml_str() -> Result<()> { + let registration = AppServiceRegistration::try_from_yaml_str(registration_string())?; + + assert_eq!(registration.id, "appservice"); + + Ok(()) + } + } } diff --git a/crates/matrix-sdk-appservice/src/registration.rs b/crates/matrix-sdk-appservice/src/registration.rs new file mode 100644 index 000000000..2c581d28f --- /dev/null +++ b/crates/matrix-sdk-appservice/src/registration.rs @@ -0,0 +1,124 @@ +// Copyright 2022 Famedly GmbH +// +// 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. + +//! AppService Registration. + +use std::{convert::TryFrom, fs::File, ops::Deref, path::PathBuf}; + +use http::Uri; +use regex::Regex; +use ruma::api::appservice::Registration; + +use crate::{Error, Result}; + +pub type Host = String; +pub type Port = u16; + +/// AppService Registration +/// +/// Wrapper around [`Registration`]. See also . +#[derive(Debug, Clone)] +pub struct AppServiceRegistration { + inner: Registration, +} + +impl AppServiceRegistration { + /// Try to load registration from yaml string + /// + /// See the fields of [`Registration`] for the required format + pub fn try_from_yaml_str(value: impl AsRef) -> Result { + Ok(Self { inner: serde_yaml::from_str(value.as_ref())? }) + } + + /// Try to load registration from yaml file + /// + /// See the fields of [`Registration`] for the required format + pub fn try_from_yaml_file(path: impl Into) -> Result { + let file = File::open(path.into())?; + + Ok(Self { inner: serde_yaml::from_reader(file)? }) + } + + /// Get the host and port from the registration URL + /// + /// If no port is found it falls back to scheme defaults: 80 for http and + /// 443 for https + pub fn get_host_and_port(&self) -> Result<(Host, Port)> { + let uri = Uri::try_from(&self.inner.url)?; + + let host = uri.host().ok_or(Error::MissingRegistrationHost)?.to_owned(); + let port = match uri.port() { + Some(port) => Ok(port.as_u16()), + None => match uri.scheme_str() { + Some("http") => Ok(80), + Some("https") => Ok(443), + _ => Err(Error::MissingRegistrationPort), + }, + }?; + + Ok((host, port)) + } +} + +impl From for AppServiceRegistration { + fn from(value: Registration) -> Self { + Self { inner: value } + } +} + +impl Deref for AppServiceRegistration { + type Target = Registration; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +/// Cache data for the registration namespaces. +#[derive(Debug, Clone)] +pub struct NamespaceCache { + /// List of user regexes in our namespace + pub(crate) users: Vec, + /// List of alias regexes in our namespace + #[allow(dead_code)] + aliases: Vec, + /// List of room id regexes in our namespace + #[allow(dead_code)] + rooms: Vec, +} + +impl NamespaceCache { + /// Creates a new registration cache from a [`Registration`] value + pub fn from_registration(registration: &Registration) -> Result { + let users = registration + .namespaces + .users + .iter() + .map(|user| Regex::new(&user.regex)) + .collect::, _>>()?; + let aliases = registration + .namespaces + .aliases + .iter() + .map(|user| Regex::new(&user.regex)) + .collect::, _>>()?; + let rooms = registration + .namespaces + .rooms + .iter() + .map(|user| Regex::new(&user.regex)) + .collect::, _>>()?; + Ok(NamespaceCache { users, aliases, rooms }) + } +} diff --git a/crates/matrix-sdk-appservice/src/virtual_user.rs b/crates/matrix-sdk-appservice/src/virtual_user.rs new file mode 100644 index 000000000..ec49e61c9 --- /dev/null +++ b/crates/matrix-sdk-appservice/src/virtual_user.rs @@ -0,0 +1,149 @@ +// Copyright 2022 Famedly GmbH +// +// 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. + +//! Virtual users. + +use matrix_sdk::{config::RequestConfig, Client, ClientBuildError, ClientBuilder, Session}; +use ruma::{ + api::client::{session::login, uiaa::UserIdentifier}, + assign, DeviceId, OwnedDeviceId, UserId, +}; +use tracing::warn; + +use crate::{AppService, Result}; + +/// Builder for a virtual user +#[derive(Debug)] +pub struct VirtualUserBuilder<'a> { + appservice: &'a AppService, + localpart: &'a str, + device_id: Option, + client_builder: ClientBuilder, + log_in: bool, + restored_session: Option, +} + +impl<'a> VirtualUserBuilder<'a> { + /// Create a new virtual user builder + /// # Arguments + /// + /// * `localpart` - The localpart of the virtual user + pub fn new(appservice: &'a AppService, localpart: &'a str) -> Self { + Self { + appservice, + localpart, + device_id: None, + client_builder: Client::builder(), + log_in: false, + restored_session: None, + } + } + + /// Set the device ID of the virtual user + pub fn device_id(mut self, device_id: Option) -> Self { + self.device_id = device_id; + self + } + + /// Sets the client builder to use for the virtual user + pub fn client_builder(mut self, client_builder: ClientBuilder) -> Self { + self.client_builder = client_builder; + self + } + + /// Log in as the virtual user + /// + /// In some cases it is necessary to log in as the virtual user, such as to + /// upload device keys + pub fn login(mut self) -> Self { + self.log_in = true; + self + } + + /// Restore a persisted session + /// + /// This is primarily useful if you enable + /// [`VirtualUserBuilder::login()`] and want to restore a session + /// from a previous run. + pub fn restored_session(mut self, session: Session) -> Self { + self.restored_session = Some(session); + self + } + + /// Build the virtual user + /// + /// # Errors + /// This function returns an error if an invalid localpart is provided. + pub async fn build(self) -> Result { + if let Some(client) = self.appservice.clients.get(self.localpart) { + return Ok(client.clone()); + } + + let user_id = UserId::parse_with_server_name(self.localpart, &self.appservice.server_name)?; + if !(self.appservice.user_id_is_in_namespace(&user_id) + || self.localpart == self.appservice.registration.sender_localpart) + { + warn!("Virtual client id '{user_id}' is not in the namespace") + } + + let mut builder = self.client_builder; + + if !self.log_in && self.localpart != self.appservice.registration.sender_localpart { + builder = builder.assert_identity(); + } + + let client = builder + .homeserver_url(self.appservice.homeserver_url.clone()) + .appservice_mode() + .build() + .await + .map_err(ClientBuildError::assert_valid_builder_args)?; + + let session = if let Some(session) = self.restored_session { + session + } else if self.log_in && self.localpart != self.appservice.registration.sender_localpart { + let login_info = + login::v3::LoginInfo::ApplicationService(login::v3::ApplicationService::new( + UserIdentifier::UserIdOrLocalpart(self.localpart), + )); + + let request = assign!(login::v3::Request::new(login_info), { + device_id: self.device_id.as_ref().map(|v| v.as_ref()), + initial_device_display_name: None, + }); + + let response = + client.send(request, Some(RequestConfig::short_retry().force_auth())).await?; + + Session { + access_token: response.access_token, + user_id: response.user_id, + device_id: response.device_id, + } + } else { + // Don’t log in + Session { + access_token: self.appservice.registration.as_token.clone(), + user_id: user_id.clone(), + device_id: self.device_id.unwrap_or_else(DeviceId::new), + } + }; + + client.restore_login(session).await?; + + self.appservice.clients.insert(self.localpart.to_owned(), client.clone()); + + Ok(client) + } +} diff --git a/crates/matrix-sdk-appservice/src/webserver.rs b/crates/matrix-sdk-appservice/src/webserver.rs index e649b6f53..9e29a08a9 100644 --- a/crates/matrix-sdk-appservice/src/webserver.rs +++ b/crates/matrix-sdk-appservice/src/webserver.rs @@ -107,10 +107,11 @@ mod filters { warp::any() .and(valid_access_token(appservice.registration().hs_token.clone())) .map(move || appservice.clone()) - .and(http_request().and_then(|request| async move { - let request = crate::transform_request_path(request).map_err(Error::from)?; - Ok::, Rejection>(request) - })) + .and( + http_request().and_then(|request| async move { + Ok::, Rejection>(request) + }), + ) .boxed() } diff --git a/crates/matrix-sdk-appservice/tests/tests.rs b/crates/matrix-sdk-appservice/tests/tests.rs deleted file mode 100644 index 61180ed97..000000000 --- a/crates/matrix-sdk-appservice/tests/tests.rs +++ /dev/null @@ -1,508 +0,0 @@ -use std::{ - future, - sync::{Arc, Mutex}, -}; - -use matrix_sdk::{ - config::RequestConfig, - ruma::{api::appservice::Registration, events::room::member::OriginalSyncRoomMemberEvent}, - Client, -}; -use matrix_sdk_appservice::*; -use matrix_sdk_test::{appservice::TransactionBuilder, async_test, EventsJson}; -use ruma::{ - api::{appservice::event::push_events, MatrixVersion}, - events::AnyRoomEvent, - room_id, - serde::Raw, -}; -use serde_json::json; -use warp::{Filter, Reply}; -use wiremock::{ - matchers::{body_json, header, method, path}, - Mock, MockServer, ResponseTemplate, -}; - -fn registration_string() -> String { - include_str!("../tests/registration.yaml").to_owned() -} - -async fn appservice( - homeserver_url: Option, - registration: Option, -) -> Result { - // env::set_var( - // "RUST_LOG", - // "wiremock=debug,matrix_sdk=debug,ruma=debug,warp=debug", - // ); - let _ = tracing_subscriber::fmt::try_init(); - - let registration = match registration { - Some(registration) => registration.into(), - None => AppServiceRegistration::try_from_yaml_str(registration_string()).unwrap(), - }; - - let homeserver_url = homeserver_url.unwrap_or_else(|| "http://localhost:1234".to_owned()); - let server_name = "localhost"; - - let client_builder = Client::builder() - .request_config(RequestConfig::default().disable_retry()) - .server_versions([MatrixVersion::V1_0]); - - AppService::with_client_builder( - homeserver_url.as_ref(), - server_name, - registration, - client_builder, - ) - .await -} - -#[async_test] -async fn test_register_virtual_user() -> Result<()> { - let server = MockServer::start().await; - let appservice = appservice(Some(server.uri()), None).await?; - - let localpart = "someone"; - Mock::given(method("POST")) - .and(path("/_matrix/client/r0/register")) - .and(header( - "authorization", - format!("Bearer {}", appservice.registration().as_token).as_str(), - )) - .and(body_json(json!({ - "username": localpart.to_owned(), - "type": "m.login.application_service" - }))) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "access_token": "abc123", - "device_id": "GHTYAJCE", - "user_id": format!("@{localpart}:localhost"), - }))) - .mount(&server) - .await; - - appservice.register_virtual_user(localpart).await?; - - Ok(()) -} - -#[async_test] -async fn test_put_transaction() -> Result<()> { - let uri = "/_matrix/app/v1/transactions/1?access_token=hs_token"; - - let mut transaction_builder = TransactionBuilder::new(); - transaction_builder.add_room_event(EventsJson::Member); - let transaction = transaction_builder.build_json_transaction(); - - let appservice = appservice(None, None).await?; - - let status = warp::test::request() - .method("PUT") - .path(uri) - .json(&transaction) - .filter(&appservice.warp_filter()) - .await - .unwrap() - .into_response() - .status(); - - assert_eq!(status, 200); - - Ok(()) -} - -#[async_test] -async fn test_put_transaction_with_repeating_txn_id() -> Result<()> { - let uri = "/_matrix/app/v1/transactions/1?access_token=hs_token"; - - let mut transaction_builder = TransactionBuilder::new(); - transaction_builder.add_room_event(EventsJson::Member); - let transaction = transaction_builder.build_json_transaction(); - - let appservice = appservice(None, None).await?; - - #[allow(clippy::mutex_atomic)] - let on_state_member = Arc::new(Mutex::new(false)); - appservice - .register_event_handler({ - let on_state_member = on_state_member.clone(); - move |_ev: OriginalSyncRoomMemberEvent| { - *on_state_member.lock().unwrap() = true; - future::ready(()) - } - }) - .await?; - - let status = warp::test::request() - .method("PUT") - .path(uri) - .json(&transaction) - .filter(&appservice.warp_filter()) - .await - .unwrap() - .into_response() - .status(); - - assert_eq!(status, 200); - { - let on_room_member_called = *on_state_member.lock().unwrap(); - assert!(on_room_member_called); - } - - // Reset this to check that next time it doesnt get called - { - let mut on_room_member_called = on_state_member.lock().unwrap(); - *on_room_member_called = false; - } - - let status = warp::test::request() - .method("PUT") - .path(uri) - .json(&transaction) - .filter(&appservice.warp_filter()) - .await - .unwrap() - .into_response() - .status(); - - // According to https://spec.matrix.org/v1.2/application-service-api/#pushing-events - // This should noop and return 200. - assert_eq!(status, 200); - { - let on_room_member_called = *on_state_member.lock().unwrap(); - // This time we should not have called the event handler. - assert!(!on_room_member_called); - } - - Ok(()) -} - -#[async_test] -async fn test_get_user() -> Result<()> { - let appservice = appservice(None, None).await?; - appservice.register_user_query(Box::new(|_, _| Box::pin(async move { true }))).await; - - let uri = "/_matrix/app/v1/users/%40_botty_1%3Adev.famedly.local?access_token=hs_token"; - - let status = warp::test::request() - .method("GET") - .path(uri) - .filter(&appservice.warp_filter()) - .await - .unwrap() - .into_response() - .status(); - - assert_eq!(status, 200); - - Ok(()) -} - -#[async_test] -async fn test_get_room() -> Result<()> { - let appservice = appservice(None, None).await?; - appservice.register_room_query(Box::new(|_, _| Box::pin(async move { true }))).await; - - let uri = "/_matrix/app/v1/rooms/%23magicforest%3Aexample.com?access_token=hs_token"; - - let status = warp::test::request() - .method("GET") - .path(uri) - .filter(&appservice.warp_filter()) - .await - .unwrap() - .into_response() - .status(); - - assert_eq!(status, 200); - - Ok(()) -} - -#[async_test] -async fn test_invalid_access_token() -> Result<()> { - let uri = "/_matrix/app/v1/transactions/1?access_token=invalid_token"; - - let mut transaction_builder = TransactionBuilder::new(); - let transaction = - transaction_builder.add_room_event(EventsJson::Member).build_json_transaction(); - - let appservice = appservice(None, None).await?; - - let status = warp::test::request() - .method("PUT") - .path(uri) - .json(&transaction) - .filter(&appservice.warp_filter()) - .await - .unwrap() - .into_response() - .status(); - - assert_eq!(status, 401); - - Ok(()) -} - -#[async_test] -async fn test_no_access_token() -> Result<()> { - let uri = "/_matrix/app/v1/transactions/1"; - - let mut transaction_builder = TransactionBuilder::new(); - transaction_builder.add_room_event(EventsJson::Member); - let transaction = transaction_builder.build_json_transaction(); - - let appservice = appservice(None, None).await?; - - { - let status = warp::test::request() - .method("PUT") - .path(uri) - .json(&transaction) - .filter(&appservice.warp_filter()) - .await - .unwrap() - .into_response() - .status(); - - assert_eq!(status, 401); - } - - Ok(()) -} - -#[async_test] -async fn test_event_handler() -> Result<()> { - let appservice = appservice(None, None).await?; - - #[allow(clippy::mutex_atomic)] - let on_state_member = Arc::new(Mutex::new(false)); - appservice - .register_event_handler({ - let on_state_member = on_state_member.clone(); - move |_ev: OriginalSyncRoomMemberEvent| { - *on_state_member.lock().unwrap() = true; - future::ready(()) - } - }) - .await?; - - let uri = "/_matrix/app/v1/transactions/1?access_token=hs_token"; - - let mut transaction_builder = TransactionBuilder::new(); - transaction_builder.add_room_event(EventsJson::Member); - let transaction = transaction_builder.build_json_transaction(); - - warp::test::request() - .method("PUT") - .path(uri) - .json(&transaction) - .filter(&appservice.warp_filter()) - .await - .unwrap(); - - let on_room_member_called = *on_state_member.lock().unwrap(); - assert!(on_room_member_called); - - Ok(()) -} - -#[async_test] -async fn test_unrelated_path() -> Result<()> { - let appservice = appservice(None, None).await?; - - let status = { - let consumer_filter = warp::any() - .and(appservice.warp_filter()) - .or(warp::get().and(warp::path("unrelated").map(warp::reply))); - - let response = warp::test::request() - .method("GET") - .path("/unrelated") - .filter(&consumer_filter) - .await? - .into_response(); - - response.status() - }; - - assert_eq!(status, 200); - - Ok(()) -} - -#[async_test] -async fn test_appservice_on_sub_path() -> Result<()> { - let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost"); - let uri_1 = "/sub_path/_matrix/app/v1/transactions/1?access_token=hs_token"; - let uri_2 = "/sub_path/_matrix/app/v1/transactions/2?access_token=hs_token"; - - let mut transaction_builder = TransactionBuilder::new(); - transaction_builder.add_room_event(EventsJson::Member); - let transaction_1 = transaction_builder.build_json_transaction(); - - let mut transaction_builder = TransactionBuilder::new(); - transaction_builder.add_room_event(EventsJson::MemberNameChange); - let transaction_2 = transaction_builder.build_json_transaction(); - - let appservice = appservice(None, None).await?; - - { - warp::test::request() - .method("PUT") - .path(uri_1) - .json(&transaction_1) - .filter(&warp::path("sub_path").and(appservice.warp_filter())) - .await?; - - warp::test::request() - .method("PUT") - .path(uri_2) - .json(&transaction_2) - .filter(&warp::path("sub_path").and(appservice.warp_filter())) - .await?; - }; - - let members = appservice - .get_cached_client(None)? - .get_room(room_id) - .expect("Expected room to be available") - .members_no_sync() - .await?; - - assert_eq!(members[0].display_name().unwrap(), "changed"); - - Ok(()) -} - -#[async_test] -async fn test_receive_transaction() -> Result<()> { - tracing_subscriber::fmt().try_init().ok(); - let json = vec![ - Raw::new(&json!({ - "content": { - "avatar_url": null, - "displayname": "Appservice", - "membership": "join" - }, - "event_id": "$151800140479rdvjg:localhost", - "membership": "join", - "origin_server_ts": 151800140, - "sender": "@_appservice:localhost", - "state_key": "@_appservice:localhost", - "type": "m.room.member", - "room_id": "!coolplace:localhost", - "unsigned": { - "age": 2970366 - } - }))? - .cast::(), - Raw::new(&json!({ - "content": { - "avatar_url": null, - "displayname": "Appservice", - "membership": "join" - }, - "event_id": "$151800140491rfbja:localhost", - "membership": "join", - "origin_server_ts": 151800140, - "sender": "@_appservice:localhost", - "state_key": "@_appservice:localhost", - "type": "m.room.member", - "room_id": "!boringplace:localhost", - "unsigned": { - "age": 2970366 - } - }))? - .cast::(), - Raw::new(&json!({ - "content": { - "avatar_url": null, - "displayname": "Alice", - "membership": "join" - }, - "event_id": "$151800140517rfvjc:localhost", - "membership": "join", - "origin_server_ts": 151800140, - "sender": "@_appservice_alice:localhost", - "state_key": "@_appservice_alice:localhost", - "type": "m.room.member", - "room_id": "!coolplace:localhost", - "unsigned": { - "age": 2970366 - } - }))? - .cast::(), - Raw::new(&json!({ - "content": { - "avatar_url": null, - "displayname": "Bob", - "membership": "invite" - }, - "event_id": "$151800140594rfvjc:localhost", - "membership": "invite", - "origin_server_ts": 151800174, - "sender": "@_appservice_bob:localhost", - "state_key": "@_appservice_bob:localhost", - "type": "m.room.member", - "room_id": "!boringplace:localhost", - "unsigned": { - "age": 2970366 - } - }))? - .cast::(), - ]; - let appservice = appservice(None, None).await?; - - let alice = appservice.virtual_user_client("_appservice_alice").await?; - let bob = appservice.virtual_user_client("_appservice_bob").await?; - appservice - .receive_transaction(push_events::v1::IncomingRequest::new("dontcare".into(), json)) - .await?; - let coolplace = room_id!("!coolplace:localhost"); - let boringplace = room_id!("!boringplace:localhost"); - assert!( - alice.get_joined_room(coolplace).is_some(), - "Alice's membership in coolplace should be join" - ); - assert!( - bob.get_invited_room(boringplace).is_some(), - "Bob's membership in boringplace should be invite" - ); - assert!(alice.get_room(boringplace).is_none(), "Alice should not know about boringplace"); - assert!(bob.get_room(coolplace).is_none(), "Bob should not know about coolplace"); - Ok(()) -} - -mod registration { - use super::*; - - #[test] - fn test_registration() -> Result<()> { - let registration: Registration = serde_yaml::from_str(®istration_string())?; - let registration: AppServiceRegistration = registration.into(); - - assert_eq!(registration.id, "appservice"); - - Ok(()) - } - - #[test] - fn test_registration_from_yaml_file() -> Result<()> { - let registration = AppServiceRegistration::try_from_yaml_file("./tests/registration.yaml")?; - - assert_eq!(registration.id, "appservice"); - - Ok(()) - } - - #[test] - fn test_registration_from_yaml_str() -> Result<()> { - let registration = AppServiceRegistration::try_from_yaml_str(registration_string())?; - - assert_eq!(registration.id, "appservice"); - - Ok(()) - } -} diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index 7c6834753..f4a3257ed 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -17,41 +17,36 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = [] -e2e-encryption = ["matrix-sdk-crypto"] -qrcode = ["matrix-sdk-crypto/qrcode"] +e2e-encryption = ["dep:matrix-sdk-crypto"] +qrcode = ["matrix-sdk-crypto?/qrcode"] experimental-timeline = [] # helpers for testing features build upon this -testing = ["http"] +testing = ["dep:http"] [dependencies] async-stream = "0.3.3" async-trait = "0.1.53" -chacha20poly1305 = { version = "0.9.0", optional = true } dashmap = "5.2.0" futures-channel = "0.3.21" futures-core = "0.3.21" futures-util = { version = "0.3.21", default-features = false } -hmac = { version = "0.12.1", optional = true } http = { version = "0.2.6", optional = true } lru = "0.7.5" matrix-sdk-common = { version = "0.5.0", path = "../matrix-sdk-common" } matrix-sdk-crypto = { version = "0.5.0", path = "../matrix-sdk-crypto", optional = true } once_cell = "1.10.0" -pbkdf2 = { version = "0.11.0", default-features = false, optional = true } -rand = { version = "0.8.5", optional = true } serde = { version = "1.0.136", features = ["rc"] } serde_json = "1.0.79" -sha2 = { version = "0.10.2", optional = true } thiserror = "1.0.30" tracing = "0.1.34" zeroize = { version = "1.3.0", features = ["zeroize_derive"] } [target.'cfg(target_arch = "wasm32")'.dependencies] -ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c", "js", "canonical-json"] } +ruma = { git = "https://github.com/ruma/ruma", rev = "ca8c66c885241a7ba3805399604eda4a38979f6b", features = ["client-api-c", "js", "canonical-json"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c", "canonical-json"] } +ruma = { git = "https://github.com/ruma/ruma", rev = "ca8c66c885241a7ba3805399604eda4a38979f6b", features = ["client-api-c", "canonical-json"] } [dev-dependencies] futures = { version = "0.3.21", default-features = false, features = ["executor"] } diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 5741a9ef0..5d58c040a 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -66,6 +66,7 @@ use crate::{ store::{ ambiguity_map::AmbiguityCache, Result as StoreResult, StateChanges, Store, StoreConfig, }, + StateStore, }; /// A no IO Client implementation. @@ -135,9 +136,20 @@ impl BaseClient { self.store.session() } + /// Get all the rooms this client knows about. + pub fn get_rooms(&self) -> Vec { + self.store.get_rooms() + } + + /// Get all the rooms this client knows about. + pub fn get_stripped_rooms(&self) -> Vec { + self.store.get_stripped_rooms() + } + /// Get a reference to the store. - pub fn store(&self) -> &Store { - &self.store + #[allow(unknown_lints, clippy::explicit_auto_deref)] + pub fn store(&self) -> &dyn StateStore { + &*self.store } /// Is the client logged in. @@ -1105,7 +1117,10 @@ impl Default for BaseClient { #[cfg(test)] mod tests { - use matrix_sdk_test::{async_test, response_from_file, EventBuilder}; + use matrix_sdk_test::{ + async_test, response_from_file, EventBuilder, InvitedRoomBuilder, LeftRoomBuilder, + StrippedStateTestEvent, TimelineTestEvent, + }; use ruma::{ api::{client as api, IncomingResponse}, room_id, user_id, @@ -1133,9 +1148,8 @@ mod tests { let mut ev_builder = EventBuilder::new(); let response = ev_builder - .add_custom_left_event( - room_id, - json!({ + .add_left_room(LeftRoomBuilder::new(room_id).add_timeline_event( + TimelineTestEvent::Custom(json!({ "content": { "displayname": "Alice", "membership": "left", @@ -1145,16 +1159,15 @@ mod tests { "sender": user_id, "state_key": user_id, "type": "m.room.member", - }), - ) + })), + )) .build_sync_response(); client.receive_sync_response(response).await.unwrap(); assert_eq!(client.get_room(room_id).unwrap().room_type(), RoomType::Left); let response = ev_builder - .add_custom_invited_event( - room_id, - json!({ + .add_invited_room(InvitedRoomBuilder::new(room_id).add_state_event( + StrippedStateTestEvent::Custom(json!({ "content": { "displayname": "Alice", "membership": "invite", @@ -1164,8 +1177,8 @@ mod tests { "sender": "@example:example.org", "state_key": user_id, "type": "m.room.member", - }), - ) + })), + )) .build_sync_response(); client.receive_sync_response(response).await.unwrap(); assert_eq!(client.get_room(room_id).unwrap().room_type(), RoomType::Invited); diff --git a/crates/matrix-sdk-base/src/lib.rs b/crates/matrix-sdk-base/src/lib.rs index 3924d80b2..b7bbaeecb 100644 --- a/crates/matrix-sdk-base/src/lib.rs +++ b/crates/matrix-sdk-base/src/lib.rs @@ -43,7 +43,7 @@ pub use http; pub use matrix_sdk_crypto as crypto; pub use once_cell; pub use rooms::{DisplayName, Room, RoomInfo, RoomMember, RoomType}; -pub use store::{StateChanges, StateStore, Store, StoreError}; +pub use store::{StateChanges, StateStore, StoreError}; pub use utils::{ MinimalRoomMemberEvent, MinimalStateEvent, OriginalMinimalStateEvent, RedactedMinimalStateEvent, }; diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 53156f060..fe286f965 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -29,7 +29,7 @@ use matrix_sdk_common::locks::Mutex; use ruma::{ api::client::sync::sync_events::v3::RoomSummary as RumaSummary, events::{ - receipt::Receipt, + receipt::{Receipt, ReceiptType}, room::{ create::RoomCreateEventContent, encryption::RoomEncryptionEventContent, guest_access::GuestAccess, history_visibility::HistoryVisibility, join_rules::JoinRule, @@ -39,7 +39,6 @@ use ruma::{ AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, RoomAccountDataEventType, StateEventType, }, - receipt::ReceiptType, room::RoomType as CreateRoomType, EventId, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedUserId, RoomAliasId, RoomId, RoomVersionId, UserId, @@ -104,16 +103,7 @@ impl Room { room_id: &RoomId, room_type: RoomType, ) -> Self { - let room_info = RoomInfo { - room_id: room_id.into(), - room_type, - notification_counts: Default::default(), - summary: Default::default(), - members_synced: false, - last_prev_batch: None, - base_info: BaseRoomInfo::new(), - }; - + let room_info = RoomInfo::new(room_id, room_type); Self::restore(own_user_id, store, room_info) } @@ -656,6 +646,19 @@ pub struct RoomInfo { } impl RoomInfo { + #[doc(hidden)] // used by store tests, otherwise it would be pub(crate) + pub fn new(room_id: &RoomId, room_type: RoomType) -> Self { + Self { + room_id: room_id.into(), + room_type, + notification_counts: Default::default(), + summary: Default::default(), + members_synced: false, + last_prev_batch: None, + base_info: BaseRoomInfo::new(), + } + } + /// Mark this Room as joined pub fn mark_as_joined(&mut self) { self.room_type = RoomType::Joined; diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index 4960110b7..0af39b2b8 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -52,9 +52,10 @@ macro_rules! statestore_integration_tests { use matrix_sdk_test::{async_test, test_json}; use ruma::{ api::client::media::get_content_thumbnail::v3::Method, - device_id, event_id, + event_id, events::{ presence::PresenceEvent, + receipt::ReceiptType, room::{ member::{ MembershipState, OriginalSyncRoomMemberEvent, SyncRoomMemberEvent, @@ -69,7 +70,6 @@ macro_rules! statestore_integration_tests { StateEventType, StateUnsigned, }, mxc_uri, - receipt::ReceiptType, room_id, serde::Raw, uint, user_id, MilliSecondsSinceUnixEpoch, UserId, EventId, OwnedEventId, @@ -83,14 +83,13 @@ macro_rules! statestore_integration_tests { deserialized_responses::{ RoomEvent, SyncRoomEvent, TimelineSlice}, }; use $crate::{ - RoomType, Session, media::{MediaFormat, MediaRequest, MediaThumbnailSize}, store::{ - Store, StateStore, Result as StoreResult, StateChanges - } + }, + RoomInfo, RoomType, }; use super::get_store; @@ -115,22 +114,13 @@ macro_rules! statestore_integration_tests { } /// Populate the given `StateStore`. - pub(crate) async fn populated_store(inner: Arc) -> StoreResult { + pub async fn populate_store(store: Arc) -> StoreResult<()> { let mut changes = StateChanges::default(); - let store = Store::new(inner); let user_id = user_id(); let invited_user_id = invited_user_id(); let room_id = room_id(); let stripped_room_id = stripped_room_id(); - let device_id = device_id!("device"); - - let session = Session { - access_token: "token".to_owned(), - user_id: user_id.to_owned(), - device_id: device_id.to_owned(), - }; - store.restore_session(session).await.unwrap(); changes.sync_token = Some("t392-516_47314_0_7_1_1_1_11444_1".to_owned()); @@ -147,7 +137,7 @@ macro_rules! statestore_integration_tests { let pushrules_event = pushrules_raw.deserialize().unwrap(); changes.add_account_data(pushrules_event, pushrules_raw); - let mut room = store.get_or_create_room(room_id, RoomType::Joined).await.clone_info(); + let mut room = RoomInfo::new(room_id, RoomType::Joined); room.mark_as_left(); let tag_json: &JsonValue = &test_json::TAG; @@ -220,8 +210,7 @@ macro_rules! statestore_integration_tests { changes.members.insert(room_id.to_owned(), room_members); changes.add_room(room); - let mut stripped_room = - store.get_or_create_stripped_room(stripped_room_id).await.clone_info(); + let mut stripped_room = RoomInfo::new(stripped_room_id, RoomType::Invited); let stripped_name_json: &JsonValue = &test_json::NAME_STRIPPED; let stripped_name_raw = @@ -249,7 +238,7 @@ macro_rules! statestore_integration_tests { changes.add_stripped_member(stripped_room_id, stripped_member_event); store.save_changes(&changes).await?; - Ok(store) + Ok(()) } fn power_level_event() -> Raw { @@ -305,11 +294,12 @@ macro_rules! statestore_integration_tests { let user_id = user_id(); let inner_store = get_store().await?; - let store = populated_store(Arc::new(inner_store)).await?; + let store = Arc::new(inner_store); + populate_store(store.clone()).await?; assert!(store.get_sync_token().await?.is_some()); assert!(store.get_presence_event(user_id).await?.is_some()); - assert_eq!(store.get_room_infos().await?.len(), 1, "Expected to find 1 room info "); + assert_eq!(store.get_room_infos().await?.len(), 1, "Expected to find 1 room info"); assert_eq!(store.get_stripped_room_infos().await?.len(), 1, "Expected to find 1 stripped room info"); assert!(store.get_account_data_event(GlobalAccountDataEventType::PushRules).await?.is_some()); @@ -580,25 +570,24 @@ macro_rules! statestore_integration_tests { #[async_test] async fn test_persist_invited_room() -> StoreResult<()> { - let stripped_room_id = stripped_room_id(); let inner_store = get_store().await?; - let store = populated_store(Arc::new(inner_store)).await?; + let store = Arc::new(inner_store); + populate_store(store.clone()).await?; assert_eq!(store.get_stripped_room_infos().await?.len(), 1); - assert!(store.get_stripped_room(stripped_room_id).is_some()); - // populate rooom Ok(()) } #[async_test] - async fn test_room_removal() -> StoreResult<()> { + async fn test_room_removal() -> StoreResult<()> { let room_id = room_id(); let user_id = user_id(); let inner_store = get_store().await?; let stripped_room_id = stripped_room_id(); - let store = populated_store(Arc::new(inner_store)).await?; + let store = Arc::new(inner_store); + populate_store(store.clone()).await?; store.remove_room(room_id).await?; @@ -641,7 +630,7 @@ macro_rules! statestore_integration_tests { async fn test_room_timeline() { let store = get_store().await.unwrap(); let mut stored_events = Vec::new(); - let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost"); + let room_id = *test_json::DEFAULT_SYNC_ROOM_ID; // Before the first sync the timeline should be empty assert!(store.room_timeline(room_id).await.expect("failed to read timeline").is_none(), "TL wasn't empty"); @@ -674,7 +663,7 @@ macro_rules! statestore_integration_tests { // Add message response let messages = MessageResponse::try_from_http_response( Response::builder() - .body(serde_json::to_vec(&*test_json::SYNC_ROOM_MESSAGES_BATCH_1).expect("Parsing SYNC_ROOM_MESSAGES_BATCH_1 failed")) + .body(serde_json::to_vec(&*test_json::ROOM_MESSAGES_BATCH_1).expect("Parsing ROOM_MESSAGES_BATCH_1 failed")) .unwrap(), ) .unwrap(); @@ -699,7 +688,7 @@ macro_rules! statestore_integration_tests { // Add second message response let messages = MessageResponse::try_from_http_response( Response::builder() - .body(serde_json::to_vec(&*test_json::SYNC_ROOM_MESSAGES_BATCH_2).expect("Parsing SYNC_ROOM_MESSAGES_BATCH_2 failed")) + .body(serde_json::to_vec(&*test_json::ROOM_MESSAGES_BATCH_2).expect("Parsing ROOM_MESSAGES_BATCH_2 failed")) .unwrap(), ) .unwrap(); diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index 76bbf4108..a0321fad8 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -35,12 +35,11 @@ use ruma::{ use ruma::{ events::{ presence::PresenceEvent, - receipt::Receipt, + receipt::{Receipt, ReceiptType}, room::member::{MembershipState, StrippedRoomMemberEvent, SyncRoomMemberEvent}, AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType, }, - receipt::ReceiptType, serde::Raw, EventId, MxcUri, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId, }; diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index c31926e69..a135d782c 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -43,12 +43,11 @@ use ruma::{ api::client::push::get_notifications::v3::Notification, events::{ presence::PresenceEvent, - receipt::{Receipt, ReceiptEventContent}, + receipt::{Receipt, ReceiptEventContent, ReceiptType}, room::member::{StrippedRoomMemberEvent, SyncRoomMemberEvent}, AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType, }, - receipt::ReceiptType, serde::Raw, EventId, MxcUri, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId, }; @@ -408,7 +407,7 @@ where /// This adds additional higher level store functionality on top of a /// `StateStore` implementation. #[derive(Debug, Clone)] -pub struct Store { +pub(crate) struct Store { pub(super) inner: Arc, session: Arc>, /// The current sync token that should be used for the next sync call. diff --git a/crates/matrix-sdk-common/Cargo.toml b/crates/matrix-sdk-common/Cargo.toml index f40bbce17..8c8d49de0 100644 --- a/crates/matrix-sdk-common/Cargo.toml +++ b/crates/matrix-sdk-common/Cargo.toml @@ -16,7 +16,7 @@ default-target = "x86_64-unknown-linux-gnu" targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"] [dependencies] -ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c"] } +ruma = { git = "https://github.com/ruma/ruma", rev = "ca8c66c885241a7ba3805399604eda4a38979f6b", features = ["client-api-c"] } serde = "1.0.136" [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index 03221c856..1721ca54d 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -17,12 +17,12 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = [] -qrcode = ["matrix-sdk-qrcode"] -backups_v1 = ["olm-rs", "bs58"] +qrcode = ["dep:matrix-sdk-qrcode"] +backups_v1 = ["dep:olm-rs", "dep:bs58"] docsrs = [] # Testing helpers for implementations based upon this -testing = ["http"] +testing = ["dep:http"] [dependencies] aes = "0.8.1" @@ -51,11 +51,11 @@ zeroize = { version = "1.3.0", features = ["zeroize_derive"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1.18", default-features = false, features = ["time"] } -ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c", "rand", "canonical-json", "unstable-msc2676", "unstable-msc2677"] } +ruma = { git = "https://github.com/ruma/ruma", rev = "ca8c66c885241a7ba3805399604eda4a38979f6b", features = ["client-api-c", "rand", "canonical-json", "unstable-msc2676", "unstable-msc2677"] } vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "2404f83f7d3a3779c1f518e4d949f7da9677c3dd" } [target.'cfg(target_arch = "wasm32")'.dependencies] -ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c", "js", "rand", "canonical-json", "unstable-msc2676", "unstable-msc2677"] } +ruma = { git = "https://github.com/ruma/ruma", rev = "ca8c66c885241a7ba3805399604eda4a38979f6b", features = ["client-api-c", "js", "rand", "canonical-json", "unstable-msc2676", "unstable-msc2677"] } vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "2404f83f7d3a3779c1f518e4d949f7da9677c3dd", features = ["js"] } [dev-dependencies] diff --git a/crates/matrix-sdk-crypto/src/verification/requests.rs b/crates/matrix-sdk-crypto/src/verification/requests.rs index fd1450e16..ceb248a67 100644 --- a/crates/matrix-sdk-crypto/src/verification/requests.rs +++ b/crates/matrix-sdk-crypto/src/verification/requests.rs @@ -1098,12 +1098,10 @@ impl RequestState { // before the other side tried to do the same; ignore it if we did and // we're the lexicographically smaller user ID (or device ID if equal). use std::cmp::Ordering; - match (sender.cmp(own_user_id), device.device_id().cmp(own_device_id)) { - (Ordering::Greater, _) | (Ordering::Equal, Ordering::Greater) => { - false - } - _ => true, - } + !matches!( + (sender.cmp(own_user_id), device.device_id().cmp(own_device_id)), + (Ordering::Greater, _) | (Ordering::Equal, Ordering::Greater) + ) } else { true }; diff --git a/crates/matrix-sdk-indexeddb/Cargo.toml b/crates/matrix-sdk-indexeddb/Cargo.toml index 8552a1474..6a988ece5 100644 --- a/crates/matrix-sdk-indexeddb/Cargo.toml +++ b/crates/matrix-sdk-indexeddb/Cargo.toml @@ -15,7 +15,7 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = ["e2e-encryption"] -e2e-encryption = ["matrix-sdk-base/e2e-encryption", "matrix-sdk-crypto", "dashmap"] +e2e-encryption = ["matrix-sdk-base/e2e-encryption", "dep:matrix-sdk-crypto", "dashmap"] experimental-timeline = ["matrix-sdk-base/experimental-timeline"] @@ -30,7 +30,7 @@ indexed_db_futures = "0.2.3" matrix-sdk-base = { version = "0.5.0", path = "../matrix-sdk-base" } matrix-sdk-crypto = { version = "0.5.0", path = "../matrix-sdk-crypto", optional = true } matrix-sdk-store-encryption = { version = "0.1.0", path = "../matrix-sdk-store-encryption" } -ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f" } +ruma = { git = "https://github.com/ruma/ruma", rev = "ca8c66c885241a7ba3805399604eda4a38979f6b" } serde = "1.0.136" serde_json = "1.0.79" thiserror = "1.0.30" diff --git a/crates/matrix-sdk-indexeddb/src/safe_encode.rs b/crates/matrix-sdk-indexeddb/src/safe_encode.rs index c6927e707..081c1a35a 100644 --- a/crates/matrix-sdk-indexeddb/src/safe_encode.rs +++ b/crates/matrix-sdk-indexeddb/src/safe_encode.rs @@ -2,8 +2,9 @@ use base64::{encode_config as base64_encode, STANDARD_NO_PAD}; use matrix_sdk_store_encryption::StoreCipher; use ruma::{ - events::{GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType}, - receipt::ReceiptType, + events::{ + receipt::ReceiptType, GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType, + }, DeviceId, EventId, MxcUri, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, TransactionId, UserId, }; diff --git a/crates/matrix-sdk-indexeddb/src/state_store.rs b/crates/matrix-sdk-indexeddb/src/state_store.rs index fb68dfa12..01cf7dc32 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store.rs @@ -39,12 +39,11 @@ use matrix_sdk_store_encryption::{Error as EncryptionError, StoreCipher}; use ruma::{ events::{ presence::PresenceEvent, - receipt::Receipt, + receipt::{Receipt, ReceiptType}, room::member::{MembershipState, RoomMemberEventContent}, AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnySyncStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType, }, - receipt::ReceiptType, serde::Raw, EventId, MxcUri, OwnedEventId, OwnedUserId, RoomId, UserId, }; diff --git a/crates/matrix-sdk-qrcode/Cargo.toml b/crates/matrix-sdk-qrcode/Cargo.toml index 8823bff77..4a05c2e75 100644 --- a/crates/matrix-sdk-qrcode/Cargo.toml +++ b/crates/matrix-sdk-qrcode/Cargo.toml @@ -17,7 +17,7 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = ["decode_image"] -decode_image = ["image", "rqrr", "qrcode/image", "qrcode/svg"] +decode_image = ["dep:image", "dep:rqrr", "qrcode/image", "qrcode/svg"] [dependencies] base64 = "0.13.0" @@ -25,7 +25,7 @@ byteorder = "1.4.3" image = { version = "0.23.0", optional = true } qrcode = { version = "0.12.0", default-features = false } rqrr = { version = "0.4.0", optional = true } -ruma-common = { git = "https://github.com/ruma/ruma", rev = "96155915f" } +ruma-common = { git = "https://github.com/ruma/ruma", rev = "ca8c66c885241a7ba3805399604eda4a38979f6b" } thiserror = "1.0.30" [dependencies.vodozemac] diff --git a/crates/matrix-sdk-sled/Cargo.toml b/crates/matrix-sdk-sled/Cargo.toml index a85b79890..01016487e 100644 --- a/crates/matrix-sdk-sled/Cargo.toml +++ b/crates/matrix-sdk-sled/Cargo.toml @@ -16,12 +16,16 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = ["state-store"] -state-store = ["matrix-sdk-base"] +state-store = ["dep:matrix-sdk-base"] crypto-store = [ - "matrix-sdk-crypto", - "matrix-sdk-base/e2e-encryption", + "dep:matrix-sdk-base", + "dep:matrix-sdk-crypto", + "matrix-sdk-base?/e2e-encryption", +] +experimental-timeline = [ + "dep:matrix-sdk-base", + "matrix-sdk-base?/experimental-timeline", ] -experimental-timeline = ["matrix-sdk-base/experimental-timeline"] [dependencies] async-stream = "0.3.3" @@ -34,7 +38,7 @@ matrix-sdk-base = { version = "0.5.0", path = "../matrix-sdk-base", optional = t matrix-sdk-common = { version = "0.5.0", path = "../matrix-sdk-common" } matrix-sdk-crypto = { version = "0.5.0", path = "../matrix-sdk-crypto", optional = true } matrix-sdk-store-encryption = { version = "0.1.0", path = "../matrix-sdk-store-encryption" } -ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f" } +ruma = { git = "https://github.com/ruma/ruma", rev = "ca8c66c885241a7ba3805399604eda4a38979f6b" } serde = "1.0.136" serde_json = "1.0.79" sled = "0.34.7" diff --git a/crates/matrix-sdk-sled/src/encode_key.rs b/crates/matrix-sdk-sled/src/encode_key.rs index b36738713..91c06e09f 100644 --- a/crates/matrix-sdk-sled/src/encode_key.rs +++ b/crates/matrix-sdk-sled/src/encode_key.rs @@ -3,10 +3,9 @@ use std::{borrow::Cow, ops::Deref}; use matrix_sdk_store_encryption::StoreCipher; use ruma::{ events::{ - secret::request::SecretName, GlobalAccountDataEventType, RoomAccountDataEventType, - StateEventType, + receipt::ReceiptType, secret::request::SecretName, GlobalAccountDataEventType, + RoomAccountDataEventType, StateEventType, }, - receipt::ReceiptType, DeviceId, EventEncryptionAlgorithm, EventId, MxcUri, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, TransactionId, UserId, }; diff --git a/crates/matrix-sdk-sled/src/state_store.rs b/crates/matrix-sdk-sled/src/state_store.rs index f178c8d9c..fac972c6e 100644 --- a/crates/matrix-sdk-sled/src/state_store.rs +++ b/crates/matrix-sdk-sled/src/state_store.rs @@ -43,12 +43,11 @@ use ruma::{ use ruma::{ events::{ presence::PresenceEvent, - receipt::Receipt, + receipt::{Receipt, ReceiptType}, room::member::{MembershipState, RoomMemberEventContent}, AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnySyncStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType, }, - receipt::ReceiptType, serde::Raw, EventId, IdParseError, MxcUri, OwnedEventId, OwnedUserId, RoomId, UserId, }; diff --git a/crates/matrix-sdk-store-encryption/Cargo.toml b/crates/matrix-sdk-store-encryption/Cargo.toml index 935999454..9bc333224 100644 --- a/crates/matrix-sdk-store-encryption/Cargo.toml +++ b/crates/matrix-sdk-store-encryption/Cargo.toml @@ -11,7 +11,7 @@ rust-version = "1.60" rustdoc-args = ["--cfg", "docsrs"] [features] -js = ["getrandom/js"] +js = ["dep:getrandom", "getrandom?/js"] [dependencies] blake3 = "1.3.1" diff --git a/crates/matrix-sdk-test/Cargo.toml b/crates/matrix-sdk-test/Cargo.toml index 323888e79..d87a513b8 100644 --- a/crates/matrix-sdk-test/Cargo.toml +++ b/crates/matrix-sdk-test/Cargo.toml @@ -18,6 +18,6 @@ appservice = [] http = "0.2.6" matrix-sdk-test-macros = { version = "0.2.0", path = "../matrix-sdk-test-macros" } once_cell = "1.10.0" -ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c"] } +ruma = { git = "https://github.com/ruma/ruma", rev = "ca8c66c885241a7ba3805399604eda4a38979f6b", features = ["client-api-c"] } serde = "1.0.136" serde_json = "1.0.79" diff --git a/crates/matrix-sdk-test/src/appservice.rs b/crates/matrix-sdk-test/src/appservice.rs index a7712e327..29244f00f 100644 --- a/crates/matrix-sdk-test/src/appservice.rs +++ b/crates/matrix-sdk-test/src/appservice.rs @@ -1,22 +1,18 @@ use std::convert::TryFrom; -use ruma::{events::AnyRoomEvent, room_id, serde::Raw}; +use ruma::{events::AnyRoomEvent, serde::Raw}; use serde_json::Value; -use crate::{test_json, EventsJson}; +use crate::{event_builder::TimelineTestEvent, test_json}; /// Clones the given [`Value`] and adds a `room_id` to it /// /// Adding the `room_id` conditionally with `cfg` directly to the lazy_static /// test_json values is blocked by "experimental attributes on expressions, see /// issue #15701 for more information" -pub fn value_with_room_id(value: &Value) -> Value { - let mut val = value.clone(); - let room_id = - Value::try_from(room_id!("!SVkFJHzfwvuaIEawgC:localhost").to_string()).expect("room_id"); - val.as_object_mut().expect("mutable test_json").insert("room_id".to_owned(), room_id); - - val +pub fn value_with_room_id(value: &mut Value) { + let room_id = Value::try_from(test_json::DEFAULT_SYNC_ROOM_ID.to_string()).expect("room_id"); + value.as_object_mut().expect("mutable test_json").insert("room_id".to_owned(), room_id); } /// The `TransactionBuilder` struct can be used to easily generate valid @@ -34,15 +30,9 @@ impl TransactionBuilder { } /// Add a room event. - pub fn add_room_event(&mut self, json: EventsJson) -> &mut Self { - let val: &Value = match json { - EventsJson::Member => &test_json::MEMBER, - EventsJson::MemberNameChange => &test_json::MEMBER_NAME_CHANGE, - EventsJson::PowerLevels => &test_json::POWER_LEVELS, - _ => panic!("unknown event json {:?}", json), - }; - - let val = value_with_room_id(val); + pub fn add_room_event(&mut self, event: TimelineTestEvent) -> &mut Self { + let mut val = event.into_json_value(); + value_with_room_id(&mut val); let event = serde_json::from_value(val).unwrap(); diff --git a/crates/matrix-sdk-test/src/event_builder/bulk.rs b/crates/matrix-sdk-test/src/event_builder/bulk.rs new file mode 100644 index 000000000..5ddf10d56 --- /dev/null +++ b/crates/matrix-sdk-test/src/event_builder/bulk.rs @@ -0,0 +1,43 @@ +use std::ops::Range; + +use ruma::{ + events::{room::member::MembershipState, AnySyncStateEvent}, + serde::Raw, +}; +use serde_json::{from_value as from_json_value, json}; + +/// Create `m.room.member` events in the given range. +/// +/// The user IDs are generated as `@user_{idx}:{server}`, with `idx` being the +/// current value in `range`, so providing the same range in several method +/// calls will create events that replace the previous state. +/// +/// The event IDs are generated as `$roommember_{batch}_{idx}` so it's important +/// to increment `batch` between method calls to avoid having two events with +/// the same event ID. +/// +/// This method can be used as input for room builders with +/// `add_timeline_state_bulk()` or `add_state_bulk()`. +pub fn bulk_room_members<'a>( + batch: usize, + range: Range, + server: &'a str, + membership: &'a MembershipState, +) -> impl Iterator> + 'a { + range.map(move |idx| { + let user_id = format!("@user_{idx}:{server}"); + let event_id = format!("$roommember_{batch}_{idx}"); + let ts = 151800000 + batch * 100 + idx; + from_json_value(json!({ + "content": { + "membership": membership, + }, + "event_id": event_id, + "origin_server_ts": ts, + "sender": user_id, + "state_key": user_id, + "type": "m.room.member", + })) + .unwrap() + }) +} diff --git a/crates/matrix-sdk-test/src/event_builder/invited_room.rs b/crates/matrix-sdk-test/src/event_builder/invited_room.rs new file mode 100644 index 000000000..649dc2385 --- /dev/null +++ b/crates/matrix-sdk-test/src/event_builder/invited_room.rs @@ -0,0 +1,43 @@ +use ruma::{ + api::client::sync::sync_events::v3::InvitedRoom, events::AnyStrippedStateEvent, serde::Raw, + OwnedRoomId, +}; + +use super::StrippedStateTestEvent; +use crate::test_json; + +pub struct InvitedRoomBuilder { + pub(super) room_id: OwnedRoomId, + pub(super) inner: InvitedRoom, +} + +impl InvitedRoomBuilder { + /// Create a new `InvitedRoomBuilder` for the given room ID. + /// + /// If the room ID is [`test_json::DEFAULT_SYNC_ROOM_ID`], + /// [`InvitedRoomBuilder::default()`] can be used instead. + pub fn new(room_id: impl Into) -> Self { + Self { room_id: room_id.into(), inner: Default::default() } + } + + /// Add an event to the state. + pub fn add_state_event(mut self, event: StrippedStateTestEvent) -> Self { + self.inner.invite_state.events.push(event.into_raw_event()); + self + } + + /// Add events to the state in bulk. + pub fn add_state_bulk(mut self, events: I) -> Self + where + I: IntoIterator>, + { + self.inner.invite_state.events.extend(events); + self + } +} + +impl Default for InvitedRoomBuilder { + fn default() -> Self { + Self::new(test_json::DEFAULT_SYNC_ROOM_ID.to_owned()) + } +} diff --git a/crates/matrix-sdk-test/src/event_builder/joined_room.rs b/crates/matrix-sdk-test/src/event_builder/joined_room.rs new file mode 100644 index 000000000..5539243c9 --- /dev/null +++ b/crates/matrix-sdk-test/src/event_builder/joined_room.rs @@ -0,0 +1,129 @@ +use ruma::{ + api::client::sync::sync_events::v3::JoinedRoom, + events::{ + AnyRoomAccountDataEvent, AnySyncEphemeralRoomEvent, AnySyncRoomEvent, AnySyncStateEvent, + }, + serde::Raw, + OwnedRoomId, +}; +use serde_json::{from_value as from_json_value, Value as JsonValue}; + +use super::{EphemeralTestEvent, RoomAccountDataTestEvent, StateTestEvent, TimelineTestEvent}; +use crate::test_json; + +pub struct JoinedRoomBuilder { + pub(super) room_id: OwnedRoomId, + pub(super) inner: JoinedRoom, +} + +impl JoinedRoomBuilder { + /// Create a new `JoinedRoomBuilder` for the given room ID. + /// + /// If the room ID is [`test_json::DEFAULT_SYNC_ROOM_ID`], + /// [`JoinedRoomBuilder::default()`] can be used instead. + pub fn new(room_id: impl Into) -> Self { + Self { room_id: room_id.into(), inner: Default::default() } + } + + /// Add an event to the timeline. + pub fn add_timeline_event(mut self, event: TimelineTestEvent) -> Self { + self.inner.timeline.events.push(event.into_raw_event()); + self + } + + /// Add events in bulk to the timeline. + pub fn add_timeline_bulk(mut self, events: I) -> Self + where + I: IntoIterator>, + { + self.inner.timeline.events.extend(events); + self + } + + /// Add state events in bulk to the timeline. + /// + /// This is a convenience method that casts `Raw` to + /// `Raw` and calls `JoinedRoom::add_timeline_bulk()`. + pub fn add_timeline_state_bulk(self, events: I) -> Self + where + I: IntoIterator>, + { + let events = events.into_iter().map(|event| event.cast()); + self.add_timeline_bulk(events) + } + + /// Set the timeline as limited. + pub fn set_timeline_limited(mut self) -> Self { + self.inner.timeline.limited = true; + self + } + + /// Set the `prev_batch` of the timeline. + pub fn set_timeline_prev_batch(mut self, prev_batch: String) -> Self { + self.inner.timeline.prev_batch = Some(prev_batch); + self + } + + /// Add an event to the state. + pub fn add_state_event(mut self, event: StateTestEvent) -> Self { + self.inner.state.events.push(event.into_raw_event()); + self + } + + /// Add events in bulk to the state. + pub fn add_state_bulk(mut self, events: I) -> Self + where + I: IntoIterator>, + { + self.inner.state.events.extend(events); + self + } + + /// Add an ephemeral event. + pub fn add_ephemeral_event(mut self, event: EphemeralTestEvent) -> Self { + self.inner.ephemeral.events.push(event.into_raw_event()); + self + } + + /// Add ephemeral events in bulk. + pub fn add_ephemeral_bulk(mut self, events: I) -> Self + where + I: IntoIterator>, + { + self.inner.ephemeral.events.extend(events); + self + } + + /// Add room account data. + pub fn add_account_data(mut self, event: RoomAccountDataTestEvent) -> Self { + self.inner.account_data.events.push(event.into_raw_event()); + self + } + + /// Add room account data in bulk. + pub fn add_account_data_bulk(mut self, events: I) -> Self + where + I: IntoIterator>, + { + self.inner.account_data.events.extend(events); + self + } + + /// Set the room summary. + pub fn set_room_summary(mut self, summary: JsonValue) -> Self { + self.inner.summary = from_json_value(summary).unwrap(); + self + } + + /// Set the unread notifications count. + pub fn set_unread_notifications_count(mut self, unread_notifications: JsonValue) -> Self { + self.inner.unread_notifications = from_json_value(unread_notifications).unwrap(); + self + } +} + +impl Default for JoinedRoomBuilder { + fn default() -> Self { + Self::new(test_json::DEFAULT_SYNC_ROOM_ID.to_owned()) + } +} diff --git a/crates/matrix-sdk-test/src/event_builder/left_room.rs b/crates/matrix-sdk-test/src/event_builder/left_room.rs new file mode 100644 index 000000000..13c7e4d70 --- /dev/null +++ b/crates/matrix-sdk-test/src/event_builder/left_room.rs @@ -0,0 +1,99 @@ +use ruma::{ + api::client::sync::sync_events::v3::LeftRoom, + events::{AnyRoomAccountDataEvent, AnySyncRoomEvent, AnySyncStateEvent}, + serde::Raw, + OwnedRoomId, +}; + +use super::{RoomAccountDataTestEvent, StateTestEvent, TimelineTestEvent}; +use crate::test_json; + +pub struct LeftRoomBuilder { + pub(super) room_id: OwnedRoomId, + pub(super) inner: LeftRoom, +} + +impl LeftRoomBuilder { + /// Create a new `LeftRoomBuilder` for the given room ID. + /// + /// If the room ID is [`test_json::DEFAULT_SYNC_ROOM_ID`], + /// [`LeftRoomBuilder::default()`] can be used instead. + pub fn new(room_id: impl Into) -> Self { + Self { room_id: room_id.into(), inner: Default::default() } + } + + /// Add an event to the timeline. + pub fn add_timeline_event(mut self, event: TimelineTestEvent) -> Self { + self.inner.timeline.events.push(event.into_raw_event()); + self + } + + /// Add events in bulk to the timeline. + pub fn add_timeline_bulk(mut self, events: I) -> Self + where + I: IntoIterator>, + { + self.inner.timeline.events.extend(events); + self + } + + /// Add state events in bulk to the timeline. + /// + /// This is a convenience method that casts `Raw` to + /// `Raw` and calls `LeftRoom::add_timeline_bulk()`. + pub fn add_timeline_state_bulk(self, events: I) -> Self + where + I: IntoIterator>, + { + let events = events.into_iter().map(|event| event.cast()); + self.add_timeline_bulk(events) + } + + /// Set the timeline as limited. + pub fn set_timeline_limited(mut self) -> Self { + self.inner.timeline.limited = true; + self + } + + /// Set the `prev_batch` of the timeline. + pub fn set_timeline_prev_batch(mut self, prev_batch: String) -> Self { + self.inner.timeline.prev_batch = Some(prev_batch); + self + } + + /// Add an event to the state. + pub fn add_state_event(mut self, event: StateTestEvent) -> Self { + self.inner.state.events.push(event.into_raw_event()); + self + } + + /// Add events in bulk to the state. + pub fn add_state_bulk(mut self, events: I) -> Self + where + I: IntoIterator>, + { + self.inner.state.events.extend(events); + self + } + + /// Add room account data. + pub fn add_account_data(mut self, event: RoomAccountDataTestEvent) -> Self { + self.inner.account_data.events.push(event.into_raw_event()); + self + } + + /// Add room account data in bulk. + pub fn add_account_data_bulk(mut self, events: I) -> Self + where + I: IntoIterator>, + { + self.inner.account_data.events.extend(events); + self + } +} + +impl Default for LeftRoomBuilder { + fn default() -> Self { + Self::new(test_json::DEFAULT_SYNC_ROOM_ID.to_owned()) + } +} diff --git a/crates/matrix-sdk-test/src/event_builder/mod.rs b/crates/matrix-sdk-test/src/event_builder/mod.rs new file mode 100644 index 000000000..4af354fe2 --- /dev/null +++ b/crates/matrix-sdk-test/src/event_builder/mod.rs @@ -0,0 +1,248 @@ +use std::collections::HashMap; + +use http::Response; +use ruma::{ + api::{ + client::sync::sync_events::v3::{ + InvitedRoom, JoinedRoom, LeftRoom, Response as SyncResponse, + }, + IncomingResponse, + }, + events::{presence::PresenceEvent, AnyGlobalAccountDataEvent}, + serde::Raw, + OwnedRoomId, +}; +use serde_json::{from_value as from_json_value, json, Value as JsonValue}; + +use super::test_json; + +mod bulk; +mod invited_room; +mod joined_room; +mod left_room; +mod test_event; + +pub use bulk::bulk_room_members; +pub use invited_room::InvitedRoomBuilder; +pub use joined_room::JoinedRoomBuilder; +pub use left_room::LeftRoomBuilder; +pub use test_event::{ + EphemeralTestEvent, GlobalAccountDataTestEvent, PresenceTestEvent, RoomAccountDataTestEvent, + StateTestEvent, StrippedStateTestEvent, TimelineTestEvent, +}; + +/// The `EventBuilder` struct can be used to easily generate valid sync +/// responses for testing. These can be then fed into either `Client` or `Room`. +/// +/// It supports generated a number of canned events, such as a member entering a +/// room, his power level and display name changing and similar. It also +/// supports insertion of custom events in the form of `EventsJson` values. +/// +/// **Important** You *must* use the *same* builder when sending multiple sync +/// responses to a single client. Otherwise, the subsequent responses will be +/// *ignored* by the client because the `next_batch` sync token will not be +/// rotated properly. +/// +/// # Example usage +/// +/// ```rust +/// use matrix_sdk_test::{EventBuilder, JoinedRoomBuilder, TimelineTestEvent}; +/// +/// let mut builder = EventBuilder::new(); +/// +/// // response1 now contains events that add an example member to the room and change their power +/// // level +/// let response1 = builder +/// .add_joined_room( +/// JoinedRoomBuilder::default() +/// .add_timeline_event(TimelineTestEvent::Member) +/// .add_timeline_event(TimelineTestEvent::PowerLevels) +/// ) +/// .build_sync_response(); +/// +/// // response2 is now empty (nothing changed) +/// let response2 = builder.build_sync_response(); +/// +/// // response3 contains a display name change for member example +/// let response3 = builder +/// .add_joined_room( +/// JoinedRoomBuilder::default() +/// .add_timeline_event(TimelineTestEvent::MemberNameChange) +/// .add_timeline_event(TimelineTestEvent::PowerLevels) +/// ) +/// .build_sync_response(); +/// ``` +#[derive(Default)] +pub struct EventBuilder { + /// Updates to joined `Room`s. + joined_rooms: HashMap, + /// Updates to invited `Room`s. + invited_rooms: HashMap, + /// Updates to left `Room`s. + left_rooms: HashMap, + /// Events that determine the presence state of a user. + presence: Vec>, + /// Global account data events. + account_data: Vec>, + /// Internal counter to enable the `prev_batch` and `next_batch` of each + /// sync response to vary. + batch_counter: i64, +} + +impl EventBuilder { + pub fn new() -> Self { + Self::default() + } + + /// Add a joined room to the next sync response. + /// + /// If a room with the same room ID already exists, it is replaced by this + /// one. + pub fn add_joined_room(&mut self, room: JoinedRoomBuilder) -> &mut Self { + self.invited_rooms.remove(&room.room_id); + self.left_rooms.remove(&room.room_id); + self.joined_rooms.insert(room.room_id, room.inner); + self + } + + /// Add an invited room to the next sync response. + /// + /// If a room with the same room ID already exists, it is replaced by this + /// one. + pub fn add_invited_room(&mut self, room: InvitedRoomBuilder) -> &mut Self { + self.joined_rooms.remove(&room.room_id); + self.left_rooms.remove(&room.room_id); + self.invited_rooms.insert(room.room_id, room.inner); + self + } + + /// Add a left room to the next sync response. + /// + /// If a room with the same room ID already exists, it is replaced by this + /// one. + pub fn add_left_room(&mut self, room: LeftRoomBuilder) -> &mut Self { + self.joined_rooms.remove(&room.room_id); + self.invited_rooms.remove(&room.room_id); + self.left_rooms.insert(room.room_id, room.inner); + self + } + + /// Add a presence event. + pub fn add_presence_event(&mut self, event: PresenceTestEvent) -> &mut Self { + let val = match event { + PresenceTestEvent::Presence => test_json::PRESENCE.to_owned(), + PresenceTestEvent::Custom(json) => json, + }; + + self.presence.push(from_json_value(val).unwrap()); + self + } + + /// Add presence in bulk. + pub fn add_presence_bulk(&mut self, events: I) -> &mut Self + where + I: IntoIterator>, + { + self.presence.extend(events); + self + } + + /// Add global account data. + pub fn add_global_account_data_event( + &mut self, + event: GlobalAccountDataTestEvent, + ) -> &mut Self { + let val = match event { + GlobalAccountDataTestEvent::PushRules => test_json::PUSH_RULES.to_owned(), + GlobalAccountDataTestEvent::Tags => test_json::TAG.to_owned(), + GlobalAccountDataTestEvent::Custom(json) => json, + }; + + self.account_data.push(from_json_value(val).unwrap()); + self + } + + /// Add global account data in bulk. + pub fn add_global_account_data_bulk(&mut self, events: I) -> &mut Self + where + I: IntoIterator>, + { + self.account_data.extend(events); + self + } + + /// Builds a sync response as a JSON Value containing the events we queued + /// so far. + /// + /// The next response returned by `build_sync_response` will then be empty + /// if no further events were queued. + /// + /// This method is raw JSON equivalent to + /// [build_sync_response()](#method.build_sync_response), use + /// [build_sync_response()](#method.build_sync_response) if you need a typed + /// response. + pub fn build_json_sync_response(&mut self) -> JsonValue { + self.batch_counter += 1; + let next_batch = self.generate_sync_token(); + + let body = json! { + { + "device_one_time_keys_count": {}, + "next_batch": next_batch, + "device_lists": { + "changed": [], + "left": [], + }, + "rooms": { + "invite": self.invited_rooms, + "join": self.joined_rooms, + "leave": self.left_rooms, + }, + "to_device": { + "events": [] + }, + "presence": { + "events": self.presence, + }, + "account_data": { + "events": self.account_data, + }, + } + }; + + // Clear state so that the next sync response will be empty if nothing + // was added. + self.clear(); + + body + } + + /// Builds a `SyncResponse` containing the events we queued so far. + /// + /// The next response returned by `build_sync_response` will then be empty + /// if no further events were queued. + /// + /// This method is high level and typed equivalent to + /// [build_json_sync_response()](#method.build_json_sync_response), use + /// [build_json_sync_response()](#method.build_json_sync_response) if you + /// need an untyped response. + pub fn build_sync_response(&mut self) -> SyncResponse { + let body = self.build_json_sync_response(); + + let response = Response::builder().body(serde_json::to_vec(&body).unwrap()).unwrap(); + + SyncResponse::try_from_http_response(response).unwrap() + } + + fn generate_sync_token(&self) -> String { + format!("t392-516_47314_0_7_1_1_1_11444_{}", self.batch_counter) + } + + pub fn clear(&mut self) { + self.account_data.clear(); + self.invited_rooms.clear(); + self.joined_rooms.clear(); + self.left_rooms.clear(); + self.presence.clear(); + } +} diff --git a/crates/matrix-sdk-test/src/event_builder/test_event.rs b/crates/matrix-sdk-test/src/event_builder/test_event.rs new file mode 100644 index 000000000..2ca6eea6a --- /dev/null +++ b/crates/matrix-sdk-test/src/event_builder/test_event.rs @@ -0,0 +1,235 @@ +use ruma::{ + events::{ + presence::PresenceEvent, AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, + AnyStrippedStateEvent, AnySyncEphemeralRoomEvent, AnySyncRoomEvent, AnySyncStateEvent, + }, + serde::Raw, +}; +use serde_json::{from_value as from_json_value, Value as JsonValue}; + +use crate::test_json; + +/// Test events that can be added to the timeline. +pub enum TimelineTestEvent { + Alias, + Aliases, + Create, + Encryption, + HistoryVisibility, + JoinRules, + Member, + MemberInvite, + MemberNameChange, + MessageEdit, + MessageEmote, + MessageNotice, + MessageText, + PowerLevels, + Reaction, + RedactedInvalid, + RedactedMessage, + RedactedState, + Redaction, + RoomAvatar, + RoomName, + RoomTopic, + Custom(JsonValue), +} + +impl TimelineTestEvent { + /// Get the JSON representation of this test event. + pub fn into_json_value(self) -> JsonValue { + match self { + Self::Alias => test_json::sync_events::ALIAS.to_owned(), + Self::Aliases => test_json::sync_events::ALIASES.to_owned(), + Self::Create => test_json::sync_events::CREATE.to_owned(), + Self::Encryption => test_json::sync_events::ENCRYPTION.to_owned(), + Self::HistoryVisibility => test_json::sync_events::HISTORY_VISIBILITY.to_owned(), + Self::JoinRules => test_json::sync_events::JOIN_RULES.to_owned(), + Self::Member => test_json::sync_events::MEMBER.to_owned(), + Self::MemberInvite => test_json::sync_events::MEMBER_INVITE.to_owned(), + Self::MemberNameChange => test_json::sync_events::MEMBER_NAME_CHANGE.to_owned(), + Self::MessageEdit => test_json::sync_events::MESSAGE_EDIT.to_owned(), + Self::MessageEmote => test_json::sync_events::MESSAGE_EMOTE.to_owned(), + Self::MessageNotice => test_json::sync_events::MESSAGE_NOTICE.to_owned(), + Self::MessageText => test_json::sync_events::MESSAGE_TEXT.to_owned(), + Self::PowerLevels => test_json::sync_events::POWER_LEVELS.to_owned(), + Self::Reaction => test_json::sync_events::REACTION.to_owned(), + Self::RedactedInvalid => test_json::sync_events::REDACTED_INVALID.to_owned(), + Self::RedactedMessage => test_json::sync_events::REDACTED.to_owned(), + Self::RedactedState => test_json::sync_events::REDACTED_STATE.to_owned(), + Self::Redaction => test_json::sync_events::REDACTION.to_owned(), + Self::RoomAvatar => test_json::sync_events::ROOM_AVATAR.to_owned(), + Self::RoomName => test_json::sync_events::NAME.to_owned(), + Self::RoomTopic => test_json::sync_events::TOPIC.to_owned(), + Self::Custom(json) => json, + } + } + + /// Get the typed JSON representation of this test event. + pub fn into_raw_event(self) -> Raw { + from_json_value(self.into_json_value()).unwrap() + } +} + +/// Test events that can be added to the state. +pub enum StateTestEvent { + Alias, + Aliases, + Create, + Encryption, + HistoryVisibility, + JoinRules, + Member, + MemberInvite, + MemberNameChange, + PowerLevels, + RedactedInvalid, + RedactedState, + RoomAvatar, + RoomName, + RoomTopic, + Custom(JsonValue), +} + +impl StateTestEvent { + /// Get the JSON representation of this test event. + pub fn into_json_value(self) -> JsonValue { + match self { + Self::Alias => test_json::sync_events::ALIAS.to_owned(), + Self::Aliases => test_json::sync_events::ALIASES.to_owned(), + Self::Create => test_json::sync_events::CREATE.to_owned(), + Self::Encryption => test_json::sync_events::ENCRYPTION.to_owned(), + Self::HistoryVisibility => test_json::sync_events::HISTORY_VISIBILITY.to_owned(), + Self::JoinRules => test_json::sync_events::JOIN_RULES.to_owned(), + Self::Member => test_json::sync_events::MEMBER.to_owned(), + Self::MemberInvite => test_json::sync_events::MEMBER_INVITE.to_owned(), + Self::MemberNameChange => test_json::sync_events::MEMBER_NAME_CHANGE.to_owned(), + Self::PowerLevels => test_json::sync_events::POWER_LEVELS.to_owned(), + Self::RedactedInvalid => test_json::sync_events::REDACTED_INVALID.to_owned(), + Self::RedactedState => test_json::sync_events::REDACTED_STATE.to_owned(), + Self::RoomAvatar => test_json::sync_events::ROOM_AVATAR.to_owned(), + Self::RoomName => test_json::sync_events::NAME.to_owned(), + Self::RoomTopic => test_json::sync_events::TOPIC.to_owned(), + Self::Custom(json) => json, + } + } + + /// Get the typed JSON representation of this test event. + pub fn into_raw_event(self) -> Raw { + from_json_value(self.into_json_value()).unwrap() + } +} + +/// Test events that can be added to the stripped state. +pub enum StrippedStateTestEvent { + Member, + RoomName, + Custom(JsonValue), +} + +impl StrippedStateTestEvent { + /// Get the JSON representation of this test event. + pub fn into_json_value(self) -> JsonValue { + match self { + Self::Member => test_json::sync_events::MEMBER_STRIPPED.to_owned(), + Self::RoomName => test_json::sync_events::NAME_STRIPPED.to_owned(), + Self::Custom(json) => json, + } + } + + /// Get the typed JSON representation of this test event. + pub fn into_raw_event(self) -> Raw { + from_json_value(self.into_json_value()).unwrap() + } +} + +/// Test events that can be added to the room account data. +pub enum RoomAccountDataTestEvent { + FullyRead, + Custom(JsonValue), +} + +impl RoomAccountDataTestEvent { + /// Get the JSON representation of this test event. + pub fn into_json_value(self) -> JsonValue { + match self { + Self::FullyRead => test_json::sync_events::FULLY_READ.to_owned(), + Self::Custom(json) => json, + } + } + + /// Get the typed JSON representation of this test event. + pub fn into_raw_event(self) -> Raw { + from_json_value(self.into_json_value()).unwrap() + } +} + +/// Test events that can be added to the ephemeral events. +pub enum EphemeralTestEvent { + ReadReceipt, + ReadReceiptOther, + Typing, + Custom(JsonValue), +} + +impl EphemeralTestEvent { + /// Get the JSON representation of this test event. + pub fn into_json_value(self) -> JsonValue { + match self { + Self::ReadReceipt => test_json::sync_events::READ_RECEIPT.to_owned(), + Self::ReadReceiptOther => test_json::sync_events::READ_RECEIPT_OTHER.to_owned(), + Self::Typing => test_json::sync_events::TYPING.to_owned(), + Self::Custom(json) => json, + } + } + + /// Get the typed JSON representation of this test event. + pub fn into_raw_event(self) -> Raw { + from_json_value(self.into_json_value()).unwrap() + } +} + +/// Test events that can be added to the presence events. +pub enum PresenceTestEvent { + Presence, + Custom(JsonValue), +} + +impl PresenceTestEvent { + /// Get the JSON representation of this test event. + pub fn into_json_value(self) -> JsonValue { + match self { + Self::Presence => test_json::sync_events::PRESENCE.to_owned(), + Self::Custom(json) => json, + } + } + + /// Get the typed JSON representation of this test event. + pub fn into_raw_event(self) -> Raw { + from_json_value(self.into_json_value()).unwrap() + } +} + +/// Test events that can be added to the global account data. +pub enum GlobalAccountDataTestEvent { + PushRules, + Tags, + Custom(JsonValue), +} + +impl GlobalAccountDataTestEvent { + /// Get the JSON representation of this test event. + pub fn into_json_value(self) -> JsonValue { + match self { + Self::PushRules => test_json::sync_events::PUSH_RULES.to_owned(), + Self::Tags => test_json::sync_events::TAG.to_owned(), + Self::Custom(json) => json, + } + } + + /// Get the typed JSON representation of this test event. + pub fn into_raw_event(self) -> Raw { + from_json_value(self.into_json_value()).unwrap() + } +} diff --git a/crates/matrix-sdk-test/src/lib.rs b/crates/matrix-sdk-test/src/lib.rs index 40a1de7c3..cbef86c99 100644 --- a/crates/matrix-sdk-test/src/lib.rs +++ b/crates/matrix-sdk-test/src/lib.rs @@ -1,381 +1,18 @@ -use std::{collections::HashMap, panic}; - use http::Response; pub use matrix_sdk_test_macros::async_test; -use ruma::{ - api::{client::sync::sync_events::v3::Response as SyncResponse, IncomingResponse}, - events::{ - presence::PresenceEvent, AnyGlobalAccountDataEvent, AnySyncEphemeralRoomEvent, - AnySyncRoomEvent, AnySyncStateEvent, - }, - room_id, - serde::Raw, - OwnedRoomId, RoomId, -}; +use ruma::api::{client::sync::sync_events::v3::Response as SyncResponse, IncomingResponse}; use serde_json::Value as JsonValue; #[cfg(feature = "appservice")] pub mod appservice; +mod event_builder; pub mod test_json; -/// Embedded event files -#[derive(Debug)] -pub enum EventsJson { - Alias, - Aliases, - Create, - Encryption, - FullyRead, - HistoryVisibility, - JoinRules, - Member, - MemberInvite, - MemberNameChange, - MessageEmote, - MessageNotice, - MessageText, - Name, - PowerLevels, - PushRules, - Presence, - ReadReceipt, - ReadReceiptOther, - RedactedInvalid, - RedactedState, - Redacted, - Redaction, - RoomAvatar, - Tag, - Topic, - Typing, -} - -/// The `EventBuilder` struct can be used to easily generate valid sync -/// responses for testing. These can be then fed into either `Client` or `Room`. -/// -/// It supports generated a number of canned events, such as a member entering a -/// room, his power level and display name changing and similar. It also -/// supports insertion of custom events in the form of `EventsJson` values. -/// -/// **Important** You *must* use the *same* builder when sending multiple sync -/// responses to a single client. Otherwise, the subsequent responses will be -/// *ignored* by the client because the `next_batch` sync token will not be -/// rotated properly. -/// -/// # Example usage -/// -/// ```rust -/// use matrix_sdk_test::{EventBuilder, EventsJson}; -/// -/// let mut builder = EventBuilder::new(); -/// -/// // response1 now contains events that add an example member to the room and change their power -/// // level -/// let response1 = builder -/// .add_room_event(EventsJson::Member) -/// .add_room_event(EventsJson::PowerLevels) -/// .build_sync_response(); -/// -/// // response2 is now empty (nothing changed) -/// let response2 = builder.build_sync_response(); -/// -/// // response3 contains a display name change for member example -/// let response3 = builder -/// .add_room_event(EventsJson::MemberNameChange) -/// .build_sync_response(); -/// ``` - -#[derive(Default)] -pub struct EventBuilder { - /// The events that determine the state of a `Room`. - joined_room_events: HashMap>>, - /// The events that determine the state of a `Room`. - invited_room_events: HashMap>>, - /// The events that determine the state of a `Room`. - left_room_events: HashMap>>, - /// The presence events that determine the presence state of a `RoomMember`. - presence_events: Vec, - /// The state events that determine the state of a `Room`. - state_events: Vec>, - /// The ephemeral room events that determine the state of a `Room`. - ephemeral: Vec>, - /// The account data events that determine the state of a `Room`. - account_data: Vec>, - /// Internal counter to enable the `prev_batch` and `next_batch` of each - /// sync response to vary. - batch_counter: i64, -} - -impl EventBuilder { - pub fn new() -> Self { - let builder: EventBuilder = Default::default(); - builder - } - - /// Add an event to the room events `Vec`. - pub fn add_ephemeral(&mut self, json: EventsJson) -> &mut Self { - let val: &JsonValue = match json { - EventsJson::ReadReceipt => &test_json::READ_RECEIPT, - EventsJson::ReadReceiptOther => &test_json::READ_RECEIPT_OTHER, - EventsJson::Typing => &test_json::TYPING, - _ => panic!("unknown ephemeral event {:?}", json), - }; - - let event = serde_json::from_value(val.clone()).unwrap(); - self.ephemeral.push(event); - self - } - - /// Add an event to the room events `Vec`. - #[allow(unused)] - pub fn add_account(&mut self, json: EventsJson) -> &mut Self { - let val: &JsonValue = match json { - EventsJson::PushRules => &test_json::PUSH_RULES, - _ => panic!("unknown account event {:?}", json), - }; - - let event = serde_json::from_value(val.clone()).unwrap(); - self.account_data.push(event); - self - } - - /// Add an event to the room events `Vec`. - pub fn add_room_event(&mut self, json: EventsJson) -> &mut Self { - let val: &JsonValue = match json { - EventsJson::Member => &test_json::MEMBER, - EventsJson::MemberInvite => &test_json::MEMBER_INVITE, - EventsJson::MemberNameChange => &test_json::MEMBER_NAME_CHANGE, - EventsJson::PowerLevels => &test_json::POWER_LEVELS, - _ => panic!("unknown room event json {:?}", json), - }; - - let event = serde_json::from_value(val.clone()).unwrap(); - - self.add_joined_event(room_id!("!SVkFJHzfwvuaIEawgC:localhost"), event); - self - } - - pub fn add_custom_joined_event( - &mut self, - room_id: &RoomId, - event: serde_json::Value, - ) -> &mut Self { - let event = serde_json::from_value(event).unwrap(); - self.add_joined_event(room_id, event); - self - } - - fn add_joined_event(&mut self, room_id: &RoomId, event: Raw) { - self.joined_room_events.entry(room_id.to_owned()).or_default().push(event); - } - - pub fn add_custom_invited_event( - &mut self, - room_id: &RoomId, - event: serde_json::Value, - ) -> &mut Self { - let event = serde_json::from_value(event).unwrap(); - self.invited_room_events.entry(room_id.to_owned()).or_default().push(event); - self - } - - pub fn add_custom_left_event( - &mut self, - room_id: &RoomId, - event: serde_json::Value, - ) -> &mut Self { - let event = serde_json::from_value(event).unwrap(); - self.left_room_events.entry(room_id.to_owned()).or_default().push(event); - self - } - - /// Add a state event to the state events `Vec`. - pub fn add_state_event(&mut self, json: EventsJson) -> &mut Self { - let val: &JsonValue = match json { - EventsJson::Alias => &test_json::ALIAS, - EventsJson::Aliases => &test_json::ALIASES, - EventsJson::Name => &test_json::NAME, - EventsJson::Member => &test_json::MEMBER, - EventsJson::PowerLevels => &test_json::POWER_LEVELS, - EventsJson::Encryption => &test_json::ENCRYPTION, - _ => panic!("unknown state event {:?}", json), - }; - - let event = serde_json::from_value(val.clone()).unwrap(); - self.state_events.push(event); - self - } - - /// Add an presence event to the presence events `Vec`. - pub fn add_presence_event(&mut self, json: EventsJson) -> &mut Self { - let val: &JsonValue = match json { - EventsJson::Presence => &test_json::PRESENCE, - _ => panic!("unknown presence event {:?}", json), - }; - - let event = serde_json::from_value::(val.clone()).unwrap(); - self.presence_events.push(event); - self - } - - /// Builds a sync response as a JSON Value containing the events we queued - /// so far. - /// - /// The next response returned by `build_sync_response` will then be empty - /// if no further events were queued. - /// - /// This method is raw JSON equivalent to - /// [build_sync_response()](#method.build_sync_response), use - /// [build_sync_response()](#method.build_sync_response) if you need a typed - /// response. - pub fn build_json_sync_response(&mut self) -> JsonValue { - let main_room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost"); - - // First time building a sync response, so initialize the `prev_batch` to a - // default one. - let prev_batch = self.generate_sync_token(); - self.batch_counter += 1; - let next_batch = self.generate_sync_token(); - - // TODO generalize this. - let joined_room = serde_json::json!({ - "summary": {}, - "account_data": { - "events": self.account_data - }, - "ephemeral": { - "events": self.ephemeral - }, - "state": { - "events": self.state_events - }, - "timeline": { - "events": self.joined_room_events.remove(main_room_id).unwrap_or_default(), - "limited": true, - "prev_batch": prev_batch - }, - "unread_notifications": { - "highlight_count": 0, - "notification_count": 11 - } - }); - - let mut joined_rooms = HashMap::new(); - - joined_rooms.insert(main_room_id.to_owned(), joined_room); - - for (room_id, events) in self.joined_room_events.drain() { - let joined_room = serde_json::json!({ - "summary": {}, - "account_data": { - "events": [], - }, - "ephemeral": { - "events": [], - }, - "state": { - "events": [], - }, - "timeline": { - "events": events, - "limited": true, - "prev_batch": prev_batch - }, - "unread_notifications": { - "highlight_count": 0, - "notification_count": 11 - } - }); - joined_rooms.insert(room_id, joined_room); - } - - let mut left_rooms = HashMap::new(); - - for (room_id, events) in self.left_room_events.drain() { - let room = serde_json::json!({ - "state": { - "events": [], - }, - "timeline": { - "events": events, - "limited": false, - "prev_batch": prev_batch - }, - }); - left_rooms.insert(room_id, room); - } - - let mut invited_rooms = HashMap::new(); - - for (room_id, events) in self.invited_room_events.drain() { - let room = serde_json::json!({ - "invite_state": { - "events": events, - }, - }); - invited_rooms.insert(room_id, room); - } - - let body = serde_json::json! { - { - "device_one_time_keys_count": {}, - "next_batch": next_batch, - "device_lists": { - "changed": [], - "left": [] - }, - "rooms": { - "invite": invited_rooms, - "join": joined_rooms, - "leave": left_rooms, - }, - "to_device": { - "events": [] - }, - "presence": { - "events": [] - } - } - }; - - // Clear state so that the next sync response will be empty if nothing - // was added. - self.clear(); - - body - } - - /// Builds a `SyncResponse` containing the events we queued so far. - /// - /// The next response returned by `build_sync_response` will then be empty - /// if no further events were queued. - /// - /// This method is high level and typed equivalent to - /// [build_json_sync_response()](#method.build_json_sync_response), use - /// [build_json_sync_response()](#method.build_json_sync_response) if you - /// need an untyped response. - pub fn build_sync_response(&mut self) -> SyncResponse { - let body = self.build_json_sync_response(); - - let response = Response::builder().body(serde_json::to_vec(&body).unwrap()).unwrap(); - - SyncResponse::try_from_http_response(response).unwrap() - } - - fn generate_sync_token(&self) -> String { - format!("t392-516_47314_0_7_1_1_1_11444_{}", self.batch_counter) - } - - pub fn clear(&mut self) { - self.account_data.clear(); - self.ephemeral.clear(); - self.invited_room_events.clear(); - self.joined_room_events.clear(); - self.left_room_events.clear(); - self.presence_events.clear(); - self.state_events.clear(); - } -} +pub use event_builder::{ + bulk_room_members, EphemeralTestEvent, EventBuilder, GlobalAccountDataTestEvent, + InvitedRoomBuilder, JoinedRoomBuilder, LeftRoomBuilder, PresenceTestEvent, + RoomAccountDataTestEvent, StateTestEvent, StrippedStateTestEvent, TimelineTestEvent, +}; /// Embedded sync response files pub enum SyncResponseFile { @@ -402,6 +39,6 @@ pub fn sync_response(kind: SyncResponseFile) -> SyncResponse { SyncResponse::try_from_http_response(response).unwrap() } -pub fn response_from_file(json: &serde_json::Value) -> Response> { +pub fn response_from_file(json: &JsonValue) -> Response> { Response::builder().status(200).body(json.to_string().as_bytes().to_vec()).unwrap() } diff --git a/crates/matrix-sdk-test/src/test_json/api_responses.rs b/crates/matrix-sdk-test/src/test_json/api_responses.rs new file mode 100644 index 000000000..9007fe061 --- /dev/null +++ b/crates/matrix-sdk-test/src/test_json/api_responses.rs @@ -0,0 +1,213 @@ +//! Responses to client API calls. + +use once_cell::sync::Lazy; +use serde_json::{json, Value as JsonValue}; + +/// `GET /_matrix/client/v3/devices` +pub static DEVICES: Lazy = Lazy::new(|| { + json!({ + "devices": [ + { + "device_id": "BNYQQWUMXO", + "display_name": "Client 1", + "last_seen_ip": "-", + "last_seen_ts": 1596117733037u64, + "user_id": "@example:localhost" + }, + { + "device_id": "LEBKSEUSNR", + "display_name": "Client 2", + "last_seen_ip": "-", + "last_seen_ts": 1599057006985u64, + "user_id": "@example:localhost" + } + ] + }) +}); + +/// `GET /_matrix/client/v3/directory/room/{roomAlias}` +pub static GET_ALIAS: Lazy = Lazy::new(|| { + json!({ + "room_id": "!lUbmUPdxdXxEQurqOs:example.net", + "servers": [ + "example.org", + "example.net", + "matrix.org", + ] + }) +}); + +/// `POST /_matrix/client/v3/keys/query` +pub static KEYS_QUERY: Lazy = Lazy::new(|| { + json!({ + "device_keys": { + "@alice:example.org": { + "JLAFKJWSCS": { + "algorithms": [ + "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" + ], + "device_id": "JLAFKJWSCS", + "user_id": "@alice:example.org", + "keys": { + "curve25519:JLAFKJWSCS": "wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4", + "ed25519:JLAFKJWSCS": "nE6W2fCblxDcOFmeEtCHNl8/l8bXcu7GKyAswA4r3mM" + }, + "signatures": { + "@alice:example.org": { + "ed25519:JLAFKJWSCS": "m53Wkbh2HXkc3vFApZvCrfXcX3AI51GsDHustMhKwlv3TuOJMj4wistcOTM8q2+e/Ro7rWFUb9ZfnNbwptSUBA" + } + }, + "unsigned": { + "device_display_name": "Alice's mobile phone" + } + } + } + }, + "failures": {} + }) +}); + +/// `` +pub static KEYS_UPLOAD: Lazy = Lazy::new(|| { + json!({ + "one_time_key_counts": { + "curve25519": 10, + "signed_curve25519": 20 + } + }) +}); + +/// Successful call to `POST /_matrix/client/v3/login` without auto-discovery. +pub static LOGIN: Lazy = Lazy::new(|| { + json!({ + "access_token": "abc123", + "device_id": "GHTYAJCE", + "home_server": "matrix.org", + "user_id": "@cheeky_monkey:matrix.org" + }) +}); + +/// Successful call to `POST /_matrix/client/v3/login` with auto-discovery. +pub static LOGIN_WITH_DISCOVERY: Lazy = Lazy::new(|| { + json!({ + "access_token": "abc123", + "device_id": "GHTYAJCE", + "home_server": "matrix.org", + "user_id": "@cheeky_monkey:matrix.org", + "well_known": { + "m.homeserver": { + "base_url": "https://example.org" + }, + "m.identity_server": { + "base_url": "https://id.example.org" + } + } + }) +}); + +/// Failed call to `POST /_matrix/client/v3/login` +pub static LOGIN_RESPONSE_ERR: Lazy = Lazy::new(|| { + json!({ + "errcode": "M_FORBIDDEN", + "error": "Invalid password" + }) +}); + +/// `GET /_matrix/client/v3/login` +pub static LOGIN_TYPES: Lazy = Lazy::new(|| { + json!({ + "flows": [ + { + "type": "m.login.password" + }, + { + "type": "m.login.sso" + }, + { + "type": "m.login.token" + } + ] + }) +}); + +/// `GET /_matrix/client/v3/publicRooms` +pub static PUBLIC_ROOMS: Lazy = Lazy::new(|| { + json!({ + "chunk": [ + { + "aliases": [ + "#murrays:cheese.bar" + ], + "avatar_url": "mxc://bleeker.street/CHEDDARandBRIE", + "guest_can_join": false, + "name": "CHEESE", + "num_joined_members": 37, + "room_id": "!ol19s:bleecker.street", + "topic": "Tasty tasty cheese", + "world_readable": true + } + ], + "next_batch": "p190q", + "prev_batch": "p1902", + "total_room_count_estimate": 115 + }) +}); + +/// Failed call to `POST /_matrix/client/v3/register` +pub static REGISTRATION_RESPONSE_ERR: Lazy = Lazy::new(|| { + json!({ + "errcode": "M_FORBIDDEN", + "error": "Invalid password", + "completed": ["example.type.foo"], + "flows": [ + { + "stages": ["example.type.foo", "example.type.bar"] + }, + { + "stages": ["example.type.foo", "example.type.baz"] + } + ], + "params": { + "example.type.baz": { + "example_key": "foobar" + } + }, + "session": "xxxxxx" + }) +}); + +/// `GET /_matrix/client/versions` +pub static VERSIONS: Lazy = Lazy::new(|| { + json!({ + "versions": [ + "r0.0.1", + "r0.1.0", + "r0.2.0", + "r0.3.0", + "r0.4.0", + "r0.5.0", + "r0.6.0" + ], + "unstable_features": { + "org.matrix.label_based_filtering":true, + "org.matrix.e2e_cross_signing":true + } + }) +}); + +/// `GET /.well-known/matrix/client` +pub static WELL_KNOWN: Lazy = Lazy::new(|| { + json!({ + "m.homeserver": { + "base_url": "HOMESERVER_URL" + } + }) +}); + +/// `GET /_matrix/client/v3/account/whoami` +pub static WHOAMI: Lazy = Lazy::new(|| { + json!({ + "user_id": "@joe:example.org" + }) +}); diff --git a/crates/matrix-sdk-test/src/test_json/members.rs b/crates/matrix-sdk-test/src/test_json/members.rs index 94bc7df7e..e875047ef 100644 --- a/crates/matrix-sdk-test/src/test_json/members.rs +++ b/crates/matrix-sdk-test/src/test_json/members.rs @@ -1,3 +1,5 @@ +//! Example responses to `GET /_matrix/client/v3/rooms/{roomId}/members` + use once_cell::sync::Lazy; use serde_json::{json, Value as JsonValue}; diff --git a/crates/matrix-sdk-test/src/test_json/messages.rs b/crates/matrix-sdk-test/src/test_json/messages.rs new file mode 100644 index 000000000..a86238033 --- /dev/null +++ b/crates/matrix-sdk-test/src/test_json/messages.rs @@ -0,0 +1,149 @@ +//! Example responses from calls to `GET +//! /_matrix/client/v3/rooms/{roomId}/messages`. + +use once_cell::sync::Lazy; +use serde_json::{json, Value as JsonValue}; + +pub static ROOM_MESSAGES: Lazy = Lazy::new(|| { + json!({ + "chunk": [ + { + "age": 1042, + "content": { + "body": "hello world", + "msgtype": "m.text" + }, + "event_id": "$1444812213350496Caaaa:example.com", + "origin_server_ts": 1444812213737i64, + "room_id": "!Xq3620DUiqCaoxq:example.com", + "sender": "@alice:example.com", + "type": "m.room.message" + }, + { + "age": 20123, + "content": { + "body": "the world is big", + "msgtype": "m.text" + }, + "event_id": "$1444812213350496Cbbbb:example.com", + "origin_server_ts": 1444812194656i64, + "room_id": "!Xq3620DUiqCaoxq:example.com", + "sender": "@bob:example.com", + "type": "m.room.message" + }, + { + "age": 50789, + "content": { + "name": "New room name" + }, + "event_id": "$1444812213350496Ccccc:example.com", + "origin_server_ts": 1444812163990i64, + "prev_content": { + "name": "Old room name" + }, + "room_id": "!Xq3620DUiqCaoxq:example.com", + "sender": "@bob:example.com", + "state_key": "", + "type": "m.room.name" + } + ], + "end": "t47409-4357353_219380_26003_2265", + "start": "t47429-4392820_219380_26003_2265" + }) +}); + +pub static ROOM_MESSAGES_BATCH_1: Lazy = Lazy::new(|| { + json!({ + "chunk": [ + { + "age": 1042, + "content": { + "body": "hello world", + "msgtype": "m.text" + }, + "event_id": "$1444812213350496Caaaf:example.com", + "origin_server_ts": 1444812213737i64, + "room_id": "!Xq3620DUiqCaoxq:example.com", + "sender": "@alice:example.com", + "type": "m.room.message" + }, + { + "age": 20123, + "content": { + "body": "the world is big", + "msgtype": "m.text" + }, + "event_id": "$1444812213350496Cbbbf:example.com", + "origin_server_ts": 1444812194656i64, + "room_id": "!Xq3620DUiqCaoxq:example.com", + "sender": "@bob:example.com", + "type": "m.room.message" + }, + { + "age": 50789, + "content": { + "name": "New room name" + }, + "event_id": "$1444812213350496Ccccf:example.com", + "origin_server_ts": 1444812163990i64, + "prev_content": { + "name": "Old room name" + }, + "room_id": "!Xq3620DUiqCaoxq:example.com", + "sender": "@bob:example.com", + "state_key": "", + "type": "m.room.name" + } + ], + "end": "t47409-4357353_219380_26003_2269", + "start": "t392-516_47314_0_7_1_1_1_11444_1" + }) +}); + +pub static ROOM_MESSAGES_BATCH_2: Lazy = Lazy::new(|| { + json!({ + "chunk": [ + { + "age": 1042, + "content": { + "body": "hello world", + "msgtype": "m.text" + }, + "event_id": "$1444812213350496Caaak:example.com", + "origin_server_ts": 1444812213737i64, + "room_id": "!Xq3620DUiqCaoxq:example.com", + "sender": "@alice:example.com", + "type": "m.room.message" + }, + { + "age": 20123, + "content": { + "body": "the world is big", + "msgtype": "m.text" + }, + "event_id": "$1444812213350496Cbbbk:example.com", + "origin_server_ts": 1444812194656i64, + "room_id": "!Xq3620DUiqCaoxq:example.com", + "sender": "@bob:example.com", + "type": "m.room.message" + }, + { + "age": 50789, + "content": { + "name": "New room name" + }, + "event_id": "$1444812213350496Cccck:example.com", + "origin_server_ts": 1444812163990i64, + "prev_content": { + "name": "Old room name" + }, + "room_id": "!Xq3620DUiqCaoxq:example.com", + "sender": "@bob:example.com", + "state_key": "", + "type": "m.room.name" + } + ], + "end": "t47409-4357353_219380_26003_2270", + "start": "t47409-4357353_219380_26003_2269" + }) +}); diff --git a/crates/matrix-sdk-test/src/test_json/mod.rs b/crates/matrix-sdk-test/src/test_json/mod.rs index c05d7f3b7..bf469e94c 100644 --- a/crates/matrix-sdk-test/src/test_json/mod.rs +++ b/crates/matrix-sdk-test/src/test_json/mod.rs @@ -7,84 +7,42 @@ use once_cell::sync::Lazy; use serde_json::{json, Value as JsonValue}; -pub mod events; +pub mod api_responses; pub mod members; +pub mod messages; pub mod sync; +pub mod sync_events; -pub use events::{ - ALIAS, ALIASES, EMPTY, ENCRYPTION, EVENT_ID, KEYS_QUERY, KEYS_UPLOAD, LOGIN, - LOGIN_RESPONSE_ERR, LOGIN_TYPES, LOGIN_WITH_DISCOVERY, MEMBER, MEMBER_INVITE, - MEMBER_NAME_CHANGE, MEMBER_STRIPPED, MESSAGE_EDIT, MESSAGE_TEXT, NAME, NAME_STRIPPED, - POWER_LEVELS, PRESENCE, PUBLIC_ROOMS, PUSH_RULES, REACTION, READ_RECEIPT, READ_RECEIPT_OTHER, - REDACTED, REDACTED_INVALID, REDACTED_STATE, REDACTION, REGISTRATION_RESPONSE_ERR, ROOM_ID, - ROOM_MESSAGES, SYNC_ROOM_MESSAGES_BATCH_1, SYNC_ROOM_MESSAGES_BATCH_2, TAG, TOPIC, TYPING, +pub use api_responses::{ + DEVICES, GET_ALIAS, KEYS_QUERY, KEYS_UPLOAD, LOGIN, LOGIN_RESPONSE_ERR, LOGIN_TYPES, + LOGIN_WITH_DISCOVERY, PUBLIC_ROOMS, REGISTRATION_RESPONSE_ERR, VERSIONS, WELL_KNOWN, WHOAMI, }; pub use members::MEMBERS; +pub use messages::{ROOM_MESSAGES, ROOM_MESSAGES_BATCH_1, ROOM_MESSAGES_BATCH_2}; pub use sync::{ - DEFAULT_SYNC_SUMMARY, INVITE_SYNC, LEAVE_SYNC, LEAVE_SYNC_EVENT, MORE_SYNC, MORE_SYNC_2, SYNC, - VOIP_SYNC, + DEFAULT_SYNC_ROOM_ID, DEFAULT_SYNC_SUMMARY, INVITE_SYNC, LEAVE_SYNC, LEAVE_SYNC_EVENT, + MORE_SYNC, MORE_SYNC_2, SYNC, VOIP_SYNC, +}; +pub use sync_events::{ + ALIAS, ALIASES, ENCRYPTION, MEMBER, MEMBER_INVITE, MEMBER_NAME_CHANGE, MEMBER_STRIPPED, + MESSAGE_EDIT, MESSAGE_TEXT, NAME, NAME_STRIPPED, POWER_LEVELS, PRESENCE, PUSH_RULES, REACTION, + READ_RECEIPT, READ_RECEIPT_OTHER, REDACTED, REDACTED_INVALID, REDACTED_STATE, REDACTION, TAG, + TOPIC, TYPING, }; -pub static DEVICES: Lazy = Lazy::new(|| { +/// An empty response. +pub static EMPTY: Lazy = Lazy::new(|| json!({})); + +/// A response with only an event ID. +pub static EVENT_ID: Lazy = Lazy::new(|| { json!({ - "devices": [ - { - "device_id": "BNYQQWUMXO", - "display_name": "Client 1", - "last_seen_ip": "-", - "last_seen_ts": 1596117733037u64, - "user_id": "@example:localhost" - }, - { - "device_id": "LEBKSEUSNR", - "display_name": "Client 2", - "last_seen_ip": "-", - "last_seen_ts": 1599057006985u64, - "user_id": "@example:localhost" - } - ] + "event_id": "$h29iv0s8:example.com" }) }); -pub static GET_ALIAS: Lazy = Lazy::new(|| { +/// A response with only a room ID. +pub static ROOM_ID: Lazy = Lazy::new(|| { json!({ - "room_id": "!lUbmUPdxdXxEQurqOs:example.net", - "servers": [ - "example.org", - "example.net", - "matrix.org", - ] - }) -}); - -pub static WELL_KNOWN: Lazy = Lazy::new(|| { - json!({ - "m.homeserver": { - "base_url": "HOMESERVER_URL" - } - }) -}); - -pub static VERSIONS: Lazy = Lazy::new(|| { - json!({ - "versions": [ - "r0.0.1", - "r0.1.0", - "r0.2.0", - "r0.3.0", - "r0.4.0", - "r0.5.0", - "r0.6.0" - ], - "unstable_features": { - "org.matrix.label_based_filtering":true, - "org.matrix.e2e_cross_signing":true - } - }) -}); - -pub static WHOAMI: Lazy = Lazy::new(|| { - json!({ - "user_id": "@joe:example.org" + "room_id": "!testroom:example.org" }) }); diff --git a/crates/matrix-sdk-test/src/test_json/sync.rs b/crates/matrix-sdk-test/src/test_json/sync.rs index 265534359..fdf4c9cf3 100644 --- a/crates/matrix-sdk-test/src/test_json/sync.rs +++ b/crates/matrix-sdk-test/src/test_json/sync.rs @@ -1,6 +1,13 @@ +//! Complete sync responses. + use once_cell::sync::Lazy; +use ruma::{room_id, RoomId}; use serde_json::{json, Value as JsonValue}; +/// The default room ID where sync events are added. +pub static DEFAULT_SYNC_ROOM_ID: Lazy<&RoomId> = + Lazy::new(|| room_id!("!SVkFJHzfwvuaIEawgC:localhost")); + pub static SYNC: Lazy = Lazy::new(|| { json!({ "device_one_time_keys_count": {}, diff --git a/crates/matrix-sdk-test/src/test_json/events.rs b/crates/matrix-sdk-test/src/test_json/sync_events.rs similarity index 73% rename from crates/matrix-sdk-test/src/test_json/events.rs rename to crates/matrix-sdk-test/src/test_json/sync_events.rs index 5bb5a4d99..04eab8456 100644 --- a/crates/matrix-sdk-test/src/test_json/events.rs +++ b/crates/matrix-sdk-test/src/test_json/sync_events.rs @@ -1,3 +1,5 @@ +//! Discrete events found in a sync response. + use once_cell::sync::Lazy; use serde_json::{json, Value as JsonValue}; @@ -95,246 +97,6 @@ pub static JOIN_RULES: Lazy = Lazy::new(|| { }) }); -pub static ROOM_MESSAGES: Lazy = Lazy::new(|| { - json!({ - "chunk": [ - { - "age": 1042, - "content": { - "body": "hello world", - "msgtype": "m.text" - }, - "event_id": "$1444812213350496Caaaa:example.com", - "origin_server_ts": 1444812213737i64, - "room_id": "!Xq3620DUiqCaoxq:example.com", - "sender": "@alice:example.com", - "type": "m.room.message" - }, - { - "age": 20123, - "content": { - "body": "the world is big", - "msgtype": "m.text" - }, - "event_id": "$1444812213350496Cbbbb:example.com", - "origin_server_ts": 1444812194656i64, - "room_id": "!Xq3620DUiqCaoxq:example.com", - "sender": "@bob:example.com", - "type": "m.room.message" - }, - { - "age": 50789, - "content": { - "name": "New room name" - }, - "event_id": "$1444812213350496Ccccc:example.com", - "origin_server_ts": 1444812163990i64, - "prev_content": { - "name": "Old room name" - }, - "room_id": "!Xq3620DUiqCaoxq:example.com", - "sender": "@bob:example.com", - "state_key": "", - "type": "m.room.name" - } - ], - "end": "t47409-4357353_219380_26003_2265", - "start": "t47429-4392820_219380_26003_2265" - }) -}); - -pub static SYNC_ROOM_MESSAGES_BATCH_1: Lazy = Lazy::new(|| { - json!({ - "chunk": [ - { - "age": 1042, - "content": { - "body": "hello world", - "msgtype": "m.text" - }, - "event_id": "$1444812213350496Caaaf:example.com", - "origin_server_ts": 1444812213737i64, - "room_id": "!Xq3620DUiqCaoxq:example.com", - "sender": "@alice:example.com", - "type": "m.room.message" - }, - { - "age": 20123, - "content": { - "body": "the world is big", - "msgtype": "m.text" - }, - "event_id": "$1444812213350496Cbbbf:example.com", - "origin_server_ts": 1444812194656i64, - "room_id": "!Xq3620DUiqCaoxq:example.com", - "sender": "@bob:example.com", - "type": "m.room.message" - }, - { - "age": 50789, - "content": { - "name": "New room name" - }, - "event_id": "$1444812213350496Ccccf:example.com", - "origin_server_ts": 1444812163990i64, - "prev_content": { - "name": "Old room name" - }, - "room_id": "!Xq3620DUiqCaoxq:example.com", - "sender": "@bob:example.com", - "state_key": "", - "type": "m.room.name" - } - ], - "end": "t47409-4357353_219380_26003_2269", - "start": "t392-516_47314_0_7_1_1_1_11444_1" - }) -}); - -pub static SYNC_ROOM_MESSAGES_BATCH_2: Lazy = Lazy::new(|| { - json!({ - "chunk": [ - { - "age": 1042, - "content": { - "body": "hello world", - "msgtype": "m.text" - }, - "event_id": "$1444812213350496Caaak:example.com", - "origin_server_ts": 1444812213737i64, - "room_id": "!Xq3620DUiqCaoxq:example.com", - "sender": "@alice:example.com", - "type": "m.room.message" - }, - { - "age": 20123, - "content": { - "body": "the world is big", - "msgtype": "m.text" - }, - "event_id": "$1444812213350496Cbbbk:example.com", - "origin_server_ts": 1444812194656i64, - "room_id": "!Xq3620DUiqCaoxq:example.com", - "sender": "@bob:example.com", - "type": "m.room.message" - }, - { - "age": 50789, - "content": { - "name": "New room name" - }, - "event_id": "$1444812213350496Cccck:example.com", - "origin_server_ts": 1444812163990i64, - "prev_content": { - "name": "Old room name" - }, - "room_id": "!Xq3620DUiqCaoxq:example.com", - "sender": "@bob:example.com", - "state_key": "", - "type": "m.room.name" - } - ], - "end": "t47409-4357353_219380_26003_2270", - "start": "t47409-4357353_219380_26003_2269" - }) -}); - -pub static KEYS_QUERY: Lazy = Lazy::new(|| { - json!({ - "device_keys": { - "@alice:example.org": { - "JLAFKJWSCS": { - "algorithms": [ - "m.olm.v1.curve25519-aes-sha2", - "m.megolm.v1.aes-sha2" - ], - "device_id": "JLAFKJWSCS", - "user_id": "@alice:example.org", - "keys": { - "curve25519:JLAFKJWSCS": "wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4", - "ed25519:JLAFKJWSCS": "nE6W2fCblxDcOFmeEtCHNl8/l8bXcu7GKyAswA4r3mM" - }, - "signatures": { - "@alice:example.org": { - "ed25519:JLAFKJWSCS": "m53Wkbh2HXkc3vFApZvCrfXcX3AI51GsDHustMhKwlv3TuOJMj4wistcOTM8q2+e/Ro7rWFUb9ZfnNbwptSUBA" - } - }, - "unsigned": { - "device_display_name": "Alice's mobile phone" - } - } - } - }, - "failures": {} - }) -}); - -pub static KEYS_UPLOAD: Lazy = Lazy::new(|| { - json!({ - "one_time_key_counts": { - "curve25519": 10, - "signed_curve25519": 20 - } - }) -}); - -pub static LOGIN: Lazy = Lazy::new(|| { - json!({ - "access_token": "abc123", - "device_id": "GHTYAJCE", - "home_server": "matrix.org", - "user_id": "@cheeky_monkey:matrix.org" - }) -}); - -pub static LOGIN_WITH_DISCOVERY: Lazy = Lazy::new(|| { - json!({ - "access_token": "abc123", - "device_id": "GHTYAJCE", - "home_server": "matrix.org", - "user_id": "@cheeky_monkey:matrix.org", - "well_known": { - "m.homeserver": { - "base_url": "https://example.org" - }, - "m.identity_server": { - "base_url": "https://id.example.org" - } - } - }) -}); - -pub static LOGIN_RESPONSE_ERR: Lazy = Lazy::new(|| { - json!({ - "errcode": "M_FORBIDDEN", - "error": "Invalid password" - }) -}); - -pub static LOGIN_TYPES: Lazy = Lazy::new(|| { - json!({ - "flows": [ - { - "type": "m.login.password" - }, - { - "type": "m.login.sso" - }, - { - "type": "m.login.token" - } - ] - }) -}); - -pub static EMPTY: Lazy = Lazy::new(|| json!({})); - -pub static EVENT_ID: Lazy = Lazy::new(|| { - json!({ - "event_id": "$h29iv0s8:example.com" - }) -}); - pub static ENCRYPTION: Lazy = Lazy::new(|| { json!({ "content": { @@ -609,28 +371,6 @@ pub static PRESENCE: Lazy = Lazy::new(|| { }) }); -pub static PUBLIC_ROOMS: Lazy = Lazy::new(|| { - json!({ - "chunk": [ - { - "aliases": [ - "#murrays:cheese.bar" - ], - "avatar_url": "mxc://bleeker.street/CHEDDARandBRIE", - "guest_can_join": false, - "name": "CHEESE", - "num_joined_members": 37, - "room_id": "!ol19s:bleecker.street", - "topic": "Tasty tasty cheese", - "world_readable": true - } - ], - "next_batch": "p190q", - "prev_batch": "p1902", - "total_room_count_estimate": 115 - }) -}); - pub static PUSH_RULES: Lazy = Lazy::new(|| { json!({ "content": { @@ -830,28 +570,6 @@ pub static PUSH_RULES: Lazy = Lazy::new(|| { }) }); -pub static REGISTRATION_RESPONSE_ERR: Lazy = Lazy::new(|| { - json!({ - "errcode": "M_FORBIDDEN", - "error": "Invalid password", - "completed": ["example.type.foo"], - "flows": [ - { - "stages": ["example.type.foo", "example.type.bar"] - }, - { - "stages": ["example.type.foo", "example.type.baz"] - } - ], - "params": { - "example.type.baz": { - "example_key": "foobar" - } - }, - "session": "xxxxxx" - }) -}); - pub static REACTION: Lazy = Lazy::new(|| { json!({ "content": { @@ -998,12 +716,6 @@ pub static ROOM_AVATAR: Lazy = Lazy::new(|| { }) }); -pub static ROOM_ID: Lazy = Lazy::new(|| { - json!({ - "room_id": "!testroom:example.org" - }) -}); - pub static TAG: Lazy = Lazy::new(|| { json!({ "content": { diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index b8421c629..1b511b14b 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -28,18 +28,18 @@ e2e-encryption = [ "matrix-sdk-indexeddb?/e2e-encryption", # activate on indexeddb if given ] -sled = ["matrix-sdk-sled/state-store"] -indexeddb = ["matrix-sdk-indexeddb"] +sled = ["dep:matrix-sdk-sled", "matrix-sdk-sled?/state-store"] +indexeddb = ["dep:matrix-sdk-indexeddb"] qrcode = ["e2e-encryption", "matrix-sdk-base/qrcode"] markdown = ["ruma/markdown"] native-tls = ["reqwest/native-tls"] rustls-tls = ["reqwest/rustls-tls"] socks = ["reqwest/socks"] -sso-login = ["warp", "rand", "tokio-stream"] +sso-login = ["warp", "dep:rand", "dep:tokio-stream"] appservice = ["ruma/appservice-api-s"] -image-proc = ["image"] -image-rayon = ["image-proc", "image/jpeg_rayon"] +image-proc = ["dep:image"] +image-rayon = ["image-proc", "image?/jpeg_rayon"] experimental-timeline = [ "matrix-sdk-base/experimental-timeline", @@ -110,13 +110,8 @@ default_features = false [dependencies.ruma] git = "https://github.com/ruma/ruma" -rev = "96155915f" -features = ["client-api-c", "compat", "rand", "unstable-msc2448"] - -[dependencies.ruma-client-api] -git = "https://github.com/ruma/ruma" -rev = "96155915f" -features = ["unstable-msc2965"] +rev = "ca8c66c885241a7ba3805399604eda4a38979f6b" +features = ["client-api-c", "compat", "rand", "unstable-msc2448", "unstable-msc2965"] [dependencies.tokio-stream] version = "0.1.8" diff --git a/crates/matrix-sdk/src/client/builder.rs b/crates/matrix-sdk/src/client/builder.rs index 77e5c639f..fa9b17c1f 100644 --- a/crates/matrix-sdk/src/client/builder.rs +++ b/crates/matrix-sdk/src/client/builder.rs @@ -239,8 +239,11 @@ impl ClientBuilder { /// All outgoing http requests will have a GET query key-value appended with /// `user_id` being the key and the `user_id` from the `Session` being - /// the value. Will error if there's no `Session`. This is called - /// [identity assertion] in the Matrix Application Service Spec + /// the value. This is called [identity assertion] in the + /// Matrix Application Service Spec. + /// + /// Requests that don't require authentication might not do identity + /// assertion. /// /// [identity assertion]: https://spec.matrix.org/unstable/application-service-api/#identity-assertion #[doc(hidden)] diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 57d312382..61840003c 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -30,7 +30,7 @@ use futures_core::stream::Stream; use matrix_sdk_base::{ deserialized_responses::SyncResponse, media::{MediaEventContent, MediaFormat, MediaRequest, MediaThumbnailSize}, - BaseClient, Session, Store, + BaseClient, Session, StateStore, }; use matrix_sdk_common::{ instant::{Duration, Instant}, @@ -320,12 +320,12 @@ impl Client { /// Can be used with [`Client::restore_login`] to restore a previously /// logged-in session. pub fn session(&self) -> Option<&Session> { - self.store().session() + self.base_client().session() } - /// Get a reference to the store. - pub fn store(&self) -> &Store { - self.inner.base_client.store() + /// Get a reference to the state store. + pub fn store(&self) -> &dyn StateStore { + self.base_client().store() } /// Get the account of the current owner of the client. @@ -564,7 +564,7 @@ impl Client { /// /// This will return the list of joined, invited, and left rooms. pub fn rooms(&self) -> Vec { - self.store() + self.base_client() .get_rooms() .into_iter() .map(|room| room::Common::new(self.clone(), room).into()) @@ -573,7 +573,7 @@ impl Client { /// Returns the joined rooms this client knows about. pub fn joined_rooms(&self) -> Vec { - self.store() + self.base_client() .get_rooms() .into_iter() .filter_map(|room| room::Joined::new(self.clone(), room)) @@ -582,7 +582,7 @@ impl Client { /// Returns the invited rooms this client knows about. pub fn invited_rooms(&self) -> Vec { - self.store() + self.base_client() .get_stripped_rooms() .into_iter() .filter_map(|room| room::Invited::new(self.clone(), room)) @@ -591,7 +591,7 @@ impl Client { /// Returns the left rooms this client knows about. pub fn left_rooms(&self) -> Vec { - self.store() + self.base_client() .get_rooms() .into_iter() .filter_map(|room| room::Left::new(self.clone(), room)) @@ -604,7 +604,9 @@ impl Client { /// /// `room_id` - The unique id of the room that should be fetched. pub fn get_room(&self, room_id: &RoomId) -> Option { - self.store().get_room(room_id).map(|room| room::Common::new(self.clone(), room).into()) + self.base_client() + .get_room(room_id) + .map(|room| room::Common::new(self.clone(), room).into()) } /// Get a joined room with the given room id. @@ -613,7 +615,7 @@ impl Client { /// /// `room_id` - The unique id of the room that should be fetched. pub fn get_joined_room(&self, room_id: &RoomId) -> Option { - self.store().get_room(room_id).and_then(|room| room::Joined::new(self.clone(), room)) + self.base_client().get_room(room_id).and_then(|room| room::Joined::new(self.clone(), room)) } /// Get an invited room with the given room id. @@ -622,7 +624,7 @@ impl Client { /// /// `room_id` - The unique id of the room that should be fetched. pub fn get_invited_room(&self, room_id: &RoomId) -> Option { - self.store().get_room(room_id).and_then(|room| room::Invited::new(self.clone(), room)) + self.base_client().get_room(room_id).and_then(|room| room::Invited::new(self.clone(), room)) } /// Get a left room with the given room id. @@ -631,7 +633,7 @@ impl Client { /// /// `room_id` - The unique id of the room that should be fetched. pub fn get_left_room(&self, room_id: &RoomId) -> Option { - self.store().get_room(room_id).and_then(|room| room::Left::new(self.clone(), room)) + self.base_client().get_room(room_id).and_then(|room| room::Left::new(self.clone(), room)) } /// Resolve a room alias to a room id and a list of servers which know @@ -2206,11 +2208,11 @@ impl Client { pub(crate) mod tests { use std::time::Duration; - use matrix_sdk_test::{async_test, test_json, EventBuilder, EventsJson}; + use matrix_sdk_test::{async_test, test_json, EventBuilder, JoinedRoomBuilder, StateTestEvent}; #[cfg(target_arch = "wasm32")] wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - use ruma::{api::MatrixVersion, device_id, room_id, user_id, UserId}; + use ruma::{api::MatrixVersion, device_id, user_id, UserId}; use url::Url; use wiremock::{ matchers::{header, method, path}, @@ -2316,12 +2318,15 @@ pub(crate) mod tests { let client = logged_in_client(Some(server.uri())).await; let response = EventBuilder::default() - .add_state_event(EventsJson::Member) - .add_state_event(EventsJson::PowerLevels) + .add_joined_room( + JoinedRoomBuilder::default() + .add_state_event(StateTestEvent::Member) + .add_state_event(StateTestEvent::PowerLevels), + ) .build_sync_response(); client.inner.base_client.receive_sync_response(response).await.unwrap(); - let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost"); + let room_id = &test_json::DEFAULT_SYNC_ROOM_ID; assert_eq!(client.homeserver().await, Url::parse(&server.uri()).unwrap()); diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index a76ec8d19..fd597437b 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -872,11 +872,10 @@ impl Encryption { #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { - use matrix_sdk_test::{async_test, EventBuilder, EventsJson}; + use matrix_sdk_test::{async_test, test_json, EventBuilder, JoinedRoomBuilder, StateTestEvent}; use ruma::{ event_id, events::reaction::{ReactionEventContent, Relation}, - room_id, }; use serde_json::json; use wiremock::{ @@ -892,7 +891,7 @@ mod tests { let client = logged_in_client(Some(server.uri())).await; let event_id = event_id!("$2:example.org"); - let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost"); + let room_id = &test_json::DEFAULT_SYNC_ROOM_ID; Mock::given(method("PUT")) .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/m%2Ereaction/.*".to_owned())) @@ -903,9 +902,12 @@ mod tests { .await; let response = EventBuilder::default() - .add_state_event(EventsJson::Member) - .add_state_event(EventsJson::PowerLevels) - .add_state_event(EventsJson::Encryption) + .add_joined_room( + JoinedRoomBuilder::default() + .add_state_event(StateTestEvent::Member) + .add_state_event(StateTestEvent::PowerLevels) + .add_state_event(StateTestEvent::Encryption), + ) .build_sync_response(); client.inner.base_client.receive_sync_response(response).await.unwrap(); diff --git a/crates/matrix-sdk/src/event_handler.rs b/crates/matrix-sdk/src/event_handler.rs index 49718a089..fec80128d 100644 --- a/crates/matrix-sdk/src/event_handler.rs +++ b/crates/matrix-sdk/src/event_handler.rs @@ -558,12 +558,14 @@ mod static_events { #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { - use matrix_sdk_test::async_test; + use matrix_sdk_test::{async_test, InvitedRoomBuilder, JoinedRoomBuilder}; #[cfg(target_arch = "wasm32")] wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); use std::{future, sync::Arc}; - use matrix_sdk_test::{EventBuilder, EventsJson}; + use matrix_sdk_test::{ + EphemeralTestEvent, EventBuilder, StateTestEvent, StrippedStateTestEvent, TimelineTestEvent, + }; use ruma::{ events::room::member::{OriginalSyncRoomMemberEvent, StrippedRoomMemberEvent}, room_id, @@ -618,45 +620,49 @@ mod tests { .await; let response = EventBuilder::default() - .add_room_event(EventsJson::Member) - .add_ephemeral(EventsJson::Typing) - .add_state_event(EventsJson::PowerLevels) - .add_custom_invited_event( - room_id!("!test_invited:example.org"), - json!({ - "content": { - "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF", - "displayname": "Alice", - "membership": "invite", - }, - "event_id": "$143273582443PhrSn:example.org", - "origin_server_ts": 1432735824653u64, - "room_id": "!jEsUZKDJdhlrceRyVU:example.org", - "sender": "@example:example.org", - "state_key": "@alice:example.org", - "type": "m.room.member", - "unsigned": { - "age": 1234, - "invite_room_state": [ - { - "content": { - "name": "Example Room" + .add_joined_room( + JoinedRoomBuilder::default() + .add_timeline_event(TimelineTestEvent::Member) + .add_ephemeral_event(EphemeralTestEvent::Typing) + .add_state_event(StateTestEvent::PowerLevels), + ) + .add_invited_room( + InvitedRoomBuilder::new(room_id!("!test_invited:example.org")).add_state_event( + StrippedStateTestEvent::Custom(json!({ + "content": { + "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF", + "displayname": "Alice", + "membership": "invite", + }, + "event_id": "$143273582443PhrSn:example.org", + "origin_server_ts": 1432735824653u64, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "sender": "@example:example.org", + "state_key": "@alice:example.org", + "type": "m.room.member", + "unsigned": { + "age": 1234, + "invite_room_state": [ + { + "content": { + "name": "Example Room" + }, + "sender": "@bob:example.org", + "state_key": "", + "type": "m.room.name" }, - "sender": "@bob:example.org", - "state_key": "", - "type": "m.room.name" - }, - { - "content": { - "join_rule": "invite" - }, - "sender": "@bob:example.org", - "state_key": "", - "type": "m.room.join_rules" - } - ] - } - }), + { + "content": { + "join_rule": "invite" + }, + "sender": "@bob:example.org", + "state_key": "", + "type": "m.room.join_rules" + } + ] + } + })), + ), ) .build_sync_response(); client.process_sync(response).await?; diff --git a/crates/matrix-sdk/src/http_client.rs b/crates/matrix-sdk/src/http_client.rs index 0d70eeb26..13b4fe25f 100644 --- a/crates/matrix-sdk/src/http_client.rs +++ b/crates/matrix-sdk/src/http_client.rs @@ -125,7 +125,8 @@ impl HttpClient { } trace!("Serializing request"); - let request = if !config.assert_identity { + // We can't assert the identity without a session. + let request = if !config.assert_identity || session.is_none() { let send_access_token = if auth_scheme == AuthScheme::None && !config.force_auth { // Small optimization: Don't take the session lock if we know the auth token // isn't going to be used anyways. diff --git a/crates/matrix-sdk/src/room/common.rs b/crates/matrix-sdk/src/room/common.rs index d637ccb49..9e740a9e2 100644 --- a/crates/matrix-sdk/src/room/common.rs +++ b/crates/matrix-sdk/src/room/common.rs @@ -176,13 +176,13 @@ impl Common { /// # let homeserver = Url::parse("http://example.com").unwrap(); /// # use futures::executor::block_on; /// # block_on(async { - /// let request = MessagesOptions::backward("t47429-4392820_219380_26003_2265"); + /// let options = MessagesOptions::backward().from("t47429-4392820_219380_26003_2265"); /// /// let mut client = Client::new(homeserver).await.unwrap(); /// let room = client /// .get_joined_room(room_id!("!roomid:example.com")) /// .unwrap(); - /// assert!(room.messages(request).await.is_ok()); + /// assert!(room.messages(options).await.is_ok()); /// # }); /// ``` pub async fn messages(&self, options: MessagesOptions<'_>) -> Result { @@ -264,7 +264,7 @@ impl Common { /// # Examples /// ```no_run /// # use std::convert::TryFrom; - /// use matrix_sdk::{room::MessagesOptions, Client}; + /// use matrix_sdk::Client; /// # use matrix_sdk::ruma::{ /// # api::client::filter::RoomEventFilter, /// # room_id, @@ -344,7 +344,7 @@ impl Common { /// # Examples /// ```no_run /// # use std::convert::TryFrom; - /// use matrix_sdk::{room::MessagesOptions, Client}; + /// use matrix_sdk::Client; /// # use matrix_sdk::ruma::{ /// # api::client::filter::RoomEventFilter, /// # room_id, @@ -403,7 +403,7 @@ impl Common { /// # Examples /// ```no_run /// # use std::convert::TryFrom; - /// use matrix_sdk::{room::MessagesOptions, Client}; + /// use matrix_sdk::Client; /// # use matrix_sdk::ruma::{ /// # api::client::filter::RoomEventFilter, /// # room_id, @@ -461,7 +461,8 @@ impl Common { let filter = assign!(RoomEventFilter::default(), { lazy_load_options: LazyLoadOptions::Enabled { include_redundant_members: false }, }); - let options = assign!(MessagesOptions::backward(token), { + let options = assign!(MessagesOptions::backward(), { + from: Some(token), limit: uint!(10), filter, }); @@ -1003,7 +1004,7 @@ impl Common { } let via = self.route().await?; - Ok(self.room_id().matrix_to_uri(via)) + Ok(self.room_id().matrix_to_uri_via(via)) } /// Get a `matrix:` permalink to this room. @@ -1022,7 +1023,7 @@ impl Common { } let via = self.route().await?; - Ok(self.room_id().matrix_uri(via, join)) + Ok(self.room_id().matrix_uri_via(via, join)) } /// Get a `matrix.to` permalink to an event in this room. @@ -1045,7 +1046,7 @@ impl Common { // Don't use the alias because an event is tied to a room ID, but an // alias might point to another room, e.g. after a room upgrade. let via = self.route().await?; - Ok(self.room_id().matrix_to_event_uri(event_id, via)) + Ok(self.room_id().matrix_to_event_uri_via(event_id, via)) } /// Get a `matrix:` permalink to an event in this room. @@ -1068,13 +1069,13 @@ impl Common { // Don't use the alias because an event is tied to a room ID, but an // alias might point to another room, e.g. after a room upgrade. let via = self.route().await?; - Ok(self.room_id().matrix_event_uri(event_id, via)) + Ok(self.room_id().matrix_event_uri_via(event_id, via)) } } /// Options for [`messages`][Common::messages]. /// -/// See that method for details. +/// See that method and for details. #[derive(Debug)] #[non_exhaustive] pub struct MessagesOptions<'a> { @@ -1083,7 +1084,11 @@ pub struct MessagesOptions<'a> { /// This token can be obtained from a `prev_batch` token returned for each /// room from the sync API, or from a start or end token returned by a /// previous `messages` call. - pub from: &'a str, + /// + /// If `from` isn't provided the homeserver shall return a list of messages + /// from the first or last (per the value of the dir parameter) visible + /// event in the room history for the requesting user. + pub from: Option<&'a str>, /// The token to stop returning events at. /// @@ -1105,27 +1110,42 @@ pub struct MessagesOptions<'a> { } impl<'a> MessagesOptions<'a> { - /// Creates `MessagesOptions` with the given start token and direction. + /// Creates `MessagesOptions` with the given direction. /// /// All other parameters will be defaulted. - pub fn new(from: &'a str, dir: Direction) -> Self { - Self { from, to: None, dir, limit: uint!(10), filter: RoomEventFilter::default() } + pub fn new(dir: Direction) -> Self { + Self { from: None, to: None, dir, limit: uint!(10), filter: RoomEventFilter::default() } } - /// Creates `MessagesOptions` with the given start token, and `dir` set to - /// `Backward`. - pub fn backward(from: &'a str) -> Self { - Self::new(from, Direction::Backward) + /// Creates `MessagesOptions` with `dir` set to `Backward`. + /// + /// If no `from` token is set afterwards, pagination will start at the + /// end of (the accessible part of) the room timeline. + pub fn backward() -> Self { + Self::new(Direction::Backward) } - /// Creates `MessagesOptions` with the given start token, and `dir` set to - /// `Forward`. - pub fn forward(from: &'a str) -> Self { - Self::new(from, Direction::Forward) + /// Creates `MessagesOptions` with `dir` set to `Forward`. + /// + /// If no `from` token is set afterwards, pagination will start at the + /// beginning of (the accessible part of) the room timeline. + pub fn forward() -> Self { + Self::new(Direction::Forward) + } + + /// Creates a new `MessagesOptions` from `self` with the `from` field set to + /// the given value. + /// + /// Since the field is public, you can also assign to it directly. This + /// method merely acts as a shorthand for that, because it is very + /// common to set this field. + pub fn from(self, from: impl Into>) -> Self { + Self { from: from.into(), ..self } } fn into_request(self, room_id: &'a RoomId) -> get_message_events::v3::Request<'_> { - assign!(get_message_events::v3::Request::new(room_id, self.from, self.dir), { + assign!(get_message_events::v3::Request::new(room_id, self.dir), { + from: self.from, to: self.to, limit: self.limit, filter: self.filter, diff --git a/crates/matrix-sdk/src/room/joined.rs b/crates/matrix-sdk/src/room/joined.rs index cbf51c20b..5ffad481e 100644 --- a/crates/matrix-sdk/src/room/joined.rs +++ b/crates/matrix-sdk/src/room/joined.rs @@ -20,14 +20,13 @@ use ruma::{ }, message::send_message_event, read_marker::set_read_marker, - receipt::create_receipt, + receipt::create_receipt::{self, v3::ReceiptType}, redact::redact_event, state::send_state_event, typing::create_typing_event::v3::{Request as TypingRequest, Typing}, }, assign, events::{room::message::RoomMessageEventContent, MessageLikeEventContent, StateEventContent}, - receipt::ReceiptType, serde::Raw, EventId, OwnedTransactionId, TransactionId, UserId, }; diff --git a/crates/matrix-sdk/src/sync.rs b/crates/matrix-sdk/src/sync.rs index 6fa924b21..a580e56c5 100644 --- a/crates/matrix-sdk/src/sync.rs +++ b/crates/matrix-sdk/src/sync.rs @@ -22,7 +22,7 @@ impl Client { rooms, presence, account_data, - to_device: _, + to_device, device_lists: _, device_one_time_keys_count: _, ambiguity_changes: _, @@ -31,6 +31,7 @@ impl Client { self.handle_sync_events(EventKind::GlobalAccountData, &None, &account_data.events).await?; self.handle_sync_events(EventKind::Presence, &None, &presence.events).await?; + self.handle_sync_events(EventKind::ToDevice, &None, &to_device.events).await?; for (room_id, room_info) in &rooms.join { let room = self.get_room(room_id); diff --git a/crates/matrix-sdk/tests/integration/client.rs b/crates/matrix-sdk/tests/integration/client.rs index f82e9ea52..c5c7b4cd0 100644 --- a/crates/matrix-sdk/tests/integration/client.rs +++ b/crates/matrix-sdk/tests/integration/client.rs @@ -365,7 +365,7 @@ async fn resolve_room_alias() { #[async_test] async fn join_leave_room() { - let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost"); + let room_id = &test_json::DEFAULT_SYNC_ROOM_ID; let (client, server) = logged_in_client().await; mock_sync(&server, &*test_json::SYNC, None).await; @@ -501,7 +501,7 @@ async fn left_rooms() { assert!(!client.left_rooms().is_empty()); assert!(client.invited_rooms().is_empty()); - assert!(client.get_left_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).is_some()) + assert!(client.get_left_room(&test_json::DEFAULT_SYNC_ROOM_ID).is_some()) } #[async_test] diff --git a/crates/matrix-sdk/tests/integration/room/common.rs b/crates/matrix-sdk/tests/integration/room/common.rs index c9c9bd177..ac855e524 100644 --- a/crates/matrix-sdk/tests/integration/room/common.rs +++ b/crates/matrix-sdk/tests/integration/room/common.rs @@ -1,13 +1,15 @@ use std::time::Duration; use matrix_sdk::{config::SyncSettings, DisplayName, RoomMember}; -use matrix_sdk_test::{async_test, test_json}; +use matrix_sdk_test::{ + async_test, bulk_room_members, test_json, EventBuilder, JoinedRoomBuilder, TimelineTestEvent, +}; use ruma::{ event_id, - events::{AnySyncStateEvent, StateEventType}, + events::{room::member::MembershipState, AnySyncStateEvent, StateEventType}, room_id, }; -use serde_json::{json, Value as JsonValue}; +use serde_json::json; use wiremock::{ matchers::{header, method, path_regex}, Mock, ResponseTemplate, @@ -32,7 +34,7 @@ async fn user_presence() { let _response = client.sync_once(sync_settings).await.unwrap(); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + let room = client.get_joined_room(&test_json::DEFAULT_SYNC_ROOM_ID).unwrap(); let members: Vec = room.active_members().await.unwrap(); assert_eq!(2, members.len()); @@ -47,7 +49,7 @@ async fn calculate_room_names_from_summary() { let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); let _response = client.sync_once(sync_settings).await.unwrap(); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + let room = client.get_joined_room(&test_json::DEFAULT_SYNC_ROOM_ID).unwrap(); assert_eq!(DisplayName::Calculated("example2".to_owned()), room.display_name().await.unwrap()); } @@ -63,7 +65,7 @@ async fn room_names() { let _response = client.sync_once(sync_settings).await.unwrap(); assert_eq!(client.rooms().len(), 1); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + let room = client.get_joined_room(&test_json::DEFAULT_SYNC_ROOM_ID).unwrap(); assert_eq!(DisplayName::Aliased("tutorial".to_owned()), room.display_name().await.unwrap()); @@ -83,7 +85,7 @@ async fn room_names() { #[async_test] async fn test_state_event_getting() { - let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost"); + let room_id = &test_json::DEFAULT_SYNC_ROOM_ID; let (client, server) = logged_in_client().await; @@ -187,7 +189,7 @@ async fn room_timeline_with_remove() { let _ = client.sync_once(sync_settings).await.unwrap(); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + let room = client.get_joined_room(&test_json::DEFAULT_SYNC_ROOM_ID).unwrap(); let (forward_stream, backward_stream) = room.timeline().await.unwrap(); // these two syncs lead to the store removing its existing timeline @@ -201,9 +203,7 @@ async fn room_timeline_with_remove() { .and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$")) .and(header("authorization", "Bearer 1234")) .and(query_param("from", "t392-516_47314_0_7_1_1_1_11444_1")) - .respond_with( - ResponseTemplate::new(200).set_body_json(&*test_json::SYNC_ROOM_MESSAGES_BATCH_1), - ) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::ROOM_MESSAGES_BATCH_1)) .expect(1) .named("messages_batch_1") .mount(&server) @@ -213,9 +213,7 @@ async fn room_timeline_with_remove() { .and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$")) .and(header("authorization", "Bearer 1234")) .and(query_param("from", "t47409-4357353_219380_26003_2269")) - .respond_with( - ResponseTemplate::new(200).set_body_json(&*test_json::SYNC_ROOM_MESSAGES_BATCH_2), - ) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::ROOM_MESSAGES_BATCH_2)) .expect(1) .named("messages_batch_2") .mount(&server) @@ -288,7 +286,7 @@ async fn room_timeline() { let _ = client.sync_once(sync_settings).await.unwrap(); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + let room = client.get_joined_room(&test_json::DEFAULT_SYNC_ROOM_ID).unwrap(); let (forward_stream, backward_stream) = room.timeline().await.unwrap(); let sync_token = client.sync_token().await.unwrap(); @@ -299,9 +297,7 @@ async fn room_timeline() { .and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$")) .and(header("authorization", "Bearer 1234")) .and(query_param("from", "t392-516_47314_0_7_1_1_1_11444_1")) - .respond_with( - ResponseTemplate::new(200).set_body_json(&*test_json::SYNC_ROOM_MESSAGES_BATCH_1), - ) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::ROOM_MESSAGES_BATCH_1)) .expect(1) .named("messages_batch_1") .mount(&server) @@ -311,9 +307,7 @@ async fn room_timeline() { .and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$")) .and(header("authorization", "Bearer 1234")) .and(query_param("from", "t47409-4357353_219380_26003_2269")) - .respond_with( - ResponseTemplate::new(200).set_body_json(&*test_json::SYNC_ROOM_MESSAGES_BATCH_2), - ) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::ROOM_MESSAGES_BATCH_2)) .expect(1) .named("messages_batch_2") .mount(&server) @@ -369,92 +363,26 @@ async fn room_timeline() { } #[async_test] -async fn room_permalink() { - fn sync_response(index: u8, room_timeline_events: &[JsonValue]) -> JsonValue { - json!({ - "device_one_time_keys_count": {}, - "next_batch": format!("s526_47314_0_7_1_1_1_11444_{}", index + 1), - "device_lists": { - "changed": [], - "left": [] - }, - "account_data": { - "events": [] - }, - "rooms": { - "invite": {}, - "join": { - "!test_room:127.0.0.1": { - "summary": {}, - "account_data": { - "events": [] - }, - "ephemeral": { - "events": [] - }, - "state": { - "events": [] - }, - "timeline": { - "events": room_timeline_events, - "limited": false, - "prev_batch": format!("s526_47314_0_7_1_1_1_11444_{}", index - 1), - }, - "unread_notifications": { - "highlight_count": 0, - "notification_count": 0, - } - } - }, - "leave": {} - }, - "to_device": { - "events": [] - }, - "presence": { - "events": [] - } - }) - } - - fn room_member_events(nb: usize, server: &str) -> Vec { - let mut events = Vec::with_capacity(nb); - for i in 0..nb { - let id = format!("${server}{i}"); - let user = format!("@user{i}:{server}"); - events.push(json!({ - "content": { - "membership": "join", - }, - "event_id": id, - "origin_server_ts": 151800140, - "sender": user, - "state_key": user, - "type": "m.room.member", - })) - } - events - } - +async fn room_route() { let (client, server) = logged_in_client().await; + let mut ev_builder = EventBuilder::new(); + let room_id = room_id!("!test_room:127.0.0.1"); // Without elligible server - let mut sync_index = 1; - let res = sync_response( - sync_index, - &[ - json!({ + ev_builder.add_joined_room( + JoinedRoomBuilder::new(room_id) + .add_timeline_event(TimelineTestEvent::Custom(json!({ "content": { "creator": "@creator:127.0.0.1", "room_version": "6", }, "event_id": "$151957878228ekrDs", "origin_server_ts": 15195787, - "sender": "@creator:localhost", + "sender": "@creator:127.0.0.1", "state_key": "", "type": "m.room.create", - }), - json!({ + }))) + .add_timeline_event(TimelineTestEvent::Custom(json!({ "content": { "membership": "join", }, @@ -463,110 +391,79 @@ async fn room_permalink() { "sender": "@creator:127.0.0.1", "state_key": "@creator:127.0.0.1", "type": "m.room.member", - }), - ], + }))), ); - mock_sync(&server, res, None).await; - client.sync_once(SyncSettings::new()).await.unwrap(); - let room = client.get_room(room_id!("!test_room:127.0.0.1")).unwrap(); - assert_eq!( - room.matrix_to_permalink().await.unwrap().to_string(), - "https://matrix.to/#/%21test_room%3A127.0.0.1" - ); - assert_eq!( - room.matrix_permalink(false).await.unwrap().to_string(), - "matrix:roomid/test_room:127.0.0.1" - ); - assert_eq!( - room.matrix_permalink(true).await.unwrap().to_string(), - "matrix:roomid/test_room:127.0.0.1?action=join" - ); + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + client.sync_once(SyncSettings::new()).await.unwrap(); + let room = client.get_room(room_id).unwrap(); + + let route = room.route().await.unwrap(); + assert_eq!(route.len(), 0); // With a single elligible server - sync_index += 1; - let res = sync_response( - sync_index, - &[json!({ - "content": { - "membership": "join", - }, - "event_id": "$151800140517rfvjc", - "origin_server_ts": 151800140, - "sender": "@example:localhost", - "state_key": "@example:localhost", - "type": "m.room.member", - })], - ); + let mut batch = 0; + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_state_bulk( + bulk_room_members(batch, 0..1, "localhost", &MembershipState::Join), + )); let sync_token = client.sync_token().await.unwrap(); - mock_sync(&server, res, Some(sync_token.clone())).await; + mock_sync(&server, ev_builder.build_json_sync_response(), Some(sync_token.clone())).await; client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap(); - assert_eq!( - room.matrix_to_permalink().await.unwrap().to_string(), - "https://matrix.to/#/%21test_room%3A127.0.0.1?via=localhost" - ); - assert_eq!( - room.matrix_permalink(false).await.unwrap().to_string(), - "matrix:roomid/test_room:127.0.0.1?via=localhost" - ); + let route = room.route().await.unwrap(); + assert_eq!(route.len(), 1); + assert_eq!(route[0], "localhost"); // With two elligible servers - sync_index += 1; - let res = sync_response(sync_index, &room_member_events(15, "notarealhs")); + batch += 1; + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_state_bulk( + bulk_room_members(batch, 0..15, "notarealhs", &MembershipState::Join), + )); let sync_token = client.sync_token().await.unwrap(); - mock_sync(&server, res, Some(sync_token.clone())).await; + mock_sync(&server, ev_builder.build_json_sync_response(), Some(sync_token.clone())).await; client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap(); - assert_eq!( - room.matrix_to_permalink().await.unwrap().to_string(), - "https://matrix.to/#/%21test_room%3A127.0.0.1?via=notarealhs&via=localhost" - ); - assert_eq!( - room.matrix_permalink(false).await.unwrap().to_string(), - "matrix:roomid/test_room:127.0.0.1?via=notarealhs&via=localhost" - ); + let route = room.route().await.unwrap(); + assert_eq!(route.len(), 2); + assert_eq!(route[0], "notarealhs"); + assert_eq!(route[1], "localhost"); // With three elligible servers - sync_index += 1; - let res = sync_response(sync_index, &room_member_events(5, "mymatrix")); + batch += 1; + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_state_bulk( + bulk_room_members(batch, 0..5, "mymatrix", &MembershipState::Join), + )); let sync_token = client.sync_token().await.unwrap(); - mock_sync(&server, res, Some(sync_token.clone())).await; + mock_sync(&server, ev_builder.build_json_sync_response(), Some(sync_token.clone())).await; client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap(); - assert_eq!( - room.matrix_to_permalink().await.unwrap().to_string(), - "https://matrix.to/#/%21test_room%3A127.0.0.1?via=notarealhs&via=mymatrix&via=localhost" - ); - assert_eq!( - room.matrix_permalink(false).await.unwrap().to_string(), - "matrix:roomid/test_room:127.0.0.1?via=notarealhs&via=mymatrix&via=localhost" - ); + let route = room.route().await.unwrap(); + assert_eq!(route.len(), 3); + assert_eq!(route[0], "notarealhs"); + assert_eq!(route[1], "mymatrix"); + assert_eq!(route[2], "localhost"); // With four elligible servers - sync_index += 1; - let res = sync_response(sync_index, &room_member_events(10, "yourmatrix")); + batch += 1; + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_state_bulk( + bulk_room_members(batch, 0..10, "yourmatrix", &MembershipState::Join), + )); let sync_token = client.sync_token().await.unwrap(); - mock_sync(&server, res, Some(sync_token.clone())).await; + mock_sync(&server, ev_builder.build_json_sync_response(), Some(sync_token.clone())).await; client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap(); - assert_eq!( - room.matrix_to_permalink().await.unwrap().to_string(), - "https://matrix.to/#/%21test_room%3A127.0.0.1?via=notarealhs&via=yourmatrix&via=mymatrix" - ); - assert_eq!( - room.matrix_permalink(false).await.unwrap().to_string(), - "matrix:roomid/test_room:127.0.0.1?via=notarealhs&via=yourmatrix&via=mymatrix" - ); + let route = room.route().await.unwrap(); + assert_eq!(route.len(), 3); + assert_eq!(route[0], "notarealhs"); + assert_eq!(route[1], "yourmatrix"); + assert_eq!(route[2], "mymatrix"); // With power levels - sync_index += 1; - let res = sync_response( - sync_index, - &[json!({ + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event( + TimelineTestEvent::Custom(json!({ "content": { "users": { - "@example:localhost": 50, + "@user_0:localhost": 50, }, }, "event_id": "$15139375512JaHAW", @@ -574,30 +471,25 @@ async fn room_permalink() { "sender": "@creator:127.0.0.1", "state_key": "", "type": "m.room.power_levels", - })], - ); + })), + )); let sync_token = client.sync_token().await.unwrap(); - mock_sync(&server, res, Some(sync_token.clone())).await; + mock_sync(&server, ev_builder.build_json_sync_response(), Some(sync_token.clone())).await; client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap(); - assert_eq!( - room.matrix_to_permalink().await.unwrap().to_string(), - "https://matrix.to/#/%21test_room%3A127.0.0.1?via=localhost&via=notarealhs&via=yourmatrix" - ); - assert_eq!( - room.matrix_permalink(false).await.unwrap().to_string(), - "matrix:roomid/test_room:127.0.0.1?via=localhost&via=notarealhs&via=yourmatrix" - ); + let route = room.route().await.unwrap(); + assert_eq!(route.len(), 3); + assert_eq!(route[0], "localhost"); + assert_eq!(route[1], "notarealhs"); + assert_eq!(route[2], "yourmatrix"); // With higher power levels - sync_index += 1; - let res = sync_response( - sync_index, - &[json!({ + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event( + TimelineTestEvent::Custom(json!({ "content": { "users": { - "@example:localhost": 50, - "@user0:mymatrix": 70, + "@user_0:localhost": 50, + "@user_2:mymatrix": 70, }, }, "event_id": "$15139375512JaHAZ", @@ -605,26 +497,21 @@ async fn room_permalink() { "sender": "@creator:127.0.0.1", "state_key": "", "type": "m.room.power_levels", - })], - ); + })), + )); let sync_token = client.sync_token().await.unwrap(); - mock_sync(&server, res, Some(sync_token.clone())).await; + mock_sync(&server, ev_builder.build_json_sync_response(), Some(sync_token.clone())).await; client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap(); - assert_eq!( - room.matrix_to_permalink().await.unwrap().to_string(), - "https://matrix.to/#/%21test_room%3A127.0.0.1?via=mymatrix&via=notarealhs&via=yourmatrix" - ); - assert_eq!( - room.matrix_permalink(false).await.unwrap().to_string(), - "matrix:roomid/test_room:127.0.0.1?via=mymatrix&via=notarealhs&via=yourmatrix" - ); + let route = room.route().await.unwrap(); + assert_eq!(route.len(), 3); + assert_eq!(route[0], "mymatrix"); + assert_eq!(route[1], "notarealhs"); + assert_eq!(route[2], "yourmatrix"); // With server ACLs - sync_index += 1; - let res = sync_response( - sync_index, - &[json!({ + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event( + TimelineTestEvent::Custom(json!({ "content": { "allow": ["*"], "allow_ip_literals": true, @@ -635,38 +522,69 @@ async fn room_permalink() { "sender": "@creator:127.0.0.1", "state_key": "", "type": "m.room.server_acl", - })], - ); + })), + )); let sync_token = client.sync_token().await.unwrap(); - mock_sync(&server, res, Some(sync_token.clone())).await; + mock_sync(&server, ev_builder.build_json_sync_response(), Some(sync_token.clone())).await; client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap(); + let route = room.route().await.unwrap(); + assert_eq!(route.len(), 3); + assert_eq!(route[0], "mymatrix"); + assert_eq!(route[1], "yourmatrix"); + assert_eq!(route[2], "localhost"); +} + +#[async_test] +async fn room_permalink() { + let (client, server) = logged_in_client().await; + let mut ev_builder = EventBuilder::new(); + let room_id = room_id!("!test_room:127.0.0.1"); + + // Without aliases + ev_builder.add_joined_room( + JoinedRoomBuilder::new(room_id) + .add_timeline_state_bulk(bulk_room_members( + 0, + 0..1, + "localhost", + &MembershipState::Join, + )) + .add_timeline_state_bulk(bulk_room_members( + 1, + 0..5, + "notarealhs", + &MembershipState::Join, + )), + ); + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + client.sync_once(SyncSettings::new()).await.unwrap(); + let room = client.get_room(room_id).unwrap(); + assert_eq!( room.matrix_to_permalink().await.unwrap().to_string(), - "https://matrix.to/#/%21test_room%3A127.0.0.1?via=mymatrix&via=yourmatrix&via=localhost" + "https://matrix.to/#/%21test_room%3A127.0.0.1?via=notarealhs&via=localhost" ); assert_eq!( room.matrix_permalink(false).await.unwrap().to_string(), - "matrix:roomid/test_room:127.0.0.1?via=mymatrix&via=yourmatrix&via=localhost" + "matrix:roomid/test_room:127.0.0.1?via=notarealhs&via=localhost" ); // With an alternative alias - sync_index += 1; - let res = sync_response( - sync_index, - &[json!({ + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event( + TimelineTestEvent::Custom(json!({ "content": { "alt_aliases": ["#alias:localhost"], }, "event_id": "$15139375513VdeRF", "origin_server_ts": 151393755, - "sender": "@example:localhost", + "sender": "@user_0:localhost", "state_key": "", "type": "m.room.canonical_alias", - })], - ); + })), + )); let sync_token = client.sync_token().await.unwrap(); - mock_sync(&server, res, Some(sync_token.clone())).await; + mock_sync(&server, ev_builder.build_json_sync_response(), Some(sync_token.clone())).await; client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap(); assert_eq!( @@ -676,23 +594,21 @@ async fn room_permalink() { assert_eq!(room.matrix_permalink(false).await.unwrap().to_string(), "matrix:r/alias:localhost"); // With a canonical alias - sync_index += 1; - let res = sync_response( - sync_index, - &[json!({ + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event( + TimelineTestEvent::Custom(json!({ "content": { "alias": "#canonical:localhost", "alt_aliases": ["#alias:localhost"], }, "event_id": "$15139375513VdeRF", "origin_server_ts": 151393755, - "sender": "@example:localhost", + "sender": "@user_0:localhost", "state_key": "", "type": "m.room.canonical_alias", - })], - ); + })), + )); let sync_token = client.sync_token().await.unwrap(); - mock_sync(&server, res, Some(sync_token.clone())).await; + mock_sync(&server, ev_builder.build_json_sync_response(), Some(sync_token.clone())).await; client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap(); assert_eq!( @@ -707,14 +623,68 @@ async fn room_permalink() { room.matrix_permalink(true).await.unwrap().to_string(), "matrix:r/canonical:localhost?action=join" ); +} +#[async_test] +async fn room_event_permalink() { + let (client, server) = logged_in_client().await; + let mut ev_builder = EventBuilder::new(); + let room_id = room_id!("!test_room:127.0.0.1"); let event_id = event_id!("$15139375512JaHAW"); + + // Without aliases + ev_builder.add_joined_room( + JoinedRoomBuilder::new(room_id) + .add_timeline_state_bulk(bulk_room_members( + 0, + 0..1, + "localhost", + &MembershipState::Join, + )) + .add_timeline_state_bulk(bulk_room_members( + 1, + 0..5, + "notarealhs", + &MembershipState::Join, + )), + ); + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + client.sync_once(SyncSettings::new()).await.unwrap(); + let room = client.get_room(room_id).unwrap(); + assert_eq!( room.matrix_to_event_permalink(event_id).await.unwrap().to_string(), - "https://matrix.to/#/%21test_room%3A127.0.0.1/%2415139375512JaHAW?via=mymatrix&via=yourmatrix&via=localhost" + "https://matrix.to/#/%21test_room%3A127.0.0.1/%2415139375512JaHAW?via=notarealhs&via=localhost" ); assert_eq!( room.matrix_event_permalink(event_id).await.unwrap().to_string(), - "matrix:roomid/test_room:127.0.0.1/e/15139375512JaHAW?via=mymatrix&via=yourmatrix&via=localhost" + "matrix:roomid/test_room:127.0.0.1/e/15139375512JaHAW?via=notarealhs&via=localhost" + ); + + // Adding an alias doesn't change anything + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event( + TimelineTestEvent::Custom(json!({ + "content": { + "alias": "#canonical:localhost", + "alt_aliases": ["#alias:localhost"], + }, + "event_id": "$15139375513VdeRF", + "origin_server_ts": 151393755, + "sender": "@user_0:localhost", + "state_key": "", + "type": "m.room.canonical_alias", + })), + )); + let sync_token = client.sync_token().await.unwrap(); + mock_sync(&server, ev_builder.build_json_sync_response(), Some(sync_token.clone())).await; + client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap(); + + assert_eq!( + room.matrix_to_event_permalink(event_id).await.unwrap().to_string(), + "https://matrix.to/#/%21test_room%3A127.0.0.1/%2415139375512JaHAW?via=notarealhs&via=localhost" + ); + assert_eq!( + room.matrix_event_permalink(event_id).await.unwrap().to_string(), + "matrix:roomid/test_room:127.0.0.1/e/15139375512JaHAW?via=notarealhs&via=localhost" ); } diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index 06b46aaf8..78d00a239 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -10,7 +10,7 @@ use matrix_sdk::{ use matrix_sdk_test::{async_test, test_json}; use ruma::{ api::client::membership::Invite3pidInit, assign, event_id, - events::room::message::RoomMessageEventContent, mxc_uri, room_id, thirdparty, uint, user_id, + events::room::message::RoomMessageEventContent, mxc_uri, thirdparty, uint, user_id, TransactionId, }; use serde_json::json; @@ -39,7 +39,7 @@ async fn invite_user_by_id() { let _response = client.sync_once(sync_settings).await.unwrap(); let user = user_id!("@example:localhost"); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + let room = client.get_joined_room(&test_json::DEFAULT_SYNC_ROOM_ID).unwrap(); room.invite_user_by_id(user).await.unwrap(); } @@ -61,7 +61,7 @@ async fn invite_user_by_3pid() { let _response = client.sync_once(sync_settings).await.unwrap(); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + let room = client.get_joined_room(&test_json::DEFAULT_SYNC_ROOM_ID).unwrap(); room.invite_user_by_3pid( Invite3pidInit { @@ -93,7 +93,7 @@ async fn leave_room() { let _response = client.sync_once(sync_settings).await.unwrap(); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + let room = client.get_joined_room(&test_json::DEFAULT_SYNC_ROOM_ID).unwrap(); room.leave().await.unwrap(); } @@ -116,7 +116,7 @@ async fn ban_user() { let _response = client.sync_once(sync_settings).await.unwrap(); let user = user_id!("@example:localhost"); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + let room = client.get_joined_room(&test_json::DEFAULT_SYNC_ROOM_ID).unwrap(); room.ban_user(user, None).await.unwrap(); } @@ -139,7 +139,7 @@ async fn kick_user() { let _response = client.sync_once(sync_settings).await.unwrap(); let user = user_id!("@example:localhost"); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + let room = client.get_joined_room(&test_json::DEFAULT_SYNC_ROOM_ID).unwrap(); room.kick_user(user, None).await.unwrap(); } @@ -162,7 +162,7 @@ async fn read_receipt() { let _response = client.sync_once(sync_settings).await.unwrap(); let event_id = event_id!("$xxxxxx:example.org"); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + let room = client.get_joined_room(&test_json::DEFAULT_SYNC_ROOM_ID).unwrap(); room.read_receipt(event_id).await.unwrap(); } @@ -185,7 +185,7 @@ async fn read_marker() { let _response = client.sync_once(sync_settings).await.unwrap(); let event_id = event_id!("$xxxxxx:example.org"); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + let room = client.get_joined_room(&test_json::DEFAULT_SYNC_ROOM_ID).unwrap(); room.read_marker(event_id, None).await.unwrap(); } @@ -207,7 +207,7 @@ async fn typing_notice() { let _response = client.sync_once(sync_settings).await.unwrap(); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + let room = client.get_joined_room(&test_json::DEFAULT_SYNC_ROOM_ID).unwrap(); room.typing_notice(true).await.unwrap(); } @@ -231,7 +231,7 @@ async fn room_state_event_send() { let _response = client.sync_once(sync_settings).await.unwrap(); - let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost"); + let room_id = &test_json::DEFAULT_SYNC_ROOM_ID; let room = client.get_joined_room(room_id).unwrap(); @@ -260,7 +260,7 @@ async fn room_message_send() { let _response = client.sync_once(sync_settings).await.unwrap(); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + let room = client.get_joined_room(&test_json::DEFAULT_SYNC_ROOM_ID).unwrap(); let content = RoomMessageEventContent::text_plain("Hello world"); let txn_id = TransactionId::new(); @@ -301,7 +301,7 @@ async fn room_attachment_send() { let _response = client.sync_once(sync_settings).await.unwrap(); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + let room = client.get_joined_room(&test_json::DEFAULT_SYNC_ROOM_ID).unwrap(); let mut media = Cursor::new("Hello world"); @@ -347,7 +347,7 @@ async fn room_attachment_send_info() { let _response = client.sync_once(sync_settings).await.unwrap(); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + let room = client.get_joined_room(&test_json::DEFAULT_SYNC_ROOM_ID).unwrap(); let mut media = Cursor::new("Hello world"); @@ -398,7 +398,7 @@ async fn room_attachment_send_wrong_info() { let _response = client.sync_once(sync_settings).await.unwrap(); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + let room = client.get_joined_room(&test_json::DEFAULT_SYNC_ROOM_ID).unwrap(); let mut media = Cursor::new("Hello world"); @@ -457,7 +457,7 @@ async fn room_attachment_send_info_thumbnail() { let _response = client.sync_once(sync_settings).await.unwrap(); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + let room = client.get_joined_room(&test_json::DEFAULT_SYNC_ROOM_ID).unwrap(); let mut media = Cursor::new("Hello world"); @@ -502,7 +502,7 @@ async fn room_redact() { let _response = client.sync_once(sync_settings).await.unwrap(); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + let room = client.get_joined_room(&test_json::DEFAULT_SYNC_ROOM_ID).unwrap(); let event_id = event_id!("$xxxxxxxx:example.com"); diff --git a/crates/matrix-sdk/tests/integration/room/left.rs b/crates/matrix-sdk/tests/integration/room/left.rs index 2614db150..6e422608f 100644 --- a/crates/matrix-sdk/tests/integration/room/left.rs +++ b/crates/matrix-sdk/tests/integration/room/left.rs @@ -2,7 +2,6 @@ use std::time::Duration; use matrix_sdk::config::SyncSettings; use matrix_sdk_test::{async_test, test_json}; -use ruma::room_id; use wiremock::{ matchers::{header, method, path_regex}, Mock, ResponseTemplate, @@ -27,7 +26,7 @@ async fn forget_room() { let _response = client.sync_once(sync_settings).await.unwrap(); - let room = client.get_left_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + let room = client.get_left_room(&test_json::DEFAULT_SYNC_ROOM_ID).unwrap(); room.forget().await.unwrap(); } diff --git a/labs/sled-state-inspector/Cargo.toml b/labs/sled-state-inspector/Cargo.toml index d4996bb38..60a0366c7 100644 --- a/labs/sled-state-inspector/Cargo.toml +++ b/labs/sled-state-inspector/Cargo.toml @@ -10,7 +10,7 @@ clap = "3.2.4" futures = { version = "0.3.21", default-features = false, features = ["executor"] } matrix-sdk-base = { path = "../../crates/matrix-sdk-base", version = "0.5.0" } matrix-sdk-sled = { path = "../../crates/matrix-sdk-sled", version = "0.1.0" } -ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f" } +ruma = { git = "https://github.com/ruma/ruma", rev = "ca8c66c885241a7ba3805399604eda4a38979f6b" } rustyline = "9.1.2" rustyline-derive = "0.6.0" serde = "1.0.136" diff --git a/labs/sled-state-inspector/src/main.rs b/labs/sled-state-inspector/src/main.rs index 5c088d7c7..2785345c3 100644 --- a/labs/sled-state-inspector/src/main.rs +++ b/labs/sled-state-inspector/src/main.rs @@ -3,7 +3,7 @@ use std::{convert::TryFrom, fmt::Debug, sync::Arc}; use atty::Stream; use clap::{Arg, ArgMatches, Command as Argparse}; use futures::executor::block_on; -use matrix_sdk_base::{RoomInfo, Store}; +use matrix_sdk_base::RoomInfo; use matrix_sdk_sled::StateStore; use ruma::{events::StateEventType, OwnedRoomId, OwnedUserId, RoomId}; use rustyline::{ @@ -26,13 +26,13 @@ use syntect::{ #[derive(Clone)] struct Inspector { - store: Store, + store: Arc, printer: Printer, } #[derive(Helper)] struct InspectorHelper { - store: Store, + store: Arc, _highlighter: MatchingBracketHighlighter, _validator: MatchingBracketValidator, _hinter: HistoryHinter, @@ -54,7 +54,7 @@ impl InspectorHelper { "m.room.topic", ]; - fn new(store: Store) -> Self { + fn new(store: Arc) -> Self { Self { store, _highlighter: MatchingBracketHighlighter::new(), @@ -72,7 +72,9 @@ impl InspectorHelper { } fn complete_rooms(&self, arg: Option<&&str>) -> Vec { - let rooms: Vec = block_on(async { self.store.get_room_infos().await.unwrap() }); + let rooms: Vec = block_on(async { + matrix_sdk_base::StateStore::get_room_infos(&*self.store).await.unwrap() + }); rooms .into_iter() @@ -200,9 +202,8 @@ impl Printer { impl Inspector { fn new(database_path: &str, json: bool, color: bool) -> Self { let printer = Printer::new(json, color); - let store = Store::new(Arc::new( - StateStore::open_with_path(database_path).expect("Can't open sled database"), - )); + let store = + Arc::new(StateStore::open_with_path(database_path).expect("Can't open sled database")); Self { store, printer } } @@ -237,7 +238,8 @@ impl Inspector { } async fn list_rooms(&self) { - let rooms: Vec = self.store.get_room_infos().await.unwrap(); + let rooms: Vec = + matrix_sdk_base::StateStore::get_room_infos(&*self.store).await.unwrap(); self.printer.pretty_print_struct(&rooms); } @@ -247,7 +249,8 @@ impl Inspector { } async fn get_profiles(&self, room_id: OwnedRoomId) { - let joined: Vec = self.store.get_joined_user_ids(&room_id).await.unwrap(); + let joined: Vec = + matrix_sdk_base::StateStore::get_joined_user_ids(&*self.store, &room_id).await.unwrap(); for member in joined { let event = self.store.get_profile(&room_id, &member).await.unwrap(); @@ -256,7 +259,8 @@ impl Inspector { } async fn get_members(&self, room_id: OwnedRoomId) { - let joined: Vec = self.store.get_joined_user_ids(&room_id).await.unwrap(); + let joined: Vec = + matrix_sdk_base::StateStore::get_joined_user_ids(&*self.store, &room_id).await.unwrap(); for member in joined { let event = self.store.get_member_event(&room_id, &member).await.unwrap();