diff --git a/.github/workflows/bindings_ci.yml b/.github/workflows/bindings_ci.yml index c97ea1bc5..5c877ffd2 100644 --- a/.github/workflows/bindings_ci.yml +++ b/.github/workflows/bindings_ci.yml @@ -149,11 +149,6 @@ jobs: profile: minimal override: true - - name: Install targets - run: | - rustup target add aarch64-apple-ios-sim --toolchain nightly - rustup target add x86_64-apple-ios --toolchain nightly - - name: Load cache uses: Swatinem/rust-cache@v1 @@ -161,17 +156,12 @@ jobs: uses: actions-rs/cargo@v1 with: command: install - # keep in sync with uniffi dependency in Cargo.toml's - args: uniffi_bindgen --git https://github.com/mozilla/uniffi-rs --rev 0eee77f67b716c4896494606e5931d249871b23a + # keep in sync with uniffi dependency in root Cargo.toml + args: uniffi_bindgen --git https://github.com/mozilla/uniffi-rs --rev fdb769b567865d9c5c7c682a18d0c1301a039c85 - - name: Generate .xcframework - working-directory: bindings/apple - run: sh ./debug_build_xcframework.sh x86_64 ci + - name: Build library & bindings + run: cargo xtask swift build-library - name: Run XCTests working-directory: bindings/apple - run: | - xcodebuild test \ - -scheme MatrixRustSDK \ - -sdk iphonesimulator \ - -destination 'platform=iOS Simulator,name=iPhone 13' + run: swift test diff --git a/.github/workflows/cancel_others.yml b/.github/workflows/cancel_others.yml new file mode 100644 index 000000000..0f1227f06 --- /dev/null +++ b/.github/workflows/cancel_others.yml @@ -0,0 +1,13 @@ +on: + pull_request: + branches: [main] + +jobs: + cancel-others: + runs-on: ubuntu-latest + steps: + - name: Cancel workflows for older commits + uses: styfle/cancel-workflow-action@0.11.0 + with: + workflow_id: all + all_but_latest: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd1db671b..5c98b8c0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,15 +16,6 @@ env: CARGO_TERM_COLOR: always jobs: - cancel-others: - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.9.1 - with: - access_token: ${{ github.token }} - xtask: runs-on: ubuntu-latest steps: diff --git a/Cargo.lock b/Cargo.lock index 7059c79ba..0096903f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,7 +30,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" dependencies = [ "generic-array", - "rand_core 0.6.4", + "rand_core 0.6.3", ] [[package]] @@ -57,9 +57,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.19" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" dependencies = [ "memchr", ] @@ -94,6 +94,18 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" +[[package]] +name = "app_dirs2" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a8d2d8dbda5fca0a522259fb88e4f55d2b10ad39f5f03adeebf85031eba501" +dependencies = [ + "jni", + "ndk-context", + "winapi", + "xdg", +] + [[package]] name = "arc-swap" version = "1.5.1" @@ -106,6 +118,15 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" +[[package]] +name = "arrayvec" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" +dependencies = [ + "nodrop", +] + [[package]] name = "arrayvec" version = "0.7.2" @@ -170,6 +191,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + [[package]] name = "assign" version = "1.1.1" @@ -260,6 +287,52 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "axum" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e3356844c4d6a6d6467b8da2cffb4a2820be256f50a3a386c9d152bab31043" +dependencies = [ + "async-trait", + "axum-core", + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa 1.0.3", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f0c0a60006f2a293d82d571f635042a72edf927539b7685bd62d361963839b" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "tower-layer", + "tower-service", +] + [[package]] name = "backoff" version = "0.4.0" @@ -284,7 +357,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide 0.5.4", + "miniz_oxide 0.5.3", "object", "rustc-demangle", ] @@ -353,11 +426,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a08e53fc5a564bb15bfe6fae56bd71522205f1f91893f9c0116edad6496c183f" dependencies = [ "arrayref", - "arrayvec", + "arrayvec 0.7.2", "cc", "cfg-if", "constant_time_eq", - "digest 0.10.5", + "digest 0.10.3", ] [[package]] @@ -371,9 +444,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.10.3" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" dependencies = [ "generic-array", ] @@ -493,6 +566,12 @@ version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.0" @@ -587,8 +666,8 @@ checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750" dependencies = [ "atty", "bitflags", - "clap_derive", - "clap_lex", + "clap_derive 3.2.18", + "clap_lex 0.2.4", "indexmap", "once_cell", "strsim 0.10.0", @@ -596,6 +675,21 @@ dependencies = [ "textwrap 0.15.1", ] +[[package]] +name = "clap" +version = "4.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef582e2c00a63a0c0aa1fb4a4870781c4f5729f51196d3537fa7c1c1992eaa3" +dependencies = [ + "atty", + "bitflags", + "clap_derive 4.0.13", + "clap_lex 0.3.0", + "once_cell", + "strsim 0.10.0", + "termcolor", +] + [[package]] name = "clap_derive" version = "3.2.18" @@ -609,6 +703,19 @@ dependencies = [ "syn", ] +[[package]] +name = "clap_derive" +version = "4.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42f169caba89a7d512b5418b09864543eeb4d497416c917d7137863bd2076ad" +dependencies = [ + "heck 0.4.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "clap_lex" version = "0.2.4" @@ -618,6 +725,15 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "clap_lex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "clipboard-win" version = "4.4.2" @@ -638,22 +754,22 @@ dependencies = [ "cc", ] -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - [[package]] name = "color_quant" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "concurrent-queue" version = "1.2.4" @@ -663,6 +779,20 @@ dependencies = [ "cache-padded", ] +[[package]] +name = "console" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89eab4d20ce20cea182308bca13088fecea9c05f6776cf287205d41a0ed3c847" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "terminal_size", + "unicode-width", + "winapi", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -681,9 +811,9 @@ checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" [[package]] name = "const_format" -version = "0.2.28" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79f926bc2341a80e6bb5a16e18057c8c90ca3edbdeb9fa497bd0f82b1f4df4e6" +checksum = "939dc9e2eb9077e0679d2ce32de1ded8531779360b003b4a972a7a39ec263495" dependencies = [ "const_format_proc_macros", ] @@ -738,9 +868,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "dc948ebb96241bb40ab73effeb80d9f93afaad49359d159a5e61be51619fe813" dependencies = [ "libc", ] @@ -815,24 +945,26 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.11" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f916dfc5d356b0ed9dae65f1db9fc9770aa2851d2662b988ccf4fe3516e86348" +checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", "memoffset", + "once_cell", "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.12" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" +checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" dependencies = [ "cfg-if", + "once_cell", ] [[package]] @@ -904,9 +1036,9 @@ dependencies = [ [[package]] name = "ctr" -version = "0.9.2" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +checksum = "0d14f329cfbaf5d0e06b5e87fff7e265d2673c5ea7d2c27691a2c107db1442a0" dependencies = [ "cipher 0.4.3", ] @@ -925,50 +1057,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "cxx" -version = "1.0.78" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f39818dcfc97d45b03953c1292efc4e80954e1583c4aa770bac1383e2310a4" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.78" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e580d70777c116df50c390d1211993f62d40302881e54d4b79727acb83d0199" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.78" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56a46460b88d1cec95112c8c363f0e2c39afdb237f60583b0b36343bf627ea9c" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.78" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747b608fecf06b0d72d440f27acc99288207324b793be2c17991839f3d4995ea" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "darling" version = "0.14.1" @@ -1042,7 +1130,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" dependencies = [ - "uuid 1.2.1", + "uuid 1.1.2", ] [[package]] @@ -1055,6 +1143,15 @@ dependencies = [ "byteorder", ] +[[package]] +name = "deflate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f" +dependencies = [ + "adler32", +] + [[package]] name = "der" version = "0.5.1" @@ -1106,6 +1203,17 @@ dependencies = [ "syn", ] +[[package]] +name = "dialoguer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92e7e37ecef6857fdc0c0c5d42fd5b0938e46590c2183cc92dd310a6d078eb1" +dependencies = [ + "console", + "tempfile", + "zeroize", +] + [[package]] name = "digest" version = "0.9.0" @@ -1117,11 +1225,11 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.5" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" dependencies = [ - "block-buffer 0.10.3", + "block-buffer 0.10.2", "crypto-common", "subtle", ] @@ -1215,6 +1323,12 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.31" @@ -1230,19 +1344,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" -[[package]] -name = "env_logger" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c90bf5f19754d10198ccb95b70664fc925bd1fc090a0fd9a6ebc54acc8cd6272" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - [[package]] name = "errno" version = "0.2.8" @@ -1340,7 +1441,8 @@ name = "example-emoji-verification" version = "0.1.0" dependencies = [ "anyhow", - "clap 3.2.22", + "clap 4.0.16", + "futures", "matrix-sdk", "tokio", "tracing-subscriber", @@ -1397,7 +1499,9 @@ name = "example-timeline" version = "0.1.0" dependencies = [ "anyhow", + "clap 4.0.16", "futures", + "futures-signals", "matrix-sdk", "tokio", "tracing-subscriber", @@ -1474,7 +1578,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" dependencies = [ "crc32fast", - "miniz_oxide 0.5.4", + "miniz_oxide 0.5.3", ] [[package]] @@ -1766,31 +1870,6 @@ dependencies = [ "ahash", ] -[[package]] -name = "headers" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" -dependencies = [ - "base64", - "bitflags", - "bytes", - "headers-core", - "http", - "httpdate", - "mime", - "sha1", -] - -[[package]] -name = "headers-core" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" -dependencies = [ - "http", -] - [[package]] name = "heck" version = "0.3.3" @@ -1830,7 +1909,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.5", + "digest 0.10.3", ] [[package]] @@ -1841,7 +1920,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", - "itoa 1.0.4", + "itoa 1.0.3", ] [[package]] @@ -1855,6 +1934,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" + [[package]] name = "http-types" version = "2.12.0" @@ -1888,12 +1973,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" version = "0.14.20" @@ -1909,7 +1988,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.4", + "itoa 1.0.3", "pin-project-lite", "socket2", "tokio", @@ -1946,28 +2025,18 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.51" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5a6ef98976b22b3b7f2f3a806f858cb862044cfa66805aa3ad84cb3d3b785ed" +checksum = "4c495f162af0bf17656d0014a0eded5f3cd2f365fdd204548c2869db89359dc7" dependencies = [ "android_system_properties", "core-foundation-sys", - "iana-time-zone-haiku", "js-sys", + "once_cell", "wasm-bindgen", "winapi", ] -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde6edd6cef363e9359ed3c98ba64590ba9eecba2293eb5a723ab32aee8926aa" -dependencies = [ - "cxx", - "cxx-build", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -2005,9 +2074,9 @@ dependencies = [ [[package]] name = "image" -version = "0.24.4" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8e4fb07cf672b1642304e731ef8a6a4c7891d67bb4fd4f5ce58cd6ed86803c" +checksum = "7e30ca2ecf7666107ff827a8e481de6a132a9b687ed3bb20bb1c144a36c00964" dependencies = [ "bytemuck", "byteorder", @@ -2016,7 +2085,7 @@ dependencies = [ "jpeg-decoder 0.2.6", "num-rational 0.4.1", "num-traits", - "png 0.17.6", + "png 0.17.5", "scoped_threadpool", "tiff 0.7.3", ] @@ -2079,14 +2148,14 @@ checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" [[package]] name = "inferno" -version = "0.11.9" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb31a5d9068fccb34562007cd564bb08fff1478255b0534c549e93fcb658cd90" +checksum = "9709543bd6c25fdc748da2bed0f6855b07b7e93a203ae31332ac2101ab2f4782" dependencies = [ "ahash", "atty", "indexmap", - "itoa 1.0.4", + "itoa 1.0.3", "log", "num-format", "once_cell", @@ -2131,9 +2200,9 @@ checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" [[package]] name = "itertools" -version = "0.10.5" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" dependencies = [ "either", ] @@ -2146,20 +2215,25 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" -version = "1.0.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" [[package]] name = "jack-in" version = "0.2.0" dependencies = [ + "app_dirs2", + "dialoguer", "eyre", "futures", "futures-signals", "log4rs", "matrix-sdk", "matrix-sdk-common", + "matrix-sdk-sled", + "sanitize-filename-reader-friendly", + "serde_json", "structopt", "tokio", "tracing", @@ -2169,6 +2243,26 @@ dependencies = [ "tuirealm", ] +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jpeg-decoder" version = "0.1.22" @@ -2189,9 +2283,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.60" +version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" dependencies = [ "wasm-bindgen", ] @@ -2245,9 +2339,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.135" +version = "0.2.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" [[package]] name = "libloading" @@ -2259,15 +2353,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "link-cplusplus" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" -dependencies = [ - "cc", -] - [[package]] name = "linux-raw-sys" version = "0.0.46" @@ -2276,9 +2361,9 @@ checksum = "d4d2456c373231a208ad294c33dc5bff30051eafd954cd4caae83a712b12854d" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "9f80bf5aacaf25cbfc8210d1cfb718f2bf3b11c4c54e5afe36c236853a8ec390" dependencies = [ "autocfg", "scopeguard", @@ -2319,9 +2404,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.8.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6e8aaa3f231bb4bd57b84b2d5dc3ae7f350265df8aa96492e0bc394a1571909" +checksum = "936d98d2ddd79c18641c6709e7bb09981449694e402d1a0f0f657ea8d61f4a51" dependencies = [ "hashbrown", ] @@ -2347,12 +2432,19 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +[[package]] +name = "matchit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" + [[package]] name = "matrix-sdk" version = "0.6.0" dependencies = [ "anyhow", "anymap2", + "assert_matches", "async-once-cell", "async-stream", "async-trait", @@ -2370,7 +2462,9 @@ dependencies = [ "futures-util", "getrandom 0.2.7", "http", - "image 0.24.4", + "hyper", + "image 0.24.3", + "indexmap", "matches", "matrix-sdk-base", "matrix-sdk-common", @@ -2388,10 +2482,10 @@ dependencies = [ "thiserror", "tokio", "tokio-stream", + "tower", "tracing", "tracing-subscriber", "url", - "warp", "wasm-bindgen-test", "wasm-timer", "wiremock", @@ -2402,11 +2496,13 @@ dependencies = [ name = "matrix-sdk-appservice" version = "0.1.0" dependencies = [ + "axum", "dashmap", "http", + "http-body", + "hyper", "matrix-sdk", "matrix-sdk-test", - "percent-encoding", "regex", "ruma", "serde", @@ -2414,10 +2510,10 @@ dependencies = [ "serde_yaml", "thiserror", "tokio", + "tower", "tracing", "tracing-subscriber", "url", - "warp", "wiremock", ] @@ -2428,8 +2524,8 @@ dependencies = [ "assign", "async-stream", "async-trait", + "ctor", "dashmap", - "env_logger", "futures", "futures-channel", "futures-core", @@ -2439,6 +2535,7 @@ dependencies = [ "lru", "matrix-sdk-common", "matrix-sdk-crypto", + "matrix-sdk-store-encryption", "matrix-sdk-test", "once_cell", "ruma", @@ -2447,6 +2544,7 @@ dependencies = [ "thiserror", "tokio", "tracing", + "tracing-subscriber", "wasm-bindgen-test", "zeroize", ] @@ -2473,15 +2571,19 @@ name = "matrix-sdk-crypto" version = "0.6.0" dependencies = [ "aes", + "anyhow", "async-trait", "atomic", "base64", "bs58", "byteorder", + "cfg-if", "ctr", "dashmap", "event-listener", "futures", + "futures-core", + "futures-signals", "futures-util", "hmac", "http", @@ -2497,7 +2599,7 @@ dependencies = [ "ruma", "serde", "serde_json", - "sha2 0.10.6", + "sha2 0.10.3", "thiserror", "tokio", "tracing", @@ -2522,7 +2624,7 @@ dependencies = [ "ruma", "serde", "serde_json", - "sha2 0.10.6", + "sha2 0.10.3", "tempfile", "thiserror", "tokio", @@ -2622,7 +2724,7 @@ dependencies = [ "serde_json", "thiserror", "tracing", - "uuid 1.2.1", + "uuid 1.1.2", "wasm-bindgen", "wasm-bindgen-test", "web-sys", @@ -2698,7 +2800,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "sha2 0.10.6", + "sha2 0.10.3", "thiserror", "zeroize", ] @@ -2793,9 +2895,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.5.4" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" dependencies = [ "adler", ] @@ -2814,9 +2916,9 @@ dependencies = [ [[package]] name = "napi" -version = "2.10.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bace9a4026eaa6631804e2ff9030c47beb0483fbb12dc17950fe1530c4961f84" +checksum = "743fece4c26c5132f8559080145fde9ba88700c0f1aa30a1ab3e057ab105814d" dependencies = [ "bitflags", "ctor", @@ -2886,6 +2988,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "nibble_vec" version = "0.1.0" @@ -2906,6 +3014,12 @@ dependencies = [ "libc", ] +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + [[package]] name = "nom" version = "7.1.1" @@ -2928,12 +3042,12 @@ dependencies = [ [[package]] name = "num-format" -version = "0.4.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b862ff8df690cf089058c98b183676a7ed0f974cc08b426800093227cbff3b" +checksum = "bafe4179722c2894288ee77a9f044f02811c86af699344c498b0840c698a2465" dependencies = [ - "arrayvec", - "itoa 1.0.4", + "arrayvec 0.4.12", + "itoa 0.4.8", ] [[package]] @@ -3041,9 +3155,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.15.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" +checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" [[package]] name = "oorandom" @@ -3059,9 +3173,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.42" +version = "0.10.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12fc0523e3bd51a692c8850d075d74dc062ccf251c0110668cbd921917118a13" +checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0" dependencies = [ "bitflags", "cfg-if", @@ -3091,9 +3205,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.76" +version = "0.9.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5230151e44c0f05157effb743e8d517472843121cf9243e8b81393edb5acd9ce" +checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f" dependencies = [ "autocfg", "cc", @@ -3175,7 +3289,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" dependencies = [ "base64ct", - "rand_core 0.6.4", + "rand_core 0.6.3", "subtle", ] @@ -3191,10 +3305,10 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ - "digest 0.10.5", + "digest 0.10.3", "hmac", "password-hash", - "sha2 0.10.6", + "sha2 0.10.3", ] [[package]] @@ -3259,9 +3373,9 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "plotters" -version = "0.3.4" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97" +checksum = "716b4eeb6c4a1d3ecc956f75b43ec2e8e8ba80026413e70a3f41fd3313d3492b" dependencies = [ "num-traits", "plotters-backend", @@ -3293,20 +3407,20 @@ checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" dependencies = [ "bitflags", "crc32fast", - "deflate", + "deflate 0.8.6", "miniz_oxide 0.3.7", ] [[package]] name = "png" -version = "0.17.6" +version = "0.17.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f0e7f4c94ec26ff209cee506314212639d6c91b80afb82984819fafce9df01c" +checksum = "dc38c0ad57efb786dd57b9864e5b18bae478c00c824dc55a38bbc9da95dde3ba" dependencies = [ "bitflags", "crc32fast", - "flate2", - "miniz_oxide 0.5.4", + "deflate 1.0.0", + "miniz_oxide 0.5.3", ] [[package]] @@ -3385,9 +3499,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.46" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" dependencies = [ "unicode-ident", ] @@ -3461,9 +3575,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.23.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11bafc859c6815fbaffbbbf4229ecb767ac913fecb27f9ad4343662e9ef099ea" +checksum = "9279fbdacaad3baf559d8cabe0acc3d06e30ea14931af31af79578ac0946decc" dependencies = [ "memchr", ] @@ -3508,7 +3622,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", - "rand_core 0.6.4", + "rand_core 0.6.3", ] [[package]] @@ -3528,7 +3642,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", + "rand_core 0.6.3", ] [[package]] @@ -3542,9 +3656,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.4" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ "getrandom 0.2.7", ] @@ -3564,7 +3678,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ - "rand_core 0.6.4", + "rand_core 0.6.3", ] [[package]] @@ -3648,9 +3762,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.12" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc" +checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" dependencies = [ "base64", "bytes", @@ -3665,14 +3779,14 @@ dependencies = [ "hyper-tls", "ipnet", "js-sys", + "lazy_static", "log", "mime", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", "rustls", - "rustls-pemfile 1.0.1", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", @@ -3697,9 +3811,9 @@ checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" [[package]] name = "rgb" -version = "0.8.34" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3603b7d71ca82644f79b5a06d1220e9a58ede60bd32255f698cb1af8838b8db3" +checksum = "c3b221de559e4a29df3b957eec92bc0de6bc8eaf6ca9cfed43e5e1d67ff65a34" dependencies = [ "bytemuck", ] @@ -3774,7 +3888,7 @@ dependencies = [ "getrandom 0.2.7", "http", "indexmap", - "itoa 1.0.4", + "itoa 1.0.3", "js-sys", "js_int", "js_option", @@ -3789,7 +3903,7 @@ dependencies = [ "thiserror", "tracing", "url", - "uuid 1.2.1", + "uuid 1.1.2", "wildmatch", ] @@ -3839,9 +3953,9 @@ checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" [[package]] name = "rustix" -version = "0.35.11" +version = "0.35.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb2fda4666def1433b1b05431ab402e42a1084285477222b72d6c564c417cef" +checksum = "72c825b8aa8010eb9ee99b75f05e10180b9278d161583034d7574c9d617aeada" dependencies = [ "bitflags", "errno", @@ -3863,15 +3977,6 @@ dependencies = [ "webpki", ] -[[package]] -name = "rustls-pemfile" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" -dependencies = [ - "base64", -] - [[package]] name = "rustls-pemfile" version = "1.0.1" @@ -3967,12 +4072,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" -[[package]] -name = "scratch" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" - [[package]] name = "scroll" version = "0.11.0" @@ -4028,18 +4127,18 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.14" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" +checksum = "93f6841e709003d68bb2deee8c343572bf446003ec20a583e76f7b15cebf3711" dependencies = [ "serde", ] [[package]] name = "serde" -version = "1.0.145" +version = "1.0.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" +checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" dependencies = [ "serde_derive", ] @@ -4065,9 +4164,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.145" +version = "1.0.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" +checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" dependencies = [ "proc-macro2", "quote", @@ -4076,11 +4175,11 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.86" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" dependencies = [ - "itoa 1.0.4", + "itoa 1.0.3", "ryu", "serde", ] @@ -4103,35 +4202,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.4", + "itoa 1.0.3", "ryu", "serde", ] [[package]] name = "serde_yaml" -version = "0.9.13" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8613d593412a0deb7bbd8de9d908efff5a0cb9ccd8f62c641e7b2ed2f57291d1" +checksum = "7a09f551ccc8210268ef848f0bab37b306e87b85b2e017b899e7fb815f5aed62" dependencies = [ "indexmap", - "itoa 1.0.4", + "itoa 1.0.3", "ryu", "serde", "unsafe-libyaml", ] -[[package]] -name = "sha1" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.5", -] - [[package]] name = "sha2" version = "0.9.9" @@ -4147,13 +4235,13 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "899bf02746a2c92bf1053d9327dadb252b01af1f81f90cdb902411f518bc7215" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.5", + "digest 0.10.3", ] [[package]] @@ -4197,9 +4285,9 @@ dependencies = [ [[package]] name = "signature" -version = "1.6.4" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +checksum = "f0ea32af43239f0d353a7dd75a22d94c329c8cdaafdcb4c1c1335aa10c298a4a" [[package]] name = "slab" @@ -4251,15 +4339,15 @@ checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" [[package]] name = "smallvec" -version = "1.10.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" [[package]] name = "socket2" -version = "0.4.7" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +checksum = "10c98bba371b9b22a71a9414e420f92ddeb2369239af08200816169d5e2dd7aa" dependencies = [ "libc", "winapi", @@ -4348,21 +4436,21 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "symbolic-common" -version = "9.2.1" +version = "9.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "800963ba330b09a2ae4a4f7c6392b81fbc2784099a98c1eac68c3437aa9382b2" +checksum = "3e555b2c3ebd97b963c8a3e94ce5e5137ba42da4a26687f81c700d8de1c997f0" dependencies = [ "debugid", "memmap2", "stable_deref_trait", - "uuid 1.2.1", + "uuid 1.1.2", ] [[package]] name = "symbolic-demangle" -version = "9.2.1" +version = "9.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b940a1fdbc72bb3369e38714efe6cd332dbbe46d093cf03d668b9ac390d1ad0" +checksum = "71a1425bccf0a24c68c9faea6c4f1f84b4865a3dd5976454d8a796c80216e38a" dependencies = [ "cpp_demangle", "rustc-demangle", @@ -4371,15 +4459,21 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.102" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" + [[package]] name = "synstructure" version = "0.12.6" @@ -4436,6 +4530,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -4453,18 +4557,18 @@ checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" [[package]] name = "thiserror" -version = "1.0.37" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +checksum = "3d0a539a918745651435ac7db7a18761589a94cd7e94cd56999f828bf73c8a57" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.37" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +checksum = "c251e90f708e16c49a16f4917dc2131e75222b72edfa9cb7f7c58ae56aae0c09" dependencies = [ "proc-macro2", "quote", @@ -4526,11 +4630,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.15" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d634a985c4d4238ec39cacaed2e7ae552fbd3c476b552c1deac3021b7d7eaf0c" +checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" dependencies = [ - "itoa 1.0.4", + "itoa 1.0.3", "libc", "num_threads", ] @@ -4624,9 +4728,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.10" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6edf2d6bc038a43d31353570e27270603f4648d18f5ed10c0e179abe43255af" +checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9" dependencies = [ "futures-core", "pin-project-lite", @@ -4635,9 +4739,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" dependencies = [ "bytes", "futures-core", @@ -4656,6 +4760,47 @@ dependencies = [ "serde", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c530c8675c1dbf98facee631536fa116b5fb6382d7dd6dc1b118d970eafe3ba" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" @@ -4664,9 +4809,9 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" dependencies = [ "cfg-if", "log", @@ -4677,9 +4822,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.23" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" dependencies = [ "proc-macro2", "quote", @@ -4731,7 +4876,7 @@ dependencies = [ "sharded-slab", "smallvec", "thread_local", - "time 0.3.15", + "time 0.3.14", "tracing", "tracing-core", "tracing-log", @@ -4819,41 +4964,41 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.5" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" -version = "1.10.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" [[package]] name = "unicode-xid" -version = "0.2.4" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" [[package]] name = "uniffi" -version = "0.20.0" -source = "git+https://github.com/mozilla/uniffi-rs?rev=0eee77f67b716c4896494606e5931d249871b23a#0eee77f67b716c4896494606e5931d249871b23a" +version = "0.21.0" +source = "git+https://github.com/mozilla/uniffi-rs?rev=fdb769b567865d9c5c7c682a18d0c1301a039c85#fdb769b567865d9c5c7c682a18d0c1301a039c85" dependencies = [ "anyhow", "bytes", @@ -4868,8 +5013,8 @@ dependencies = [ [[package]] name = "uniffi_bindgen" -version = "0.20.0" -source = "git+https://github.com/mozilla/uniffi-rs?rev=0eee77f67b716c4896494606e5931d249871b23a#0eee77f67b716c4896494606e5931d249871b23a" +version = "0.21.0" +source = "git+https://github.com/mozilla/uniffi-rs?rev=fdb769b567865d9c5c7c682a18d0c1301a039c85#fdb769b567865d9c5c7c682a18d0c1301a039c85" dependencies = [ "anyhow", "askama", @@ -4890,8 +5035,8 @@ dependencies = [ [[package]] name = "uniffi_build" -version = "0.20.0" -source = "git+https://github.com/mozilla/uniffi-rs?rev=0eee77f67b716c4896494606e5931d249871b23a#0eee77f67b716c4896494606e5931d249871b23a" +version = "0.21.0" +source = "git+https://github.com/mozilla/uniffi-rs?rev=fdb769b567865d9c5c7c682a18d0c1301a039c85#fdb769b567865d9c5c7c682a18d0c1301a039c85" dependencies = [ "anyhow", "camino", @@ -4900,8 +5045,8 @@ dependencies = [ [[package]] name = "uniffi_macros" -version = "0.20.0" -source = "git+https://github.com/mozilla/uniffi-rs?rev=0eee77f67b716c4896494606e5931d249871b23a#0eee77f67b716c4896494606e5931d249871b23a" +version = "0.21.0" +source = "git+https://github.com/mozilla/uniffi-rs?rev=fdb769b567865d9c5c7c682a18d0c1301a039c85#fdb769b567865d9c5c7c682a18d0c1301a039c85" dependencies = [ "bincode", "camino", @@ -4918,8 +5063,8 @@ dependencies = [ [[package]] name = "uniffi_meta" -version = "0.20.0" -source = "git+https://github.com/mozilla/uniffi-rs?rev=0eee77f67b716c4896494606e5931d249871b23a#0eee77f67b716c4896494606e5931d249871b23a" +version = "0.21.0" +source = "git+https://github.com/mozilla/uniffi-rs?rev=fdb769b567865d9c5c7c682a18d0c1301a039c85#fdb769b567865d9c5c7c682a18d0c1301a039c85" dependencies = [ "serde", ] @@ -4936,9 +5081,9 @@ dependencies = [ [[package]] name = "unsafe-libyaml" -version = "0.2.4" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e5fa573d8ac5f1a856f8d7be41d390ee973daf97c806b2c1a465e4e1406e68" +checksum = "931179334a56395bcf64ba5e0ff56781381c1a5832178280c7d7f91d1679aeb0" [[package]] name = "untrusted" @@ -4975,12 +5120,11 @@ dependencies = [ [[package]] name = "uuid" -version = "1.2.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83" +checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" dependencies = [ "getrandom 0.2.7", - "wasm-bindgen", ] [[package]] @@ -5014,7 +5158,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f20153a1c82ac5f1243b62e80f067ae608facc415c6ef82f88426a61c79886" dependencies = [ "aes", - "arrayvec", + "arrayvec 0.7.2", "base64", "cbc", "ed25519-dalek", @@ -5025,7 +5169,7 @@ dependencies = [ "rand 0.7.3", "serde", "serde_json", - "sha2 0.10.6", + "sha2 0.10.3", "subtle", "thiserror", "x25519-dalek", @@ -5059,35 +5203,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "warp" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7b8be92646fc3d18b06147664ebc5f48d222686cb11a8755e561a735aacc6d" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "headers", - "http", - "hyper", - "log", - "mime", - "mime_guess", - "percent-encoding", - "pin-project", - "rustls-pemfile 0.2.1", - "scoped-tls", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tokio-stream", - "tokio-util", - "tower-service", - "tracing", -] - [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -5108,9 +5223,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.83" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" dependencies = [ "cfg-if", "serde", @@ -5120,9 +5235,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.83" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" dependencies = [ "bumpalo", "log", @@ -5135,9 +5250,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.33" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" +checksum = "fa76fb221a1f8acddf5b54ace85912606980ad661ac7a503b4570ffd3a624dad" dependencies = [ "cfg-if", "js-sys", @@ -5147,9 +5262,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.83" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5157,9 +5272,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.83" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" dependencies = [ "proc-macro2", "quote", @@ -5170,15 +5285,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.83" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" [[package]] name = "wasm-bindgen-test" -version = "0.3.33" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d2fff962180c3fadf677438054b1db62bee4aa32af26a45388af07d1287e1d" +checksum = "513df541345bb9fcc07417775f3d51bbb677daf307d8035c0afafd87dc2e6599" dependencies = [ "console_error_panic_hook", "js-sys", @@ -5190,9 +5305,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.33" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4683da3dfc016f704c9f82cf401520c4f1cb3ee440f7f52b3d6ac29506a49ca7" +checksum = "6150d36a03e90a3cf6c12650be10626a9902d70c5270fd47d7a47e5389a10d56" dependencies = [ "proc-macro2", "quote", @@ -5228,9 +5343,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.60" +version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -5248,9 +5363,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.22.5" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368bfe657969fb01238bb756d351dcade285e0f6fcbd36dcb23359a5169975be" +checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf" dependencies = [ "webpki", ] @@ -5258,7 +5373,7 @@ dependencies = [ [[package]] name = "weedle2" version = "4.0.0" -source = "git+https://github.com/mozilla/uniffi-rs?rev=0eee77f67b716c4896494606e5931d249871b23a#0eee77f67b716c4896494606e5931d249871b23a" +source = "git+https://github.com/mozilla/uniffi-rs?rev=fdb769b567865d9c5c7c682a18d0c1301a039c85#fdb769b567865d9c5c7c682a18d0c1301a039c85" dependencies = [ "nom", ] @@ -5360,9 +5475,9 @@ dependencies = [ [[package]] name = "wiremock" -version = "0.5.15" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "249dc68542861d17eae4b4e5e8fb381c2f9e8f255a84f6771d5fdf8b6c03ce3c" +checksum = "cc3c7b7557dbfdad6431b5a51196c9110cef9d83f6a9b26699f35cdc0ae113ec" dependencies = [ "assert-json-diff", "async-trait", @@ -5392,6 +5507,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "xdg" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4583db5cbd4c4c0303df2d15af80f0539db703fa1c68802d4cbbd2dd0f88f6" +dependencies = [ + "dirs", +] + [[package]] name = "xshell" version = "0.1.17" @@ -5411,9 +5535,11 @@ checksum = "4916a4a3cad759e499a3620523bf9545cc162d7a06163727dde97ce9aaa4cf39" name = "xtask" version = "0.1.0" dependencies = [ + "camino", "clap 3.2.22", "serde", "serde_json", + "uniffi_bindgen", "xshell", ] diff --git a/Cargo.toml b/Cargo.toml index b5e57c990..a5f5618f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,16 @@ members = [ default-members = ["benchmarks", "crates/*"] resolver = "2" +[workspace.dependencies] +ruma = { version = "0.7.4", features = ["client-api-c"] } +tracing = { version = "0.1.36", default-features = false, features = ["std"] } +uniffi = { git = "https://github.com/mozilla/uniffi-rs", rev = "fdb769b567865d9c5c7c682a18d0c1301a039c85" } +uniffi_macros = { git = "https://github.com/mozilla/uniffi-rs", rev = "fdb769b567865d9c5c7c682a18d0c1301a039c85" } +uniffi_bindgen = { git = "https://github.com/mozilla/uniffi-rs", rev = "fdb769b567865d9c5c7c682a18d0c1301a039c85" } +uniffi_build = { git = "https://github.com/mozilla/uniffi-rs", rev = "fdb769b567865d9c5c7c682a18d0c1301a039c85", features = ["builtin-bindgen"] } +vodozemac = "0.3.0" +zeroize = "1.3.0" + [profile.release] lto = true diff --git a/README.md b/README.md index 0fb8473f9..22cb464dd 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The rust-sdk consists of multiple crates that can be picked at your convenience: ## Minimum Supported Rust Version (MSRV) -These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.62` +These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.64` ## Status diff --git a/benchmarks/Cargo.toml b/benchmarks/Cargo.toml index f5ebcb688..f9280879a 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.6.0"} matrix-sdk-sled = { path = "../crates/matrix-sdk-sled", version = "0.2.0", default-features = false, features = ["crypto-store"] } matrix-sdk-test = { path = "../testing/matrix-sdk-test", version = "0.6.0"} -ruma = "0.7.0" +ruma = { workspace = true } 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/README.md b/bindings/README.md index ff55b5573..b4c07977e 100644 --- a/bindings/README.md +++ b/bindings/README.md @@ -5,18 +5,18 @@ maintained by the owners of the Matrix Rust SDK project. * [`apple`] or `matrix-rust-components-swift`, Swift bindings of the [`matrix-sdk`] crate via [`matrix-sdk-ffi`], -* [`matrix-sdk-crypto-ffi`], bindings of the [`matrix-sdk-crypto`] +* [`matrix-sdk-crypto-ffi`], UniFFI (Kotlin, Swift, Python, Ruby) bindings of the [`matrix-sdk-crypto`] crate, * [`matrix-sdk-crypto-js`], JavaScript bindings of the [`matrix-sdk-crypto`] crate, * [`matrix-sdk-crypto-nodejs`], Node.js bindings of the [`matrix-sdk-crypto`] crate, -* [`matrix-sdk-ffi`], bindings of the [`matrix-sdk`] crate, +* [`matrix-sdk-ffi`], UniFFI bindings of the [`matrix-sdk`] crate. [`apple`]: ./apple [`matrix-sdk-crypto-ffi`]: ./matrix-sdk-crypto-ffi -[`matrix-sdk-crypto-js`]: ../crates/matrix-sdk-crypto -[`matrix-sdk-crypto-nodejs`]: ../crates/matrix-sdk-crypto +[`matrix-sdk-crypto-js`]: ./matrix-sdk-crypto-js +[`matrix-sdk-crypto-nodejs`]: ./matrix-sdk-crypto-nodejs [`matrix-sdk-crypto`]: ../crates/matrix-sdk-crypto [`matrix-sdk-ffi`]: ./matrix-sdk-ffi [`matrix-sdk`]: ../crates/matrix-sdk diff --git a/bindings/apple/Package.swift b/bindings/apple/Package.swift index 13b9a0fa4..05f4003ab 100644 --- a/bindings/apple/Package.swift +++ b/bindings/apple/Package.swift @@ -5,17 +5,25 @@ import PackageDescription let package = Package( name: "MatrixRustSDK", - platforms: [.iOS(.v15)], products: [ .library(name: "MatrixRustSDK", targets: ["MatrixRustSDK"]), ], targets: [ - .binaryTarget(name: "MatrixSDKFFI", path: "generated/MatrixSDKFFI.xcframework"), .target(name: "MatrixRustSDK", - dependencies: [.target(name: "MatrixSDKFFI")], - path: "generated/swift"), + path: "generated/swift", + swiftSettings: [ + .unsafeFlags(["-I", "./generated/matrix_sdk_ffi"]) + ]), .testTarget(name: "MatrixRustSDKTests", - dependencies: ["MatrixRustSDK"]), + dependencies: ["MatrixRustSDK"], + swiftSettings: [ + .unsafeFlags(["-I", "./generated/matrix_sdk_ffi"]) + ], + linkerSettings: [ + .linkedLibrary("matrix_sdk_ffi", .when(platforms: [.macOS])), + .linkedLibrary("matrix_sdk_ffiFFI", .when(platforms: [.linux])), + .unsafeFlags(["-L./generated/matrix_sdk_ffi"]) + ]) ] ) diff --git a/bindings/apple/README.md b/bindings/apple/README.md index 14b698939..9027cb77d 100644 --- a/bindings/apple/README.md +++ b/bindings/apple/README.md @@ -38,15 +38,11 @@ The `build_crypto_xcframework.sh` script will go through all the steps required 4. `xcodebuild` an `xcframework` from the fat static libs and the original iOS one, and add the header and module map to it under `generated/MatrixSDKCryptoFFI.xcframework` 5. cleanup and delete the generated files except the .xcframework and the swift sources (that aren't part of the framework) -## Running the Xcode project +## Building & testing the Swift package -The Xcode project is meant to provide a simple example on how to integrate everything together but also a place to run unit and integration tests from. +`Package.swift` is meant to provide a simple example on how to integrate everything together but also a place to run unit and integration tests from. -It's pre-configured to link to the generated .xcframework and .swift files so successfully running the script first is necessary for it to compile. - -It makes the compiled code available to swift by importing the C header through its bridging header. - -Once all the generated components are available running it should be as easy as choosing a platform and clicking run. +It's pre-configured to link to the generated static lib and .swift files so successfully running `cargo xtask swift build-library` first is necessary for it to compile. Afterwards you can execute the tests with `swift test`. Note that for the moment this only works on macOS but we're planning to add Linux support in the future. ## Distribution diff --git a/bindings/apple/build_crypto_xcframework.sh b/bindings/apple/build_crypto_xcframework.sh index a4cfa81d3..11e29fd40 100755 --- a/bindings/apple/build_crypto_xcframework.sh +++ b/bindings/apple/build_crypto_xcframework.sh @@ -51,7 +51,12 @@ lipo -create \ -output "${GENERATED_DIR}/simulator/libmatrix_sdk_crypto_ffi.a" # Generate uniffi files -uniffi-bindgen generate "${SRC_ROOT}/bindings/${TARGET_CRATE}/src/olm.udl" --language swift --config "${SRC_ROOT}/bindings/${TARGET_CRATE}/uniffi.toml" --out-dir ${GENERATED_DIR} +uniffi-bindgen generate \ + --language swift \ + --lib-file "${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \ + --config "${SRC_ROOT}/bindings/${TARGET_CRATE}/uniffi.toml" \ + --out-dir ${GENERATED_DIR} \ + "${SRC_ROOT}/bindings/${TARGET_CRATE}/src/olm.udl" # Move headers to the right place HEADERS_DIR=${GENERATED_DIR}/headers diff --git a/bindings/kotlin/scripts/build_sdk.sh b/bindings/kotlin/scripts/build_sdk.sh index 3a18d3ec3..0e5c56bb2 100755 --- a/bindings/kotlin/scripts/build_sdk.sh +++ b/bindings/kotlin/scripts/build_sdk.sh @@ -52,7 +52,8 @@ uniffi-bindgen generate "${SRC_ROOT}/bindings/matrix-sdk-ffi/src/api.udl" \ --language kotlin \ --out-dir ${GENERATED_DIR} \ --lib-file "${BASE_TARGET_DIR}/aarch64-apple-darwin/${RELEASE_TYPE_DIR}/libmatrix_sdk_ffi.a" \ - + --version \ + # Create android library cd "${KOTLIN_ROOT}" ./gradlew :sdk:sdk-android:assemble diff --git a/bindings/matrix-sdk-crypto-ffi/Cargo.toml b/bindings/matrix-sdk-crypto-ffi/Cargo.toml index f0075e5eb..e8fc2de34 100644 --- a/bindings/matrix-sdk-crypto-ffi/Cargo.toml +++ b/bindings/matrix-sdk-crypto-ffi/Cargo.toml @@ -19,17 +19,18 @@ hmac = "0.12.1" http = "0.2.6" pbkdf2 = "0.11.0" rand = "0.8.5" -ruma = { version = "0.7.0", features = ["client-api-c"] } +ruma = { workspace = true } serde = "1.0.136" serde_json = "1.0.79" sha2 = "0.10.2" thiserror = "1.0.30" -tracing = "0.1.34" +tracing = { workspace = true } tracing-subscriber = { version = "0.3.11", features = ["env-filter"] } # keep in sync with uniffi dependency in matrix-sdk-ffi, and uniffi_bindgen in ffi CI job -uniffi = { git = "https://github.com/mozilla/uniffi-rs", rev = "0eee77f67b716c4896494606e5931d249871b23a" } -uniffi_macros = { git = "https://github.com/mozilla/uniffi-rs", rev = "0eee77f67b716c4896494606e5931d249871b23a" } -zeroize = { version = "1.3.0", features = ["zeroize_derive"] } +uniffi = { workspace = true } +uniffi_macros = { workspace = true } +vodozemac = { workspace = true } +zeroize = { workspace = true, features = ["zeroize_derive"] } [dependencies.js_int] version = "0.2.2" @@ -55,11 +56,8 @@ version = "1.17.0" default_features = false features = ["rt-multi-thread"] -[dependencies.vodozemac] -version = "0.3.0" - [build-dependencies] -uniffi_build = { git = "https://github.com/mozilla/uniffi-rs", rev = "0eee77f67b716c4896494606e5931d249871b23a", features = ["builtin-bindgen"] } +uniffi_build = { workspace = true, features = ["builtin-bindgen"] } [dev-dependencies] tempfile = "3.3.0" diff --git a/bindings/matrix-sdk-crypto-ffi/src/lib.rs b/bindings/matrix-sdk-crypto-ffi/src/lib.rs index 79e874b6d..b619c88fe 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/lib.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/lib.rs @@ -16,7 +16,7 @@ mod uniffi_api; mod users; mod verification; -use std::{borrow::Borrow, collections::HashMap, str::FromStr, sync::Arc}; +use std::{borrow::Borrow, collections::HashMap, str::FromStr, sync::Arc, time::Duration}; pub use backup_recovery_key::{ BackupRecoveryKey, DecodeError, MegolmV1BackupKey, PassphraseInfo, PkDecryptionError, @@ -28,12 +28,18 @@ pub use error::{ use js_int::UInt; pub use logger::{set_logger, Logger}; pub use machine::{KeyRequestPair, OlmMachine}; -use matrix_sdk_crypto::types::{EventEncryptionAlgorithm, SigningKey}; +use matrix_sdk_crypto::{ + types::{EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey}, + EncryptionSettings as RustEncryptionSettings, LocalTrust, +}; pub use responses::{ BootstrapCrossSigningResult, DeviceLists, KeysImportResult, OutgoingVerificationRequest, Request, RequestType, SignatureUploadRequest, UploadSigningKeysRequest, }; -use ruma::{DeviceId, DeviceKeyAlgorithm, OwnedUserId, RoomId, SecondsSinceUnixEpoch, UserId}; +use ruma::{ + events::room::history_visibility::HistoryVisibility as RustHistoryVisibility, DeviceId, + DeviceKeyAlgorithm, OwnedUserId, RoomId, SecondsSinceUnixEpoch, UserId, +}; use serde::{Deserialize, Serialize}; pub use users::UserIdentity; pub use verification::{ @@ -272,7 +278,7 @@ pub fn migrate( imported: session.imported, backed_up: session.backed_up, history_visibility: None, - algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2, + algorithm: RustEventEncryptionAlgorithm::MegolmV1AesSha2, }; let session = matrix_sdk_crypto::olm::InboundGroupSession::from_pickle(pickle)?; @@ -347,6 +353,99 @@ impl ProgressListener for T { } } +/// An encryption algorithm to be used to encrypt messages sent to a room. +pub enum EventEncryptionAlgorithm { + /// Olm version 1 using Curve25519, AES-256, and SHA-256. + OlmV1Curve25519AesSha2, + /// Megolm version 1 using AES-256 and SHA-256. + MegolmV1AesSha2, +} + +impl From for RustEventEncryptionAlgorithm { + fn from(a: EventEncryptionAlgorithm) -> Self { + match a { + EventEncryptionAlgorithm::OlmV1Curve25519AesSha2 => { + RustEventEncryptionAlgorithm::OlmV1Curve25519AesSha2 + } + EventEncryptionAlgorithm::MegolmV1AesSha2 => { + RustEventEncryptionAlgorithm::MegolmV1AesSha2 + } + } + } +} + +/// Who can see a room's history. +pub enum HistoryVisibility { + /// Previous events are accessible to newly joined members from the point + /// they were invited onwards. + /// + /// Events stop being accessible when the member's state changes to + /// something other than *invite* or *join*. + Invited, + + /// Previous events are accessible to newly joined members from the point + /// they joined the room onwards. + /// Events stop being accessible when the member's state changes to + /// something other than *join*. + Joined, + + /// Previous events are always accessible to newly joined members. + /// + /// All events in the room are accessible, even those sent when the member + /// was not a part of the room. + Shared, + + /// All events while this is the `HistoryVisibility` value may be shared by + /// any participating homeserver with anyone, regardless of whether they + /// have ever joined the room. + WorldReadable, +} + +impl From for RustHistoryVisibility { + fn from(h: HistoryVisibility) -> Self { + match h { + HistoryVisibility::Invited => RustHistoryVisibility::Invited, + HistoryVisibility::Joined => RustHistoryVisibility::Joined, + HistoryVisibility::Shared => RustHistoryVisibility::Shared, + HistoryVisibility::WorldReadable => RustHistoryVisibility::Shared, + } + } +} + +/// Settings that should be used when a room key is shared. +/// +/// These settings control which algorithm the room key should use, how long a +/// room key should be used and some other important information that determines +/// the lifetime of a room key. +pub struct EncryptionSettings { + /// The encryption algorithm that should be used in the room. + pub algorithm: EventEncryptionAlgorithm, + /// How long can the room key be used before it should be rotated. Time in + /// seconds. + pub rotation_period: u64, + /// How many messages should be sent before the room key should be rotated. + pub rotation_period_msgs: u64, + /// The current history visibility of the room. The visibility will be + /// tracked by the room key and the key will be rotated if the visibility + /// changes. + pub history_visibility: HistoryVisibility, + /// Should untrusted devices receive the room key, or should they be + /// excluded from the conversation. + pub only_allow_trusted_devices: bool, +} + +impl From for RustEncryptionSettings { + fn from(v: EncryptionSettings) -> Self { + RustEncryptionSettings { + algorithm: v.algorithm.into(), + rotation_period: Duration::from_secs(v.rotation_period), + rotation_period_msgs: v.rotation_period_msgs, + history_visibility: v.history_visibility.into(), + only_allow_trusted_devices: v.only_allow_trusted_devices, + } + } +} + /// An event that was successfully decrypted. pub struct DecryptedEvent { /// The decrypted version of the event. diff --git a/bindings/matrix-sdk-crypto-ffi/src/logger.rs b/bindings/matrix-sdk-crypto-ffi/src/logger.rs index b047784dd..ce9ea0687 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/logger.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/logger.rs @@ -51,6 +51,7 @@ pub fn set_logger(logger: Box) { let _ = tracing_subscriber::fmt() .with_writer(logger) .with_env_filter(filter) + .with_ansi(false) .without_time() .try_init(); } diff --git a/bindings/matrix-sdk-crypto-ffi/src/machine.rs b/bindings/matrix-sdk-crypto-ffi/src/machine.rs index 6cc3b09b1..a859b432e 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/machine.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/machine.rs @@ -11,9 +11,8 @@ use js_int::UInt; use matrix_sdk_common::deserialized_responses::AlgorithmInfo; use matrix_sdk_crypto::{ backups::MegolmV1BackupKey as RustBackupKey, decrypt_room_key_export, encrypt_room_key_export, - matrix_sdk_qrcode::QrVerificationData, olm::ExportedRoomKey, store::RecoveryKey, - EncryptionSettings, LocalTrust, OlmMachine as InnerMachine, UserIdentities, - Verification as RustVerification, + matrix_sdk_qrcode::QrVerificationData, olm::ExportedRoomKey, store::RecoveryKey, LocalTrust, + OlmMachine as InnerMachine, UserIdentities, Verification as RustVerification, }; use ruma::{ api::{ @@ -46,9 +45,9 @@ use crate::{ responses::{response_from_string, OutgoingVerificationRequest, OwnedResponse}, BackupKeys, BackupRecoveryKey, BootstrapCrossSigningResult, ConfirmVerificationResult, CrossSigningKeyExport, CrossSigningStatus, DecodeError, DecryptedEvent, Device, DeviceLists, - KeyImportError, KeysImportResult, MegolmV1BackupKey, ProgressListener, QrCode, Request, - RequestType, RequestVerificationResult, RoomKeyCounts, ScanResult, SignatureUploadRequest, - StartSasResult, UserIdentity, Verification, VerificationRequest, + EncryptionSettings, KeyImportError, KeysImportResult, MegolmV1BackupKey, ProgressListener, + QrCode, Request, RequestType, RequestVerificationResult, RoomKeyCounts, ScanResult, + SignatureUploadRequest, StartSasResult, UserIdentity, Verification, VerificationRequest, }; /// A high level state machine that handles E2EE for Matrix. @@ -283,11 +282,13 @@ impl OlmMachine { } } - /// Mark the device of the given user with the given device ID as trusted. - pub fn mark_device_as_trusted( + /// Set local trust state for the device of the given user without creating + /// or uploading any signatures if verified + pub fn set_local_trust( &self, user_id: &str, device_id: &str, + trust_state: LocalTrust, ) -> Result<(), CryptoStoreError> { let user_id = parse_user_id(user_id)?; @@ -295,7 +296,7 @@ impl OlmMachine { self.runtime.block_on(self.inner.get_device(&user_id, device_id.into(), None))?; if let Some(device) = device { - self.runtime.block_on(device.set_local_trust(LocalTrust::Verified))?; + self.runtime.block_on(device.set_local_trust(trust_state))?; } Ok(()) @@ -418,7 +419,7 @@ impl OlmMachine { key_counts: HashMap, unused_fallback_keys: Option>, ) -> Result { - let events: ToDevice = serde_json::from_str(events)?; + let to_device: ToDevice = serde_json::from_str(events)?; let device_changes: RumaDeviceLists = device_changes.into(); let key_counts: BTreeMap = key_counts .into_iter() @@ -436,7 +437,7 @@ impl OlmMachine { unused_fallback_keys.map(|u| u.into_iter().map(DeviceKeyAlgorithm::from).collect()); let events = self.runtime.block_on(self.inner.receive_sync_changes( - events, + to_device.events, &device_changes, &key_counts, unused_fallback_keys.as_deref(), @@ -515,10 +516,13 @@ impl OlmMachine { /// /// * `users` - The list of users which are considered to be members of the /// room and should receive the room key. + /// + /// * `settings` - The settings that should be used for the room key. pub fn share_room_key( &self, room_id: &str, users: Vec, + settings: EncryptionSettings, ) -> Result, CryptoStoreError> { let users: Vec = users.into_iter().filter_map(|u| UserId::parse(u).ok()).collect(); @@ -527,7 +531,7 @@ impl OlmMachine { let requests = self.runtime.block_on(self.inner.share_room_key( &room_id, users.iter().map(Deref::deref), - EncryptionSettings::default(), + settings, ))?; Ok(requests.into_iter().map(|r| r.as_ref().into()).collect()) diff --git a/bindings/matrix-sdk-crypto-ffi/src/olm.udl b/bindings/matrix-sdk-crypto-ffi/src/olm.udl index a30f65407..9044d09ec 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/olm.udl +++ b/bindings/matrix-sdk-crypto-ffi/src/olm.udl @@ -247,6 +247,33 @@ enum RequestType { "RoomMessage", }; +enum LocalTrust { + "Verified", + "BlackListed", + "Ignored", + "Unset", +}; + +enum EventEncryptionAlgorithm { + "OlmV1Curve25519AesSha2", + "MegolmV1AesSha2", +}; + +enum HistoryVisibility { + "Invited", + "Joined", + "Shared", + "WorldReadable", +}; + +dictionary EncryptionSettings { + EventEncryptionAlgorithm algorithm; + u64 rotation_period; + u64 rotation_period_msgs; + HistoryVisibility history_visibility; + boolean only_allow_trusted_devices; +}; + interface OlmMachine { [Throws=CryptoStoreError] constructor( @@ -282,7 +309,7 @@ interface OlmMachine { [Throws=CryptoStoreError] Device? get_device([ByRef] string user_id, [ByRef] string device_id, u32 timeout); [Throws=CryptoStoreError] - void mark_device_as_trusted([ByRef] string user_id, [ByRef] string device_id); + void set_local_trust([ByRef] string user_id, [ByRef] string device_id, LocalTrust trust_state); [Throws=SignatureError] SignatureUploadRequest verify_device([ByRef] string user_id, [ByRef] string device_id); [Throws=CryptoStoreError] @@ -294,7 +321,11 @@ interface OlmMachine { [Throws=CryptoStoreError] Request? get_missing_sessions(sequence users); [Throws=CryptoStoreError] - sequence share_room_key([ByRef] string room_id, sequence users); + sequence share_room_key( + [ByRef] string room_id, + sequence users, + EncryptionSettings settings + ); [Throws=CryptoStoreError] void receive_unencrypted_verification_event([ByRef] string event, [ByRef] string room_id); diff --git a/bindings/matrix-sdk-crypto-ffi/src/responses.rs b/bindings/matrix-sdk-crypto-ffi/src/responses.rs index feb6b89c9..0a2512804 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/responses.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/responses.rs @@ -19,7 +19,7 @@ use ruma::{ }, }, message::send_message_event::v3::Response as RoomMessageResponse, - sync::sync_events::v3::DeviceLists as RumaDeviceLists, + sync::sync_events::DeviceLists as RumaDeviceLists, to_device::send_event_to_device::v3::Response as ToDeviceResponse, }, assign, diff --git a/bindings/matrix-sdk-crypto-js/Cargo.toml b/bindings/matrix-sdk-crypto-js/Cargo.toml index 718c6df90..17b62cd99 100644 --- a/bindings/matrix-sdk-crypto-js/Cargo.toml +++ b/bindings/matrix-sdk-crypto-js/Cargo.toml @@ -24,15 +24,15 @@ crate-type = ["cdylib"] [features] default = ["tracing", "qrcode"] qrcode = ["matrix-sdk-crypto/qrcode", "dep:matrix-sdk-qrcode"] -tracing = [] +tracing = ["dep:tracing"] [dependencies] matrix-sdk-common = { version = "0.6.0", path = "../../crates/matrix-sdk-common", features = ["js"] } matrix-sdk-crypto = { version = "0.6.0", path = "../../crates/matrix-sdk-crypto", features = ["js"] } matrix-sdk-indexeddb = { version = "0.2.0", path = "../../crates/matrix-sdk-indexeddb", features = ["experimental-nodejs"] } matrix-sdk-qrcode = { version = "0.4.0", path = "../../crates/matrix-sdk-qrcode", optional = true } -ruma = { version = "0.7.0", features = ["client-api-c", "js", "rand", "unstable-msc2676", "unstable-msc2677"] } -vodozemac = { version = "0.3.0", features = ["js"] } +ruma = { workspace = true, features = ["js", "rand", "unstable-msc2676", "unstable-msc2677"] } +vodozemac = { workspace = true, features = ["js"] } wasm-bindgen = "0.2.80" wasm-bindgen-futures = "0.4.30" js-sys = "0.3.49" @@ -40,6 +40,6 @@ console_error_panic_hook = "0.1.7" serde_json = "1.0.79" http = "0.2.6" anyhow = "1.0.58" -tracing = { version = "0.1.35", default-features = false, features = ["attributes"] } +tracing = { workspace = true, optional = true } tracing-subscriber = { version = "0.3.14", default-features = false, features = ["registry", "std"] } -zeroize = "1.3.0" +zeroize = { workspace = true } diff --git a/bindings/matrix-sdk-crypto-js/src/machine.rs b/bindings/matrix-sdk-crypto-js/src/machine.rs index b13d96686..3133d7331 100644 --- a/bindings/matrix-sdk-crypto-js/src/machine.rs +++ b/bindings/matrix-sdk-crypto-js/src/machine.rs @@ -30,20 +30,25 @@ pub struct OlmMachine { impl OlmMachine { /// Create a new memory based `OlmMachine`. /// - /// The created machine will keep the encryption keys only in - /// memory and once the objects is dropped, the keys will be lost. + /// The created machine will keep the encryption keys either in a IndexedDB + /// based store, or in a memory store and once the objects is dropped, + /// the keys will be lost. /// - /// `user_id` represents the unique ID of the user that owns this - /// machine. `device_id` represents the unique ID of the device + /// # Arguments + /// + /// * `user_id` - represents the unique ID of the user that owns this + /// machine. + /// + /// * `device_id` - represents the unique ID of the device /// that owns this machine. /// - /// `store_name` and `store_passphrase` are both optional, but - /// must be both set to have an effect. If they are both set, the - /// state of the machine will persist in a database named - /// `store_name` where its content is encrypted by the passphrase - /// given by `store_passphrase`. If they are not both set, the - /// created machine will keep the encryption keys only in memory, - /// and once the object is dropped, the keys will be lost. + /// * `store_name` - The name that should be used to open the IndexedDB + /// based database. If this isn't provided, a memory-only store will be + /// used. *Note* the memory-only store will lose your E2EE keys when the + /// `OlmMachine` gets dropped. + /// + /// * `store_passphrase` - The passphrase that should be used to encrypt the + /// IndexedDB based #[wasm_bindgen(constructor)] #[allow(clippy::new_ret_no_self)] pub fn new( @@ -66,6 +71,7 @@ impl OlmMachine { #[cfg(target_arch = "wasm32")] (Some(store_name), Some(mut store_passphrase)) => { use std::sync::Arc; + use zeroize::Zeroize; let store = Some( @@ -82,9 +88,22 @@ impl OlmMachine { store } - (Some(_), None) => return Err(anyhow::Error::msg("The `store_name` has been set, and so, it expects a `store_passphrase`, which is not set; please provide one")), + #[cfg(target_arch = "wasm32")] + (Some(store_name), None) => { + use std::sync::Arc; + Some( + matrix_sdk_indexeddb::IndexeddbCryptoStore::open_with_name(&store_name) + .await + .map(Arc::new)?, + ) + } - (None, Some(_)) => return Err(anyhow::Error::msg("The `store_passphrase` has been set, but it has an effect only if `store_name` is set, which is not; please provide one")), + (None, Some(_)) => { + return Err(anyhow::Error::msg( + "The `store_passphrase` has been set, but it has an effect only if \ + `store_name` is set, which is not; please provide one", + )) + } _ => None, }; diff --git a/bindings/matrix-sdk-crypto-js/src/responses.rs b/bindings/matrix-sdk-crypto-js/src/responses.rs index 888847f48..24d51cd48 100644 --- a/bindings/matrix-sdk-crypto-js/src/responses.rs +++ b/bindings/matrix-sdk-crypto-js/src/responses.rs @@ -156,7 +156,7 @@ impl DecryptedRoomEvent { /// trusted. #[wasm_bindgen(getter, js_name = "senderDevice")] pub fn sender_device(&self) -> Option { - Some(identifiers::DeviceId::from(self.encryption_info.as_ref()?.sender_device.clone())) + Some(self.encryption_info.as_ref()?.sender_device.as_ref()?.clone().into()) } /// The Curve25519 key of the device that created the megolm diff --git a/bindings/matrix-sdk-crypto-js/src/sync_events.rs b/bindings/matrix-sdk-crypto-js/src/sync_events.rs index 8e04c632c..a06815f9b 100644 --- a/bindings/matrix-sdk-crypto-js/src/sync_events.rs +++ b/bindings/matrix-sdk-crypto-js/src/sync_events.rs @@ -9,7 +9,7 @@ use crate::{identifiers, js::downcast}; #[wasm_bindgen] #[derive(Debug)] pub struct DeviceLists { - pub(crate) inner: ruma::api::client::sync::sync_events::v3::DeviceLists, + pub(crate) inner: ruma::api::client::sync::sync_events::DeviceLists, } #[wasm_bindgen] @@ -19,7 +19,7 @@ impl DeviceLists { /// `changed` and `left` must be an array of `UserId`. #[wasm_bindgen(constructor)] pub fn new(changed: Option, left: Option) -> Result { - let mut inner = ruma::api::client::sync::sync_events::v3::DeviceLists::default(); + let mut inner = ruma::api::client::sync::sync_events::DeviceLists::default(); inner.changed = changed .unwrap_or_default() diff --git a/bindings/matrix-sdk-crypto-js/tests/device.test.js b/bindings/matrix-sdk-crypto-js/tests/device.test.js index 532624e26..ec01a664e 100644 --- a/bindings/matrix-sdk-crypto-js/tests/device.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/device.test.js @@ -176,13 +176,11 @@ describe('Key Verification', () => { outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.request'); - const toDeviceEvents = { - events: [{ - sender: userId1.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], - }] - }; + const toDeviceEvents = [{ + sender: userId1.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], + }]; // Let's send the verification request to `m2`. await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -230,13 +228,11 @@ describe('Key Verification', () => { outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.ready'); - const toDeviceEvents = { - events: [{ - sender: userId2.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], - }], - }; + const toDeviceEvents = [{ + sender: userId2.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + }]; // Let's send the verification ready to `m1`. await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -287,13 +283,11 @@ describe('Key Verification', () => { outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.start'); - const toDeviceEvents = { - events: [{ - sender: userId2.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], - }], - }; + const toDeviceEvents = [{ + sender: userId2.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + }]; // Let's send the SAS start to `m1`. await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -335,13 +329,11 @@ describe('Key Verification', () => { outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.accept'); - const toDeviceEvents = { - events: [{ - sender: userId1.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], - }], - }; + const toDeviceEvents = [{ + sender: userId1.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], + }]; // Let's send the SAS accept to `m2`. await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -364,13 +356,11 @@ describe('Key Verification', () => { toDeviceRequest = JSON.parse(toDeviceRequest.body); expect(toDeviceRequest.event_type).toStrictEqual('m.key.verification.key'); - const toDeviceEvents = { - events: [{ - sender: userId2.toString(), - type: toDeviceRequest.event_type, - content: toDeviceRequest.messages[userId1.toString()][deviceId1.toString()], - }], - }; + const toDeviceEvents = [{ + sender: userId2.toString(), + type: toDeviceRequest.event_type, + content: toDeviceRequest.messages[userId1.toString()][deviceId1.toString()], + }]; // Let's send te SAS key to `m1`. await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -390,13 +380,11 @@ describe('Key Verification', () => { toDeviceRequest = JSON.parse(toDeviceRequest.body); expect(toDeviceRequest.event_type).toStrictEqual('m.key.verification.key'); - const toDeviceEvents = { - events: [{ - sender: userId1.toString(), - type: toDeviceRequest.event_type, - content: toDeviceRequest.messages[userId2.toString()][deviceId2.toString()], - }], - }; + const toDeviceEvents = [{ + sender: userId1.toString(), + type: toDeviceRequest.event_type, + content: toDeviceRequest.messages[userId2.toString()][deviceId2.toString()], + }]; // Let's send te SAS key to `m2`. await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -463,13 +451,11 @@ describe('Key Verification', () => { outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.mac'); - const toDeviceEvents = { - events: [{ - sender: userId1.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], - }], - }; + const toDeviceEvents = [{ + sender: userId1.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], + }]; // Let's send te SAS confirmation to `m2`. await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -491,13 +477,11 @@ describe('Key Verification', () => { outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.mac'); - const toDeviceEvents = { - events: [{ - sender: userId2.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], - }], - }; + const toDeviceEvents = [{ + sender: userId2.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + }]; // Let's send te SAS confirmation to `m1`. await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -512,13 +496,11 @@ describe('Key Verification', () => { outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.done'); - const toDeviceEvents = { - events: [{ - sender: userId2.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], - }], - }; + const toDeviceEvents = [{ + sender: userId2.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + }]; // Let's send te SAS done to `m1`. await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -538,13 +520,11 @@ describe('Key Verification', () => { toDeviceRequest = JSON.parse(toDeviceRequest.body); expect(toDeviceRequest.event_type).toStrictEqual('m.key.verification.done'); - const toDeviceEvents = { - events: [{ - sender: userId1.toString(), - type: toDeviceRequest.event_type, - content: toDeviceRequest.messages[userId2.toString()][deviceId2.toString()], - }], - }; + const toDeviceEvents = [{ + sender: userId1.toString(), + type: toDeviceRequest.event_type, + content: toDeviceRequest.messages[userId2.toString()][deviceId2.toString()], + }]; // Let's send te SAS key to `m2`. await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -628,13 +608,11 @@ describe('Key Verification', () => { outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.request'); - const toDeviceEvents = { - events: [{ - sender: userId1.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], - }] - }; + const toDeviceEvents = [{ + sender: userId1.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], + }]; // Let's send the verification request to `m2`. await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -685,13 +663,11 @@ describe('Key Verification', () => { outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.ready'); - const toDeviceEvents = { - events: [{ - sender: userId2.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], - }], - }; + const toDeviceEvents = [{ + sender: userId2.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + }]; // Let's send the verification ready to `m1`. await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -848,13 +824,11 @@ describe('Key Verification', () => { outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.start'); - const toDeviceEvents = { - events: [{ - sender: userId1.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], - }] - }; + const toDeviceEvents = [{ + sender: userId1.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], + }]; // Let's send the verification request to `m2`. await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -872,13 +846,11 @@ describe('Key Verification', () => { outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.done'); - const toDeviceEvents = { - events: [{ - sender: userId2.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], - }] - }; + const toDeviceEvents = [{ + sender: userId2.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + }]; // Let's send the verification request to `m2`. await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); diff --git a/bindings/matrix-sdk-crypto-js/tests/helper.js b/bindings/matrix-sdk-crypto-js/tests/helper.js index 1ced03d1a..29f21b61a 100644 --- a/bindings/matrix-sdk-crypto-js/tests/helper.js +++ b/bindings/matrix-sdk-crypto-js/tests/helper.js @@ -11,14 +11,14 @@ function* zip(...arrays) { // Add a machine to another machine, i.e. be sure a machine knows // another exists. async function addMachineToMachine(machineToAdd, machine) { - const toDeviceEvents = JSON.stringify({}); + const toDeviceEvents = JSON.stringify([]); const changedDevices = new DeviceLists(); const oneTimeKeyCounts = new Map(); const unusedFallbackKeys = new Set(); const receiveSyncChanges = JSON.parse(await machineToAdd.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys)); - expect(receiveSyncChanges).toEqual({}); + expect(receiveSyncChanges).toEqual([]); const outgoingRequests = await machineToAdd.outgoingRequests(); diff --git a/bindings/matrix-sdk-crypto-js/tests/machine.test.js b/bindings/matrix-sdk-crypto-js/tests/machine.test.js index 7eda1903c..c375015dd 100644 --- a/bindings/matrix-sdk-crypto-js/tests/machine.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/machine.test.js @@ -128,26 +128,26 @@ describe(OlmMachine.name, () => { test('can receive sync changes', async () => { const m = await machine(); - const toDeviceEvents = JSON.stringify({}); + const toDeviceEvents = JSON.stringify([]); const changedDevices = new DeviceLists(); const oneTimeKeyCounts = new Map(); const unusedFallbackKeys = new Set(); const receiveSyncChanges = JSON.parse(await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys)); - expect(receiveSyncChanges).toEqual({}); + expect(receiveSyncChanges).toEqual([]); }); test('can get the outgoing requests that need to be send out', async () => { const m = await machine(); - const toDeviceEvents = JSON.stringify({}); + const toDeviceEvents = JSON.stringify([]); const changedDevices = new DeviceLists(); const oneTimeKeyCounts = new Map(); const unusedFallbackKeys = new Set(); const receiveSyncChanges = JSON.parse(await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys)); - expect(receiveSyncChanges).toEqual({}); + expect(receiveSyncChanges).toEqual([]); const outgoingRequests = await m.outgoingRequests(); @@ -182,7 +182,7 @@ describe(OlmMachine.name, () => { beforeAll(async () => { m = await machine(new UserId('@alice:example.org'), new DeviceId('DEVICEID')); - const toDeviceEvents = JSON.stringify({}); + const toDeviceEvents = JSON.stringify([]); const changedDevices = new DeviceLists(); const oneTimeKeyCounts = new Map(); const unusedFallbackKeys = new Set(); diff --git a/bindings/matrix-sdk-crypto-nodejs/Cargo.toml b/bindings/matrix-sdk-crypto-nodejs/Cargo.toml index c5779d622..f3e8bff8b 100644 --- a/bindings/matrix-sdk-crypto-nodejs/Cargo.toml +++ b/bindings/matrix-sdk-crypto-nodejs/Cargo.toml @@ -26,17 +26,14 @@ tracing = ["dep:tracing-subscriber"] matrix-sdk-crypto = { version = "0.6.0", path = "../../crates/matrix-sdk-crypto", features = ["js"] } matrix-sdk-common = { version = "0.6.0", path = "../../crates/matrix-sdk-common", features = ["js"] } matrix-sdk-sled = { version = "0.2.0", path = "../../crates/matrix-sdk-sled", default-features = false, features = ["crypto-store"] } -ruma = { version = "0.7.0", features = ["client-api-c", "rand", "unstable-msc2676", "unstable-msc2677"] } +ruma = { workspace = true, features = ["rand", "unstable-msc2676", "unstable-msc2677"] } napi = { version = "2.9.1", default-features = false, features = ["napi6", "tokio_rt"] } napi-derive = "2.9.1" serde_json = "1.0.79" http = "0.2.6" -zeroize = "1.3.0" tracing-subscriber = { version = "0.3", default-features = false, features = ["tracing-log", "time", "smallvec", "fmt", "env-filter"], optional = true } - -[dependencies.vodozemac] -version = "0.3.0" -features = ["js"] +vodozemac = { workspace = true, features = ["js"]} +zeroize = { workspace = true } [build-dependencies] napi-build = "2.0.0" diff --git a/bindings/matrix-sdk-crypto-nodejs/src/machine.rs b/bindings/matrix-sdk-crypto-nodejs/src/machine.rs index 5a51a7fcf..d98919653 100644 --- a/bindings/matrix-sdk-crypto-nodejs/src/machine.rs +++ b/bindings/matrix-sdk-crypto-nodejs/src/machine.rs @@ -36,7 +36,7 @@ impl OlmMachine { // the factory function. We also manually implement the // constructor to raise an error when called. - /// Create a new memory-based `OlmMachine` asynchronously. + /// Create a new `OlmMachine` asynchronously. /// /// The persistence of the encryption keys and all the inner /// objects are controlled by the `store_path` argument. diff --git a/bindings/matrix-sdk-crypto-nodejs/src/responses.rs b/bindings/matrix-sdk-crypto-nodejs/src/responses.rs index 0efc96976..d6488f528 100644 --- a/bindings/matrix-sdk-crypto-nodejs/src/responses.rs +++ b/bindings/matrix-sdk-crypto-nodejs/src/responses.rs @@ -152,7 +152,7 @@ impl DecryptedRoomEvent { /// trusted. #[napi(getter)] pub fn sender_device(&self) -> Option { - Some(identifiers::DeviceId::from(self.encryption_info.as_ref()?.sender_device.clone())) + Some(self.encryption_info.as_ref()?.sender_device.as_ref()?.clone().into()) } /// The Curve25519 key of the device that created the megolm diff --git a/bindings/matrix-sdk-crypto-nodejs/src/sync_events.rs b/bindings/matrix-sdk-crypto-nodejs/src/sync_events.rs index 63e017021..4122bba2d 100644 --- a/bindings/matrix-sdk-crypto-nodejs/src/sync_events.rs +++ b/bindings/matrix-sdk-crypto-nodejs/src/sync_events.rs @@ -7,7 +7,7 @@ use crate::identifiers; /// Information on E2E device updates. #[napi] pub struct DeviceLists { - pub(crate) inner: ruma::api::client::sync::sync_events::v3::DeviceLists, + pub(crate) inner: ruma::api::client::sync::sync_events::DeviceLists, } #[napi] @@ -18,7 +18,7 @@ impl DeviceLists { changed: Option>, left: Option>, ) -> Self { - let mut inner = ruma::api::client::sync::sync_events::v3::DeviceLists::default(); + let mut inner = ruma::api::client::sync::sync_events::DeviceLists::default(); inner.changed = changed.into_iter().flatten().map(|user| user.inner.clone()).collect(); inner.left = left.into_iter().flatten().map(|user| user.inner.clone()).collect(); diff --git a/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js b/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js index 020486bd2..a3b424683 100644 --- a/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js +++ b/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js @@ -51,26 +51,26 @@ describe(OlmMachine.name, () => { test('can receive sync changes', async () => { const m = await machine(); - const toDeviceEvents = JSON.stringify({}); + const toDeviceEvents = JSON.stringify([]); const changedDevices = new DeviceLists(); const oneTimeKeyCounts = {}; const unusedFallbackKeys = []; const receiveSyncChanges = JSON.parse(await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys)); - expect(receiveSyncChanges).toEqual({}); + expect(receiveSyncChanges).toEqual([]); }); test('can get the outgoing requests that need to be send out', async () => { const m = await machine(); - const toDeviceEvents = JSON.stringify({}); + const toDeviceEvents = JSON.stringify([]); const changedDevices = new DeviceLists(); const oneTimeKeyCounts = {}; const unusedFallbackKeys = []; const receiveSyncChanges = JSON.parse(await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys)); - expect(receiveSyncChanges).toEqual({}); + expect(receiveSyncChanges).toEqual([]); const outgoingRequests = await m.outgoingRequests(); @@ -105,12 +105,12 @@ describe(OlmMachine.name, () => { beforeAll(async () => { m = await machine(new UserId('@alice:example.org'), new DeviceId('DEVICEID')); - const toDeviceEvents = JSON.stringify({}); + const toDeviceEvents = JSON.stringify([]); const changedDevices = new DeviceLists(); const oneTimeKeyCounts = {}; const unusedFallbackKeys = []; - const receiveSyncChanges = await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys); + await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys); outgoingRequests = await m.outgoingRequests(); expect(outgoingRequests).toHaveLength(2); diff --git a/bindings/matrix-sdk-ffi/Cargo.toml b/bindings/matrix-sdk-ffi/Cargo.toml index f00b4540d..c5f188560 100644 --- a/bindings/matrix-sdk-ffi/Cargo.toml +++ b/bindings/matrix-sdk-ffi/Cargo.toml @@ -12,19 +12,23 @@ repository = "https://github.com/matrix-org/matrix-rust-sdk" [lib] crate-type = ["cdylib", "staticlib"] +[features] +default = ["experimental-room-preview"] # the whole crate is still very experimental, so this is fine +experimental-room-preview = ["matrix-sdk/experimental-room-preview"] + [build-dependencies] -uniffi_build = { git = "https://github.com/mozilla/uniffi-rs", rev = "0eee77f67b716c4896494606e5931d249871b23a", features = ["builtin-bindgen"] } +uniffi_build = { workspace = true, features = ["builtin-bindgen"] } [dependencies] anyhow = "1.0.51" extension-trait = "1.0.1" futures-core = "0.3.17" -futures-signals = { version = "0.3.28" } +futures-signals = { version = "0.3.30", default-features = false } futures-util = { version = "0.3.17", default-features = false } # FIXME: we currently can't feature flag anything in the api.udl, therefore we must enforce sliding-sync being exposed here.. # see https://github.com/matrix-org/matrix-rust-sdk/issues/1014 -#matrix-sdk = { path = "../../crates/matrix-sdk", features = ["anyhow", "markdown", "sliding-sync", "socks"], version = "0.6.0" } -matrix-sdk = { path = "../../crates/matrix-sdk", default-features = false, features = ["anyhow", "e2e-encryption", "sled", "markdown", "sliding-sync", "socks", "rustls-tls"], version = "0.6.0" } +#matrix-sdk = { path = "../../crates/matrix-sdk", features = ["anyhow", "experimental-timeline", "markdown", "sliding-sync", "socks"], version = "0.6.0" } +matrix-sdk = { path = "../../crates/matrix-sdk", default-features = false, features = ["anyhow", "sled", "e2e-encryption", "experimental-timeline", "markdown", "sliding-sync", "socks", "rustls-tls"], version = "0.6.0" } once_cell = "1.10.0" sanitize-filename-reader-friendly = "2.2.1" serde = { version = "1", features = ["derive"] } @@ -32,8 +36,7 @@ serde_json = { version = "1" } thiserror = "1.0.30" tokio = { version = "1", features = ["rt-multi-thread", "macros"] } tokio-stream = "0.1.8" -tracing = "0.1.32" +tracing = { workspace = true } 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 = { git = "https://github.com/mozilla/uniffi-rs", rev = "0eee77f67b716c4896494606e5931d249871b23a" } -uniffi_macros = { git = "https://github.com/mozilla/uniffi-rs", rev = "0eee77f67b716c4896494606e5931d249871b23a" } +uniffi = { workspace = true } +uniffi_macros = { workspace = true } diff --git a/bindings/matrix-sdk-ffi/src/api.udl b/bindings/matrix-sdk-ffi/src/api.udl index 09f961707..61b7da5ee 100644 --- a/bindings/matrix-sdk-ffi/src/api.udl +++ b/bindings/matrix-sdk-ffi/src/api.udl @@ -175,6 +175,9 @@ interface SlidingSyncBuilder { [Self=ByArc] SlidingSyncBuilder add_view(SlidingSyncView view); + [Self=ByArc] + SlidingSyncBuilder with_common_extensions(); + [Throws=ClientError, Self=ByArc] SlidingSync build(); }; @@ -225,10 +228,6 @@ interface Client { void logout(); }; -callback interface RoomDelegate { - void did_receive_message(AnyMessage message); -}; - enum Membership { "Invited", "Joined", @@ -247,6 +246,14 @@ interface Room { [Throws=ClientError] string? member_display_name(string user_id); + void add_timeline_listener(TimelineListener listener); + + // Loads older messages into the timeline. + // + // Raises an exception if there are no timeline listeners. + [Throws=ClientError] + PaginationOutcome paginate_backwards(u16 limit); + [Throws=ClientError] void send(RoomMessageEventContent msg, string? txn_id); @@ -257,50 +264,140 @@ interface Room { void redact(string event_id, string? reason, string? txn_id); }; -interface BackwardsStream { - sequence paginate_backwards(u64 count); +callback interface TimelineListener { + void on_update(TimelineDiff update); +}; + +interface TimelineDiff { + TimelineChange change(); + + [Self=ByArc] + sequence? replace(); + [Self=ByArc] + InsertAtData? insert_at(); + [Self=ByArc] + UpdateAtData? update_at(); + u32? remove_at(); + MoveData? move(); + [Self=ByArc] + TimelineItem? push(); +}; + +enum TimelineChange { + "Replace", + "InsertAt", + "UpdateAt", + "RemoveAt", + "Move", + "Push", + "Pop", + "Clear", +}; + +dictionary InsertAtData { + u32 index; + TimelineItem item; +}; + +dictionary UpdateAtData { + u32 index; + TimelineItem item; +}; + +dictionary MoveData { + u32 old_index; + u32 new_index; +}; + +interface TimelineItem {}; + +interface EventTimelineItem { + TimelineKey key(); + sequence reactions(); +}; + +[Enum] +interface TimelineKey { + TransactionId(string txn_id); + EventId(string event_id); +}; + +// Other methods defined via proc-macro +interface Message { + MessageType? msgtype(); +}; + +[Enum] +interface MessageType { + Emote(EmoteMessageContent content); + Image(ImageMessageContent content); + Notice(NoticeMessageContent content); + Text(TextMessageContent content); +}; + +dictionary EmoteMessageContent { + string body; + FormattedBody? formatted; +}; + +dictionary ImageMessageContent { + string body; + MediaSource source; + ImageInfo? info; +}; + +dictionary ImageInfo { + u64? height; + u64? width; + string? mimetype; + u64? size; + ThumbnailInfo? thumbnail_info; + MediaSource? thumbnail_source; + string? blurhash; +}; + +dictionary ThumbnailInfo { + u64? height; + u64? width; + string? mimetype; + u64? size; +}; + +dictionary NoticeMessageContent { + string body; + FormattedBody? formatted; +}; + +dictionary TextMessageContent { + string body; + FormattedBody? formatted; +}; + +dictionary FormattedBody { + MessageFormat format; + string body; +}; + +enum MessageFormat { + "Html", + "Unknown", +}; + +dictionary Reaction { + string key; + u64 count; + // senders to come +}; + +interface VirtualTimelineItem {}; + +dictionary PaginationOutcome { + // Whether there's more messages to be paginated. + boolean more_messages; }; interface RoomMessageEventContent {}; -interface AnyMessage { - TextMessage? text_message(); - ImageMessage? image_message(); - NoticeMessage? notice_message(); - EmoteMessage? emote_message(); -}; - -interface BaseMessage { - string id(); - string body(); - string sender(); - u64 origin_server_ts(); - string? transaction_id(); -}; - -interface TextMessage { - BaseMessage base_message(); - string? html_body(); -}; - -interface ImageMessage { - BaseMessage base_message(); - MediaSource source(); - u64? width(); - u64? height(); - string? blurhash(); -}; - -interface NoticeMessage { - BaseMessage base_message(); - string? html_body(); -}; - -interface EmoteMessage { - BaseMessage base_message(); - string? html_body(); -}; - interface MediaSource { string url(); }; diff --git a/bindings/matrix-sdk-ffi/src/backward_stream.rs b/bindings/matrix-sdk-ffi/src/backward_stream.rs deleted file mode 100644 index 4b664675a..000000000 --- a/bindings/matrix-sdk-ffi/src/backward_stream.rs +++ /dev/null @@ -1,42 +0,0 @@ -use core::pin::Pin; -use std::sync::Arc; - -use futures_core::Stream; -use matrix_sdk::{deserialized_responses::SyncTimelineEvent, locks::Mutex, Result}; -use tokio_stream::StreamExt; -use tracing::error; - -use super::{ - messages::{sync_event_to_message, AnyMessage}, - RUNTIME, -}; - -type MsgStream = Pin> + Send>>; - -pub struct BackwardsStream { - stream: Arc>, -} - -impl BackwardsStream { - pub fn new(stream: MsgStream) -> Self { - BackwardsStream { stream: Arc::new(Mutex::new(Box::pin(stream))) } - } - - pub fn paginate_backwards(&self, count: u64) -> Vec> { - let stream = self.stream.clone(); - RUNTIME.block_on(async move { - let mut stream = stream.lock().await; - (&mut *stream) - .take(count as usize) - .filter_map(|r| match r { - Ok(ev) => sync_event_to_message(ev), - Err(e) => { - error!("Pagniation error: {e}"); - None - } - }) - .collect() - .await - }) - } -} diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 452b3491a..28f2e961c 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -1,26 +1,23 @@ use std::sync::{Arc, RwLock}; -use anyhow::anyhow; +use anyhow::{anyhow, Context}; use matrix_sdk::{ config::SyncSettings, media::{MediaFormat, MediaRequest, MediaThumbnailSize}, ruma::{ - api::{ - client::{ - account::whoami, - error::ErrorKind, - filter::{FilterDefinition, LazyLoadOptions, RoomEventFilter, RoomFilter}, - media::get_content_thumbnail::v3::Method, - session::get_login_types, - sync::sync_events::v3::Filter, - }, - error::{FromHttpResponseError, ServerError}, + api::client::{ + account::whoami, + error::ErrorKind, + filter::{FilterDefinition, LazyLoadOptions, RoomEventFilter, RoomFilter}, + media::get_content_thumbnail::v3::Method, + session::get_login_types, + sync::sync_events::v3::Filter, }, events::room::MediaSource, serde::Raw, TransactionId, UInt, }, - Client as MatrixClient, Error, HttpError, LoopCtrl, RumaApiError, Session, + Client as MatrixClient, Error, LoopCtrl, RumaApiError, Session, }; use super::{ @@ -132,7 +129,7 @@ impl Client { pub fn restore_token(&self) -> anyhow::Result { RUNTIME.block_on(async move { - let session = self.client.session().expect("Missing session"); + let session = self.client.session().context("Missing session")?; let homeurl = self.client.homeserver().await.into(); Ok(serde_json::to_string(&RestoreToken { session, @@ -144,14 +141,14 @@ impl Client { } pub fn user_id(&self) -> anyhow::Result { - let user_id = self.client.user_id().expect("No User ID found"); + let user_id = self.client.user_id().context("No User ID found")?; Ok(user_id.to_string()) } pub fn display_name(&self) -> anyhow::Result { let l = self.client.clone(); RUNTIME.block_on(async move { - let display_name = l.account().get_display_name().await?.expect("No User ID found"); + let display_name = l.account().get_display_name().await?.context("No User ID found")?; Ok(display_name) }) } @@ -159,13 +156,13 @@ impl Client { pub fn avatar_url(&self) -> anyhow::Result { let l = self.client.clone(); RUNTIME.block_on(async move { - let avatar_url = l.account().get_avatar_url().await?.expect("No User ID found"); + let avatar_url = l.account().get_avatar_url().await?.context("No User ID found")?; Ok(avatar_url.to_string()) }) } pub fn device_id(&self) -> anyhow::Result { - let device_id = self.client.device_id().expect("No Device ID found"); + let device_id = self.client.device_id().context("No Device ID found")?; Ok(device_id.to_string()) } @@ -238,13 +235,13 @@ impl Client { return Ok(Arc::new(session_verification_controller.clone())); } - let user_id = self.client.user_id().expect("Failed retrieving current user_id"); + let user_id = self.client.user_id().context("Failed retrieving current user_id")?; let user_identity = self .client .encryption() .get_user_identity(user_id) .await? - .expect("Failed retrieving user identity"); + .context("Failed retrieving user identity")?; let session_verification_controller = SessionVerificationController::new(user_identity); @@ -268,10 +265,7 @@ impl Client { /// Process a sync error and return loop control accordingly fn process_sync_error(&self, sync_error: Error) -> LoopCtrl { let mut control = LoopCtrl::Continue; - if let Error::Http(HttpError::Api(FromHttpResponseError::Server(ServerError::Known( - RumaApiError::ClientApi(error), - )))) = sync_error - { + if let Some(RumaApiError::ClientApi(error)) = sync_error.as_ruma_api_error() { if let ErrorKind::UnknownToken { soft_logout } = error.kind { self.state.write().unwrap().is_soft_logout = soft_logout; if let Some(delegate) = &*self.delegate.read().unwrap() { @@ -363,7 +357,7 @@ impl Client { &*session_verification_controller.read().await { session_verification_controller - .process_to_device_messages(sync_response.to_device) + .process_to_device_messages(sync_response.to_device_events) .await; } diff --git a/bindings/matrix-sdk-ffi/src/client_builder.rs b/bindings/matrix-sdk-ffi/src/client_builder.rs index 7a9317d39..b665633b7 100644 --- a/bindings/matrix-sdk-ffi/src/client_builder.rs +++ b/bindings/matrix-sdk-ffi/src/client_builder.rs @@ -74,7 +74,7 @@ impl ClientBuilder { let data_path = PathBuf::from(base_path).join(sanitize(username)); fs::create_dir_all(&data_path)?; - inner_builder = RUNTIME.block_on(inner_builder.sled_store(data_path, None))?; + inner_builder = inner_builder.sled_store(data_path, None); } // Determine server either from URL, server name or user ID. diff --git a/bindings/matrix-sdk-ffi/src/lib.rs b/bindings/matrix-sdk-ffi/src/lib.rs index 7cfce4947..fbc927c2c 100644 --- a/bindings/matrix-sdk-ffi/src/lib.rs +++ b/bindings/matrix-sdk-ffi/src/lib.rs @@ -2,15 +2,32 @@ #![allow(unused_qualifications)] +macro_rules! unwrap_or_clone_arc_into_variant { + ( + $arc:ident $(, .$field:tt)?, $pat:pat => $body:expr + ) => { + #[allow(unused_variables)] + match &(*$arc)$(.$field)? { + $pat => { + #[warn(unused_variables)] + match crate::helpers::unwrap_or_clone_arc($arc)$(.$field)? { + $pat => Some($body), + _ => unreachable!(), + } + }, + _ => None, + } + }; +} + pub mod authentication_service; -pub mod backward_stream; pub mod client; pub mod client_builder; mod helpers; -pub mod messages; pub mod room; pub mod session_verification; pub mod sliding_sync; +pub mod timeline; mod uniffi_api; use std::io; @@ -27,11 +44,14 @@ pub use uniffi_api::*; pub static RUNTIME: Lazy = Lazy::new(|| Runtime::new().expect("Can't start Tokio runtime")); -pub use matrix_sdk::ruma::{api::client::account::register, UserId}; +pub use matrix_sdk::{ + room::timeline::PaginationOutcome, + ruma::{api::client::account::register, UserId}, +}; pub use self::{ - authentication_service::*, backward_stream::*, client::*, messages::*, room::*, - session_verification::*, sliding_sync::*, + authentication_service::*, client::*, room::*, session_verification::*, sliding_sync::*, + timeline::*, }; #[derive(Default, Debug)] @@ -79,12 +99,14 @@ mod uniffi_types { authentication_service::{AuthenticationService, HomeserverLoginDetails}, client::Client, client_builder::ClientBuilder, - messages::AnyMessage, room::Room, session_verification::SessionVerificationEmoji, sliding_sync::{ SlidingSync, SlidingSyncBuilder, SlidingSyncRoom, SlidingSyncView, StoppableSpawn, UnreadNotificationsCount, }, + timeline::{ + EventTimelineItem, Message, TimelineItem, TimelineItemContent, VirtualTimelineItem, + }, }; } diff --git a/bindings/matrix-sdk-ffi/src/messages.rs b/bindings/matrix-sdk-ffi/src/messages.rs deleted file mode 100644 index bae2d189e..000000000 --- a/bindings/matrix-sdk-ffi/src/messages.rs +++ /dev/null @@ -1,254 +0,0 @@ -use std::sync::Arc; - -use extension_trait::extension_trait; -pub use matrix_sdk::ruma::events::room::{message::RoomMessageEventContent, MediaSource}; -use matrix_sdk::{ - deserialized_responses::SyncTimelineEvent, - ruma::events::{ - room::{ - message::{ImageMessageEventContent, MessageFormat, MessageType}, - ImageInfo, - }, - AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent, - }, -}; - -#[derive(Clone)] -pub struct BaseMessage { - id: String, - body: String, - sender: String, - origin_server_ts: u64, - transaction_id: Option, -} - -impl BaseMessage { - pub fn id(&self) -> String { - self.id.clone() - } - - pub fn body(&self) -> String { - self.body.clone() - } - - pub fn sender(&self) -> String { - self.sender.clone() - } - - pub fn origin_server_ts(&self) -> u64 { - self.origin_server_ts - } - - pub fn transaction_id(&self) -> Option { - self.transaction_id.clone() - } -} - -pub struct TextMessage { - base_message: Arc, - html_body: Option, -} - -impl TextMessage { - pub fn base_message(&self) -> Arc { - self.base_message.clone() - } - - pub fn html_body(&self) -> Option { - self.html_body.clone() - } -} - -pub struct ImageMessage { - base_message: Arc, - source: Arc, - info: Option>, -} - -impl ImageMessage { - pub fn base_message(&self) -> Arc { - self.base_message.clone() - } - - pub fn source(&self) -> Arc { - self.source.clone() - } - - pub fn width(&self) -> Option { - self.info.clone()?.width?.try_into().ok() - } - - pub fn height(&self) -> Option { - self.info.clone()?.height?.try_into().ok() - } - - pub fn blurhash(&self) -> Option { - self.info.clone()?.blurhash - } -} - -pub struct NoticeMessage { - base_message: Arc, - html_body: Option, -} - -impl NoticeMessage { - pub fn base_message(&self) -> Arc { - self.base_message.clone() - } - - pub fn html_body(&self) -> Option { - self.html_body.clone() - } -} - -pub struct EmoteMessage { - base_message: Arc, - html_body: Option, -} - -impl EmoteMessage { - pub fn base_message(&self) -> Arc { - self.base_message.clone() - } - - pub fn html_body(&self) -> Option { - self.html_body.clone() - } -} - -pub struct AnyMessage { - text: Option>, - image: Option>, - notice: Option>, - emote: Option>, -} - -impl AnyMessage { - pub fn text_message(&self) -> Option> { - self.text.clone() - } - - pub fn image_message(&self) -> Option> { - self.image.clone() - } - - pub fn notice_message(&self) -> Option> { - self.notice.clone() - } - - pub fn emote_message(&self) -> Option> { - self.emote.clone() - } -} - -pub fn sync_event_to_message(sync_event: SyncTimelineEvent) -> Option> { - match sync_event.event.deserialize() { - Ok(AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( - SyncMessageLikeEvent::Original(m), - ))) => { - let base_message = Arc::new(BaseMessage { - id: m.event_id.to_string(), - body: m.content.body().to_owned(), - sender: m.sender.to_string(), - origin_server_ts: m.origin_server_ts.as_secs().into(), - transaction_id: m.unsigned.transaction_id.map(|txn_id| txn_id.to_string()), - }); - - match m.content.msgtype { - MessageType::Image(ImageMessageEventContent { source, info, .. }) => { - let any_message = AnyMessage { - text: None, - image: Some(Arc::new(ImageMessage { - base_message, - source: Arc::new(source), - info, - })), - notice: None, - emote: None, - }; - - Some(Arc::new(any_message)) - } - MessageType::Text(content) => { - let mut html_body: Option = None; - if let Some(formatted_body) = content.formatted { - if formatted_body.format == MessageFormat::Html { - html_body = Some(formatted_body.body); - } - } - - let any_message = AnyMessage { - text: Some(Arc::new(TextMessage { base_message, html_body })), - image: None, - notice: None, - emote: None, - }; - Some(Arc::new(any_message)) - } - MessageType::Notice(content) => { - let mut html_body: Option = None; - if let Some(formatted_body) = content.formatted { - if formatted_body.format == MessageFormat::Html { - html_body = Some(formatted_body.body); - } - } - - let any_message = AnyMessage { - text: None, - image: None, - notice: Some(Arc::new(NoticeMessage { base_message, html_body })), - emote: None, - }; - Some(Arc::new(any_message)) - } - MessageType::Emote(content) => { - let mut html_body: Option = None; - if let Some(formatted_body) = content.formatted { - if formatted_body.format == MessageFormat::Html { - html_body = Some(formatted_body.body); - } - } - - let any_message = AnyMessage { - text: None, - image: None, - notice: None, - emote: Some(Arc::new(EmoteMessage { base_message, html_body })), - }; - Some(Arc::new(any_message)) - } - _ => { - let any_message = AnyMessage { - text: Some(Arc::new(TextMessage { base_message, html_body: None })), - image: None, - notice: None, - emote: None, - }; - Some(Arc::new(any_message)) - } - } - } - _ => None, - } -} - -#[uniffi::export] -pub fn media_source_from_url(url: String) -> Arc { - Arc::new(MediaSource::Plain(url.into())) -} - -#[uniffi::export] -pub fn message_event_content_from_markdown(md: String) -> Arc { - Arc::new(RoomMessageEventContent::text_markdown(md)) -} - -#[extension_trait] -pub impl MediaSourceExt for MediaSource { - fn url(&self) -> String { - match self { - MediaSource::Plain(url) => url.to_string(), - MediaSource::Encrypted(file) => file.url.to_string(), - } - } -} diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index 104b48b94..ea77f0177 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -1,19 +1,24 @@ -use std::{convert::TryFrom, sync::Arc}; +use std::{ + convert::TryFrom, + sync::{Arc, RwLock}, +}; use anyhow::{bail, Context, Result}; +use futures_signals::signal_vec::SignalVecExt; use matrix_sdk::{ - room::Room as MatrixRoom, + room::{ + timeline::{PaginationOutcome, Timeline}, + Room as SdkRoom, + }, ruma::{ events::room::message::{RoomMessageEvent, RoomMessageEventContent}, EventId, UserId, }, }; +use tracing::error; -use super::{messages::AnyMessage, RUNTIME}; - -pub trait RoomDelegate: Sync + Send { - fn did_receive_message(&self, messages: Arc); -} +use super::RUNTIME; +use crate::{TimelineDiff, TimelineListener}; pub enum Membership { Invited, @@ -22,7 +27,8 @@ pub enum Membership { } pub struct Room { - room: MatrixRoom, + room: SdkRoom, + timeline: RwLock>>, } #[uniffi::export] @@ -62,11 +68,19 @@ impl Room { pub fn is_tombstoned(&self) -> bool { self.room.is_tombstoned() } + + /// Removes the timeline. + /// + /// Timeline items cached in memory as well as timeline listeners are + /// dropped. + pub fn remove_timeline(&self) { + *self.timeline.write().unwrap() = None; + } } impl Room { - pub fn new(room: MatrixRoom) -> Self { - Room { room } + pub fn new(room: SdkRoom) -> Self { + Room { room, timeline: RwLock::default() } } pub fn display_name(&self) -> Result { @@ -78,8 +92,8 @@ impl Room { let room = self.room.clone(); let user_id = user_id; RUNTIME.block_on(async move { - let user_id = <&UserId>::try_from(&*user_id).expect("Invalid user id."); - let member = room.get_member(user_id).await?.expect("No user found"); + let user_id = <&UserId>::try_from(&*user_id).context("Invalid user id.")?; + let member = room.get_member(user_id).await?.context("No user found")?; let avatar_url_string = member.avatar_url().map(|m| m.to_string()); Ok(avatar_url_string) }) @@ -89,8 +103,8 @@ impl Room { let room = self.room.clone(); let user_id = user_id; RUNTIME.block_on(async move { - let user_id = <&UserId>::try_from(&*user_id).expect("Invalid user id."); - let member = room.get_member(user_id).await?.expect("No user found"); + let user_id = <&UserId>::try_from(&*user_id).context("Invalid user id.")?; + let member = room.get_member(user_id).await?.context("No user found")?; let avatar_url_string = member.display_name().map(|m| m.to_owned()); Ok(avatar_url_string) }) @@ -98,20 +112,50 @@ impl Room { pub fn membership(&self) -> Membership { match &self.room { - MatrixRoom::Invited(_) => Membership::Invited, - MatrixRoom::Joined(_) => Membership::Joined, - MatrixRoom::Left(_) => Membership::Left, + SdkRoom::Invited(_) => Membership::Invited, + SdkRoom::Joined(_) => Membership::Joined, + SdkRoom::Left(_) => Membership::Left, + } + } + + pub fn add_timeline_listener(&self, listener: Box) { + let timeline_signal = self + .timeline + .write() + .unwrap() + .get_or_insert_with(|| Arc::new(self.room.timeline())) + .signal(); + + let listener: Arc = listener.into(); + RUNTIME.spawn(timeline_signal.for_each(move |diff| { + let listener = listener.clone(); + let fut = RUNTIME + .spawn_blocking(move || listener.on_update(Arc::new(TimelineDiff::new(diff)))); + + async move { + if let Err(e) = fut.await { + error!("Timeline listener error: {e}"); + } + } + })); + } + + pub fn paginate_backwards(&self, limit: u16) -> Result { + if let Some(timeline) = &*self.timeline.read().unwrap() { + RUNTIME.block_on(async move { Ok(timeline.paginate_backwards(limit.into()).await?) }) + } else { + bail!("No timeline listeners registered, can't paginate"); } } pub fn send(&self, msg: Arc, txn_id: Option) -> Result<()> { - let room = match &self.room { - MatrixRoom::Joined(j) => j.clone(), - _ => bail!("Can't send to a room that isn't in joined state"), + let timeline = match &*self.timeline.read().unwrap() { + Some(t) => Arc::clone(t), + None => bail!("Timeline not set up, can't send message"), }; RUNTIME.block_on(async move { - room.send((*msg).to_owned(), txn_id.as_deref().map(Into::into)).await?; + timeline.send((*msg).to_owned().into(), txn_id.as_deref().map(Into::into)).await?; Ok(()) }) } @@ -123,10 +167,15 @@ impl Room { txn_id: Option, ) -> Result<()> { let room = match &self.room { - MatrixRoom::Joined(j) => j.clone(), + SdkRoom::Joined(j) => j.clone(), _ => bail!("Can't send to a room that isn't in joined state"), }; + let timeline = match &*self.timeline.read().unwrap() { + Some(t) => Arc::clone(t), + None => bail!("Timeline not set up, can't send message"), + }; + let event_id: &EventId = in_reply_to_event_id.as_str().try_into().context("Failed to create EventId.")?; @@ -144,7 +193,7 @@ impl Room { let reply_content = RoomMessageEventContent::text_markdown(msg).make_reply_to(original_message); - room.send(reply_content, txn_id.as_deref().map(Into::into)).await?; + timeline.send(reply_content.into(), txn_id.as_deref().map(Into::into)).await?; Ok(()) }) @@ -167,7 +216,7 @@ impl Room { txn_id: Option, ) -> Result<()> { let room = match &self.room { - MatrixRoom::Joined(j) => j.clone(), + SdkRoom::Joined(j) => j.clone(), _ => bail!("Can't redact in a room that isn't in joined state"), }; @@ -180,8 +229,9 @@ impl Room { } impl std::ops::Deref for Room { - type Target = MatrixRoom; - fn deref(&self) -> &MatrixRoom { + type Target = SdkRoom; + + fn deref(&self) -> &SdkRoom { &self.room } } diff --git a/bindings/matrix-sdk-ffi/src/session_verification.rs b/bindings/matrix-sdk-ffi/src/session_verification.rs index bc9b3d59e..9db8f5c4c 100644 --- a/bindings/matrix-sdk-ffi/src/session_verification.rs +++ b/bindings/matrix-sdk-ffi/src/session_verification.rs @@ -6,8 +6,8 @@ use matrix_sdk::{ verification::{SasVerification, VerificationRequest}, }, ruma::{ - api::client::sync::sync_events::v3::ToDevice, events::{key::verification::VerificationMethod, AnyToDeviceEvent}, + serde::Raw, }, }; @@ -106,10 +106,10 @@ impl SessionVerificationController { }) } - pub async fn process_to_device_messages(&self, to_device: ToDevice) { + pub async fn process_to_device_messages(&self, to_device_events: Vec>) { let sas_verification = self.sas_verification.clone(); - for event in to_device.events.into_iter().filter_map(|e| e.deserialize().ok()) { + for event in to_device_events.into_iter().filter_map(|e| e.deserialize().ok()) { match event { AnyToDeviceEvent::KeyVerificationReady(event) => { if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) { diff --git a/bindings/matrix-sdk-ffi/src/sliding_sync.rs b/bindings/matrix-sdk-ffi/src/sliding_sync.rs index 36bc48086..97eda7b9e 100644 --- a/bindings/matrix-sdk-ffi/src/sliding_sync.rs +++ b/bindings/matrix-sdk-ffi/src/sliding_sync.rs @@ -5,6 +5,10 @@ use futures_signals::{ signal_vec::{SignalVecExt, VecDiff}, }; use futures_util::{pin_mut, StreamExt}; +#[cfg(feature = "experimental-room-preview")] +use matrix_sdk::ruma::events::{ + room::message::SyncRoomMessageEvent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, +}; use matrix_sdk::ruma::{ api::client::sync::sync_events::{ v4::RoomSubscription as RumaRoomSubscription, @@ -19,7 +23,9 @@ pub use matrix_sdk::{ use tokio::task::JoinHandle; use super::{Client, Room, RUNTIME}; -use crate::{helpers::unwrap_or_clone_arc, messages::AnyMessage}; +use crate::helpers::unwrap_or_clone_arc; +#[cfg(feature = "experimental-room-preview")] +use crate::EventTimelineItem; pub struct StoppableSpawn { handle: Arc>>>, @@ -115,22 +121,31 @@ impl SlidingSyncRoom { Arc::new(self.inner.unread_notifications.clone().into()) } + pub fn full_room(&self) -> Option> { + self.client.get_room(self.inner.room_id()).map(|room| Arc::new(Room::new(room))) + } +} + +#[cfg(feature = "experimental-room-preview")] +#[uniffi::export] +impl SlidingSyncRoom { #[allow(clippy::significant_drop_in_scrutinee)] - pub fn latest_room_message(&self) -> Option> { + pub fn latest_room_message(&self) -> Option> { let messages = self.inner.timeline(); // room is having the latest events at the end, let lock = messages.lock_ref(); - for m in lock.iter().rev() { - if let Some(e) = crate::messages::sync_event_to_message(m.clone().into()) { - return Some(e); + for ev in lock.iter().rev() { + if let Ok(AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( + SyncRoomMessageEvent::Original(o), + ))) = ev.event.deserialize() + { + let inner = + matrix_sdk::room::timeline::EventTimelineItem::_new(o, ev.event.clone()); + return Some(Arc::new(EventTimelineItem(inner))); } } None } - - pub fn full_room(&self) -> Option> { - self.client.get_room(self.inner.room_id()).map(|room| Arc::new(Room::new(room))) - } } pub struct UpdateSummary { @@ -570,6 +585,12 @@ impl SlidingSyncBuilder { Arc::new(builder) } + pub fn with_common_extensions(self: Arc) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + builder.inner = builder.inner.with_common_extensions(); + Arc::new(builder) + } + pub fn build(self: Arc) -> anyhow::Result> { let builder = unwrap_or_clone_arc(self); Ok(Arc::new(SlidingSync::new(builder.inner.build()?, builder.client))) diff --git a/bindings/matrix-sdk-ffi/src/timeline.rs b/bindings/matrix-sdk-ffi/src/timeline.rs new file mode 100644 index 000000000..7179d18ff --- /dev/null +++ b/bindings/matrix-sdk-ffi/src/timeline.rs @@ -0,0 +1,405 @@ +use std::sync::Arc; + +use extension_trait::extension_trait; +use futures_signals::signal_vec::VecDiff; +pub use matrix_sdk::ruma::events::room::{message::RoomMessageEventContent, MediaSource}; + +#[uniffi::export] +pub fn media_source_from_url(url: String) -> Arc { + Arc::new(MediaSource::Plain(url.into())) +} + +#[uniffi::export] +pub fn message_event_content_from_markdown(md: String) -> Arc { + Arc::new(RoomMessageEventContent::text_markdown(md)) +} + +pub trait TimelineListener: Sync + Send { + fn on_update(&self, diff: Arc); +} + +#[repr(transparent)] +#[derive(Clone)] +pub struct TimelineDiff(VecDiff>); + +impl TimelineDiff { + pub(crate) fn new(inner: VecDiff>) -> Self { + TimelineDiff(match inner { + // Note: It's _probably_ valid to only transmute here too but not + // as clear, and less important because this only happens + // once when constructing the timeline. + VecDiff::Replace { values } => VecDiff::Replace { + values: values.into_iter().map(TimelineItem::from_arc).collect(), + }, + VecDiff::InsertAt { index, value } => { + VecDiff::InsertAt { index, value: TimelineItem::from_arc(value) } + } + VecDiff::UpdateAt { index, value } => { + VecDiff::UpdateAt { index, value: TimelineItem::from_arc(value) } + } + VecDiff::RemoveAt { index } => VecDiff::RemoveAt { index }, + VecDiff::Move { old_index, new_index } => VecDiff::Move { old_index, new_index }, + VecDiff::Push { value } => VecDiff::Push { value: TimelineItem::from_arc(value) }, + VecDiff::Pop {} => VecDiff::Pop {}, + VecDiff::Clear {} => VecDiff::Clear {}, + }) + } + + pub fn change(&self) -> TimelineChange { + match &self.0 { + VecDiff::Replace { .. } => TimelineChange::Replace, + VecDiff::InsertAt { .. } => TimelineChange::InsertAt, + VecDiff::UpdateAt { .. } => TimelineChange::UpdateAt, + VecDiff::RemoveAt { .. } => TimelineChange::RemoveAt, + VecDiff::Move { .. } => TimelineChange::Move, + VecDiff::Push { .. } => TimelineChange::Push, + VecDiff::Pop {} => TimelineChange::Pop, + VecDiff::Clear {} => TimelineChange::Clear, + } + } + + pub fn replace(self: Arc) -> Option>> { + unwrap_or_clone_arc_into_variant!(self, .0, VecDiff::Replace { values } => values) + } + + pub fn insert_at(self: Arc) -> Option { + unwrap_or_clone_arc_into_variant!(self, .0, VecDiff::InsertAt { index, value } => { + InsertAtData { index: index.try_into().unwrap(), item: value } + }) + } + + pub fn update_at(self: Arc) -> Option { + unwrap_or_clone_arc_into_variant!(self, .0, VecDiff::UpdateAt { index, value } => { + UpdateAtData { index: index.try_into().unwrap(), item: value } + }) + } + + pub fn remove_at(&self) -> Option { + match &self.0 { + VecDiff::RemoveAt { index } => Some((*index).try_into().unwrap()), + _ => None, + } + } + + pub fn r#move(&self) -> Option { + match &self.0 { + VecDiff::Move { old_index, new_index } => Some(MoveData { + old_index: (*old_index).try_into().unwrap(), + new_index: (*new_index).try_into().unwrap(), + }), + _ => None, + } + } + + pub fn push(self: Arc) -> Option> { + unwrap_or_clone_arc_into_variant!(self, .0, VecDiff::Push { value } => value) + } +} + +pub struct InsertAtData { + pub index: u32, + pub item: Arc, +} + +pub struct UpdateAtData { + pub index: u32, + pub item: Arc, +} + +pub struct MoveData { + pub old_index: u32, + pub new_index: u32, +} + +#[derive(Clone, Copy)] +pub enum TimelineChange { + Replace, + InsertAt, + UpdateAt, + RemoveAt, + Move, + Push, + Pop, + Clear, +} + +#[repr(transparent)] +#[derive(Clone)] +pub struct TimelineItem(matrix_sdk::room::timeline::TimelineItem); + +impl TimelineItem { + fn from_arc(arc: Arc) -> Arc { + // SAFETY: This is valid because Self is a repr(transparent) wrapper + // around the other Timeline type. + unsafe { Arc::from_raw(Arc::into_raw(arc) as _) } + } +} + +#[uniffi::export] +impl TimelineItem { + pub fn as_event(self: Arc) -> Option> { + use matrix_sdk::room::timeline::TimelineItem as Item; + unwrap_or_clone_arc_into_variant!(self, .0, Item::Event(evt) => { + Arc::new(EventTimelineItem(evt)) + }) + } + + pub fn as_virtual(self: Arc) -> Option> { + use matrix_sdk::room::timeline::TimelineItem as Item; + unwrap_or_clone_arc_into_variant!(self, .0, Item::Virtual(vt) => { + Arc::new(VirtualTimelineItem(vt)) + }) + } +} + +pub struct EventTimelineItem(pub(crate) matrix_sdk::room::timeline::EventTimelineItem); + +impl EventTimelineItem { + pub fn key(&self) -> TimelineKey { + self.0.key().into() + } + + pub fn reactions(&self) -> Vec { + self.0 + .reactions() + .iter() + .map(|(k, v)| Reaction { key: k.to_owned(), count: v.count.into() }) + .collect() + } +} + +#[uniffi::export] +impl EventTimelineItem { + pub fn event_id(&self) -> Option { + self.0.event_id().map(ToString::to_string) + } + + pub fn sender(&self) -> String { + self.0.sender().to_string() + } + + pub fn is_own(&self) -> bool { + self.0.is_own() + } + + pub fn content(&self) -> Arc { + Arc::new(TimelineItemContent(self.0.content().clone())) + } + + pub fn origin_server_ts(&self) -> Option { + self.0.origin_server_ts().map(|ts| ts.0.into()) + } + + pub fn raw(&self) -> Option { + self.0.raw().map(|r| r.json().get().to_owned()) + } +} + +#[derive(Clone, uniffi::Object)] +pub struct TimelineItemContent(matrix_sdk::room::timeline::TimelineItemContent); + +#[uniffi::export] +impl TimelineItemContent { + pub fn as_message(self: Arc) -> Option> { + use matrix_sdk::room::timeline::TimelineItemContent as C; + unwrap_or_clone_arc_into_variant!(self, .0, C::Message(msg) => Arc::new(Message(msg))) + } + + pub fn is_redacted_message(&self) -> bool { + use matrix_sdk::room::timeline::TimelineItemContent as C; + matches!(self.0, C::RedactedMessage) + } +} + +#[derive(Clone)] +pub struct Message(matrix_sdk::room::timeline::Message); + +impl Message { + pub fn msgtype(&self) -> Option { + use matrix_sdk::ruma::events::room::message::MessageType as MTy; + match self.0.msgtype() { + MTy::Emote(c) => Some(MessageType::Emote { + content: EmoteMessageContent { + body: c.body.clone(), + formatted: c.formatted.as_ref().map(Into::into), + }, + }), + MTy::Image(c) => Some(MessageType::Image { + content: ImageMessageContent { + body: c.body.clone(), + source: Arc::new(c.source.clone()), + info: c.info.as_deref().map(Into::into), + }, + }), + MTy::Notice(c) => Some(MessageType::Notice { + content: NoticeMessageContent { + body: c.body.clone(), + formatted: c.formatted.as_ref().map(Into::into), + }, + }), + MTy::Text(c) => Some(MessageType::Text { + content: TextMessageContent { + body: c.body.clone(), + formatted: c.formatted.as_ref().map(Into::into), + }, + }), + _ => None, + } + } +} + +#[uniffi::export] +impl Message { + pub fn body(&self) -> String { + self.0.msgtype().body().to_owned() + } + + // This event ID string will be replaced by something more useful later. + pub fn in_reply_to(&self) -> Option { + self.0.in_reply_to().map(ToString::to_string) + } + + pub fn is_edited(&self) -> bool { + self.0.is_edited() + } +} + +#[derive(Clone)] +pub enum MessageType { + Emote { content: EmoteMessageContent }, + Image { content: ImageMessageContent }, + Notice { content: NoticeMessageContent }, + Text { content: TextMessageContent }, +} + +#[derive(Clone)] +pub struct EmoteMessageContent { + pub body: String, + pub formatted: Option, +} + +#[derive(Clone)] +pub struct ImageMessageContent { + pub body: String, + pub source: Arc, + pub info: Option, +} + +#[derive(Clone)] +pub struct ImageInfo { + pub height: Option, + pub width: Option, + pub mimetype: Option, + pub size: Option, + pub thumbnail_info: Option, + pub thumbnail_source: Option>, + pub blurhash: Option, +} + +#[derive(Clone)] +pub struct ThumbnailInfo { + pub height: Option, + pub width: Option, + pub mimetype: Option, + pub size: Option, +} + +#[derive(Clone)] +pub struct NoticeMessageContent { + pub body: String, + pub formatted: Option, +} + +#[derive(Clone)] +pub struct TextMessageContent { + pub body: String, + pub formatted: Option, +} + +#[derive(Clone)] +pub struct FormattedBody { + pub format: MessageFormat, + pub body: String, +} + +impl From<&matrix_sdk::ruma::events::room::message::FormattedBody> for FormattedBody { + fn from(f: &matrix_sdk::ruma::events::room::message::FormattedBody) -> Self { + Self { + format: match &f.format { + matrix_sdk::ruma::events::room::message::MessageFormat::Html => MessageFormat::Html, + _ => MessageFormat::Unknown, + }, + body: f.body.clone(), + } + } +} + +#[derive(Clone, Copy)] +pub enum MessageFormat { + Html, + Unknown, +} + +impl From<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo { + fn from(info: &matrix_sdk::ruma::events::room::ImageInfo) -> Self { + let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo { + height: info.height.map(Into::into), + width: info.width.map(Into::into), + mimetype: info.mimetype.clone(), + size: info.size.map(Into::into), + }); + + Self { + height: info.height.map(Into::into), + width: info.width.map(Into::into), + mimetype: info.mimetype.clone(), + size: info.size.map(Into::into), + thumbnail_info, + thumbnail_source: info.thumbnail_source.clone().map(Arc::new), + blurhash: info.blurhash.clone(), + } + } +} + +#[derive(Clone)] +pub struct Reaction { + pub key: String, + pub count: u64, + // TODO: Also expose senders +} + +#[derive(Clone)] +pub struct ReactionDetails { + pub id: TimelineKey, + pub sender: String, +} + +#[derive(Clone)] +pub enum TimelineKey { + TransactionId { txn_id: String }, + EventId { event_id: String }, +} + +impl From<&matrix_sdk::room::timeline::TimelineKey> for TimelineKey { + fn from(timeline_key: &matrix_sdk::room::timeline::TimelineKey) -> Self { + use matrix_sdk::room::timeline::TimelineKey::*; + + match timeline_key { + TransactionId(txn_id) => TimelineKey::TransactionId { txn_id: txn_id.to_string() }, + EventId(event_id) => TimelineKey::EventId { event_id: event_id.to_string() }, + } + } +} + +#[derive(Clone)] +pub struct VirtualTimelineItem(matrix_sdk::room::timeline::VirtualTimelineItem); + +#[extension_trait] +pub impl MediaSourceExt for MediaSource { + fn url(&self) -> String { + match self { + MediaSource::Plain(url) => url.to_string(), + MediaSource::Encrypted(file) => file.url.to_string(), + } + } +} diff --git a/crates/matrix-sdk-appservice/Cargo.toml b/crates/matrix-sdk-appservice/Cargo.toml index 565f30f29..c4bd10106 100644 --- a/crates/matrix-sdk-appservice/Cargo.toml +++ b/crates/matrix-sdk-appservice/Cargo.toml @@ -30,20 +30,22 @@ sso-login = ["matrix-sdk/sso-login"] docs = [] [dependencies] +axum = { version = "0.5.16", default-features = false, features = ["json"] } dashmap = "5.2.0" http = "0.2.6" +http-body = "0.4.5" +hyper = { version = "0.14.20", features = ["http1", "http2", "server"] } matrix-sdk = { version = "0.6.0", path = "../matrix-sdk", default-features = false, features = ["appservice"] } -percent-encoding = "2.1.0" regex = "1.5.5" -ruma = { version = "0.7.0", features = ["client-api-c", "appservice-api-s"] } +ruma = { workspace = true, features = ["appservice-api-s"] } serde = "1.0.136" serde_json = "1.0.79" serde_yaml = "0.9.4" tokio = { version = "1.17.0", default-features = false, features = ["rt-multi-thread"] } thiserror = "1.0.30" -tracing = "0.1.34" +tower = { version = "0.4.13", default-features = false } +tracing = { workspace = true } url = "2.2.2" -warp = { version = "0.3.2", default-features = false } [dev-dependencies] matrix-sdk-test = { version = "0.6.0", path = "../../testing/matrix-sdk-test", features = ["appservice"] } diff --git a/crates/matrix-sdk-appservice/src/error.rs b/crates/matrix-sdk-appservice/src/error.rs index 68a863ed5..69a825c2a 100644 --- a/crates/matrix-sdk-appservice/src/error.rs +++ b/crates/matrix-sdk-appservice/src/error.rs @@ -77,8 +77,8 @@ pub enum Error { #[error("utf8 error: {0}")] Utf8(#[from] std::str::Utf8Error), - #[error("warp rejection: {0}")] - WarpRejection(String), + #[error("hyper error: {0}")] + Hyper(#[from] hyper::Error), } impl Error { @@ -101,14 +101,6 @@ impl Error { } } -impl warp::reject::Reject for Error {} - -impl From for Error { - fn from(rejection: warp::Rejection) -> Self { - Self::WarpRejection(format!("{:?}", rejection)) - } -} - impl From for Error { fn from(e: matrix_sdk::HttpError) -> Self { matrix_sdk::Error::from(e).into() diff --git a/crates/matrix-sdk-appservice/src/lib.rs b/crates/matrix-sdk-appservice/src/lib.rs index 36df96cc5..cde90fb70 100644 --- a/crates/matrix-sdk-appservice/src/lib.rs +++ b/crates/matrix-sdk-appservice/src/lib.rs @@ -79,8 +79,9 @@ //! [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::sync::Arc; +use std::{fmt::Debug, sync::Arc}; +use axum::body::HttpBody; use dashmap::DashMap; pub use error::Error; use event_handler::AppserviceFn; @@ -114,6 +115,7 @@ mod webserver; pub use registration::AppServiceRegistration; use registration::NamespaceCache; pub use virtual_user::VirtualUserBuilder; +pub use webserver::AppServiceRouter; pub type Result = std::result::Result; @@ -403,30 +405,21 @@ impl AppService { } /// Check if given `user_id` is in any of the [`AppServiceRegistration`]'s - /// `users` namespaces + /// `users` namespaces. pub fn user_id_is_in_namespace(&self, user_id: impl AsRef) -> bool { - for regex in &self.namespaces.users { - if regex.is_match(user_id.as_ref()) { - return true; - } - } - - false + let user_id = user_id.as_ref(); + self.namespaces.users.iter().any(|regex| regex.is_match(user_id)) } - /// 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()) + /// Returns a [`Service`][tower::Service] that processes appservice + /// requests. + pub fn service(&self) -> AppServiceRouter + where + B: HttpBody + Send + 'static, + B::Data: Send, + B::Error: Into, + { + webserver::router(self.clone()) } /// Receive an incoming [transaction], pushing the contained events to @@ -572,6 +565,8 @@ mod tests { sync::{Arc, Mutex}, }; + use http::{Method, Request}; + use hyper::Body; use matrix_sdk::{ config::RequestConfig, ruma::{api::appservice::Registration, events::room::member::OriginalSyncRoomMemberEvent}, @@ -585,7 +580,7 @@ mod tests { serde::Raw, }; use serde_json::json; - use warp::{Filter, Reply}; + use tower::{Service, ServiceExt}; use wiremock::{ matchers::{body_json, header, method, path}, Mock, MockServer, ResponseTemplate, @@ -656,21 +651,22 @@ mod tests { let mut transaction_builder = TransactionBuilder::new(); transaction_builder.add_timeline_event(TimelineTestEvent::Member); - let transaction = transaction_builder.build_json_transaction(); + let transaction = transaction_builder.build_transaction(); - let appservice = appservice(None, None).await?; - - let status = warp::test::request() - .method("PUT") - .path(uri) - .json(&transaction) - .filter(&appservice.warp_filter()) + let response = appservice(None, None) + .await? + .service() + .oneshot( + Request::builder() + .method(Method::PUT) + .uri(uri) + .body(Body::from(transaction)) + .unwrap(), + ) .await - .unwrap() - .into_response() - .status(); + .unwrap(); - assert_eq!(status, 200); + assert_eq!(response.status(), 200); Ok(()) } @@ -681,7 +677,7 @@ mod tests { let mut transaction_builder = TransactionBuilder::new(); transaction_builder.add_timeline_event(TimelineTestEvent::Member); - let transaction = transaction_builder.build_json_transaction(); + let transaction = transaction_builder.build_transaction(); let appservice = appservice(None, None).await?; @@ -695,17 +691,20 @@ mod tests { } }); - let status = warp::test::request() - .method("PUT") - .path(uri) - .json(&transaction) - .filter(&appservice.warp_filter()) - .await - .unwrap() - .into_response() - .status(); + let mut service = appservice.service(); - assert_eq!(status, 200); + let response = service + .call( + Request::builder() + .method(Method::PUT) + .uri(uri) + .body(Body::from(transaction.clone())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), 200); { let on_room_member_called = *on_state_member.lock().unwrap(); assert!(on_room_member_called); @@ -717,19 +716,20 @@ mod tests { *on_room_member_called = false; } - let status = warp::test::request() - .method("PUT") - .path(uri) - .json(&transaction) - .filter(&appservice.warp_filter()) + let response = service + .call( + Request::builder() + .method(Method::PUT) + .uri(uri) + .body(Body::from(transaction)) + .unwrap(), + ) .await - .unwrap() - .into_response() - .status(); + .unwrap(); // According to https://spec.matrix.org/v1.2/application-service-api/#pushing-events // This should noop and return 200. - assert_eq!(status, 200); + assert_eq!(response.status(), 200); { let on_room_member_called = *on_state_member.lock().unwrap(); // This time we should not have called the event handler. @@ -746,16 +746,13 @@ mod tests { 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()) + let response = appservice + .service() + .oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap()) .await - .unwrap() - .into_response() - .status(); + .unwrap(); - assert_eq!(status, 200); + assert_eq!(response.status(), 200); Ok(()) } @@ -767,16 +764,13 @@ mod tests { 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()) + let response = appservice + .service() + .oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap()) .await - .unwrap() - .into_response() - .status(); + .unwrap(); - assert_eq!(status, 200); + assert_eq!(response.status(), 200); Ok(()) } @@ -786,23 +780,24 @@ mod tests { let uri = "/_matrix/app/v1/transactions/1?access_token=invalid_token"; let mut transaction_builder = TransactionBuilder::new(); - let transaction = transaction_builder - .add_timeline_event(TimelineTestEvent::Member) - .build_json_transaction(); + let transaction = + transaction_builder.add_timeline_event(TimelineTestEvent::Member).build_transaction(); let appservice = appservice(None, None).await?; - let status = warp::test::request() - .method("PUT") - .path(uri) - .json(&transaction) - .filter(&appservice.warp_filter()) + let response = appservice + .service() + .oneshot( + Request::builder() + .method(Method::PUT) + .uri(uri) + .body(Body::from(transaction)) + .unwrap(), + ) .await - .unwrap() - .into_response() - .status(); + .unwrap(); - assert_eq!(status, 401); + assert_eq!(response.status(), 401); Ok(()) } @@ -813,23 +808,23 @@ mod tests { let mut transaction_builder = TransactionBuilder::new(); transaction_builder.add_timeline_event(TimelineTestEvent::Member); - let transaction = transaction_builder.build_json_transaction(); + let transaction = transaction_builder.build_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(); + let response = appservice + .service() + .oneshot( + Request::builder() + .method(Method::PUT) + .uri(uri) + .body(Body::from(transaction)) + .unwrap(), + ) + .await + .unwrap(); - assert_eq!(status, 401); - } + assert_eq!(response.status(), 401); Ok(()) } @@ -852,13 +847,17 @@ mod tests { let mut transaction_builder = TransactionBuilder::new(); transaction_builder.add_timeline_event(TimelineTestEvent::Member); - let transaction = transaction_builder.build_json_transaction(); + let transaction = transaction_builder.build_transaction(); - warp::test::request() - .method("PUT") - .path(uri) - .json(&transaction) - .filter(&appservice.warp_filter()) + appservice + .service() + .oneshot( + Request::builder() + .method(Method::PUT) + .uri(uri) + .body(Body::from(transaction)) + .unwrap(), + ) .await .unwrap(); @@ -868,30 +867,6 @@ mod tests { 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"); @@ -900,29 +875,33 @@ mod tests { let mut transaction_builder = TransactionBuilder::new(); transaction_builder.add_timeline_event(TimelineTestEvent::Member); - let transaction_1 = transaction_builder.build_json_transaction(); + let transaction_1 = transaction_builder.build_transaction(); let mut transaction_builder = TransactionBuilder::new(); transaction_builder.add_timeline_event(TimelineTestEvent::MemberNameChange); - let transaction_2 = transaction_builder.build_json_transaction(); + let transaction_2 = transaction_builder.build_transaction(); let appservice = appservice(None, None).await?; + let mut service = axum::Router::new().nest("/sub_path", appservice.service()); - { - 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?; - }; + service + .call( + Request::builder() + .method(Method::PUT) + .uri(uri_1) + .body(Body::from(transaction_1))?, + ) + .await + .unwrap(); + service + .call( + Request::builder() + .method(Method::PUT) + .uri(uri_2) + .body(Body::from(transaction_2))?, + ) + .await + .unwrap(); let members = appservice .virtual_user(None) @@ -1037,7 +1016,9 @@ mod tests { } mod registration { - use super::*; + use ruma::api::appservice::Registration; + + use crate::{tests::registration_string, AppServiceRegistration, Result}; #[test] fn test_registration() -> Result<()> { diff --git a/crates/matrix-sdk-appservice/src/webserver.rs b/crates/matrix-sdk-appservice/src/webserver.rs index 49fa38894..a021745fd 100644 --- a/crates/matrix-sdk-appservice/src/webserver.rs +++ b/crates/matrix-sdk-appservice/src/webserver.rs @@ -12,22 +12,28 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::net::ToSocketAddrs; - -use matrix_sdk::{ - bytes::Bytes, - ruma::{ - self, - api::{ - appservice::query::{ - query_room_alias::v1 as query_room, query_user_id::v1 as query_user, - }, - IncomingRequest, - }, - }, +use std::{ + convert::Infallible, + future::Future, + net::ToSocketAddrs, + pin::Pin, + task::{self, Poll}, }; -use serde::Serialize; -use warp::{filters::BoxedFilter, path::FullPath, Filter, Rejection, Reply}; + +use axum::{ + async_trait, + body::{Bytes, HttpBody}, + extract::{FromRequest, Path, RequestParts}, + middleware::{self, Next}, + response::{ErrorResponse, IntoResponse, Response}, + routing::{future::RouteFuture, get, put}, + BoxError, Extension, Json, Router, +}; +use http::StatusCode; +use hyper::Body; +use matrix_sdk::ruma::{self, api::IncomingRequest}; +use serde::{Deserialize, Serialize}; +use tower::{make, Service, ServiceBuilder}; use crate::{AppService, Error, Result}; @@ -36,195 +42,179 @@ pub async fn run_server( host: impl Into, port: impl Into, ) -> Result<()> { - let routes = warp_filter(appservice); + let router: AppServiceRouter = router(appservice); - let mut addr = format!("{}:{}", host.into(), port.into()).to_socket_addrs()?; + let mut addr = (host.into(), port.into()).to_socket_addrs()?; if let Some(addr) = addr.next() { - warp::serve(routes).run(addr).await; + hyper::Server::bind(&addr).serve(make::Shared::new(router)).await?; Ok(()) } else { Err(Error::HostPortToSocketAddrs) } } -pub fn warp_filter(appservice: AppService) -> BoxedFilter<(impl Reply,)> { - // TODO: try to use a struct instead of needlessly cloning appservice multiple - // times on every request - warp::any() - .and(filters::transactions(appservice.clone())) - .or(filters::users(appservice.clone())) - .or(filters::rooms(appservice)) - .recover(handle_rejection) - .boxed() +pub fn router(appservice: AppService) -> AppServiceRouter +where + B: HttpBody + Send + 'static, + B::Data: Send, + B::Error: Into, +{ + AppServiceRouter( + Router::new() + .route("/_matrix/app/v1/users/:user_id", get(handlers::user)) + .route("/_matrix/app/v1/rooms/:room_id", get(handlers::room)) + .route("/_matrix/app/v1/transactions/:txn_id", put(handlers::transaction)) + .route("/users/:user_id", get(handlers::user)) + .route("/rooms/:room_id", get(handlers::room)) + .route("/transactions/:txn_id", put(handlers::transaction)) + // FIXME: Use Route::with_state instead of an Extension layer in axum 0.6 + .layer( + ServiceBuilder::new() + .layer(Extension(appservice)) + .layer(middleware::from_fn(validate_access_token)), + ), + ) } -mod filters { - use super::*; +#[derive(Debug)] +pub struct AppServiceRouter(Router); - pub fn users(appservice: AppService) -> BoxedFilter<(impl Reply,)> { - warp::get() - .and( - warp::path!("_matrix" / "app" / "v1" / "users" / String) - // legacy route - .or(warp::path!("users" / String)) - .unify(), - ) - .and(warp::path::end()) - .and(common(appservice)) - .and_then(handlers::user) - .boxed() +impl Clone for AppServiceRouter { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl Service> for AppServiceRouter +where + B: HttpBody + Send + 'static, + B::Data: Send, + B::Error: Into, +{ + // axum's Response type is part of the signature because axum::Router::nest + // requires the inner service to have that exact response (body) type in + // 0.5.x; this will be fixed in axum 0.6.0. + type Response = Response; + type Error = Infallible; + type Future = AppServiceRouteFuture; + + fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> Poll> { + self.0.poll_ready(cx) } - pub fn rooms(appservice: AppService) -> BoxedFilter<(impl Reply,)> { - warp::get() - .and( - warp::path!("_matrix" / "app" / "v1" / "rooms" / String) - // legacy route - .or(warp::path!("rooms" / String)) - .unify(), - ) - .and(warp::path::end()) - .and(common(appservice)) - .and_then(handlers::room) - .boxed() + fn call(&mut self, req: http::Request) -> Self::Future { + AppServiceRouteFuture(self.0.call(req)) } +} - pub fn transactions(appservice: AppService) -> BoxedFilter<(impl Reply,)> { - warp::put() - .and( - warp::path!("_matrix" / "app" / "v1" / "transactions" / String) - // legacy route - .or(warp::path!("transactions" / String)) - .unify(), - ) - .and(warp::path::end()) - .and(common(appservice)) - .and_then(handlers::transaction) - .boxed() +pub struct AppServiceRouteFuture(RouteFuture); + +impl Future for AppServiceRouteFuture +where + B: HttpBody, +{ + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll { + Pin::new(&mut self.0).poll(cx) } +} - fn common(appservice: AppService) -> BoxedFilter<(AppService, http::Request)> { - warp::any() - .and(valid_access_token(appservice.registration().hs_token.clone())) - .map(move || appservice.clone()) - .and( - http_request().and_then(|request| async move { - Ok::, Rejection>(request) - }), - ) - .boxed() - } +pub struct MatrixRequest(T); - pub fn valid_access_token(token: String) -> BoxedFilter<()> { - warp::any() - .map(move || token.clone()) - .and(warp::query::raw()) - .and_then(|token: String, query: String| async move { - let query: Vec<(String, String)> = - ruma::serde::urlencoded::from_str(&query).map_err(Error::from)?; +#[async_trait] +impl FromRequest for MatrixRequest +where + T: IncomingRequest, + B: HttpBody + Send, + B::Data: Send, + B::Error: Into, +{ + type Rejection = Response; - if query.into_iter().any(|(key, value)| key == "access_token" && value == token) { - Ok::<(), Rejection>(()) - } else { - Err(warp::reject::custom(Unauthorized)) - } - }) - .untuple_one() - .boxed() - } + async fn from_request(req: &mut RequestParts) -> Result { + let path_params = + req.extract::>>().await.map_err(IntoResponse::into_response)?; + let parts = req.extract::().await.map_err(|e| match e {})?; + let body = req.extract::().await.map_err(IntoResponse::into_response)?; - pub fn http_request() -> impl Filter,), Error = Rejection> + Copy - { - // TODO: extract `hyper::Request` instead - // blocked by https://github.com/seanmonstar/warp/issues/139 - warp::any() - .and(warp::method()) - .and(warp::filters::path::full()) - .and(warp::filters::query::raw()) - .and(warp::header::headers_cloned()) - .and(warp::body::bytes()) - .and_then(|method, path: FullPath, query, headers, bytes| async move { - let uri = http::uri::Builder::new() - .path_and_query(format!("{}?{query}", path.as_str())) - .build() - .map_err(Error::from)?; + let http_request = http::Request::from_parts(parts, body); - let mut request = http::Request::builder() - .method(method) - .uri(uri) - .body(bytes) - .map_err(Error::from)?; + let request = T::try_from_http_request(http_request, &path_params).map_err(|_e| { + // TODO: JSON error response + StatusCode::BAD_REQUEST.into_response() + })?; - *request.headers_mut() = headers; - - Ok::, Rejection>(request) - }) + Ok(Self(request)) } } mod handlers { - use percent_encoding::percent_decode_str; + use axum::{response::IntoResponse, Extension, Json}; + use http::StatusCode; + use ruma::api::appservice::{ + event::push_events, + query::{query_room_alias, query_user_id}, + }; use serde::Serialize; - use super::*; + use super::{ErrorMessage, MatrixRequest}; + use crate::AppService; #[derive(Serialize)] struct EmptyObject {} pub async fn user( - user_id: String, - appservice: AppService, - request: http::Request, - ) -> Result { + Extension(appservice): Extension, + MatrixRequest(request): MatrixRequest, + ) -> impl IntoResponse { if let Some(user_exists) = appservice.event_handler.users.lock().await.as_mut() { - let user_id = percent_decode_str(&user_id).decode_utf8().map_err(Error::from)?; - let request = query_user::IncomingRequest::try_from_http_request(request, &[user_id]) - .map_err(Error::from)?; - return if user_exists(appservice.clone(), request).await { - Ok(warp::reply::json(&EmptyObject {})) + if user_exists(appservice.clone(), request).await { + Ok(Json(EmptyObject {})) } else { - Err(warp::reject::not_found()) - }; + Err(StatusCode::NOT_FOUND) + } + } else { + Ok(Json(EmptyObject {})) } - Ok(warp::reply::json(&EmptyObject {})) } pub async fn room( - room_id: String, - appservice: AppService, - request: http::Request, - ) -> Result { + Extension(appservice): Extension, + MatrixRequest(request): MatrixRequest, + ) -> impl IntoResponse { if let Some(room_exists) = appservice.event_handler.rooms.lock().await.as_mut() { - let room_id = percent_decode_str(&room_id).decode_utf8().map_err(Error::from)?; - let request = query_room::IncomingRequest::try_from_http_request(request, &[room_id]) - .map_err(Error::from)?; - return if room_exists(appservice.clone(), request).await { - Ok(warp::reply::json(&EmptyObject {})) + if room_exists(appservice.clone(), request).await { + Ok(Json(&EmptyObject {})) } else { - Err(warp::reject::not_found()) - }; + Err(StatusCode::NOT_FOUND) + } + } else { + Ok(Json(&EmptyObject {})) } - Ok(warp::reply::json(&EmptyObject {})) } pub async fn transaction( - txn_id: String, - appservice: AppService, - request: http::Request, - ) -> Result { - let incoming_transaction: ruma::api::appservice::event::push_events::v1::IncomingRequest = - ruma::api::IncomingRequest::try_from_http_request(request, &[txn_id]) - .map_err(Error::from)?; - - appservice.receive_transaction(incoming_transaction).await?; - Ok(warp::reply::json(&EmptyObject {})) + appservice: Extension, + MatrixRequest(request): MatrixRequest, + ) -> impl IntoResponse { + match appservice.receive_transaction(request).await { + Ok(_) => Ok(Json(&EmptyObject {})), + Err(e) => { + let status_code = StatusCode::INTERNAL_SERVER_ERROR; + Err(( + status_code, + Json(ErrorMessage { code: status_code.as_u16(), message: e.to_string() }), + )) + } + } } } -#[derive(Debug)] -struct Unauthorized; - -impl warp::reject::Reject for Unauthorized {} +#[derive(Deserialize)] +struct QueryParameters { + access_token: String, +} #[derive(Serialize)] struct ErrorMessage { @@ -232,15 +222,23 @@ struct ErrorMessage { message: String, } -pub async fn handle_rejection(err: Rejection) -> Result { - if err.find::().is_some() || err.find::().is_some() { - let code = http::StatusCode::UNAUTHORIZED; - let message = "UNAUTHORIZED"; +async fn validate_access_token( + req: http::Request, + next: Next, +) -> Result { + let appservice = + req.extensions().get::().ok_or(StatusCode::INTERNAL_SERVER_ERROR)?; - let json = - warp::reply::json(&ErrorMessage { code: code.as_u16(), message: message.into() }); - Ok(warp::reply::with_status(json, code)) - } else { - Err(err) + let query_string = req.uri().query().unwrap_or(""); + match ruma::serde::urlencoded::from_str::(query_string) { + Ok(query) if query.access_token == appservice.registration.hs_token => { + Ok(next.run(req).await) + } + _ => { + let status_code = StatusCode::UNAUTHORIZED; + let message = + ErrorMessage { code: status_code.as_u16(), message: "UNAUTHORIZED".into() }; + Err((status_code, Json(message)).into()) + } } } diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index 9b1f101db..d7bc4acef 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -18,7 +18,7 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = [] e2e-encryption = ["dep:matrix-sdk-crypto"] -js = ["matrix-sdk-common/js", "matrix-sdk-crypto?/js", "ruma/js"] +js = ["matrix-sdk-common/js", "matrix-sdk-crypto?/js", "ruma/js", "matrix-sdk-store-encryption/js"] qrcode = ["matrix-sdk-crypto?/qrcode"] sliding-sync = ["ruma/unstable-msc3575"] @@ -37,21 +37,22 @@ http = { version = "0.2.6", optional = true } lru = "0.8.0" matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" } matrix-sdk-crypto = { version = "0.6.0", path = "../matrix-sdk-crypto", optional = true } +matrix-sdk-store-encryption = { version = "0.2.0", path = "../matrix-sdk-store-encryption" } once_cell = "1.10.0" -ruma = { version = "0.7.0", features = ["client-api-c", "canonical-json"] } +ruma = { workspace = true, features = ["canonical-json"] } serde = { version = "1.0.136", features = ["rc"] } serde_json = "1.0.79" thiserror = "1.0.30" -tracing = "0.1.34" -zeroize = { version = "1.3.0", features = ["zeroize_derive"] } +tracing = { workspace = true } +zeroize = { workspace = true, features = ["zeroize_derive"] } [dev-dependencies] -futures = { version = "0.3.21", default-features = false, features = ["executor"] } -tracing = { version = "0.1.26", features = ["log"] } -http = "0.2.6" assign = "1.1.1" -env_logger = "0.9.0" +ctor = "0.1.23" +futures = { version = "0.3.21", default-features = false, features = ["executor"] } +http = "0.2.6" matrix-sdk-test = { version = "0.6.0", path = "../../testing/matrix-sdk-test" } +tracing-subscriber = { version = "0.3.11", features = ["env-filter"] } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tokio = { version = "1.17.0", default-features = false, features = ["rt-multi-thread", "macros"] } diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 724e3f5c2..0daff8a96 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -94,9 +94,8 @@ impl fmt::Debug for BaseClient { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Client") .field("session_meta", &self.store.session_meta()) - .field("session_tokens", &self.store.session_tokens) .field("sync_token", &self.store.sync_token) - .finish() + .finish_non_exhaustive() } } @@ -253,10 +252,12 @@ impl BaseClient { } #[allow(clippy::too_many_arguments)] - async fn handle_timeline( + pub(crate) async fn handle_timeline( &self, room: &Room, - ruma_timeline: api::sync::sync_events::v3::Timeline, + limited: bool, + events: Vec>, + prev_batch: Option, push_rules: &Ruleset, user_ids: &mut BTreeSet, room_info: &mut RoomInfo, @@ -265,10 +266,10 @@ impl BaseClient { ) -> Result { let room_id = room.room_id(); let user_id = room.own_user_id(); - let mut timeline = Timeline::new(ruma_timeline.limited, ruma_timeline.prev_batch.clone()); + let mut timeline = Timeline::new(limited, prev_batch); let mut push_context = self.get_push_room_context(room, room_info, changes).await?; - for event in ruma_timeline.events { + for event in events { #[allow(unused_mut)] let mut event: SyncTimelineEvent = event.into(); @@ -497,7 +498,7 @@ impl BaseClient { Ok(user_ids) } - async fn handle_room_account_data( + pub(crate) async fn handle_room_account_data( &self, room_id: &RoomId, events: &[Raw], @@ -510,7 +511,7 @@ impl BaseClient { } } - async fn handle_account_data( + pub(crate) async fn handle_account_data( &self, events: &[Raw], changes: &mut StateChanges, @@ -553,6 +554,31 @@ impl BaseClient { changes.account_data = account_data; } + #[cfg(feature = "e2e-encryption")] + pub(crate) async fn preprocess_to_device_events( + &self, + to_device_events: Vec>, + changed_devices: &api::sync::sync_events::DeviceLists, + one_time_keys_counts: &BTreeMap, + unused_fallback_keys: Option<&[ruma::DeviceKeyAlgorithm]>, + ) -> Result>> { + if let Some(o) = self.olm_machine() { + // Let the crypto machine handle the sync response, this + // decrypts to-device events, but leaves room events alone. + // This makes sure that we have the decryption keys for the room + // events at hand. + Ok(o.receive_sync_changes( + to_device_events, + changed_devices, + one_time_keys_counts, + unused_fallback_keys, + ) + .await?) + } else { + Ok(to_device_events) + } + } + /// Receive a response from a sync call. /// /// # Arguments @@ -583,25 +609,17 @@ impl BaseClient { } let now = Instant::now(); + let to_device_events = to_device.events; #[cfg(feature = "e2e-encryption")] - let to_device = { - if let Some(o) = self.olm_machine() { - // Let the crypto machine handle the sync response, this - // decrypts to-device events, but leaves room events alone. - // This makes sure that we have the decryption keys for the room - // events at hand. - o.receive_sync_changes( - to_device, - &device_lists, - &device_one_time_keys_count, - device_unused_fallback_key_types.as_deref(), - ) - .await? - } else { - to_device - } - }; + let to_device_events = self + .preprocess_to_device_events( + to_device_events, + &device_lists, + &device_one_time_keys_count, + device_unused_fallback_key_types.as_deref(), + ) + .await?; let mut changes = StateChanges::new(next_batch.clone()); let mut ambiguity_cache = AmbiguityCache::new(self.store.inner.clone()); @@ -645,7 +663,9 @@ impl BaseClient { let timeline = self .handle_timeline( &room, - new_info.timeline, + new_info.timeline.limited, + new_info.timeline.events, + new_info.timeline.prev_batch, &push_rules, &mut user_ids, &mut room_info, @@ -684,7 +704,7 @@ impl BaseClient { JoinedRoom::new( timeline, new_info.state, - new_info.account_data, + new_info.account_data.events, new_info.ephemeral, notification_count, ), @@ -710,7 +730,9 @@ impl BaseClient { let timeline = self .handle_timeline( &room, - new_info.timeline, + new_info.timeline.limited, + new_info.timeline.events, + new_info.timeline.prev_batch, &push_rules, &mut user_ids, &mut room_info, @@ -772,8 +794,8 @@ impl BaseClient { next_batch, rooms: new_rooms, presence, - account_data, - to_device, + account_data: account_data.events, + to_device_events, device_lists, device_one_time_keys_count: device_one_time_keys_count .into_iter() diff --git a/crates/matrix-sdk-base/src/lib.rs b/crates/matrix-sdk-base/src/lib.rs index 24afa1ef6..3861745cd 100644 --- a/crates/matrix-sdk-base/src/lib.rs +++ b/crates/matrix-sdk-base/src/lib.rs @@ -45,3 +45,13 @@ pub use store::{StateChanges, StateStore, StoreError}; pub use utils::{ MinimalRoomMemberEvent, MinimalStateEvent, OriginalMinimalStateEvent, RedactedMinimalStateEvent, }; + +#[cfg(all(test, not(target_arch = "wasm32")))] +#[ctor::ctor] +fn init_logging() { + use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::from_default_env()) + .with(tracing_subscriber::fmt::layer().with_test_writer()) + .init(); +} diff --git a/crates/matrix-sdk-base/src/media.rs b/crates/matrix-sdk-base/src/media.rs index c2018e89a..32356e989 100644 --- a/crates/matrix-sdk-base/src/media.rs +++ b/crates/matrix-sdk-base/src/media.rs @@ -60,7 +60,7 @@ pub struct MediaThumbnailSize { impl UniqueKey for MediaThumbnailSize { fn unique_key(&self) -> String { - format!("{}{}{}x{}", self.method, UNIQUE_SEPARATOR, self.width, self.height) + format!("{}{UNIQUE_SEPARATOR}{}x{}", self.method, self.width, self.height) } } @@ -85,7 +85,7 @@ pub struct MediaRequest { impl UniqueKey for MediaRequest { fn unique_key(&self) -> String { - format!("{}{}{}", self.source.unique_key(), UNIQUE_SEPARATOR, self.format.unique_key()) + format!("{}{UNIQUE_SEPARATOR}{}", self.source.unique_key(), self.format.unique_key()) } } /// Trait for media event content. diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 6d3805dd0..21e0e35ac 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -748,7 +748,6 @@ mod test { #[async_test] async fn test_display_name_default() { - let _ = env_logger::try_init(); let (_, room) = make_room(RoomType::Joined); assert_eq!(room.display_name().await.unwrap(), DisplayName::Empty); @@ -786,7 +785,6 @@ mod test { #[async_test] async fn test_display_name_dm_invited() { - let _ = env_logger::try_init(); let (store, room) = make_room(RoomType::Invited); let room_id = room_id!("!test:localhost"); let matthew = user_id!("@matthew:example.org"); @@ -809,7 +807,6 @@ mod test { #[async_test] async fn test_display_name_dm_invited_no_heroes() { - let _ = env_logger::try_init(); let (store, room) = make_room(RoomType::Invited); let room_id = room_id!("!test:localhost"); let matthew = user_id!("@matthew:example.org"); @@ -828,7 +825,6 @@ mod test { #[async_test] async fn test_display_name_dm_joined() { - let _ = env_logger::try_init(); let (store, room) = make_room(RoomType::Joined); let room_id = room_id!("!test:localhost"); let matthew = user_id!("@matthew:example.org"); @@ -860,7 +856,6 @@ mod test { #[async_test] async fn test_display_name_dm_joined_no_heroes() { - let _ = env_logger::try_init(); let (store, room) = make_room(RoomType::Joined); let room_id = room_id!("!test:localhost"); let matthew = user_id!("@matthew:example.org"); @@ -887,7 +882,6 @@ mod test { #[async_test] async fn test_display_name_dm_alone() { - let _ = env_logger::try_init(); let (store, room) = make_room(RoomType::Joined); let room_id = room_id!("!test:localhost"); let matthew = user_id!("@matthew:example.org"); diff --git a/crates/matrix-sdk-base/src/session.rs b/crates/matrix-sdk-base/src/session.rs index de3b30539..35b58213b 100644 --- a/crates/matrix-sdk-base/src/session.rs +++ b/crates/matrix-sdk-base/src/session.rs @@ -15,6 +15,8 @@ //! User sessions. +use std::fmt; + use ruma::{api::client::session::refresh_token, OwnedDeviceId, OwnedUserId}; use serde::{Deserialize, Serialize}; @@ -36,7 +38,7 @@ use serde::{Deserialize, Serialize}; /// /// assert_eq!(session.device_id.as_str(), "MYDEVICEID"); /// ``` -#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] pub struct Session { /// The access token used for this session. pub access_token: String, @@ -66,6 +68,15 @@ impl Session { } } +impl fmt::Debug for Session { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Session") + .field("user_id", &self.user_id) + .field("device_id", &self.device_id) + .finish_non_exhaustive() + } +} + impl From for Session { fn from(response: ruma::api::client::session::login::v3::Response) -> Self { Self { @@ -88,7 +99,8 @@ pub struct SessionMeta { /// The mutable parts of the session: the access token and optional refresh /// token. -#[derive(Clone, Debug)] +#[derive(Clone)] +#[allow(missing_debug_implementations)] pub struct SessionTokens { /// The access token used for this session. pub access_token: String, diff --git a/crates/matrix-sdk-base/src/sliding_sync.rs b/crates/matrix-sdk-base/src/sliding_sync.rs index 92c062864..c18c0293a 100644 --- a/crates/matrix-sdk-base/src/sliding_sync.rs +++ b/crates/matrix-sdk-base/src/sliding_sync.rs @@ -30,60 +30,68 @@ impl BaseClient { // next_batch, rooms, lists, + extensions, // FIXME: missing compared to v3::Response //presence, - //account_data, - //to_device, - //device_lists, - //device_one_time_keys_count, - //device_unused_fallback_key_types, .. } = response; - // FIXME not yet supported by sliding sync. see - // https://github.com/matrix-org/matrix-rust-sdk/issues/1014 - // #[cfg(feature = "encryption")] - // let to_device = { - // if let Some(o) = self.olm_machine().await { - // // Let the crypto machine handle the sync response, this - // // decrypts to-device events, but leaves room events alone. - // // This makes sure that we have the decryption keys for the room - // // events at hand. - // o.receive_sync_changes( - // to_device, - // &device_lists, - // &device_one_time_keys_count, - // device_unused_fallback_key_types.as_deref(), - // ) - // .await? - // } else { - // to_device - // } - // }; - - if rooms.is_empty() { - // nothing for us to handle here + if rooms.is_empty() && extensions.is_empty() { + // we received a room reshuffling event only, there won't be anything for us to + // process. stop early return Ok(SyncResponse::default()); }; + let v4::Extensions { to_device, e2ee, account_data, .. } = extensions; + + let to_device_events = to_device.map(|v4| v4.events).unwrap_or_default(); + + #[cfg(feature = "e2e-encryption")] + let to_device_events = { + if let Some(e2ee) = &e2ee { + self.preprocess_to_device_events( + to_device_events, + &e2ee.device_lists, + &e2ee.device_one_time_keys_count, + e2ee.device_unused_fallback_key_types.as_deref(), + ) + .await? + } else { + to_device_events + } + }; + + let (device_lists, device_one_time_keys_count) = e2ee + .map(|e2ee| { + ( + e2ee.device_lists, + e2ee.device_one_time_keys_count + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(), + ) + }) + .unwrap_or_default(); + let store = self.store.clone(); let mut changes = StateChanges::default(); let mut ambiguity_cache = AmbiguityCache::new(store.inner.clone()); - // FIXME not yet supported by sliding sync. - // self.handle_account_data(&account_data.events, &mut changes).await; + if let Some(global_data) = account_data.as_ref().map(|a| &a.global) { + self.handle_account_data(global_data, &mut changes).await; + } - let _push_rules = self.get_push_rules(&changes).await?; + let push_rules = self.get_push_rules(&changes).await?; let mut new_rooms = Rooms::default(); - for (room_id, room_data) in &rooms { + for (room_id, room_data) in rooms.into_iter() { if !room_data.invite_state.is_empty() { let invite_states = &room_data.invite_state; - let room = store.get_or_create_stripped_room(room_id).await; + let room = store.get_or_create_stripped_room(&room_id).await; let mut room_info = room.clone_info(); - if let Some(r) = store.get_room(room_id) { + if let Some(r) = store.get_room(&room_id) { let mut room_info = r.clone_info(); room_info.mark_as_invited(); // FIXME: this might not be accurate changes.add_room(room_info); @@ -96,7 +104,7 @@ impl BaseClient { v3::InvitedRoom::from(v3::InviteState::from(invite_states.clone())), ); } else { - let room = store.get_or_create_room(room_id, RoomType::Joined).await; + let room = store.get_or_create_room(&room_id, RoomType::Joined).await; let mut room_info = room.clone_info(); room_info.mark_as_joined(); // FIXME: this might not be accurate @@ -105,18 +113,16 @@ impl BaseClient { room_info.set_prev_batch(room_data.prev_batch.as_deref()); - let user_ids = if room_data.required_state.is_empty() { - None - } else { - Some( - self.handle_state( - &room_data.required_state, - &mut room_info, - &mut changes, - &mut ambiguity_cache, - ) - .await?, + let mut user_ids = if !room_data.required_state.is_empty() { + self.handle_state( + &room_data.required_state, + &mut room_info, + &mut changes, + &mut ambiguity_cache, ) + .await? + } else { + Default::default() }; // FIXME not yet supported by sliding sync. see @@ -130,36 +136,34 @@ impl BaseClient { // changes.add_receipts(&room_id, event); // } - // FIXME not yet supported by sliding sync. - // self.handle_room_account_data(&room_id, &room_data.account_data.events, &mut - // changes) .await; + let room_account_data = if let Some(inner_account_data) = &account_data { + if let Some(events) = inner_account_data.rooms.get(&room_id) { + self.handle_room_account_data(&room_id, events, &mut changes).await; + Some(events.to_vec()) + } else { + None + } + } else { + None + }; - // FIXME not yet supported by sliding sync. - // if room_data.timeline.limited { - // room_info.mark_members_missing(); - // } + if room_data.limited { + room_info.mark_members_missing(); + } - // let timeline = self - // .handle_timeline( - // &room, - // room_data.timeline, - // &push_rules, - // &mut room_info, - // &mut changes, - // &mut ambiguity_cache, - // &mut user_ids, - // ) - // .await?; - - // let timeline_slice = TimelineSlice::new( - // timeline.events.clone(), - // next_batch.clone(), - // timeline.prev_batch.clone(), - // timeline.limited, - // true, - // ); - - // changes.add_timeline(&room_id, timeline_slice); + let timeline = self + .handle_timeline( + &room, + room_data.limited, + room_data.timeline, + room_data.prev_batch, + &push_rules, + &mut user_ids, + &mut room_info, + &mut changes, + &mut ambiguity_cache, + ) + .await?; #[cfg(feature = "e2e-encryption")] if room_info.is_encrypted() { @@ -168,15 +172,15 @@ impl BaseClient { // The room turned on encryption in this sync, we need // to also get all the existing users and mark them for // tracking. - let joined = store.get_joined_user_ids(room_id).await?; - let invited = store.get_invited_user_ids(room_id).await?; + let joined = store.get_joined_user_ids(&room_id).await?; + let invited = store.get_invited_user_ids(&room_id).await?; let user_ids: Vec<&UserId> = joined.iter().chain(&invited).map(Deref::deref).collect(); o.update_tracked_users(user_ids).await } - if let Some(user_ids) = user_ids { + if !user_ids.is_empty() { o.update_tracked_users(user_ids.iter().map(Deref::deref)).await; } } @@ -187,9 +191,9 @@ impl BaseClient { new_rooms.join.insert( room_id.clone(), JoinedRoom::new( - Default::default(), //timeline, + timeline, v3::State::with_events(room_data.required_state.clone()), - Default::default(), // room_info.account_data, + room_account_data.unwrap_or_default(), Default::default(), // room_info.ephemeral, notification_count, ), @@ -199,9 +203,13 @@ impl BaseClient { } } - // FIXME not yet supported by sliding sync. see - // https://github.com/matrix-org/matrix-rust-sdk/issues/1014 - // self.handle_account_data(&account_data.events, &mut changes).await; + // TODO remove this, we're processing account data events here again + // because we want to have the push rules in place before we process + // rooms and their events, but we want to create the rooms before we + // process the `m.direct` account data event. + if let Some(global_data) = account_data.as_ref().map(|a| &a.global) { + self.handle_account_data(global_data, &mut changes).await; + } // FIXME not yet supported by sliding sync. // changes.presence = presence @@ -228,10 +236,10 @@ impl BaseClient { notifications: changes.notifications, // FIXME not yet supported by sliding sync. presence: Default::default(), - account_data: Default::default(), - to_device: Default::default(), - device_lists: Default::default(), - device_one_time_keys_count: Default::default(), + account_data: account_data.map(|a| a.global).unwrap_or_default(), + to_device_events, + device_lists, + device_one_time_keys_count, }) } } diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index bcc5e67c6..3eee556ea 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -23,9 +23,11 @@ use std::{ borrow::Borrow, collections::{BTreeMap, BTreeSet}, + fmt, ops::Deref, pin::Pin, result::Result as StdResult, + str::Utf8Error, sync::Arc, }; @@ -41,6 +43,7 @@ use dashmap::DashMap; use matrix_sdk_common::{locks::RwLock, AsyncTraitDeps}; #[cfg(feature = "e2e-encryption")] use matrix_sdk_crypto::store::{CryptoStore, IntoCryptoStore}; +pub use matrix_sdk_store_encryption::Error as StoreEncryptionError; use ruma::{ api::client::push::get_notifications::v3::Notification, events::{ @@ -98,10 +101,11 @@ pub enum StoreError { UnencryptedStore, /// The store failed to encrypt or decrypt some data. #[error("Error encrypting or decrypting data from the store: {0}")] - Encryption(String), + Encryption(#[from] StoreEncryptionError), + /// The store failed to encode or decode some data. #[error("Error encoding or decoding data from the store: {0}")] - Codec(String), + Codec(#[from] Utf8Error), /// The database format has changed in a backwards incompatible way. #[error( @@ -492,7 +496,7 @@ where /// /// This adds additional higher level store functionality on top of a /// `StateStore` implementation. -#[derive(Debug, Clone)] +#[derive(Clone)] pub(crate) struct Store { pub(super) inner: Arc, session_meta: Arc>, @@ -510,10 +514,8 @@ impl Store { Self::new(inner) } -} -impl Store { - /// Create a new store, wrappning the given `StateStore` + /// Create a new store, wrapping the given `StateStore` pub fn new(inner: Arc) -> Self { Self { inner, @@ -634,6 +636,18 @@ impl Store { } } +impl fmt::Debug for Store { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Store") + .field("inner", &self.inner) + .field("session_meta", &self.session_meta) + .field("sync_token", &self.sync_token) + .field("rooms", &self.rooms) + .field("stripped_rooms", &self.stripped_rooms) + .finish_non_exhaustive() + } +} + impl Deref for Store { type Target = dyn StateStore; diff --git a/crates/matrix-sdk-common/Cargo.toml b/crates/matrix-sdk-common/Cargo.toml index 95b710a84..678a2e110 100644 --- a/crates/matrix-sdk-common/Cargo.toml +++ b/crates/matrix-sdk-common/Cargo.toml @@ -21,7 +21,7 @@ js = ["instant/wasm-bindgen", "instant/inaccurate", "wasm-bindgen-futures"] [dependencies] futures-core = "0.3.21" instant = "0.1.12" -ruma = { version = "0.7.0", features = ["client-api-c"] } +ruma = { workspace = true } serde = "1.0.136" [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index a928d9132..e573b59dd 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -4,11 +4,8 @@ use ruma::{ api::client::{ push::get_notifications::v3::Notification, sync::sync_events::{ - v3::{ - DeviceLists, Ephemeral, GlobalAccountData, InvitedRoom, Presence, RoomAccountData, - State, ToDevice, - }, - UnreadNotificationsCount as RumaUnreadNotificationsCount, + v3::{Ephemeral, InvitedRoom, Presence, RoomAccountData, State}, + DeviceLists, UnreadNotificationsCount as RumaUnreadNotificationsCount, }, }, events::{ @@ -16,7 +13,8 @@ use ruma::{ MembershipState, RoomMemberEvent, RoomMemberEventContent, StrippedRoomMemberEvent, SyncRoomMemberEvent, }, - AnySyncTimelineEvent, AnyTimelineEvent, + AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnySyncTimelineEvent, AnyTimelineEvent, + AnyToDeviceEvent, }, serde::Raw, DeviceKeyAlgorithm, EventId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedEventId, @@ -79,7 +77,7 @@ pub struct EncryptionInfo { pub sender: OwnedUserId, /// The device ID of the device that sent us the event, note this is /// untrusted data unless `verification_state` is as well trusted. - pub sender_device: OwnedDeviceId, + pub sender_device: Option, /// Information about the algorithm that was used to encrypt the event. pub algorithm_info: AlgorithmInfo, /// The verification state of the device that sent us the event, note this @@ -133,9 +131,9 @@ pub struct SyncResponse { /// Updates to the presence status of other users. pub presence: Presence, /// The global private data created by this user. - pub account_data: GlobalAccountData, + pub account_data: Vec>, /// Messages sent directly between devices. - pub to_device: ToDevice, + pub to_device_events: Vec>, /// Information on E2E device updates. /// /// Only present on an incremental sync. @@ -187,7 +185,7 @@ pub struct JoinedRoom { /// true). pub state: State, /// The private data that this user has attached to this room. - pub account_data: RoomAccountData, + pub account_data: Vec>, /// The ephemeral events in the room that aren't recorded in the timeline or /// state of the room. e.g. typing. pub ephemeral: Ephemeral, @@ -197,7 +195,7 @@ impl JoinedRoom { pub fn new( timeline: Timeline, state: State, - account_data: RoomAccountData, + account_data: Vec>, ephemeral: Ephemeral, unread_notifications: UnreadNotificationsCount, ) -> Self { diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index ed4530c73..c4e37cc48 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -34,7 +34,9 @@ byteorder = "1.4.3" ctr = "0.9.1" dashmap = "5.2.0" event-listener = "2.5.2" +futures-core = "0.3.24" futures-util = { version = "0.3.21", default-features = false, features = ["alloc"] } +futures-signals = { version = "0.3.31", default-features = false } hmac = "0.12.1" http = { version = "0.2.6", optional = true } # feature = testing only matrix-sdk-qrcode = { version = "0.4.0", path = "../matrix-sdk-qrcode", optional = true } @@ -42,19 +44,21 @@ matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" } olm-rs = { version = "2.2.0", features = ["serde"], optional = true } pbkdf2 = { version = "0.11.0", default-features = false } rand = "0.8.5" -ruma = { version = "0.7.0", features = ["client-api-c", "rand", "canonical-json", "unstable-msc2676", "unstable-msc2677"] } +ruma = { workspace = true, features = ["rand", "canonical-json", "unstable-msc2676", "unstable-msc2677"] } serde = { version = "1.0.136", features = ["derive", "rc"] } serde_json = "1.0.79" sha2 = "0.10.2" thiserror = "1.0.30" -tracing = "0.1.34" -vodozemac = "0.3.0" -zeroize = { version = "1.3.0", features = ["zeroize_derive"] } +tracing = { workspace = true, features = ["attributes"] } +vodozemac = { workspace = true } +zeroize = { workspace = true, features = ["zeroize_derive"] } +cfg-if = "1.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1.18", default-features = false, features = ["time"] } [dev-dependencies] +anyhow = "1.0.65" futures = { version = "0.3.21", default-features = false, features = ["executor"] } http = "0.2.6" indoc = "1.0.4" diff --git a/crates/matrix-sdk-crypto/README.md b/crates/matrix-sdk-crypto/README.md index b8dc9ad80..a7a7f7a69 100644 --- a/crates/matrix-sdk-crypto/README.md +++ b/crates/matrix-sdk-crypto/README.md @@ -30,14 +30,13 @@ async fn main() -> Result<(), OlmError> { let alice = user_id!("@alice:example.org"); let machine = OlmMachine::new(&alice, device_id!("DEVICEID")).await; - let to_device_events = ToDevice::default(); let changed_devices = DeviceLists::default(); let one_time_key_counts = BTreeMap::default(); let unused_fallback_keys = Some(Vec::new()); // Push changes that the server sent to us in a sync response. let decrypted_to_device = machine.receive_sync_changes( - to_device_events, + vec![], &changed_devices, &one_time_key_counts, unused_fallback_keys.as_deref(), diff --git a/crates/matrix-sdk-crypto/src/gossiping/machine.rs b/crates/matrix-sdk-crypto/src/gossiping/machine.rs index 228ce4580..2a1fc1ef7 100644 --- a/crates/matrix-sdk-crypto/src/gossiping/machine.rs +++ b/crates/matrix-sdk-crypto/src/gossiping/machine.rs @@ -41,18 +41,13 @@ use crate::{ requests::{OutgoingRequest, ToDeviceRequest}, session_manager::GroupSessionCache, store::{Changes, CryptoStoreError, SecretImportError, Store}, - types::{ - events::{ - forwarded_room_key::{ForwardedMegolmV1AesSha2Content, ForwardedRoomKeyContent}, - olm_v1::{DecryptedForwardedRoomKeyEvent, DecryptedSecretSendEvent}, - room::encrypted::EncryptedEvent, - room_key_request::{ - Action, MegolmV1AesSha2Content, RequestedKeyInfo, RoomKeyRequestEvent, - }, - secret_send::SecretSendContent, - EventType, - }, - EventEncryptionAlgorithm, + types::events::{ + forwarded_room_key::ForwardedRoomKeyContent, + olm_v1::{DecryptedForwardedRoomKeyEvent, DecryptedSecretSendEvent}, + room::encrypted::EncryptedEvent, + room_key_request::{Action, RequestedKeyInfo, RoomKeyRequestEvent}, + secret_send::SecretSendContent, + EventType, }, Device, MegolmError, }; @@ -310,27 +305,13 @@ impl GossipMachine { }) } - async fn handle_megolm_v1_request( + /// Answer a room key request after we found the matching + /// `InboundGroupSession`. + async fn answer_room_key_request( &self, event: &RoomKeyRequestEvent, - key_info: &MegolmV1AesSha2Content, + session: InboundGroupSession, ) -> OlmResult> { - let session = - self.store.get_inbound_group_session(&key_info.room_id, &key_info.session_id).await?; - - let session = if let Some(s) = session { - s - } else { - debug!( - user_id = event.sender.as_str(), - device_id = event.content.requesting_device_id.as_str(), - session_id = key_info.session_id.as_str(), - room_id = key_info.room_id.as_str(), - "Received a room key request for an unknown inbound group session", - ); - return Ok(None); - }; - let device = self.store.get_device(&event.sender, &event.content.requesting_device_id).await?; @@ -342,7 +323,7 @@ impl GossipMachine { user_id = device.user_id().as_str(), device_id = device.device_id().as_str(), "Received a key request from a device that changed \ - their curve25519 sender key" + their Curve25519 sender key" ); } else { debug!( @@ -357,21 +338,21 @@ impl GossipMachine { } Ok(message_index) => { info!( - user_id = device.user_id().as_str(), - device_id = device.device_id().as_str(), - session_id = key_info.session_id.as_str(), - room_id = key_info.room_id.as_str(), + user_id = %device.user_id(), + device_id = %device.device_id(), + session_id = session.session_id(), + room_id = %session.room_id, ?message_index, "Serving a room key request", ); - match self.share_session(&session, &device, message_index).await { + match self.forward_room_key(&session, &device, message_index).await { Ok(s) => Ok(Some(s)), Err(OlmError::MissingSession) => { info!( - user_id = device.user_id().as_str(), - device_id = device.device_id().as_str(), - session_id = key_info.session_id.as_str(), + user_id = %device.user_id(), + device_id = %device.device_id(), + session_id = session.session_id(), "Key request is missing an Olm session, \ putting the request in the wait queue", ); @@ -381,9 +362,9 @@ impl GossipMachine { } Err(OlmError::SessionExport(e)) => { warn!( - user_id = device.user_id().as_str(), - device_id = device.device_id().as_str(), - session_id = key_info.session_id.as_str(), + user_id = %device.user_id(), + device_id = %device.device_id(), + session_id = session.session_id(), "Can't serve a room key request, the session \ can't be exported into a forwarded room key: \ {:?}", @@ -397,8 +378,8 @@ impl GossipMachine { } } else { warn!( - user_id = event.sender.as_str(), - device_id = event.content.requesting_device_id.as_str(), + user_id = %event.sender, + device_id = %event.content.requesting_device_id, "Received a key request from an unknown device", ); self.store.update_tracked_user(&event.sender, true).await?; @@ -407,18 +388,40 @@ impl GossipMachine { } } + async fn handle_supported_key_request( + &self, + event: &RoomKeyRequestEvent, + room_id: &RoomId, + session_id: &str, + ) -> OlmResult> { + let session = self.store.get_inbound_group_session(room_id, session_id).await?; + + if let Some(s) = session { + self.answer_room_key_request(event, s).await + } else { + debug!( + user_id = %event.sender, + device_id = %event.content.requesting_device_id, + session_id, + %room_id, + "Received a room key request for an unknown inbound group session", + ); + + Ok(None) + } + } + /// Handle a single incoming key request. async fn handle_key_request(&self, event: &RoomKeyRequestEvent) -> OlmResult> { match &event.content.action { Action::Request(info) => match info { RequestedKeyInfo::MegolmV1AesSha2(i) => { - self.handle_megolm_v1_request(event, i).await + self.handle_supported_key_request(event, &i.room_id, &i.session_id).await } - // V2 room key requests don't have a sender_key field, we - // currently can't fetch an inbound group session without a - // sender key, so ignore the request. #[cfg(feature = "experimental-algorithms")] - RequestedKeyInfo::MegolmV2AesSha2(_) => Ok(None), + RequestedKeyInfo::MegolmV2AesSha2(i) => { + self.handle_supported_key_request(event, &i.room_id, &i.session_id).await + } RequestedKeyInfo::Unknown(i) => { debug!( sender = %event.sender, @@ -458,7 +461,7 @@ impl GossipMachine { Ok(used_session) } - async fn share_session( + async fn forward_room_key( &self, session: &InboundGroupSession, device: &Device, @@ -689,18 +692,6 @@ impl GossipMachine { Ok(()) } - /// Get an outgoing key info that matches the forwarded room key content. - async fn get_key_info( - &self, - event: &DecryptedForwardedRoomKeyEvent, - ) -> Result, CryptoStoreError> { - if let Some(info) = event.room_key_info().map(|i| i.into()) { - self.store.get_secret_request_by_info(&info).await - } else { - Ok(None) - } - } - /// Delete the given outgoing key info. async fn delete_key_info(&self, info: &GossipRequest) -> Result<(), CryptoStoreError> { self.store.delete_outgoing_secret_requests(&info.request_id).await @@ -868,18 +859,16 @@ impl GossipMachine { async fn accept_forwarded_room_key( &self, info: &GossipRequest, - sender: &UserId, sender_key: Curve25519PublicKey, - algorithm: EventEncryptionAlgorithm, - content: &ForwardedMegolmV1AesSha2Content, + event: &DecryptedForwardedRoomKeyEvent, ) -> Result, CryptoStoreError> { - match InboundGroupSession::from_forwarded_key(&algorithm, content) { + match InboundGroupSession::try_from(event) { Ok(session) => { if self.store.compare_group_session(&session).await? == SessionOrdering::Better { self.mark_as_done(info).await?; info!( - %sender, + sender = %event.sender, %sender_key, claimed_sender_key = %session.sender_key(), room_id = session.room_id().as_str(), @@ -891,7 +880,7 @@ impl GossipMachine { Ok(Some(session)) } else { info!( - %sender, + sender = %event.sender, %sender_key, claimed_sender_key = %session.sender_key(), room_id = %session.room_id, @@ -905,11 +894,8 @@ impl GossipMachine { } Err(e) => { warn!( - %sender, - sender_key = sender_key.to_base64(), - claimed_sender_key = content.claimed_sender_key.to_base64(), - room_id = content.room_id.as_str(), - %algorithm, + sender = %event.sender, + %sender_key, "Couldn't create a group session from a received room key" ); Err(e.into()) @@ -938,36 +924,44 @@ impl GossipMachine { &self, sender_key: Curve25519PublicKey, event: &DecryptedForwardedRoomKeyEvent, - content: &ForwardedMegolmV1AesSha2Content, ) -> Result, CryptoStoreError> { - let algorithm = event.content.algorithm(); + if let Some(info) = event.room_key_info() { + if let Some(request) = + self.store.get_secret_request_by_info(&info.clone().into()).await? + { + if self.should_accept_forward(&request, sender_key).await? { + self.accept_forwarded_room_key(&request, sender_key, event).await + } else { + warn!( + sender = %event.sender, + %sender_key, + room_id = %info.room_id(), + session_id = info.session_id(), + "Received a forwarded room key from an unknown device, or \ + from a device that the key request recipient doesn't own", + ); - if let Some(info) = self.get_key_info(event).await? { - if self.should_accept_forward(&info, sender_key).await? { - self.accept_forwarded_room_key(&info, &event.sender, sender_key, algorithm, content) - .await + Ok(None) + } } else { warn!( sender = %event.sender, - %sender_key, - room_id = %content.room_id, - session_id = content.session_id.as_str(), - claimed_sender_key = %content.claimed_sender_key, - "Received a forwarded room key from an unknown device, or \ - from a device that the key request recipient doesn't own", + sender_key = %sender_key, + room_id = %info.room_id(), + session_id = info.session_id(), + sender_key = %sender_key, + algorithm = %info.algorithm(), + "Received a forwarded room key that we didn't request", ); Ok(None) } } else { warn!( - sender = %event.sender, - sender_key = %sender_key, - room_id = %content.room_id, - session_id = content.session_id.as_str(), - claimed_sender_key = %content.claimed_sender_key, - algorithm = %algorithm, - "Received a forwarded room key that we didn't request", + sender = event.sender.as_str(), + sender_key = sender_key.to_base64(), + algorithm = %event.content.algorithm(), + "Received a forwarded room key with an unsupported algorithm", ); Ok(None) @@ -980,13 +974,13 @@ impl GossipMachine { sender_key: Curve25519PublicKey, event: &DecryptedForwardedRoomKeyEvent, ) -> Result, CryptoStoreError> { - match &event.content { - ForwardedRoomKeyContent::MegolmV1AesSha2(content) => { - self.receive_supported_keys(sender_key, event, content).await + match event.content { + ForwardedRoomKeyContent::MegolmV1AesSha2(_) => { + self.receive_supported_keys(sender_key, event).await } #[cfg(feature = "experimental-algorithms")] - ForwardedRoomKeyContent::MegolmV2AesSha2(content) => { - self.receive_supported_keys(sender_key, event, content).await + ForwardedRoomKeyContent::MegolmV2AesSha2(_) => { + self.receive_supported_keys(sender_key, event).await } ForwardedRoomKeyContent::Unknown(_) => { warn!( @@ -1029,14 +1023,19 @@ mod tests { olm::{Account, OutboundGroupSession, PrivateCrossSigningIdentity, ReadOnlyAccount}, session_manager::GroupSessionCache, store::{Changes, CryptoStore, MemoryStore, Store}, - types::events::{ - forwarded_room_key::ForwardedRoomKeyContent, - olm_v1::{AnyDecryptedOlmEvent, DecryptedOlmV1Event}, - room::encrypted::{EncryptedEvent, EncryptedToDeviceEvent, RoomEncryptedEventContent}, - EventType, ToDeviceEvent, + types::{ + events::{ + forwarded_room_key::ForwardedRoomKeyContent, + olm_v1::{AnyDecryptedOlmEvent, DecryptedOlmV1Event}, + room::encrypted::{ + EncryptedEvent, EncryptedToDeviceEvent, RoomEncryptedEventContent, + }, + EventType, ToDeviceEvent, + }, + EventEncryptionAlgorithm, }, verification::VerificationMachine, - OutgoingRequest, OutgoingRequests, + EncryptionSettings, OutgoingRequest, OutgoingRequests, }; fn alice_id() -> &'static UserId { @@ -1122,6 +1121,7 @@ mod tests { async fn machines_for_key_share( other_machine_owner: &UserId, create_sessions: bool, + algorithm: EventEncryptionAlgorithm, ) -> (GossipMachine, Account, OutboundGroupSession, GossipMachine) { let alice_machine = get_machine().await; let alice_account = Account { @@ -1151,8 +1151,13 @@ mod tests { bob_machine.store.save_sessions(&[bob_session]).await.unwrap(); } - let (group_session, inbound_group_session) = - bob_machine.store.account().create_group_session_pair_with_defaults(room_id()).await; + let settings = EncryptionSettings { algorithm, ..Default::default() }; + let (group_session, inbound_group_session) = bob_machine + .store + .account() + .create_group_session_pair(room_id(), settings) + .await + .unwrap(); let content = group_session.encrypt(json!({}), "m.dummy").await; let event = wrap_encrypted_content(bob_machine.user_id(), content); @@ -1503,10 +1508,9 @@ mod tests { assert_matches!(machine.should_share_key(&own_device, &other_inbound).await, Ok(None)); } - #[async_test] - async fn key_share_cycle() { + async fn key_share_cycle(algorithm: EventEncryptionAlgorithm) { let (alice_machine, alice_account, group_session, bob_machine) = - machines_for_key_share(alice_id(), true).await; + machines_for_key_share(alice_id(), true, algorithm).await; // Get the request and convert it into a event. let requests = alice_machine.outgoing_to_device_requests().await.unwrap(); @@ -1565,7 +1569,7 @@ mod tests { #[async_test] async fn reject_forward_from_another_user() { let (alice_machine, alice_account, group_session, bob_machine) = - machines_for_key_share(bob_id(), true).await; + machines_for_key_share(bob_id(), true, EventEncryptionAlgorithm::MegolmV1AesSha2).await; // Get the request and convert it into a event. let requests = alice_machine.outgoing_to_device_requests().await.unwrap(); @@ -1611,6 +1615,17 @@ mod tests { } } + #[async_test] + async fn key_share_cycle_megolm_v1() { + key_share_cycle(EventEncryptionAlgorithm::MegolmV1AesSha2).await; + } + + #[cfg(feature = "experimental-algorithms")] + #[async_test] + async fn key_share_cycle_megolm_v2() { + key_share_cycle(EventEncryptionAlgorithm::MegolmV2AesSha2).await; + } + #[async_test] async fn secret_share_cycle() { let alice_machine = get_machine().await; @@ -1684,7 +1699,8 @@ mod tests { #[async_test] async fn key_share_cycle_without_session() { let (alice_machine, alice_account, group_session, bob_machine) = - machines_for_key_share(alice_id(), false).await; + machines_for_key_share(alice_id(), false, EventEncryptionAlgorithm::MegolmV1AesSha2) + .await; // Get the request and convert it into a event. let requests = alice_machine.outgoing_to_device_requests().await.unwrap(); diff --git a/crates/matrix-sdk-crypto/src/gossiping/mod.rs b/crates/matrix-sdk-crypto/src/gossiping/mod.rs index 2c2b0450d..4980a85c4 100644 --- a/crates/matrix-sdk-crypto/src/gossiping/mod.rs +++ b/crates/matrix-sdk-crypto/src/gossiping/mod.rs @@ -20,13 +20,14 @@ use dashmap::{DashMap, DashSet}; pub(crate) use machine::GossipMachine; use ruma::{ events::{ - room_key_request::{Action, RequestedKeyInfo, ToDeviceRoomKeyRequestEventContent}, + room_key_request::{Action, ToDeviceRoomKeyRequestEventContent}, secret::request::{ RequestAction, SecretName, ToDeviceSecretRequestEvent as SecretRequestEvent, ToDeviceSecretRequestEventContent as SecretRequestEventContent, }, - AnyToDeviceEventContent, + AnyToDeviceEventContent, ToDeviceEventType, }, + serde::Raw, to_device::DeviceIdOrAllDevices, DeviceId, OwnedDeviceId, OwnedTransactionId, OwnedUserId, TransactionId, UserId, }; @@ -36,7 +37,9 @@ use tracing::error; use crate::{ requests::{OutgoingRequest, ToDeviceRequest}, - types::events::room_key_request::{RoomKeyRequestEvent, SupportedKeyInfo}, + types::events::room_key_request::{ + RoomKeyRequestContent, RoomKeyRequestEvent, SupportedKeyInfo, + }, Device, }; @@ -84,16 +87,16 @@ pub enum SecretInfo { impl SecretInfo { /// Serialize `SecretInfo` into `String` for usage as database keys and - /// comparison + /// comparison. pub fn as_key(&self) -> String { match &self { - SecretInfo::KeyRequest(ref info) => format!( + SecretInfo::KeyRequest(info) => format!( "keyRequest:{:}:{:}:{:}", info.room_id().as_str(), info.session_id(), &info.algorithm(), ), - SecretInfo::SecretRequest(ref sname) => format!("secretName:{:}", sname), + SecretInfo::SecretRequest(sname) => format!("secretName:{sname}"), } } } @@ -132,45 +135,42 @@ impl GossipRequest { } fn to_request(&self, own_device_id: &DeviceId) -> OutgoingRequest { - let content = match &self.info { + let request = match &self.info { SecretInfo::KeyRequest(r) => { - let info = match r { - SupportedKeyInfo::MegolmV1AesSha2(c) => RequestedKeyInfo::new( - ruma::EventEncryptionAlgorithm::MegolmV1AesSha2, - c.room_id.to_owned(), - c.sender_key.to_base64(), - c.session_id.to_owned(), - ), - #[cfg(feature = "experimental-algorithms")] - #[allow(clippy::todo)] - SupportedKeyInfo::MegolmV2AesSha2(_) => { - todo!("Requesting megolm.v2 room keys is not supported yet") - } - }; - - AnyToDeviceEventContent::RoomKeyRequest(ToDeviceRoomKeyRequestEventContent::new( - Action::Request, - Some(info), + let content = RoomKeyRequestContent::new_request( + r.clone().into(), own_device_id.to_owned(), + self.request_id.to_owned(), + ); + let content = Raw::new(&content) + .expect("We can always serialize a room key request info") + .cast(); + + ToDeviceRequest::with_id_raw( + &self.request_recipient, + DeviceIdOrAllDevices::AllDevices, + content, + ToDeviceEventType::RoomKeyRequest, self.request_id.clone(), - )) + ) } SecretInfo::SecretRequest(s) => { - AnyToDeviceEventContent::SecretRequest(SecretRequestEventContent::new( - RequestAction::Request(s.clone()), - own_device_id.to_owned(), + let content = + AnyToDeviceEventContent::SecretRequest(SecretRequestEventContent::new( + RequestAction::Request(s.clone()), + own_device_id.to_owned(), + self.request_id.clone(), + )); + + ToDeviceRequest::with_id( + &self.request_recipient, + DeviceIdOrAllDevices::AllDevices, + content, self.request_id.clone(), - )) + ) } }; - let request = ToDeviceRequest::with_id( - &self.request_recipient, - DeviceIdOrAllDevices::AllDevices, - content, - self.request_id.clone(), - ); - OutgoingRequest { request_id: request.txn_id.clone(), request: Arc::new(request.into()) } } diff --git a/crates/matrix-sdk-crypto/src/identities/user.rs b/crates/matrix-sdk-crypto/src/identities/user.rs index fb360216d..62ba60f5e 100644 --- a/crates/matrix-sdk-crypto/src/identities/user.rs +++ b/crates/matrix-sdk-crypto/src/identities/user.rs @@ -806,6 +806,23 @@ impl ReadOnlyOwnUserIdentity { }) } + #[cfg(test)] + pub(crate) async fn from_private(identity: &crate::olm::PrivateCrossSigningIdentity) -> Self { + let master_key = identity.master_key.lock().await.as_ref().unwrap().public_key.clone(); + let self_signing_key = + identity.self_signing_key.lock().await.as_ref().unwrap().public_key.clone(); + let user_signing_key = + identity.user_signing_key.lock().await.as_ref().unwrap().public_key.clone(); + + Self { + user_id: identity.user_id().into(), + master_key, + self_signing_key, + user_signing_key, + verified: Arc::new(AtomicBool::new(false)), + } + } + /// Get the user id of this identity. pub fn user_id(&self) -> &UserId { &self.user_id diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index eb5c40b20..7c44c785c 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -88,7 +88,8 @@ pub use requests::{ }; pub use store::{CrossSigningKeyExport, CryptoStoreError, SecretImportError, SecretInfo}; pub use verification::{ - format_emojis, AcceptSettings, CancelInfo, Emoji, Sas, Verification, VerificationRequest, + format_emojis, AcceptSettings, AcceptedProtocols, CancelInfo, Emoji, EmojiShortAuthString, Sas, + SasState, Verification, VerificationRequest, }; #[cfg(feature = "qrcode")] pub use verification::{QrVerification, ScanError}; diff --git a/crates/matrix-sdk-crypto/src/machine.rs b/crates/matrix-sdk-crypto/src/machine.rs index 8b43edf6a..382727794 100644 --- a/crates/matrix-sdk-crypto/src/machine.rs +++ b/crates/matrix-sdk-crypto/src/machine.rs @@ -31,15 +31,16 @@ use ruma::{ upload_keys, upload_signatures::v3::Request as UploadSignaturesRequest, }, - sync::sync_events::v3::{DeviceLists, ToDevice}, + sync::sync_events::DeviceLists, }, assign, events::{ - secret::request::SecretName, AnyMessageLikeEvent, AnyTimelineEvent, MessageLikeEventContent, + secret::request::SecretName, AnyMessageLikeEvent, AnyTimelineEvent, AnyToDeviceEvent, + MessageLikeEventContent, }, serde::Raw, - DeviceId, DeviceKeyAlgorithm, OwnedDeviceKeyId, OwnedTransactionId, OwnedUserId, RoomId, - TransactionId, UInt, UserId, + DeviceId, DeviceKeyAlgorithm, OwnedDeviceId, OwnedDeviceKeyId, OwnedTransactionId, OwnedUserId, + RoomId, TransactionId, UInt, UserId, }; use serde_json::{value::to_raw_value, Value}; use tracing::{debug, error, info, trace, warn}; @@ -891,11 +892,11 @@ impl OlmMachine { /// [`decrypt_room_event`]: #method.decrypt_room_event pub async fn receive_sync_changes( &self, - to_device_events: ToDevice, + to_device_events: Vec>, changed_devices: &DeviceLists, one_time_keys_counts: &BTreeMap, unused_fallback_keys: Option<&[DeviceKeyAlgorithm]>, - ) -> OlmResult { + ) -> OlmResult>> { // Remove verification objects that have expired or are done. let mut events = self.verification_machine.garbage_collect(); @@ -912,7 +913,7 @@ impl OlmMachine { } } - for mut raw_event in to_device_events.events { + for mut raw_event in to_device_events { let event: ToDeviceEvents = match raw_event.deserialize_as() { Ok(e) => e, Err(e) => { @@ -1002,10 +1003,7 @@ impl OlmMachine { self.store.save_changes(changes).await?; - let mut to_device = ToDevice::new(); - to_device.events = events; - - Ok(to_device) + Ok(events) } /// Request a room key from our devices. @@ -1037,16 +1035,16 @@ impl OlmMachine { &self, session: &InboundGroupSession, sender: &UserId, - device_id: &DeviceId, - ) -> StoreResult { + ) -> StoreResult<(VerificationState, Option)> { Ok( // First find the device corresponding to the Curve25519 identity // key that sent us the session (recorded upon successful // decryption of the `m.room_key` to-device message). if let Some(device) = self - .get_device(sender, device_id, None) + .get_user_devices(sender, None) .await? - .filter(|d| d.curve25519_key().map(|k| k == session.sender_key()).unwrap_or(false)) + .devices() + .find(|d| d.curve25519_key() == Some(session.sender_key())) { // If the `Device` is confirmed to be the owner of the // `InboundGroupSession` we will consider the session (i.e. @@ -1058,14 +1056,14 @@ impl OlmMachine { if device.is_owner_of_session(session) && (device.is_our_own_device() || device.is_verified()) { - VerificationState::Trusted + (VerificationState::Trusted, Some(device.device_id().to_owned())) } else { - VerificationState::Untrusted + (VerificationState::Untrusted, Some(device.device_id().to_owned())) } } else { // We didn't find a device, no way to know if we should trust // the `InboundGroupSession` or not. - VerificationState::UnknownDevice + (VerificationState::UnknownDevice, None) }, ) } @@ -1079,12 +1077,10 @@ impl OlmMachine { &self, session: &InboundGroupSession, sender: &UserId, - device_id: &DeviceId, ) -> StoreResult { - let verification_state = self.get_verification_state(session, sender, device_id).await?; + let (verification_state, device_id) = self.get_verification_state(session, sender).await?; let sender = sender.to_owned(); - let device_id = device_id.to_owned(); Ok(EncryptionInfo { sender, @@ -1143,8 +1139,7 @@ impl OlmMachine { } } - let encryption_info = - self.get_encryption_info(&session, &event.sender, content.device_id()).await?; + let encryption_info = self.get_encryption_info(&session, &event.sender).await?; Ok(TimelineEvent { encryption_info: Some(encryption_info), event: decrypted_event }) } else { @@ -1589,7 +1584,7 @@ pub(crate) mod tests { api::{ client::{ keys::{claim_keys, get_keys, upload_keys}, - sync::sync_events::v3::{DeviceLists, ToDevice}, + sync::sync_events::v3::DeviceLists, }, IncomingResponse, }, @@ -1984,7 +1979,7 @@ pub(crate) mod tests { if let AnyToDeviceEvent::Dummy(e) = event { assert_eq!(&e.sender, alice.user_id()); } else { - panic!("Wrong event type found {:?}", event); + panic!("Wrong event type found {event:?}"); } } @@ -2008,21 +2003,18 @@ pub(crate) mod tests { let alice_session = alice.group_session_manager.get_outbound_group_session(room_id).unwrap(); - let mut to_device = ToDevice::new(); - to_device.events.push(event); - let decrypted = bob - .receive_sync_changes(to_device, &Default::default(), &Default::default(), None) + .receive_sync_changes(vec![event], &Default::default(), &Default::default(), None) .await .unwrap(); - let event = decrypted.events[0].deserialize().unwrap(); + let event = decrypted[0].deserialize().unwrap(); if let AnyToDeviceEvent::RoomKey(event) = event { assert_eq!(&event.sender, alice.user_id()); assert!(event.content.session_key.is_empty()); } else { - panic!("expected RoomKeyEvent found {:?}", event); + panic!("expected RoomKeyEvent found {event:?}"); } let session = @@ -2301,7 +2293,7 @@ pub(crate) mod tests { // Bob verifies that the MAC is valid and also sends a "done" message. let msgs = bob.verification_machine.outgoing_messages(); - eprintln!("{:?}", msgs); + eprintln!("{msgs:?}"); assert!(msgs.len() == 1); let event = msgs.first().map(|r| outgoing_request_to_event(bob.user_id(), r)).unwrap(); @@ -2322,7 +2314,7 @@ pub(crate) mod tests { assert!(!alice_sas.is_done()); assert!(!bob_device.is_verified()); // Alices receives the done message - eprintln!("{:?}", event); + eprintln!("{event:?}"); alice.handle_verification_event(&event).await; assert!(alice_sas.is_done()); @@ -2345,13 +2337,13 @@ pub(crate) mod tests { other: Default::default(), }; let event = json_convert(&event).unwrap(); - let mut to_device = ToDevice::new(); - to_device.events.push(event); let changed_devices = DeviceLists::new(); let key_counts = Default::default(); - let _ = - bob.receive_sync_changes(to_device, &changed_devices, &key_counts, None).await.unwrap(); + let _ = bob + .receive_sync_changes(vec![event], &changed_devices, &key_counts, None) + .await + .unwrap(); let group_session = GroupSession::new(SessionConfig::version_1()); let session_key = group_session.session_key(); @@ -2381,10 +2373,8 @@ pub(crate) mod tests { ); let event: Raw = json_convert(&event).unwrap(); - let mut to_device = ToDevice::new(); - to_device.events.push(event.clone()); - bob.receive_sync_changes(to_device, &changed_devices, &key_counts, None).await.unwrap(); + bob.receive_sync_changes(vec![event], &changed_devices, &key_counts, None).await.unwrap(); let session = bob.store.get_inbound_group_session(room_id, &session_id).await; diff --git a/crates/matrix-sdk-crypto/src/olm/account.rs b/crates/matrix-sdk-crypto/src/olm/account.rs index 943098cd8..1de7f31cb 100644 --- a/crates/matrix-sdk-crypto/src/olm/account.rs +++ b/crates/matrix-sdk-crypto/src/olm/account.rs @@ -138,7 +138,7 @@ impl OlmMessageHash { let sha = Sha256::new() .chain_update(sender_key.as_bytes()) .chain_update([message_type as u8]) - .chain_update(&ciphertext) + .chain_update(ciphertext) .finalize(); Self { sender_key, hash: encode(sha.as_slice()) } diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs index 6d62b60b1..5ba13c7c7 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs @@ -14,6 +14,7 @@ use std::{ fmt, + ops::Deref, sync::{ atomic::{AtomicBool, Ordering::SeqCst}, Arc, @@ -44,7 +45,11 @@ use crate::{ types::{ deserialize_curve_key, events::{ - forwarded_room_key::ForwardedMegolmV1AesSha2Content, + forwarded_room_key::{ + ForwardedMegolmV1AesSha2Content, ForwardedMegolmV2AesSha2Content, + ForwardedRoomKeyContent, + }, + olm_v1::DecryptedForwardedRoomKeyEvent, room::encrypted::{EncryptedEvent, RoomEventEncryptionScheme}, }, serialize_curve_key, EventEncryptionAlgorithm, SigningKeys, @@ -157,42 +162,6 @@ impl InboundGroupSession { }) } - /// Create a new inbound group session from a forwarded room key content. - /// - /// # Arguments - /// - /// * `sender_key` - The public curve25519 key of the account that - /// sent us the session - /// - /// * `content` - A forwarded room key content that contains the session key - /// to create the `InboundGroupSession`. - pub fn from_forwarded_key( - algorithm: &EventEncryptionAlgorithm, - content: &ForwardedMegolmV1AesSha2Content, - ) -> Result { - let config = OutboundGroupSession::session_config(algorithm)?; - - let session = InnerSession::import(&content.session_key, config); - - let first_known_index = session.first_known_index(); - - let mut sender_claimed_key = SigningKeys::new(); - sender_claimed_key.insert(DeviceKeyAlgorithm::Ed25519, content.claimed_ed25519_key.into()); - - Ok(InboundGroupSession { - inner: Mutex::new(session).into(), - session_id: content.session_id.as_str().into(), - sender_key: content.claimed_sender_key, - first_known_index, - history_visibility: None.into(), - signing_keys: sender_claimed_key.into(), - room_id: (*content.room_id).into(), - imported: true, - backed_up: AtomicBool::new(false).into(), - algorithm: algorithm.to_owned().into(), - }) - } - /// Store the group session as a base64 encoded string. /// /// # Arguments @@ -498,6 +467,67 @@ impl TryFrom<&ExportedRoomKey> for InboundGroupSession { } } +impl From<&ForwardedMegolmV1AesSha2Content> for InboundGroupSession { + fn from(value: &ForwardedMegolmV1AesSha2Content) -> Self { + let session = InnerSession::import(&value.session_key, SessionConfig::version_1()); + let session_id = session.session_id().into(); + let first_known_index = session.first_known_index(); + + InboundGroupSession { + inner: Mutex::new(session).into(), + session_id, + sender_key: value.claimed_sender_key, + history_visibility: None.into(), + first_known_index, + signing_keys: SigningKeys::from([( + DeviceKeyAlgorithm::Ed25519, + value.claimed_ed25519_key.into(), + )]) + .into(), + room_id: value.room_id.to_owned().into(), + imported: true, + algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2.into(), + backed_up: AtomicBool::from(false).into(), + } + } +} + +impl From<&ForwardedMegolmV2AesSha2Content> for InboundGroupSession { + fn from(value: &ForwardedMegolmV2AesSha2Content) -> Self { + let session = InnerSession::import(&value.session_key, SessionConfig::version_2()); + let session_id = session.session_id().into(); + let first_known_index = session.first_known_index(); + + InboundGroupSession { + inner: Mutex::new(session).into(), + session_id, + sender_key: value.claimed_sender_key, + history_visibility: None.into(), + first_known_index, + signing_keys: value.claimed_signing_keys.to_owned().into(), + room_id: value.room_id.to_owned().into(), + imported: true, + algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2.into(), + backed_up: AtomicBool::from(false).into(), + } + } +} + +impl TryFrom<&DecryptedForwardedRoomKeyEvent> for InboundGroupSession { + type Error = SessionCreationError; + + fn try_from(value: &DecryptedForwardedRoomKeyEvent) -> Result { + match &value.content { + ForwardedRoomKeyContent::MegolmV1AesSha2(c) => Ok(Self::from(c.deref())), + #[cfg(feature = "experimental-algorithms")] + ForwardedRoomKeyContent::MegolmV2AesSha2(c) => Ok(Self::from(c.deref())), + ForwardedRoomKeyContent::Unknown(c) => { + Err(SessionCreationError::Algorithm(c.algorithm.to_owned())) + } + } + } +} + #[cfg(test)] mod test { use matrix_sdk_test::async_test; diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs index 75461453b..0a1484ec9 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs @@ -27,6 +27,8 @@ use thiserror::Error; pub use vodozemac::megolm::{ExportedSessionKey, SessionKey}; use vodozemac::{megolm::SessionKeyDecodeError, Curve25519PublicKey}; +#[cfg(feature = "experimental-algorithms")] +use crate::types::events::forwarded_room_key::ForwardedMegolmV2AesSha2Content; use crate::types::{ deserialize_curve_key, deserialize_curve_key_vec, events::forwarded_room_key::{ForwardedMegolmV1AesSha2Content, ForwardedRoomKeyContent}, @@ -128,35 +130,50 @@ impl TryFrom for ForwardedRoomKeyContent { /// This will fail if the exported room key doesn't contain an Ed25519 /// claimed sender key. fn try_from(room_key: ExportedRoomKey) -> Result { - // The forwarded room key content only supports a single claimed sender - // key and it requires it to be a Ed25519 key. This here will be lossy - // conversion since we're dropping all other key types. - // - // This isn't yet a problem since no other key types exist, but still - // something that will need to be addressed sooner or later. - if let Some(SigningKey::Ed25519(claimed_ed25519_key)) = - room_key.sender_claimed_keys.get(&DeviceKeyAlgorithm::Ed25519) - { - if room_key.algorithm == EventEncryptionAlgorithm::MegolmV1AesSha2 { - Ok(ForwardedRoomKeyContent::MegolmV1AesSha2( - ForwardedMegolmV1AesSha2Content { + match room_key.algorithm { + EventEncryptionAlgorithm::MegolmV1AesSha2 => { + // The forwarded room key content only supports a single claimed sender + // key and it requires it to be a Ed25519 key. This here will be lossy + // conversion since we're dropping all other key types. + // + // This was fixed by the megolm v2 content. Hopefully we'll deprecate megolm v1 + // before we have multiple signing keys. + if let Some(SigningKey::Ed25519(claimed_ed25519_key)) = + room_key.sender_claimed_keys.get(&DeviceKeyAlgorithm::Ed25519) + { + Ok(ForwardedRoomKeyContent::MegolmV1AesSha2( + ForwardedMegolmV1AesSha2Content { + room_id: room_key.room_id, + session_id: room_key.session_id, + session_key: room_key.session_key, + claimed_sender_key: room_key.sender_key, + claimed_ed25519_key: *claimed_ed25519_key, + forwarding_curve25519_key_chain: room_key + .forwarding_curve25519_key_chain + .clone(), + other: Default::default(), + } + .into(), + )) + } else { + Err(SessionExportError::MissingEd25519Key) + } + } + #[cfg(feature = "experimental-algorithms")] + EventEncryptionAlgorithm::MegolmV2AesSha2 => { + Ok(ForwardedRoomKeyContent::MegolmV2AesSha2( + ForwardedMegolmV2AesSha2Content { room_id: room_key.room_id, session_id: room_key.session_id, session_key: room_key.session_key, claimed_sender_key: room_key.sender_key, - claimed_ed25519_key: *claimed_ed25519_key, - forwarding_curve25519_key_chain: room_key - .forwarding_curve25519_key_chain - .clone(), + claimed_signing_keys: room_key.sender_claimed_keys, other: Default::default(), } .into(), )) - } else { - Err(SessionExportError::MissingEd25519Key) } - } else { - Err(SessionExportError::Algorithm(room_key.algorithm)) + _ => Err(SessionExportError::Algorithm(room_key.algorithm)), } } } @@ -180,26 +197,32 @@ impl TryFrom for ExportedRoomKey { fn try_from(forwarded_key: ForwardedRoomKeyContent) -> Result { let algorithm = forwarded_key.algorithm(); - let handle_key = |content: Box| { - let mut sender_claimed_keys = SigningKeys::new(); - sender_claimed_keys - .insert(DeviceKeyAlgorithm::Ed25519, content.claimed_ed25519_key.into()); + match forwarded_key { + ForwardedRoomKeyContent::MegolmV1AesSha2(content) => { + let mut sender_claimed_keys = SigningKeys::new(); + sender_claimed_keys + .insert(DeviceKeyAlgorithm::Ed25519, content.claimed_ed25519_key.into()); - Ok(Self { + Ok(Self { + algorithm, + room_id: content.room_id, + session_id: content.session_id, + forwarding_curve25519_key_chain: content.forwarding_curve25519_key_chain, + sender_claimed_keys, + sender_key: content.claimed_sender_key, + session_key: content.session_key, + }) + } + #[cfg(feature = "experimental-algorithms")] + ForwardedRoomKeyContent::MegolmV2AesSha2(content) => Ok(Self { algorithm, room_id: content.room_id, session_id: content.session_id, - forwarding_curve25519_key_chain: content.forwarding_curve25519_key_chain, - sender_claimed_keys, + forwarding_curve25519_key_chain: Default::default(), + sender_claimed_keys: content.claimed_signing_keys, sender_key: content.claimed_sender_key, session_key: content.session_key, - }) - }; - - match forwarded_key { - ForwardedRoomKeyContent::MegolmV1AesSha2(content) => handle_key(content), - #[cfg(feature = "experimental-algorithms")] - ForwardedRoomKeyContent::MegolmV2AesSha2(content) => handle_key(content), + }), ForwardedRoomKeyContent::Unknown(c) => Err(SessionExportError::Algorithm(c.algorithm)), } } diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs index eb072e313..753b84dc7 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs @@ -325,13 +325,10 @@ impl OutboundGroupSession { } .into(), #[cfg(feature = "experimental-algorithms")] - EventEncryptionAlgorithm::MegolmV2AesSha2 => MegolmV2AesSha2Content { - ciphertext, - session_id: self.session_id().to_owned(), - sender_key: self.account_identity_keys.curve25519, - device_id: (*self.device_id).to_owned(), + EventEncryptionAlgorithm::MegolmV2AesSha2 => { + MegolmV2AesSha2Content { ciphertext, session_id: self.session_id().to_owned() } + .into() } - .into(), _ => unreachable!( "An outbound group session is always using one of the supported algorithms" ), diff --git a/crates/matrix-sdk-crypto/src/requests.rs b/crates/matrix-sdk-crypto/src/requests.rs index fa382bb52..5cec2b344 100644 --- a/crates/matrix-sdk-crypto/src/requests.rs +++ b/crates/matrix-sdk-crypto/src/requests.rs @@ -116,6 +116,19 @@ impl ToDeviceRequest { } } + pub(crate) fn with_id_raw( + recipient: &UserId, + recipient_device: impl Into, + content: Raw, + event_type: ToDeviceEventType, + txn_id: OwnedTransactionId, + ) -> Self { + let user_messages = iter::once((recipient_device.into(), content)).collect(); + let messages = iter::once((recipient.to_owned(), user_messages)).collect(); + + ToDeviceRequest { event_type, txn_id, messages } + } + pub(crate) fn with_id( recipient: &UserId, recipient_device: impl Into, diff --git a/crates/matrix-sdk-crypto/src/store/mod.rs b/crates/matrix-sdk-crypto/src/store/mod.rs index 69f4023fa..4cf48151b 100644 --- a/crates/matrix-sdk-crypto/src/store/mod.rs +++ b/crates/matrix-sdk-crypto/src/store/mod.rs @@ -325,7 +325,7 @@ impl Store { } #[cfg(test)] - /// Testing helper to allo to save only a set of devices + /// Testing helper to allow to save only a set of devices pub async fn save_devices(&self, devices: &[ReadOnlyDevice]) -> Result<()> { let changes = Changes { devices: DeviceChanges { changed: devices.to_vec(), ..Default::default() }, diff --git a/crates/matrix-sdk-crypto/src/types/events/forwarded_room_key.rs b/crates/matrix-sdk-crypto/src/types/events/forwarded_room_key.rs index c830b033c..ab15f9a59 100644 --- a/crates/matrix-sdk-crypto/src/types/events/forwarded_room_key.rs +++ b/crates/matrix-sdk-crypto/src/types/events/forwarded_room_key.rs @@ -16,7 +16,7 @@ use std::collections::BTreeMap; -use ruma::OwnedRoomId; +use ruma::{DeviceKeyAlgorithm, OwnedRoomId}; use serde::{Deserialize, Serialize}; use serde_json::Value; use vodozemac::{megolm::ExportedSessionKey, Curve25519PublicKey, Ed25519PublicKey}; @@ -24,7 +24,7 @@ use vodozemac::{megolm::ExportedSessionKey, Curve25519PublicKey, Ed25519PublicKe use super::{EventType, ToDeviceEvent}; use crate::types::{ deserialize_curve_key, deserialize_curve_key_vec, deserialize_ed25519_key, serialize_curve_key, - serialize_curve_key_vec, serialize_ed25519_key, EventEncryptionAlgorithm, + serialize_curve_key_vec, serialize_ed25519_key, EventEncryptionAlgorithm, SigningKeys, }; /// The `m.forwarded_room_key` to-device event. @@ -53,7 +53,7 @@ pub enum ForwardedRoomKeyContent { /// The `m.megolm.v2.aes-sha2` variant of the `m.forwarded_room_key` /// content. #[cfg(feature = "experimental-algorithms")] - MegolmV2AesSha2(Box), + MegolmV2AesSha2(Box), /// An unknown and unsupported variant of the `m.forwarded_room_key` /// content. Unknown(UnknownRoomKeyContent), @@ -79,7 +79,7 @@ impl EventType for ForwardedRoomKeyContent { const EVENT_TYPE: &'static str = "m.forwarded_room_key"; } -/// The `m.megolm.v1.aes-sha2` variant of the `m.room_key` content. +/// The `m.megolm.v1.aes-sha2` variant of the `m.forwarded_room_key` content. #[derive(Deserialize, Serialize)] pub struct ForwardedMegolmV1AesSha2Content { /// The room where the key is used. @@ -131,7 +131,42 @@ pub struct ForwardedMegolmV1AesSha2Content { pub(crate) other: BTreeMap, } -/// An unknown and unsupported `m.room_key` algorithm. +/// The `m.megolm.v2.aes-sha2` variant of the `m.forwarded_room_key` content. +#[derive(Deserialize, Serialize)] +pub struct ForwardedMegolmV2AesSha2Content { + /// The room where the key is used. + pub room_id: OwnedRoomId, + + /// The ID of the session that the key is for. + pub session_id: String, + + /// The key to be exchanged. Can be used to create a [`InboundGroupSession`] + /// that can be used to decrypt room events. + /// + /// [`InboundGroupSession`]: vodozemac::megolm::InboundGroupSession + pub session_key: ExportedSessionKey, + + /// The Curve25519 key of the device which initiated the session originally. + /// + /// It is ‘claimed’ because the receiving device has no way to tell that + /// the original room_key actually came from a device which owns the private + /// part of this key. + #[serde(deserialize_with = "deserialize_curve_key", serialize_with = "serialize_curve_key")] + pub claimed_sender_key: Curve25519PublicKey, + + /// The Ed25519 key of the device which initiated the session originally. + /// + /// It is ‘claimed’ because the receiving device has no way to tell that + /// the original room_key actually came from a device which owns the private + /// part of this key. + #[serde(default)] + pub claimed_signing_keys: SigningKeys, + + #[serde(flatten)] + pub(crate) other: BTreeMap, +} + +/// An unknown and unsupported `m.forwarded_room_key` algorithm. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct UnknownRoomKeyContent { /// The algorithm of the unknown room key. @@ -153,6 +188,17 @@ impl std::fmt::Debug for ForwardedMegolmV1AesSha2Content { } } +impl std::fmt::Debug for ForwardedMegolmV2AesSha2Content { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ForwardedMegolmV2AesSha2Content") + .field("room_id", &self.room_id) + .field("session_id", &self.session_id) + .field("claimed_sender_key", &self.claimed_sender_key) + .field("sender_claimed_keys", &self.claimed_signing_keys) + .finish_non_exhaustive() + } +} + #[derive(Deserialize, Serialize)] struct RoomKeyHelper { algorithm: EventEncryptionAlgorithm, @@ -171,7 +217,7 @@ impl TryFrom for ForwardedRoomKeyContent { } #[cfg(feature = "experimental-algorithms")] EventEncryptionAlgorithm::MegolmV2AesSha2 => { - let content: ForwardedMegolmV1AesSha2Content = serde_json::from_value(value.other)?; + let content: ForwardedMegolmV2AesSha2Content = serde_json::from_value(value.other)?; Self::MegolmV2AesSha2(content.into()) } _ => Self::Unknown(UnknownRoomKeyContent { diff --git a/crates/matrix-sdk-crypto/src/types/events/room/encrypted.rs b/crates/matrix-sdk-crypto/src/types/events/room/encrypted.rs index d8b9a2642..552d898ed 100644 --- a/crates/matrix-sdk-crypto/src/types/events/room/encrypted.rs +++ b/crates/matrix-sdk-crypto/src/types/events/room/encrypted.rs @@ -16,7 +16,7 @@ use std::collections::BTreeMap; -use ruma::{DeviceId, OwnedDeviceId, RoomId}; +use ruma::{OwnedDeviceId, RoomId}; use serde::{Deserialize, Serialize}; use serde_json::Value; use vodozemac::{megolm::MegolmMessage, olm::OlmMessage, Curve25519PublicKey}; @@ -250,15 +250,6 @@ impl SupportedEventEncryptionSchemes<'_> { } } - /// The ID of the sending device. - pub fn device_id(&self) -> &DeviceId { - match self { - SupportedEventEncryptionSchemes::MegolmV1AesSha2(c) => &c.device_id, - #[cfg(feature = "experimental-algorithms")] - SupportedEventEncryptionSchemes::MegolmV2AesSha2(c) => &c.device_id, - } - } - /// The algorithm that was used to encrypt the event content. pub fn algorithm(&self) -> EventEncryptionAlgorithm { match self { @@ -314,13 +305,6 @@ pub struct MegolmV2AesSha2Content { /// The ID of the session used to encrypt the message. pub session_id: String, - - /// The Curve25519 key of the sender. - #[serde(deserialize_with = "deserialize_curve_key", serialize_with = "serialize_curve_key")] - pub sender_key: Curve25519PublicKey, - - /// The ID of the sending device. - pub device_id: OwnedDeviceId, } /// An unknown and unsupported `m.room.encrypted` event content. diff --git a/crates/matrix-sdk-crypto/src/types/events/room_key_request.rs b/crates/matrix-sdk-crypto/src/types/events/room_key_request.rs index dd6ab19ee..a87a71691 100644 --- a/crates/matrix-sdk-crypto/src/types/events/room_key_request.rs +++ b/crates/matrix-sdk-crypto/src/types/events/room_key_request.rs @@ -51,6 +51,18 @@ pub struct RoomKeyRequestContent { pub request_id: OwnedTransactionId, } +impl RoomKeyRequestContent { + /// Create a new content for a `m.room_key_request` event with the action + /// set to request a room key with the given `RequestedKeyInfo`. + pub fn new_request( + info: RequestedKeyInfo, + requesting_device_id: OwnedDeviceId, + request_id: OwnedTransactionId, + ) -> Self { + Self { action: Action::Request(info), requesting_device_id, request_id } + } +} + impl EventType for RoomKeyRequestContent { const EVENT_TYPE: &'static str = "m.room_key_request"; } diff --git a/crates/matrix-sdk-crypto/src/verification/cache.rs b/crates/matrix-sdk-crypto/src/verification/cache.rs index bb674b4e4..5b6bb9c32 100644 --- a/crates/matrix-sdk-crypto/src/verification/cache.rs +++ b/crates/matrix-sdk-crypto/src/verification/cache.rs @@ -16,7 +16,7 @@ use std::sync::Arc; use dashmap::DashMap; use ruma::{DeviceId, OwnedTransactionId, OwnedUserId, TransactionId, UserId}; -use tracing::trace; +use tracing::{trace, warn}; use super::{event_enums::OutgoingContent, Sas, Verification}; #[cfg(feature = "qrcode")] @@ -56,6 +56,14 @@ impl VerificationCache { let old_verification = old.value(); if !old_verification.is_cancelled() { + warn!( + user_id = verification.other_user().as_str(), + old_flow_id = old_verification.flow_id(), + new_flow_id = verification.flow_id(), + "Received a new verification whilst another one with \ + the same user is ongoing. Cancelling both verifications" + ); + if let Some(r) = old_verification.cancel() { self.add_request(r.into()) } @@ -78,11 +86,7 @@ impl VerificationCache { pub fn replace_sas(&self, sas: Sas) { let verification: Verification = sas.into(); - - self.verification - .entry(verification.other_user().to_owned()) - .or_default() - .insert(verification.flow_id().to_owned(), verification.clone()); + self.replace(verification); } #[cfg(feature = "qrcode")] @@ -90,6 +94,12 @@ impl VerificationCache { self.insert(qr) } + #[cfg(feature = "qrcode")] + pub fn replace_qr(&self, qr: QrVerification) { + let verification: Verification = qr.into(); + self.replace(verification); + } + #[cfg(feature = "qrcode")] pub fn get_qr(&self, sender: &UserId, flow_id: &str) -> Option { self.get(sender, flow_id).and_then(|v| { @@ -101,6 +111,13 @@ impl VerificationCache { }) } + pub fn replace(&self, verification: Verification) { + self.verification + .entry(verification.other_user().to_owned()) + .or_default() + .insert(verification.flow_id().to_owned(), verification.clone()); + } + pub fn get(&self, sender: &UserId, flow_id: &str) -> Option { self.verification.get(sender).and_then(|m| m.get(flow_id).map(|v| v.clone())) } diff --git a/crates/matrix-sdk-crypto/src/verification/mod.rs b/crates/matrix-sdk-crypto/src/verification/mod.rs index 6eb7b01c5..b5018afa0 100644 --- a/crates/matrix-sdk-crypto/src/verification/mod.rs +++ b/crates/matrix-sdk-crypto/src/verification/mod.rs @@ -47,7 +47,7 @@ use ruma::{ DeviceId, EventId, OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedTransactionId, RoomId, UserId, }; -pub use sas::{AcceptSettings, Sas}; +pub use sas::{AcceptSettings, AcceptedProtocols, EmojiShortAuthString, Sas, SasState}; use tracing::{error, info, trace, warn}; use crate::{ @@ -100,7 +100,7 @@ pub fn format_emojis(emojis: [Emoji; 7]) -> String { // Hack to make terminals behave properly when one of the above is printed. let emoji = if VARIATION_SELECTOR_EMOJIS.contains(&emoji) { - format!("{} ", emoji) + format!("{emoji} ") } else { emoji.to_owned() }; @@ -109,13 +109,12 @@ pub fn format_emojis(emojis: [Emoji; 7]) -> String { // monospace characters. let placeholder = ".".repeat(EMOJI_WIDTH); - format!("{:^12}", placeholder).replace(&placeholder, &emoji) + format!("{placeholder:^12}").replace(&placeholder, &emoji) }; let emoji_string = emojis.iter().map(|e| center_emoji(e)).collect::>().join(""); - let description = - descriptions.iter().map(|d| format!("{:^12}", d)).collect::>().join(""); + let description = descriptions.iter().map(|d| format!("{d:^12}")).collect::>().join(""); format!("{emoji_string}\n{description}") } @@ -826,7 +825,9 @@ mod test { use super::VerificationStore; use crate::{ - olm::PrivateCrossSigningIdentity, store::MemoryStore, ReadOnlyAccount, ReadOnlyDevice, + olm::PrivateCrossSigningIdentity, + store::{Changes, CryptoStore, IdentityChanges, MemoryStore}, + ReadOnlyAccount, ReadOnlyDevice, ReadOnlyOwnUserIdentity, ReadOnlyUserIdentity, }; pub fn alice_id() -> &'static UserId { @@ -848,28 +849,57 @@ mod test { pub(crate) async fn setup_stores() -> (VerificationStore, VerificationStore) { let alice = ReadOnlyAccount::new(alice_id(), alice_device_id()); let alice_store = MemoryStore::new(); - let alice_identity = Mutex::new(PrivateCrossSigningIdentity::empty(alice_id())); + let (alice_private_identity, _, _) = + PrivateCrossSigningIdentity::with_account(&alice).await; + let alice_private_identity = Mutex::new(alice_private_identity); let bob = ReadOnlyAccount::new(bob_id(), bob_device_id()); let bob_store = MemoryStore::new(); - let bob_identity = Mutex::new(PrivateCrossSigningIdentity::empty(bob_id())); + let (bob_private_identity, _, _) = PrivateCrossSigningIdentity::with_account(&bob).await; + let bob_private_identity = Mutex::new(bob_private_identity); + + let alice_public_identity = + ReadOnlyUserIdentity::from_private(&*alice_private_identity.lock().await).await; + let alice_readonly_identity = + ReadOnlyOwnUserIdentity::from_private(&*alice_private_identity.lock().await).await; + let bob_public_identity = + ReadOnlyUserIdentity::from_private(&*bob_private_identity.lock().await).await; + let bob_readonly_identity = + ReadOnlyOwnUserIdentity::from_private(&*bob_private_identity.lock().await).await; let alice_device = ReadOnlyDevice::from_account(&alice).await; let bob_device = ReadOnlyDevice::from_account(&bob).await; + let alice_changes = Changes { + identities: IdentityChanges { + new: vec![alice_readonly_identity.into(), bob_public_identity.into()], + changed: vec![], + }, + ..Default::default() + }; + alice_store.save_changes(alice_changes).await.unwrap(); alice_store.save_devices(vec![bob_device]).await; + + let bob_changes = Changes { + identities: IdentityChanges { + new: vec![bob_readonly_identity.into(), alice_public_identity.into()], + changed: vec![], + }, + ..Default::default() + }; + bob_store.save_changes(bob_changes).await.unwrap(); bob_store.save_devices(vec![alice_device]).await; let alice_store = VerificationStore { account: alice, inner: Arc::new(alice_store), - private_identity: alice_identity.into(), + private_identity: alice_private_identity.into(), }; let bob_store = VerificationStore { account: bob.clone(), inner: Arc::new(bob_store), - private_identity: bob_identity.into(), + private_identity: bob_private_identity.into(), }; (alice_store, bob_store) diff --git a/crates/matrix-sdk-crypto/src/verification/requests.rs b/crates/matrix-sdk-crypto/src/verification/requests.rs index fd33342a0..4f678694a 100644 --- a/crates/matrix-sdk-crypto/src/verification/requests.rs +++ b/crates/matrix-sdk-crypto/src/verification/requests.rs @@ -36,6 +36,8 @@ use ruma::{ DeviceId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId, RoomId, TransactionId, UserId, }; +#[cfg(feature = "qrcode")] +use tracing::debug; use tracing::{info, trace, warn}; #[cfg(feature = "qrcode")] @@ -336,7 +338,29 @@ impl VerificationRequest { if let Some(future) = fut { let qr_verification = future.await?; - self.verification_cache.insert_qr(qr_verification.clone()); + + // We may have previously started our own QR verification (e.g. two devices + // displaying QR code at the same time), so we need to replace it with the newly + // scanned code. + if self + .verification_cache + .get_qr(qr_verification.other_user_id(), qr_verification.flow_id().as_str()) + .is_some() + { + debug!( + user_id = %self.other_user(), + flow_id = self.flow_id().as_str(), + "Replacing existing QR verification" + ); + self.verification_cache.replace_qr(qr_verification.clone()); + } else { + debug!( + user_id = %self.other_user(), + flow_id = self.flow_id().as_str(), + "Inserting new QR verification" + ); + self.verification_cache.insert_qr(qr_verification.clone()); + } Ok(Some(qr_verification)) } else { @@ -634,7 +658,24 @@ impl VerificationRequest { if let Some((sas, content)) = s.clone().start_sas(self.we_started, self.inner.clone().into()).await? { - self.verification_cache.insert_sas(sas.clone()); + // We may have previously started QR verification and generated a QR code. If we + // now switch to SAS flow, the previous verification has to be replaced + cfg_if::cfg_if! { + if #[cfg(feature = "qrcode")] { + if self.verification_cache.get_qr(sas.other_user_id(), sas.flow_id().as_str()).is_some() { + debug!( + user_id = %self.other_user(), + flow_id = self.flow_id().as_str(), + "We have an ongoing QR verification, replacing with SAS" + ); + self.verification_cache.replace(sas.clone().into()) + } else { + self.verification_cache.insert_sas(sas.clone()); + } + } else { + self.verification_cache.insert_sas(sas.clone()); + } + } let request = match content { OutgoingContent::ToDevice(content) => ToDeviceRequest::with_id( @@ -1222,7 +1263,11 @@ mod tests { use std::convert::{TryFrom, TryInto}; + #[cfg(feature = "qrcode")] + use matrix_sdk_qrcode::QrVerificationData; use matrix_sdk_test::async_test; + #[cfg(feature = "qrcode")] + use ruma::events::key::verification::VerificationMethod; use ruma::{event_id, room_id}; use super::VerificationRequest; @@ -1385,4 +1430,125 @@ mod tests { assert!(alice_sas.started_from_request()); assert!(bob_sas.started_from_request()); } + + #[async_test] + #[cfg(feature = "qrcode")] + async fn can_scan_another_qr_after_creating_mine() { + let (alice_store, bob_store) = setup_stores().await; + + let flow_id = FlowId::ToDevice("TEST_FLOW_ID".into()); + + // We setup the initial verification request + let bob_request = VerificationRequest::new( + VerificationCache::new(), + bob_store, + flow_id.clone(), + alice_id(), + vec![], + Some(vec![VerificationMethod::QrCodeScanV1, VerificationMethod::QrCodeShowV1]), + ); + + let request = bob_request.request_to_device(); + let content: OutgoingContent = request.try_into().unwrap(); + let content = RequestContent::try_from(&content).unwrap(); + + let alice_request = VerificationRequest::from_request( + VerificationCache::new(), + alice_store, + bob_id(), + flow_id, + &content, + ); + + let content: OutgoingContent = alice_request + .accept_with_methods(vec![ + VerificationMethod::QrCodeScanV1, + VerificationMethod::QrCodeShowV1, + ]) + .unwrap() + .try_into() + .unwrap(); + let content = ReadyContent::try_from(&content).unwrap(); + bob_request.receive_ready(alice_id(), &content); + + assert!(bob_request.is_ready()); + assert!(alice_request.is_ready()); + + // Each side can start its own QR verification flow by generating QR code + let alice_verification = alice_request.generate_qr_code().await.unwrap(); + let bob_verification = bob_request.generate_qr_code().await.unwrap(); + + assert!(alice_verification.is_some()); + assert!(bob_verification.is_some()); + + // Now only Alice scans Bob's code + let bob_qr_code = bob_verification.unwrap().to_bytes().unwrap(); + let bob_qr_code = QrVerificationData::from_bytes(bob_qr_code).unwrap(); + let alice_verification = alice_request.scan_qr_code(bob_qr_code).await.unwrap().unwrap(); + + // Finally we assert that the verification has been reciprocated rather than + // cancelled due to a duplicate verification flow + assert!(!alice_verification.is_cancelled()); + assert!(alice_verification.reciprocated()); + } + + #[async_test] + #[cfg(feature = "qrcode")] + async fn can_start_sas_after_generating_qr_code() { + let (alice_store, bob_store) = setup_stores().await; + + let flow_id = FlowId::ToDevice("TEST_FLOW_ID".into()); + + // We setup the initial verification request + let bob_request = VerificationRequest::new( + VerificationCache::new(), + bob_store, + flow_id.clone(), + alice_id(), + vec![], + Some(vec![ + VerificationMethod::QrCodeScanV1, + VerificationMethod::QrCodeShowV1, + VerificationMethod::SasV1, + ]), + ); + + let request = bob_request.request_to_device(); + let content: OutgoingContent = request.try_into().unwrap(); + let content = RequestContent::try_from(&content).unwrap(); + + let alice_request = VerificationRequest::from_request( + VerificationCache::new(), + alice_store, + bob_id(), + flow_id, + &content, + ); + + let content: OutgoingContent = alice_request + .accept_with_methods(vec![ + VerificationMethod::QrCodeScanV1, + VerificationMethod::QrCodeShowV1, + ]) + .unwrap() + .try_into() + .unwrap(); + let content = ReadyContent::try_from(&content).unwrap(); + bob_request.receive_ready(alice_id(), &content); + + assert!(bob_request.is_ready()); + assert!(alice_request.is_ready()); + + // Each side can start its own QR verification flow by generating QR code + let alice_verification = alice_request.generate_qr_code().await.unwrap(); + let bob_verification = bob_request.generate_qr_code().await.unwrap(); + + assert!(alice_verification.is_some()); + assert!(bob_verification.is_some()); + + // Alice can now start SAS verification flow instead of QR without cancelling + // the request + let (sas, _) = alice_request.start_sas().await.unwrap().unwrap(); + assert!(!sas.is_cancelled()); + } } diff --git a/crates/matrix-sdk-crypto/src/verification/sas/helpers.rs b/crates/matrix-sdk-crypto/src/verification/sas/helpers.rs index da617e73a..860f797bb 100644 --- a/crates/matrix-sdk-crypto/src/verification/sas/helpers.rs +++ b/crates/matrix-sdk-crypto/src/verification/sas/helpers.rs @@ -62,7 +62,7 @@ pub fn calculate_commitment(public_key: Curve25519PublicKey, content: &StartCont Base64::new( Sha256::new() .chain_update(public_key.to_base64()) - .chain_update(&content_string) + .chain_update(content_string) .finalize() .as_slice() .to_owned(), @@ -228,7 +228,7 @@ pub fn receive_mac_event( if let Some(key) = ids.other_device.keys().get(&key_id) { let calculated_mac = Base64::parse( - sas.calculate_mac_invalid_base64(&key.to_base64(), &format!("{}{}", info, key_id)), + sas.calculate_mac_invalid_base64(&key.to_base64(), &format!("{info}{key_id}")), ) .expect("Can't base64-decode SAS MAC"); diff --git a/crates/matrix-sdk-crypto/src/verification/sas/mod.rs b/crates/matrix-sdk-crypto/src/verification/sas/mod.rs index ab802967b..8f513b6da 100644 --- a/crates/matrix-sdk-crypto/src/verification/sas/mod.rs +++ b/crates/matrix-sdk-crypto/src/verification/sas/mod.rs @@ -18,17 +18,18 @@ mod sas_state; use std::sync::{Arc, Mutex}; +use futures_core::Stream; +use futures_signals::signal::{Mutable, SignalExt}; use inner_sas::InnerSas; -#[cfg(test)] -use matrix_sdk_common::instant::Instant; use ruma::{ api::client::keys::upload_signatures::v3::Request as SignatureUploadRequest, events::{ - key::verification::{cancel::CancelCode, ShortAuthenticationString}, + key::verification::{cancel::CancelCode, start::SasV1Content, ShortAuthenticationString}, AnyMessageLikeEventContent, AnyToDeviceEventContent, }, DeviceId, OwnedEventId, OwnedRoomId, OwnedTransactionId, RoomId, TransactionId, UserId, }; +pub use sas_state::AcceptedProtocols; use tracing::trace; use super::{ @@ -47,6 +48,7 @@ use crate::{ #[derive(Clone, Debug)] pub struct Sas { inner: Arc>, + state: Arc>, account: ReadOnlyAccount, identities_being_verified: IdentitiesBeingVerified, flow_id: Arc, @@ -54,6 +56,133 @@ pub struct Sas { request_handle: Option, } +/// The short auth string for the emoji method of SAS verification. +#[derive(Debug, Clone)] +pub struct EmojiShortAuthString { + /// A list of seven indices that should be used for the SAS verification. + /// + /// The indices can be put into the emoji table in the [spec] to figure out + /// the symbols and descriptions. + /// + /// If you have a table of [translated descriptions] for the emojis you will + /// want to use this field. + /// + /// [spec]: https://spec.matrix.org/unstable/client-server-api/#sas-method-emoji + /// [translated descriptions]: https://github.com/matrix-org/matrix-doc/blob/master/data-definitions/ + pub indices: [u8; 7], + + /// A list of seven emojis that should be used for the SAS verification. + pub emojis: [Emoji; 7], +} + +/// An Enum describing the state the SAS verification is in. +#[derive(Debug, Clone)] +pub enum SasState { + /// The verification has been started, the protocols that should be used + /// have been proposed and can be accepted. + Started { + /// The protocols that were proposed in the `m.key.verification.start` + /// event. + protocols: SasV1Content, + }, + /// The verification has been accepted and both sides agreed to a set of + /// protocols that will be used for the verification process. + Accepted { + /// The protocols that were accepted in the `m.key.verification.accept` + /// event. + accepted_protocols: AcceptedProtocols, + }, + /// The public keys have been exchanged and the short auth string can be + /// presented to the user. + KeysExchanged { + /// The emojis that represent the short auth string, will be `None` if + /// the emoji SAS method wasn't part of the [`AcceptedProtocols`]. + emojis: Option, + /// The list of decimals that represent the short auth string. + decimals: (u16, u16, u16), + }, + /// The verification process has been confirmed from our side, we're waiting + /// for the other side to confirm as well. + Confirmed, + /// The verification process has been successfully concluded. + Done { + /// The list of devices that has been verified. + verified_devices: Vec, + /// The list of user identities that has been verified. + verified_identities: Vec, + }, + /// The verification process has been cancelled. + Cancelled(CancelInfo), +} + +impl PartialEq for SasState { + fn eq(&self, other: &Self) -> bool { + matches!( + (self, other), + (Self::Started { .. }, Self::Started { .. }) + | (Self::Accepted { .. }, Self::Accepted { .. }) + | (Self::KeysExchanged { .. }, Self::KeysExchanged { .. }) + | (Self::Confirmed, Self::Confirmed) + | (Self::Done { .. }, Self::Done { .. }) + | (Self::Cancelled(_), Self::Cancelled(_)) + ) + } +} + +impl From<&InnerSas> for SasState { + fn from(value: &InnerSas) -> Self { + match value { + InnerSas::Created(s) => { + Self::Started { protocols: s.state.protocol_definitions.to_owned() } + } + InnerSas::Started(s) => { + Self::Started { protocols: s.state.protocol_definitions.to_owned() } + } + InnerSas::Accepted(s) => { + Self::Accepted { accepted_protocols: s.state.accepted_protocols.to_owned() } + } + InnerSas::WeAccepted(s) => { + Self::Accepted { accepted_protocols: s.state.accepted_protocols.to_owned() } + } + InnerSas::KeyReceived(s) => { + let emojis = if value.supports_emoji() { + let emojis = s.get_emoji(); + let indices = s.get_emoji_index(); + + Some(EmojiShortAuthString { emojis, indices }) + } else { + None + }; + + let decimals = s.get_decimal(); + + Self::KeysExchanged { emojis, decimals } + } + InnerSas::MacReceived(s) => { + let emojis = if value.supports_emoji() { + let emojis = s.get_emoji(); + let indices = s.get_emoji_index(); + + Some(EmojiShortAuthString { emojis, indices }) + } else { + None + }; + + let decimals = s.get_decimal(); + + Self::KeysExchanged { emojis, decimals } + } + InnerSas::Confirmed(_) => Self::Confirmed, + InnerSas::WaitingForDone(_) => Self::Confirmed, + InnerSas::Done(s) => Self::Done { + verified_devices: s.verified_devices().to_vec(), + verified_identities: s.verified_identities().to_vec(), + }, + InnerSas::Cancelled(c) => Self::Cancelled(c.state.as_ref().clone().into()), + } + } +} + impl Sas { /// Get our own user id. pub fn user_id(&self) -> &UserId { @@ -137,7 +266,7 @@ impl Sas { #[cfg(test)] #[allow(dead_code)] - pub(crate) fn set_creation_time(&self, time: Instant) { + pub(crate) fn set_creation_time(&self, time: matrix_sdk_common::instant::Instant) { self.inner.lock().unwrap().set_creation_time(time) } @@ -156,11 +285,13 @@ impl Sas { request_handle.is_some(), ); + let state = (&inner).into(); let account = identities.store.account.clone(); ( Sas { inner: Arc::new(Mutex::new(inner)), + state: Mutable::new(state).into(), account, identities_being_verified: identities, flow_id: flow_id.into(), @@ -241,10 +372,12 @@ impl Sas { request_handle.is_some(), )?; + let state = (&inner).into(); let account = identities.store.account.clone(); Ok(Sas { inner: Arc::new(Mutex::new(inner)), + state: Mutable::new(state).into(), account, identities_being_verified: identities, flow_id: flow_id.into(), @@ -271,28 +404,41 @@ impl Sas { &self, settings: AcceptSettings, ) -> Option { - let mut guard = self.inner.lock().unwrap(); - let sas: InnerSas = (*guard).clone(); - let methods = settings.allowed_methods; + let (request, state) = { + let mut guard = self.inner.lock().unwrap(); + let sas: InnerSas = (*guard).clone(); + let methods = settings.allowed_methods; - if let Some((sas, content)) = sas.accept(methods) { - *guard = sas; + if let Some((sas, content)) = sas.accept(methods) { + let state: SasState = (&sas).into(); - Some(match content { - OwnedAcceptContent::ToDevice(c) => { - let content = AnyToDeviceEventContent::KeyVerificationAccept(c); - self.content_to_request(content).into() - } - OwnedAcceptContent::Room(room_id, content) => RoomMessageRequest { - room_id, - txn_id: TransactionId::new(), - content: AnyMessageLikeEventContent::KeyVerificationAccept(content), - } - .into(), - }) - } else { - None + *guard = sas; + + ( + Some(match content { + OwnedAcceptContent::ToDevice(c) => { + let content = AnyToDeviceEventContent::KeyVerificationAccept(c); + self.content_to_request(content).into() + } + OwnedAcceptContent::Room(room_id, content) => RoomMessageRequest { + room_id, + txn_id: TransactionId::new(), + content: AnyMessageLikeEventContent::KeyVerificationAccept(content), + } + .into(), + }), + Some(state), + ) + } else { + (None, None) + } + }; + + if let Some(new_state) = state { + self.update_state(new_state); } + + request } /// Confirm the Sas verification. @@ -306,13 +452,15 @@ impl Sas { &self, ) -> Result<(Vec, Option), CryptoStoreError> { - let (contents, done) = { + let (contents, done, state) = { let mut guard = self.inner.lock().unwrap(); + let sas: InnerSas = (*guard).clone(); let (sas, contents) = sas.confirm(); + let state: SasState = (&sas).into(); *guard = sas; - (contents, guard.is_done()) + (contents, guard.is_done(), state) }; let mac_requests = contents @@ -339,10 +487,17 @@ impl Sas { VerificationResult::Cancel(c) => { Ok((self.cancel_with_code(c).into_iter().collect(), None)) } - VerificationResult::Ok => Ok((mac_requests, None)), - VerificationResult::SignatureUpload(r) => Ok((mac_requests, Some(r))), + VerificationResult::Ok => { + self.update_state(state); + Ok((mac_requests, None)) + } + VerificationResult::SignatureUpload(r) => { + self.update_state(state); + Ok((mac_requests, Some(r))) + } } } else { + self.update_state(state); Ok((mac_requests, None)) } } @@ -377,21 +532,32 @@ impl Sas { /// /// [`cancel()`]: #method.cancel pub fn cancel_with_code(&self, code: CancelCode) -> Option { - let mut guard = self.inner.lock().unwrap(); + let (content, state) = { + let mut guard = self.inner.lock().unwrap(); - if let Some(request) = &self.request_handle { - request.cancel_with_code(&code); - } - - let sas: InnerSas = (*guard).clone(); - let (sas, content) = sas.cancel(true, code); - *guard = sas; - content.map(|c| match c { - OutgoingContent::Room(room_id, content) => { - RoomMessageRequest { room_id, txn_id: TransactionId::new(), content }.into() + if let Some(request) = &self.request_handle { + request.cancel_with_code(&code); } - OutgoingContent::ToDevice(c) => self.content_to_request(c).into(), - }) + + let sas: InnerSas = (*guard).clone(); + let (sas, content) = sas.cancel(true, code); + let state: SasState = (&sas).into(); + *guard = sas; + + ( + content.map(|c| match c { + OutgoingContent::Room(room_id, content) => { + RoomMessageRequest { room_id, txn_id: TransactionId::new(), content }.into() + } + OutgoingContent::ToDevice(c) => self.content_to_request(c).into(), + }), + state, + ) + }; + + self.update_state(state); + + content } pub(crate) fn cancel_if_timed_out(&self) -> Option { @@ -451,15 +617,134 @@ impl Sas { self.inner.lock().unwrap().decimals() } + /// Listen for changes in the SAS verification process. + /// + /// The changes are presented as a stream of [`SasState`] values. + /// + /// This method can be used to react to changes in the state of the + /// verification process, or rather the method can be used to handle + /// each step of the verification process. + /// + /// # Flowchart + /// + /// The flow of the verification process is pictured bellow. Please note + /// that the process can be cancelled at each step of the process. + /// Either side can cancel the process. + /// + /// ```text + /// ┌───────┐ + /// │Started│ + /// └───┬───┘ + /// │ + /// ┌────⌄───┐ + /// │Accepted│ + /// └────┬───┘ + /// │ + /// ┌───────⌄──────┐ + /// │Keys Exchanged│ + /// └───────┬──────┘ + /// │ + /// ________⌄________ + /// ╱ ╲ ┌─────────┐ + /// ╱ Does the short ╲______│Cancelled│ + /// ╲ auth string match ╱ no └─────────┘ + /// ╲_________________╱ + /// │yes + /// │ + /// ┌────⌄────┐ + /// │Confirmed│ + /// └────┬────┘ + /// │ + /// ┌───⌄───┐ + /// │ Done │ + /// └───────┘ + /// ``` + /// # Example + /// + /// ```no_run + /// use futures::stream::{Stream, StreamExt}; + /// use matrix_sdk_crypto::{Sas, SasState}; + /// + /// # futures::executor::block_on(async { + /// # let sas: Sas = unimplemented!(); + /// + /// let mut stream = sas.changes(); + /// + /// while let Some(state) = stream.next().await { + /// match state { + /// SasState::KeysExchanged { emojis, decimals: _ } => { + /// let emojis = + /// emojis.expect("We only support emoji verification"); + /// println!("Do these emojis match {emojis:#?}"); + /// + /// // Ask the user to confirm or cancel here. + /// } + /// SasState::Done { .. } => { + /// let device = sas.other_device(); + /// + /// println!( + /// "Successfully verified device {} {} {:?}", + /// device.user_id(), + /// device.device_id(), + /// device.local_trust_state() + /// ); + /// + /// break; + /// } + /// SasState::Cancelled(cancel_info) => { + /// println!( + /// "The verification has been cancelled, reason: {}", + /// cancel_info.reason() + /// ); + /// break; + /// } + /// SasState::Started { .. } + /// | SasState::Accepted { .. } + /// | SasState::Confirmed => (), + /// } + /// } + /// # anyhow::Ok(()) }); + /// ``` + pub fn changes(&self) -> impl Stream { + self.state.signal_cloned().to_stream() + } + + /// Get the current state of the verification process. + pub fn state(&self) -> SasState { + self.state.lock_ref().to_owned() + } + + fn update_state(&self, new_state: SasState) { + let mut lock = self.state.lock_mut(); + + // Only update the state if it differs, this is important so clients don't end + // up printing the emoji twice. For example, the internal state might + // change into a MacReceived, because the other side already confirmed, + // but our side still needs to just show the emoji and wait for + // confirmation. + if *lock != new_state { + *lock = new_state; + } + } + pub(crate) fn receive_any_event( &self, sender: &UserId, content: &AnyVerificationContent<'_>, ) -> Option { - let mut guard = self.inner.lock().unwrap(); - let sas: InnerSas = (*guard).clone(); - let (sas, content) = sas.receive_any_event(sender, content); - *guard = sas; + let (content, state) = { + let mut guard = self.inner.lock().unwrap(); + let sas: InnerSas = (*guard).clone(); + let (sas, content) = sas.receive_any_event(sender, content); + + let state: SasState = (&sas).into(); + + *guard = sas; + + (content, state) + }; + + self.update_state(state); content } @@ -527,7 +812,7 @@ mod tests { event_enums::{AcceptContent, KeyContent, MacContent, OutgoingContent, StartContent}, VerificationStore, }, - ReadOnlyAccount, ReadOnlyDevice, + ReadOnlyAccount, ReadOnlyDevice, SasState, }; fn alice_id() -> &'static UserId { @@ -573,18 +858,25 @@ mod tests { let (alice, content) = Sas::start(identities, TransactionId::new(), true, None); + matches!(alice.state(), SasState::Started { .. }); + let flow_id = alice.flow_id().to_owned(); let content = StartContent::try_from(&content).unwrap(); let identities = bob_store.get_identities(alice_device).await.unwrap(); let bob = Sas::from_start_event(flow_id, &content, identities, None, false).unwrap(); + matches!(bob.state(), SasState::Started { .. }); + let request = bob.accept().unwrap(); + let content = OutgoingContent::try_from(request).unwrap(); let content = AcceptContent::try_from(&content).unwrap(); let content = alice.receive_any_event(bob.user_id(), &content.into()).unwrap(); + matches!(alice.state(), SasState::Accepted { .. }); + matches!(bob.state(), SasState::Accepted { .. }); assert!(!alice.can_be_presented()); assert!(!bob.can_be_presented()); @@ -592,22 +884,27 @@ mod tests { let content = bob.receive_any_event(alice.user_id(), &content.into()).unwrap(); assert!(bob.can_be_presented()); + matches!(bob.state(), SasState::KeysExchanged { .. }); let content = KeyContent::try_from(&content).unwrap(); alice.receive_any_event(bob.user_id(), &content.into()); + matches!(alice.state(), SasState::KeysExchanged { .. }); assert!(alice.can_be_presented()); assert_eq!(alice.emoji().unwrap(), bob.emoji().unwrap()); assert_eq!(alice.decimals().unwrap(), bob.decimals().unwrap()); let mut requests = alice.confirm().await.unwrap().0; + matches!(alice.state(), SasState::Confirmed); assert!(requests.len() == 1); let request = requests.pop().unwrap(); let content = OutgoingContent::try_from(request).unwrap(); let content = MacContent::try_from(&content).unwrap(); bob.receive_any_event(alice.user_id(), &content.into()); + matches!(bob.state(), SasState::KeysExchanged { .. }); let mut requests = bob.confirm().await.unwrap().0; + matches!(bob.state(), SasState::Confirmed); assert!(requests.len() == 1); let request = requests.pop().unwrap(); let content = OutgoingContent::try_from(request).unwrap(); @@ -616,5 +913,7 @@ mod tests { assert!(alice.verified_devices().unwrap().contains(alice.other_device())); assert!(bob.verified_devices().unwrap().contains(bob.other_device())); + matches!(alice.state(), SasState::Done { .. }); + matches!(bob.state(), SasState::Done { .. }); } } diff --git a/crates/matrix-sdk-crypto/src/verification/sas/sas_state.rs b/crates/matrix-sdk-crypto/src/verification/sas/sas_state.rs index ac72d0b35..5223c123d 100644 --- a/crates/matrix-sdk-crypto/src/verification/sas/sas_state.rs +++ b/crates/matrix-sdk-crypto/src/verification/sas/sas_state.rs @@ -34,7 +34,7 @@ use ruma::{ ToDeviceKeyVerificationStartEventContent, }, HashAlgorithm, KeyAgreementProtocol, MessageAuthenticationCode, Relation, - ShortAuthenticationString, VerificationMethod, + ShortAuthenticationString, }, AnyMessageLikeEventContent, AnyToDeviceEventContent, }, @@ -92,12 +92,16 @@ const MAX_EVENT_TIMEOUT: Duration = Duration::from_secs(60); /// Struct containing the protocols that were agreed to be used for the SAS /// flow. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct AcceptedProtocols { - pub method: VerificationMethod, + /// The key agreement protocol the device is choosing to use. pub key_agreement_protocol: KeyAgreementProtocol, + /// The hash method the device is choosing to use. pub hash: HashAlgorithm, + /// The message authentication code the device is choosing to use pub message_auth_code: MessageAuthenticationCode, + /// The SAS methods both devices involved in the verification process + /// understand. pub short_auth_string: Vec, } @@ -116,7 +120,6 @@ impl TryFrom for AcceptedProtocols { Err(CancelCode::UnknownMethod) } else { Ok(Self { - method: VerificationMethod::SasV1, hash: content.hash, key_agreement_protocol: content.key_agreement_protocol, message_auth_code: content.message_authentication_code, @@ -163,7 +166,6 @@ impl TryFrom<&SasV1Content> for AcceptedProtocols { } Ok(Self { - method: VerificationMethod::SasV1, hash: HashAlgorithm::Sha256, key_agreement_protocol: KeyAgreementProtocol::Curve25519HkdfSha256, message_auth_code: MessageAuthenticationCode::HkdfHmacSha256, @@ -177,7 +179,6 @@ impl TryFrom<&SasV1Content> for AcceptedProtocols { impl Default for AcceptedProtocols { fn default() -> Self { AcceptedProtocols { - method: VerificationMethod::SasV1, hash: HashAlgorithm::Sha256, key_agreement_protocol: KeyAgreementProtocol::Curve25519HkdfSha256, message_auth_code: MessageAuthenticationCode::HkdfHmacSha256, @@ -239,21 +240,22 @@ impl std::fmt::Debug for SasState { /// The initial SAS state. #[derive(Clone, Debug)] pub struct Created { - protocol_definitions: SasV1Content, + pub protocol_definitions: SasV1Content, } /// The initial SAS state if the other side started the SAS verification. #[derive(Clone, Debug)] pub struct Started { commitment: Base64, - pub accepted_protocols: Arc, + pub protocol_definitions: SasV1Content, + pub accepted_protocols: AcceptedProtocols, } /// The SAS state we're going to be in after the other side accepted our /// verification start event. #[derive(Clone, Debug)] pub struct Accepted { - pub accepted_protocols: Arc, + pub accepted_protocols: AcceptedProtocols, start_content: Arc, commitment: Base64, } @@ -263,7 +265,7 @@ pub struct Accepted { #[derive(Clone, Debug)] pub struct WeAccepted { we_started: bool, - pub accepted_protocols: Arc, + pub accepted_protocols: AcceptedProtocols, commitment: Base64, } @@ -275,7 +277,7 @@ pub struct WeAccepted { pub struct KeyReceived { sas: Arc>, we_started: bool, - pub accepted_protocols: Arc, + pub accepted_protocols: AcceptedProtocols, } /// The SAS state we're going to be in after the user has confirmed that the @@ -284,7 +286,7 @@ pub struct KeyReceived { #[derive(Clone, Debug)] pub struct Confirmed { sas: Arc>, - pub accepted_protocols: Arc, + pub accepted_protocols: AcceptedProtocols, } /// The SAS state we're going to be in after we receive a MAC event from the @@ -296,7 +298,7 @@ pub struct MacReceived { we_started: bool, verified_devices: Arc<[ReadOnlyDevice]>, verified_master_keys: Arc<[ReadOnlyUserIdentities]>, - pub accepted_protocols: Arc, + pub accepted_protocols: AcceptedProtocols, } /// The SAS state we're going to be in after we receive a MAC event in a DM. DMs @@ -485,7 +487,7 @@ impl SasState { state: Arc::new(Accepted { start_content, commitment: content.commitment.clone(), - accepted_protocols: accepted_protocols.into(), + accepted_protocols, }), }) } else { @@ -565,7 +567,8 @@ impl SasState { verification_flow_id: flow_id, state: Arc::new(Started { - accepted_protocols: accepted_protocols.into(), + protocol_definitions: method_content.to_owned(), + accepted_protocols, commitment, }), }) @@ -578,7 +581,7 @@ impl SasState { } pub fn into_we_accepted(self, methods: Vec) -> SasState { - let mut accepted_protocols = self.state.accepted_protocols.as_ref().to_owned(); + let mut accepted_protocols = self.state.accepted_protocols.to_owned(); accepted_protocols.short_auth_string = methods; // Decimal is required per spec. @@ -596,7 +599,7 @@ impl SasState { started_from_request: self.started_from_request, state: Arc::new(WeAccepted { we_started: false, - accepted_protocols: accepted_protocols.into(), + accepted_protocols, commitment: self.state.commitment.clone(), }), } @@ -658,7 +661,7 @@ impl SasState { state: Arc::new(Accepted { start_content, commitment: content.commitment.clone(), - accepted_protocols: accepted_protocols.into(), + accepted_protocols, }), }) } else { diff --git a/crates/matrix-sdk-indexeddb/Cargo.toml b/crates/matrix-sdk-indexeddb/Cargo.toml index b12a39be8..4b796b6b4 100644 --- a/crates/matrix-sdk-indexeddb/Cargo.toml +++ b/crates/matrix-sdk-indexeddb/Cargo.toml @@ -30,11 +30,11 @@ matrix-sdk-crypto = { version = "0.6.0", path = "../matrix-sdk-crypto", features matrix-sdk-store-encryption = { version = "0.2.0", path = "../matrix-sdk-store-encryption" } indexed_db_futures = "0.2.3" indexed_db_futures_nodejs = { version = "0.2.3", package = "indexed_db_futures", git = "https://github.com/Hywan/rust-indexed-db", branch = "feat-factory-nodejs", optional = true } -ruma = "0.7.0" +ruma = { workspace = true } serde = "1.0.136" serde_json = "1.0.79" thiserror = "1.0.30" -tracing = "0.1.34" +tracing = { workspace = true } wasm-bindgen = { version = "0.2.80", features = ["serde-serialize"] } web-sys = { version = "0.3.57", features = ["IdbKeyRange"] } diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store.rs b/crates/matrix-sdk-indexeddb/src/crypto_store.rs index b453a918b..b0d469359 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store.rs @@ -140,7 +140,7 @@ impl IndexeddbCryptoStore { prefix: &str, store_cipher: Option>, ) -> Result { - let name = format!("{:0}::matrix-sdk-crypto", prefix); + let name = format!("{prefix:0}::matrix-sdk-crypto"); // Open my_db v1 let mut db_req: OpenDbRequest = IdbDatabase::open_f64(&name, 1.1)?; @@ -232,7 +232,7 @@ impl IndexeddbCryptoStore { /// Open a new `IndexeddbCryptoStore` with given name and passphrase pub async fn open_with_passphrase(prefix: &str, passphrase: &str) -> Result { - let name = format!("{:0}::matrix-sdk-crypto-meta", prefix); + let name = format!("{prefix:0}::matrix-sdk-crypto-meta"); let mut db_req: OpenDbRequest = IdbDatabase::open_f64(&name, 1.0)?; db_req.set_on_upgrade_needed(Some(|evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { @@ -907,7 +907,7 @@ impl IndexeddbCryptoStore { if let Some(inner) = request { tx.object_store(KEYS::SECRET_REQUESTS_BY_INFO)? - .delete(&self.encode_key(KEYS::KEY_REQUEST, &inner.info.as_key()))?; + .delete(&self.encode_key(KEYS::KEY_REQUEST, inner.info.as_key()))?; } tx.object_store(KEYS::UNSENT_SECRET_REQUESTS)?.delete(&jskey)?; diff --git a/crates/matrix-sdk-indexeddb/src/safe_encode.rs b/crates/matrix-sdk-indexeddb/src/safe_encode.rs index 55bfe4df4..e26486088 100644 --- a/crates/matrix-sdk-indexeddb/src/safe_encode.rs +++ b/crates/matrix-sdk-indexeddb/src/safe_encode.rs @@ -61,7 +61,7 @@ pub trait SafeEncode { /// encode self into a JsValue, internally using `as_encoded_string` /// to escape the value of self, and append the given counter fn encode_with_counter(&self, i: usize) -> JsValue { - format!("{}{}{:016x}", self.as_encoded_string(), KEY_SEPARATOR, i).into() + format!("{}{KEY_SEPARATOR}{i:016x}", self.as_encoded_string()).into() } /// encode self into a JsValue, internally using `as_secure_string` diff --git a/crates/matrix-sdk-indexeddb/src/state_store.rs b/crates/matrix-sdk-indexeddb/src/state_store.rs index 1d0675d58..62a8890d0 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store.rs @@ -97,17 +97,7 @@ impl From for StoreError { match e { IndexeddbStateStoreError::Json(e) => StoreError::Json(e), IndexeddbStateStoreError::StoreError(e) => e, - IndexeddbStateStoreError::Encryption(e) => match e { - EncryptionError::Random(e) => StoreError::Encryption(e.to_string()), - EncryptionError::Serialization(e) => StoreError::Json(e), - EncryptionError::Encryption(e) => StoreError::Encryption(e.to_string()), - EncryptionError::Version(found, expected) => StoreError::Encryption(format!( - "Bad Database Encryption Version: expected {expected}, found {found}", - )), - EncryptionError::Length(found, expected) => StoreError::Encryption(format!( - "The database key an invalid length: expected {expected}, found {found}", - )), - }, + IndexeddbStateStoreError::Encryption(e) => StoreError::Encryption(e), _ => StoreError::backend(e), } } @@ -201,7 +191,7 @@ fn create_stores(db: &IdbDatabase) -> Result<(), JsValue> { async fn backup(source: &IdbDatabase, meta: &IdbDatabase) -> Result<()> { let now = JsDate::now(); - let backup_name = format!("backup-{}-{}", source.name(), now); + let backup_name = format!("backup-{}-{now}", source.name()); let mut db_req: OpenDbRequest = IdbDatabase::open_f64(&backup_name, source.version())?; db_req.set_on_upgrade_needed(Some(move |evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { @@ -267,7 +257,7 @@ impl IndexeddbStateStoreBuilder { .unwrap_or(MigrationConflictStrategy::BackupAndDrop); let name = self.name.clone().unwrap_or_else(|| "state".to_owned()); - let meta_name = format!("{}::{}", name, KEYS::INTERNAL_STATE); + let meta_name = format!("{name}::{}", KEYS::INTERNAL_STATE); let mut db_req: OpenDbRequest = IdbDatabase::open_f64(&meta_name, KEYS::CURRENT_META_DB_VERSION)?; @@ -1140,9 +1130,7 @@ impl IndexeddbStateStore { } async fn get_custom_value(&self, key: &[u8]) -> Result>> { - let jskey = &JsValue::from_str( - core::str::from_utf8(key).map_err(|e| StoreError::Codec(format!("{:}", e)))?, - ); + let jskey = &JsValue::from_str(core::str::from_utf8(key).map_err(StoreError::Codec)?); self.get_custom_value_for_js(jskey).await } @@ -1157,9 +1145,7 @@ impl IndexeddbStateStore { } async fn set_custom_value(&self, key: &[u8], value: Vec) -> Result>> { - let jskey = JsValue::from_str( - core::str::from_utf8(key).map_err(|e| StoreError::Codec(format!("{:}", e)))?, - ); + let jskey = JsValue::from_str(core::str::from_utf8(key).map_err(StoreError::Codec)?); let prev = self.get_custom_value_for_js(&jskey).await?; diff --git a/crates/matrix-sdk-qrcode/Cargo.toml b/crates/matrix-sdk-qrcode/Cargo.toml index e4b3dc580..50cc9dcb8 100644 --- a/crates/matrix-sdk-qrcode/Cargo.toml +++ b/crates/matrix-sdk-qrcode/Cargo.toml @@ -21,9 +21,7 @@ byteorder = "1.4.3" qrcode = { version = "0.12.0", default-features = false } ruma-common = "0.10.0" thiserror = "1.0.30" - -[dependencies.vodozemac] -version = "0.3.0" +vodozemac = { workspace = true } [dev-dependencies] image = "0.23.0" diff --git a/crates/matrix-sdk-sled/Cargo.toml b/crates/matrix-sdk-sled/Cargo.toml index 46031db3c..b9b3b689a 100644 --- a/crates/matrix-sdk-sled/Cargo.toml +++ b/crates/matrix-sdk-sled/Cargo.toml @@ -35,13 +35,13 @@ matrix-sdk-base = { version = "0.6.0", path = "../matrix-sdk-base", optional = t matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" } matrix-sdk-crypto = { version = "0.6.0", path = "../matrix-sdk-crypto", optional = true } matrix-sdk-store-encryption = { version = "0.2.0", path = "../matrix-sdk-store-encryption" } -ruma = "0.7.0" +ruma = { workspace = true } serde = "1.0.136" serde_json = "1.0.79" sled = "0.34.7" thiserror = "1.0.30" tokio = { version = "1.17.0", default-features = false, features = ["sync", "fs"] } -tracing = "0.1.34" +tracing = { workspace = true } [dev-dependencies] glob = "0.3.0" diff --git a/crates/matrix-sdk-sled/src/crypto_store.rs b/crates/matrix-sdk-sled/src/crypto_store.rs index 7beceea3d..1c244154c 100644 --- a/crates/matrix-sdk-sled/src/crypto_store.rs +++ b/crates/matrix-sdk-sled/src/crypto_store.rs @@ -792,7 +792,7 @@ impl CryptoStore for SledCryptoStore { let key = self.encode_key(INBOUND_GROUP_TABLE_NAME, (room_id, session_id)); let pickle = self .inbound_group_sessions - .get(&key) + .get(key) .map_err(CryptoStoreError::backend)? .map(|p| self.deserialize_value(&p)); diff --git a/crates/matrix-sdk-sled/src/state_store.rs b/crates/matrix-sdk-sled/src/state_store.rs index 677c20c79..46ad8cff5 100644 --- a/crates/matrix-sdk-sled/src/state_store.rs +++ b/crates/matrix-sdk-sled/src/state_store.rs @@ -104,25 +104,14 @@ impl From> for SledStoreError { } } -#[allow(clippy::from_over_into)] -impl Into for SledStoreError { - fn into(self) -> StoreError { - match self { +impl From for StoreError { + fn from(err: SledStoreError) -> StoreError { + match err { SledStoreError::Json(e) => StoreError::Json(e), SledStoreError::Identifier(e) => StoreError::Identifier(e), - SledStoreError::Encryption(e) => match e { - KeyEncryptionError::Random(e) => StoreError::Encryption(e.to_string()), - KeyEncryptionError::Serialization(e) => StoreError::Json(e), - KeyEncryptionError::Encryption(e) => StoreError::Encryption(e.to_string()), - KeyEncryptionError::Version(found, expected) => StoreError::Encryption(format!( - "Bad Database Encryption Version: expected {expected}, found {found}", - )), - KeyEncryptionError::Length(found, expected) => StoreError::Encryption(format!( - "The database key an invalid length: expected {expected}, found {found}", - )), - }, + SledStoreError::Encryption(e) => StoreError::Encryption(e), SledStoreError::StoreError(e) => e, - _ => StoreError::backend(self), + _ => StoreError::backend(err), } } } @@ -701,7 +690,7 @@ impl SledStateStore { let make_room_version = |room_id| { self.room_info - .get(&self.encode_key(ROOM_INFO, room_id)) + .get(self.encode_key(ROOM_INFO, room_id)) .ok() .flatten() .map(|r| self.deserialize_value::(&r)) @@ -1588,7 +1577,7 @@ mod migration { if let Err(SledStoreError::MigrationConflict { .. }) = res { // all good } else { - panic!("Didn't raise the expected error: {:?}", res); + panic!("Didn't raise the expected error: {res:?}"); } assert_eq!(std::fs::read_dir(folder.path())?.count(), 1); Ok(()) diff --git a/crates/matrix-sdk-store-encryption/Cargo.toml b/crates/matrix-sdk-store-encryption/Cargo.toml index 53ea8a1b0..35a423218 100644 --- a/crates/matrix-sdk-store-encryption/Cargo.toml +++ b/crates/matrix-sdk-store-encryption/Cargo.toml @@ -25,7 +25,7 @@ serde = { version = "1.0.136", features = ["derive"] } serde_json = "1.0.79" sha2 = "0.10.2" thiserror = "1.0.30" -zeroize = { version = "1.3.0", features = ["zeroize_derive"] } +zeroize = { workspace = true, features = ["zeroize_derive"] } [dev-dependencies] anyhow = "1.0.57" diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 5ee456e48..5ec0ee230 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -37,11 +37,14 @@ markdown = ["ruma/markdown"] native-tls = ["reqwest/native-tls"] rustls-tls = ["reqwest/rustls-tls"] socks = ["reqwest/socks"] -sso-login = ["warp", "dep:rand", "dep:tokio-stream"] +sso-login = ["dep:hyper", "dep:rand", "dep:tokio-stream", "dep:tower"] appservice = ["ruma/appservice-api-s"] image-proc = ["dep:image"] image-rayon = ["image-proc", "image?/jpeg_rayon"] +experimental-room-preview = [] +experimental-timeline = [] + sliding-sync = [ "matrix-sdk-base/sliding-sync", "anyhow", @@ -70,21 +73,24 @@ futures-core = "0.3.21" futures-signals = { version = "0.3.30", default-features = false } futures-util = { version = "0.3.21", default-features = false } http = "0.2.6" +indexmap = "1.9.1" +hyper = { version = "0.14.20", features = ["http1", "http2", "server"], optional = true } matrix-sdk-base = { version = "0.6.0", path = "../matrix-sdk-base", default_features = false } matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" } matrix-sdk-indexeddb = { version = "0.2.0", path = "../matrix-sdk-indexeddb", default-features = false, optional = true } matrix-sdk-sled = { version = "0.2.0", path = "../matrix-sdk-sled", default-features = false, optional = true } mime = "0.3.16" rand = { version = "0.8.5", optional = true } -reqwest = { version = "0.11.10", default_features = false} +reqwest = { version = "0.11.10", default_features = false } +ruma = { workspace = true, features = ["compat", "rand", "unstable-msc2448", "unstable-msc2965"] } serde = "1.0.136" serde_json = "1.0.79" thiserror = "1.0.30" tokio-stream = { version = "0.1.8", features = ["net"], optional = true } -tracing = "0.1.34" +tower = { version = "0.4.13", features = ["make"], optional = true } +tracing = { workspace = true, features = ["attributes"] } url = "2.2.2" -warp = { version = "0.3.2", default-features = false, optional = true } -zeroize = "1.3.0" +zeroize = { workspace = true } [dependencies.image] version = "0.24.2" @@ -106,10 +112,6 @@ features = [ ] optional = true -[dependencies.ruma] -version = "0.7.0" -features = ["client-api-c", "compat", "rand", "unstable-msc2448", "unstable-msc2965"] - [target.'cfg(target_arch = "wasm32")'.dependencies] async-once-cell = "0.4.2" wasm-timer = "0.2.5" @@ -120,6 +122,7 @@ tokio = { version = "1.17.0", default-features = false, features = ["fs", "rt"] [dev-dependencies] anyhow = "1.0.57" +assert_matches = "1.5.0" dirs = "4.0.0" futures = { version = "0.3.21", default-features = false, features = ["executor"] } matches = "0.1.9" diff --git a/crates/matrix-sdk/README.md b/crates/matrix-sdk/README.md index 9da5eef15..af0d40f42 100644 --- a/crates/matrix-sdk/README.md +++ b/crates/matrix-sdk/README.md @@ -43,8 +43,8 @@ async fn main() -> anyhow::Result<()> { }); // Syncing is important to synchronize the client state with the server. - // This method will never return. - client.sync(SyncSettings::default()).await; + // This method will never return unless there is an error. + client.sync(SyncSettings::default()).await?; Ok(()) } diff --git a/crates/matrix-sdk/src/account.rs b/crates/matrix-sdk/src/account.rs index 8eb42b8cb..897366b05 100644 --- a/crates/matrix-sdk/src/account.rs +++ b/crates/matrix-sdk/src/account.rs @@ -79,7 +79,8 @@ impl Account { pub async fn get_display_name(&self) -> Result> { let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?; let request = get_display_name::v3::Request::new(user_id); - let response = self.client.send(request, None).await?; + let request_config = self.client.request_config().force_auth(); + let response = self.client.send(request, Some(request_config)).await?; Ok(response.displayname) } @@ -239,7 +240,8 @@ impl Account { pub async fn get_profile(&self) -> Result { let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?; let request = get_profile::v3::Request::new(user_id); - Ok(self.client.send(request, None).await?) + let request_config = self.client.request_config().force_auth(); + Ok(self.client.send(request, Some(request_config)).await?) } /// Change the password of the account. diff --git a/crates/matrix-sdk/src/client/builder.rs b/crates/matrix-sdk/src/client/builder.rs index d08ed6299..5b993702a 100644 --- a/crates/matrix-sdk/src/client/builder.rs +++ b/crates/matrix-sdk/src/client/builder.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::Arc; +use std::{fmt, sync::Arc}; #[cfg(target_arch = "wasm32")] use async_once_cell::OnceCell; @@ -82,7 +82,7 @@ use crate::{ pub struct ClientBuilder { homeserver_cfg: Option, http_cfg: Option, - store_config: StoreConfig, + store_config: BuilderStoreConfig, request_config: RequestConfig, respect_login_well_known: bool, appservice_mode: bool, @@ -95,7 +95,7 @@ impl ClientBuilder { Self { homeserver_cfg: None, http_cfg: None, - store_config: Default::default(), + store_config: BuilderStoreConfig::Custom(StoreConfig::default()), request_config: Default::default(), respect_login_well_known: true, appservice_mode: false, @@ -126,30 +126,36 @@ impl ClientBuilder { /// Set up the store configuration for a sled store. /// - /// This is a shorthand for + /// This is the same as /// .[store_config](Self::store_config)([matrix_sdk_sled]::[make_store_config](matrix_sdk_sled::make_store_config)(path, passphrase)?). + /// except it delegates the actual store config creation to when + /// `.build().await` is called. #[cfg(feature = "sled")] - pub async fn sled_store( - self, + pub fn sled_store( + mut self, path: impl AsRef, passphrase: Option<&str>, - ) -> Result { - let config = matrix_sdk_sled::make_store_config(path, passphrase).await?; - Ok(self.store_config(config)) + ) -> Self { + self.store_config = BuilderStoreConfig::Sled { + path: path.as_ref().to_owned(), + passphrase: passphrase.map(ToOwned::to_owned), + }; + self } /// Set up the store configuration for a IndexedDB store. /// - /// This is a shorthand for - /// .[store_config](Self::store_config)([matrix_sdk_indexeddb]::[make_store_config](matrix_sdk_indexeddb::make_store_config)(path, passphrase).await?). + /// This is the same as + /// .[store_config](Self::store_config)([matrix_sdk_indexeddb]::[make_store_config](matrix_sdk_indexeddb::make_store_config)(path, passphrase).await?), + /// except it delegates the actual store config creation to when + /// `.build().await` is called. #[cfg(feature = "indexeddb")] - pub async fn indexeddb_store( - self, - name: &str, - passphrase: Option<&str>, - ) -> Result { - let config = matrix_sdk_indexeddb::make_store_config(name, passphrase).await?; - Ok(self.store_config(config)) + pub fn indexeddb_store(mut self, name: &str, passphrase: Option<&str>) -> Self { + self.store_config = BuilderStoreConfig::IndexedDb { + name: name.to_owned(), + passphrase: passphrase.map(ToOwned::to_owned), + }; + self } /// Set up the store configuration. @@ -172,7 +178,7 @@ impl ClientBuilder { /// let client_builder = Client::builder().store_config(store_config); /// ``` pub fn store_config(mut self, store_config: StoreConfig) -> Self { - self.store_config = store_config; + self.store_config = BuilderStoreConfig::Custom(store_config); self } @@ -336,7 +342,20 @@ impl ClientBuilder { HttpConfig::Custom(c) => c, }; - let base_client = BaseClient::with_store_config(self.store_config); + #[allow(clippy::infallible_destructuring_match)] + let store_config = match self.store_config { + #[cfg(feature = "sled")] + BuilderStoreConfig::Sled { path, passphrase } => { + matrix_sdk_sled::make_store_config(&path, passphrase.as_deref()).await? + } + #[cfg(feature = "indexeddb")] + BuilderStoreConfig::IndexedDb { name, passphrase } => { + matrix_sdk_indexeddb::make_store_config(&name, passphrase.as_deref()).await? + } + BuilderStoreConfig::Custom(config) => config, + }; + + let base_client = BaseClient::with_store_config(store_config); let http_client = HttpClient::new(inner_http_client.clone(), self.request_config); let mut authentication_issuer: Option = None; @@ -439,6 +458,38 @@ impl Default for HttpConfig { } } +#[derive(Clone)] +enum BuilderStoreConfig { + #[cfg(feature = "sled")] + Sled { + path: std::path::PathBuf, + passphrase: Option, + }, + #[cfg(feature = "indexeddb")] + IndexedDb { + name: String, + passphrase: Option, + }, + Custom(StoreConfig), +} + +impl fmt::Debug for BuilderStoreConfig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + #[allow(clippy::infallible_destructuring_match)] + match self { + #[cfg(feature = "sled")] + Self::Sled { path, .. } => { + f.debug_struct("Sled").field("path", path).finish_non_exhaustive() + } + #[cfg(feature = "indexeddb")] + Self::IndexedDb { name, .. } => { + f.debug_struct("IndexedDb").field("name", name).finish_non_exhaustive() + } + Self::Custom(store_config) => f.debug_tuple("Custom").field(store_config).finish(), + } + } +} + /// Errors that can happen in [`ClientBuilder::build`]. #[derive(Debug, Error)] pub enum ClientBuildError { diff --git a/crates/matrix-sdk/src/client/login_builder.rs b/crates/matrix-sdk/src/client/login_builder.rs index d43114f90..0fbcb6924 100644 --- a/crates/matrix-sdk/src/client/login_builder.rs +++ b/crates/matrix-sdk/src/client/login_builder.rs @@ -263,17 +263,19 @@ where #[instrument(target = "matrix_sdk::client", name = "login", skip_all, fields(method = "sso"))] pub async fn send(self) -> Result { use std::{ - collections::HashMap, + convert::Infallible, io::{Error as IoError, ErrorKind as IoErrorKind}, ops::Range, sync::{Arc, Mutex}, }; + use http::{Method, StatusCode}; + use hyper::{server::conn::AddrIncoming, service::service_fn}; use rand::{thread_rng, Rng}; + use serde::Deserialize; use tokio::{net::TcpListener, sync::oneshot}; - use tokio_stream::wrappers::TcpListenerStream; + use tracing::debug; use url::Url; - use warp::Filter; /// The range of ports the SSO server will try to bind to randomly. /// @@ -302,14 +304,30 @@ where .unwrap_or("The Single Sign-On login process is complete. You can close this page now.") .to_owned(); - let route = warp::get().and(warp::query::>()).map( - move |p: HashMap| { - if let Some(data_tx) = data_tx_mutex.lock().unwrap().take() { - data_tx.send(p.get("loginToken").cloned()).unwrap(); - } - http::Response::builder().body(response.clone()) - }, - ); + #[derive(Deserialize)] + struct QueryParameters { + #[serde(rename = "loginToken")] + login_token: Option, + } + + let handle_request = move |request: http::Request<_>| { + if request.method() != Method::HEAD && request.method() != Method::GET { + return Err(StatusCode::METHOD_NOT_ALLOWED); + } + + if let Some(data_tx) = data_tx_mutex.lock().unwrap().take() { + let query_string = request.uri().query().unwrap_or(""); + let query: QueryParameters = ruma::serde::urlencoded::from_str(query_string) + .map_err(|_| { + debug!("Failed to deserialize query parameters"); + StatusCode::BAD_REQUEST + })?; + + data_tx.send(query.login_token).unwrap(); + } + + Ok(http::Response::new(response.clone())) + }; let listener = { if redirect_url.port().expect("The redirect URL doesn't include a port") == 0 { @@ -338,12 +356,24 @@ where } }; - let server = warp::serve(route).serve_incoming_with_graceful_shutdown( - TcpListenerStream::new(listener), - async { + let incoming = AddrIncoming::from_listener(listener).unwrap(); + let server = hyper::Server::builder(incoming) + .serve(tower::make::Shared::new(service_fn(move |request| { + let handle_request = handle_request.clone(); + async move { + match handle_request(request) { + Ok(res) => Ok::<_, Infallible>(res.map(hyper::Body::from)), + Err(status_code) => { + let mut res = http::Response::new(hyper::Body::default()); + *res.status_mut() = status_code; + Ok(res) + } + } + } + }))) + .with_graceful_shutdown(async { signal_rx.await.ok(); - }, - ); + }); tokio::spawn(server); diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index ec9fb18dd..f75308dbd 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -58,7 +58,7 @@ use ruma::{ sync::sync_events, uiaa::{AuthData, UserIdentifier}, }, - error::{FromHttpResponseError, ServerError}, + error::FromHttpResponseError, MatrixVersion, OutgoingRequest, SendAccessToken, }, assign, DeviceId, OwnedDeviceId, OwnedRoomId, OwnedServerName, RoomAliasId, RoomId, @@ -532,7 +532,12 @@ impl Client { /// context argument types are only available for a subset of event types: /// /// * [`Room`][room::Room] is only available for room-specific events, i.e. - /// not for events like global account data events or presence events + /// not for events like global account data events or presence events. + /// + /// You can provide custom context via + /// [`add_event_handler_context`](Client::add_event_handler_context) and + /// then use [`Ctx`](crate::event_handler::Ctx) to extract the context + /// into the event handler. /// /// [`EventHandlerContext`]: crate::event_handler::EventHandlerContext /// @@ -544,6 +549,7 @@ impl Client { /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); /// use matrix_sdk::{ /// deserialized_responses::EncryptionInfo, + /// event_handler::Ctx, /// room::Room, /// ruma::{ /// events::{ @@ -588,6 +594,16 @@ impl Client { /// }); /// client.remove_event_handler(handle); /// + /// // Registering custom event handler context: + /// #[derive(Debug, Clone)] // The context will be cloned for event handler. + /// struct MyContext { + /// number: usize, + /// } + /// client.add_event_handler_context(MyContext { number: 5 }); + /// client.add_event_handler(|ev: SyncRoomMessageEvent, context: Ctx| async move { + /// // Use the context + /// }); + /// /// // Custom events work exactly the same way, you just need to declare /// // the content struct and use the EventContent derive macro on it. /// #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] @@ -1338,13 +1354,11 @@ impl Client { Ok(Some(res)) } Err(error) => { - *guard = if let HttpError::Api(FromHttpResponseError::Server( - ServerError::Known(RumaApiError::ClientApi(api_error)), - )) = &error - { - Err(RefreshTokenError::ClientApi(api_error.to_owned())) - } else { - Err(RefreshTokenError::UnableToRefreshToken) + *guard = match error.as_ruma_api_error() { + Some(RumaApiError::ClientApi(api_error)) => { + Err(RefreshTokenError::ClientApi(api_error.to_owned())) + } + _ => Err(RefreshTokenError::UnableToRefreshToken), }; Err(error) @@ -1688,9 +1702,9 @@ impl Client { // If this is an `M_UNKNOWN_TOKEN` error and refresh token handling is active, // try to refresh the token and retry the request. if self.inner.handle_refresh_tokens { - if let Err(HttpError::Api(FromHttpResponseError::Server(ServerError::Known( - RumaApiError::ClientApi(error), - )))) = &res + // FIXME: Use if-let chain once available + if let Err(Some(RumaApiError::ClientApi(error))) = + res.as_ref().map_err(HttpError::as_ruma_api_error) { if matches!(error.kind, ErrorKind::UnknownToken { .. }) { let refresh_res = self.refresh_access_token().await; @@ -1733,9 +1747,9 @@ impl Client { // If this is an `M_UNKNOWN_TOKEN` error and refresh token handling is active, // try to refresh the token and retry the request. if self.inner.handle_refresh_tokens { - if let Err(HttpError::Api(FromHttpResponseError::Server(ServerError::Known( - RumaApiError::ClientApi(error), - )))) = &res + // FIXME: Use if-let chain once available + if let Err(Some(RumaApiError::ClientApi(error))) = + res.as_ref().map_err(HttpError::as_ruma_api_error) { if matches!(error.kind, ErrorKind::UnknownToken { .. }) { let refresh_res = self.refresh_access_token().await; diff --git a/crates/matrix-sdk/src/config/sync.rs b/crates/matrix-sdk/src/config/sync.rs index e66ed3dc0..a848adfa8 100644 --- a/crates/matrix-sdk/src/config/sync.rs +++ b/crates/matrix-sdk/src/config/sync.rs @@ -48,7 +48,6 @@ impl<'a> fmt::Debug for SyncSettings<'a> { opt_field!(filter); opt_field!(timeout); - opt_field!(token); s.field("full_state", &self.full_state).finish() } diff --git a/crates/matrix-sdk/src/encryption/verification/mod.rs b/crates/matrix-sdk/src/encryption/verification/mod.rs index 8209ef63c..6de95f3bf 100644 --- a/crates/matrix-sdk/src/encryption/verification/mod.rs +++ b/crates/matrix-sdk/src/encryption/verification/mod.rs @@ -35,7 +35,10 @@ mod qrcode; mod requests; mod sas; -pub use matrix_sdk_base::crypto::{format_emojis, AcceptSettings, CancelInfo, Emoji}; +pub use matrix_sdk_base::crypto::{ + format_emojis, AcceptSettings, AcceptedProtocols, CancelInfo, Emoji, EmojiShortAuthString, + SasState, +}; #[cfg(feature = "qrcode")] pub use matrix_sdk_base::crypto::{ matrix_sdk_qrcode::{DecodingError, EncodingError, QrVerificationData}, diff --git a/crates/matrix-sdk/src/encryption/verification/sas.rs b/crates/matrix-sdk/src/encryption/verification/sas.rs index 9618daed8..0df131760 100644 --- a/crates/matrix-sdk/src/encryption/verification/sas.rs +++ b/crates/matrix-sdk/src/encryption/verification/sas.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use matrix_sdk_base::crypto::{AcceptSettings, CancelInfo, Emoji, ReadOnlyDevice, Sas as BaseSas}; +use futures_core::Stream; +use matrix_sdk_base::crypto::{ + AcceptSettings, CancelInfo, Emoji, ReadOnlyDevice, Sas as BaseSas, SasState, +}; use ruma::{events::key::verification::cancel::CancelCode, UserId}; use crate::{error::Result, Client}; @@ -217,4 +220,110 @@ impl SasVerification { pub fn other_user_id(&self) -> &UserId { self.inner.other_user_id() } + + /// Listen for changes in the SAS verification process. + /// + /// The changes are presented as a stream of [`SasState`] values. + /// + /// This method can be used to react to changes in the state of the + /// verification process, or rather the method can be used to handle + /// each step of the verification process. + /// + /// # Flowchart + /// + /// The flow of the verification process is pictured bellow. Please note + /// that the process can be cancelled at each step of the process. + /// Either side can cancel the process. + /// + /// ```text + /// ┌───────┐ + /// │Started│ + /// └───┬───┘ + /// │ + /// ┌────⌄───┐ + /// │Accepted│ + /// └────┬───┘ + /// │ + /// ┌───────⌄──────┐ + /// │Keys Exchanged│ + /// └───────┬──────┘ + /// │ + /// ________⌄________ + /// ╱ ╲ ┌─────────┐ + /// ╱ Does the short ╲______│Cancelled│ + /// ╲ auth string match ╱ no └─────────┘ + /// ╲_________________╱ + /// │yes + /// │ + /// ┌────⌄────┐ + /// │Confirmed│ + /// └────┬────┘ + /// │ + /// ┌───⌄───┐ + /// │ Done │ + /// └───────┘ + /// ``` + /// # Example + /// + /// ```no_run + /// use futures::stream::{Stream, StreamExt}; + /// use matrix_sdk::encryption::verification::{SasState, SasVerification}; + /// + /// # futures::executor::block_on(async { + /// # let sas: SasVerification = unimplemented!(); + /// # let user_confirmed = false; + /// + /// let mut stream = sas.changes(); + /// + /// while let Some(state) = stream.next().await { + /// match state { + /// SasState::KeysExchanged { emojis, decimals: _ } => { + /// let emojis = + /// emojis.expect("We only support emoji verification"); + /// println!("Do these emojis match {emojis:#?}"); + /// + /// // Ask the user to confirm or cancel here. + /// if user_confirmed { + /// sas.confirm().await?; + /// } else { + /// sas.cancel().await?; + /// } + /// } + /// SasState::Done { .. } => { + /// let device = sas.other_device(); + /// + /// println!( + /// "Successfully verified device {} {} {:?}", + /// device.user_id(), + /// device.device_id(), + /// device.local_trust_state() + /// ); + /// + /// break; + /// } + /// SasState::Cancelled(cancel_info) => { + /// println!( + /// "The verification has been cancelled, reason: {}", + /// cancel_info.reason() + /// ); + /// break; + /// } + /// SasState::Started { .. } + /// | SasState::Accepted { .. } + /// | SasState::Confirmed => (), + /// } + /// } + /// # anyhow::Ok(()) }); + /// ``` + pub fn changes(&self) -> impl Stream { + self.inner.changes() + } + + /// Get the current state the verification process is in. + /// + /// To listen to changes to the [`SasState`] use the + /// [`SasVerification::changes`] method. + pub fn state(&self) -> SasState { + self.inner.state() + } } diff --git a/crates/matrix-sdk/src/error.rs b/crates/matrix-sdk/src/error.rs index 754dbb19e..e1e6c565d 100644 --- a/crates/matrix-sdk/src/error.rs +++ b/crates/matrix-sdk/src/error.rs @@ -27,7 +27,7 @@ use matrix_sdk_base::{Error as SdkBaseError, StoreError}; use reqwest::Error as ReqwestError; use ruma::{ api::{ - client::uiaa::{UiaaInfo, UiaaResponse as UiaaError}, + client::uiaa::{UiaaInfo, UiaaResponse}, error::{FromHttpResponseError, IntoHttpError, ServerError}, }, events::tag::InvalidUserTagName, @@ -51,6 +51,15 @@ pub enum RumaApiError { #[error(transparent)] ClientApi(ruma::api::client::Error), + /// A user-interactive authentication API error. + /// + /// When registering or authenticating, the Matrix server can send a + /// `UiaaResponse` as the error type, this is a User-Interactive + /// Authentication API response. This represents an error with + /// information about how to authenticate the user. + #[error(transparent)] + Uiaa(UiaaResponse), + /// Another API response error. #[error(transparent)] Other(ruma::api::error::MatrixError), @@ -81,15 +90,6 @@ pub enum HttpError { #[error(transparent)] IntoHttp(#[from] IntoHttpError), - /// An error occurred while authenticating. - /// - /// When registering or authenticating the Matrix server can send a - /// `UiaaResponse` as the error type, this is a User-Interactive - /// Authentication API response. This represents an error with - /// information about how to authenticate the user. - #[error(transparent)] - UiaaError(#[from] FromHttpResponseError), - /// The server returned a status code that should be retried. #[error("Server returned an error {0}")] Server(StatusCode), @@ -103,6 +103,18 @@ pub enum HttpError { RefreshToken(#[from] RefreshTokenError), } +impl HttpError { + /// If `self` is `Api(Server(Known(e)))`, returns `Some(e)`. + /// + /// Otherwise, returns `None`. + pub fn as_ruma_api_error(&self) -> Option<&RumaApiError> { + match self { + Self::Api(FromHttpResponseError::Server(ServerError::Known(e))) => Some(e), + _ => None, + } + } +} + /// Internal representation of errors. #[derive(Error, Debug)] #[non_exhaustive] @@ -188,6 +200,18 @@ pub enum Error { UnknownError(Box), } +impl Error { + /// If `self` is `Http(Api(Server(Known(e))))`, returns `Some(e)`. + /// + /// Otherwise, returns `None`. + pub fn as_ruma_api_error(&self) -> Option<&RumaApiError> { + match self { + Error::Http(e) => e.as_ruma_api_error(), + _ => None, + } + } +} + /// Error for the room key importing functionality. #[cfg(feature = "e2e-encryption")] #[derive(Error, Debug)] @@ -229,13 +253,9 @@ impl HttpError { /// This method is an convenience method to get to the info the server /// returned on the first, failed request. pub fn uiaa_response(&self) -> Option<&UiaaInfo> { - if let HttpError::UiaaError(FromHttpResponseError::Server(ServerError::Known( - UiaaError::AuthResponse(i), - ))) = self - { - Some(i) - } else { - None + match self.as_ruma_api_error() { + Some(RumaApiError::Uiaa(UiaaResponse::AuthResponse(i))) => Some(i), + _ => None, } } } @@ -253,13 +273,9 @@ impl Error { /// This method is an convenience method to get to the info the server /// returned on the first, failed request. pub fn uiaa_response(&self) -> Option<&UiaaInfo> { - if let Error::Http(HttpError::UiaaError(FromHttpResponseError::Server( - ServerError::Known(UiaaError::AuthResponse(i)), - ))) = self - { - Some(i) - } else { - None + match self.as_ruma_api_error() { + Some(RumaApiError::Uiaa(UiaaResponse::AuthResponse(i))) => Some(i), + _ => None, } } } @@ -270,6 +286,12 @@ impl From> for HttpError { } } +impl From> for HttpError { + fn from(err: FromHttpResponseError) -> Self { + Self::Api(err.map(|e| e.map(RumaApiError::Uiaa))) + } +} + impl From> for HttpError { fn from(err: FromHttpResponseError) -> Self { Self::Api(err.map(|e| e.map(RumaApiError::Other))) @@ -297,7 +319,7 @@ impl From for Error { _ => Self::UnknownError(anyhow::anyhow!(e).into()), #[cfg(all(not(feature = "eyre"), not(feature = "anyhow")))] _ => { - let e: Box = format!("{:?}", e).into(); + let e: Box = format!("{e:?}").into(); Self::UnknownError(e) } } diff --git a/crates/matrix-sdk/src/http_client.rs b/crates/matrix-sdk/src/http_client.rs index a4cd45a47..fd0ce54fb 100644 --- a/crates/matrix-sdk/src/http_client.rs +++ b/crates/matrix-sdk/src/http_client.rs @@ -105,7 +105,10 @@ impl HttpClient { HttpClient { inner, request_config } } - #[tracing::instrument(skip(self, request), fields(request_type = type_name::()))] + #[tracing::instrument( + skip(self, request, access_token), + fields(request_type = type_name::()), + )] pub async fn send( &self, request: Request, diff --git a/crates/matrix-sdk/src/room/common.rs b/crates/matrix-sdk/src/room/common.rs index 5cc0aa6fc..d9aeead01 100644 --- a/crates/matrix-sdk/src/room/common.rs +++ b/crates/matrix-sdk/src/room/common.rs @@ -37,6 +37,8 @@ use ruma::{ }; use serde::de::DeserializeOwned; +#[cfg(feature = "experimental-timeline")] +use super::timeline::Timeline; use crate::{ event_handler::{EventHandler, EventHandlerHandle, EventHandlerResult, SyncEvent}, media::{MediaFormat, MediaRequest}, @@ -251,6 +253,16 @@ impl Common { self.client.add_room_event_handler(self.room_id(), handler) } + /// Get a [`Timeline`] for this room. + /// + /// This offers a higher-level API than event handlers, in treating things + /// like edits and reactions as updates of existing items rather than new + /// independent events. + #[cfg(feature = "experimental-timeline")] + pub fn timeline(&self) -> Timeline { + Timeline::new(self) + } + /// Fetch the event with the given `EventId` in this room. pub async fn event(&self, event_id: &EventId) -> Result { let request = get_room_event::v3::Request::new(self.room_id(), event_id); diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 13905c733..633213bec 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -9,6 +9,8 @@ mod invited; mod joined; mod left; mod member; +#[cfg(feature = "experimental-timeline")] +pub mod timeline; pub use self::{ common::{Common, Messages, MessagesOptions}, diff --git a/crates/matrix-sdk/src/room/timeline/event_handler.rs b/crates/matrix-sdk/src/room/timeline/event_handler.rs new file mode 100644 index 000000000..f7ced1e72 --- /dev/null +++ b/crates/matrix-sdk/src/room/timeline/event_handler.rs @@ -0,0 +1,488 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::Arc; + +use indexmap::map::Entry; +use matrix_sdk_base::deserialized_responses::EncryptionInfo; +use ruma::{ + events::{ + reaction::ReactionEventContent, + room::{ + message::{Relation, Replacement, RoomMessageEventContent}, + redaction::{ + OriginalSyncRoomRedactionEvent, RoomRedactionEventContent, SyncRoomRedactionEvent, + }, + }, + AnyMessageLikeEventContent, AnyStateEventContent, AnySyncMessageLikeEvent, + AnySyncTimelineEvent, Relations, + }, + serde::Raw, + uint, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId, + UserId, +}; +use tracing::{debug, error, info, warn}; + +use super::{ + event_item::{BundledReactions, TimelineDetails}, + find_event, EventTimelineItem, Message, TimelineInner, TimelineItem, TimelineItemContent, + TimelineKey, +}; + +impl TimelineInner { + pub(super) fn handle_live_event( + &self, + raw: Raw, + encryption_info: Option, + own_user_id: &UserId, + ) { + self.handle_remote_event(raw, encryption_info, own_user_id, TimelineItemPosition::End) + } + + pub(super) fn handle_local_event( + &self, + txn_id: OwnedTransactionId, + content: AnyMessageLikeEventContent, + own_user_id: &UserId, + ) { + let meta = TimelineEventMetadata { + sender: own_user_id.to_owned(), + origin_server_ts: None, + raw_event: None, + is_own_event: true, + relations: None, + // FIXME: Should we supply something here for encrypted rooms? + encryption_info: None, + }; + + let flow = Flow::Local { txn_id }; + let kind = TimelineEventKind::Message { content }; + + TimelineEventHandler::new(meta, flow, self).handle_event(kind) + } + + pub(super) fn handle_back_paginated_event( + &self, + raw: Raw, + encryption_info: Option, + own_user_id: &UserId, + ) { + self.handle_remote_event(raw, encryption_info, own_user_id, TimelineItemPosition::Start) + } + + fn handle_remote_event( + &self, + raw: Raw, + encryption_info: Option, + own_user_id: &UserId, + position: TimelineItemPosition, + ) { + let event = match raw.deserialize() { + Ok(ev) => ev, + Err(_e) => { + // TODO: Add some sort of error timeline item + return; + } + }; + + let sender = event.sender().to_owned(); + let is_own_event = sender == own_user_id; + + let meta = TimelineEventMetadata { + raw_event: Some(raw), + sender, + origin_server_ts: Some(event.origin_server_ts()), + is_own_event, + relations: event.relations().cloned(), + encryption_info, + }; + let flow = Flow::Remote { + event_id: event.event_id().to_owned(), + txn_id: event.transaction_id().map(ToOwned::to_owned), + position, + }; + + TimelineEventHandler::new(meta, flow, self).handle_event(event.into()) + } +} + +enum Flow { + Local { + txn_id: OwnedTransactionId, + }, + Remote { + event_id: OwnedEventId, + txn_id: Option, + position: TimelineItemPosition, + }, +} + +impl Flow { + fn to_key(&self) -> TimelineKey { + match self { + Self::Remote { event_id, .. } => TimelineKey::EventId(event_id.to_owned()), + Self::Local { txn_id } => TimelineKey::TransactionId(txn_id.to_owned()), + } + } +} + +struct TimelineEventMetadata { + raw_event: Option>, + sender: OwnedUserId, + origin_server_ts: Option, + is_own_event: bool, + relations: Option, + encryption_info: Option, +} + +impl From for TimelineEventKind { + fn from(event: AnySyncTimelineEvent) -> Self { + match event { + AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomRedaction( + SyncRoomRedactionEvent::Original(OriginalSyncRoomRedactionEvent { + redacts, + content, + .. + }), + )) => Self::Redaction { redacts, content }, + AnySyncTimelineEvent::MessageLike(ev) => match ev.original_content() { + Some(content) => Self::Message { content }, + None => Self::RedactedMessage, + }, + AnySyncTimelineEvent::State(ev) => match ev.original_content() { + Some(_content) => Self::State { _content }, + None => Self::RedactedState, + }, + } + } +} + +#[derive(Clone)] +enum TimelineEventKind { + Message { content: AnyMessageLikeEventContent }, + RedactedMessage, + Redaction { redacts: OwnedEventId, content: RoomRedactionEventContent }, + // FIXME: Split further for state keys of different type + State { _content: AnyStateEventContent }, + RedactedState, // AnyRedactedStateEventContent +} + +enum TimelineItemPosition { + Start, + End, +} + +// Bundles together a few things that are needed throughout the different stages +// of handling an event (figuring out whether it should update an existing +// timeline item, transforming that item or creating a new one, updating the +// reactive Vec). +struct TimelineEventHandler<'a> { + meta: TimelineEventMetadata, + flow: Flow, + timeline: &'a TimelineInner, + event_added: bool, +} + +impl<'a> TimelineEventHandler<'a> { + fn new(meta: TimelineEventMetadata, flow: Flow, timeline: &'a TimelineInner) -> Self { + Self { meta, flow, timeline, event_added: false } + } + + fn handle_event(mut self, event_kind: TimelineEventKind) { + match event_kind { + TimelineEventKind::Message { content } => match content { + AnyMessageLikeEventContent::Reaction(c) => self.handle_reaction(c), + AnyMessageLikeEventContent::RoomMessage(c) => self.handle_room_message(c), + // TODO + _ => {} + }, + TimelineEventKind::RedactedMessage => { + self.add(NewEventTimelineItem::redacted_message()); + } + TimelineEventKind::Redaction { redacts, content } => { + self.handle_redaction(redacts, content) + } + // TODO: State events + _ => {} + } + + if !self.event_added { + // TODO: Add event as raw + } + } + + fn handle_room_message(&mut self, content: RoomMessageEventContent) { + match content.relates_to { + Some(Relation::Replacement(re)) => { + self.handle_room_message_edit(re); + } + _ => { + self.add(NewEventTimelineItem::message(content, self.meta.relations.clone())); + } + } + } + + fn handle_room_message_edit(&mut self, replacement: Replacement) { + let event_id = &replacement.event_id; + + self.maybe_update_timeline_item(event_id, "edit", |item| { + if self.meta.sender != item.sender() { + info!( + %event_id, original_sender = %item.sender(), edit_sender = %self.meta.sender, + "Event tries to edit another user's timeline item, discarding" + ); + return None; + } + + let msg = match &item.content { + TimelineItemContent::Message(msg) => msg, + TimelineItemContent::RedactedMessage => { + info!( + %event_id, + "Event tries to edit a non-editable timeline item, discarding" + ); + return None; + } + }; + + let content = TimelineItemContent::Message(Message { + msgtype: replacement.new_content.msgtype, + in_reply_to: msg.in_reply_to.clone(), + edited: true, + }); + + Some(item.with_content(content)) + }); + } + + // Redacted reaction events are no-ops so don't need to be handled + fn handle_reaction(&mut self, c: ReactionEventContent) { + let event_id: &EventId = &c.relates_to.event_id; + + // This lock should never be contended, same as the timeline item lock. + // If this is ever run in parallel for some reason though, make sure the + // reaction lock is held for the entire time of the timeline items being + // locked so these two things can't get out of sync. + let mut lock = self.timeline.reaction_map.lock().unwrap(); + + let did_update = self.maybe_update_timeline_item(event_id, "reaction", |item| { + // Handling of reactions on redacted events is an open question. + // For now, ignore reactions on redacted events like Element does. + if let TimelineItemContent::RedactedMessage = item.content { + debug!(%event_id, "Ignoring reaction on redacted event"); + None + } else { + let mut reactions = item.reactions.clone(); + let reaction_details = + reactions.bundled.entry(c.relates_to.key.clone()).or_default(); + + reaction_details.count += uint!(1); + if let TimelineDetails::Ready(senders) = &mut reaction_details.senders { + senders.push(self.meta.sender.clone()); + } + + Some(item.with_reactions(reactions)) + } + }); + + if did_update { + lock.insert(self.flow.to_key(), (self.meta.sender.clone(), c.relates_to)); + } + } + + // Redacted redactions are no-ops (unfortunately) + fn handle_redaction(&mut self, redacts: OwnedEventId, _content: RoomRedactionEventContent) { + let mut did_update = false; + + // Don't release this lock until after update_timeline_item. + // See first comment in handle_reaction for why. + let mut lock = self.timeline.reaction_map.lock().unwrap(); + if let Some((sender, rel)) = lock.remove(&TimelineKey::EventId(redacts.clone())) { + did_update = self.maybe_update_timeline_item(&rel.event_id, "redaction", |item| { + let mut reactions = item.reactions.clone(); + + let mut details_entry = match reactions.bundled.entry(rel.key) { + Entry::Occupied(o) => o, + Entry::Vacant(_) => return None, + }; + let details = details_entry.get_mut(); + details.count -= uint!(1); + + if details.count == uint!(0) { + details_entry.remove(); + return Some(item.with_reactions(reactions)); + } + + let senders = match &mut details.senders { + TimelineDetails::Ready(senders) => senders, + _ => { + // FIXME: We probably want to support this somehow in + // the future, but right now it's not possible. + warn!( + "inconsistent state: shouldn't have a reaction_map entry for a \ + timeline item with incomplete reactions" + ); + return None; + } + }; + + if let Some(idx) = senders.iter().position(|s| *s == sender) { + senders.remove(idx); + } else { + error!( + "inconsistent state: sender from reaction_map not in reaction sender list \ + of timeline item" + ); + return None; + } + + if u64::from(details.count) != senders.len() as u64 { + error!("inconsistent state: reaction count differs from number of senders"); + // Can't make things worse by updating the item, so no early + // return here. + } + + Some(item.with_reactions(reactions)) + }); + + if !did_update { + warn!("reaction_map out of sync with timeline items"); + } + } + + // Even if the event being redacted is a reaction (found in + // `reaction_map`), it can still be present in the timeline items + // directly with the raw event timeline feature (not yet implemented). + did_update |= self.update_timeline_item(&redacts, "redaction", |item| item.to_redacted()); + + if !did_update { + // We will want to know this when debugging redaction issues. + debug!(redaction_key = ?self.flow.to_key(), %redacts, "redaction affected no event"); + } + } + + fn add(&mut self, item: NewEventTimelineItem) { + self.event_added = true; + + let NewEventTimelineItem { content, reactions } = item; + let item = EventTimelineItem { + key: self.flow.to_key(), + event_id: None, + sender: self.meta.sender.to_owned(), + content, + reactions, + origin_server_ts: self.meta.origin_server_ts, + is_own: self.meta.is_own_event, + encryption_info: self.meta.encryption_info.clone(), + raw: self.meta.raw_event.clone(), + }; + + let item = Arc::new(TimelineItem::Event(item)); + let mut lock = self.timeline.items.lock_mut(); + match &self.flow { + Flow::Local { .. } + | Flow::Remote { position: TimelineItemPosition::End, txn_id: None, .. } => { + lock.push_cloned(item); + } + Flow::Remote { position: TimelineItemPosition::Start, txn_id: None, .. } => { + lock.insert_cloned(0, item); + } + Flow::Remote { txn_id: Some(txn_id), event_id, position } => { + if let Some((idx, _old_item)) = find_event(&lock, txn_id) { + // TODO: Check whether anything is different about the old and new item? + lock.set_cloned(idx, item); + } else { + debug!( + %txn_id, %event_id, + "Received event with transaction ID, but didn't find matching timeline item" + ); + + match position { + TimelineItemPosition::Start => lock.insert_cloned(0, item), + TimelineItemPosition::End => lock.push_cloned(item), + } + } + } + } + } + + /// Returns whether an update happened + fn maybe_update_timeline_item( + &self, + event_id: &EventId, + action: &str, + update: impl FnOnce(&EventTimelineItem) -> Option, + ) -> bool { + // No point in trying to update items with relations when back- + // paginating, the event the relation applies to can't be processed yet. + if matches!(self.flow, Flow::Remote { position: TimelineItemPosition::Start, .. }) { + return false; + } + + let mut lock = self.timeline.items.lock_mut(); + if let Some((idx, item)) = find_event(&lock, event_id) { + if let Some(new_item) = update(item) { + lock.set_cloned(idx, Arc::new(TimelineItem::Event(new_item))); + return true; + } + } else { + debug!(%event_id, "Timeline item not found, discarding {action}"); + } + + false + } + + /// Returns whether an update happened + fn update_timeline_item( + &self, + event_id: &EventId, + action: &str, + update: impl FnOnce(&EventTimelineItem) -> EventTimelineItem, + ) -> bool { + self.maybe_update_timeline_item(event_id, action, move |item| Some(update(item))) + } +} + +struct NewEventTimelineItem { + content: TimelineItemContent, + reactions: BundledReactions, +} + +impl NewEventTimelineItem { + // These constructors could also be `From` implementations, but that would + // allow users to call them directly, which should not be supported + pub(crate) fn message(c: RoomMessageEventContent, relations: Option) -> Self { + let edited = relations.as_ref().map_or(false, |r| r.replace.is_some()); + let content = TimelineItemContent::Message(Message { + msgtype: c.msgtype, + in_reply_to: c.relates_to.and_then(|rel| match rel { + Relation::Reply { in_reply_to } => Some(in_reply_to.event_id), + _ => None, + }), + edited, + }); + + let reactions = + relations.and_then(|r| r.annotation).map(BundledReactions::from).unwrap_or_default(); + + Self { content, reactions } + } + + pub(crate) fn redacted_message() -> Self { + Self { + content: TimelineItemContent::RedactedMessage, + reactions: BundledReactions::default(), + } + } +} diff --git a/crates/matrix-sdk/src/room/timeline/event_item.rs b/crates/matrix-sdk/src/room/timeline/event_item.rs new file mode 100644 index 000000000..c532aa7de --- /dev/null +++ b/crates/matrix-sdk/src/room/timeline/event_item.rs @@ -0,0 +1,353 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use indexmap::IndexMap; +use matrix_sdk_base::deserialized_responses::EncryptionInfo; +#[cfg(feature = "experimental-room-preview")] +use ruma::events::room::message::{OriginalSyncRoomMessageEvent, Relation}; +use ruma::{ + events::{ + relation::{AnnotationChunk, AnnotationType}, + room::message::MessageType, + AnySyncTimelineEvent, + }, + serde::Raw, + uint, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId, + TransactionId, UInt, UserId, +}; + +/// An item in the timeline that represents at least one event. +/// +/// There is always one main event that gives the `EventTimelineItem` its +/// identity (see [key](Self::key)) but in many cases, additional events like +/// reactions and edits are also part of the item. +#[derive(Clone, Debug)] +pub struct EventTimelineItem { + pub(super) key: TimelineKey, + // If this item is a local echo that has been acknowledged by the server + // but not remote-echoed yet, this field holds the event ID from the send + // response. + pub(super) event_id: Option, + pub(super) sender: OwnedUserId, + pub(super) content: TimelineItemContent, + pub(super) reactions: BundledReactions, + pub(super) origin_server_ts: Option, + pub(super) is_own: bool, + pub(super) encryption_info: Option, + // FIXME: Expose the raw JSON of aggregated events somehow + pub(super) raw: Option>, +} + +macro_rules! build { + ( + $ty:ident { + $( $field:ident $(: $value:expr)?, )* + ..$this:ident( $($this_field:ident),* $(,)? ) + } + ) => { + $ty { + $( $field $(: $value)?, )* + $( $this_field: $this.$this_field.clone() ),* + } + } +} + +impl EventTimelineItem { + #[cfg(feature = "experimental-room-preview")] + #[doc(hidden)] // FIXME: Remove. Used for matrix-sdk-ffi temporarily. + pub fn _new(ev: OriginalSyncRoomMessageEvent, raw: Raw) -> Self { + let edited = ev.unsigned.relations.as_ref().map_or(false, |r| r.replace.is_some()); + let reactions = ev + .unsigned + .relations + .and_then(|r| r.annotation) + .map(BundledReactions::from) + .unwrap_or_default(); + + Self { + key: TimelineKey::EventId(ev.event_id), + event_id: None, + sender: ev.sender, + content: TimelineItemContent::Message(Message { + msgtype: ev.content.msgtype, + in_reply_to: ev.content.relates_to.and_then(|rel| match rel { + Relation::Reply { in_reply_to } => Some(in_reply_to.event_id), + _ => None, + }), + edited, + }), + reactions, + origin_server_ts: Some(ev.origin_server_ts), + is_own: false, // FIXME: Potentially wrong + encryption_info: None, // FIXME: Potentially wrong + raw: Some(raw), + } + } + + /// Get the [`TimelineKey`] of this item. + pub fn key(&self) -> &TimelineKey { + &self.key + } + + /// Get the event ID of this item. + /// + /// If this returns `Some(_)`, the event was successfully created by the + /// server. + /// + /// Even if the [`key()`](Self::key) of this timeline item holds a + /// transaction ID, this can be `Some(_)` as the event ID can be known not + /// just from the remote echo via `sync_events`, but also from the response + /// of the send request that created the event. + pub fn event_id(&self) -> Option<&EventId> { + match &self.key { + TimelineKey::TransactionId(_) => self.event_id.as_deref(), + TimelineKey::EventId(id) => Some(id), + } + } + + /// Get the sender of this item. + pub fn sender(&self) -> &UserId { + &self.sender + } + + /// Get the content of this item. + pub fn content(&self) -> &TimelineItemContent { + &self.content + } + + /// Get the reactions of this item. + pub fn reactions(&self) -> &IndexMap { + // FIXME: Find out the state of incomplete bundled reactions, adjust + // Ruma if necessary, return the whole BundledReactions field + &self.reactions.bundled + } + + /// Get the origin server timestamp of this item. + /// + /// Returns `None` if this event hasn't been echoed back by the server yet. + pub fn origin_server_ts(&self) -> Option { + self.origin_server_ts + } + + /// Whether this timeline item was sent by the logged-in user themselves. + pub fn is_own(&self) -> bool { + self.is_own + } + + /// Get the raw JSON representation of the initial event (the one that + /// caused this timeline item to be created). + /// + /// Returns `None` if this event hasn't been echoed back by the server yet. + pub fn raw(&self) -> Option<&Raw> { + self.raw.as_ref() + } + + pub(super) fn to_redacted(&self) -> Self { + build!(Self { + // FIXME: Change when we support state events + content: TimelineItemContent::RedactedMessage, + reactions: BundledReactions::default(), + ..self(key, event_id, sender, origin_server_ts, is_own, encryption_info, raw) + }) + } + + pub(super) fn with_event_id(&self, event_id: Option) -> Self { + build!(Self { + event_id, + ..self(key, sender, content, reactions, origin_server_ts, is_own, encryption_info, raw,) + }) + } + + #[rustfmt::skip] + pub(super) fn with_content(&self, content: TimelineItemContent) -> Self { + build!(Self { + content, + ..self( + key, event_id, sender, reactions, origin_server_ts, is_own, encryption_info, raw, + ) + }) + } + + #[rustfmt::skip] + pub(super) fn with_reactions(&self, reactions: BundledReactions) -> Self { + build!(Self { + reactions, + ..self( + key, event_id, sender, content, origin_server_ts, is_own, encryption_info, raw, + ) + }) + } +} + +/// A unique identifier for a timeline item. +/// +/// This identifier is used to find the item in the timeline in order to update +/// its state. +/// +/// When an event is created locally, the timeline reflects this with an item +/// that has a [`TransactionId`](Self::TransactionId) key. Once the server has +/// acknowledged the event and given it an ID, that item's key is replaced by +/// [`EventId`](Self::EventId) containing the new ID. +/// +/// When an event related to the original event whose ID is stored in a +/// [`TimelineKey`] is received, the key is left untouched, but other parts of +/// the timeline item may be updated. Thus, the current data model is only able +/// to handle relations that reference the initial event that resulted in a +/// timeline item being created, not other related events. At the time of +/// writing, there is no relation that is meant to refer to other events that +/// only exist for their relation (e.g. edits, replies). +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum TimelineKey { + /// Transaction ID, for an event that was created locally and hasn't been + /// acknowledged by the server yet. + TransactionId(OwnedTransactionId), + /// Event ID, for an event that is synced with the server. + EventId(OwnedEventId), +} + +impl PartialEq for &TransactionId { + fn eq(&self, key: &TimelineKey) -> bool { + matches!(key, TimelineKey::TransactionId(txn_id) if txn_id == self) + } +} + +impl PartialEq for &OwnedTransactionId { + fn eq(&self, key: &TimelineKey) -> bool { + matches!(key, TimelineKey::TransactionId(txn_id) if txn_id == *self) + } +} + +impl PartialEq for &EventId { + fn eq(&self, key: &TimelineKey) -> bool { + matches!(key, TimelineKey::EventId(event_id) if event_id == self) + } +} + +/// Some details of an [`EventTimelineItem`] that may require server requests +/// other than just the regular +/// [`sync_events`][ruma::api::client::sync::sync_events]. +#[derive(Clone, Debug)] +pub enum TimelineDetails { + /// The details are not available yet, and have not been request from the + /// server. + Unavailable, + + /// The details are not available yet, but have been requested. + Pending, + + /// The details are available. + Ready(T), +} + +/// The content of an [`EventTimelineItem`]. +#[derive(Clone, Debug)] +pub enum TimelineItemContent { + /// An `m.room.message` event or extensible event, including edits. + Message(Message), + + /// A redacted message. + RedactedMessage, +} + +/// An `m.room.message` event or extensible event, including edits. +#[derive(Clone, Debug)] +pub struct Message { + pub(super) msgtype: MessageType, + // TODO: Add everything required to display the replied-to event, plus a + // 'loading' state that is entered at first, until the user requests the + // reply to be loaded. + pub(super) in_reply_to: Option, + pub(super) edited: bool, +} + +impl Message { + /// Get the `msgtype`-specific data of this message. + pub fn msgtype(&self) -> &MessageType { + &self.msgtype + } + + /// Get the event ID of the event this message is replying to, if any. + pub fn in_reply_to(&self) -> Option<&EventId> { + self.in_reply_to.as_deref() + } + + /// Get the edit state of this message (has been edited: `true` / `false`). + pub fn is_edited(&self) -> bool { + self.edited + } +} + +#[derive(Clone, Debug)] +pub struct BundledReactions { + /// Whether all reactions are known, or some may be missing. + /// + /// If this is `false`, the remaining reactions can be fetched via **TODO**. + pub complete: bool, // FIXME: Unclear whether this is needed + + /// The reactions. + /// + /// Key: The reaction, usually an emoji.\ + /// Value: The count. + pub bundled: IndexMap, +} + +impl From for BundledReactions { + fn from(ann: AnnotationChunk) -> Self { + let bundled = ann + .chunk + .into_iter() + .filter_map(|a| { + (a.annotation_type == AnnotationType::Reaction).then(|| { + let details = + ReactionDetails { count: a.count, senders: TimelineDetails::Unavailable }; + (a.key, details) + }) + }) + .collect(); + + BundledReactions { bundled, complete: ann.next_batch.is_none() } + } +} + +impl Default for BundledReactions { + fn default() -> Self { + Self { complete: true, bundled: IndexMap::new() } + } +} + +/// The details of a group of reaction events on the same event with the same +/// key. +#[derive(Clone, Debug)] +pub struct ReactionDetails { + /// The amount of reactions with this key. + pub count: UInt, + + /// The senders of the reactions. + pub senders: TimelineDetails>, +} + +impl Default for ReactionDetails { + fn default() -> Self { + Self { count: uint!(0), senders: TimelineDetails::Ready(Vec::new()) } + } +} + +/// The result of a successful pagination request. +#[derive(Debug)] +// TODO: non-exhaustive breaks UniFFI bridge +//#[non_exhaustive] +pub struct PaginationOutcome { + /// Whether there's more messages to be paginated. + pub more_messages: bool, +} diff --git a/crates/matrix-sdk/src/room/timeline/mod.rs b/crates/matrix-sdk/src/room/timeline/mod.rs new file mode 100644 index 000000000..210466656 --- /dev/null +++ b/crates/matrix-sdk/src/room/timeline/mod.rs @@ -0,0 +1,241 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! A high-level view into a room's contents. +//! +//! See [`Timeline`] for details. + +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; + +use futures_core::Stream; +use futures_signals::signal_vec::{MutableVec, SignalVec, SignalVecExt, VecDiff}; +use matrix_sdk_base::deserialized_responses::EncryptionInfo; +use ruma::{ + assign, + events::{reaction::Relation as AnnotationRelation, AnyMessageLikeEventContent}, + OwnedEventId, OwnedUserId, TransactionId, UInt, +}; +use tracing::{debug, error, instrument}; + +use super::{Joined, Room}; +use crate::{ + event_handler::EventHandlerDropGuard, + room::{self, MessagesOptions}, + Result, +}; + +mod event_handler; +mod event_item; +mod virtual_item; + +pub use self::{ + event_item::{ + EventTimelineItem, Message, PaginationOutcome, ReactionDetails, TimelineDetails, + TimelineItemContent, TimelineKey, + }, + virtual_item::VirtualTimelineItem, +}; + +/// A high-level view into a regular¹ room's contents. +/// +/// ¹ This type is meant to be used in the context of rooms without a +/// `room_type`, that is rooms that are primarily used to exchange text +/// messages. +#[derive(Debug)] +pub struct Timeline { + inner: TimelineInner, + room: room::Common, + start_token: Mutex>, + _end_token: Mutex>, + _event_handler_guard: EventHandlerDropGuard, +} + +#[derive(Clone, Debug, Default)] +struct TimelineInner { + items: MutableVec>, + // Reaction event / txn ID => sender and reaction data + reaction_map: Arc>>, +} + +impl Timeline { + pub(super) fn new(room: &room::Common) -> Self { + let inner = TimelineInner::default(); + + let handle = room.add_event_handler({ + let inner = inner.clone(); + move |event, encryption_info: Option, room: Room| { + let inner = inner.clone(); + async move { + inner.handle_live_event(event, encryption_info, room.own_user_id()); + } + } + }); + let _event_handler_guard = room.client.event_handler_drop_guard(handle); + + Timeline { + inner, + room: room.clone(), + start_token: Mutex::new(None), + _end_token: Mutex::new(None), + _event_handler_guard, + } + } + + /// Add more events to the start of the timeline. + #[instrument(skip(self), fields(room_id = %self.room.room_id()))] + pub async fn paginate_backwards(&self, limit: UInt) -> Result { + let start = self.start_token.lock().unwrap().clone(); + let messages = self + .room + .messages(assign!(MessagesOptions::backward(), { + from: start.as_deref(), + limit, + })) + .await?; + + let outcome = PaginationOutcome { more_messages: messages.end.is_some() }; + *self.start_token.lock().unwrap() = messages.end; + + let own_user_id = self.room.own_user_id(); + for room_ev in messages.chunk { + self.inner.handle_back_paginated_event( + room_ev.event.cast(), + room_ev.encryption_info, + own_user_id, + ); + } + + Ok(outcome) + } + + /// Get a signal of the timeline's items. + /// + /// You can poll this signal to receive updates, the first of which will + /// be the full list of items currently available. + /// + /// See [`SignalVecExt`](futures_signals::signal_vec::SignalVecExt) for a + /// high-level API on top of [`SignalVec`]. + pub fn signal(&self) -> impl SignalVec> { + self.inner.items.signal_vec_cloned() + } + + /// Get a stream of timeline changes. + /// + /// This is a convenience shorthand for `timeline.signal().to_stream()`. + pub fn stream(&self) -> impl Stream>> { + self.signal().to_stream() + } + + /// Send a message to the room, and add it to the timeline as a local echo. + /// + /// For simplicity, this method doesn't currently allow custom message + /// types. + /// + /// If the encryption feature is enabled, this method will transparently + /// encrypt the room message if the room is encrypted. + /// + /// # Arguments + /// + /// * `content` - The content of the message event. + /// + /// * `txn_id` - A locally-unique ID describing a message transaction with + /// the homeserver. Unless you're doing something special, you can pass in + /// `None` which will create a suitable one for you automatically. + /// * On the sending side, this field is used for re-trying earlier + /// failed transactions. Subsequent messages *must never* re-use an + /// earlier transaction ID. + /// * On the receiving side, the field is used for recognizing our own + /// messages when they arrive down the sync: the server includes the + /// ID in the [`MessageLikeUnsigned`] field `transaction_id` of the + /// corresponding [`SyncMessageLikeEvent`], but only for the *sending* + /// device. Other devices will not see it. + /// + /// [`MessageLikeUnsigned`]: ruma::events::MessageLikeUnsigned + /// [`SyncMessageLikeEvent`]: ruma::events::SyncMessageLikeEvent + #[instrument(skip(self, content), fields(room_id = %self.room.room_id()))] + pub async fn send( + &self, + content: AnyMessageLikeEventContent, + txn_id: Option<&TransactionId>, + ) -> Result<()> { + let txn_id = txn_id.map_or_else(TransactionId::new, ToOwned::to_owned); + self.inner.handle_local_event(txn_id.clone(), content.clone(), self.room.own_user_id()); + + // If this room isn't actually in joined state, we'll get a server error. + // Not ideal, but works for now. + let room = Joined { inner: self.room.clone() }; + + let response = room.send(content, Some(&txn_id)).await?; + add_event_id(&self.inner, &txn_id, response.event_id); + + Ok(()) + } +} + +/// A single entry in timeline. +#[derive(Clone, Debug)] +#[allow(clippy::large_enum_variant)] +pub enum TimelineItem { + /// An event or aggregation of multiple events. + Event(EventTimelineItem), + /// An item that doesn't correspond to an event, for example the user's own + /// read marker. + Virtual(VirtualTimelineItem), +} + +impl TimelineItem { + /// Get the inner `EventTimelineItem`, if this is a `TimelineItem::Event`. + pub fn as_event(&self) -> Option<&EventTimelineItem> { + match self { + Self::Event(v) => Some(v), + _ => None, + } + } +} + +// FIXME: Put an upper bound on timeline size or add a separate map to look up +// the index of a timeline item by its key, to avoid large linear scans. +fn find_event( + lock: &[Arc], + key: impl PartialEq, +) -> Option<(usize, &EventTimelineItem)> { + lock.iter() + .enumerate() + .filter_map(|(idx, item)| Some((idx, item.as_event()?))) + .rfind(|(_, it)| key == it.key) +} + +fn add_event_id(items: &TimelineInner, txn_id: &TransactionId, event_id: OwnedEventId) { + let mut lock = items.items.lock_mut(); + if let Some((idx, item)) = find_event(&lock, txn_id) { + match &item.key { + TimelineKey::TransactionId(_) => { + lock.set_cloned( + idx, + Arc::new(TimelineItem::Event(item.with_event_id(Some(event_id)))), + ); + } + TimelineKey::EventId(ev_id) => { + if *ev_id != event_id { + error!("remote echo and send-event response disagree on the event ID"); + } + } + } + } else { + debug!(%txn_id, "Timeline item not found, can't mark as sent"); + } +} diff --git a/crates/matrix-sdk/src/room/timeline/virtual_item.rs b/crates/matrix-sdk/src/room/timeline/virtual_item.rs new file mode 100644 index 000000000..dedc18bf3 --- /dev/null +++ b/crates/matrix-sdk/src/room/timeline/virtual_item.rs @@ -0,0 +1,22 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// A [`TimelineItem`](super::TimelineItem) that doesn't correspond to an event. +#[derive(Clone, Debug)] +pub enum VirtualTimelineItem { + /// A divider between messages of two days. + DayDivider, + /// The user's own read marker. + ReadMarker, +} diff --git a/crates/matrix-sdk/src/sliding_sync.rs b/crates/matrix-sdk/src/sliding_sync.rs index ea2fc19cf..4ba3afe12 100644 --- a/crates/matrix-sdk/src/sliding_sync.rs +++ b/crates/matrix-sdk/src/sliding_sync.rs @@ -17,12 +17,14 @@ use std::{fmt::Debug, sync::Arc}; use anyhow::{bail, Context}; use futures_core::stream::Stream; -use matrix_sdk_base::deserialized_responses::SyncResponse; +use futures_signals::signal::Mutable; +use matrix_sdk_base::deserialized_responses::{SyncResponse, SyncTimelineEvent}; use ruma::{ - api::client::sync::sync_events::v4, + api::client::sync::sync_events::v4::{ + self, AccountDataConfig, E2EEConfig, ExtensionsConfig, ToDeviceConfig, + }, assign, - events::{AnySyncTimelineEvent, RoomEventType}, - serde::Raw, + events::RoomEventType, OwnedRoomId, RoomId, UInt, }; use url::Url; @@ -89,28 +91,30 @@ impl RoomListEntry { } } -pub type AliveRoomTimeline = - Arc>>; +pub type AliveRoomTimeline = Arc>; /// Room info as giving by the SlidingSync Feature. #[derive(Debug, Clone)] pub struct SlidingSyncRoom { room_id: OwnedRoomId, inner: v4::SlidingSyncRoom, - is_loading_more: futures_signals::signal::Mutable, - prev_batch: futures_signals::signal::Mutable>, + is_loading_more: Mutable, + prev_batch: Mutable>, timeline: AliveRoomTimeline, } impl SlidingSyncRoom { - fn from(room_id: OwnedRoomId, mut inner: v4::SlidingSyncRoom) -> Self { - let v4::SlidingSyncRoom { timeline, .. } = inner; + fn from( + room_id: OwnedRoomId, + mut inner: v4::SlidingSyncRoom, + timeline: Vec, + ) -> Self { // we overwrite to only keep one copy inner.timeline = vec![]; Self { room_id, - is_loading_more: futures_signals::signal::Mutable::new(false), - prev_batch: futures_signals::signal::Mutable::new(inner.prev_batch.clone()), + is_loading_more: Mutable::new(false), + prev_batch: Mutable::new(inner.prev_batch.clone()), timeline: Arc::new(futures_signals::signal_vec::MutableVec::new_with_values(timeline)), inner, } @@ -141,7 +145,7 @@ impl SlidingSyncRoom { self.inner.name.as_deref() } - fn update(&mut self, room_data: &v4::SlidingSyncRoom) { + fn update(&mut self, room_data: &v4::SlidingSyncRoom, timeline: Vec) { let v4::SlidingSyncRoom { name, initial, @@ -150,7 +154,6 @@ impl SlidingSyncRoom { unread_notifications, required_state, prev_batch, - timeline, .. } = room_data; @@ -178,8 +181,8 @@ impl SlidingSyncRoom { if !timeline.is_empty() { let mut ref_timeline = self.timeline.lock_mut(); - for e in timeline { - ref_timeline.push_cloned(e.clone()); + for e in timeline.into_iter() { + ref_timeline.push_cloned(e); } } } @@ -192,11 +195,11 @@ impl std::ops::Deref for SlidingSyncRoom { } } -type ViewState = futures_signals::signal::Mutable; -type SyncMode = futures_signals::signal::Mutable; -type PosState = futures_signals::signal::Mutable>; -type RangeState = futures_signals::signal::Mutable>; -type RoomsCount = futures_signals::signal::Mutable>; +type ViewState = Mutable; +type SyncMode = Mutable; +type PosState = Mutable>; +type RangeState = Mutable>; +type RoomsCount = Mutable>; type RoomsList = Arc>; type RoomsMap = Arc>; type RoomsSubscriptions = @@ -243,6 +246,9 @@ pub struct SlidingSync { /// The rooms details #[builder(private, default)] rooms: RoomsMap, + + #[builder(private, default)] + extensions: Mutable>, } impl SlidingSyncBuilder { @@ -275,6 +281,91 @@ impl SlidingSyncBuilder { self.views = Some(views); self } + + /// Activate e2ee, to-device-message and account data extensions if not yet + /// configured. + /// + /// Will leave any extension configuration found untouched, so the order + /// does not matter. + pub fn with_common_extensions(mut self) -> Self { + { + let mut lock = self.extensions.get_or_insert_with(Default::default).lock_mut(); + let mut cfg = lock.get_or_insert_with(Default::default); + if cfg.to_device.is_none() { + cfg.to_device = Some(assign!(ToDeviceConfig::default(), {enabled : Some(true)})); + } + + if cfg.e2ee.is_none() { + cfg.e2ee = Some(assign!(E2EEConfig::default(), {enabled : Some(true)})); + } + + if cfg.account_data.is_none() { + cfg.account_data = + Some(assign!(AccountDataConfig::default(), {enabled : Some(true)})); + } + } + self + } + + /// Set the E2EE extension configuration. + pub fn with_e2ee_extension(mut self, e2ee: E2EEConfig) -> Self { + self.extensions + .get_or_insert_with(Default::default) + .lock_mut() + .get_or_insert_with(Default::default) + .e2ee = Some(e2ee); + self + } + + /// Unset the E2EE extension configuration. + pub fn without_e2ee_extension(mut self) -> Self { + self.extensions + .get_or_insert_with(Default::default) + .lock_mut() + .get_or_insert_with(Default::default) + .e2ee = None; + self + } + + /// Set the ToDevice extension configuration. + pub fn with_to_device_extension(mut self, to_device: ToDeviceConfig) -> Self { + self.extensions + .get_or_insert_with(Default::default) + .lock_mut() + .get_or_insert_with(Default::default) + .to_device = Some(to_device); + self + } + + /// Unset the ToDevice extension configuration. + pub fn without_to_device_extension(mut self) -> Self { + self.extensions + .get_or_insert_with(Default::default) + .lock_mut() + .get_or_insert_with(Default::default) + .to_device = None; + self + } + + /// Set the account data extension configuration. + pub fn with_account_data_extension(mut self, account_data: AccountDataConfig) -> Self { + self.extensions + .get_or_insert_with(Default::default) + .lock_mut() + .get_or_insert_with(Default::default) + .account_data = Some(account_data); + self + } + + /// Unset the account data extension configuration. + pub fn without_account_data_extension(mut self) -> Self { + self.extensions + .get_or_insert_with(Default::default) + .lock_mut() + .get_or_insert_with(Default::default) + .account_data = None; + self + } } impl SlidingSync { @@ -329,6 +420,15 @@ impl SlidingSync { self.rooms.lock_ref().get(&room_id).cloned() } + fn update_to_device_since(&self, since: String) { + self.extensions + .lock_mut() + .get_or_insert_with(Default::default) + .to_device + .get_or_insert_with(Default::default) + .since = Some(since); + } + /// Lookup a set of rooms pub fn get_rooms>( &self, @@ -343,7 +443,7 @@ impl SlidingSync { resp: v4::Response, views: &[SlidingSyncView], ) -> anyhow::Result { - self.client.process_sliding_sync(resp.clone()).await?; + let mut processed = self.client.process_sliding_sync(resp.clone()).await?; tracing::info!("main client processed."); self.pos.replace(Some(resp.pos)); let mut updated_views = Vec::new(); @@ -362,20 +462,33 @@ impl SlidingSync { let mut rooms = Vec::new(); let mut rooms_map = self.rooms.lock_mut(); - for (id, room_data) in resp.rooms.iter() { - if let Some(mut r) = rooms_map.remove(id) { - r.update(room_data); + for (id, mut room_data) in resp.rooms.into_iter() { + let timeline = if let Some(joined_room) = processed.rooms.join.remove(&id) { + joined_room.timeline.events + } else { + let events = room_data.timeline.into_iter().map(Into::into).collect(); + room_data.timeline = vec![]; + events + }; + + if let Some(mut r) = rooms_map.remove(&id) { + r.update(&room_data, timeline); rooms_map.insert_cloned(id.clone(), r); rooms.push(id.clone()); } else { rooms_map.insert_cloned( id.clone(), - SlidingSyncRoom::from(id.clone(), room_data.clone()), + SlidingSyncRoom::from(id.clone(), room_data, timeline), ); - rooms.push(id.clone()); + rooms.push(id); } } + // Update the `to-device` next-batch if found. + if let Some(to_device_since) = resp.extensions.to_device.map(|t| t.next_batch) { + self.update_to_device_since(to_device_since) + } + Ok(UpdateSummary { views: updated_views, rooms }) } @@ -386,9 +499,7 @@ impl SlidingSync { &self, ) -> anyhow::Result> + '_> { let views = self.views.lock_ref().to_vec(); - let _pos = self.pos.clone(); - - // FIXME: hack for while the sliding sync server is on a proxy + let extensions = self.extensions.clone(); let client = self.client.clone(); Ok(async_stream::try_stream! { @@ -398,6 +509,11 @@ impl SlidingSync { .map(SlidingSyncView::request_generator) .collect(); loop { + #[cfg(feature = "e2e-encryption")] + if let Err(e) = client.send_outgoing_requests().await { + tracing::error!(error = ?e, "Error while sending outgoing E2EE requests"); + } + let mut requests = Vec::new(); let mut new_remaining_generators = Vec::new(); let mut new_remaining_views = Vec::new(); @@ -431,6 +547,7 @@ impl SlidingSync { pos: pos.as_deref(), room_subscriptions, unsubscribe_rooms: &unsubscribe_rooms, + extensions: extensions.lock_mut().take().unwrap_or_default(), // extensions are sticky, we pop them here once }); tracing::debug!("requesting"); let resp = client diff --git a/crates/matrix-sdk/src/sync.rs b/crates/matrix-sdk/src/sync.rs index 1145dd971..48491d5e9 100644 --- a/crates/matrix-sdk/src/sync.rs +++ b/crates/matrix-sdk/src/sync.rs @@ -30,17 +30,16 @@ impl Client { rooms, presence, account_data, - to_device, + to_device_events, device_lists: _, device_one_time_keys_count: _, ambiguity_changes: _, notifications, } = &response; - self.handle_sync_events(HandlerKind::GlobalAccountData, &None, &account_data.events) - .await?; + self.handle_sync_events(HandlerKind::GlobalAccountData, &None, account_data).await?; self.handle_sync_events(HandlerKind::Presence, &None, &presence.events).await?; - self.handle_sync_events(HandlerKind::ToDevice, &None, &to_device.events).await?; + self.handle_sync_events(HandlerKind::ToDevice, &None, to_device_events).await?; for (room_id, room_info) in &rooms.join { let room = self.get_room(room_id); @@ -54,8 +53,7 @@ impl Client { self.handle_sync_events(HandlerKind::EphemeralRoomData, &room, &ephemeral.events) .await?; - self.handle_sync_events(HandlerKind::RoomAccountData, &room, &account_data.events) - .await?; + self.handle_sync_events(HandlerKind::RoomAccountData, &room, account_data).await?; self.handle_sync_state_events(&room, &state.events).await?; self.handle_sync_timeline_events(&room, &timeline.events).await?; } diff --git a/crates/matrix-sdk/tests/integration/client.rs b/crates/matrix-sdk/tests/integration/client.rs index e6fd39606..ef69fb287 100644 --- a/crates/matrix-sdk/tests/integration/client.rs +++ b/crates/matrix-sdk/tests/integration/client.rs @@ -3,23 +3,20 @@ use std::{collections::BTreeMap, str::FromStr, time::Duration}; use matrix_sdk::{ config::SyncSettings, media::{MediaFormat, MediaRequest, MediaThumbnailSize}, - Error, HttpError, RumaApiError, Session, + RumaApiError, Session, }; use matrix_sdk_test::{async_test, test_json}; use ruma::{ - api::{ - client::{ - self as client_api, - account::register::{v3::Request as RegistrationRequest, RegistrationKind}, - directory::{ - get_public_rooms, - get_public_rooms_filtered::{self, v3::Request as PublicRoomsFilterRequest}, - }, - media::get_content_thumbnail::v3::Method, - session::get_login_types::v3::LoginType, - uiaa::{self, UiaaResponse}, + api::client::{ + self as client_api, + account::register::{v3::Request as RegistrationRequest, RegistrationKind}, + directory::{ + get_public_rooms, + get_public_rooms_filtered::{self, v3::Request as PublicRoomsFilterRequest}, }, - error::{FromHttpResponseError, ServerError}, + media::get_content_thumbnail::v3::Method, + session::get_login_types::v3::LoginType, + uiaa::{self, UiaaResponse}, }, assign, device_id, directory::Filter, @@ -188,18 +185,17 @@ async fn login_error() { .await; if let Err(err) = client.login_username("example", "wordpass").send().await { - if let Error::Http(HttpError::Api(FromHttpResponseError::Server(ServerError::Known( - RumaApiError::ClientApi(client_api::Error { kind, message, status_code }), - )))) = err + if let Some(RumaApiError::ClientApi(client_api::Error { kind, message, status_code })) = + err.as_ruma_api_error() { - if let client_api::error::ErrorKind::Forbidden = kind { - } else { - panic!("found the wrong `ErrorKind` {:?}, expected `Forbidden", kind); + if *kind != client_api::error::ErrorKind::Forbidden { + panic!("found the wrong `ErrorKind` {kind:?}, expected `Forbidden"); } - assert_eq!(message, "Invalid password".to_owned()); - assert_eq!(status_code, http::StatusCode::from_u16(403).unwrap()); + + assert_eq!(message, "Invalid password"); + assert_eq!(*status_code, http::StatusCode::from_u16(403).unwrap()); } else { - panic!("found the wrong `Error` type {:?}, expected `Error::RumaResponse", err); + panic!("found the wrong `Error` type {err:?}, expected `Error::RumaResponse"); } } else { panic!("this request should return an `Err` variant") @@ -228,18 +224,20 @@ async fn register_error() { }); if let Err(err) = client.register(user).await { - if let HttpError::UiaaError(FromHttpResponseError::Server(ServerError::Known( - UiaaResponse::MatrixError(client_api::Error { kind, message, status_code }), - ))) = err + if let Some(RumaApiError::Uiaa(UiaaResponse::MatrixError(client_api::Error { + kind, + message, + status_code, + }))) = err.as_ruma_api_error() { - if let client_api::error::ErrorKind::Forbidden = kind { - } else { - panic!("found the wrong `ErrorKind` {:?}, expected `Forbidden", kind); + if *kind != client_api::error::ErrorKind::Forbidden { + panic!("found the wrong `ErrorKind` {kind:?}, expected `Forbidden"); } - assert_eq!(message, "Invalid password".to_owned()); - assert_eq!(status_code, http::StatusCode::from_u16(403).unwrap()); + + assert_eq!(message, "Invalid password"); + assert_eq!(*status_code, http::StatusCode::from_u16(403).unwrap()); } else { - panic!("found the wrong `Error` type {:#?}, expected `UiaaResponse`", err); + panic!("found the wrong `Error` type {err:#?}, expected `UiaaResponse`"); } } else { panic!("this request should return an `Err` variant") diff --git a/crates/matrix-sdk/tests/integration/main.rs b/crates/matrix-sdk/tests/integration/main.rs index 709342fa9..e33adebbf 100644 --- a/crates/matrix-sdk/tests/integration/main.rs +++ b/crates/matrix-sdk/tests/integration/main.rs @@ -13,6 +13,16 @@ mod client; mod refresh_token; mod room; +#[cfg(all(test, not(target_arch = "wasm32")))] +#[ctor::ctor] +fn init_logging() { + use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::from_default_env()) + .with(tracing_subscriber::fmt::layer().with_test_writer()) + .init(); +} + async fn test_client_builder() -> (ClientBuilder, MockServer) { let server = MockServer::start().await; let builder = diff --git a/crates/matrix-sdk/tests/integration/refresh_token.rs b/crates/matrix-sdk/tests/integration/refresh_token.rs index a433388a5..eedc4cb6c 100644 --- a/crates/matrix-sdk/tests/integration/refresh_token.rs +++ b/crates/matrix-sdk/tests/integration/refresh_token.rs @@ -13,7 +13,6 @@ use matrix_sdk_test::{async_test, test_json}; use ruma::{ api::{ client::{account::register, error::ErrorKind, Error as ClientApiError}, - error::{FromHttpResponseError, ServerError}, MatrixVersion, }, assign, device_id, user_id, @@ -229,13 +228,11 @@ async fn refresh_token_not_handled() { .mount(&server) .await; - let res = client.whoami().await; + let res = client.whoami().await.unwrap_err(); assert_matches!( - res, - Err(HttpError::Api(FromHttpResponseError::Server(ServerError::Known( - RumaApiError::ClientApi(ClientApiError { kind, .. }) - )))) if matches!(kind, ErrorKind::UnknownToken { .. }) - ) + res.as_ruma_api_error(), + Some(RumaApiError::ClientApi(ClientApiError { kind: ErrorKind::UnknownToken { .. }, .. })) + ); } #[async_test] @@ -362,12 +359,10 @@ async fn refresh_token_handled_failure() { .mount(&server) .await; - let res = client.whoami().await; + let res = client.whoami().await.unwrap_err(); assert_matches!( - res, - Err(HttpError::Api(FromHttpResponseError::Server(ServerError::Known( - RumaApiError::ClientApi(ClientApiError { kind, .. }) - )))) if matches!(kind, ErrorKind::UnknownToken { .. }) + res.as_ruma_api_error(), + Some(RumaApiError::ClientApi(ClientApiError { kind: ErrorKind::UnknownToken { .. }, .. })) ) } diff --git a/crates/matrix-sdk/tests/integration/room/mod.rs b/crates/matrix-sdk/tests/integration/room/mod.rs index b9e0e2c78..3325d1320 100644 --- a/crates/matrix-sdk/tests/integration/room/mod.rs +++ b/crates/matrix-sdk/tests/integration/room/mod.rs @@ -1,3 +1,4 @@ mod common; mod joined; mod left; +mod timeline; diff --git a/crates/matrix-sdk/tests/integration/room/timeline.rs b/crates/matrix-sdk/tests/integration/room/timeline.rs new file mode 100644 index 000000000..16844c4fa --- /dev/null +++ b/crates/matrix-sdk/tests/integration/room/timeline.rs @@ -0,0 +1,427 @@ +#![cfg(feature = "experimental-timeline")] + +use std::{sync::Arc, time::Duration}; + +use assert_matches::assert_matches; +use futures_signals::signal_vec::{SignalVecExt, VecDiff}; +use futures_util::StreamExt; +use matrix_sdk::{ + config::SyncSettings, + room::timeline::{TimelineDetails, TimelineItemContent, TimelineKey}, + ruma::MilliSecondsSinceUnixEpoch, +}; +use matrix_sdk_common::executor::spawn; +use matrix_sdk_test::{async_test, test_json, EventBuilder, JoinedRoomBuilder, TimelineTestEvent}; +use ruma::{ + event_id, + events::room::message::{MessageType, RoomMessageEventContent}, + room_id, uint, user_id, TransactionId, +}; +use serde_json::json; +use wiremock::{ + matchers::{header, method, path_regex}, + Mock, ResponseTemplate, +}; + +use crate::{logged_in_client, mock_sync}; + +#[async_test] +async fn edit() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let (client, server) = logged_in_client().await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let mut ev_builder = EventBuilder::new(); + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let room = client.get_room(room_id).unwrap(); + let timeline = room.timeline(); + let mut timeline_stream = timeline.signal().to_stream(); + + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event( + TimelineTestEvent::Custom(json!({ + "content": { + "body": "hello", + "msgtype": "m.text", + }, + "event_id": "$msda7m:localhost", + "origin_server_ts": 152037280, + "sender": "@alice:example.org", + "type": "m.room.message", + })), + )); + + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let first = + assert_matches!(timeline_stream.next().await, Some(VecDiff::Push { value }) => value); + let msg = assert_matches!( + first.as_event().unwrap().content(), + TimelineItemContent::Message(msg) => msg + ); + assert_matches!(msg.msgtype(), MessageType::Text(_)); + assert_matches!(msg.in_reply_to(), None); + assert!(!msg.is_edited()); + + ev_builder.add_joined_room( + JoinedRoomBuilder::new(room_id) + .add_timeline_event(TimelineTestEvent::Custom(json!({ + "content": { + "body": "Test", + "formatted_body": "Test", + "msgtype": "m.text", + "format": "org.matrix.custom.html", + }, + "event_id": "$7at8sd:localhost", + "origin_server_ts": 152038280, + "sender": "@bob:example.org", + "type": "m.room.message", + }))) + .add_timeline_event(TimelineTestEvent::Custom(json!({ + "content": { + "body": " * hi", + "m.new_content": { + "body": "hi", + "msgtype": "m.text", + }, + "m.relates_to": { + "event_id": "$msda7m:localhost", + "rel_type": "m.replace", + }, + "msgtype": "m.text", + }, + "event_id": "$msda7m2:localhost", + "origin_server_ts": 159056300, + "sender": "@alice:example.org", + "type": "m.room.message", + }))), + ); + + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let second = + assert_matches!(timeline_stream.next().await, Some(VecDiff::Push { value }) => value); + let item = second.as_event().unwrap(); + assert_eq!(item.origin_server_ts(), Some(MilliSecondsSinceUnixEpoch(uint!(152038280)))); + assert!(item.event_id().is_some()); + assert!(!item.is_own()); + assert!(item.raw().is_some()); + + let msg = assert_matches!(item.content(), TimelineItemContent::Message(msg) => msg); + assert_matches!(msg.msgtype(), MessageType::Text(_)); + assert_matches!(msg.in_reply_to(), None); + assert!(!msg.is_edited()); + + let edit = assert_matches!( + timeline_stream.next().await, + Some(VecDiff::UpdateAt { index: 0, value }) => value + ); + let edited = assert_matches!( + edit.as_event().unwrap().content(), + TimelineItemContent::Message(msg) => msg + ); + let text = assert_matches!(edited.msgtype(), MessageType::Text(text) => text); + assert_eq!(text.body, "hi"); + assert_matches!(edited.in_reply_to(), None); + assert!(edited.is_edited()); +} + +#[async_test] +async fn echo() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let (client, server) = logged_in_client().await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let mut ev_builder = EventBuilder::new(); + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let room = client.get_room(room_id).unwrap(); + let timeline = Arc::new(room.timeline()); + let mut timeline_stream = timeline.signal().to_stream(); + + let event_id = event_id!("$wWgymRfo7ri1uQx0NXO40vLJ"); + let txn_id: &TransactionId = "my-txn-id".into(); + + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&json!({ "event_id": event_id }))) + .mount(&server) + .await; + + // Don't move the original timeline, it must live until the end of the test + let timeline = timeline.clone(); + let send_hdl = spawn(async move { + timeline + .send(RoomMessageEventContent::text_plain("Hello, World!").into(), Some(txn_id)) + .await + }); + + let local_echo = + assert_matches!(timeline_stream.next().await, Some(VecDiff::Push { value }) => value); + let item = local_echo.as_event().unwrap(); + assert!(item.event_id().is_none()); + assert!(item.is_own()); + assert_matches!(item.key(), TimelineKey::TransactionId(_)); + assert_eq!(item.origin_server_ts(), None); + assert_matches!(item.raw(), None); + + let msg = assert_matches!(item.content(), TimelineItemContent::Message(msg) => msg); + let text = assert_matches!(msg.msgtype(), MessageType::Text(text) => text); + assert_eq!(text.body, "Hello, World!"); + + // Wait for the sending to finish and assert everything was successful + send_hdl.await.unwrap().unwrap(); + + let sent_confirmation = assert_matches!( + timeline_stream.next().await, + Some(VecDiff::UpdateAt { index: 0, value }) => value + ); + let item = sent_confirmation.as_event().unwrap(); + assert!(item.event_id().is_some()); + assert!(item.is_own()); + assert_matches!(item.key(), TimelineKey::TransactionId(_)); + assert_eq!(item.origin_server_ts(), None); + assert_matches!(item.raw(), None); + + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event( + TimelineTestEvent::Custom(json!({ + "content": { + "body": "Hello, World!", + "msgtype": "m.text", + }, + "event_id": "$7at8sd:localhost", + "origin_server_ts": 152038280, + "sender": "@example:localhost", + "type": "m.room.message", + "unsigned": { "transaction_id": txn_id, }, + })), + )); + + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let remote_echo = assert_matches!( + timeline_stream.next().await, + Some(VecDiff::UpdateAt { index: 0, value }) => value + ); + let item = remote_echo.as_event().unwrap(); + assert!(item.event_id().is_some()); + assert!(item.is_own()); + assert_eq!(item.origin_server_ts(), Some(MilliSecondsSinceUnixEpoch(uint!(152038280)))); + assert_matches!(item.key(), TimelineKey::EventId(_)); + assert_matches!(item.raw(), Some(_)); +} + +#[async_test] +async fn back_pagination() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let (client, server) = logged_in_client().await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let mut ev_builder = EventBuilder::new(); + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let room = client.get_room(room_id).unwrap(); + let timeline = Arc::new(room.timeline()); + let mut timeline_stream = timeline.signal().to_stream(); + + Mock::given(method("GET")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::ROOM_MESSAGES_BATCH_1)) + .expect(1) + .named("messages_batch_1") + .mount(&server) + .await; + + timeline.paginate_backwards(uint!(10)).await.unwrap(); + + let message = assert_matches!( + timeline_stream.next().await, + Some(VecDiff::Push { value }) => value + ); + let msg = assert_matches!( + message.as_event().unwrap().content(), + TimelineItemContent::Message(msg) => msg + ); + let text = assert_matches!(msg.msgtype(), MessageType::Text(text) => text); + assert_eq!(text.body, "hello world"); + + let message = assert_matches!( + timeline_stream.next().await, + Some(VecDiff::InsertAt { index: 0, value }) => value + ); + let msg = assert_matches!( + message.as_event().unwrap().content(), + TimelineItemContent::Message(msg) => msg + ); + let text = assert_matches!(msg.msgtype(), MessageType::Text(text) => text); + assert_eq!(text.body, "the world is big"); +} + +#[async_test] +async fn reaction() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let (client, server) = logged_in_client().await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let mut ev_builder = EventBuilder::new(); + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let room = client.get_room(room_id).unwrap(); + let timeline = room.timeline(); + let mut timeline_stream = timeline.signal().to_stream(); + + ev_builder.add_joined_room( + JoinedRoomBuilder::new(room_id) + .add_timeline_event(TimelineTestEvent::Custom(json!({ + "content": { + "body": "hello", + "msgtype": "m.text", + }, + "event_id": "$TTvQUp1e17qkw41rBSjpZ", + "origin_server_ts": 152037280, + "sender": "@alice:example.org", + "type": "m.room.message", + }))) + .add_timeline_event(TimelineTestEvent::Custom(json!({ + "content": { + "m.relates_to": { + "event_id": "$TTvQUp1e17qkw41rBSjpZ", + "key": "👍", + "rel_type": "m.annotation", + }, + }, + "event_id": "$031IXQRi27504", + "origin_server_ts": 152038300, + "sender": "@bob:example.org", + "type": "m.reaction", + }))), + ); + + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let message = + assert_matches!(timeline_stream.next().await, Some(VecDiff::Push { value }) => value); + assert_matches!(message.as_event().unwrap().content(), TimelineItemContent::Message(_)); + + let updated_message = assert_matches!( + timeline_stream.next().await, + Some(VecDiff::UpdateAt { index: 0, value }) => value + ); + let event_item = updated_message.as_event().unwrap(); + let msg = assert_matches!(event_item.content(), TimelineItemContent::Message(msg) => msg); + assert!(!msg.is_edited()); + assert_eq!(event_item.reactions().len(), 1); + let details = &event_item.reactions()["👍"]; + assert_eq!(details.count, uint!(1)); + let senders = assert_matches!(&details.senders, TimelineDetails::Ready(s) => s); + assert_eq!(*senders, vec![user_id!("@bob:example.org").to_owned()]); + + // TODO: After adding raw timeline items, check for one here + + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event( + TimelineTestEvent::Custom(json!({ + "content": {}, + "redacts": "$031IXQRi27504", + "event_id": "$N6eUCBc3vu58PL8TobGaVQzM", + "sender": "@bob:example.org", + "origin_server_ts": 152037280, + "type": "m.room.redaction", + })), + )); + + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let updated_message = assert_matches!( + timeline_stream.next().await, + Some(VecDiff::UpdateAt { index: 0, value }) => value + ); + let event_item = updated_message.as_event().unwrap(); + let msg = assert_matches!(event_item.content(), TimelineItemContent::Message(msg) => msg); + assert!(!msg.is_edited()); + assert_eq!(event_item.reactions().len(), 0); +} + +#[async_test] +async fn redacted_message() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let (client, server) = logged_in_client().await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let mut ev_builder = EventBuilder::new(); + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let room = client.get_room(room_id).unwrap(); + let timeline = room.timeline(); + let mut timeline_stream = timeline.signal().to_stream(); + + ev_builder.add_joined_room( + JoinedRoomBuilder::new(room_id) + .add_timeline_event(TimelineTestEvent::Custom(json!({ + "content": {}, + "event_id": "$eeG0HA0FAZ37wP8kXlNkxx3I", + "origin_server_ts": 152035910, + "sender": "@alice:example.org", + "type": "m.room.message", + "unsigned": { + "redacted_because": { + "content": {}, + "redacts": "$eeG0HA0FAZ37wP8kXlNkxx3I", + "event_id": "$N6eUCBc3vu58PL8TobGaVQzM", + "sender": "@alice:example.org", + "origin_server_ts": 152037280, + "type": "m.room.redaction", + }, + }, + }))) + .add_timeline_event(TimelineTestEvent::Custom(json!({ + "content": {}, + "redacts": "$eeG0HA0FAZ37wP8kXlNkxx3I", + "event_id": "$N6eUCBc3vu58PL8TobGaVQzM", + "sender": "@alice:example.org", + "origin_server_ts": 152037280, + "type": "m.room.redaction", + }))), + ); + + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let first = + assert_matches!(timeline_stream.next().await, Some(VecDiff::Push { value }) => value); + assert_matches!(first.as_event().unwrap().content(), TimelineItemContent::RedactedMessage); + + // TODO: After adding raw timeline items, check for one here +} diff --git a/examples/appservice_autojoin/Cargo.toml b/examples/appservice_autojoin/Cargo.toml index 34a812fe1..431d94433 100644 --- a/examples/appservice_autojoin/Cargo.toml +++ b/examples/appservice_autojoin/Cargo.toml @@ -12,7 +12,7 @@ test = false anyhow = "1" tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread"] } tracing-subscriber = "0.3.15" -tracing = "0.1.36" +tracing = { workspace = true } [dependencies.matrix-sdk-appservice] path = "../../crates/matrix-sdk-appservice" diff --git a/examples/appservice_autojoin/src/main.rs b/examples/appservice_autojoin/src/main.rs index a68ab3a73..de95c0d3e 100644 --- a/examples/appservice_autojoin/src/main.rs +++ b/examples/appservice_autojoin/src/main.rs @@ -2,19 +2,15 @@ use std::env; use matrix_sdk_appservice::{ matrix_sdk::{ - self, event_handler::Ctx, room::Room, ruma::{ events::room::member::{MembershipState, OriginalSyncRoomMemberEvent}, UserId, }, - HttpError, - }, - ruma::api::{ - client::{error::ErrorKind, uiaa::UiaaResponse}, - error::{FromHttpResponseError, ServerError}, + RumaApiError, }, + ruma::api::client::{error::ErrorKind, uiaa::UiaaResponse}, AppService, AppServiceBuilder, AppServiceRegistration, Result, }; use tracing::trace; @@ -40,13 +36,19 @@ pub async fn handle_room_member( } pub fn error_if_user_not_in_use(error: matrix_sdk_appservice::Error) -> Result<()> { - match error { + // FIXME: Use if-let chain once available + match &error { // If user is already in use that's OK. - matrix_sdk_appservice::Error::Matrix(matrix_sdk::Error::Http(HttpError::UiaaError( - FromHttpResponseError::Server(ServerError::Known(UiaaResponse::MatrixError(error))), - ))) if matches!(error.kind, ErrorKind::UserInUse) => Ok(()), - // In all other cases return with an error. - error => Err(error), + matrix_sdk_appservice::Error::Matrix(err) => match err.as_ruma_api_error() { + Some(RumaApiError::Uiaa(UiaaResponse::MatrixError(error))) + if matches!(error.kind, ErrorKind::UserInUse) => + { + Ok(()) + } + // In all other cases return with an error. + _ => Err(error), + }, + _ => Err(error), } } diff --git a/examples/cross_signing_bootstrap/src/main.rs b/examples/cross_signing_bootstrap/src/main.rs index 3a35c2730..2217f96a6 100644 --- a/examples/cross_signing_bootstrap/src/main.rs +++ b/examples/cross_signing_bootstrap/src/main.rs @@ -30,7 +30,7 @@ async fn bootstrap(client: Client, user_id: OwnedUserId, password: String) { .await .expect("Couldn't bootstrap cross signing") } else { - panic!("Error during cross-signing bootstrap {:#?}", e); + panic!("Error during cross-signing bootstrap {e:#?}"); } } } diff --git a/examples/custom_events/src/main.rs b/examples/custom_events/src/main.rs index ee2a2df74..ff2a32066 100644 --- a/examples/custom_events/src/main.rs +++ b/examples/custom_events/src/main.rs @@ -107,11 +107,9 @@ async fn login_and_sync( username: &str, password: &str, ) -> anyhow::Result<()> { - #[allow(unused_mut)] - let mut client_builder = Client::builder().homeserver_url(homeserver_url); let home = dirs::data_dir().expect("no home directory found").join("getting_started"); - client_builder = client_builder.sled_store(home, None).await?; - let client = client_builder.build().await?; + let client = + Client::builder().homeserver_url(homeserver_url).sled_store(home, None).build().await?; client .login_username(username, password) .initial_device_display_name("getting started bot") diff --git a/examples/emoji_verification/Cargo.toml b/examples/emoji_verification/Cargo.toml index 5aff532ab..b7e5b4637 100644 --- a/examples/emoji_verification/Cargo.toml +++ b/examples/emoji_verification/Cargo.toml @@ -10,10 +10,11 @@ test = false [dependencies] anyhow = "1" -tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread"] } -clap = { version = "3.2.20", features = ["derive"] } -tracing-subscriber = "0.3.15" -url = "2.2.2" +tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread"] } +clap = { version = "4.0.15", features = ["derive"] } +futures = "0.3.24" +tracing-subscriber = "0.3.16" +url = "2.3.1" [dependencies.matrix-sdk] path = "../../crates/matrix-sdk" diff --git a/examples/emoji_verification/src/main.rs b/examples/emoji_verification/src/main.rs index 051b2ecaa..496c4d18d 100644 --- a/examples/emoji_verification/src/main.rs +++ b/examples/emoji_verification/src/main.rs @@ -1,16 +1,14 @@ -use std::io::{self, Write}; +use std::io::Write; use anyhow::Result; use clap::Parser; +use futures::stream::StreamExt; use matrix_sdk::{ - self, config::SyncSettings, - encryption::verification::{format_emojis, SasVerification, Verification}, + encryption::verification::{format_emojis, Emoji, SasState, SasVerification, Verification}, ruma::{ events::{ key::verification::{ - done::{OriginalSyncKeyVerificationDoneEvent, ToDeviceKeyVerificationDoneEvent}, - key::{OriginalSyncKeyVerificationKeyEvent, ToDeviceKeyVerificationKeyEvent}, request::ToDeviceKeyVerificationRequestEvent, start::{OriginalSyncKeyVerificationStartEvent, ToDeviceKeyVerificationStartEvent}, }, @@ -22,42 +20,22 @@ use matrix_sdk::{ }; use url::Url; -async fn wait_for_confirmation(client: Client, sas: SasVerification) { - let emoji = sas.emoji().expect("The emoji should be available now"); - +async fn wait_for_confirmation(sas: SasVerification, emoji: [Emoji; 7]) { println!("\nDo the emojis match: \n{}", format_emojis(emoji)); print!("Confirm with `yes` or cancel with `no`: "); std::io::stdout().flush().expect("We should be able to flush stdout"); let mut input = String::new(); - io::stdin().read_line(&mut input).expect("error: unable to read user input"); + std::io::stdin().read_line(&mut input).expect("error: unable to read user input"); match input.trim().to_lowercase().as_ref() { - "yes" | "true" | "ok" => { - sas.confirm().await.unwrap(); - - if sas.is_done() { - print_result(&sas); - print_devices(sas.other_device().user_id(), &client).await; - } - } + "yes" | "true" | "ok" => sas.confirm().await.unwrap(), _ => sas.cancel().await.unwrap(), } } -fn print_result(sas: &SasVerification) { - let device = sas.other_device(); - - println!( - "Successfully verified device {} {} {:?}", - device.user_id(), - device.device_id(), - device.local_trust_state() - ); -} - async fn print_devices(user_id: &UserId, client: &Client) { - println!("Devices of user {}", user_id); + println!("Devices of user {user_id}"); for device in client.encryption().get_user_devices(user_id).await.unwrap().devices() { println!( @@ -69,6 +47,49 @@ async fn print_devices(user_id: &UserId, client: &Client) { } } +async fn sas_verification_handler(client: Client, sas: SasVerification) { + println!( + "Starting verification with {} {}", + &sas.other_device().user_id(), + &sas.other_device().device_id() + ); + print_devices(sas.other_device().user_id(), &client).await; + sas.accept().await.unwrap(); + + let mut stream = sas.changes(); + + while let Some(state) = stream.next().await { + match state { + SasState::KeysExchanged { emojis, decimals: _ } => { + tokio::spawn(wait_for_confirmation( + sas.clone(), + emojis.expect("We only support verifications using emojis").emojis, + )); + } + SasState::Done { .. } => { + let device = sas.other_device(); + + println!( + "Successfully verified device {} {} {:?}", + device.user_id(), + device.device_id(), + device.local_trust_state() + ); + + print_devices(sas.other_device().user_id(), &client).await; + + break; + } + SasState::Cancelled(cancel_info) => { + println!("The verification has been cancelled, reason: {}", cancel_info.reason()); + + break; + } + SasState::Started { .. } | SasState::Accepted { .. } | SasState::Confirmed => (), + } + } +} + async fn sync(client: Client) -> matrix_sdk::Result<()> { client.add_event_handler( |ev: ToDeviceKeyVerificationRequestEvent, client: Client| async move { @@ -88,36 +109,7 @@ async fn sync(client: Client) -> matrix_sdk::Result<()> { .get_verification(&ev.sender, ev.content.transaction_id.as_str()) .await { - println!( - "Starting verification with {} {}", - &sas.other_device().user_id(), - &sas.other_device().device_id() - ); - print_devices(&ev.sender, &client).await; - sas.accept().await.unwrap(); - } - }); - - client.add_event_handler(|ev: ToDeviceKeyVerificationKeyEvent, client: Client| async move { - if let Some(Verification::SasV1(sas)) = client - .encryption() - .get_verification(&ev.sender, ev.content.transaction_id.as_str()) - .await - { - tokio::spawn(wait_for_confirmation(client, sas)); - } - }); - - client.add_event_handler(|ev: ToDeviceKeyVerificationDoneEvent, client: Client| async move { - if let Some(Verification::SasV1(sas)) = client - .encryption() - .get_verification(&ev.sender, ev.content.transaction_id.as_str()) - .await - { - if sas.is_done() { - print_result(&sas); - print_devices(&ev.sender, &client).await; - } + tokio::spawn(sas_verification_handler(client, sas)); } }); @@ -140,40 +132,7 @@ async fn sync(client: Client) -> matrix_sdk::Result<()> { .get_verification(&ev.sender, ev.content.relates_to.event_id.as_str()) .await { - println!( - "Starting verification with {} {}", - &sas.other_device().user_id(), - &sas.other_device().device_id() - ); - print_devices(&ev.sender, &client).await; - sas.accept().await.unwrap(); - } - }, - ); - - client.add_event_handler( - |ev: OriginalSyncKeyVerificationKeyEvent, client: Client| async move { - if let Some(Verification::SasV1(sas)) = client - .encryption() - .get_verification(&ev.sender, ev.content.relates_to.event_id.as_str()) - .await - { - tokio::spawn(wait_for_confirmation(client.clone(), sas)); - } - }, - ); - - client.add_event_handler( - |ev: OriginalSyncKeyVerificationDoneEvent, client: Client| async move { - if let Some(Verification::SasV1(sas)) = client - .encryption() - .get_verification(&ev.sender, ev.content.relates_to.event_id.as_str()) - .await - { - if sas.is_done() { - print_result(&sas); - print_devices(&ev.sender, &client).await; - } + tokio::spawn(sas_verification_handler(client, sas)); } }, ); diff --git a/examples/get_profiles/src/main.rs b/examples/get_profiles/src/main.rs index 73b6964f9..87ab43928 100644 --- a/examples/get_profiles/src/main.rs +++ b/examples/get_profiles/src/main.rs @@ -68,6 +68,6 @@ async fn main() -> anyhow::Result<()> { let user_id = UserId::parse(username).expect("Couldn't parse the MXID"); let profile = get_profile(client, &user_id).await?; - println!("{:#?}", profile); + println!("{profile:#?}"); Ok(()) } diff --git a/examples/getting_started/src/main.rs b/examples/getting_started/src/main.rs index dd6ca5828..162b4ca60 100644 --- a/examples/getting_started/src/main.rs +++ b/examples/getting_started/src/main.rs @@ -60,28 +60,28 @@ async fn login_and_sync( username: &str, password: &str, ) -> anyhow::Result<()> { - // first, we set up the client. We use the convenient client builder to set our - // custom homeserver URL on it - #[allow(unused_mut)] - let mut client_builder = Client::builder().homeserver_url(homeserver_url); + // First, we set up the client. - // Matrix-SDK has support for pluggable, configurable state and crypto-store - // support we use the default sled-store (enabled by default on native - // architectures), to configure a local cache and store for our crypto keys let home = dirs::data_dir().expect("no home directory found").join("getting_started"); - client_builder = client_builder.sled_store(home, None).await?; - // alright, let's make that into a client - let client = client_builder.build().await?; + let client = Client::builder() + // We use the convenient client builder to set our custom homeserver URL on it. + .homeserver_url(homeserver_url) + // Matrix-SDK has support for pluggable, configurable state and crypto-store + // support we use the default sled-store (enabled by default on native + // architectures), to configure a local cache and store for our crypto keys + .sled_store(home, None) + .build() + .await?; - // then let's log that client in + // Then let's log that client in client .login_username(username, password) .initial_device_display_name("getting started bot") .send() .await?; - // it worked! + // It worked! println!("logged in as {username}"); // Now, we want our client to react to invites. Invites sent us stripped member diff --git a/examples/timeline/Cargo.toml b/examples/timeline/Cargo.toml index 9290178fa..0a0697c9e 100644 --- a/examples/timeline/Cargo.toml +++ b/examples/timeline/Cargo.toml @@ -10,12 +10,14 @@ test = false [dependencies] anyhow = "1" +clap = "4.0.16" futures = "0.3" +futures-signals = { version = "0.3.30", default-features = false } tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread"] } tracing-subscriber = "0.3.15" url = "2.2.2" [dependencies.matrix-sdk] path = "../../crates/matrix-sdk" -features = ["sled"] +features = ["experimental-timeline", "sled"] version = "0.6.0" diff --git a/examples/timeline/src/main.rs b/examples/timeline/src/main.rs index b92ab877a..c4f5c92b9 100644 --- a/examples/timeline/src/main.rs +++ b/examples/timeline/src/main.rs @@ -1,99 +1,84 @@ -use std::{env, process::exit, sync::Mutex, time::Duration}; - -use matrix_sdk::{ - self, - config::SyncSettings, - room::Room, - ruma::{ - api::client::filter::{FilterDefinition, LazyLoadOptions}, - events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent}, - }, - Client, LoopCtrl, -}; -use tokio::sync::oneshot; +use anyhow::Result; +use clap::Parser; +use futures::StreamExt; +use futures_signals::signal_vec::SignalVecExt; +use matrix_sdk::{self, config::SyncSettings, ruma::OwnedRoomId, Client}; use url::Url; -async fn login(homeserver_url: String, username: &str, password: &str) -> Client { - let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL"); - let client = Client::builder() - .homeserver_url(homeserver_url) - .sled_store("./", Some("some password")) - .await - .unwrap() - .build() - .await - .unwrap(); +#[derive(Parser, Debug)] +struct Cli { + /// The homeserver to connect to. + #[clap(value_parser)] + homeserver: Url, + + /// The user name that should be used for the login. + #[clap(value_parser)] + user_name: String, + + /// The password that should be used for the login. + #[clap(value_parser)] + password: String, + + /// Set the proxy that should be used for the connection. + #[clap(short, long)] + proxy: Option, + + /// Enable verbose logging output. + #[clap(short, long, action)] + verbose: bool, + + /// The room id that we should listen for the, + #[clap(value_parser)] + room_id: OwnedRoomId, +} + +async fn login(cli: Cli) -> Result { + let mut builder = + Client::builder().homeserver_url(cli.homeserver).sled_store("./", Some("some password")); + + if let Some(proxy) = cli.proxy { + builder = builder.proxy(proxy); + } + + let client = builder.build().await?; client - .login_username(username, password) + .login_username(&cli.user_name, &cli.password) .initial_device_display_name("rust-sdk") .send() - .await - .unwrap(); - client -} + .await?; -fn _event_content(event: AnySyncTimelineEvent) -> Option { - if let AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( - SyncMessageLikeEvent::Original(event), - )) = event - { - Some(event.content.msgtype.body().to_owned()) - } else { - None - } -} - -async fn print_timeline(_room: Room) { - // TODO + Ok(client) } #[tokio::main] -async fn main() -> anyhow::Result<()> { +async fn main() -> Result<()> { tracing_subscriber::fmt::init(); - let (homeserver_url, username, password, room_id) = - match (env::args().nth(1), env::args().nth(2), env::args().nth(3), env::args().nth(4)) { - (Some(a), Some(b), Some(c), Some(d)) => (a, b, c, d), - _ => { - eprintln!( - "Usage: {} ", - env::args().next().unwrap() - ); - exit(1) - } - }; + let cli = Cli::parse(); + let room_id = cli.room_id.clone(); + let client = login(cli).await?; - let client = login(homeserver_url, &username, &password).await; - - let mut filter = FilterDefinition::default(); - filter.room.include_leave = true; - filter.room.state.lazy_load_options = - LazyLoadOptions::Enabled { include_redundant_members: false }; - - let sync_settings = SyncSettings::new().timeout(Duration::from_secs(30)).filter(filter.into()); - let (sender, receiver) = oneshot::channel::<()>(); - let sender = Mutex::new(Some(sender)); - let client_clone = client.clone(); - tokio::spawn(async move { - client_clone - .sync_with_callback(sync_settings, |_| async { - if let Some(sender) = sender.lock().unwrap().take() { - sender.send(()).unwrap(); - } - LoopCtrl::Continue - }) - .await - .unwrap(); - }); + let sync_settings = SyncSettings::default(); // Wait for the first sync response println!("Wait for the first sync"); - receiver.await.unwrap(); - let room = client.get_room(room_id.as_str().try_into().unwrap()).unwrap(); + client.sync_once(sync_settings.clone()).await?; - print_timeline(room).await; + // Get the timeline stream and listen to it. + let room = client.get_room(&room_id).unwrap(); + let timeline = room.timeline(); + let mut timeline_stream = timeline.signal().to_stream(); + + tokio::spawn(async move { + while let Some(diff) = timeline_stream.next().await { + println!("Received a timeline diff {diff:#?}"); + } + }); + + // Sync forever + client.sync(sync_settings).await?; Ok(()) } diff --git a/labs/jack-in/Cargo.toml b/labs/jack-in/Cargo.toml index d9ad0e3f0..47a5acb98 100644 --- a/labs/jack-in/Cargo.toml +++ b/labs/jack-in/Cargo.toml @@ -9,18 +9,22 @@ edition = "2021" file-logging = ["dep:log4rs"] [dependencies] -tuirealm = "~1.7.1" -matrix-sdk = { path = "../../crates/matrix-sdk", default-features = false, features = ["e2e-encryption", "anyhow", "native-tls", "sled", "sliding-sync"] , version = "0.6.0" } -matrix-sdk-common = { path = "../../crates/matrix-sdk-common" , version = "0.6.0" } -structopt = "0.3" -tokio = { version = "1", features = ["rt-multi-thread", "sync", "macros"] } +app_dirs2 = "2" +dialoguer = "0.10.2" +eyre = "0.6" futures = { version = "0.3.1" } futures-signals = "0.3.24" +matrix-sdk = { path = "../../crates/matrix-sdk", default-features = false, features = ["e2e-encryption", "anyhow", "native-tls", "sled", "sliding-sync"], version = "0.6.0" } +matrix-sdk-common = { path = "../../crates/matrix-sdk-common", version = "0.6.0" } +matrix-sdk-sled = { path = "../../crates/matrix-sdk-sled", features = ["state-store", "crypto-store"], version = "0.2.0" } +sanitize-filename-reader-friendly = "2.2.1" +serde_json = "1.0.85" +structopt = "0.3" +tokio = { version = "1", features = ["rt-multi-thread", "sync", "macros"] } tracing-flame = "0.2" tracing-subscriber = "0.3.15" -eyre = "0.6" - tui-logger = "0.8.0" +tuirealm = "~1.7.1" # file-logging specials tracing = { version = "0.1.35", features = ["log"] } diff --git a/labs/jack-in/src/client/mod.rs b/labs/jack-in/src/client/mod.rs index d9e0156f5..8181d8465 100644 --- a/labs/jack-in/src/client/mod.rs +++ b/labs/jack-in/src/client/mod.rs @@ -1,7 +1,7 @@ use eyre::{Result, WrapErr}; use futures::{pin_mut, StreamExt}; use tokio::sync::mpsc; -use tracing::{error, warn}; +use tracing::{error, info, warn}; pub mod state; @@ -12,13 +12,14 @@ pub async fn run_client( sliding_sync_proxy: String, tx: mpsc::Sender, ) -> Result<()> { - warn!("Starting sliding sync now"); + info!("Starting sliding sync now"); let builder = client.sliding_sync().await; let full_sync_view = SlidingSyncViewBuilder::default_with_fullsync().timeline_limit(10u32).build()?; let syncer = builder .homeserver(sliding_sync_proxy.parse().wrap_err("can't parse sync proxy")?) .add_view(full_sync_view) + .with_common_extensions() .build()?; let stream = syncer.stream().await.expect("we can build the stream"); let view = syncer.views.lock_ref().first().expect("we have the full syncer there").clone(); @@ -26,8 +27,13 @@ pub async fn run_client( let mut ssync_state = state::SlidingSyncState::new(view); tx.send(ssync_state.clone()).await?; + info!("starting polling"); + pin_mut!(stream); - let _first_poll = stream.next().await; + if let Some(Err(e)) = stream.next().await { + error!("Initial Query on sliding sync failed: {:#?}", e); + return Ok(()); + } let view_state = state.read_only().get_cloned(); if view_state != SlidingSyncState::CatchingUp { warn!("Sliding Query failed: {:#?}", view_state); @@ -38,25 +44,26 @@ pub async fn run_client( ssync_state.set_first_render_now(); tx.send(ssync_state.clone()).await?; } - warn!("Done initial sliding sync"); + info!("Done initial sliding sync"); loop { match stream.next().await { Some(Ok(_)) => { // we are switching into live updates mode next. ignoring + let state = state.read_only().get_cloned(); - if state.read_only().get_cloned() == SlidingSyncState::Live { - warn!("Reached live sync"); + if state == SlidingSyncState::Live { + info!("Reached live sync"); break; } let _ = tx.send(ssync_state.clone()).await; } Some(Err(e)) => { - warn!("Error: {:}", e); + error!("Error: {:}", e); break; } None => { - warn!("Never reached live state"); + error!("Never reached live state"); break; } } @@ -88,7 +95,7 @@ pub async fn run_client( } match update { Ok(update) => { - warn!("Live update received: {:?}", update); + info!("Live update received: {:?}", update); tx.send(ssync_state.clone()).await?; err_counter = 0; } diff --git a/labs/jack-in/src/components/details.rs b/labs/jack-in/src/components/details.rs index 4b847d08a..3610eed68 100644 --- a/labs/jack-in/src/components/details.rs +++ b/labs/jack-in/src/components/details.rs @@ -79,7 +79,7 @@ impl Details { .timeline() .lock_ref() .iter() - .filter_map(|d| d.deserialize().ok()) + .filter_map(|d| d.event.deserialize().ok()) .map(|e| e.into_full_event(room_id.clone())) .collect(); timeline.reverse(); @@ -161,7 +161,7 @@ impl MockComponent for Details { let mut tabs = vec![]; for (title, count) in &self.state_events_counts { - tabs.push(Spans::from(format!("{}: {}", title.clone(), count))); + tabs.push(Spans::from(format!("{title}: {count}"))); } frame.render_widget( diff --git a/labs/jack-in/src/components/statusbar.rs b/labs/jack-in/src/components/statusbar.rs index 60ef104d4..0ca25027f 100644 --- a/labs/jack-in/src/components/statusbar.rs +++ b/labs/jack-in/src/components/statusbar.rs @@ -41,7 +41,7 @@ impl MockComponent for StatusBar { if let Some(dur) = self.sstate.time_to_full_sync() { tabs.push(Spans::from(format!("Full sync: {}ms", dur.as_millis()))); if let Some(count) = self.sstate.total_rooms_count() { - tabs.push(Spans::from(format!("{} rooms", count))); + tabs.push(Spans::from(format!("{count} rooms"))); } } else { tabs.push(Spans::from(format!( diff --git a/labs/jack-in/src/main.rs b/labs/jack-in/src/main.rs index d3b47a41c..52c11bab7 100644 --- a/labs/jack-in/src/main.rs +++ b/labs/jack-in/src/main.rs @@ -4,16 +4,22 @@ use std::path::{Path, PathBuf}; +use app_dirs2::{app_root, AppDataType, AppInfo}; +use dialoguer::{theme::ColorfulTheme, Password}; use eyre::{eyre, Result}; use matrix_sdk::{ - ruma::{OwnedDeviceId, OwnedRoomId, OwnedUserId}, - Client, Session, + ruma::{OwnedRoomId, OwnedUserId}, + Client, }; -use tracing::{log::LevelFilter, warn}; +use matrix_sdk_sled::make_store_config; +use sanitize_filename_reader_friendly::sanitize; +use tracing::{log, warn}; use tracing_flame::FlameLayer; use tracing_subscriber::prelude::*; use tuirealm::{application::PollStrategy, Event, Update}; +const APP_INFO: AppInfo = AppInfo { name: "jack-in", author: "Matrix-Rust-SDK Core Team" }; + // -- internal mod app; mod client; @@ -75,14 +81,27 @@ struct Opt { #[structopt(short, long, default_value = "http://localhost:8008", env = "JACKIN_SYNC_PROXY")] sliding_sync_proxy: String, - /// Your access token to connect via the - #[structopt(short, long, env = "JACKIN_TOKEN")] - token: String, + /// The password of your account. If not given and no database found, it + /// will prompt you for it + #[structopt(short, long, env = "JACKIN_PASSWORD")] + password: Option, - /// The userID associated with this access token + /// Create a fresh database, drop all existing cache + #[structopt(long)] + fresh: bool, + + /// RUST_LOG log-levels + #[structopt(short, long, env = "JACKIN_LOG", default_value = "jack_in=info,warn")] + log: String, + + /// The userID to log in with #[structopt(short, long, env = "JACKIN_USER")] user: String, + /// The password to encrypt the store with + #[structopt(long, env = "JACKIN_STORE_PASSWORD")] + store_pass: Option, + #[structopt(long)] /// Activate tracing and write the flamegraph to the specified file flames: Option, @@ -112,7 +131,6 @@ async fn main() -> Result<()> { let opt = Opt::from_args(); let user_id: OwnedUserId = opt.user.clone().parse()?; - let device_id: OwnedDeviceId = "XdftAsd".into(); if let Some(ref p) = opt.flames { setup_flames(p.as_path()); @@ -138,41 +156,104 @@ async fn main() -> Result<()> { .logger( Logger::builder() .appender("file") - .build("matrix_sdk::sliding_sync", LevelFilter::Trace), + .build("matrix_sdk::sliding_sync", log::LevelFilter::Trace), ) .logger( Logger::builder() .appender("file") - .build("matrix_sdk::http_client", LevelFilter::Debug), + .build("matrix_sdk::http_client", log::LevelFilter::Debug), ) .logger( Logger::builder() .appender("file") - .build("matrix_sdk_base::sliding_sync", LevelFilter::Debug), + .build("matrix_sdk_base::sliding_sync", log::LevelFilter::Debug), ) - .logger(Logger::builder().appender("file").build("reqwest", LevelFilter::Trace)) - .logger(Logger::builder().appender("file").build("matrix_sdk", LevelFilter::Warn)) - .build(Root::builder().build(LevelFilter::Error)) + .logger( + Logger::builder().appender("file").build("reqwest", log::LevelFilter::Trace), + ) + .logger( + Logger::builder().appender("file").build("matrix_sdk", log::LevelFilter::Warn), + ) + .build(Root::builder().build(log::LevelFilter::Error)) .unwrap(); log4rs::init_config(config).expect("Logging with log4rs failed to initialize"); } #[cfg(not(feature = "file-logging"))] { - tui_logger::init_logger(LevelFilter::Trace).expect("Could not set up logging"); - tui_logger::set_default_level(LevelFilter::Warn); - tui_logger::set_level_for_target("matrix_sdk", LevelFilter::Warn); + tui_logger::init_logger(log::LevelFilter::Trace).unwrap(); + // Set default level for unknown targets to Trace + tui_logger::set_default_level(log::LevelFilter::Warn); + + for pair in opt.log.split(',') { + if let Some((name, lvl)) = pair.split_once('=') { + let level = match lvl.to_lowercase().as_str() { + "trace" => log::LevelFilter::Trace, + "debug" => log::LevelFilter::Debug, + "info" => log::LevelFilter::Info, + "warn" => log::LevelFilter::Warn, + "error" => log::LevelFilter::Error, + // nothing means error + _ => continue, + }; + tui_logger::set_level_for_target(name, level); + } else { + let level = match pair.to_lowercase().as_str() { + "trace" => log::LevelFilter::Trace, + "debug" => log::LevelFilter::Debug, + "info" => log::LevelFilter::Info, + "warn" => log::LevelFilter::Warn, + "error" => log::LevelFilter::Error, + // nothing means error + _ => continue, + }; + tui_logger::set_default_level(level); + } + } } } - let client = Client::builder().server_name(user_id.server_name()).build().await?; - let session = Session { - access_token: opt.token.clone(), - refresh_token: None, - user_id: user_id.clone(), - device_id, - }; - client.restore_login(session).await?; + let data_path = app_root(AppDataType::UserData, &APP_INFO)?.join(sanitize(user_id.as_str())); + if opt.fresh { + // drop the database first; + std::fs::remove_dir_all(&data_path)?; + } + std::fs::create_dir_all(&data_path)?; + let store_config = make_store_config(&data_path, opt.store_pass.as_deref()).await?; + + let client = Client::builder() + .user_agent("jack-in") + .server_name(user_id.server_name()) + .store_config(store_config) + .build() + .await?; + + let session_key = b"jackin::session_token"; + + if let Some(session) = client + .store() + .get_custom_value(session_key) + .await? + .map(|v| serde_json::from_slice(&v)) + .transpose()? + { + tracing::info!("Restoring session from store"); + client.restore_login(session).await?; + } else { + let theme = ColorfulTheme::default(); + let password = match opt.password { + Some(ref pw) => pw.clone(), + _ => Password::with_theme(&theme) + .with_prompt(format!("Password for {user_id:} :")) + .interact()?, + }; + client.login_username(&user_id, &password).send().await?; + } + + if let Some(session) = client.session() { + client.store().set_custom_value(session_key, serde_json::to_vec(&session)?).await?; + } + let sliding_client = client.clone(); let proxy = opt.sliding_sync_proxy.clone(); @@ -192,11 +273,11 @@ async fn main() -> Result<()> { .account() .get_display_name() .await? - .map(|s| format!("{} ({})", s, user_id)) - .unwrap_or_else(|| format!("{}", user_id)); + .map(|s| format!("{s} ({user_id})")) + .unwrap_or_else(|| format!("{user_id}")); let poller = MatrixPoller(rx); let mut model = Model::new(start_sync, model_tx, poller); - model.set_title(format!("{} via {}", display_name, opt.sliding_sync_proxy)); + model.set_title(format!("{display_name} via {}", opt.sliding_sync_proxy)); run_ui(model).await; Ok(()) @@ -213,7 +294,7 @@ async fn run_ui(mut model: Model) { // Tick match model.app.tick(PollStrategy::Once) { Err(err) => { - model.set_title(format!("Application error: {}", err)); + model.set_title(format!("Application error: {err}")); } Ok(messages) if !messages.is_empty() => { // NOTE: redraw if at least one msg has been processed diff --git a/labs/sled-state-inspector/Cargo.toml b/labs/sled-state-inspector/Cargo.toml index ce5201284..418d14bd8 100644 --- a/labs/sled-state-inspector/Cargo.toml +++ b/labs/sled-state-inspector/Cargo.toml @@ -14,7 +14,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.6.0"} matrix-sdk-sled = { path = "../../crates/matrix-sdk-sled", version = "0.2.0"} -ruma = "0.7.0" +ruma = { workspace = true } rustyline = "10.0.0" rustyline-derive = "0.7.0" serde = "1.0.136" diff --git a/labs/sled-state-inspector/src/main.rs b/labs/sled-state-inspector/src/main.rs index 748f9c721..8c939d275 100644 --- a/labs/sled-state-inspector/src/main.rs +++ b/labs/sled-state-inspector/src/main.rs @@ -172,7 +172,7 @@ impl Printer { let data = if self.json { serde_json::to_string_pretty(data).expect("Can't serialize struct") } else { - format!("{:#?}", data) + format!("{data:#?}") }; let syntax = if self.json { diff --git a/tarpaulin.toml b/tarpaulin.toml index 7733409f1..6a74337d3 100644 --- a/tarpaulin.toml +++ b/tarpaulin.toml @@ -21,4 +21,6 @@ exclude = [ # labs "jack-in", "sled-state-inspector", + # repo automation (ci, codegen) + "xtask", ] diff --git a/testing/matrix-sdk-integration-testing/Cargo.toml b/testing/matrix-sdk-integration-testing/Cargo.toml index 44e3eb710..d093a4157 100644 --- a/testing/matrix-sdk-integration-testing/Cargo.toml +++ b/testing/matrix-sdk-integration-testing/Cargo.toml @@ -13,5 +13,5 @@ matrix-sdk = { path = "../../crates/matrix-sdk" } once_cell = "1.13.0" tempfile = "3.3.0" tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } -tracing = "0.1.36" +tracing = { workspace = true } tracing-subscriber = { version = "0.3.15", features = ["env-filter"] } diff --git a/testing/matrix-sdk-integration-testing/src/helpers.rs b/testing/matrix-sdk-integration-testing/src/helpers.rs index 622d2de65..97466fb39 100644 --- a/testing/matrix-sdk-integration-testing/src/helpers.rs +++ b/testing/matrix-sdk-integration-testing/src/helpers.rs @@ -42,7 +42,6 @@ pub async fn get_client_for_user(username: String) -> Result { let client = Client::builder() .user_agent("matrix-sdk-integation-tests") .sled_store(tmp_dir.path(), None) - .await? .homeserver_url(homeserver_url) .build() .await?; diff --git a/testing/matrix-sdk-test/Cargo.toml b/testing/matrix-sdk-test/Cargo.toml index bd76f9218..1719d2b8c 100644 --- a/testing/matrix-sdk-test/Cargo.toml +++ b/testing/matrix-sdk-test/Cargo.toml @@ -22,7 +22,7 @@ appservice = [] http = "0.2.6" matrix-sdk-test-macros = { version = "0.3.0", path = "../matrix-sdk-test-macros" } once_cell = "1.10.0" -ruma = { version = "0.7.0", features = ["client-api-c"] } +ruma = { workspace = true } serde = "1.0.136" serde_json = "1.0.79" diff --git a/testing/matrix-sdk-test/src/appservice.rs b/testing/matrix-sdk-test/src/appservice.rs index 46e6e02dc..9505accc2 100644 --- a/testing/matrix-sdk-test/src/appservice.rs +++ b/testing/matrix-sdk-test/src/appservice.rs @@ -38,16 +38,9 @@ impl TransactionBuilder { self } - /// Build the transaction - #[cfg(feature = "appservice")] - pub fn build_json_transaction(&self) -> Value { - let body = serde_json::json! { - { - "events": self.events - } - }; - - body + /// Build the transaction as a serialized HTTP body + pub fn build_transaction(&self) -> Vec { + serde_json::to_vec(&serde_json::json!({ "events": self.events })).unwrap() } pub fn clear(&mut self) { diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 20159e73e..013ba4161 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -9,7 +9,9 @@ name = "xtask" test = false [dependencies] +camino = "1.0.8" clap = { version = "3.2.4", features = ["derive"] } serde = { version = "1.0.136", features = ["derive"] } serde_json = "1.0.79" +uniffi_bindgen = { workspace = true } xshell = "0.1.17" diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs index bcce11bc1..d7911f1bc 100644 --- a/xtask/src/ci.rs +++ b/xtask/src/ci.rs @@ -1,10 +1,9 @@ -use std::{collections::BTreeMap, env, path::PathBuf}; +use std::collections::BTreeMap; use clap::{Args, Subcommand}; -use serde::Deserialize; use xshell::{cmd, pushd}; -use crate::{build_docs, DenyWarnings, Result}; +use crate::{build_docs, workspace, DenyWarnings, Result}; #[derive(Args)] pub struct CiArgs { @@ -79,7 +78,7 @@ enum WasmFeatureSet { impl CiArgs { pub fn run(self) -> Result<()> { - let _p = pushd(&workspace_root()?)?; + let _p = pushd(&workspace::root_path()?)?; match self.cmd { Some(cmd) => match cmd { @@ -133,7 +132,7 @@ fn check_clippy() -> Result<()> { cmd!( "rustup run nightly cargo clippy --workspace --all-targets --exclude matrix-sdk-crypto --exclude xtask - --no-default-features --features native-tls,warp + --no-default-features --features native-tls,sso-login -- -D warnings" ) .run()?; @@ -362,14 +361,3 @@ fn run_wasm_pack_tests(cmd: Option) -> Result<()> { Ok(()) } - -fn workspace_root() -> Result { - #[derive(Deserialize)] - struct Metadata { - workspace_root: PathBuf, - } - - let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_owned()); - let metadata_json = cmd!("{cargo} metadata --no-deps --format-version 1").read()?; - Ok(serde_json::from_str::(&metadata_json)?.workspace_root) -} diff --git a/xtask/src/fixup.rs b/xtask/src/fixup.rs index 70fa0ebf2..9788c03d2 100644 --- a/xtask/src/fixup.rs +++ b/xtask/src/fixup.rs @@ -1,10 +1,7 @@ -use std::{env, path::PathBuf}; - use clap::{Args, Subcommand}; -use serde::Deserialize; use xshell::{cmd, pushd}; -use crate::Result; +use crate::{workspace, Result}; #[derive(Args)] pub struct FixupArgs { @@ -24,7 +21,7 @@ enum FixupCommand { impl FixupArgs { pub fn run(self) -> Result<()> { - let _p = pushd(&workspace_root()?)?; + let _p = pushd(&workspace::root_path()?)?; match self.cmd { Some(cmd) => match cmd { @@ -66,7 +63,7 @@ fn fix_clippy() -> Result<()> { "rustup run nightly cargo clippy --workspace --all-targets --fix --allow-dirty --allow-staged --exclude matrix-sdk-crypto --exclude xtask - --no-default-features --features native-tls,warp + --no-default-features --features native-tls,sso-login -- -D warnings" ) .run()?; @@ -78,14 +75,3 @@ fn fix_clippy() -> Result<()> { .run()?; Ok(()) } - -fn workspace_root() -> Result { - #[derive(Deserialize)] - struct Metadata { - workspace_root: PathBuf, - } - - let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_owned()); - let metadata_json = cmd!("{cargo} metadata --no-deps --format-version 1").read()?; - Ok(serde_json::from_str::(&metadata_json)?.workspace_root) -} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 2f58419f5..15cfd9443 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,9 +1,12 @@ mod ci; mod fixup; +mod swift; +mod workspace; use ci::CiArgs; use clap::{Parser, Subcommand}; use fixup::FixupArgs; +use swift::SwiftArgs; use xshell::cmd; type Result> = std::result::Result; @@ -26,6 +29,7 @@ enum Command { #[clap(long)] open: bool, }, + Swift(SwiftArgs), } fn main() -> Result<()> { @@ -33,6 +37,7 @@ fn main() -> Result<()> { Command::Ci(ci) => ci.run(), Command::Fixup(cfg) => cfg.run(), Command::Doc { open } => build_docs(open.then_some("--open"), DenyWarnings::No), + Command::Swift(cfg) => cfg.run(), } } diff --git a/xtask/src/swift.rs b/xtask/src/swift.rs new file mode 100644 index 000000000..269afaacc --- /dev/null +++ b/xtask/src/swift.rs @@ -0,0 +1,91 @@ +use std::fs; + +use clap::{Args, Subcommand}; +use xshell::{cmd, pushd}; + +use crate::{workspace, Result}; + +/// Builds the SDK for Swift as a Static Library or XCFramework. +#[derive(Args)] +pub struct SwiftArgs { + #[clap(subcommand)] + cmd: SwiftCommand, +} + +#[derive(Subcommand)] +enum SwiftCommand { + /// Builds the SDK for Swift as a static lib. + BuildLibrary, + /// Builds the SDK for Swift as an XCFramework. + BuildFramework, +} + +impl SwiftArgs { + pub fn run(self) -> Result<()> { + let _p = pushd(&workspace::root_path()?)?; + + match self.cmd { + SwiftCommand::BuildLibrary => build_library(), + SwiftCommand::BuildFramework => build_xcframework(), + } + } +} + +fn build_library() -> Result<()> { + println!("Running debug library build."); + + let release_type = "debug"; + let static_lib_filename = "libmatrix_sdk_ffi.a"; + + let root_directory = workspace::root_path()?; + let target_directory = root_directory.join("target"); + let ffi_directory = root_directory.join("bindings/apple/generated/matrix_sdk_ffi"); + let swift_directory = root_directory.join("bindings/apple/generated/swift"); + let udl_file = camino::Utf8PathBuf::from_path_buf( + root_directory.join("bindings/matrix-sdk-ffi/src/api.udl"), + ) + .expect("Root Dir contains non-utf8 characters"); + let outdir_overwrite = camino::Utf8PathBuf::from_path_buf(ffi_directory.clone()) + .expect("Root Dir contains non-utf8 characters"); + let library_file = camino::Utf8PathBuf::from_path_buf(ffi_directory.join(static_lib_filename)) + .expect("Root Dir contains non-utf8 characters"); + + fs::create_dir_all(ffi_directory.as_path())?; + fs::create_dir_all(swift_directory.as_path())?; + + cmd!("cargo build -p matrix-sdk-ffi").run()?; + + fs::rename( + target_directory.join(release_type).join(static_lib_filename), + ffi_directory.join(static_lib_filename), + )?; + + uniffi_bindgen::generate_bindings( + udl_file.as_path(), + None, + vec!["swift"], + Some(outdir_overwrite.as_path()), + Some(library_file.as_path()), + false, + )?; + + let module_map_file = ffi_directory.join("module.modulemap"); + if module_map_file.exists() { + fs::remove_file(module_map_file.as_path())?; + } + + // TODO: Find the modulemap in the ffi directory. + fs::rename(ffi_directory.join("matrix_sdk_ffiFFI.modulemap"), module_map_file)?; + // TODO: Move all swift files. + fs::rename( + ffi_directory.join("matrix_sdk_ffi.swift"), + swift_directory.join("matrix_sdk_ffi.swift"), + )?; + + Ok(()) +} + +fn build_xcframework() -> Result<()> { + println!("XCFramework not yet implemented."); + Ok(()) +} diff --git a/xtask/src/workspace.rs b/xtask/src/workspace.rs new file mode 100644 index 000000000..c97ffd5e6 --- /dev/null +++ b/xtask/src/workspace.rs @@ -0,0 +1,17 @@ +use std::{env, path::PathBuf}; + +use serde::Deserialize; +use xshell::cmd; + +use crate::Result; + +pub fn root_path() -> Result { + #[derive(Deserialize)] + struct Metadata { + workspace_root: PathBuf, + } + + let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_owned()); + let metadata_json = cmd!("{cargo} metadata --no-deps --format-version 1").read()?; + Ok(serde_json::from_str::(&metadata_json)?.workspace_root) +}