From 517b71cb0653640decffadf1c1fdb218e25a492d Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 27 Jan 2023 16:29:44 +0100 Subject: [PATCH 001/166] kotlin bindings: replace shell by xtask scripts --- Cargo.lock | 294 ++++++++++++++---------- bindings/kotlin/scripts/build_crypto.sh | 67 ------ bindings/kotlin/scripts/build_sdk.sh | 64 ------ xtask/src/kotlin.rs | 198 ++++++++++++++++ xtask/src/main.rs | 5 + 5 files changed, 375 insertions(+), 253 deletions(-) delete mode 100755 bindings/kotlin/scripts/build_crypto.sh delete mode 100755 bindings/kotlin/scripts/build_sdk.sh create mode 100644 xtask/src/kotlin.rs diff --git a/Cargo.lock b/Cargo.lock index 34a146d4c..e0b1b820d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,9 +57,9 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf6ccdb167abbf410dcb915cabd428929d7f6a04980b54a11f26a39f1c7f7107" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ "cfg-if", "getrandom 0.2.8", @@ -271,9 +271,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.61" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "705339e0e4a9690e2908d2b3d049d85682cf19fbd5782494498fbf7003a6a282" +checksum = "eff18d764974428cf3a9328e23fc5c986f5fbed46e6cd4cdf42544df5d297ec1" dependencies = [ "proc-macro2", "quote", @@ -308,9 +308,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.6.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1304eab461cf02bd70b083ed8273388f9724c549b316ba3d1e213ce0e9e7fb7e" +checksum = "e5694b64066a2459918d8074c2ce0d5a88f409431994c2356617c8ae0c4721fc" dependencies = [ "async-trait", "axum-core", @@ -339,9 +339,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f487e40dc9daee24d8a1779df88522f159a54a980f99cfbe43db0be0bd3444a8" +checksum = "1cae3e661676ffbacb30f1a824089a8c9150e71017f7e1e38f2aa32009188d34" dependencies = [ "async-trait", "bytes", @@ -395,6 +395,12 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" +[[package]] +name = "base64" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" + [[package]] name = "base64ct" version = "1.5.3" @@ -495,15 +501,15 @@ checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" [[package]] name = "bumpalo" -version = "3.11.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" [[package]] name = "bytemuck" -version = "1.12.3" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" +checksum = "c041d3eab048880cb0b86b256447da3f18859a163c3b8d8893f4e6368abe6393" [[package]] name = "byteorder" @@ -705,13 +711,13 @@ dependencies = [ [[package]] name = "clap" -version = "4.0.32" +version = "4.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7db700bc935f9e43e88d00b0850dae18a63773cfbec6d8e070fccf7fef89a39" +checksum = "f13b9c79b5d1dd500d20ef541215a6423c75829ef43117e1b4d17fd8af0b5d76" dependencies = [ "bitflags", - "clap_derive 4.0.21", - "clap_lex 0.3.0", + "clap_derive 4.1.0", + "clap_lex 0.3.1", "is-terminal", "once_cell", "strsim", @@ -733,9 +739,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.0.21" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" +checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8" dependencies = [ "heck", "proc-macro-error", @@ -755,9 +761,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" +checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade" dependencies = [ "os_str_bytes", ] @@ -810,18 +816,18 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b" +checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e" dependencies = [ "crossbeam-utils", ] [[package]] name = "console" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9b6515d269224923b26b5febea2ed42b2d5f2ce37284a4dd670fedd6cb8347a" +checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60" dependencies = [ "encode_unicode", "lazy_static", @@ -1075,9 +1081,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.86" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d1075c37807dcf850c379432f0df05ba52cc30f279c5cfc43cc221ce7f8579" +checksum = "b61a7545f753a88bcbe0a70de1fcc0221e10bfc752f576754fa91e663db1622e" dependencies = [ "cc", "cxxbridge-flags", @@ -1087,9 +1093,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.86" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5044281f61b27bc598f2f6647d480aed48d2bf52d6eb0b627d84c0361b17aa70" +checksum = "f464457d494b5ed6905c63b0c4704842aba319084a0a3561cdc1359536b53200" dependencies = [ "cc", "codespan-reporting", @@ -1102,15 +1108,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.86" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b50bc93ba22c27b0d31128d2d130a0a6b3d267ae27ef7e4fae2167dfe8781c" +checksum = "43c7119ce3a3701ed81aca8410b9acf6fc399d2629d057b87e2efa4e63a3aaea" [[package]] name = "cxxbridge-macro" -version = "1.0.86" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e61fda7e62115119469c7b3591fd913ecca96fb766cfd3f2e2502ab7bc87a5" +checksum = "65e07508b90551e610910fa648a1878991d367064997a596135b86df30daf07e" dependencies = [ "proc-macro2", "quote", @@ -1162,7 +1168,7 @@ dependencies = [ "hashbrown", "lock_api", "once_cell", - "parking_lot_core 0.9.5", + "parking_lot_core 0.9.6", ] [[package]] @@ -1256,11 +1262,12 @@ dependencies = [ [[package]] name = "dialoguer" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92e7e37ecef6857fdc0c0c5d42fd5b0938e46590c2183cc92dd310a6d078eb1" +checksum = "af3c796f3b0b408d9fd581611b47fa850821fcb84aa640b83a3c1a5be2d691f2" dependencies = [ "console", + "shell-words", "tempfile", "zeroize", ] @@ -1345,9 +1352,9 @@ dependencies = [ [[package]] name = "ed25519" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" dependencies = [ "serde", "signature", @@ -1502,7 +1509,7 @@ name = "example-emoji-verification" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.0.32", + "clap 4.1.4", "futures", "matrix-sdk", "tokio", @@ -1560,7 +1567,7 @@ name = "example-timeline" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.0.32", + "clap 4.1.4", "futures", "futures-signals", "matrix-sdk", @@ -1611,9 +1618,9 @@ dependencies = [ [[package]] name = "fd-lock" -version = "3.0.8" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb21c69b9fea5e15dbc1049e4b77145dd0ba1c84019c488102de0dc4ea4b0a27" +checksum = "28c0190ff0bd3b28bfdd4d0cf9f92faa12880fb0b8ae2054723dd6c76a4efd42" dependencies = [ "cfg-if", "rustix", @@ -1876,9 +1883,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.0" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec7af912d60cdbd3677c1af9352ebae6fb8394d165568a2234df0fa00f87793" +checksum = "221996f774192f0f718773def8201c4ae31f02616a54ccfc2d358bb0e5cefdec" [[package]] name = "glob" @@ -2219,11 +2226,11 @@ checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" [[package]] name = "inferno" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7207d75fcf6c1868f1390fc1c610431fe66328e9ee6813330a041ef6879eca1" +checksum = "d6e66fa9bb3c52f40d05c11b78919ff2f18993c2305bd8a62556d20cb3e9606f" dependencies = [ - "ahash 0.8.2", + "ahash 0.8.3", "atty", "indexmap", "itoa", @@ -2259,9 +2266,9 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" +checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e" dependencies = [ "libc", "windows-sys", @@ -2306,7 +2313,7 @@ version = "0.2.0" dependencies = [ "app_dirs2", "chrono", - "clap 4.0.32", + "clap 4.1.4", "dialoguer", "eyre", "futures", @@ -3055,9 +3062,9 @@ dependencies = [ [[package]] name = "napi" -version = "2.10.5" +version = "2.10.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c8ae31209e4268eae6003d37c298135d0f36e721b4d1fa91dd938a52388ccf" +checksum = "e1f86c0b4e791f8fa79448670b7294016560448696ebc87c89cab935c5c03df3" dependencies = [ "bitflags", "ctor", @@ -3075,9 +3082,9 @@ checksum = "882a73d9ef23e8dc2ebbffb6a6ae2ef467c0f18ac10711e4cc59c5485d41df0e" [[package]] name = "napi-derive" -version = "2.9.3" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4e44e34e70aa61be9036ae652e27c20db5bca80e006be0f482419f6601352a" +checksum = "0265746190f318c66aca7b2a6ad3cabff25906e43a939dddc17ea59300d0a827" dependencies = [ "convert_case", "napi-derive-backend", @@ -3088,9 +3095,9 @@ dependencies = [ [[package]] name = "napi-derive-backend" -version = "1.0.40" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17925fff04b6fa636f8e4b4608cc1a4f1360b64ac8ecbfdb7da1be1dc74f6843" +checksum = "aac6a809a2b7f1c2d29f835fa6a1c8aed4054738c5cf77f456f13009a13f528c" dependencies = [ "convert_case", "once_cell", @@ -3102,9 +3109,9 @@ dependencies = [ [[package]] name = "napi-sys" -version = "2.2.2" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "529671ebfae679f2ce9630b62dd53c72c56b3eb8b2c852e7e2fa91704ff93d67" +checksum = "166b5ef52a3ab5575047a9fe8d4a030cdd0f63c96f071cd6907674453b07bae3" dependencies = [ "libloading", ] @@ -3154,15 +3161,36 @@ dependencies = [ ] [[package]] -name = "nom" -version = "7.1.2" +name = "nix" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5507769c4919c998e69e49c839d9dc6e693ede4cc4290d6ad8b41d4f09c548c" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] +[[package]] +name = "nom8" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8" +dependencies = [ + "memchr", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -3247,9 +3275,9 @@ dependencies = [ [[package]] name = "object" -version = "0.30.1" +version = "0.30.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d864c91689fdc196779b98dba0aceac6118594c2df6ee5d943eb6a8df4d107a" +checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" dependencies = [ "memchr", ] @@ -3376,7 +3404,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.5", + "parking_lot_core 0.9.6", ] [[package]] @@ -3395,9 +3423,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" +checksum = "ba1ef8814b5c993410bb3adfad7a5ed269563e4a2f90c41f5d85be7fb47133bf" dependencies = [ "cfg-if", "libc", @@ -3571,7 +3599,7 @@ dependencies = [ "inferno", "libc", "log", - "nix", + "nix 0.24.3", "once_cell", "parking_lot 0.12.1", "smallvec", @@ -3588,13 +3616,12 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro-crate" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" +checksum = "66618389e4ec1c7afe67d51a9bf34ff9236480f8d51e7489b7d5ab0303c13f34" dependencies = [ "once_cell", - "thiserror", - "toml", + "toml_edit", ] [[package]] @@ -3623,9 +3650,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.49" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" +checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" dependencies = [ "unicode-ident", ] @@ -3649,9 +3676,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c01db6702aa05baa3f57dec92b8eeeeb4cb19e894e73996b32a4093289e54592" +checksum = "21dc42e00223fc37204bd4aa177e69420c604ca4a183209a8f9de30c6d934698" dependencies = [ "bytes", "prost-derive", @@ -3659,9 +3686,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8842bad1a5419bca14eac663ba798f6bc19c413c2fdceb5f3ba3b0932d96720" +checksum = "8bda8c0881ea9f722eb9629376db3d0b903b462477c1aafcb0566610ac28ac5d" dependencies = [ "anyhow", "itertools", @@ -3817,9 +3844,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.10.1" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" +checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b" dependencies = [ "crossbeam-channel", "crossbeam-deque", @@ -3884,11 +3911,11 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c" +checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9" dependencies = [ - "base64 0.13.1", + "base64 0.21.0", "bytes", "encoding_rs", "futures-core", @@ -4071,9 +4098,9 @@ checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" [[package]] name = "rustix" -version = "0.36.6" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4feacf7db682c6c329c4ede12649cd36ecab0f3be5b7d74e6a20304725db4549" +checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03" dependencies = [ "bitflags", "errno", @@ -4085,9 +4112,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.20.7" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c" +checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" dependencies = [ "log", "ring", @@ -4097,11 +4124,11 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" +checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ - "base64 0.13.1", + "base64 0.21.0", ] [[package]] @@ -4112,9 +4139,9 @@ checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" [[package]] name = "rustyline" -version = "10.0.0" +version = "10.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1cd5ae51d3f7bf65d7969d579d502168ef578f289452bd8ccc91de28fda20e" +checksum = "c1e83c32c3f3c33b08496e0d1df9ea8c64d39adb8eb36a1ebb1440c690697aef" dependencies = [ "bitflags", "cfg-if", @@ -4124,7 +4151,7 @@ dependencies = [ "libc", "log", "memchr", - "nix", + "nix 0.25.1", "radix_trie", "scopeguard", "unicode-segmentation", @@ -4233,9 +4260,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.7.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +checksum = "7c4437699b6d34972de58652c68b98cb5b53a4199ab126db8e20ec8ded29a721" dependencies = [ "bitflags", "core-foundation", @@ -4246,9 +4273,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" dependencies = [ "core-foundation-sys", "libc", @@ -4350,9 +4377,9 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.9.16" +version = "0.9.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92b5b431e8907b50339b51223b97d102db8d987ced36f6e4d03621db9316c834" +checksum = "8fb06d4b6cdaef0e0c51fa881acb721bed3c924cfaa71d9c94a3b771dfdf6567" dependencies = [ "indexmap", "itoa", @@ -4394,6 +4421,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "signal-hook" version = "0.3.14" @@ -4653,9 +4686,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.1.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" dependencies = [ "winapi-util", ] @@ -4804,9 +4837,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.24.1" +version = "1.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9f76183f91ecfb55e1d7d5602bd1d979e38a3a522fe900241cf195624d67ae" +checksum = "597a12a59981d9e3c38d216785b0c37399f6e415e8d0712047620f189371b0bb" dependencies = [ "autocfg", "bytes", @@ -4891,13 +4924,30 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.10" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4553f467ac8e3d374bc9a177a26801e5d0f9b211aa1673fb137a403afd1c9cf5" + +[[package]] +name = "toml_edit" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "729bfd096e40da9c001f778f5cdecbd2957929a24e10e5883d9392220a751581" +dependencies = [ + "indexmap", + "nom8", + "toml_datetime", +] + [[package]] name = "tower" version = "0.4.13" @@ -5107,9 +5157,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.8" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" +checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58" [[package]] name = "unicode-ident" @@ -5596,45 +5646,45 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" [[package]] name = "windows_aarch64_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" [[package]] name = "windows_i686_gnu" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" [[package]] name = "windows_i686_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" [[package]] name = "windows_x86_64_gnu" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" [[package]] name = "windows_x86_64_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" [[package]] name = "winreg" @@ -5647,9 +5697,9 @@ dependencies = [ [[package]] name = "wiremock" -version = "0.5.16" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "631cafe37a030d8453218cf7c650abcc359be8fba4a2fbc5c27fdb9728635406" +checksum = "12316b50eb725e22b2f6b9c4cbede5b7b89984274d113a7440c86e5c3fc6f99b" dependencies = [ "assert-json-diff", "async-trait", @@ -5708,7 +5758,7 @@ name = "xtask" version = "0.1.0" dependencies = [ "camino", - "clap 4.0.32", + "clap 4.1.4", "fs_extra", "serde", "serde_json", diff --git a/bindings/kotlin/scripts/build_crypto.sh b/bindings/kotlin/scripts/build_crypto.sh deleted file mode 100755 index 86756e848..000000000 --- a/bindings/kotlin/scripts/build_crypto.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash -set -eEu - -cd "$(dirname "$0")" -CURRENT_DIR=$(pwd) - -# FOR DEBUG -#RELEASE_FLAG="" -#RELEASE_TYPE_DIR="debug" -#RELEASE_AAR_NAME="crypto-android-debug" - -# FOR RELEASE -RELEASE_FLAG="--release" -RELEASE_TYPE_DIR="release" -RELEASE_AAR_NAME="crypto-android-release" - -SRC_ROOT=../../.. -# Path to the kotlin root project -KOTLIN_ROOT=.. - -BASE_TARGET_DIR="${SRC_ROOT}/target" -SDK_ROOT="${KOTLIN_ROOT}/crypto/crypto-android" -SDK_TARGET_DIR="${SDK_ROOT}/src/main/jniLibs" -BUILD_DIR="${SDK_ROOT}/build" -GENERATED_DIR="${BUILD_DIR}/generated/source/${RELEASE_TYPE_DIR}" -mkdir -p ${GENERATED_DIR} - -TARGET_CRATE=matrix-sdk-crypto-ffi - -AAR_DESTINATION=$1 - -# Build libs for all the different architectures - -echo -e "Building for x86_64-linux-android[1/4]" -cargo ndk --target x86_64-linux-android -o ${SDK_TARGET_DIR}/ build "${RELEASE_FLAG}" -p ${TARGET_CRATE} - -echo -e "Building for aarch64-linux-android[2/4]" -cargo ndk --target aarch64-linux-android -o ${SDK_TARGET_DIR}/ build "${RELEASE_FLAG}" -p ${TARGET_CRATE} - -echo -e "Building for armv7-linux-androideabi[3/4]" -cargo ndk --target armv7-linux-androideabi -o ${SDK_TARGET_DIR}/ build "${RELEASE_FLAG}" -p ${TARGET_CRATE} - -echo -e "Building for i686-linux-android[4/4]" -cargo ndk --target i686-linux-android -o ${SDK_TARGET_DIR}/ build "${RELEASE_FLAG}" -p ${TARGET_CRATE} - -# Generate uniffi files -echo -e "Generate uniffi kotlin file" -uniffi-bindgen generate "${SRC_ROOT}/bindings/${TARGET_CRATE}/src/olm.udl" \ - --language kotlin \ - --config "${SRC_ROOT}/bindings/${TARGET_CRATE}/uniffi.toml" \ - --out-dir ${GENERATED_DIR} \ - --lib-file "${BASE_TARGET_DIR}/x86_64-linux-android/${RELEASE_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" - -# Create android library -cd "${KOTLIN_ROOT}" -./gradlew :crypto:crypto-android:assemble -cd "${CURRENT_DIR}" - -echo -e "Moving the generated aar file to ${AAR_DESTINATION}/matrix-rust-sdk-crypto.aar" -mv "${BUILD_DIR}/outputs/aar/${RELEASE_AAR_NAME}.aar" "${AAR_DESTINATION}/matrix-rust-sdk-crypto.aar" - -# Clean-up -echo -e "Cleaning up temporary files" - -rm -r "${BUILD_DIR}" -rm -r "${SDK_TARGET_DIR}" - diff --git a/bindings/kotlin/scripts/build_sdk.sh b/bindings/kotlin/scripts/build_sdk.sh deleted file mode 100755 index 30c8d92b6..000000000 --- a/bindings/kotlin/scripts/build_sdk.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env bash -set -eEu - -cd "$(dirname "$0")" -CURRENT_DIR=$(pwd) - -# FOR DEBUG -#RELEASE_FLAG="" -#RELEASE_TYPE_DIR="debug" -#RELEASE_AAR_NAME="sdk-android-debug" - -# FOR RELEASE -RELEASE_FLAG="--release" -RELEASE_TYPE_DIR="release" -RELEASE_AAR_NAME="sdk-android-release" - -SRC_ROOT=../../.. -# Path to the kotlin root project -KOTLIN_ROOT=.. - -BASE_TARGET_DIR="${SRC_ROOT}/target" -SDK_ROOT="${KOTLIN_ROOT}/sdk/sdk-android" -SDK_TARGET_DIR="${SDK_ROOT}/src/main/jniLibs" -BUILD_DIR="${SDK_ROOT}/build" -GENERATED_DIR="${BUILD_DIR}/generated/source/${RELEASE_TYPE_DIR}" -mkdir -p ${GENERATED_DIR} - -AAR_DESTINATION=$1 - -# Build libs for all the different architectures - -echo -e "Building for x86_64-linux-android[1/4]" -cargo ndk --target x86_64-linux-android -o ${SDK_TARGET_DIR}/ build "${RELEASE_FLAG}" -p matrix-sdk-ffi - -echo -e "Building for aarch64-linux-android[2/4]" -cargo ndk --target aarch64-linux-android -o ${SDK_TARGET_DIR}/ build "${RELEASE_FLAG}" -p matrix-sdk-ffi - -echo -e "Building for armv7-linux-androideabi[3/4]" -cargo ndk --target armv7-linux-androideabi -o ${SDK_TARGET_DIR}/ build "${RELEASE_FLAG}" -p matrix-sdk-ffi - -echo -e "Building for i686-linux-android[4/4]" -cargo ndk --target i686-linux-android -o ${SDK_TARGET_DIR}/ build "${RELEASE_FLAG}" -p matrix-sdk-ffi - -# Generate uniffi files -echo -e "Generate uniffi kotlin file" -uniffi-bindgen generate "${SRC_ROOT}/bindings/matrix-sdk-ffi/src/api.udl" \ - --language kotlin \ - --out-dir ${GENERATED_DIR} \ - --lib-file "${BASE_TARGET_DIR}/x86_64-linux-android/${RELEASE_TYPE_DIR}/libmatrix_sdk_ffi.a" - -# Create android library -cd "${KOTLIN_ROOT}" -./gradlew :sdk:sdk-android:assemble -cd "${CURRENT_DIR}" - -echo -e "Moving the generated aar file to ${AAR_DESTINATION}/matrix-rust-sdk.aar" -mv "${BUILD_DIR}/outputs/aar/${RELEASE_AAR_NAME}.aar" "${AAR_DESTINATION}/matrix-rust-sdk.aar" - -# Clean-up -echo -e "Cleaning up temporary files" - -rm -r "${BUILD_DIR}" -rm -r "${SDK_TARGET_DIR}" - diff --git a/xtask/src/kotlin.rs b/xtask/src/kotlin.rs new file mode 100644 index 000000000..2c9281947 --- /dev/null +++ b/xtask/src/kotlin.rs @@ -0,0 +1,198 @@ +use std::{ + fs::{create_dir_all, remove_dir_all, rename}, + path::{Path, PathBuf}, +}; + +use clap::{Args, Subcommand, ValueEnum}; +use xshell::{cmd, pushd}; + +use crate::{workspace, Result}; + +struct PackageValues { + name: &'static str, + udl_path: &'static str, + gradle_path: &'static str, + gradle_module: &'static str, + aar_name: &'static str, +} + +#[derive(ValueEnum, Clone)] +enum Package { + CryptoSDK, + FullSDK, +} + +impl Package { + fn values(self) -> PackageValues { + match self { + Package::CryptoSDK => PackageValues { + name: "matrix-sdk-crypto-ffi", + udl_path : "bindings/matrix-sdk-crypto-ffi/src/olm.udl", + gradle_path : "crypto/crypto-android", + gradle_module : "crypto:crypto-android:", + aar_name : "crypto-android", + }, + Package::FullSDK => PackageValues { + name: "matrix-sdk-ffi", + udl_path : "bindings/matrix-sdk-ffi/src/api.udl", + gradle_path : "sdk/sdk-android", + gradle_module : ":sdk:sdk-android:", + aar_name : "sdk-android", + }, + } + } +} + +#[derive(Args)] +pub struct KotlinArgs { + #[clap(subcommand)] + cmd: KotlinCommand, +} + +#[derive(Subcommand)] +enum KotlinCommand { + /// Builds the SDK for Android as an AAR. + BuildAndroidLibrary { + #[clap(value_enum, long)] + package: Package, + /// Build with the release profile + #[clap(long)] + release: bool, + + /// Build with a custom profile, takes precedence over `--release` + #[clap(long)] + profile: Option, + + /// Build the given target only + #[clap(long)] + only_target: Option, + + /// Move the generated aar into the given path + #[clap(long)] + aar_path: Option, + }, +} + +impl KotlinArgs { + pub fn run(self) -> Result<()> { + let _p = pushd(workspace::root_path()?)?; + + match self.cmd { + KotlinCommand::BuildAndroidLibrary { release, profile, aar_path, only_target,package } => { + let profile = profile.as_deref().unwrap_or(if release { "release" } else { "dev" }); + build_android_library(profile, only_target, aar_path, package) + } + } + } +} + +fn build_android_library( + profile: &str, + only_target: Option, + aar_path: Option, + package: Package, +) -> Result<()> { + + let root_dir = workspace::root_path()?; + + let package_values = package.values(); + let package_name = package_values.name; + let gradle_path = package_values.gradle_path; + let udl_path = root_dir.join(package_values.udl_path); + let kotlin_dir = root_dir.join("bindings/kotlin"); + + let gradle_root_dir = kotlin_dir.join(gradle_path); + let gradle_target_dir = gradle_root_dir.join("src/main/jniLibs"); + let gradle_build_dir = gradle_root_dir.join("build"); + + let build_variant = get_build_variant_values(profile).0; + let gradle_generated_dir_path = format!("generated/source/{build_variant}"); + let gradle_generated_dir = gradle_build_dir.join(gradle_generated_dir_path); + create_dir_all(gradle_generated_dir.clone())?; + + let sdk_target_dir_str = gradle_target_dir.to_str().unwrap(); + + let uniffi_lib_path = if let Some(target) = only_target { + println!("-- Building for {target} [1/1]"); + build_for_android_target(target.as_str(), profile,sdk_target_dir_str, package_name)? + } else { + println!("-- Building for x86_64-linux-android[1/4]"); + build_for_android_target("x86_64-linux-android", profile,sdk_target_dir_str, package_name)?; + println!("-- Building for aarch64-linux-android[2/4]"); + build_for_android_target("aarch64-linux-android", profile,sdk_target_dir_str, package_name)?; + println!("-- Building for armv7-linux-androideabi[3/4]"); + build_for_android_target("armv7-linux-androideabi", profile,sdk_target_dir_str, package_name)?; + println!("-- Building for i686-linux-android[4/4]"); + build_for_android_target("i686-linux-android", profile,sdk_target_dir_str, package_name)? + }; + + println!("-- Generate uniffi files"); + generate_uniffi_bindings(&udl_path, &uniffi_lib_path, &gradle_generated_dir)?; + + println!("-- Building aar"); + build_android_aar(profile, &kotlin_dir, &gradle_build_dir, aar_path, &package_values)?; + + println!("-- Cleaning up temporary files"); + remove_dir_all(gradle_build_dir.as_path())?; + remove_dir_all(gradle_target_dir.as_path())?; + + println!("-- All done and hunky dory. Enjoy!"); + Ok(()) +} + +fn generate_uniffi_bindings(udl_path: &Path, library_path: &Path, ffi_generated_dir: &Path) -> Result<()> { + println!("-- library_path = {}", library_path.to_string_lossy()); + let udl_file = camino::Utf8Path::from_path(udl_path).unwrap(); + let out_dir_overwrite = camino::Utf8Path::from_path(ffi_generated_dir).unwrap(); + let library_file = camino::Utf8Path::from_path(library_path).unwrap(); + + uniffi_bindgen::generate_bindings( + udl_file, + None, + vec!["kotlin"], + Some(out_dir_overwrite), + Some(library_file), + false, + )?; + Ok(()) +} + +fn build_for_android_target(target: &str, profile: &str, dest_dir: &str, package_name: &str) -> Result { + + cmd!("cargo ndk --target {target} -o {dest_dir} build --profile {profile} -p {package_name}").run()?; + + // The builtin dev profile has its files stored under target/debug, all + // other targets have matching directory names + let profile_dir_name = if profile == "dev" { "debug" } else { profile }; + let package_camel = package_name.replace("-","_"); + let lib_name = format!("lib{package_camel}.a"); + Ok(workspace::target_path()?.join(target).join(profile_dir_name).join(lib_name)) +} + +fn build_android_aar(profile: &str, kotlin_dir: &Path, build_dir: &Path, aar_path: Option, package_values: &PackageValues) -> Result<()> { + let _p = pushd(kotlin_dir)?; + let (build_variant, build_variant_command) = get_build_variant_values(profile); + let gradle_module = package_values.gradle_module; + + cmd!("./gradlew {gradle_module}{build_variant_command}").run()?; + + if let Some(path) = aar_path { + let aar_name = package_values.aar_name; + let full_aar_name = &format!("{aar_name}-{build_variant}.aar"); + println!("-- Copying aar to {path:?}"); + let aar_path = &format!("outputs/aar/{full_aar_name}"); + rename( + build_dir.join(aar_path), + path.join(full_aar_name), + )?; + }; + Ok(()) +} + +fn get_build_variant_values(profile: &str)-> (&str, &str){ + if profile == "dev" { + ("debug", "assembleDebug") + } else { + ("release","assembleRelease") + } +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 15cfd9443..abb7b6e25 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,12 +1,14 @@ mod ci; mod fixup; mod swift; +mod kotlin; mod workspace; use ci::CiArgs; use clap::{Parser, Subcommand}; use fixup::FixupArgs; use swift::SwiftArgs; +use kotlin::KotlinArgs; use xshell::cmd; type Result> = std::result::Result; @@ -30,6 +32,8 @@ enum Command { open: bool, }, Swift(SwiftArgs), + Kotlin(KotlinArgs) + } fn main() -> Result<()> { @@ -38,6 +42,7 @@ fn main() -> Result<()> { Command::Fixup(cfg) => cfg.run(), Command::Doc { open } => build_docs(open.then_some("--open"), DenyWarnings::No), Command::Swift(cfg) => cfg.run(), + Command::Kotlin(cfg) => cfg.run(), } } From 1bcd8c7aae3a5e823df916156b2d0d793229b8bc Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 2 Feb 2023 11:50:47 +0100 Subject: [PATCH 002/166] Comment remove gradle_build --- xtask/src/kotlin.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xtask/src/kotlin.rs b/xtask/src/kotlin.rs index 2c9281947..6c656bd0c 100644 --- a/xtask/src/kotlin.rs +++ b/xtask/src/kotlin.rs @@ -133,7 +133,7 @@ fn build_android_library( build_android_aar(profile, &kotlin_dir, &gradle_build_dir, aar_path, &package_values)?; println!("-- Cleaning up temporary files"); - remove_dir_all(gradle_build_dir.as_path())?; + //remove_dir_all(gradle_build_dir.as_path())?; remove_dir_all(gradle_target_dir.as_path())?; println!("-- All done and hunky dory. Enjoy!"); From d600af7c0ede527ac3a5cea7d925c2fc31f5970a Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 16 Feb 2023 15:10:48 +0100 Subject: [PATCH 003/166] feat(crypto-nodejs): `OlmMachine.initialize` takes a new optional store type. This patch allows an `OlmMachine` to use a different store than Sled, like SQLite. A new enum is introduced: `StoreType` which 2 variants: `Sled` (the default), and `Sqlite`. The `OlmMachine.initialize` constructor now takes a new optional `store_type: Option` argument. If no value is passed, the default `StoreType` variant will be used (as mentioned: `StoreType.Sled`). The code has been rewritten a little bit to make the type system happy without introducing too much type indirections. This patch finally adds a parameterized tests that exhaustively test: no store type, `Sled` and `Sqlite`. --- Cargo.lock | 1 + bindings/matrix-sdk-crypto-nodejs/Cargo.toml | 1 + .../matrix-sdk-crypto-nodejs/src/machine.rs | 102 ++++++++++++------ .../tests/machine.test.js | 50 ++++++--- 4 files changed, 111 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1c2b56288..27bc19c67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2877,6 +2877,7 @@ dependencies = [ "matrix-sdk-common", "matrix-sdk-crypto", "matrix-sdk-sled", + "matrix-sdk-sqlite", "napi", "napi-build", "napi-derive", diff --git a/bindings/matrix-sdk-crypto-nodejs/Cargo.toml b/bindings/matrix-sdk-crypto-nodejs/Cargo.toml index 85e965b3b..15d4eca0d 100644 --- a/bindings/matrix-sdk-crypto-nodejs/Cargo.toml +++ b/bindings/matrix-sdk-crypto-nodejs/Cargo.toml @@ -26,6 +26,7 @@ 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"] } +matrix-sdk-sqlite = { version = "0.1.0", path = "../../crates/matrix-sdk-sqlite", features = ["crypto-store"] } ruma = { workspace = true, features = ["rand", "unstable-msc2677"] } napi = { version = "2.9.1", default-features = false, features = ["napi6", "tokio_rt"] } napi-derive = "2.9.1" diff --git a/bindings/matrix-sdk-crypto-nodejs/src/machine.rs b/bindings/matrix-sdk-crypto-nodejs/src/machine.rs index 82c7bb3c9..52ef0e8d5 100644 --- a/bindings/matrix-sdk-crypto-nodejs/src/machine.rs +++ b/bindings/matrix-sdk-crypto-nodejs/src/machine.rs @@ -2,11 +2,12 @@ use std::{ collections::{BTreeMap, HashMap}, + mem::ManuallyDrop, ops::Deref, sync::Arc, }; -use napi::bindgen_prelude::Either7; +use napi::bindgen_prelude::{within_runtime_if_available, Either7, ToNapiValue}; use napi_derive::*; use ruma::{serde::Raw, DeviceKeyAlgorithm, OwnedTransactionId, UInt}; use serde_json::{value::RawValue, Value as JsonValue}; @@ -27,10 +28,21 @@ use crate::{ /// /// Using the `OlmMachine` when its state is `Closed` will panic. enum OlmMachineInner { - Opened(matrix_sdk_crypto::OlmMachine), + Opened(ManuallyDrop), Closed, } +impl Drop for OlmMachineInner { + fn drop(&mut self) { + if let Self::Opened(machine) = self { + // SAFETY: `self` won't be used anymore after this `take`, so it's safe to do it + // here. + let machine = unsafe { ManuallyDrop::take(machine) }; + within_runtime_if_available(move || drop(machine)); + } + } +} + impl Deref for OlmMachineInner { type Target = matrix_sdk_crypto::OlmMachine; @@ -43,6 +55,18 @@ impl Deref for OlmMachineInner { } } +/// Represents the type of store an `OlmMachine` can use. +#[derive(Default)] +#[napi] +pub enum StoreType { + /// Use `matrix-sdk-sled`. + #[default] + Sled, + + /// Use `matrix-sdk-sqlite`. + Sqlite, +} + /// State machine implementation of the Olm/Megolm encryption protocol /// used for Matrix end to end encryption. // #[napi(custom_finalize)] @@ -87,40 +111,56 @@ impl OlmMachine { device_id: &identifiers::DeviceId, store_path: Option, mut store_passphrase: Option, + store_type: Option, ) -> napi::Result { - let user_id = user_id.clone(); - let device_id = device_id.clone(); + let user_id = user_id.clone().inner; + let device_id = device_id.clone().inner; - let store = if let Some(store_path) = store_path { - Some( - matrix_sdk_sled::SledCryptoStore::open(store_path, store_passphrase.as_deref()) - .await - .map(Arc::new) - .map_err(into_err)?, - ) - } else { - None - }; - - store_passphrase.zeroize(); + let user_id = user_id.as_ref(); + let device_id = device_id.as_ref(); Ok(OlmMachine { - inner: OlmMachineInner::Opened(match store { - Some(store) => matrix_sdk_crypto::OlmMachine::with_store( - user_id.inner.as_ref(), - device_id.inner.as_ref(), - store, - ) - .await - .map_err(into_err)?, - None => { - matrix_sdk_crypto::OlmMachine::new( - user_id.inner.as_ref(), - device_id.inner.as_ref(), - ) - .await + inner: OlmMachineInner::Opened(ManuallyDrop::new(match store_path { + Some(store_path) => { + let machine = match store_type.unwrap_or_default() { + StoreType::Sled => { + matrix_sdk_crypto::OlmMachine::with_store( + user_id, + device_id, + matrix_sdk_sled::SledCryptoStore::open( + store_path, + store_passphrase.as_deref(), + ) + .await + .map(Arc::new) + .map_err(into_err)?, + ) + .await + } + + StoreType::Sqlite => { + matrix_sdk_crypto::OlmMachine::with_store( + user_id, + device_id, + matrix_sdk_sqlite::SqliteCryptoStore::open( + store_path, + store_passphrase.as_deref(), + ) + .await + .map(Arc::new) + .map_err(into_err)?, + ) + .await + } + }; + + store_passphrase.zeroize(); + + machine.map_err(into_err)? } - }), + + None => matrix_sdk_crypto::OlmMachine::new(user_id, device_id).await, + })), }) } diff --git a/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js b/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js index 5e00930ab..74611aaf9 100644 --- a/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js +++ b/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js @@ -14,11 +14,19 @@ const { VerificationState, CrossSigningStatus, MaybeSignature, + StoreType, } = require("../"); const path = require("path"); const os = require("os"); const fs = require("fs/promises"); +describe("StoreType", () => { + test("has the correct variant values", () => { + expect(StoreType.Sled).toStrictEqual(0); + expect(StoreType.Sqlite).toStrictEqual(1); + }); +}); + describe(OlmMachine.name, () => { test("cannot be instantiated with the constructor", () => { expect(() => { @@ -31,21 +39,39 @@ describe(OlmMachine.name, () => { }); describe("can be instantiated with a store", () => { - test("with no passphrase", async () => { - const temp_directory = await fs.mkdtemp(path.join(os.tmpdir(), "matrix-sdk-crypto--")); + for (const [store_type, store_name] of [ + [StoreType.Sled, "sled"], + [StoreType.Sqlite, "sqlite"], + [null, "default"], + ]) { + test(`with no passphrase (store: ${store_name})`, async () => { + const temp_directory = await fs.mkdtemp(path.join(os.tmpdir(), "matrix-sdk-crypto--")); - expect( - await OlmMachine.initialize(new UserId("@foo:bar.org"), new DeviceId("baz"), temp_directory), - ).toBeInstanceOf(OlmMachine); - }); + expect( + await OlmMachine.initialize( + new UserId("@foo:bar.org"), + new DeviceId("baz"), + temp_directory, + null, + store_type, + ), + ).toBeInstanceOf(OlmMachine); + }); - test("with a passphrase", async () => { - const temp_directory = await fs.mkdtemp(path.join(os.tmpdir(), "matrix-sdk-crypto--")); + test(`with a passphrase (store: ${store_name})`, async () => { + const temp_directory = await fs.mkdtemp(path.join(os.tmpdir(), "matrix-sdk-crypto--")); - expect( - await OlmMachine.initialize(new UserId("@foo:bar.org"), new DeviceId("baz"), temp_directory, "hello"), - ).toBeInstanceOf(OlmMachine); - }); + expect( + await OlmMachine.initialize( + new UserId("@foo:bar.org"), + new DeviceId("baz"), + temp_directory, + "hello", + store_type, + ), + ).toBeInstanceOf(OlmMachine); + }); + } }); const user = new UserId("@alice:example.org"); From ae4ed2e7c88b514d21f1ec7eaea1f21c09a66ac3 Mon Sep 17 00:00:00 2001 From: Archit Bhonsle Date: Wed, 22 Feb 2023 14:15:25 +0530 Subject: [PATCH 004/166] feat(crypto): Re-expose the version of vodozemac --- crates/matrix-sdk-crypto/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index 875ca0914..d0a4ecb22 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -103,6 +103,6 @@ pub mod vodozemac { olm::{ DecryptionError as OlmDecryptionError, SessionCreationError as OlmSessionCreationError, }, - DecodeError, KeyError, PickleError, SignatureError, + DecodeError, KeyError, PickleError, SignatureError, VERSION, }; } From 260e7c2d5ee43c94243dccb6bd5e7e9214d1b147 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 22 Feb 2023 10:28:08 +0100 Subject: [PATCH 005/166] Update kotlin xtask to not build aar --- xtask/src/kotlin.rs | 85 +++++++++------------------------------------ 1 file changed, 17 insertions(+), 68 deletions(-) diff --git a/xtask/src/kotlin.rs b/xtask/src/kotlin.rs index 6c656bd0c..f00ebbbde 100644 --- a/xtask/src/kotlin.rs +++ b/xtask/src/kotlin.rs @@ -1,5 +1,5 @@ use std::{ - fs::{create_dir_all, remove_dir_all, rename}, + fs::create_dir_all, path::{Path, PathBuf}, }; @@ -11,9 +11,6 @@ use crate::{workspace, Result}; struct PackageValues { name: &'static str, udl_path: &'static str, - gradle_path: &'static str, - gradle_module: &'static str, - aar_name: &'static str, } #[derive(ValueEnum, Clone)] @@ -28,16 +25,10 @@ impl Package { Package::CryptoSDK => PackageValues { name: "matrix-sdk-crypto-ffi", udl_path : "bindings/matrix-sdk-crypto-ffi/src/olm.udl", - gradle_path : "crypto/crypto-android", - gradle_module : "crypto:crypto-android:", - aar_name : "crypto-android", }, Package::FullSDK => PackageValues { name: "matrix-sdk-ffi", udl_path : "bindings/matrix-sdk-ffi/src/api.udl", - gradle_path : "sdk/sdk-android", - gradle_module : ":sdk:sdk-android:", - aar_name : "sdk-android", }, } } @@ -67,9 +58,9 @@ enum KotlinCommand { #[clap(long)] only_target: Option, - /// Move the generated aar into the given path + /// Move the generated files into the given src direct #[clap(long)] - aar_path: Option, + src_dir: PathBuf, }, } @@ -78,9 +69,9 @@ impl KotlinArgs { let _p = pushd(workspace::root_path()?)?; match self.cmd { - KotlinCommand::BuildAndroidLibrary { release, profile, aar_path, only_target,package } => { + KotlinCommand::BuildAndroidLibrary { release, profile, src_dir, only_target,package } => { let profile = profile.as_deref().unwrap_or(if release { "release" } else { "dev" }); - build_android_library(profile, only_target, aar_path, package) + build_android_library(profile, only_target, src_dir, package) } } } @@ -89,7 +80,7 @@ impl KotlinArgs { fn build_android_library( profile: &str, only_target: Option, - aar_path: Option, + src_dir: PathBuf, package: Package, ) -> Result<()> { @@ -97,44 +88,30 @@ fn build_android_library( let package_values = package.values(); let package_name = package_values.name; - let gradle_path = package_values.gradle_path; let udl_path = root_dir.join(package_values.udl_path); - let kotlin_dir = root_dir.join("bindings/kotlin"); - let gradle_root_dir = kotlin_dir.join(gradle_path); - let gradle_target_dir = gradle_root_dir.join("src/main/jniLibs"); - let gradle_build_dir = gradle_root_dir.join("build"); + let jni_libs_dir = src_dir.join("jniLibs"); + let jni_libs_dir_str = jni_libs_dir.to_str().unwrap(); - let build_variant = get_build_variant_values(profile).0; - let gradle_generated_dir_path = format!("generated/source/{build_variant}"); - let gradle_generated_dir = gradle_build_dir.join(gradle_generated_dir_path); - create_dir_all(gradle_generated_dir.clone())?; - - let sdk_target_dir_str = gradle_target_dir.to_str().unwrap(); + let kotlin_generated_dir = src_dir.join("kotlin"); + create_dir_all(kotlin_generated_dir.clone())?; let uniffi_lib_path = if let Some(target) = only_target { println!("-- Building for {target} [1/1]"); - build_for_android_target(target.as_str(), profile,sdk_target_dir_str, package_name)? + build_for_android_target(target.as_str(), profile,jni_libs_dir_str, package_name)? } else { println!("-- Building for x86_64-linux-android[1/4]"); - build_for_android_target("x86_64-linux-android", profile,sdk_target_dir_str, package_name)?; + build_for_android_target("x86_64-linux-android", profile,jni_libs_dir_str, package_name)?; println!("-- Building for aarch64-linux-android[2/4]"); - build_for_android_target("aarch64-linux-android", profile,sdk_target_dir_str, package_name)?; + build_for_android_target("aarch64-linux-android", profile,jni_libs_dir_str, package_name)?; println!("-- Building for armv7-linux-androideabi[3/4]"); - build_for_android_target("armv7-linux-androideabi", profile,sdk_target_dir_str, package_name)?; + build_for_android_target("armv7-linux-androideabi", profile,jni_libs_dir_str, package_name)?; println!("-- Building for i686-linux-android[4/4]"); - build_for_android_target("i686-linux-android", profile,sdk_target_dir_str, package_name)? + build_for_android_target("i686-linux-android", profile,jni_libs_dir_str, package_name)? }; println!("-- Generate uniffi files"); - generate_uniffi_bindings(&udl_path, &uniffi_lib_path, &gradle_generated_dir)?; - - println!("-- Building aar"); - build_android_aar(profile, &kotlin_dir, &gradle_build_dir, aar_path, &package_values)?; - - println!("-- Cleaning up temporary files"); - //remove_dir_all(gradle_build_dir.as_path())?; - remove_dir_all(gradle_target_dir.as_path())?; + generate_uniffi_bindings(&udl_path, &uniffi_lib_path, &kotlin_generated_dir)?; println!("-- All done and hunky dory. Enjoy!"); Ok(()) @@ -165,34 +142,6 @@ fn build_for_android_target(target: &str, profile: &str, dest_dir: &str, package // other targets have matching directory names let profile_dir_name = if profile == "dev" { "debug" } else { profile }; let package_camel = package_name.replace("-","_"); - let lib_name = format!("lib{package_camel}.a"); + let lib_name = format!("lib{package_camel}.so"); Ok(workspace::target_path()?.join(target).join(profile_dir_name).join(lib_name)) } - -fn build_android_aar(profile: &str, kotlin_dir: &Path, build_dir: &Path, aar_path: Option, package_values: &PackageValues) -> Result<()> { - let _p = pushd(kotlin_dir)?; - let (build_variant, build_variant_command) = get_build_variant_values(profile); - let gradle_module = package_values.gradle_module; - - cmd!("./gradlew {gradle_module}{build_variant_command}").run()?; - - if let Some(path) = aar_path { - let aar_name = package_values.aar_name; - let full_aar_name = &format!("{aar_name}-{build_variant}.aar"); - println!("-- Copying aar to {path:?}"); - let aar_path = &format!("outputs/aar/{full_aar_name}"); - rename( - build_dir.join(aar_path), - path.join(full_aar_name), - )?; - }; - Ok(()) -} - -fn get_build_variant_values(profile: &str)-> (&str, &str){ - if profile == "dev" { - ("debug", "assembleDebug") - } else { - ("release","assembleRelease") - } -} From 953d4c0ef77a2bf11205d05ba654251be92ca71a Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 22 Feb 2023 11:00:05 +0100 Subject: [PATCH 006/166] Remove kotlin bindings for now --- bindings/kotlin/README.md | 23 --- bindings/kotlin/SECURITY.md | 5 - bindings/kotlin/build.gradle | 23 --- bindings/kotlin/buildSrc/build.gradle.kts | 9 - .../src/main/java/ConfigurationData.kt | 11 -- .../buildSrc/src/main/java/Dependencies.kt | 22 --- .../kotlin/crypto/crypto-android/.gitignore | 1 - .../kotlin/crypto/crypto-android/build.gradle | 51 ----- .../crypto/crypto-android/consumer-rules.pro | 0 .../crypto/crypto-android/proguard-rules.pro | 21 -- .../src/main/AndroidManifest.xml | 4 - bindings/kotlin/crypto/crypto-jvm/.gitignore | 1 - .../kotlin/crypto/crypto-jvm/build.gradle | 13 -- bindings/kotlin/gradle.properties | 25 --- .../kotlin/gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 - bindings/kotlin/gradlew | 185 ------------------ bindings/kotlin/gradlew.bat | 89 --------- bindings/kotlin/scripts/publish-module.gradle | 77 -------- bindings/kotlin/scripts/publish-root.gradle | 43 ---- bindings/kotlin/sdk/sdk-android/.gitignore | 1 - bindings/kotlin/sdk/sdk-android/build.gradle | 51 ----- .../kotlin/sdk/sdk-android/consumer-rules.pro | 0 .../kotlin/sdk/sdk-android/proguard-rules.pro | 21 -- .../sdk-android/src/main/AndroidManifest.xml | 2 - bindings/kotlin/sdk/sdk-jvm/.gitignore | 1 - bindings/kotlin/sdk/sdk-jvm/build.gradle | 13 -- bindings/kotlin/settings.gradle | 27 --- 28 files changed, 725 deletions(-) delete mode 100644 bindings/kotlin/README.md delete mode 100644 bindings/kotlin/SECURITY.md delete mode 100644 bindings/kotlin/build.gradle delete mode 100644 bindings/kotlin/buildSrc/build.gradle.kts delete mode 100644 bindings/kotlin/buildSrc/src/main/java/ConfigurationData.kt delete mode 100644 bindings/kotlin/buildSrc/src/main/java/Dependencies.kt delete mode 100644 bindings/kotlin/crypto/crypto-android/.gitignore delete mode 100644 bindings/kotlin/crypto/crypto-android/build.gradle delete mode 100644 bindings/kotlin/crypto/crypto-android/consumer-rules.pro delete mode 100644 bindings/kotlin/crypto/crypto-android/proguard-rules.pro delete mode 100644 bindings/kotlin/crypto/crypto-android/src/main/AndroidManifest.xml delete mode 100644 bindings/kotlin/crypto/crypto-jvm/.gitignore delete mode 100644 bindings/kotlin/crypto/crypto-jvm/build.gradle delete mode 100644 bindings/kotlin/gradle.properties delete mode 100644 bindings/kotlin/gradle/wrapper/gradle-wrapper.jar delete mode 100644 bindings/kotlin/gradle/wrapper/gradle-wrapper.properties delete mode 100755 bindings/kotlin/gradlew delete mode 100644 bindings/kotlin/gradlew.bat delete mode 100644 bindings/kotlin/scripts/publish-module.gradle delete mode 100644 bindings/kotlin/scripts/publish-root.gradle delete mode 100644 bindings/kotlin/sdk/sdk-android/.gitignore delete mode 100644 bindings/kotlin/sdk/sdk-android/build.gradle delete mode 100644 bindings/kotlin/sdk/sdk-android/consumer-rules.pro delete mode 100644 bindings/kotlin/sdk/sdk-android/proguard-rules.pro delete mode 100644 bindings/kotlin/sdk/sdk-android/src/main/AndroidManifest.xml delete mode 100644 bindings/kotlin/sdk/sdk-jvm/.gitignore delete mode 100644 bindings/kotlin/sdk/sdk-jvm/build.gradle delete mode 100644 bindings/kotlin/settings.gradle diff --git a/bindings/kotlin/README.md b/bindings/kotlin/README.md deleted file mode 100644 index ebb206b7e..000000000 --- a/bindings/kotlin/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Matrix rust components kotlin - -This project and build scripts demonstrate how to create an aar and how to import it in your android projects. - -## Prerequisites - -* the Rust toolchain -* cargo-ndk < 2.12.0 `cargo install cargo-ndk --version 2.11.0` -* android targets (e.g. `rustup target add \ - aarch64-linux-android \ - armv7-linux-androideabi \ - x86_64-linux-android \ - i686-linux-android`) - -## Building the SDK - -To build the full sdk and get an aar you can call : -`./bindings/kotlin/scripts/build_sdk.sh /matrix-rust_sdk/bindings/kotlin/sample/libs` -where the parameter is the path for the aar to go - -## License - -[Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) diff --git a/bindings/kotlin/SECURITY.md b/bindings/kotlin/SECURITY.md deleted file mode 100644 index 3126b47a0..000000000 --- a/bindings/kotlin/SECURITY.md +++ /dev/null @@ -1,5 +0,0 @@ -# Reporting a Vulnerability - -**If you've found a security vulnerability, please report it to security@matrix.org** - -For more information on our security disclosure policy, visit https://www.matrix.org/security-disclosure-policy/ diff --git a/bindings/kotlin/build.gradle b/bindings/kotlin/build.gradle deleted file mode 100644 index 4924a37c7..000000000 --- a/bindings/kotlin/build.gradle +++ /dev/null @@ -1,23 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. - -apply plugin: 'io.github.gradle-nexus.publish-plugin' -apply from: "${rootDir}/scripts/publish-root.gradle" - -buildscript { - repositories { - maven { url "https://plugins.gradle.org/m2/" } - google() - mavenCentral() - } - - dependencies { - classpath BuildPlugins.android - classpath BuildPlugins.kotlin - classpath BuildPlugins.nexusPublish - } -} - - -task clean(type: Delete) { - delete rootProject.buildDir -} \ No newline at end of file diff --git a/bindings/kotlin/buildSrc/build.gradle.kts b/bindings/kotlin/buildSrc/build.gradle.kts deleted file mode 100644 index 8e88a958d..000000000 --- a/bindings/kotlin/buildSrc/build.gradle.kts +++ /dev/null @@ -1,9 +0,0 @@ -import org.gradle.kotlin.dsl.`kotlin-dsl` - -plugins { - `kotlin-dsl` -} - -repositories { - mavenCentral() -} \ No newline at end of file diff --git a/bindings/kotlin/buildSrc/src/main/java/ConfigurationData.kt b/bindings/kotlin/buildSrc/src/main/java/ConfigurationData.kt deleted file mode 100644 index e32455732..000000000 --- a/bindings/kotlin/buildSrc/src/main/java/ConfigurationData.kt +++ /dev/null @@ -1,11 +0,0 @@ -object ConfigurationData { - const val compileSdk = 31 - const val targetSdk = 31 - const val minSdk = 21 - const val majorVersion = 0 - const val minorVersion = 2 - const val patchVersion = 0 - const val versionName = "$majorVersion.$minorVersion.$patchVersion" - const val snapshotVersionName = "$majorVersion.$minorVersion.${patchVersion + 1}-SNAPSHOT" - const val publishGroupId = "org.matrix.rustcomponents" -} \ No newline at end of file diff --git a/bindings/kotlin/buildSrc/src/main/java/Dependencies.kt b/bindings/kotlin/buildSrc/src/main/java/Dependencies.kt deleted file mode 100644 index f5f6e394e..000000000 --- a/bindings/kotlin/buildSrc/src/main/java/Dependencies.kt +++ /dev/null @@ -1,22 +0,0 @@ -internal object Versions { - const val androidGradlePlugin = "7.1.2" - const val kotlin = "1.6.10" - const val jUnit = "4.12" - const val nexusPublishGradlePlugin = "1.1.0" - const val jna = "5.10.0" -} - -internal object BuildPlugins { - const val android = "com.android.tools.build:gradle:${Versions.androidGradlePlugin}" - const val kotlin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}" - const val nexusPublish = "io.github.gradle-nexus:publish-plugin:${Versions.nexusPublishGradlePlugin}" -} - -/** - * To define dependencies - */ -internal object Dependencies { - const val kotlin = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.kotlin}" - const val junit = "junit:junit:${Versions.jUnit}" - const val jna = "net.java.dev.jna:jna:${Versions.jna}@aar" -} \ No newline at end of file diff --git a/bindings/kotlin/crypto/crypto-android/.gitignore b/bindings/kotlin/crypto/crypto-android/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/bindings/kotlin/crypto/crypto-android/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/bindings/kotlin/crypto/crypto-android/build.gradle b/bindings/kotlin/crypto/crypto-android/build.gradle deleted file mode 100644 index 543cd2a60..000000000 --- a/bindings/kotlin/crypto/crypto-android/build.gradle +++ /dev/null @@ -1,51 +0,0 @@ -plugins { - id 'com.android.library' - id 'org.jetbrains.kotlin.android' -} - -ext { - PUBLISH_GROUP_ID = ConfigurationData.publishGroupId - PUBLISH_ARTIFACT_ID = 'crypto-android' - PUBLISH_VERSION = rootVersionName - PUBLISH_DESCRIPTION = 'Android Bindings to the Matrix Rust Crypto SDK' -} - -apply from: "${rootDir}/scripts/publish-module.gradle" - -android { - - compileSdk ConfigurationData.compileSdk - - defaultConfig { - minSdk ConfigurationData.minSdk - targetSdk ConfigurationData.targetSdk - versionName ConfigurationData.versionName - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles "consumer-rules.pro" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = '1.8' - } -} - -android.libraryVariants.all { variant -> - def sourceSet = variant.sourceSets.find { it.name == variant.name } - sourceSet.java.srcDir new File(buildDir, "generated/source/${variant.name}") -} - -dependencies { - implementation Dependencies.jna - testImplementation Dependencies.junit -} \ No newline at end of file diff --git a/bindings/kotlin/crypto/crypto-android/consumer-rules.pro b/bindings/kotlin/crypto/crypto-android/consumer-rules.pro deleted file mode 100644 index e69de29bb..000000000 diff --git a/bindings/kotlin/crypto/crypto-android/proguard-rules.pro b/bindings/kotlin/crypto/crypto-android/proguard-rules.pro deleted file mode 100644 index 481bb4348..000000000 --- a/bindings/kotlin/crypto/crypto-android/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/bindings/kotlin/crypto/crypto-android/src/main/AndroidManifest.xml b/bindings/kotlin/crypto/crypto-android/src/main/AndroidManifest.xml deleted file mode 100644 index 730df2c4c..000000000 --- a/bindings/kotlin/crypto/crypto-android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/bindings/kotlin/crypto/crypto-jvm/.gitignore b/bindings/kotlin/crypto/crypto-jvm/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/bindings/kotlin/crypto/crypto-jvm/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/bindings/kotlin/crypto/crypto-jvm/build.gradle b/bindings/kotlin/crypto/crypto-jvm/build.gradle deleted file mode 100644 index ce669345b..000000000 --- a/bindings/kotlin/crypto/crypto-jvm/build.gradle +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - id 'java-library' - id 'org.jetbrains.kotlin.jvm' -} - -java { - sourceCompatibility = JavaVersion.VERSION_1_7 - targetCompatibility = JavaVersion.VERSION_1_7 -} - -dependencies { - implementation 'net.java.dev.jna:jna:5.10.0@aar' -} \ No newline at end of file diff --git a/bindings/kotlin/gradle.properties b/bindings/kotlin/gradle.properties deleted file mode 100644 index d2c86c8ce..000000000 --- a/bindings/kotlin/gradle.properties +++ /dev/null @@ -1,25 +0,0 @@ -# Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app"s APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn -android.useAndroidX=true -# Automatically convert third-party libraries to use AndroidX -android.enableJetifier=true -# Kotlin code style for this project: "official" or "obsolete": -kotlin.code.style=official -# Enables namespacing of each library's R class so that its R class includes only the -# resources declared in the library itself and none from the library's dependencies, -# thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true diff --git a/bindings/kotlin/gradle/wrapper/gradle-wrapper.jar b/bindings/kotlin/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM diff --git a/bindings/kotlin/gradle/wrapper/gradle-wrapper.properties b/bindings/kotlin/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index a10cc8b8d..000000000 --- a/bindings/kotlin/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Mon Feb 28 18:48:31 CET 2022 -distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip -distributionPath=wrapper/dists -zipStorePath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME diff --git a/bindings/kotlin/gradlew b/bindings/kotlin/gradlew deleted file mode 100755 index 4f906e0c8..000000000 --- a/bindings/kotlin/gradlew +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env sh - -# -# Copyright 2015 the original author or authors. -# -# 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 -# -# https://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. -# - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -exec "$JAVACMD" "$@" diff --git a/bindings/kotlin/gradlew.bat b/bindings/kotlin/gradlew.bat deleted file mode 100644 index ac1b06f93..000000000 --- a/bindings/kotlin/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/bindings/kotlin/scripts/publish-module.gradle b/bindings/kotlin/scripts/publish-module.gradle deleted file mode 100644 index 0b7d3f195..000000000 --- a/bindings/kotlin/scripts/publish-module.gradle +++ /dev/null @@ -1,77 +0,0 @@ -apply plugin: 'maven-publish' -apply plugin: 'signing' - -task androidSourcesJar(type: Jar) { - archiveClassifier.set('sources') - if (project.plugins.findPlugin("com.android.library")) { - // For Android libraries - from android.sourceSets.main.java.srcDirs - from android.sourceSets.main.kotlin.srcDirs - } else { - // For pure Kotlin libraries, in case you have them - from sourceSets.main.java.srcDirs - from sourceSets.main.kotlin.srcDirs - } -} - -artifacts { - archives androidSourcesJar -} - -group = PUBLISH_GROUP_ID -version = rootVersionName - -afterEvaluate { - publishing { - publications { - release(MavenPublication) { - - groupId PUBLISH_GROUP_ID - artifactId PUBLISH_ARTIFACT_ID - version PUBLISH_VERSION - - if (project.plugins.findPlugin("com.android.library")) { - from components.release - } else { - from components.java - } - - artifact androidSourcesJar - - pom { - name = PUBLISH_ARTIFACT_ID - description = PUBLISH_DESCRIPTION - url = 'https://github.com/matrix-org/matrix-rust-components-kotlin' - licenses { - license { - name = 'The Apache Software License, Version 2.0' - url = 'https://www.apache.org/licenses/LICENSE-2.0.txt' - } - } - developers { - developer { - id = 'matrixdev' - name = 'matrixdev' - email = 'android@element.io' - } - } - - scm { - connection = 'scm:git:git://github.com/matrix-org/matrix-rust-components-kotlin.git' - developerConnection = 'scm:git:ssh://git@github.com/matrix-org/matrix-rust-components-kotlin.git' - url = 'https://github.com/matrix-org/matrix-rust-components-kotlin' - } - } - } - } - } -} - -signing { - useInMemoryPgpKeys( - rootProject.ext["signing.keyId"], - rootProject.ext["signing.key"], - rootProject.ext["signing.password"], - ) - sign publishing.publications -} \ No newline at end of file diff --git a/bindings/kotlin/scripts/publish-root.gradle b/bindings/kotlin/scripts/publish-root.gradle deleted file mode 100644 index 609cb9a8f..000000000 --- a/bindings/kotlin/scripts/publish-root.gradle +++ /dev/null @@ -1,43 +0,0 @@ -ext["signing.keyId"] = '' -ext["signing.password"] = '' -ext["signing.key"] = '' -ext["ossrhUsername"] = '' -ext["ossrhPassword"] = '' -ext["sonatypeStagingProfileId"] = '' -ext["snapshot"] = '' - -File secretPropsFile = project.rootProject.file('local.properties') -if (secretPropsFile.exists()) { - // Read local.properties file first if it exists - Properties p = new Properties() - new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) } - p.each { name, value -> ext[name] = value } -} else { - // Use system environment variables - ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME') - ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD') - ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID') - ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID') - ext["signing.password"] = System.getenv('SIGNING_PASSWORD') - ext["signing.key"] = System.getenv('SIGNING_KEY') - ext["snapshot"] = System.getenv('SNAPSHOT') -} - -if (snapshot.toBoolean()) { - ext["rootVersionName"] = ConfigurationData.snapshotVersionName -} else { - ext["rootVersionName"] = ConfigurationData.versionName -} - -nexusPublishing { - repositories { - sonatype { - stagingProfileId = sonatypeStagingProfileId - username = ossrhUsername - password = ossrhPassword - version = rootVersionName - nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) - snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) - } - } -} \ No newline at end of file diff --git a/bindings/kotlin/sdk/sdk-android/.gitignore b/bindings/kotlin/sdk/sdk-android/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/bindings/kotlin/sdk/sdk-android/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/bindings/kotlin/sdk/sdk-android/build.gradle b/bindings/kotlin/sdk/sdk-android/build.gradle deleted file mode 100644 index 14cb16560..000000000 --- a/bindings/kotlin/sdk/sdk-android/build.gradle +++ /dev/null @@ -1,51 +0,0 @@ -plugins { - id 'com.android.library' - id 'org.jetbrains.kotlin.android' -} - -ext { - PUBLISH_GROUP_ID = ConfigurationData.publishGroupId - PUBLISH_ARTIFACT_ID = 'sdk-android' - PUBLISH_VERSION = rootVersionName - PUBLISH_DESCRIPTION = 'Android Bindings to the Matrix Rust SDK' -} - -apply from: "${rootDir}/scripts/publish-module.gradle" - -android { - - compileSdk ConfigurationData.compileSdk - - defaultConfig { - minSdk ConfigurationData.minSdk - targetSdk ConfigurationData.targetSdk - versionName ConfigurationData.versionName - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles "consumer-rules.pro" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = '1.8' - } -} - -android.libraryVariants.all { variant -> - def sourceSet = variant.sourceSets.find { it.name == variant.name } - sourceSet.java.srcDir new File(buildDir, "generated/source/${variant.name}") -} - -dependencies { - implementation Dependencies.jna - testImplementation Dependencies.junit -} diff --git a/bindings/kotlin/sdk/sdk-android/consumer-rules.pro b/bindings/kotlin/sdk/sdk-android/consumer-rules.pro deleted file mode 100644 index e69de29bb..000000000 diff --git a/bindings/kotlin/sdk/sdk-android/proguard-rules.pro b/bindings/kotlin/sdk/sdk-android/proguard-rules.pro deleted file mode 100644 index 481bb4348..000000000 --- a/bindings/kotlin/sdk/sdk-android/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/bindings/kotlin/sdk/sdk-android/src/main/AndroidManifest.xml b/bindings/kotlin/sdk/sdk-android/src/main/AndroidManifest.xml deleted file mode 100644 index 1ae1f06b5..000000000 --- a/bindings/kotlin/sdk/sdk-android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/bindings/kotlin/sdk/sdk-jvm/.gitignore b/bindings/kotlin/sdk/sdk-jvm/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/bindings/kotlin/sdk/sdk-jvm/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/bindings/kotlin/sdk/sdk-jvm/build.gradle b/bindings/kotlin/sdk/sdk-jvm/build.gradle deleted file mode 100644 index ce669345b..000000000 --- a/bindings/kotlin/sdk/sdk-jvm/build.gradle +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - id 'java-library' - id 'org.jetbrains.kotlin.jvm' -} - -java { - sourceCompatibility = JavaVersion.VERSION_1_7 - targetCompatibility = JavaVersion.VERSION_1_7 -} - -dependencies { - implementation 'net.java.dev.jna:jna:5.10.0@aar' -} \ No newline at end of file diff --git a/bindings/kotlin/settings.gradle b/bindings/kotlin/settings.gradle deleted file mode 100644 index f1f0f2279..000000000 --- a/bindings/kotlin/settings.gradle +++ /dev/null @@ -1,27 +0,0 @@ -pluginManagement { - repositories { - gradlePluginPortal() - google() - mavenCentral() - } - plugins { - id 'com.android.application' version '7.1.0-beta01' - id 'com.android.library' version '7.1.0-beta01' - id 'org.jetbrains.kotlin.android' version '1.5.30' - id 'org.jetbrains.kotlin.jvm' version '1.5.30' - } -} -dependencyResolutionManagement { - repositories { - google() - mavenCentral() - flatDir { - dirs 'libs' - } - } -} -rootProject.name = "MatrixKotlinRustSDK" -include ':crypto:crypto-android' -include ':crypto:crypto-jvm' -include ':sdk:sdk-jvm' -include ':sdk:sdk-android' From 64ec5ec5619527e811cc0e6549840c61a39608fa Mon Sep 17 00:00:00 2001 From: Archit Bhonsle Date: Wed, 22 Feb 2023 15:51:48 +0530 Subject: [PATCH 007/166] feat(crypto): Expose and re-expose the version of the matrix-sdk-crypto crate --- crates/matrix-sdk-crypto/src/lib.rs | 3 +++ crates/matrix-sdk/src/encryption/mod.rs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index d0a4ecb22..0d2b781ce 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -106,3 +106,6 @@ pub mod vodozemac { DecodeError, KeyError, PickleError, SignatureError, VERSION, }; } + +/// The version of the matrix-sdk-cypto crate being used +pub static VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index 0f0755379..c93734a4d 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -33,7 +33,7 @@ pub use matrix_sdk_base::crypto::{ }, vodozemac, CryptoStoreError, DecryptorError, EventError, KeyExportError, LocalTrust, MediaEncryptionInfo, MegolmError, OlmError, RoomKeyImportResult, SecretImportError, - SessionCreationError, SignatureError, + SessionCreationError, SignatureError, VERSION, }; use matrix_sdk_base::crypto::{ CrossSigningStatus, OutgoingRequest, RoomMessageRequest, ToDeviceRequest, From 02a213c1b540cbef759d87e1a8cb82570cadf46d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janne=20He=C3=9F?= Date: Wed, 22 Feb 2023 11:55:54 +0100 Subject: [PATCH 008/166] feat(sdk): Add support to change display names of devices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Janne Heß --- crates/matrix-sdk/src/client/mod.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 7d40f9e27..33108a66e 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -41,7 +41,7 @@ use ruma::{ client::{ account::{register, whoami}, alias::get_alias, - device::{delete_devices, get_devices}, + device::{delete_devices, get_devices, update_device}, directory::{get_public_rooms, get_public_rooms_filtered}, discovery::{ get_capabilities::{self, Capabilities}, @@ -1928,6 +1928,26 @@ impl Client { self.send(request, None).await } + /// Change the display name of a device owned by the current user. + /// + /// Returns a `update_device::Response` which specifies the result + /// of the operation. + /// + /// # Arguments + /// + /// * `device_id` - The ID of the device to change the display name of. + /// * `display_name` - The new display name to set. + pub async fn rename_device( + &self, + device_id: &DeviceId, + display_name: &str, + ) -> HttpResult { + let mut request = update_device::v3::Request::new(device_id.to_owned()); + request.display_name = Some(display_name.to_owned()); + + self.send(request, None).await + } + /// Synchronize the client's state with the latest state on the server. /// /// ## Syncing Events From b1062a67e0bb16d2dda830c51ec62f66c75e41cf Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 21 Feb 2023 12:49:04 +0100 Subject: [PATCH 009/166] fix(sdk): Rewrite decryption retrying to fix invalid index bug Previously, it was possible for us to use invalid indices when: - We retried decrypting multiple events at once - One of them (not the last) was an edit or reaction This lead to a situation where we would remove the UTD item once decryption for it was successfully retried, but not account for the resulting index shift for all later timeline items. --- .typos.toml | 1 + .../src/room/timeline/event_handler.rs | 2 + crates/matrix-sdk/src/room/timeline/inner.rs | 104 +++++++++-------- .../src/room/timeline/tests/encryption.rs | 107 +++++++++++++++++- 4 files changed, 167 insertions(+), 47 deletions(-) diff --git a/.typos.toml b/.typos.toml index 0f7252474..4f4ad0c08 100644 --- a/.typos.toml +++ b/.typos.toml @@ -5,6 +5,7 @@ Fo = "Fo" BA = "BA" UE = "UE" Ure = "Ure" +OFO = "OFO" Ot = "Ot" # This is the thead html tag, remove this once typos is updated in the github # action. 1.3.1 seems to work correctly, while 1.11.0 on the CI seems to get diff --git a/crates/matrix-sdk/src/room/timeline/event_handler.rs b/crates/matrix-sdk/src/room/timeline/event_handler.rs index d311f27a2..8c06e04f3 100644 --- a/crates/matrix-sdk/src/room/timeline/event_handler.rs +++ b/crates/matrix-sdk/src/room/timeline/event_handler.rs @@ -181,6 +181,7 @@ pub(super) enum TimelineItemPosition { #[derive(Default)] pub(super) struct HandleEventResult { pub(super) item_added: bool, + pub(super) item_removed: bool, pub(super) items_updated: u16, } @@ -319,6 +320,7 @@ impl<'a> TimelineEventHandler<'a> { // wouldn't normally be visible. Remove it. trace!("Removing UTD that was successfully retried"); self.items.remove(idx); + self.result.item_removed = true; } // TODO: Add event as raw diff --git a/crates/matrix-sdk/src/room/timeline/inner.rs b/crates/matrix-sdk/src/room/timeline/inner.rs index 6aaf0d121..4bcf8a119 100644 --- a/crates/matrix-sdk/src/room/timeline/inner.rs +++ b/crates/matrix-sdk/src/room/timeline/inner.rs @@ -35,7 +35,11 @@ use ruma::{ EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId, }; -use tracing::{debug, error, field::debug, info, warn}; +use tracing::{ + debug, error, + field::{self, debug}, + info, info_span, warn, Instrument as _, +}; #[cfg(feature = "e2e-encryption")] use tracing::{instrument, trace}; @@ -312,6 +316,7 @@ impl TimelineInner

{ ) { use super::EncryptedMessage; + trace!("Retrying decryption"); let should_retry = |session_id: &str| { if let Some(session_ids) = &session_ids { session_ids.contains(session_id) @@ -320,63 +325,64 @@ impl TimelineInner

{ } }; - let mut state = self.state.lock().await; + let retry_one = |item: Arc| { + async move { + let event_item = item.as_event()?; - trace!("Collecting UTD timeline items"); - let utds_for_session: Vec<_> = state - .items - .iter() - .enumerate() - .filter_map(|(idx, item)| { - let event_item = &item.as_event()?; - let utd = event_item.content().as_unable_to_decrypt()?; - - match utd { + let session_id = match event_item.content().as_unable_to_decrypt()? { EncryptedMessage::MegolmV1AesSha2 { session_id, .. } if should_retry(session_id) => { - let EventTimelineItem::Remote( - RemoteEventTimelineItem { event_id, raw, .. }, - ) = event_item else { - error!("Key for unable-to-decrypt timeline item is not an event ID"); - return None; - }; - - Some((idx, event_id.to_owned(), session_id.to_owned(), raw.clone())) + session_id } EncryptedMessage::MegolmV1AesSha2 { .. } | EncryptedMessage::OlmV1Curve25519AesSha2 { .. } - | EncryptedMessage::Unknown => None, - } - }) - .collect(); + | EncryptedMessage::Unknown => return None, + }; - if utds_for_session.is_empty() { - trace!("Found no events to retry decryption for"); - return; - } + tracing::Span::current().record("session_id", session_id); - debug!("Retrying decryption for {} items", utds_for_session.len()); - for (idx, event_id, session_id, utd) in utds_for_session { - let event = match olm_machine.decrypt_room_event(utd.cast_ref(), room_id).await { - Ok(ev) => ev, - Err(e) => { - info!( - ?event_id, - ?session_id, - "Failed to decrypt event after receiving room key: {e}" - ); - continue; + let EventTimelineItem::Remote( + RemoteEventTimelineItem { event_id, raw, .. }, + ) = event_item else { + error!("Key for unable-to-decrypt timeline item is not an event ID"); + return None; + }; + + tracing::Span::current().record("event_id", debug(event_id)); + + let raw = raw.cast_ref(); + match olm_machine.decrypt_room_event(raw, room_id).await { + Ok(event) => { + trace!("Successfully decrypted event that previously failed to decrypt"); + Some(event) + } + Err(e) => { + info!("Failed to decrypt event after receiving room key: {e}"); + None + } } + } + .instrument(info_span!( + "retry_one", + session_id = field::Empty, + event_id = field::Empty + )) + }; + + let mut state = self.state.lock().await; + + // We loop through all the items in the timeline, if we successfully + // decrypt a UTD item we either replace it or remove it and update + // another one. + let mut idx = 0; + while let Some(item) = state.items.get(idx) { + let Some(event) = retry_one(item.clone()).await else { + idx += 1; + continue; }; - trace!( - ?event_id, - ?session_id, - "Successfully decrypted event that previously failed to decrypt" - ); - - handle_remote_event( + let result = handle_remote_event( event.event.cast(), event.encryption_info, TimelineItemPosition::Update(idx), @@ -384,6 +390,12 @@ impl TimelineInner

{ &self.profile_provider, ) .await; + + // If the UTD was removed rather than updated, run the loop again + // with the same index. + if !result.item_removed { + idx += 1; + } } } diff --git a/crates/matrix-sdk/src/room/timeline/tests/encryption.rs b/crates/matrix-sdk/src/room/timeline/tests/encryption.rs index f4c3fef73..daec42f9f 100644 --- a/crates/matrix-sdk/src/room/timeline/tests/encryption.rs +++ b/crates/matrix-sdk/src/room/timeline/tests/encryption.rs @@ -1,6 +1,6 @@ #![cfg(not(target_arch = "wasm32"))] -use std::{io::Cursor, iter}; +use std::{collections::BTreeSet, io::Cursor, iter}; use assert_matches::assert_matches; use eyeball_im::VectorDiff; @@ -206,3 +206,108 @@ async fn retry_edit_decryption() { assert!(msg.is_edited()); assert_eq!(msg.body(), "This is Error"); } + +#[async_test] +async fn retry_edit_and_more() { + const DEVICE_ID: &str = "MTEGRRVPEN"; + const SENDER_KEY: &str = "NFPM2+ucU3n3sEdbDdwwv48Bsj4AiQ185lGuRFjy+gs"; + const SESSION_ID: &str = "SMNh04luorH5E8J3b4XYuOBFp8dldO5njacq0OFO70o"; + const SESSION_KEY: &[u8] = b"\ + -----BEGIN MEGOLM SESSION DATA-----\n\ + AXT1CtOfPgmZRXEk4st3ZwIGShWtZ6iDW0+fwku7AIonAAAACr31UJxAbryf6bH3eF5y+WrOipWmZ6G/59A3\ + kuCwntIOrdIC5ShTRWo0qmcWHav2TaFBCx7kWFUs1ryFZjzksCB7sRnVhfXsDUgGGKgj0MOESlPH9Px+IOcV\ + B6Dr9rjj2STtapCknlit9FMrOcfQhsV5q+ymZwm1C32Zc3UTEtyxfpXiIVyru4Xsrzti61fDIiWFj7Mie4Wn\ + 7YQ8SQ1Q9CZUnOCzflP4Yw+5cXHwMRDcz7/kIPzczCYILLp89G//Uh8QN25tN+oCPhBmTxMxoHhabEwkZ/rK\ + D1T+jXDK/dClfXqDXxjjAhQpcUI0soWeAGEq8nMEE5J2D/42AOpKVYqfq2GPiGoPQk3suy4GtDJQlXZaFuz/\ + l4fmHwB1CJCxMUlgpRJ4PhRHAfJn9zfiskM19/dj/G9foGt8KQBRnnbxDVM4eYuoMJZn7SaQfXFmybBTY+Z/\ + bYGg9FUKn/LyjYc8jqbyXCnddzCHB+YENwEOP3WQQrZccyvjuTv5oB/TqK4yS90phIvkLlqEyJXKxxPnzAvV\ + CArjU7naYXMeVieMqcntbeaXutLftLUIF7KUUCPu357sTKjaAp8z98YfPZBctrHRrx7Oo2t6Wtph0A5N/NwA\ + dSN2ceRzRzkoupc4FCxvH6o6PmmtD9DfxtZsk+HA+8NQhgFpvm/VYalikckW+wGFxB4nn1nVViS4GN5n8fc/\ + Ug\n\ + -----END MEGOLM SESSION DATA-----"; + + fn encrypted_message(ciphertext: &str) -> RoomEncryptedEventContent { + RoomEncryptedEventContent::new( + EncryptedEventScheme::MegolmV1AesSha2( + MegolmV1AesSha2ContentInit { + ciphertext: ciphertext.into(), + sender_key: SENDER_KEY.into(), + device_id: DEVICE_ID.into(), + session_id: SESSION_ID.into(), + } + .into(), + ), + None, + ) + } + + let timeline = TestTimeline::new(); + + timeline + .handle_live_message_event( + &BOB, + encrypted_message( + "AwgDEoABQsTrPTYDh22PTmfODR9EucX3qLl3buDcahHPjKJA8QIM+wW0s+e08Zi7/JbLdnZL1VL\ + jO47HcRhxDTyHZPXPg8wd1l0Qb3irjnCnS7LFAc98+ko18CFJUGNeRZZwzGiorKK5VLMv0WQZI8\ + mBZdKIaqDTUBFvcvbn2gQaWtUipQdJQRKyv2h0AWveVkv75lp5hRb7jolCi08oMX8cM+V3Zzyi7\ + mlPAzZjDz0PaRbQwfbMTTHkcL7TZybBi4vLX4f5ZR2Iiysc7gw", + ), + ) + .await; + + let event_id = + timeline.inner.items().await[1].as_event().unwrap().event_id().unwrap().to_owned(); + + let msg2 = encrypted_message( + "AwgEErABt7svMEHDYJTjCQEHypR21l34f9IZLNyFaAbI+EiCIN7C8X5iKmkzuYSmGUodyGKbFRYrW9l5dLj\ + 35xIRli3SZ6duZpmBI7D4pBGPj2T2Jkc/I9kd/I4EhpvV2emDTioB7jwUfFoATfdA0z/6ciTmU73PStKHZM\ + +WYNxCWZERsCQBtiINzC80FymwLjh4nBhnyW0nlMihGGasakn+3wKQUY0HkVoFM8TXQlCXl1RM2oxL9nn0C\ + dRu2LPArXc5K/1GBSyfluSrdQuA9DciLwVHJB9NwvbZ/7flIkaOC7ahahmk2ws+QeSz8MmHt+9QityK3ZUB\ + 4uEzsQ0", + ); + timeline + .handle_live_message_event( + &BOB, + assign!(msg2, { relates_to: Some(Relation::Replacement(Replacement::new(event_id))) }), + ) + .await; + + timeline + .handle_live_message_event( + &BOB, + encrypted_message( + "AwgFEoABUAwzBLYStHEa1RaZtojePQ6sue9terXNMFufeLKci/UcpOpZC9o3lDxp9rxlNjk4Ii+\ + fkOeSClib/qxt+wLszeQZVa04bRr6byK1dOhlptvAPjUCcEsaHyMMR1AnjT2vmFlJRGviwN6cvQ\ + 2r/fEvAW/9QB+N6fX4g9729bt5ftXRqa5QI7NA351RNUveRHxVvx+2x0WJArQjYGRk7tMS2rUto\ + IYt2ZY17nE1UJjN7M87STnCF9c9qy4aGNqIpeVIht6XbtgD7gQ", + ), + ) + .await; + + assert_eq!(timeline.inner.items().await.len(), 4); + + let olm_machine = OlmMachine::new(user_id!("@jptest:matrix.org"), DEVICE_ID.into()).await; + let keys = decrypt_room_key_export(Cursor::new(SESSION_KEY), "testing").unwrap(); + olm_machine.import_room_keys(keys, false, |_, _| {}).await.unwrap(); + + timeline + .inner + .retry_event_decryption( + room_id!("!wFnAUSQbxMcfIMgvNX:flipdot.org"), + &olm_machine, + Some(BTreeSet::from_iter([SESSION_ID])), + ) + .await; + + let timeline_items = timeline.inner.items().await; + assert_eq!(timeline_items.len(), 3); + assert!(timeline_items[0].is_day_divider()); + assert_eq!( + timeline_items[1].as_event().unwrap().content().as_message().unwrap().body(), + "edited" + ); + assert_eq!( + timeline_items[2].as_event().unwrap().content().as_message().unwrap().body(), + "Another message" + ); +} From 95f1867816a15e5d50172963c28601dc95e47967 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 22 Feb 2023 12:32:54 +0100 Subject: [PATCH 010/166] chore: Delete sled-state-inspector The sled store is on its way out and nobody is using this. --- CONVENTIONAL_COMMITS.md | 4 +- Cargo.lock | 201 +------------- labs/sled-state-inspector/Cargo.toml | 22 -- labs/sled-state-inspector/src/main.rs | 367 ------------------------- labs/sled-state-inspector/syntaxes.bin | Bin 678334 -> 0 bytes labs/sled-state-inspector/themes.bin | Bin 29671 -> 0 bytes tarpaulin.toml | 1 - 7 files changed, 3 insertions(+), 592 deletions(-) delete mode 100644 labs/sled-state-inspector/Cargo.toml delete mode 100644 labs/sled-state-inspector/src/main.rs delete mode 100644 labs/sled-state-inspector/syntaxes.bin delete mode 100644 labs/sled-state-inspector/themes.bin diff --git a/CONVENTIONAL_COMMITS.md b/CONVENTIONAL_COMMITS.md index f3d3b1358..e08252feb 100644 --- a/CONVENTIONAL_COMMITS.md +++ b/CONVENTIONAL_COMMITS.md @@ -101,8 +101,8 @@ section aims at listing all the scopes used inside this project: Labs - sled-state-inspector - About the sled-state-inspector project. + jack-in + About the jack-in project. Continuous Integration diff --git a/Cargo.lock b/Cargo.lock index 56045b388..1c1e82ac7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -426,21 +426,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - [[package]] name = "bitflags" version = "1.3.2" @@ -772,17 +757,6 @@ dependencies = [ "os_str_bytes", ] -[[package]] -name = "clipboard-win" -version = "4.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" -dependencies = [ - "error-code", - "str-buf", - "winapi", -] - [[package]] name = "cmake" version = "0.1.49" @@ -1262,16 +1236,6 @@ dependencies = [ "dirs-sys", ] -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - [[package]] name = "dirs-sys" version = "0.3.7" @@ -1283,17 +1247,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - [[package]] name = "discard" version = "1.0.4" @@ -1357,12 +1310,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "endian-type" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" - [[package]] name = "errno" version = "0.2.8" @@ -1384,16 +1331,6 @@ dependencies = [ "libc", ] -[[package]] -name = "error-code" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" -dependencies = [ - "libc", - "str-buf", -] - [[package]] name = "event-listener" version = "2.5.3" @@ -1572,16 +1509,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" -[[package]] -name = "fancy-regex" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6b8560a05112eb52f04b00e5d3790c0dd75d9d980eb8a122fb23b92a623ccf" -dependencies = [ - "bit-set", - "regex", -] - [[package]] name = "fastrand" version = "1.9.0" @@ -1591,17 +1518,6 @@ dependencies = [ "instant", ] -[[package]] -name = "fd-lock" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ef1a30ae415c3a691a4f41afddc2dbcd6d70baf338368d85ebc1e8ed92cedb9" -dependencies = [ - "cfg-if", - "rustix", - "windows-sys 0.45.0", -] - [[package]] name = "findshlibs" version = "0.10.2" @@ -3228,15 +3144,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" -[[package]] -name = "nibble_vec" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" -dependencies = [ - "smallvec", -] - [[package]] name = "nix" version = "0.24.3" @@ -3248,18 +3155,6 @@ dependencies = [ "libc", ] -[[package]] -name = "nix" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" -dependencies = [ - "autocfg", - "bitflags", - "cfg-if", - "libc", -] - [[package]] name = "nom" version = "7.1.3" @@ -3793,7 +3688,7 @@ dependencies = [ "inferno", "libc", "log", - "nix 0.24.3", + "nix", "once_cell", "parking_lot 0.12.1", "smallvec", @@ -3979,16 +3874,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "radix_trie" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" -dependencies = [ - "endian-type", - "nibble_vec", -] - [[package]] name = "rand" version = "0.7.3" @@ -4428,40 +4313,6 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" -[[package]] -name = "rustyline" -version = "10.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e83c32c3f3c33b08496e0d1df9ea8c64d39adb8eb36a1ebb1440c690697aef" -dependencies = [ - "bitflags", - "cfg-if", - "clipboard-win", - "dirs-next", - "fd-lock", - "libc", - "log", - "memchr", - "nix 0.25.1", - "radix_trie", - "scopeguard", - "unicode-segmentation", - "unicode-width", - "utf8parse", - "winapi", -] - -[[package]] -name = "rustyline-derive" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "107c3d5d7f370ac09efa62a78375f94d94b8a33c61d8c278b96683fb4dbf2d8d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "ryu" version = "1.0.12" @@ -4804,23 +4655,6 @@ dependencies = [ "parking_lot 0.11.2", ] -[[package]] -name = "sled-state-inspector" -version = "0.1.0" -dependencies = [ - "atty", - "clap 3.2.23", - "futures", - "matrix-sdk-base", - "matrix-sdk-sled", - "ruma", - "rustyline", - "rustyline-derive", - "serde", - "serde_json", - "syntect", -] - [[package]] name = "sliding-sync-integration-test" version = "0.1.0" @@ -4890,12 +4724,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "str-buf" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" - [[package]] name = "str_stack" version = "0.1.0" @@ -4966,27 +4794,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "syntect" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6c454c27d9d7d9a84c7803aaa3c50cd088d2906fe3c6e42da3209aa623576a8" -dependencies = [ - "bincode", - "bitflags", - "fancy-regex", - "flate2", - "fnv", - "lazy_static", - "once_cell", - "regex-syntax", - "serde", - "serde_derive", - "serde_json", - "thiserror", - "walkdir", -] - [[package]] name = "tempfile" version = "3.3.0" @@ -5819,12 +5626,6 @@ dependencies = [ "serde", ] -[[package]] -name = "utf8parse" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" - [[package]] name = "uuid" version = "1.3.0" diff --git a/labs/sled-state-inspector/Cargo.toml b/labs/sled-state-inspector/Cargo.toml deleted file mode 100644 index b7126992d..000000000 --- a/labs/sled-state-inspector/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "sled-state-inspector" -version = "0.1.0" -edition = "2021" -publish = false - -[[bin]] -name = "sled-state-inspector" -test = false - -[dependencies] -atty = "0.2.14" -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 = { workspace = true } -rustyline = "10.0.0" -rustyline-derive = "0.7.0" -serde = { workspace = true } -serde_json = { workspace = true } -syntect = { version = "5.0.0", default-features = false, features = ["dump-load", "parsing", "regex-fancy"] } diff --git a/labs/sled-state-inspector/src/main.rs b/labs/sled-state-inspector/src/main.rs deleted file mode 100644 index 8c939d275..000000000 --- a/labs/sled-state-inspector/src/main.rs +++ /dev/null @@ -1,367 +0,0 @@ -use std::{fmt::Debug, sync::Arc}; - -use atty::Stream; -use clap::{Arg, ArgMatches, Command as Argparse}; -use futures::executor::block_on; -use matrix_sdk_base::{RoomInfo, StateStore}; -use matrix_sdk_sled::SledStateStore; -use ruma::{events::StateEventType, OwnedRoomId, OwnedUserId, RoomId}; -use rustyline::{ - completion::{Completer, Pair}, - error::ReadlineError, - highlight::{Highlighter, MatchingBracketHighlighter}, - hint::{Hinter, HistoryHinter}, - validate::{MatchingBracketValidator, Validator}, - CompletionType, Config, Context, EditMode, Editor, -}; -use rustyline_derive::Helper; -use serde::Serialize; -use syntect::{ - dumps::from_binary, - easy::HighlightLines, - highlighting::{Style, ThemeSet}, - parsing::SyntaxSet, - util::{as_24_bit_terminal_escaped, LinesWithEndings}, -}; - -#[derive(Clone)] -struct Inspector { - store: Arc, - printer: Printer, -} - -#[derive(Helper)] -struct InspectorHelper { - store: Arc, - _highlighter: MatchingBracketHighlighter, - _validator: MatchingBracketValidator, - _hinter: HistoryHinter, -} - -impl InspectorHelper { - const EVENT_TYPES: &'static [&'static str] = &[ - "m.room.aliases", - "m.room.avatar", - "m.room.canonical_alias", - "m.room.create", - "m.room.encryption", - "m.room.guest_access", - "m.room.history_visibility", - "m.room.join_rules", - "m.room.name", - "m.room.power_levels", - "m.room.tombstone", - "m.room.topic", - ]; - - fn new(store: Arc) -> Self { - Self { - store, - _highlighter: MatchingBracketHighlighter::new(), - _validator: MatchingBracketValidator::new(), - _hinter: HistoryHinter {}, - } - } - - fn complete_event_types(&self, arg: Option<&&str>) -> Vec { - Self::EVENT_TYPES - .iter() - .map(|&t| Pair { display: t.to_owned(), replacement: format!("{t} ") }) - .filter(|r| if let Some(arg) = arg { r.replacement.starts_with(arg) } else { true }) - .collect() - } - - fn complete_rooms(&self, arg: Option<&&str>) -> Vec { - let rooms: Vec = - block_on(async { StateStore::get_room_infos(&*self.store).await.unwrap() }); - - rooms - .into_iter() - .map(|r| Pair { - display: r.room_id().to_string(), - replacement: format!("{} ", r.room_id()), - }) - .filter(|r| if let Some(arg) = arg { r.replacement.starts_with(arg) } else { true }) - .collect() - } -} - -impl Completer for InspectorHelper { - type Candidate = Pair; - - fn complete( - &self, - line: &str, - pos: usize, - _: &Context<'_>, - ) -> Result<(usize, Vec), ReadlineError> { - let args: Vec<&str> = line.split_ascii_whitespace().collect(); - - let commands = vec![ - ("get-state", "get a state event in the given room"), - ("get-profiles", "get all the stored profiles in the given room"), - ("list-rooms", "list all rooms"), - ("get-members", "get all the membership events in the given room"), - ] - .iter() - .map(|(r, d)| Pair { display: format!("{r} ({d})"), replacement: format!("{r} ") }) - .collect(); - - if args.is_empty() { - Ok((pos, commands)) - } else if args.len() == 1 { - if (args[0] == "get-state" || args[0] == "get-members" || args[0] == "get-profiles") - && line.ends_with(' ') - { - Ok((args[0].len() + 1, self.complete_rooms(args.get(1)))) - } else { - Ok(( - 0, - commands.into_iter().filter(|c| c.replacement.starts_with(args[0])).collect(), - )) - } - } else if args.len() == 2 { - if args[0] == "get-state" { - if line.ends_with(' ') { - Ok((args[0].len() + args[1].len() + 2, self.complete_event_types(args.get(2)))) - } else { - Ok((args[0].len() + 1, self.complete_rooms(args.get(1)))) - } - } else if args[0] == "get-members" || args[0] == "get-profiles" { - Ok((args[0].len() + 1, self.complete_rooms(args.get(1)))) - } else { - Ok((pos, vec![])) - } - } else if args.len() == 3 { - if args[0] == "get-state" { - Ok((args[0].len() + args[1].len() + 2, self.complete_event_types(args.get(2)))) - } else { - Ok((pos, vec![])) - } - } else { - Ok((pos, vec![])) - } - } -} - -impl Hinter for InspectorHelper { - type Hint = String; -} - -impl Highlighter for InspectorHelper {} - -impl Validator for InspectorHelper {} - -#[derive(Clone, Debug)] -struct Printer { - ps: Arc, - ts: Arc, - json: bool, - color: bool, -} - -impl Printer { - fn new(json: bool, color: bool) -> Self { - let syntax_set: SyntaxSet = from_binary(include_bytes!("../syntaxes.bin")); - let themes: ThemeSet = from_binary(include_bytes!("../themes.bin")); - - Self { ps: syntax_set.into(), ts: themes.into(), json, color } - } - - fn pretty_print_struct(&self, data: &T) { - let data = if self.json { - serde_json::to_string_pretty(data).expect("Can't serialize struct") - } else { - format!("{data:#?}") - }; - - let syntax = if self.json { - self.ps.find_syntax_by_extension("rs").expect("Can't find rust syntax extension") - } else { - self.ps.find_syntax_by_extension("json").expect("Can't find json syntax extension") - }; - - if self.color { - let mut h = HighlightLines::new(syntax, &self.ts.themes["Forest Night"]); - - for line in LinesWithEndings::from(&data) { - let ranges: Vec<(Style, &str)> = - h.highlight_line(line, &self.ps).expect("Failed to highlight line"); - let escaped = as_24_bit_terminal_escaped(&ranges[..], false); - print!("{escaped}"); - } - - // Clear the formatting - println!("\x1b[0m"); - } else { - println!("{data}"); - } - } -} - -impl Inspector { - fn new(database_path: &str, json: bool, color: bool) -> Self { - let printer = Printer::new(json, color); - let store = Arc::new( - SledStateStore::builder() - .path(database_path.into()) - .build() - .expect("Can't open sled database"), - ); - - Self { store, printer } - } - - async fn run(&self, matches: ArgMatches) { - match matches.subcommand() { - Some(("get-profiles", args)) => { - let room_id = RoomId::parse(args.value_of("room-id").unwrap()).unwrap(); - - self.get_profiles(room_id).await; - } - - Some(("get-members", args)) => { - let room_id = RoomId::parse(args.value_of("room-id").unwrap()).unwrap(); - - self.get_members(room_id).await; - } - Some(("list-rooms", _)) => self.list_rooms().await, - Some(("get-display-names", args)) => { - let room_id = RoomId::parse(args.value_of("room-id").unwrap()).unwrap(); - let display_name = args.value_of("display-name").unwrap().to_owned(); - self.get_display_name_owners(room_id, display_name).await; - } - Some(("get-state", args)) => { - let room_id = RoomId::parse(args.value_of("room-id").unwrap()).unwrap(); - let event_type = - StateEventType::try_from(args.value_of("event-type").unwrap()).unwrap(); - self.get_state(room_id, event_type).await; - } - _ => unreachable!(), - } - } - - async fn list_rooms(&self) { - let rooms: Vec = StateStore::get_room_infos(&*self.store).await.unwrap(); - self.printer.pretty_print_struct(&rooms); - } - - async fn get_display_name_owners(&self, room_id: OwnedRoomId, display_name: String) { - let users = self.store.get_users_with_display_name(&room_id, &display_name).await.unwrap(); - self.printer.pretty_print_struct(&users); - } - - async fn get_profiles(&self, room_id: OwnedRoomId) { - let joined: Vec = - StateStore::get_joined_user_ids(&*self.store, &room_id).await.unwrap(); - - for member in joined { - let event = self.store.get_profile(&room_id, &member).await.unwrap(); - self.printer.pretty_print_struct(&event); - } - } - - async fn get_members(&self, room_id: OwnedRoomId) { - let joined: Vec = - StateStore::get_joined_user_ids(&*self.store, &room_id).await.unwrap(); - - for member in joined { - let event = self.store.get_member_event(&room_id, &member).await.unwrap(); - self.printer.pretty_print_struct(&event); - } - } - - async fn get_state(&self, room_id: OwnedRoomId, event_type: StateEventType) { - self.printer.pretty_print_struct( - &self.store.get_state_event(&room_id, event_type, "").await.unwrap(), - ); - } - - fn subcommands() -> Vec> { - vec![ - Argparse::new("list-rooms"), - Argparse::new("get-members").arg(Arg::new("room-id").required(true).validator(|r| { - RoomId::parse(r).map(|_| ()).map_err(|_| "Invalid room id given".to_owned()) - })), - Argparse::new("get-profiles").arg(Arg::new("room-id").required(true).validator(|r| { - RoomId::parse(r).map(|_| ()).map_err(|_| "Invalid room id given".to_owned()) - })), - Argparse::new("get-display-names") - .arg(Arg::new("room-id").required(true).validator(|r| { - RoomId::parse(r).map(|_| ()).map_err(|_| "Invalid room id given".to_owned()) - })) - .arg(Arg::new("display-name").required(true)), - Argparse::new("get-state") - .arg(Arg::new("room-id").required(true).validator(|r| { - RoomId::parse(r).map(|_| ()).map_err(|_| "Invalid room id given".to_owned()) - })) - .arg(Arg::new("event-type").required(true).validator(|e| { - StateEventType::try_from(e) - .map(|_| ()) - .map_err(|_| "Invalid event type".to_owned()) - })), - ] - } - - async fn parse_and_run(&self, input: &str) { - let argparse = Argparse::new("state-inspector") - .disable_version_flag(true) - .disable_help_flag(true) - .no_binary_name(true) - .subcommand_required(true) - .arg_required_else_help(true) - .subcommands(Inspector::subcommands()); - - match argparse.try_get_matches_from(input.split_ascii_whitespace()) { - Ok(m) => { - self.run(m).await; - } - Err(e) => { - println!("{e}"); - } - } - } -} - -fn main() { - let argparse = Argparse::new("state-inspector") - .disable_version_flag(true) - .arg(Arg::new("database").required(true)) - .arg( - Arg::new("json") - .long("json") - .help("set the output to raw json instead of Rust structs") - .global(true) - .takes_value(false), - ) - .subcommands(Inspector::subcommands()); - - let matches = argparse.get_matches(); - - let database_path = matches.value_of("database").expect("No database path"); - let json = matches.is_present("json"); - let color = atty::is(Stream::Stdout); - - let inspector = Inspector::new(database_path, json, color); - - if matches.subcommand().is_none() { - let config = Config::builder() - .history_ignore_space(true) - .completion_type(CompletionType::List) - .edit_mode(EditMode::Emacs) - .build(); - - let helper = InspectorHelper::new(inspector.store.clone()); - - let mut rl = - Editor::::with_config(config).expect("Failed to create Editor"); - rl.set_helper(Some(helper)); - - while let Ok(input) = rl.readline(">> ") { - rl.add_history_entry(input.as_str()); - block_on(inspector.parse_and_run(input.as_str())); - } - } else { - block_on(inspector.run(matches)); - } -} diff --git a/labs/sled-state-inspector/syntaxes.bin b/labs/sled-state-inspector/syntaxes.bin deleted file mode 100644 index 71c64c84dff525aa8684d2a5d69cf5bd8f6a1745..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 678334 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

N&LceoE1E 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

  • IE>xcJJ9$d2bG3L{ujMMNB{FZZn5y;s#T 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 <@Ob+pU1XGakxDk%v`WW8o~f06IIf4Z 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

    OC~_ipT}>L@ZTFx<7>eG;6kq|`JenSCcuCM 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≫`O^oM#x)Tl(IBa@ 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

    T6ne;LYGhQkt##|8sWH_`q6iHsRJXI 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%0DF&$q>4QZ>dlJ@YRir!mS`a(y0lV8zRqyjQ-KKZ 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
    di@OoJN$(o@mXoo&AhQv@~uVpgNVD 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 BNH72Z diff --git a/labs/sled-state-inspector/themes.bin b/labs/sled-state-inspector/themes.bin deleted file mode 100644 index 7342dbdba1f226a422ce426b482556243a48638b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29671 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 Date: Wed, 22 Feb 2023 15:42:32 +0100 Subject: [PATCH 011/166] fix: look for the right list when waiting for updates Otherwise it will time out after 30s and then continue executing, causing a slow test. --- testing/sliding-sync-integration-test/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/sliding-sync-integration-test/src/lib.rs b/testing/sliding-sync-integration-test/src/lib.rs index 853c00e9a..12e2a1741 100644 --- a/testing/sliding-sync-integration-test/src/lib.rs +++ b/testing/sliding-sync-integration-test/src/lib.rs @@ -1203,7 +1203,7 @@ mod tests { let room_summary = stream.next().await.context("sync has closed unexpectedly")??; // we only heard about the ones we had asked for - if room_summary.views.iter().any(|s| s == "sliding") { + if room_summary.views.iter().any(|s| s == "sliding_view") { break; } } From cfcafc84259792443ea720e8a071933f76dd7f98 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 22 Feb 2023 16:48:46 +0100 Subject: [PATCH 012/166] chore(ffi) Rename `StoppableSpawn` to `TaskHandle`. Because it is what it is. --- bindings/matrix-sdk-ffi/src/api.udl | 13 +++-- bindings/matrix-sdk-ffi/src/lib.rs | 2 +- bindings/matrix-sdk-ffi/src/sliding_sync.rs | 56 +++++++++++---------- 3 files changed, 36 insertions(+), 35 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/api.udl b/bindings/matrix-sdk-ffi/src/api.udl index ab2e57bac..5610266bb 100644 --- a/bindings/matrix-sdk-ffi/src/api.udl +++ b/bindings/matrix-sdk-ffi/src/api.udl @@ -1,8 +1,7 @@ namespace matrix_sdk_ffi {}; -/// Cancels on drop -interface StoppableSpawn {}; +interface TaskHandle {}; [Error] interface ClientError { @@ -112,10 +111,10 @@ interface SlidingSyncViewBuilder { }; interface SlidingSyncView { - StoppableSpawn observe_room_list(SlidingSyncViewRoomListObserver observer); - StoppableSpawn observe_rooms_count(SlidingSyncViewRoomsCountObserver observer); - StoppableSpawn observe_state(SlidingSyncViewStateObserver observer); - StoppableSpawn observe_room_items(SlidingSyncViewRoomItemsObserver observer); + TaskHandle observe_room_list(SlidingSyncViewRoomListObserver observer); + TaskHandle observe_rooms_count(SlidingSyncViewRoomsCountObserver observer); + TaskHandle observe_state(SlidingSyncViewStateObserver observer); + TaskHandle observe_room_items(SlidingSyncViewRoomItemsObserver observer); }; interface SlidingSyncRoom { @@ -127,7 +126,7 @@ interface SlidingSyncRoom { dictionary SlidingSyncSubscribeResult { sequence items; - StoppableSpawn task_handle; + TaskHandle task_handle; }; interface SlidingSync { diff --git a/bindings/matrix-sdk-ffi/src/lib.rs b/bindings/matrix-sdk-ffi/src/lib.rs index 326034614..16c1b16f0 100644 --- a/bindings/matrix-sdk-ffi/src/lib.rs +++ b/bindings/matrix-sdk-ffi/src/lib.rs @@ -90,7 +90,7 @@ mod uniffi_types { sliding_sync::{ RequiredState, RoomListEntry, SlidingSync, SlidingSyncBuilder, SlidingSyncRequestListFilters, SlidingSyncRoom, SlidingSyncView, - SlidingSyncViewBuilder, StoppableSpawn, UnreadNotificationsCount, + SlidingSyncViewBuilder, TaskHandle, UnreadNotificationsCount, }, timeline::{ AudioInfo, AudioMessageContent, EmoteMessageContent, EncryptedMessage, EventSendState, diff --git a/bindings/matrix-sdk-ffi/src/sliding_sync.rs b/bindings/matrix-sdk-ffi/src/sliding_sync.rs index dbb3439f5..0b79f83a7 100644 --- a/bindings/matrix-sdk-ffi/src/sliding_sync.rs +++ b/bindings/matrix-sdk-ffi/src/sliding_sync.rs @@ -28,34 +28,35 @@ use crate::{ TimelineItem, TimelineListener, }; -type StoppableSpawnCallback = Box; +type TaskHandleCallback = Box; -pub struct StoppableSpawn { +pub struct TaskHandle { handle: Option>, - callback: RwLock>, + callback: RwLock>, } -impl StoppableSpawn { - fn with_handle(handle: JoinHandle<()>) -> StoppableSpawn { - StoppableSpawn { handle: Some(handle), callback: Default::default() } - } - fn with_callback(callback: StoppableSpawnCallback) -> StoppableSpawn { - StoppableSpawn { handle: Default::default(), callback: RwLock::new(Some(callback)) } +impl TaskHandle { + fn with_handle(handle: JoinHandle<()>) -> Self { + Self { handle: Some(handle), callback: Default::default() } } - fn set_callback(&mut self, f: StoppableSpawnCallback) { + fn with_callback(callback: TaskHandleCallback) -> Self { + Self { handle: Default::default(), callback: RwLock::new(Some(callback)) } + } + + fn set_callback(&mut self, f: TaskHandleCallback) { *self.callback.write().unwrap() = Some(f) } } -impl From> for StoppableSpawn { +impl From> for TaskHandle { fn from(value: JoinHandle<()>) -> Self { - StoppableSpawn::with_handle(value) + Self::with_handle(value) } } #[uniffi::export] -impl StoppableSpawn { +impl TaskHandle { pub fn cancel(&self) { debug!("stoppable.cancel() called"); if let Some(handle) = &self.handle { @@ -65,12 +66,13 @@ impl StoppableSpawn { callback(); } } + pub fn is_finished(&self) -> bool { self.handle.as_ref().map(|h| h.is_finished()).unwrap_or_default() } } -impl Drop for StoppableSpawn { +impl Drop for TaskHandle { fn drop(&mut self) { self.cancel(); } @@ -185,7 +187,7 @@ impl SlidingSyncRoom { fn add_timeline_listener_inner( &self, listener: Box, - ) -> anyhow::Result<(Vec>, StoppableSpawn)> { + ) -> anyhow::Result<(Vec>, TaskHandle)> { let mut timeline_lock = self.timeline.write().unwrap(); let timeline = match &*timeline_lock { Some(timeline) => timeline, @@ -230,7 +232,7 @@ impl SlidingSyncRoom { }; let items = timeline_items.into_iter().map(TimelineItem::from_arc).collect(); - let task_handle = StoppableSpawn::with_handle(RUNTIME.spawn(async move { + let task_handle = TaskHandle::with_handle(RUNTIME.spawn(async move { join(handle_events, handle_sliding_sync_reset).await; })); Ok((items, task_handle)) @@ -239,7 +241,7 @@ impl SlidingSyncRoom { pub struct SlidingSyncSubscribeResult { pub items: Vec>, - pub task_handle: Arc, + pub task_handle: Arc, } pub struct UpdateSummary { @@ -516,10 +518,10 @@ impl SlidingSyncView { pub fn observe_state( &self, observer: Box, - ) -> Arc { + ) -> Arc { let mut state_stream = self.inner.state_stream(); - Arc::new(StoppableSpawn::with_handle(RUNTIME.spawn(async move { + Arc::new(TaskHandle::with_handle(RUNTIME.spawn(async move { loop { if let Some(new_state) = state_stream.next().await { observer.did_receive_update(new_state); @@ -531,10 +533,10 @@ impl SlidingSyncView { pub fn observe_room_list( &self, observer: Box, - ) -> Arc { + ) -> Arc { let mut rooms_list_stream = self.inner.rooms_list_stream(); - Arc::new(StoppableSpawn::with_handle(RUNTIME.spawn(async move { + Arc::new(TaskHandle::with_handle(RUNTIME.spawn(async move { loop { if let Some(diff) = rooms_list_stream.next().await { observer.did_receive_update(diff.into()); @@ -546,9 +548,9 @@ impl SlidingSyncView { pub fn observe_room_items( &self, observer: Box, - ) -> Arc { + ) -> Arc { let mut rooms_updated = self.inner.rooms_updated_broadcaster.signal_cloned().to_stream(); - Arc::new(StoppableSpawn::with_handle(RUNTIME.spawn(async move { + Arc::new(TaskHandle::with_handle(RUNTIME.spawn(async move { loop { if rooms_updated.next().await.is_some() { observer.did_receive_update(); @@ -560,10 +562,10 @@ impl SlidingSyncView { pub fn observe_rooms_count( &self, observer: Box, - ) -> Arc { + ) -> Arc { let mut rooms_count_stream = self.inner.rooms_count_stream(); - Arc::new(StoppableSpawn::with_handle(RUNTIME.spawn(async move { + Arc::new(TaskHandle::with_handle(RUNTIME.spawn(async move { loop { if let Some(Some(new)) = rooms_count_stream.next().await { observer.did_receive_update(new); @@ -713,14 +715,14 @@ impl SlidingSync { self.inner.add_common_extensions(); } - pub fn sync(&self) -> Arc { + pub fn sync(&self) -> Arc { let inner = self.inner.clone(); let client = self.client.clone(); let observer = self.observer.clone(); let stop_loop = Arc::new(AtomicBool::new(false)); let remote_stopper = stop_loop.clone(); - let stoppable = Arc::new(StoppableSpawn::with_callback(Box::new(move || { + let stoppable = Arc::new(TaskHandle::with_callback(Box::new(move || { remote_stopper.store(true, Ordering::Relaxed); }))); From dec4b2122b2bc843d596862d83317bcabb39aa0f Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 22 Feb 2023 16:51:42 +0100 Subject: [PATCH 013/166] doc(sdk): Improve documentation of `SlidingSyncMode`. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index a35f0989f..418b834cc 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -690,11 +690,13 @@ pub enum SlidingSyncState { /// The mode by which the the [`SlidingSyncView`] is in fetching the data. #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum SlidingSyncMode { - /// fully sync all rooms in the background, page by page of `batch_size` + /// Fully sync all rooms in the background, page by page of `batch_size`, + /// like `0..20`, `21..40`, 41..60` etc. assuming the `batch_size` is 20. #[serde(alias = "FullSync")] PagingFullSync, - /// fully sync all rooms in the background, with a growing window of - /// `batch_size`, + /// Fully sync all rooms in the background, with a growing window of + /// `batch_size`, like `0..20`, `0..40`, `0..60` etc. assuming the + /// `batch_size` is 20. GrowingFullSync, /// Only sync the specific windows defined #[default] From 5727726e5d2bf03722f057d6ba2bd91c06ec8dfc Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 22 Feb 2023 17:07:50 +0100 Subject: [PATCH 014/166] feat(sdk): Make `SlidingSync.pos` already private. So far, the `SlidingSync.pos` field was public to the crate. In order to avoid breaking the internal state of this type, its visibility is now private. However, we need to be able to change the value when testing the `SlidingSync` type itself. To achieve that, this patch removes the old `force_sliding_sync_pos` function, and implements 2 new functions: `set_pos` and `pos` directly on `SlidingSync` only when `#[cfg(any(test, feature ="testing"))]`. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 23 ++++++++++++++++--- crates/matrix-sdk/src/test_utils.rs | 8 ------- .../sliding-sync-integration-test/src/lib.rs | 5 ++-- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 418b834cc..cd90467cf 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -766,8 +766,9 @@ pub struct SlidingSync { /// The storage key to keep this cache at and load it from storage_key: Option, - // ------ Internal state - pub(crate) pos: Mutable>, + /// The `pos` marker. + pos: Mutable>, + delta_token: Mutable>, /// The views of this sliding sync instance @@ -1153,11 +1154,14 @@ impl SlidingSync { match self.sync_once(&mut views).instrument(sync_span.clone()).await { Ok(Some(updates)) => { self.failure_count.store(0, Ordering::SeqCst); + yield Ok(updates) - }, + } + Ok(None) => { break; } + Err(e) => { if e.client_api_error_kind() == Some(&ErrorKind::UnknownPos) { // session expired, let's reset @@ -1189,6 +1193,19 @@ impl SlidingSync { } } +#[cfg(any(test, feature = "testing"))] +impl SlidingSync { + /// Get a copy of the `pos` value. + pub fn pos(&self) -> Option { + self.pos.get_cloned() + } + + /// Set a new value for `pos`. + pub fn set_pos(&self, new_pos: String) { + self.pos.set(Some(new_pos)) + } +} + #[cfg(test)] mod test { use ruma::room_id; diff --git a/crates/matrix-sdk/src/test_utils.rs b/crates/matrix-sdk/src/test_utils.rs index 30ebe50fe..53efd0d34 100644 --- a/crates/matrix-sdk/src/test_utils.rs +++ b/crates/matrix-sdk/src/test_utils.rs @@ -4,8 +4,6 @@ use matrix_sdk_base::Session; use ruma::{api::MatrixVersion, device_id, user_id}; -#[cfg(feature = "experimental-sliding-sync")] -use crate::sliding_sync::SlidingSync; use crate::{config::RequestConfig, Client, ClientBuilder}; pub(crate) fn test_client_builder(homeserver_url: Option) -> ClientBuilder { @@ -33,9 +31,3 @@ pub(crate) async fn logged_in_client(homeserver_url: Option) -> Client { client } - -/// Force a specific pos-value to be used for the given sliding-sync instance. -#[cfg(feature = "experimental-sliding-sync")] -pub fn force_sliding_sync_pos(sliding_sync: &SlidingSync, new_pos: String) { - sliding_sync.pos.set(Some(new_pos)); -} diff --git a/testing/sliding-sync-integration-test/src/lib.rs b/testing/sliding-sync-integration-test/src/lib.rs index 853c00e9a..52bc287e0 100644 --- a/testing/sliding-sync-integration-test/src/lib.rs +++ b/testing/sliding-sync-integration-test/src/lib.rs @@ -81,7 +81,6 @@ mod tests { api::client::error::ErrorKind as RumaError, events::room::message::RoomMessageEventContent, UInt, }, - test_utils::force_sliding_sync_pos, SlidingSyncMode, SlidingSyncState, SlidingSyncView, }; @@ -1042,7 +1041,7 @@ mod tests { ); // force the pos to be invalid and thus this being reset internally - force_sliding_sync_pos(&sync_proxy, "100".to_owned()); + sync_proxy.set_pos("100".to_string()); let mut error_seen = false; for _n in 0..2 { @@ -1249,7 +1248,7 @@ mod tests { assert!(room_updated, "Room update has not been seen"); // force the pos to be invalid and thus this being reset internally - force_sliding_sync_pos(&sync_proxy, "100".to_owned()); + sync_proxy.set_pos("100".to_owned()); let mut error_seen = false; let mut room_updated = false; From fe7f157253b6dcb668ffb3071ec392d87fea5d05 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 6 Oct 2022 22:43:42 +0200 Subject: [PATCH 015/166] refactor(bindings): Use UniFFI proc-macros for CrossSigningStatus --- bindings/matrix-sdk-crypto-ffi/src/lib.rs | 4 ++-- bindings/matrix-sdk-crypto-ffi/src/machine.rs | 19 +++++++++++-------- bindings/matrix-sdk-crypto-ffi/src/olm.udl | 7 ------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/lib.rs b/bindings/matrix-sdk-crypto-ffi/src/lib.rs index d15fd04be..a6fa482f8 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/lib.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/lib.rs @@ -484,7 +484,7 @@ pub struct DecryptedEvent { /// Struct representing the state of our private cross signing keys, it shows /// which private cross signing keys we have locally stored. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, uniffi::Record)] pub struct CrossSigningStatus { /// Do we have the master key. pub has_master: bool, @@ -602,7 +602,7 @@ mod uniffi_types { error::CryptoStoreError, machine::OlmMachine, responses::Request, - BackupKeys, RoomKeyCounts, + BackupKeys, CrossSigningStatus, RoomKeyCounts, }; } diff --git a/bindings/matrix-sdk-crypto-ffi/src/machine.rs b/bindings/matrix-sdk-crypto-ffi/src/machine.rs index 2855ccd39..b80d906ed 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/machine.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/machine.rs @@ -149,6 +149,17 @@ impl OlmMachine { } } +#[uniffi::export] +impl OlmMachine { + /// Get the status of the private cross signing keys. + /// + /// This can be used to check which private cross signing keys we have + /// stored locally. + pub fn cross_signing_status(&self) -> CrossSigningStatus { + self.runtime.block_on(self.inner.cross_signing_status()).into() + } +} + impl OlmMachine { /// Create a new `OlmMachine` /// @@ -1125,14 +1136,6 @@ impl OlmMachine { Ok(self.runtime.block_on(self.inner.bootstrap_cross_signing(true))?.into()) } - /// Get the status of the private cross signing keys. - /// - /// This can be used to check which private cross signing keys we have - /// stored locally. - pub fn cross_signing_status(&self) -> CrossSigningStatus { - self.runtime.block_on(self.inner.cross_signing_status()).into() - } - /// Export all our private cross signing keys. /// /// The export will contain the seed for the ed25519 keys as a base64 diff --git a/bindings/matrix-sdk-crypto-ffi/src/olm.udl b/bindings/matrix-sdk-crypto-ffi/src/olm.udl index 2ab938a67..5a3155755 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/olm.udl +++ b/bindings/matrix-sdk-crypto-ffi/src/olm.udl @@ -110,12 +110,6 @@ interface UserIdentity { ); }; -dictionary CrossSigningStatus { - boolean has_master; - boolean has_self_signing; - boolean has_user_signing; -}; - dictionary CrossSigningKeyExport { string? master_key; string? self_signing_key; @@ -449,7 +443,6 @@ interface OlmMachine { [Throws=CryptoStoreError] void discard_room_key([ByRef] string room_id); - CrossSigningStatus cross_signing_status(); [Throws=CryptoStoreError] BootstrapCrossSigningResult bootstrap_cross_signing(); CrossSigningKeyExport? export_cross_signing_keys(); From 0abef7740846c912d462fb4ba514f1774954224a Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 22 Feb 2023 17:29:40 +0100 Subject: [PATCH 016/166] doc(sdk): Improve documentation of `SlidingSyncBuilder`. --- crates/matrix-sdk/src/sliding_sync/builder.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/builder.rs b/crates/matrix-sdk/src/sliding_sync/builder.rs index d7f8beafd..7a92dfa96 100644 --- a/crates/matrix-sdk/src/sliding_sync/builder.rs +++ b/crates/matrix-sdk/src/sliding_sync/builder.rs @@ -21,7 +21,10 @@ use super::{ }; use crate::{Client, Result}; -/// Configuration for a Sliding Sync Instance +/// Configuration for a Sliding Sync instance. +/// +/// Get a new builder with methods like [`crate::Client::sliding_sync`], or +/// [`crate::SlidingSync::builder`]. #[derive(Clone, Debug)] pub struct SlidingSyncBuilder { storage_key: Option, @@ -223,7 +226,8 @@ impl SlidingSyncBuilder { /// Build the Sliding Sync. /// - /// If configured, load the cached data from cold storage. + /// If `self.storage_key` has some value, load the cached data from + /// cold storage. pub async fn build(mut self) -> Result { let client = self.client.ok_or(Error::BuildMissingField("client"))?; @@ -245,6 +249,7 @@ impl SlidingSyncBuilder { let FrozenSlidingSyncView { rooms_count, rooms_list, rooms } = frozen_view; view.set_from_cold(rooms_count, rooms_list); + for (key, frozen_room) in rooms.into_iter() { rooms_found.entry(key).or_insert_with(|| { SlidingSyncRoom::from_frozen(frozen_room, client.clone()) @@ -263,6 +268,7 @@ impl SlidingSyncBuilder { .transpose()? { trace!("frozen for generic found"); + if let Some(since) = to_device_since { if let Some(to_device_ext) = self.extensions.get_or_insert_with(Default::default).to_device.as_mut() @@ -270,12 +276,15 @@ impl SlidingSyncBuilder { to_device_ext.since = Some(since); } } + delta_token_inner = delta_token; } + trace!("sync unfrozen done"); }; trace!(len = rooms_found.len(), "rooms unfrozen"); + let rooms = Arc::new(StdRwLock::new(rooms_found)); let views = Arc::new(StdRwLock::new(self.views)); From 1d02515186d9342e9a6f4726789f6bfc2a05d31e Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 22 Feb 2023 17:36:00 +0100 Subject: [PATCH 017/166] chore(sdk): Re-organizing the code inside `sliding_sync::room`. 1. Put `FrozenSlidingSyncRoom` at the bottom of the module. 2. Put merge 2 `impl SlidingSyncRoom` together. 3. Remove the `AliveRoomTimeline` type alias. 4. Improve the documentation. --- crates/matrix-sdk/src/sliding_sync/room.rs | 150 ++++++++++----------- 1 file changed, 75 insertions(+), 75 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/room.rs b/crates/matrix-sdk/src/sliding_sync/room.rs index e2ea8d890..7c28a9a1d 100644 --- a/crates/matrix-sdk/src/sliding_sync/room.rs +++ b/crates/matrix-sdk/src/sliding_sync/room.rs @@ -23,9 +23,7 @@ use crate::{ Client, }; -type AliveRoomTimeline = Arc>; - -/// Room info as giving by the SlidingSync Feature. +/// Room details, provided by a [`SlidingSync`] instance. #[derive(Debug, Clone)] pub struct SlidingSyncRoom { client: Client, @@ -34,76 +32,7 @@ pub struct SlidingSyncRoom { is_loading_more: Mutable, is_cold: Arc, prev_batch: Mutable>, - timeline_queue: AliveRoomTimeline, -} - -#[derive(Serialize, Deserialize)] -pub(super) struct FrozenSlidingSyncRoom { - room_id: OwnedRoomId, - inner: v4::SlidingSyncRoom, - prev_batch: Option, - #[serde(rename = "timeline")] - timeline_queue: Vec, -} - -#[cfg(test)] -mod tests { - use matrix_sdk_base::deserialized_responses::TimelineEvent; - use ruma::{events::room::message::RoomMessageEventContent, RoomId}; - use serde_json::json; - - use super::*; - - #[test] - fn test_frozen_sliding_sync_room_serialize() { - let frozen_sliding_sync_room = FrozenSlidingSyncRoom { - room_id: <&RoomId>::try_from("!29fhd83h92h0:example.com").unwrap().to_owned(), - inner: v4::SlidingSyncRoom::default(), - prev_batch: Some("let it go!".to_owned()), - timeline_queue: vec![TimelineEvent { - event: Raw::new(&json! ({ - "content": RoomMessageEventContent::text_plain("let it gooo!"), - "type": "m.room.message", - "event_id": "$xxxxx:example.org", - "room_id": "!someroom:example.com", - "origin_server_ts": 2189, - "sender": "@bob:example.com", - })) - .unwrap() - .cast(), - encryption_info: None, - } - .into()], - }; - - assert_eq!( - serde_json::to_string(&frozen_sliding_sync_room).unwrap(), - "{\"room_id\":\"!29fhd83h92h0:example.com\",\"inner\":{},\"prev_batch\":\"let it go!\",\"timeline\":[{\"event\":{\"content\":{\"body\":\"let it gooo!\",\"msgtype\":\"m.text\"},\"event_id\":\"$xxxxx:example.org\",\"origin_server_ts\":2189,\"room_id\":\"!someroom:example.com\",\"sender\":\"@bob:example.com\",\"type\":\"m.room.message\"},\"encryption_info\":null}]}", - ); - } -} - -impl From<&SlidingSyncRoom> for FrozenSlidingSyncRoom { - fn from(value: &SlidingSyncRoom) -> Self { - let locked_tl = value.timeline_queue.lock_ref(); - let tl_len = locked_tl.len(); - // To not overflow the database, we only freeze the newest 10 items. On doing - // so, we must drop the `prev_batch` key however, as we'd otherwise - // create a gap between what we have loaded and where the - // prev_batch-key will start loading when paginating backwards. - let (prev_batch, timeline) = if tl_len > 10 { - let pos = tl_len - 10; - (None, locked_tl.iter().skip(pos).cloned().collect()) - } else { - (value.prev_batch.lock_ref().clone(), locked_tl.to_vec()) - }; - FrozenSlidingSyncRoom { - prev_batch, - timeline_queue: timeline, - room_id: value.room_id.clone(), - inner: value.inner.clone(), - } - } + timeline_queue: Arc>, } impl SlidingSyncRoom { @@ -119,9 +48,7 @@ impl SlidingSyncRoom { timeline_queue: Arc::new(MutableVec::new_with_values(timeline)), } } -} -impl SlidingSyncRoom { pub(crate) fn from( client: Client, room_id: OwnedRoomId, @@ -361,3 +288,76 @@ impl SlidingSyncRoom { } } } + +/// A “frozen” [`SlidingSyncRoom`], i.e. that can be written into, or read from +/// a store. +#[derive(Serialize, Deserialize)] +pub(super) struct FrozenSlidingSyncRoom { + room_id: OwnedRoomId, + inner: v4::SlidingSyncRoom, + prev_batch: Option, + #[serde(rename = "timeline")] + timeline_queue: Vec, +} + +impl From<&SlidingSyncRoom> for FrozenSlidingSyncRoom { + fn from(value: &SlidingSyncRoom) -> Self { + let locked_tl = value.timeline_queue.lock_ref(); + let tl_len = locked_tl.len(); + + // To not overflow the database, we only freeze the newest 10 items. On doing + // so, we must drop the `prev_batch` key however, as we'd otherwise + // create a gap between what we have loaded and where the + // prev_batch-key will start loading when paginating backwards. + let (prev_batch, timeline) = if tl_len > 10 { + let pos = tl_len - 10; + (None, locked_tl.iter().skip(pos).cloned().collect()) + } else { + (value.prev_batch.lock_ref().clone(), locked_tl.to_vec()) + }; + + Self { + prev_batch, + timeline_queue: timeline, + room_id: value.room_id.clone(), + inner: value.inner.clone(), + } + } +} + +#[cfg(test)] +mod tests { + use matrix_sdk_base::deserialized_responses::TimelineEvent; + use ruma::{events::room::message::RoomMessageEventContent, RoomId}; + use serde_json::json; + + use super::*; + + #[test] + fn test_frozen_sliding_sync_room_serialize() { + let frozen_sliding_sync_room = FrozenSlidingSyncRoom { + room_id: <&RoomId>::try_from("!29fhd83h92h0:example.com").unwrap().to_owned(), + inner: v4::SlidingSyncRoom::default(), + prev_batch: Some("let it go!".to_owned()), + timeline_queue: vec![TimelineEvent { + event: Raw::new(&json! ({ + "content": RoomMessageEventContent::text_plain("let it gooo!"), + "type": "m.room.message", + "event_id": "$xxxxx:example.org", + "room_id": "!someroom:example.com", + "origin_server_ts": 2189, + "sender": "@bob:example.com", + })) + .unwrap() + .cast(), + encryption_info: None, + } + .into()], + }; + + assert_eq!( + serde_json::to_string(&frozen_sliding_sync_room).unwrap(), + "{\"room_id\":\"!29fhd83h92h0:example.com\",\"inner\":{},\"prev_batch\":\"let it go!\",\"timeline\":[{\"event\":{\"content\":{\"body\":\"let it gooo!\",\"msgtype\":\"m.text\"},\"event_id\":\"$xxxxx:example.org\",\"origin_server_ts\":2189,\"room_id\":\"!someroom:example.com\",\"sender\":\"@bob:example.com\",\"type\":\"m.room.message\"},\"encryption_info\":null}]}", + ); + } +} From bcc04bdf353f75da6464404b76f3c0d5336c8cb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= <76261501+zecakeh@users.noreply.github.com> Date: Wed, 22 Feb 2023 17:59:22 +0100 Subject: [PATCH 018/166] feat(sdk): Add conversion from EventTimelineItem and VirtualTimelineItem to TimelineItem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- crates/matrix-sdk/src/room/timeline/mod.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/matrix-sdk/src/room/timeline/mod.rs b/crates/matrix-sdk/src/room/timeline/mod.rs index ee073fde1..cde9874b4 100644 --- a/crates/matrix-sdk/src/room/timeline/mod.rs +++ b/crates/matrix-sdk/src/room/timeline/mod.rs @@ -435,6 +435,18 @@ impl TimelineItem { } } +impl From for TimelineItem { + fn from(item: EventTimelineItem) -> Self { + Self::Event(item) + } +} + +impl From for TimelineItem { + fn from(item: VirtualTimelineItem) -> Self { + Self::Virtual(item) + } +} + // 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 rfind_event_item( From 56daa6cb8fd056831f4de804def7bc06b138d69c Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 22 Feb 2023 17:45:59 +0100 Subject: [PATCH 019/166] chore(sdk): Rename `SlidingSyncRoom` `pub(crate) from` to `pub(super) new`. First off, `SlidingSyncRoom.from` doesn't need to be visible to the entire crate, only to `crate::sliding_sync. Second, it's a constructor, so let's call it `new`. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 2 +- crates/matrix-sdk/src/sliding_sync/room.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index a35f0989f..b1937b6b2 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -1005,7 +1005,7 @@ impl SlidingSync { rooms_map.insert( id.clone(), - SlidingSyncRoom::from(self.client.clone(), id.clone(), room_data, timeline), + SlidingSyncRoom::new(self.client.clone(), id.clone(), room_data, timeline), ); } diff --git a/crates/matrix-sdk/src/sliding_sync/room.rs b/crates/matrix-sdk/src/sliding_sync/room.rs index 7c28a9a1d..e3b891158 100644 --- a/crates/matrix-sdk/src/sliding_sync/room.rs +++ b/crates/matrix-sdk/src/sliding_sync/room.rs @@ -49,7 +49,7 @@ impl SlidingSyncRoom { } } - pub(crate) fn from( + pub(super) fn new( client: Client, room_id: OwnedRoomId, mut inner: v4::SlidingSyncRoom, From 5d356ca1b5ceb7b7320f470209daace6c034cd7d Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 22 Feb 2023 18:04:29 +0100 Subject: [PATCH 020/166] feat(sdk): Avoid locking `prev_batch` in `SlidingSyncRoom::timeline_builder`. --- crates/matrix-sdk/src/sliding_sync/room.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/room.rs b/crates/matrix-sdk/src/sliding_sync/room.rs index e3b891158..05eabf7e4 100644 --- a/crates/matrix-sdk/src/sliding_sync/room.rs +++ b/crates/matrix-sdk/src/sliding_sync/room.rs @@ -57,6 +57,7 @@ impl SlidingSyncRoom { ) -> Self { // we overwrite to only keep one copy inner.timeline = vec![]; + Self { client, room_id, @@ -78,7 +79,7 @@ impl SlidingSyncRoom { *self.is_loading_more.lock_ref() } - /// the `prev_batch` key to fetch more timeline events for this room + /// The `prev_batch` key to fetch more timeline events for this room. pub fn prev_batch(&self) -> Option { self.prev_batch.lock_ref().clone() } @@ -90,9 +91,10 @@ impl SlidingSyncRoom { fn timeline_builder(&self) -> Option { if let Some(room) = self.client.get_room(&self.room_id) { - let timeline_queue = self.timeline_queue.lock_ref().to_vec(); - let prev_batch = self.prev_batch.lock_ref().clone(); - Some(Timeline::builder(&room).events(prev_batch, timeline_queue)) + Some( + Timeline::builder(&room) + .events(self.prev_batch.get_cloned(), self.timeline_queue.lock_ref().to_vec()), + ) } else if let Some(invited_room) = self.client.get_invited_room(&self.room_id) { Some(Timeline::builder(&invited_room).events(None, vec![])) } else { @@ -100,6 +102,7 @@ impl SlidingSyncRoom { room_id = ?self.room_id, "Room not found in client. Can't provide a timeline for it" ); + None } } From e80713e9336e88e844820161896b4fd37f8c6c20 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 22 Feb 2023 18:09:57 +0100 Subject: [PATCH 021/166] chore(sdk): `SlidingSyncRoom::update` takes ownership of `room_data`. First off, it's not necessary for `SlidingSyncRoom::update` to take a reference to `room_data: v4::SlidingSyncRoom`. When `update` is called, the iterator owns its items. Second, by taking ownership of `room_data`, we no longer need to clone all the data we need to assign to `self.inner`. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 2 +- crates/matrix-sdk/src/sliding_sync/room.rs | 29 +++++++--------------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index b1937b6b2..f3838fd72 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -998,7 +998,7 @@ impl SlidingSync { if let Some(mut room) = rooms_map.remove(&id) { // The room existed before, let's update it. - room.update(&room_data, timeline); + room.update(room_data, timeline); rooms_map.insert(id.clone(), room); } else { // First time we need this room, let's create it. diff --git a/crates/matrix-sdk/src/sliding_sync/room.rs b/crates/matrix-sdk/src/sliding_sync/room.rs index 05eabf7e4..eb9268ee3 100644 --- a/crates/matrix-sdk/src/sliding_sync/room.rs +++ b/crates/matrix-sdk/src/sliding_sync/room.rs @@ -148,7 +148,7 @@ impl SlidingSyncRoom { pub(super) fn update( &mut self, - room_data: &v4::SlidingSyncRoom, + room_data: v4::SlidingSyncRoom, timeline_updates: Vec, ) { let v4::SlidingSyncRoom { @@ -163,23 +163,12 @@ impl SlidingSyncRoom { .. } = room_data; - self.inner.unread_notifications = unread_notifications.clone(); - - if name.is_some() { - self.inner.name = name.clone(); - } - if initial.is_some() { - self.inner.initial = *initial; - } - if is_dm.is_some() { - self.inner.is_dm = *is_dm; - } - if !invite_state.is_empty() { - self.inner.invite_state = invite_state.clone(); - } - if !required_state.is_empty() { - self.inner.required_state = required_state.clone(); - } + self.inner.unread_notifications = unread_notifications; + self.inner.name = name; + self.inner.initial = initial; + self.inner.is_dm = is_dm; + self.inner.invite_state = invite_state; + self.inner.required_state = required_state; if let Some(batch) = prev_batch { self.prev_batch.lock_mut().replace(batch.clone()); @@ -193,7 +182,7 @@ impl SlidingSyncRoom { self.timeline_queue.lock_mut().replace_cloned(timeline_updates); self.is_cold.store(false, Ordering::SeqCst); - } else if *limited { + } else if limited { // The server alerted us that we missed items in between. self.timeline_queue.lock_mut().replace_cloned(timeline_updates); @@ -282,7 +271,7 @@ impl SlidingSyncRoom { } } } - } else if *limited { + } else if limited { // The timeline updates are empty. But `limited` is set to true. It's a way to // alert that we are stale. In this case, we should just clear the // existing timeline. From 4d33fe1c3132690b4820c6ff604ac88cbd2492b8 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 22 Feb 2023 18:14:03 +0100 Subject: [PATCH 022/166] chore(sdk): Rename some variable inside `FrozenSlidingSyncRoom`. --- crates/matrix-sdk/src/sliding_sync/room.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/room.rs b/crates/matrix-sdk/src/sliding_sync/room.rs index eb9268ee3..0f20a4b8f 100644 --- a/crates/matrix-sdk/src/sliding_sync/room.rs +++ b/crates/matrix-sdk/src/sliding_sync/room.rs @@ -294,18 +294,18 @@ pub(super) struct FrozenSlidingSyncRoom { impl From<&SlidingSyncRoom> for FrozenSlidingSyncRoom { fn from(value: &SlidingSyncRoom) -> Self { - let locked_tl = value.timeline_queue.lock_ref(); - let tl_len = locked_tl.len(); + let timeline = value.timeline_queue.lock_ref(); + let timeline_length = timeline.len(); // To not overflow the database, we only freeze the newest 10 items. On doing // so, we must drop the `prev_batch` key however, as we'd otherwise // create a gap between what we have loaded and where the // prev_batch-key will start loading when paginating backwards. - let (prev_batch, timeline) = if tl_len > 10 { - let pos = tl_len - 10; - (None, locked_tl.iter().skip(pos).cloned().collect()) + let (prev_batch, timeline) = if timeline_length > 10 { + let pos = timeline_length - 10; + (None, timeline.iter().skip(pos).cloned().collect()) } else { - (value.prev_batch.lock_ref().clone(), locked_tl.to_vec()) + (value.prev_batch.lock_ref().clone(), timeline.to_vec()) }; Self { From c766d8b3ef81465aa6d691d2229c4ff765e9e204 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 22 Feb 2023 18:25:16 +0100 Subject: [PATCH 023/166] chore(sdk): Address PR feedback. --- crates/matrix-sdk/src/sliding_sync/builder.rs | 4 +-- crates/matrix-sdk/src/sliding_sync/room.rs | 28 ++++++++++--------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/builder.rs b/crates/matrix-sdk/src/sliding_sync/builder.rs index 7a92dfa96..d823f6ff2 100644 --- a/crates/matrix-sdk/src/sliding_sync/builder.rs +++ b/crates/matrix-sdk/src/sliding_sync/builder.rs @@ -226,8 +226,8 @@ impl SlidingSyncBuilder { /// Build the Sliding Sync. /// - /// If `self.storage_key` has some value, load the cached data from - /// cold storage. + /// If `self.storage_key` is `Some(_)`, load the cached data from cold + /// storage. pub async fn build(mut self) -> Result { let client = self.client.ok_or(Error::BuildMissingField("client"))?; diff --git a/crates/matrix-sdk/src/sliding_sync/room.rs b/crates/matrix-sdk/src/sliding_sync/room.rs index 0f20a4b8f..c40279b65 100644 --- a/crates/matrix-sdk/src/sliding_sync/room.rs +++ b/crates/matrix-sdk/src/sliding_sync/room.rs @@ -36,19 +36,6 @@ pub struct SlidingSyncRoom { } impl SlidingSyncRoom { - pub(super) fn from_frozen(val: FrozenSlidingSyncRoom, client: Client) -> Self { - let FrozenSlidingSyncRoom { room_id, inner, prev_batch, timeline_queue: timeline } = val; - SlidingSyncRoom { - client, - room_id, - inner, - is_loading_more: Mutable::new(false), - is_cold: Arc::new(AtomicBool::new(true)), - prev_batch: Mutable::new(prev_batch), - timeline_queue: Arc::new(MutableVec::new_with_values(timeline)), - } - } - pub(super) fn new( client: Client, room_id: OwnedRoomId, @@ -279,6 +266,21 @@ impl SlidingSyncRoom { self.timeline_queue.lock_mut().clear(); } } + + pub(super) fn from_frozen(frozen_room: FrozenSlidingSyncRoom, client: Client) -> Self { + let FrozenSlidingSyncRoom { room_id, inner, prev_batch, timeline_queue: timeline } = + frozen_room; + + Self { + client, + room_id, + inner, + is_loading_more: Mutable::new(false), + is_cold: Arc::new(AtomicBool::new(true)), + prev_batch: Mutable::new(prev_batch), + timeline_queue: Arc::new(MutableVec::new_with_values(timeline)), + } + } } /// A “frozen” [`SlidingSyncRoom`], i.e. that can be written into, or read from From f3928e8d358c0dff03c332a00cf296913117d733 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 22 Feb 2023 20:03:11 +0100 Subject: [PATCH 024/166] chore(sdk): Remove an unnecessary clone in `SlidingSyncRoom::update`. --- crates/matrix-sdk/src/sliding_sync/room.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/sliding_sync/room.rs b/crates/matrix-sdk/src/sliding_sync/room.rs index c40279b65..ab2d70cb2 100644 --- a/crates/matrix-sdk/src/sliding_sync/room.rs +++ b/crates/matrix-sdk/src/sliding_sync/room.rs @@ -158,7 +158,7 @@ impl SlidingSyncRoom { self.inner.required_state = required_state; if let Some(batch) = prev_batch { - self.prev_batch.lock_mut().replace(batch.clone()); + self.prev_batch.lock_mut().replace(batch); } // There is timeline updates. From d5154577a0d79800abf2d7de6cbceb6cc571c7c8 Mon Sep 17 00:00:00 2001 From: boxdot Date: Thu, 23 Feb 2023 08:16:04 +0100 Subject: [PATCH 025/166] feat(sdk): Add custom login method The `Client::login_custom` allows to login by using a custom login method. In particular, it is possible to login to Synapse which supports JWT authentication. Signed-off-by: boxdot --- crates/matrix-sdk/src/client/login_builder.rs | 20 +++++++- crates/matrix-sdk/src/client/mod.rs | 50 ++++++++++++++++++- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk/src/client/login_builder.rs b/crates/matrix-sdk/src/client/login_builder.rs index efc4c602a..b589c372e 100644 --- a/crates/matrix-sdk/src/client/login_builder.rs +++ b/crates/matrix-sdk/src/client/login_builder.rs @@ -22,6 +22,7 @@ use std::{ use ruma::{ api::client::{session::login, uiaa::UserIdentifier}, assign, + serde::JsonObject, }; use tracing::{info, instrument}; @@ -35,16 +36,20 @@ use crate::{config::RequestConfig, Result}; /// [the spec]: https://spec.matrix.org/v1.3/client-server-api/#post_matrixclientv3login enum LoginMethod { /// Login type `m.login.password` - UserPassword { id: UserIdentifier, password: String }, + UserPassword { + id: UserIdentifier, + password: String, + }, /// Login type `m.token` Token(String), + Custom(login::v3::LoginInfo), } impl LoginMethod { fn id(&self) -> Option<&UserIdentifier> { match self { LoginMethod::UserPassword { id, .. } => Some(id), - LoginMethod::Token(_) => None, + LoginMethod::Token(_) | LoginMethod::Custom(_) => None, } } @@ -52,6 +57,7 @@ impl LoginMethod { match self { LoginMethod::UserPassword { .. } => "identifier and password", LoginMethod::Token(_) => "token", + LoginMethod::Custom(_) => "custom", } } @@ -61,6 +67,7 @@ impl LoginMethod { login::v3::LoginInfo::Password(login::v3::Password::new(id, password)) } LoginMethod::Token(token) => login::v3::LoginInfo::Token(login::v3::Token::new(token)), + LoginMethod::Custom(login_info) => login_info, } } } @@ -98,6 +105,15 @@ impl LoginBuilder { Self::new(client, LoginMethod::Token(token)) } + pub(super) fn new_custom( + client: Client, + login_type: &str, + data: JsonObject, + ) -> serde_json::Result { + let login_info = login::v3::LoginInfo::new(login_type, data)?; + Ok(Self::new(client, LoginMethod::Custom(login_info))) + } + /// Set the device ID. /// /// The device ID is a unique ID that will be associated with this session. diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 33108a66e..2f4b758e9 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -61,8 +61,10 @@ use ruma::{ error::FromHttpResponseError, MatrixVersion, OutgoingRequest, SendAccessToken, }, - assign, DeviceId, OwnedDeviceId, OwnedRoomId, OwnedServerName, RoomAliasId, RoomId, - RoomOrAliasId, ServerName, UInt, UserId, + assign, + serde::JsonObject, + DeviceId, OwnedDeviceId, OwnedRoomId, OwnedServerName, RoomAliasId, RoomId, RoomOrAliasId, + ServerName, UInt, UserId, }; use serde::de::DeserializeOwned; #[cfg(not(target_arch = "wasm32"))] @@ -1017,6 +1019,50 @@ impl Client { LoginBuilder::new_password(self.clone(), id, password.to_owned()) } + /// Login to the server with a custom login type + /// + /// # Arguments + /// + /// * `login_type` - Identifier of the custom login type, e.g. + /// `org.matrix.login.jwt` + /// + /// * `data` - The additional data which should be attached to the login + /// request. + /// + /// ```no_run + /// # use futures::executor::block_on; + /// # use url::Url; + /// # let homeserver = Url::parse("http://example.com").unwrap(); + /// # block_on(async { + /// use matrix_sdk::Client; + /// + /// let client = Client::new(homeserver).await?; + /// let user = "example"; + /// + /// let response = client + /// .login_custom( + /// "org.matrix.login.jwt", + /// [("token".to_owned(), "jwt_token_content".into())] + /// .into_iter() + /// .collect(), + /// )? + /// .initial_device_display_name("My bot") + /// .await?; + /// + /// println!( + /// "Logged in as {user}, got device_id {} and access_token {}", + /// response.device_id, response.access_token, + /// ); + /// # anyhow::Ok(()) }); + /// ``` + pub fn login_custom( + &self, + login_type: &str, + data: JsonObject, + ) -> serde_json::Result { + LoginBuilder::new_custom(self.clone(), login_type, data) + } + /// Login to the server with a token. /// /// This token is usually received in the SSO flow after following the URL From 17b86b7c38e0d535a819134571e4fb85549b0d71 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 22 Feb 2023 16:09:30 +0100 Subject: [PATCH 026/166] refactor(crypto): Replace futures-signals with eyeball --- Cargo.lock | 20 +++- Cargo.toml | 1 + crates/matrix-sdk-crypto/Cargo.toml | 2 +- .../src/verification/qrcode.rs | 69 +++++++------ .../src/verification/requests.rs | 96 +++++++++---------- .../src/verification/sas/mod.rs | 68 ++++++------- 6 files changed, 138 insertions(+), 118 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1c1e82ac7..094f13eac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1475,6 +1475,18 @@ dependencies = [ "syn", ] +[[package]] +name = "eyeball" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "705cca477a21fef6fb7359c13ad9008e087787c7a3faca0e2e75507326e15bd7" +dependencies = [ + "futures-core", + "readlock", + "tokio", + "tokio-stream", +] + [[package]] name = "eyeball-im" version = "0.1.0" @@ -2689,9 +2701,9 @@ dependencies = [ "ctr", "dashmap", "event-listener", + "eyeball", "futures", "futures-core", - "futures-signals", "futures-util", "hmac", "http", @@ -3985,6 +3997,12 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "readlock" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a8f0cb425ba44d6bde0d063097aae68a2ce31e1d5359e96427704f33f4f73d9" + [[package]] name = "redox_syscall" version = "0.2.16" diff --git a/Cargo.toml b/Cargo.toml index 4d32bc6be..3e38fbf58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ base64 = "0.21.0" byteorder = "1.4.3" ctor = "0.1.26" dashmap = "5.2.0" +eyeball = "0.1.3" eyeball-im = "0.1.0" http = "0.2.6" ruma = { version = "0.8.0", features = ["client-api-c"] } diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index 7e4b0b1af..146093242 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -34,9 +34,9 @@ byteorder = { workspace = true } ctr = "0.9.1" dashmap = { workspace = true } event-listener = "2.5.2" +eyeball = { workspace = true } 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 = { workspace = true, optional = true } # feature = testing only matrix-sdk-qrcode = { version = "0.4.0", path = "../matrix-sdk-qrcode", optional = true } diff --git a/crates/matrix-sdk-crypto/src/verification/qrcode.rs b/crates/matrix-sdk-crypto/src/verification/qrcode.rs index d72fcb9a8..3bcdedbc7 100644 --- a/crates/matrix-sdk-crypto/src/verification/qrcode.rs +++ b/crates/matrix-sdk-crypto/src/verification/qrcode.rs @@ -12,10 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::Arc; +use std::sync::{Arc, RwLock as StdRwLock}; +use eyeball::Observable; use futures_core::Stream; -use futures_signals::signal::{Mutable, SignalExt}; use futures_util::StreamExt; use matrix_sdk_qrcode::{ qrcode::QrCode, EncodingError, QrVerificationData, SelfVerificationData, @@ -135,7 +135,7 @@ impl From<&InnerState> for QrVerificationState { pub struct QrVerification { flow_id: FlowId, inner: Arc, - state: Arc>, + state: Arc>>, identities: IdentitiesBeingVerified, request_handle: Option, we_started: bool, @@ -145,8 +145,8 @@ impl std::fmt::Debug for QrVerification { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("QrVerification") .field("flow_id", &self.flow_id) - .field("inner", self.inner.as_ref()) - .field("state", &self.state.lock_ref()) + .field("inner", &self.inner) + .field("state", &self.state) .finish() } } @@ -157,12 +157,12 @@ impl QrVerification { /// When the verification object is in this state it's required that the /// user confirms that the other side has scanned the QR code. pub fn has_been_scanned(&self) -> bool { - matches!(&*self.state.lock_ref(), InnerState::Scanned(_)) + matches!(**self.state.read().unwrap(), InnerState::Scanned(_)) } /// Has the scanning of the QR code been confirmed by us. pub fn has_been_confirmed(&self) -> bool { - matches!(&*self.state.lock_ref(), InnerState::Confirmed(_)) + matches!(**self.state.read().unwrap(), InnerState::Confirmed(_)) } /// Get our own user id. @@ -189,7 +189,7 @@ impl QrVerification { /// Get info about the cancellation if the verification flow has been /// cancelled. pub fn cancel_info(&self) -> Option { - if let InnerState::Cancelled(c) = &*self.state.lock_ref() { + if let InnerState::Cancelled(c) = &**self.state.read().unwrap() { Some(c.state.clone().into()) } else { None @@ -198,12 +198,12 @@ impl QrVerification { /// Has the verification flow completed. pub fn is_done(&self) -> bool { - matches!(&*self.state.lock_ref(), InnerState::Done(_)) + matches!(**self.state.read().unwrap(), InnerState::Done(_)) } /// Has the verification flow been cancelled. pub fn is_cancelled(&self) -> bool { - matches!(&*self.state.lock_ref(), InnerState::Cancelled(_)) + matches!(**self.state.read().unwrap(), InnerState::Cancelled(_)) } /// Is this a verification that is veryfying one of our own devices @@ -214,7 +214,7 @@ impl QrVerification { /// Have we successfully scanned the QR code and are able to send a /// reciprocation event. pub fn reciprocated(&self) -> bool { - matches!(&*self.state.lock_ref(), InnerState::Reciprocated(_)) + matches!(**self.state.read().unwrap(), InnerState::Reciprocated(_)) } /// Get the unique ID that identifies this QR code verification flow. @@ -268,7 +268,7 @@ impl QrVerification { /// /// [`cancel()`]: #method.cancel pub fn cancel_with_code(&self, code: CancelCode) -> Option { - let mut state = self.state.lock_mut(); + let mut state = self.state.write().unwrap(); if let Some(request) = &self.request_handle { request.cancel_with_code(&code); @@ -277,13 +277,13 @@ impl QrVerification { let new_state = QrState::::new(true, code); let content = new_state.as_content(self.flow_id()); - match &*state { + match &**state { InnerState::Confirmed(_) | InnerState::Created(_) | InnerState::Scanned(_) | InnerState::Reciprocated(_) | InnerState::Done(_) => { - *state = InnerState::Cancelled(new_state); + Observable::set(&mut state, InnerState::Cancelled(new_state)); Some(self.content_to_request(content)) } InnerState::Cancelled(_) => None, @@ -296,7 +296,7 @@ impl QrVerification { /// This will return some `OutgoingContent` if the object is in the correct /// state to start the verification flow, otherwise `None`. pub fn reciprocate(&self) -> Option { - match &*self.state.lock_ref() { + match &**self.state.read().unwrap() { InnerState::Reciprocated(s) => { Some(self.content_to_request(s.as_content(self.flow_id()))) } @@ -310,13 +310,13 @@ impl QrVerification { /// Confirm that the other side has scanned our QR code. pub fn confirm_scanning(&self) -> Option { - let mut state = self.state.lock_mut(); + let mut state = self.state.write().unwrap(); - match &*state { + match &**state { InnerState::Scanned(s) => { let new_state = s.clone().confirm_scanning(); let content = new_state.as_content(&self.flow_id); - *state = InnerState::Confirmed(new_state); + Observable::set(&mut state, InnerState::Confirmed(new_state)); Some(self.content_to_request(content)) } @@ -366,9 +366,7 @@ impl QrVerification { VerificationResult::SignatureUpload(s) => (None, Some(s)), }; - let mut guard = self.state.lock_mut(); - *guard = new_state; - + Observable::set(&mut self.state.write().unwrap(), new_state); Ok((content.map(|c| self.content_to_request(c)), request)) } @@ -379,7 +377,7 @@ impl QrVerification { (Option, Option), CryptoStoreError, > { - let state = (*self.state.lock_ref()).clone(); + let state = (*self.state.read().unwrap()).clone(); Ok(match state { InnerState::Confirmed(s) => { @@ -432,17 +430,17 @@ impl QrVerification { &self, content: &StartContent<'_>, ) -> Option { - let mut state = self.state.lock_mut(); + let mut state = self.state.write().unwrap(); - match &*state { + match &**state { InnerState::Created(s) => match s.clone().receive_reciprocate(content) { Ok(s) => { - *state = InnerState::Scanned(s); + Observable::set(&mut state, InnerState::Scanned(s)); None } Err(s) => { let content = s.as_content(self.flow_id()); - *state = InnerState::Cancelled(s); + Observable::set(&mut state, InnerState::Cancelled(s)); Some(self.content_to_request(content)) } }, @@ -456,9 +454,9 @@ impl QrVerification { pub(crate) fn receive_cancel(&self, sender: &UserId, content: &CancelContent<'_>) { if sender == self.other_user_id() { - let mut state = self.state.lock_mut(); + let mut state = self.state.write().unwrap(); - let new_state = match &*state { + let new_state = match &**state { InnerState::Created(s) => s.clone().into_cancelled(content), InnerState::Scanned(s) => s.clone().into_cancelled(content), InnerState::Confirmed(s) => s.clone().into_cancelled(content), @@ -472,7 +470,7 @@ impl QrVerification { "Cancelling a QR verification, other user has cancelled" ); - *state = InnerState::Cancelled(new_state); + Observable::set(&mut state, InnerState::Cancelled(new_state)); } } @@ -630,10 +628,9 @@ impl QrVerification { Ok(Self { flow_id, inner: qr_code.into(), - state: Mutable::new(InnerState::Reciprocated(QrState { + state: Arc::new(StdRwLock::new(Observable::new(InnerState::Reciprocated(QrState { state: Reciprocated { secret, own_device_id }, - })) - .into(), + })))), identities, we_started, request_handle, @@ -652,7 +649,9 @@ impl QrVerification { Self { flow_id, inner: inner.into(), - state: Mutable::new(InnerState::Created(QrState { state: Created { secret } })).into(), + state: Arc::new(StdRwLock::new(Observable::new(InnerState::Created(QrState { + state: Created { secret }, + })))), identities, we_started, request_handle, @@ -663,7 +662,7 @@ impl QrVerification { /// /// The changes are presented as a stream of [`QrVerificationState`] values. pub fn changes(&self) -> impl Stream { - self.state.signal_cloned().to_stream().map(|s| (&s).into()) + Observable::subscribe(&self.state.read().unwrap()).map(|s| (&s).into()) } /// Get the current state the verification process is in. @@ -671,7 +670,7 @@ impl QrVerification { /// To listen to changes to the [`QrVerificationState`] use the /// [`QrVerification::changes`] method. pub fn state(&self) -> QrVerificationState { - (&*self.state.lock_ref()).into() + (&**self.state.read().unwrap()).into() } } diff --git a/crates/matrix-sdk-crypto/src/verification/requests.rs b/crates/matrix-sdk-crypto/src/verification/requests.rs index 21ffd2e27..c15712d79 100644 --- a/crates/matrix-sdk-crypto/src/verification/requests.rs +++ b/crates/matrix-sdk-crypto/src/verification/requests.rs @@ -12,10 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{sync::Arc, time::Duration}; +use std::{ + sync::{Arc, RwLock as StdRwLock}, + time::Duration, +}; +use eyeball::Observable; use futures_core::Stream; -use futures_signals::signal::{Mutable, SignalExt}; use futures_util::StreamExt; use matrix_sdk_common::instant::Instant; #[cfg(feature = "qrcode")] @@ -136,7 +139,7 @@ pub struct VerificationRequest { account: ReadOnlyAccount, flow_id: Arc, other_user_id: Arc, - inner: Arc>, + inner: Arc>>, creation_time: Arc, we_started: bool, recipient_devices: Arc>, @@ -152,20 +155,20 @@ pub struct VerificationRequest { /// `VerificationRequest` object. #[derive(Clone, Debug)] pub(crate) struct RequestHandle { - inner: Arc>, + inner: Arc>>, } impl RequestHandle { pub fn cancel_with_code(&self, cancel_code: &CancelCode) { - let mut guard = self.inner.lock_mut(); + let mut guard = self.inner.write().unwrap(); if let Some(updated) = guard.cancel(true, cancel_code) { - *guard = updated; + Observable::set(&mut guard, updated); } } } -impl From>> for RequestHandle { - fn from(inner: Arc>) -> Self { +impl From>>> for RequestHandle { + fn from(inner: Arc>>) -> Self { Self { inner } } } @@ -180,14 +183,9 @@ impl VerificationRequest { methods: Option>, ) -> Self { let account = store.account.clone(); - let inner = Mutable::new(InnerRequest::Created(RequestState::new( - cache.clone(), - store, - other_user, - &flow_id, - methods, - ))) - .into(); + let inner = Arc::new(StdRwLock::new(Observable::new(InnerRequest::Created( + RequestState::new(cache.clone(), store, other_user, &flow_id, methods), + )))); Self { account, @@ -206,9 +204,9 @@ impl VerificationRequest { /// self-verifications and it should be sent to the specific device that we /// want to verify. pub(crate) fn request_to_device(&self) -> ToDeviceRequest { - let inner = self.inner.lock_ref(); + let inner = self.inner.read().unwrap(); - let methods = if let InnerRequest::Created(c) = &*inner { + let methods = if let InnerRequest::Created(c) = &**inner { c.state.our_methods.clone() } else { SUPPORTED_METHODS.to_vec() @@ -264,7 +262,7 @@ impl VerificationRequest { /// The id of the other device that is participating in this verification. pub fn other_device_id(&self) -> Option { - match &*self.inner.lock_ref() { + match &**self.inner.read().unwrap() { InnerRequest::Requested(r) => Some(r.state.other_device_id.clone()), InnerRequest::Ready(r) => Some(r.state.other_device_id.clone()), InnerRequest::Created(_) @@ -285,7 +283,7 @@ impl VerificationRequest { /// Get info about the cancellation if the verification request has been /// cancelled. pub fn cancel_info(&self) -> Option { - if let InnerRequest::Cancelled(c) = &*self.inner.lock_ref() { + if let InnerRequest::Cancelled(c) = &**self.inner.read().unwrap() { Some(c.state.clone().into()) } else { None @@ -294,12 +292,12 @@ impl VerificationRequest { /// Has the verification request been answered by another device. pub fn is_passive(&self) -> bool { - matches!(&*self.inner.lock_ref(), InnerRequest::Passive(_)) + matches!(**self.inner.read().unwrap(), InnerRequest::Passive(_)) } /// Is the verification request ready to start a verification flow. pub fn is_ready(&self) -> bool { - matches!(&*self.inner.lock_ref(), InnerRequest::Ready(_)) + matches!(**self.inner.read().unwrap(), InnerRequest::Ready(_)) } /// Has the verification flow timed out. @@ -312,7 +310,7 @@ impl VerificationRequest { /// Will be present only if the other side requested the verification or if /// we're in the ready state. pub fn their_supported_methods(&self) -> Option> { - match &*self.inner.lock_ref() { + match &**self.inner.read().unwrap() { InnerRequest::Requested(r) => Some(r.state.their_methods.clone()), InnerRequest::Ready(r) => Some(r.state.their_methods.clone()), InnerRequest::Created(_) @@ -327,7 +325,7 @@ impl VerificationRequest { /// Will be present only we requested the verification or if we're in the /// ready state. pub fn our_supported_methods(&self) -> Option> { - match &*self.inner.lock_ref() { + match &**self.inner.read().unwrap() { InnerRequest::Created(r) => Some(r.state.our_methods.clone()), InnerRequest::Ready(r) => Some(r.state.our_methods.clone()), InnerRequest::Requested(_) @@ -354,20 +352,20 @@ impl VerificationRequest { /// Has the verification flow that was started with this request finished. pub fn is_done(&self) -> bool { - matches!(&*self.inner.lock_ref(), InnerRequest::Done(_)) + matches!(**self.inner.read().unwrap(), InnerRequest::Done(_)) } /// Has the verification flow that was started with this request been /// cancelled. pub fn is_cancelled(&self) -> bool { - matches!(&*self.inner.lock_ref(), InnerRequest::Cancelled(_)) + matches!(**self.inner.read().unwrap(), InnerRequest::Cancelled(_)) } /// Generate a QR code that can be used by another client to start a QR code /// based verification. #[cfg(feature = "qrcode")] pub async fn generate_qr_code(&self) -> Result, CryptoStoreError> { - let inner = self.inner.lock_ref().clone(); + let inner = self.inner.read().unwrap().clone(); inner.generate_qr_code(self.we_started, self.inner.clone().into()).await } @@ -384,7 +382,7 @@ impl VerificationRequest { &self, data: QrVerificationData, ) -> Result, ScanError> { - let future = if let InnerRequest::Ready(r) = &*self.inner.lock_ref() { + let future = if let InnerRequest::Ready(r) = &**self.inner.read().unwrap() { QrVerification::from_scan( r.store.clone(), r.other_user_id.clone(), @@ -437,9 +435,9 @@ impl VerificationRequest { Self { verification_cache: cache.clone(), - inner: Arc::new(Mutable::new(InnerRequest::Requested( + inner: Arc::new(StdRwLock::new(Observable::new(InnerRequest::Requested( RequestState::from_request_event(cache, store, sender, &flow_id, content), - ))), + )))), account, other_user_id: sender.into(), flow_id: flow_id.into(), @@ -459,13 +457,13 @@ impl VerificationRequest { &self, methods: Vec, ) -> Option { - let mut guard = self.inner.lock_mut(); + let mut guard = self.inner.write().unwrap(); let Some((updated, content)) = guard.accept(methods) else { return None; }; - *guard = updated; + Observable::set(&mut guard, updated); let request = match content { OutgoingContent::ToDevice(content) => ToDeviceRequest::with_id( @@ -505,16 +503,16 @@ impl VerificationRequest { } fn cancel_with_code(&self, cancel_code: CancelCode) -> Option { - let mut guard = self.inner.lock_mut(); + let mut guard = self.inner.write().unwrap(); - let send_to_everyone = self.we_started() && matches!(&*guard, InnerRequest::Created(_)); + let send_to_everyone = self.we_started() && matches!(**guard, InnerRequest::Created(_)); let other_device = guard.other_device_id(); if let Some(updated) = guard.cancel(true, &cancel_code) { - *guard = updated; + Observable::set(&mut guard, updated); } - let content = if let InnerRequest::Cancelled(c) = &*guard { + let content = if let InnerRequest::Cancelled(c) = &**guard { Some(c.state.as_content(self.flow_id())) } else { None @@ -630,11 +628,12 @@ impl VerificationRequest { } pub(crate) fn receive_ready(&self, sender: &UserId, content: &ReadyContent<'_>) { - let mut guard = self.inner.lock_mut(); + let mut guard = self.inner.write().unwrap(); - match &*guard { + match &**guard { InnerRequest::Created(s) => { - *guard = InnerRequest::Ready(s.clone().into_ready(sender, content)); + let new_value = InnerRequest::Ready(s.clone().into_ready(sender, content)); + Observable::set(&mut guard, new_value); if let Some(request) = self.cancel_for_other_devices(CancelCode::Accepted, Some(content.from_device())) @@ -645,7 +644,8 @@ impl VerificationRequest { InnerRequest::Requested(s) => { if sender == self.own_user_id() && content.from_device() != self.account.device_id() { - *guard = InnerRequest::Passive(s.clone().into_passive(content)) + let new_value = InnerRequest::Passive(s.clone().into_passive(content)); + Observable::set(&mut guard, new_value); } } InnerRequest::Ready(_) @@ -660,7 +660,7 @@ impl VerificationRequest { sender: &UserId, content: &StartContent<'_>, ) -> Result<(), CryptoStoreError> { - let inner = self.inner.lock_ref().clone(); + let inner = self.inner.read().unwrap().clone(); let InnerRequest::Ready(s) = inner else { warn!( @@ -682,9 +682,9 @@ impl VerificationRequest { "Marking a verification request as done" ); - let mut guard = self.inner.lock_mut(); + let mut guard = self.inner.write().unwrap(); if let Some(updated) = guard.receive_done(content) { - *guard = updated; + Observable::set(&mut guard, updated); } } } @@ -699,9 +699,9 @@ impl VerificationRequest { code = content.cancel_code().as_str(), "Cancelling a verification request, other user has cancelled" ); - let mut guard = self.inner.lock_mut(); + let mut guard = self.inner.write().unwrap(); if let Some(updated) = guard.cancel(false, content.cancel_code()) { - *guard = updated; + Observable::set(&mut guard, updated); } if self.we_started() { @@ -717,7 +717,7 @@ impl VerificationRequest { pub async fn start_sas( &self, ) -> Result, CryptoStoreError> { - let inner = self.inner.lock_ref().clone(); + let inner = self.inner.read().unwrap().clone(); Ok(match &inner { InnerRequest::Ready(s) => { @@ -771,7 +771,7 @@ impl VerificationRequest { /// The changes are presented as a stream of [`VerificationRequestState`] /// values. pub fn changes(&self) -> impl Stream { - self.inner.signal_cloned().to_stream().map(|s| (&s).into()) + Observable::subscribe(&self.inner.read().unwrap()).map(|s| (&s).into()) } /// Get the current state the verification request is in. @@ -779,7 +779,7 @@ impl VerificationRequest { /// To listen to changes to the [`VerificationRequestState`] use the /// [`VerificationRequest::changes`] method. pub fn state(&self) -> VerificationRequestState { - (&*self.inner.lock_ref()).into() + (&**self.inner.read().unwrap()).into() } } diff --git a/crates/matrix-sdk-crypto/src/verification/sas/mod.rs b/crates/matrix-sdk-crypto/src/verification/sas/mod.rs index 0ffad3c12..311bd8419 100644 --- a/crates/matrix-sdk-crypto/src/verification/sas/mod.rs +++ b/crates/matrix-sdk-crypto/src/verification/sas/mod.rs @@ -16,10 +16,10 @@ mod helpers; mod inner_sas; mod sas_state; -use std::sync::Arc; +use std::sync::{Arc, RwLock as StdRwLock}; +use eyeball::Observable; use futures_core::Stream; -use futures_signals::signal::{Mutable, SignalExt}; use futures_util::StreamExt; use inner_sas::InnerSas; use ruma::{ @@ -49,7 +49,7 @@ use crate::{ /// Short authentication string object. #[derive(Clone, Debug)] pub struct Sas { - inner: Arc>, + inner: Arc>>, account: ReadOnlyAccount, identities_being_verified: IdentitiesBeingVerified, flow_id: Arc, @@ -268,12 +268,12 @@ impl Sas { /// Does this verification flow support displaying emoji for the short /// authentication string. pub fn supports_emoji(&self) -> bool { - self.inner.lock_ref().supports_emoji() + self.inner.read().unwrap().supports_emoji() } /// Did this verification flow start from a verification request. pub fn started_from_request(&self) -> bool { - self.inner.lock_ref().started_from_request() + self.inner.read().unwrap().started_from_request() } /// Is this a verification that is veryfying one of our own devices. @@ -283,18 +283,18 @@ impl Sas { /// Have we confirmed that the short auth string matches. pub fn have_we_confirmed(&self) -> bool { - self.inner.lock_ref().have_we_confirmed() + self.inner.read().unwrap().have_we_confirmed() } /// Has the verification been accepted by both parties. pub fn has_been_accepted(&self) -> bool { - self.inner.lock_ref().has_been_accepted() + self.inner.read().unwrap().has_been_accepted() } /// Get info about the cancellation if the verification flow has been /// cancelled. pub fn cancel_info(&self) -> Option { - if let InnerSas::Cancelled(c) = &*self.inner.lock_ref() { + if let InnerSas::Cancelled(c) = &**self.inner.read().unwrap() { Some(c.state.as_ref().clone().into()) } else { None @@ -309,7 +309,9 @@ impl Sas { #[cfg(test)] #[allow(dead_code)] pub(crate) fn set_creation_time(&self, time: matrix_sdk_common::instant::Instant) { - self.inner.lock_mut().set_creation_time(time) + Observable::update(&mut self.inner.write().unwrap(), |inner| { + inner.set_creation_time(time); + }); } fn start_helper( @@ -331,7 +333,7 @@ impl Sas { ( Sas { - inner: Arc::new(Mutable::new(inner)), + inner: Arc::new(StdRwLock::new(Observable::new(inner))), account, identities_being_verified: identities, flow_id: flow_id.into(), @@ -415,7 +417,7 @@ impl Sas { let account = identities.store.account.clone(); Ok(Sas { - inner: Arc::new(Mutable::new(inner)), + inner: Arc::new(StdRwLock::new(Observable::new(inner))), account, identities_being_verified: identities, flow_id: flow_id.into(), @@ -445,12 +447,12 @@ impl Sas { let old_state = self.state_debug(); let request = { - let mut guard = self.inner.lock_mut(); + let mut guard = self.inner.write().unwrap(); let sas: InnerSas = (*guard).clone(); let methods = settings.allowed_methods; if let Some((sas, content)) = sas.accept(methods) { - *guard = sas; + Observable::set(&mut guard, sas); Some(match content { OwnedAcceptContent::ToDevice(c) => { @@ -493,12 +495,12 @@ impl Sas { ) -> Result<(Vec, Option), CryptoStoreError> { let (contents, done) = { - let mut guard = self.inner.lock_mut(); + let mut guard = self.inner.write().unwrap(); let sas: InnerSas = (*guard).clone(); let (sas, contents) = sas.confirm(); - *guard = sas; + Observable::set(&mut guard, sas); (contents, guard.is_done()) }; @@ -565,7 +567,7 @@ impl Sas { /// [`cancel()`]: #method.cancel pub fn cancel_with_code(&self, code: CancelCode) -> Option { let content = { - let mut guard = self.inner.lock_mut(); + let mut guard = self.inner.write().unwrap(); if let Some(request) = &self.request_handle { request.cancel_with_code(&code); @@ -573,7 +575,7 @@ impl Sas { let sas: InnerSas = (*guard).clone(); let (sas, content) = sas.cancel(true, code); - *guard = sas; + Observable::set(&mut guard, sas); content.map(|c| match c { OutgoingContent::Room(room_id, content) => { @@ -598,22 +600,22 @@ impl Sas { /// Has the SAS verification flow timed out. pub fn timed_out(&self) -> bool { - self.inner.lock_ref().timed_out() + self.inner.read().unwrap().timed_out() } /// Are we in a state where we can show the short auth string. pub fn can_be_presented(&self) -> bool { - self.inner.lock_ref().can_be_presented() + self.inner.read().unwrap().can_be_presented() } /// Is the SAS flow done. pub fn is_done(&self) -> bool { - self.inner.lock_ref().is_done() + self.inner.read().unwrap().is_done() } /// Is the SAS flow canceled. pub fn is_cancelled(&self) -> bool { - self.inner.lock_ref().is_cancelled() + self.inner.read().unwrap().is_cancelled() } /// Get the emoji version of the short auth string. @@ -621,7 +623,7 @@ impl Sas { /// Returns None if we can't yet present the short auth string, otherwise /// seven tuples containing the emoji and description. pub fn emoji(&self) -> Option<[Emoji; 7]> { - self.inner.lock_ref().emoji() + self.inner.read().unwrap().emoji() } /// Get the index of the emoji representing the short auth string @@ -631,7 +633,7 @@ impl Sas { /// converted to an emoji using the /// [relevant spec entry](https://spec.matrix.org/unstable/client-server-api/#sas-method-emoji). pub fn emoji_index(&self) -> Option<[u8; 7]> { - self.inner.lock_ref().emoji_index() + self.inner.read().unwrap().emoji_index() } /// Get the decimal version of the short auth string. @@ -640,7 +642,7 @@ impl Sas { /// tuple containing three 4-digit integers that represent the short auth /// string. pub fn decimals(&self) -> Option<(u16, u16, u16)> { - self.inner.lock_ref().decimals() + self.inner.read().unwrap().decimals() } /// Listen for changes in the SAS verification process. @@ -732,16 +734,16 @@ impl Sas { /// # anyhow::Ok(()) }); /// ``` pub fn changes(&self) -> impl Stream { - self.inner.signal_cloned().to_stream().map(|s| (&s).into()) + Observable::subscribe(&self.inner.read().unwrap()).map(|s| (&s).into()) } /// Get the current state of the verification process. pub fn state(&self) -> SasState { - (&*self.inner.lock_ref()).into() + (&**self.inner.read().unwrap()).into() } fn state_debug(&self) -> State { - (&*self.inner.lock_ref()).into() + (&**self.inner.read().unwrap()).into() } pub(crate) fn receive_any_event( @@ -752,11 +754,11 @@ impl Sas { let old_state = self.state_debug(); let content = { - let mut guard = self.inner.lock_mut(); + let mut guard = self.inner.write().unwrap(); let sas: InnerSas = (*guard).clone(); let (sas, content) = sas.receive_any_event(sender, content); - *guard = sas; + Observable::set(&mut guard, sas); content }; @@ -776,12 +778,12 @@ impl Sas { let old_state = self.state_debug(); { - let mut guard = self.inner.lock_mut(); + let mut guard = self.inner.write().unwrap(); let sas: InnerSas = (*guard).clone(); if let Some(sas) = sas.mark_request_as_sent(request_id) { - *guard = sas; + Observable::set(&mut guard, sas); } else { error!( flow_id = self.flow_id().as_str(), @@ -803,11 +805,11 @@ impl Sas { } pub(crate) fn verified_devices(&self) -> Option> { - self.inner.lock_ref().verified_devices() + self.inner.read().unwrap().verified_devices() } pub(crate) fn verified_identities(&self) -> Option> { - self.inner.lock_ref().verified_identities() + self.inner.read().unwrap().verified_identities() } pub(crate) fn content_to_request(&self, content: AnyToDeviceEventContent) -> ToDeviceRequest { From ba6fc794b0346e847e138628e19d3816466fe24f Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 22 Feb 2023 18:07:31 +0100 Subject: [PATCH 027/166] refactor(base): Replace futures-signals with eyeball --- Cargo.lock | 3 +- crates/matrix-sdk-base/Cargo.toml | 2 +- crates/matrix-sdk-base/src/client.rs | 5 +-- crates/matrix-sdk-base/src/store/mod.rs | 18 +++++------ crates/matrix-sdk/Cargo.toml | 1 + crates/matrix-sdk/src/client/builder.rs | 2 +- crates/matrix-sdk/src/client/mod.rs | 31 +++++++++---------- .../tests/integration/refresh_token.rs | 29 +++++------------ 8 files changed, 38 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 094f13eac..96a5a382e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2571,6 +2571,7 @@ dependencies = [ "dashmap", "dirs", "event-listener", + "eyeball", "eyeball-im", "eyre", "futures", @@ -2646,9 +2647,9 @@ dependencies = [ "async-trait", "ctor", "dashmap", + "eyeball", "futures", "futures-core", - "futures-signals", "futures-util", "http", "matrix-sdk-common", diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index ab7f75a27..64c4f84dc 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -29,8 +29,8 @@ testing = ["dep:http"] async-stream = { workspace = true } async-trait = { workspace = true } dashmap = { workspace = true } +eyeball = { workspace = true } futures-core = "0.3.21" -futures-signals = { version = "0.3.30", default-features = false } futures-util = { version = "0.3.21", default-features = false } http = { workspace = true, optional = true } matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" } diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index c76617e72..f3194c7e8 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -17,11 +17,12 @@ use std::{ borrow::Borrow, collections::{BTreeMap, BTreeSet}, fmt, + sync::RwLockReadGuard as StdRwLockReadGuard, }; #[cfg(feature = "e2e-encryption")] use std::{ops::Deref, sync::Arc}; -use futures_signals::signal::ReadOnlyMutable; +use eyeball::Observable; use matrix_sdk_common::{instant::Instant, locks::RwLock}; #[cfg(feature = "e2e-encryption")] use matrix_sdk_crypto::{ @@ -136,7 +137,7 @@ impl BaseClient { /// If the client is currently logged in, this will return a /// [`SessionTokens`] object which contains the access token and optional /// refresh token. Otherwise it returns `None`. - pub fn session_tokens(&self) -> ReadOnlyMutable> { + pub fn session_tokens(&self) -> StdRwLockReadGuard<'_, Observable>> { self.store.session_tokens() } diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index 210a731be..83a72da4a 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -28,10 +28,10 @@ use std::{ pin::Pin, result::Result as StdResult, str::Utf8Error, - sync::Arc, + sync::{Arc, RwLock as StdRwLock, RwLockReadGuard as StdRwLockReadGuard}, }; -use futures_signals::signal::{Mutable, ReadOnlyMutable}; +use eyeball::Observable; use once_cell::sync::OnceCell; #[cfg(any(test, feature = "testing"))] @@ -505,7 +505,7 @@ where pub(crate) struct Store { pub(super) inner: Arc, session_meta: Arc>, - pub(super) session_tokens: Mutable>, + pub(super) session_tokens: Arc>>>, /// The current sync token that should be used for the next sync call. pub(super) sync_token: Arc>>, rooms: Arc>, @@ -567,22 +567,22 @@ impl Store { self.session_meta.get() } - /// The current [`SessionTokens`] containing our access token and optional - /// refresh token. - pub fn session_tokens(&self) -> ReadOnlyMutable> { - self.session_tokens.read_only() + /// The [`SessionTokens`] containing our access token and optional refresh + /// token. + pub fn session_tokens(&self) -> StdRwLockReadGuard<'_, Observable>> { + self.session_tokens.read().unwrap() } /// Set the current [`SessionTokens`]. pub fn set_session_tokens(&self, tokens: SessionTokens) { - self.session_tokens.set(Some(tokens)); + Observable::set(&mut self.session_tokens.write().unwrap(), Some(tokens)); } /// The current [`Session`] containing our user id, device ID, access /// token and optional refresh token. pub fn session(&self) -> Option { let meta = self.session_meta.get()?; - let tokens = self.session_tokens.get_cloned()?; + let tokens = self.session_tokens().clone()?; Some(Session::from_parts(meta.to_owned(), tokens)) } diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 825003073..7bdeccaf3 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -69,6 +69,7 @@ bytesize = "1.1" chrono = { version = "0.4.23", optional = true } dashmap = { workspace = true } event-listener = "2.5.2" +eyeball = { workspace = true } eyeball-im = { workspace = true } eyre = { version = "0.6.8", optional = true } futures-core = "0.3.21" diff --git a/crates/matrix-sdk/src/client/builder.rs b/crates/matrix-sdk/src/client/builder.rs index bea2d937f..bf4790688 100644 --- a/crates/matrix-sdk/src/client/builder.rs +++ b/crates/matrix-sdk/src/client/builder.rs @@ -318,7 +318,7 @@ impl ClientBuilder { /// is encountered, it means that the user needs to be logged in again. /// /// * The access token and refresh token need to be watched for changes, - /// using [`Client::session_tokens_signal()`] for example, to be able to + /// using [`Client::session_tokens_stream()`] for example, to be able to /// [restore the session] later. /// /// [refreshing access tokens]: https://spec.matrix.org/v1.3/client-server-api/#refreshing-access-tokens diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 2f4b758e9..5628da653 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -24,8 +24,9 @@ use std::{ #[cfg(target_arch = "wasm32")] use async_once_cell::OnceCell; use dashmap::DashMap; -use futures_core::stream::Stream; -use futures_signals::signal::Signal; +use eyeball::Observable; +use futures_core::Stream; +use futures_util::StreamExt; use matrix_sdk_base::{ BaseClient, RoomType, SendOutsideWasm, Session, SessionMeta, SessionTokens, StateStore, SyncOutsideWasm, @@ -353,7 +354,7 @@ impl Client { /// /// [refreshing access tokens]: https://spec.matrix.org/v1.3/client-server-api/#refreshing-access-tokens pub fn session_tokens(&self) -> Option { - self.base_client().session_tokens().get_cloned() + self.base_client().session_tokens().clone() } /// Get the current access token for this session. @@ -381,7 +382,7 @@ impl Client { self.session_tokens().and_then(|tokens| tokens.refresh_token) } - /// [`Signal`] to get notified when the current access token and optional + /// [`Stream`] to get notified when the current access token and optional /// refresh token for this session change. /// /// This can be used with [`Client::session()`] to persist the [`Session`] @@ -393,7 +394,7 @@ impl Client { /// # Example /// /// ```no_run - /// use futures_signals::signal::SignalExt; + /// use futures_util::StreamExt; /// use matrix_sdk::Client; /// # use matrix_sdk::Session; /// # use futures::executor::block_on; @@ -417,7 +418,7 @@ impl Client { /// persist_session(client.session()); /// /// // Handle when at least one of the tokens changed. - /// let future = client.session_tokens_changed_signal().for_each(move |_| { + /// let future = client.session_tokens_changed_stream().for_each(move |_| { /// let client = client.clone(); /// async move { /// persist_session(client.session()); @@ -430,15 +431,12 @@ impl Client { /// ``` /// /// [refreshing access tokens]: https://spec.matrix.org/v1.3/client-server-api/#refreshing-access-tokens - pub fn session_tokens_changed_signal(&self) -> impl Signal { - self.base_client().session_tokens().signal_ref(|_| ()) + pub fn session_tokens_changed_stream(&self) -> impl Stream { + self.session_tokens_stream().map(|_| ()) } - /// Get the current access token and optional refresh token for this - /// session as a [`Signal`]. - /// - /// This can be used to watch changes of the tokens by calling methods like - /// `for_each()` or `to_stream()`. + /// Get changes to the access token and optional refresh token for this + /// session as a [`Stream`]. /// /// The value will be `None` if the client has not been logged in. /// @@ -449,7 +447,6 @@ impl Client { /// /// ```no_run /// use futures::StreamExt; - /// use futures_signals::signal::SignalExt; /// use matrix_sdk::Client; /// # use matrix_sdk::Session; /// # use futures::executor::block_on; @@ -474,7 +471,7 @@ impl Client { /// persist_session(&session); /// /// // Handle when at least one of the tokens changed. - /// let mut tokens_stream = client.session_tokens_signal().to_stream(); + /// let mut tokens_stream = client.session_tokens_stream(); /// loop { /// if let Some(tokens) = tokens_stream.next().await.flatten() { /// session.access_token = tokens.access_token; @@ -491,8 +488,8 @@ impl Client { /// ``` /// /// [refreshing access tokens]: https://spec.matrix.org/v1.3/client-server-api/#refreshing-access-tokens - pub fn session_tokens_signal(&self) -> impl Signal> { - self.base_client().session_tokens().signal_cloned() + pub fn session_tokens_stream(&self) -> impl Stream> { + Observable::subscribe(&self.base_client().session_tokens()) } /// Get the whole session info of this client. diff --git a/crates/matrix-sdk/tests/integration/refresh_token.rs b/crates/matrix-sdk/tests/integration/refresh_token.rs index 5b1e6263f..a83a29bc7 100644 --- a/crates/matrix-sdk/tests/integration/refresh_token.rs +++ b/crates/matrix-sdk/tests/integration/refresh_token.rs @@ -1,11 +1,7 @@ use std::time::Duration; use assert_matches::assert_matches; -use futures::{ - channel::{mpsc, oneshot}, - StreamExt, -}; -use futures_signals::signal::SignalExt; +use futures::{channel::mpsc, StreamExt}; use matrix_sdk::{config::RequestConfig, executor::spawn, HttpError, RefreshTokenError, Session}; use matrix_sdk_test::{async_test, test_json}; use ruma::{ @@ -248,27 +244,16 @@ async fn refresh_token_handled_success() { }; client.restore_session(session).await.unwrap(); - let mut tokens_stream = client.session_tokens_signal().to_stream(); - let (tokens_sender, tokens_receiver) = oneshot::channel::<()>(); - spawn(async move { - let tokens = tokens_stream.next().await.flatten().unwrap(); - assert_eq!(tokens.access_token, "1234"); - assert_eq!(tokens.refresh_token.as_deref(), Some("abcd")); - + let mut tokens_stream = client.session_tokens_stream(); + let tokens_join_handle = spawn(async move { let tokens = tokens_stream.next().await.flatten().unwrap(); assert_eq!(tokens.access_token, "5678"); assert_eq!(tokens.refresh_token.as_deref(), Some("abcd")); - - tokens_sender.send(()).unwrap(); }); - let mut tokens_changed_stream = client.session_tokens_changed_signal().to_stream(); - let (changed_sender, changed_receiver) = oneshot::channel::<()>(); - spawn(async move { + let mut tokens_changed_stream = client.session_tokens_changed_stream(); + let changed_join_handle = spawn(async move { tokens_changed_stream.next().await.unwrap(); - tokens_changed_stream.next().await.unwrap(); - - changed_sender.send(()).unwrap(); }); Mock::given(method("POST")) @@ -300,8 +285,8 @@ async fn refresh_token_handled_success() { .await; client.whoami().await.unwrap(); - tokens_receiver.await.unwrap(); - changed_receiver.await.unwrap(); + tokens_join_handle.await.unwrap(); + changed_join_handle.await.unwrap(); } #[async_test] From 6c92bf87454593f1476e439d4189c739f4d2083d Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 22 Feb 2023 18:50:30 +0100 Subject: [PATCH 028/166] refactor(sdk): Remove unused dependency --- Cargo.lock | 1 - crates/matrix-sdk/Cargo.toml | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96a5a382e..3e8b47f84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2602,7 +2602,6 @@ dependencies = [ "tempfile", "thiserror", "tokio", - "tokio-stream", "tower", "tracing", "tracing-subscriber", diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 7bdeccaf3..72fb373f6 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -38,7 +38,7 @@ markdown = ["ruma/markdown"] native-tls = ["reqwest/native-tls"] rustls-tls = ["reqwest/rustls-tls"] socks = ["reqwest/socks"] -sso-login = ["dep:hyper", "dep:rand", "dep:tokio-stream", "dep:tower"] +sso-login = ["dep:hyper", "dep:rand", "dep:tower"] appservice = ["ruma/appservice-api-s"] image-proc = ["dep:image"] image-rayon = ["image-proc", "image?/jpeg_rayon"] @@ -92,7 +92,6 @@ serde = { workspace = true } serde_html_form = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } -tokio-stream = { version = "0.1.8", features = ["net"], optional = true } tower = { version = "0.4.13", features = ["make"], optional = true } tracing = { workspace = true, features = ["attributes"] } url = "2.2.2" From 83dcb0eb481d242444dd0b5cab9840a512a83fee Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 23 Feb 2023 11:53:32 +0100 Subject: [PATCH 029/166] xtask kotlin : fix formatting --- xtask/src/kotlin.rs | 55 ++++++++++++++++++++++++++++++--------------- xtask/src/main.rs | 7 +++--- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/xtask/src/kotlin.rs b/xtask/src/kotlin.rs index f00ebbbde..d9055da34 100644 --- a/xtask/src/kotlin.rs +++ b/xtask/src/kotlin.rs @@ -22,13 +22,13 @@ enum Package { impl Package { fn values(self) -> PackageValues { match self { - Package::CryptoSDK => PackageValues { + Package::CryptoSDK => PackageValues { name: "matrix-sdk-crypto-ffi", - udl_path : "bindings/matrix-sdk-crypto-ffi/src/olm.udl", + udl_path: "bindings/matrix-sdk-crypto-ffi/src/olm.udl", }, - Package::FullSDK => PackageValues { + Package::FullSDK => PackageValues { name: "matrix-sdk-ffi", - udl_path : "bindings/matrix-sdk-ffi/src/api.udl", + udl_path: "bindings/matrix-sdk-ffi/src/api.udl", }, } } @@ -58,7 +58,7 @@ enum KotlinCommand { #[clap(long)] only_target: Option, - /// Move the generated files into the given src direct + /// Move the generated files into the given src direct #[clap(long)] src_dir: PathBuf, }, @@ -69,7 +69,13 @@ impl KotlinArgs { let _p = pushd(workspace::root_path()?)?; match self.cmd { - KotlinCommand::BuildAndroidLibrary { release, profile, src_dir, only_target,package } => { + KotlinCommand::BuildAndroidLibrary { + release, + profile, + src_dir, + only_target, + package, + } => { let profile = profile.as_deref().unwrap_or(if release { "release" } else { "dev" }); build_android_library(profile, only_target, src_dir, package) } @@ -83,9 +89,8 @@ fn build_android_library( src_dir: PathBuf, package: Package, ) -> Result<()> { - let root_dir = workspace::root_path()?; - + let package_values = package.values(); let package_name = package_values.name; let udl_path = root_dir.join(package_values.udl_path); @@ -98,16 +103,21 @@ fn build_android_library( let uniffi_lib_path = if let Some(target) = only_target { println!("-- Building for {target} [1/1]"); - build_for_android_target(target.as_str(), profile,jni_libs_dir_str, package_name)? + build_for_android_target(target.as_str(), profile, jni_libs_dir_str, package_name)? } else { println!("-- Building for x86_64-linux-android[1/4]"); - build_for_android_target("x86_64-linux-android", profile,jni_libs_dir_str, package_name)?; + build_for_android_target("x86_64-linux-android", profile, jni_libs_dir_str, package_name)?; println!("-- Building for aarch64-linux-android[2/4]"); - build_for_android_target("aarch64-linux-android", profile,jni_libs_dir_str, package_name)?; + build_for_android_target("aarch64-linux-android", profile, jni_libs_dir_str, package_name)?; println!("-- Building for armv7-linux-androideabi[3/4]"); - build_for_android_target("armv7-linux-androideabi", profile,jni_libs_dir_str, package_name)?; + build_for_android_target( + "armv7-linux-androideabi", + profile, + jni_libs_dir_str, + package_name, + )?; println!("-- Building for i686-linux-android[4/4]"); - build_for_android_target("i686-linux-android", profile,jni_libs_dir_str, package_name)? + build_for_android_target("i686-linux-android", profile, jni_libs_dir_str, package_name)? }; println!("-- Generate uniffi files"); @@ -117,7 +127,11 @@ fn build_android_library( Ok(()) } -fn generate_uniffi_bindings(udl_path: &Path, library_path: &Path, ffi_generated_dir: &Path) -> Result<()> { +fn generate_uniffi_bindings( + udl_path: &Path, + library_path: &Path, + ffi_generated_dir: &Path, +) -> Result<()> { println!("-- library_path = {}", library_path.to_string_lossy()); let udl_file = camino::Utf8Path::from_path(udl_path).unwrap(); let out_dir_overwrite = camino::Utf8Path::from_path(ffi_generated_dir).unwrap(); @@ -134,14 +148,19 @@ fn generate_uniffi_bindings(udl_path: &Path, library_path: &Path, ffi_generated_ Ok(()) } -fn build_for_android_target(target: &str, profile: &str, dest_dir: &str, package_name: &str) -> Result { - - cmd!("cargo ndk --target {target} -o {dest_dir} build --profile {profile} -p {package_name}").run()?; +fn build_for_android_target( + target: &str, + profile: &str, + dest_dir: &str, + package_name: &str, +) -> Result { + cmd!("cargo ndk --target {target} -o {dest_dir} build --profile {profile} -p {package_name}") + .run()?; // The builtin dev profile has its files stored under target/debug, all // other targets have matching directory names let profile_dir_name = if profile == "dev" { "debug" } else { profile }; - let package_camel = package_name.replace("-","_"); + let package_camel = package_name.replace("-", "_"); let lib_name = format!("lib{package_camel}.so"); Ok(workspace::target_path()?.join(target).join(profile_dir_name).join(lib_name)) } diff --git a/xtask/src/main.rs b/xtask/src/main.rs index abb7b6e25..4e1706e4a 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,14 +1,14 @@ mod ci; mod fixup; -mod swift; mod kotlin; +mod swift; mod workspace; use ci::CiArgs; use clap::{Parser, Subcommand}; use fixup::FixupArgs; -use swift::SwiftArgs; use kotlin::KotlinArgs; +use swift::SwiftArgs; use xshell::cmd; type Result> = std::result::Result; @@ -32,8 +32,7 @@ enum Command { open: bool, }, Swift(SwiftArgs), - Kotlin(KotlinArgs) - + Kotlin(KotlinArgs), } fn main() -> Result<()> { From 01e3a31a119d64305fb3ce7be09c5aee46467449 Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Thu, 23 Feb 2023 12:12:53 +0000 Subject: [PATCH 030/166] fix(bindings): Verify entered homeserver before proxy availability The reported errors weren't that helpful to the user. Additionally attempt discovery when a URL is entered before falling back to the URL directly. --- .../src/authentication_service.rs | 55 +++++++++++++------ crates/matrix-sdk/src/lib.rs | 7 +++ 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/authentication_service.rs b/bindings/matrix-sdk-ffi/src/authentication_service.rs index 8467d4d32..bce8f761e 100644 --- a/bindings/matrix-sdk-ffi/src/authentication_service.rs +++ b/bindings/matrix-sdk-ffi/src/authentication_service.rs @@ -2,7 +2,7 @@ use std::sync::{Arc, RwLock}; use futures_util::future::join3; use matrix_sdk::{ - ruma::{OwnedDeviceId, UserId}, + ruma::{IdParseError, OwnedDeviceId, UserId}, Session, }; use zeroize::Zeroize; @@ -28,6 +28,8 @@ impl Drop for AuthenticationService { pub enum AuthenticationError { #[error("A successful call to configure_homeserver must be made first.")] ClientMissing, + #[error("{message}")] + InvalidServerName { message: String }, #[error("The homeserver doesn't provide a trusted a sliding sync proxy in its well-known configuration.")] SlidingSyncNotAvailable, #[error("Login was successful but is missing a valid Session to configure the file store.")] @@ -42,6 +44,12 @@ impl From for AuthenticationError { } } +impl From for AuthenticationError { + fn from(e: IdParseError) -> AuthenticationError { + AuthenticationError::InvalidServerName { message: e.to_string() } + } +} + #[derive(uniffi::Object)] pub struct HomeserverLoginDetails { url: String, @@ -112,32 +120,47 @@ impl AuthenticationService { /// Updates the service to authenticate with the homeserver for the /// specified address. - pub fn configure_homeserver(&self, server_name: String) -> Result<(), AuthenticationError> { + pub fn configure_homeserver( + &self, + server_name_or_homeserver_url: String, + ) -> Result<(), AuthenticationError> { let mut builder = Arc::new(ClientBuilder::new()).base_path(self.base_path.clone()); - if server_name.starts_with("http://") || server_name.starts_with("https://") { - builder = builder.homeserver_url(server_name) - } else { - builder = builder.server_name(server_name); - } + // Remove any URL scheme from the name to attempt discovery first. + let server_name = matrix_sdk::sanitize_server_name(&server_name_or_homeserver_url) + .map_err(AuthenticationError::from)?; - let client = builder.build().map_err(AuthenticationError::from)?; + builder = builder.server_name(server_name.to_string()); - // Make sure there's a sliding sync proxy available one way or another. + let client = builder + .build() + .or_else(|e| { + if !server_name_or_homeserver_url.starts_with("http://") + && !server_name_or_homeserver_url.starts_with("http://") + { + return Err(e); + } + // When discovery fails, fallback to the homeserver URL if supplied. + let mut builder = Arc::new(ClientBuilder::new()).base_path(self.base_path.clone()); + builder = builder.homeserver_url(server_name_or_homeserver_url); + builder.build() + }) + .map_err(AuthenticationError::from)?; + + let details = RUNTIME.block_on(async { self.details_from_client(&client).await })?; + + // Now we've verified that it's a valid homeserver, make sure + // there's a sliding sync proxy available one way or another. if self.custom_sliding_sync_proxy.read().unwrap().is_none() && client.discovered_sliding_sync_proxy().is_none() { return Err(AuthenticationError::SlidingSyncNotAvailable); } - RUNTIME.block_on(async move { - let details = Arc::new(self.details_from_client(&client).await?); + *self.client.write().unwrap() = Some(client); + *self.homeserver_details.write().unwrap() = Some(Arc::new(details)); - *self.client.write().unwrap() = Some(client); - *self.homeserver_details.write().unwrap() = Some(details); - - Ok(()) - }) + Ok(()) } /// Performs a password login using the current homeserver. diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index f69df0506..ae905703c 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -55,6 +55,7 @@ pub use error::ImageError; pub use error::{Error, HttpError, HttpResult, RefreshTokenError, Result, RumaApiError}; pub use http_client::HttpSend; pub use media::Media; +pub use ruma::{IdParseError, OwnedServerName, ServerName}; #[cfg(feature = "experimental-sliding-sync")] pub use sliding_sync::{ RoomListEntry, SlidingSync, SlidingSyncBuilder, SlidingSyncMode, SlidingSyncRoom, @@ -73,3 +74,9 @@ fn init_logging() { .with(tracing_subscriber::fmt::layer().with_test_writer()) .init(); } + +/// Creates a server name from a user supplied string. The string is first +/// sanitized by removing the http(s) scheme before being parsed. +pub fn sanitize_server_name(s: &str) -> Result { + ServerName::parse(s.trim_start_matches("http://").trim_start_matches("https://")) +} From d0c8ec7a22cf3f732983edb735c5b1f560bdd070 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 23 Feb 2023 13:47:23 +0100 Subject: [PATCH 031/166] refactor: Replace remaining usage of futures-signal with eyeball(-im) --- Cargo.lock | 29 +-- bindings/matrix-sdk-ffi/Cargo.toml | 2 +- bindings/matrix-sdk-ffi/src/api.udl | 25 +- bindings/matrix-sdk-ffi/src/sliding_sync.rs | 78 ++++--- crates/matrix-sdk/Cargo.toml | 4 +- .../matrix-sdk/src/room/timeline/builder.rs | 7 +- crates/matrix-sdk/src/room/timeline/inner.rs | 2 +- .../src/room/timeline/tests/basic.rs | 5 +- crates/matrix-sdk/src/sliding_sync/builder.rs | 6 +- crates/matrix-sdk/src/sliding_sync/mod.rs | 23 +- crates/matrix-sdk/src/sliding_sync/room.rs | 81 ++++--- crates/matrix-sdk/src/sliding_sync/view.rs | 220 ++++++++++-------- examples/timeline/Cargo.toml | 1 - labs/jack-in/Cargo.toml | 2 +- labs/jack-in/src/app/model.rs | 4 +- labs/jack-in/src/client/mod.rs | 2 +- labs/jack-in/src/client/state.rs | 59 +++-- labs/jack-in/src/components/details.rs | 5 +- labs/jack-in/src/main.rs | 4 +- .../sliding-sync-integration-test/Cargo.toml | 1 + .../sliding-sync-integration-test/src/lib.rs | 7 +- 21 files changed, 296 insertions(+), 271 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3e8b47f84..c29b9031f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1247,12 +1247,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "discard" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" - [[package]] name = "displaydoc" version = "0.2.3" @@ -1457,7 +1451,6 @@ dependencies = [ "anyhow", "clap 4.1.6", "futures", - "futures-signals", "matrix-sdk", "tokio", "tracing-subscriber", @@ -1684,21 +1677,6 @@ dependencies = [ "syn", ] -[[package]] -name = "futures-signals" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3acc659ba666cff13fdf65242d16428f2f11935b688f82e4024ad39667a5132" -dependencies = [ - "discard", - "futures-channel", - "futures-core", - "futures-util", - "log", - "pin-project", - "serde", -] - [[package]] name = "futures-sink" version = "0.3.26" @@ -2095,6 +2073,7 @@ dependencies = [ "bitmaps", "rand_core 0.6.4", "rand_xoshiro", + "serde", "sized-chunks", "typenum", "version_check", @@ -2271,10 +2250,10 @@ dependencies = [ "chrono", "clap 4.1.6", "dialoguer", + "eyeball", "eyeball-im", "eyre", "futures", - "futures-signals", "log4rs", "matrix-sdk", "matrix-sdk-common", @@ -2576,7 +2555,6 @@ dependencies = [ "eyre", "futures", "futures-core", - "futures-signals", "futures-util", "getrandom 0.2.8", "gloo-timers", @@ -2803,9 +2781,9 @@ dependencies = [ "anyhow", "base64 0.21.0", "extension-trait", + "eyeball", "eyeball-im", "futures-core", - "futures-signals", "futures-util", "log-panics", "matrix-sdk", @@ -4679,6 +4657,7 @@ version = "0.1.0" dependencies = [ "anyhow", "assert_matches", + "eyeball", "eyeball-im", "futures", "matrix-sdk", diff --git a/bindings/matrix-sdk-ffi/Cargo.toml b/bindings/matrix-sdk-ffi/Cargo.toml index 0eb88cb54..5bb8133bf 100644 --- a/bindings/matrix-sdk-ffi/Cargo.toml +++ b/bindings/matrix-sdk-ffi/Cargo.toml @@ -18,10 +18,10 @@ uniffi = { workspace = true, features = ["build"] } [dependencies] anyhow = { workspace = true } base64 = "0.21" +eyeball = { workspace = true } eyeball-im = { workspace = true } extension-trait = "1.0.1" futures-core = "0.3.17" -futures-signals = { version = "0.3.30", default-features = false } futures-util = { version = "0.3.17", default-features = false } mime = "0.3.16" # FIXME: we currently can't feature flag anything in the api.udl, therefore we must enforce experimental-sliding-sync being exposed here.. diff --git a/bindings/matrix-sdk-ffi/src/api.udl b/bindings/matrix-sdk-ffi/src/api.udl index 5610266bb..602d6f8e6 100644 --- a/bindings/matrix-sdk-ffi/src/api.udl +++ b/bindings/matrix-sdk-ffi/src/api.udl @@ -66,23 +66,16 @@ interface RoomListEntry { [Enum] interface SlidingSyncViewRoomsListDiff { - Replace(sequence values); - InsertAt( - u32 index, - RoomListEntry value - ); - UpdateAt( - u32 index, - RoomListEntry value - ); - RemoveAt(u32 index); - Move( - u32 old_index, - u32 new_index - ); - Push(RoomListEntry value); - Pop(); + Append(sequence values); + Insert(u32 index, RoomListEntry value); + Set(u32 index, RoomListEntry value); + Remove(u32 index); + PushBack(RoomListEntry value); + PushFront(RoomListEntry value); + PopBack(); + PopFront(); Clear(); + Reset(sequence values); }; callback interface SlidingSyncViewRoomListObserver { diff --git a/bindings/matrix-sdk-ffi/src/sliding_sync.rs b/bindings/matrix-sdk-ffi/src/sliding_sync.rs index 0b79f83a7..e0367b027 100644 --- a/bindings/matrix-sdk-ffi/src/sliding_sync.rs +++ b/bindings/matrix-sdk-ffi/src/sliding_sync.rs @@ -4,7 +4,8 @@ use std::sync::{ }; use anyhow::Context; -use futures_signals::{signal::SignalExt, signal_vec::VecDiff}; +use eyeball::Observable; +use eyeball_im::VectorDiff; use futures_util::{future::join, pin_mut, StreamExt}; use matrix_sdk::ruma::{ api::client::sync::sync_events::{ @@ -282,42 +283,48 @@ impl From for UpdateSummary { } pub enum SlidingSyncViewRoomsListDiff { - Replace { values: Vec }, - InsertAt { index: u32, value: RoomListEntry }, - UpdateAt { index: u32, value: RoomListEntry }, - RemoveAt { index: u32 }, - Move { old_index: u32, new_index: u32 }, - Push { value: RoomListEntry }, - Pop, // removes the last item - Clear, // clears the list + Append { values: Vec }, + Insert { index: u32, value: RoomListEntry }, + Set { index: u32, value: RoomListEntry }, + Remove { index: u32 }, + PushBack { value: RoomListEntry }, + PushFront { value: RoomListEntry }, + PopBack, + PopFront, + Clear, + Reset { values: Vec }, } -impl From> for SlidingSyncViewRoomsListDiff { - fn from(other: VecDiff) -> Self { +impl From> for SlidingSyncViewRoomsListDiff { + fn from(other: VectorDiff) -> Self { match other { - VecDiff::Replace { values } => SlidingSyncViewRoomsListDiff::Replace { + VectorDiff::Append { values } => SlidingSyncViewRoomsListDiff::Append { values: values.into_iter().map(|e| (&e).into()).collect(), }, - VecDiff::InsertAt { index, value } => SlidingSyncViewRoomsListDiff::InsertAt { - index: index as u32, - value: (&value).into(), - }, - VecDiff::UpdateAt { index, value } => SlidingSyncViewRoomsListDiff::UpdateAt { - index: index as u32, - value: (&value).into(), - }, - VecDiff::RemoveAt { index } => { - SlidingSyncViewRoomsListDiff::RemoveAt { index: index as u32 } + VectorDiff::Insert { index, value } => { + SlidingSyncViewRoomsListDiff::Insert { index: index as u32, value: (&value).into() } } - VecDiff::Move { old_index, new_index } => SlidingSyncViewRoomsListDiff::Move { - old_index: old_index as u32, - new_index: new_index as u32, - }, - VecDiff::Push { value } => { - SlidingSyncViewRoomsListDiff::Push { value: (&value).into() } + VectorDiff::Set { index, value } => { + SlidingSyncViewRoomsListDiff::Set { index: index as u32, value: (&value).into() } + } + VectorDiff::Remove { index } => { + SlidingSyncViewRoomsListDiff::Remove { index: index as u32 } + } + VectorDiff::PushBack { value } => { + SlidingSyncViewRoomsListDiff::PushBack { value: (&value).into() } + } + VectorDiff::PushFront { value } => { + SlidingSyncViewRoomsListDiff::PushFront { value: (&value).into() } + } + VectorDiff::PopBack => SlidingSyncViewRoomsListDiff::PopBack, + VectorDiff::PopFront => SlidingSyncViewRoomsListDiff::PopFront, + VectorDiff::Clear => SlidingSyncViewRoomsListDiff::Clear, + VectorDiff::Reset { values } => { + warn!("Room list subscriber lagged behind and was reset"); + SlidingSyncViewRoomsListDiff::Reset { + values: values.into_iter().map(|e| (&e).into()).collect(), + } } - VecDiff::Pop {} => SlidingSyncViewRoomsListDiff::Pop, - VecDiff::Clear {} => SlidingSyncViewRoomsListDiff::Clear, } } } @@ -549,7 +556,8 @@ impl SlidingSyncView { &self, observer: Box, ) -> Arc { - let mut rooms_updated = self.inner.rooms_updated_broadcaster.signal_cloned().to_stream(); + let mut rooms_updated = + Observable::subscribe(&self.inner.rooms_updated_broadcast.read().unwrap()); Arc::new(TaskHandle::with_handle(RUNTIME.spawn(async move { loop { if rooms_updated.next().await.is_some() { @@ -610,17 +618,19 @@ impl SlidingSyncView { /// The current timeline limit pub fn get_timeline_limit(&self) -> Option { - self.inner.timeline_limit.get_cloned().map(|limit| u32::try_from(limit).unwrap_or_default()) + (**self.inner.timeline_limit.read().unwrap()) + .map(|limit| u32::try_from(limit).unwrap_or_default()) } /// The current timeline limit pub fn set_timeline_limit(&self, value: u32) { - self.inner.timeline_limit.set(Some(UInt::try_from(value).unwrap())) + let value = Some(UInt::try_from(value).unwrap()); + Observable::set(&mut self.inner.timeline_limit.write().unwrap(), value); } /// Unset the current timeline limit pub fn unset_timeline_limit(&self) { - self.inner.timeline_limit.set(None) + Observable::set(&mut self.inner.timeline_limit.write().unwrap(), None); } } diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 72fb373f6..c83957e8d 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -73,10 +73,9 @@ eyeball = { workspace = true } eyeball-im = { workspace = true } eyre = { version = "0.6.8", optional = true } futures-core = "0.3.21" -futures-signals = { version = "0.3.30", default-features = false } futures-util = { version = "0.3.21", default-features = false } http = { workspace = true } -im = "15.1.0" +im = { version = "15.1.0", features = ["serde"] } 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 } @@ -120,6 +119,7 @@ optional = true [target.'cfg(target_arch = "wasm32")'.dependencies] async-once-cell = "0.4.2" gloo-timers = { version = "0.2.6", features = ["futures"] } +tokio = { version = "1.24.2", default-features = false, features = ["sync"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] backoff = { version = "0.4.0", features = ["tokio"] } diff --git a/crates/matrix-sdk/src/room/timeline/builder.rs b/crates/matrix-sdk/src/room/timeline/builder.rs index f0e0be4f9..5ebcd858d 100644 --- a/crates/matrix-sdk/src/room/timeline/builder.rs +++ b/crates/matrix-sdk/src/room/timeline/builder.rs @@ -14,6 +14,7 @@ use std::sync::Arc; +use im::Vector; use matrix_sdk_base::{ deserialized_responses::{EncryptionInfo, SyncTimelineEvent}, locks::Mutex, @@ -35,7 +36,7 @@ use crate::room; pub(crate) struct TimelineBuilder { room: room::Common, prev_token: Option, - events: Vec, + events: Vector, track_fully_read: bool, } @@ -44,7 +45,7 @@ impl TimelineBuilder { Self { room: room.clone(), prev_token: None, - events: Vec::default(), + events: Vector::new(), track_fully_read: false, } } @@ -54,7 +55,7 @@ impl TimelineBuilder { pub(crate) fn events( mut self, prev_token: Option, - events: Vec, + events: Vector, ) -> Self { self.prev_token = prev_token; self.events = events; diff --git a/crates/matrix-sdk/src/room/timeline/inner.rs b/crates/matrix-sdk/src/room/timeline/inner.rs index 4bcf8a119..c966a2f76 100644 --- a/crates/matrix-sdk/src/room/timeline/inner.rs +++ b/crates/matrix-sdk/src/room/timeline/inner.rs @@ -108,7 +108,7 @@ impl TimelineInner

    { (items, stream) } - pub(super) async fn add_initial_events(&mut self, events: Vec) { + pub(super) async fn add_initial_events(&mut self, events: Vector) { if events.is_empty() { return; } diff --git a/crates/matrix-sdk/src/room/timeline/tests/basic.rs b/crates/matrix-sdk/src/room/timeline/tests/basic.rs index 8304c21b3..d68f6cea2 100644 --- a/crates/matrix-sdk/src/room/timeline/tests/basic.rs +++ b/crates/matrix-sdk/src/room/timeline/tests/basic.rs @@ -1,6 +1,7 @@ use assert_matches::assert_matches; use eyeball_im::VectorDiff; use futures_util::StreamExt; +use im::vector; use matrix_sdk_base::deserialized_responses::SyncTimelineEvent; use matrix_sdk_test::async_test; use ruma::{ @@ -39,7 +40,7 @@ async fn initial_events() { timeline .inner - .add_initial_events(vec![ + .add_initial_events(vector![ sync_timeline_event( timeline.make_message_event(*ALICE, RoomMessageEventContent::text_plain("A")), ), @@ -273,7 +274,7 @@ async fn dedup_initial() { timeline.make_message_event(*BOB, RoomMessageEventContent::text_plain("B")), ); - timeline.inner.add_initial_events(vec![event_a.clone(), event_b, event_a]).await; + timeline.inner.add_initial_events(vector![event_a.clone(), event_b, event_a]).await; let timeline_items = timeline.inner.items().await; assert_eq!(timeline_items.len(), 3); diff --git a/crates/matrix-sdk/src/sliding_sync/builder.rs b/crates/matrix-sdk/src/sliding_sync/builder.rs index d823f6ff2..39a5af3a0 100644 --- a/crates/matrix-sdk/src/sliding_sync/builder.rs +++ b/crates/matrix-sdk/src/sliding_sync/builder.rs @@ -4,7 +4,7 @@ use std::{ sync::{Arc, Mutex, RwLock as StdRwLock}, }; -use futures_signals::signal::Mutable; +use eyeball::Observable; use ruma::{ api::client::sync::sync_events::v4::{ self, AccountDataConfig, E2EEConfig, ExtensionsConfig, ReceiptConfig, ToDeviceConfig, @@ -300,8 +300,8 @@ impl SlidingSyncBuilder { sent_extensions: Mutex::new(None).into(), failure_count: Default::default(), - pos: Mutable::new(None), - delta_token: Mutable::new(delta_token_inner), + pos: Arc::new(StdRwLock::new(Observable::new(None))), + delta_token: Arc::new(StdRwLock::new(Observable::new(delta_token_inner))), subscriptions: Arc::new(StdRwLock::new(self.subscriptions)), unsubscribe: Default::default(), }) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 3d0621b27..a60361367 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -630,8 +630,8 @@ use std::{ pub use builder::*; pub use client::*; +use eyeball::Observable; use futures_core::stream::Stream; -use futures_signals::signal::Mutable; pub use room::*; use ruma::{ api::client::{ @@ -767,9 +767,9 @@ pub struct SlidingSync { storage_key: Option, /// The `pos` marker. - pos: Mutable>, + pos: Arc>>>, - delta_token: Mutable>, + delta_token: Arc>>>, /// The views of this sliding sync instance views: Arc>>, @@ -803,7 +803,7 @@ struct FrozenSlidingSync { impl From<&SlidingSync> for FrozenSlidingSync { fn from(v: &SlidingSync) -> Self { FrozenSlidingSync { - delta_token: v.delta_token.get_cloned(), + delta_token: v.delta_token.read().unwrap().clone(), to_device_since: v .extensions .lock() @@ -984,8 +984,9 @@ impl SlidingSync { ) -> Result { let mut processed = self.client.process_sliding_sync(resp.clone()).await?; debug!("main client processed."); - self.pos.replace(Some(resp.pos)); - self.delta_token.replace(resp.delta_token); + Observable::set(&mut self.pos.write().unwrap(), Some(resp.pos)); + Observable::set(&mut self.delta_token.write().unwrap(), resp.delta_token); + let update = { let mut rooms = Vec::new(); let mut rooms_map = self.rooms.write().unwrap(); @@ -1071,8 +1072,8 @@ impl SlidingSync { return Ok(None); } - let pos = self.pos.get_cloned(); - let delta_token = self.delta_token.get_cloned(); + let pos = self.pos.read().unwrap().clone(); + let delta_token = self.delta_token.read().unwrap().clone(); let room_subscriptions = self.subscriptions.read().unwrap().clone(); let unsubscribe_rooms = mem::take(&mut *self.unsubscribe.write().unwrap()); let timeout = Duration::from_secs(30); @@ -1174,7 +1175,7 @@ impl SlidingSync { sync_span.in_scope(|| { warn!("Session expired. Restarting sliding sync."); - *self.pos.lock_mut() = None; + Observable::set(&mut self.pos.write().unwrap(), None); // reset our extensions to the last known good ones. *self.extensions.lock().unwrap() = self.sent_extensions.lock().unwrap().take(); @@ -1197,12 +1198,12 @@ impl SlidingSync { impl SlidingSync { /// Get a copy of the `pos` value. pub fn pos(&self) -> Option { - self.pos.get_cloned() + self.pos.read().unwrap().clone() } /// Set a new value for `pos`. pub fn set_pos(&self, new_pos: String) { - self.pos.set(Some(new_pos)) + Observable::set(&mut self.pos.write().unwrap(), Some(new_pos)); } } diff --git a/crates/matrix-sdk/src/sliding_sync/room.rs b/crates/matrix-sdk/src/sliding_sync/room.rs index ab2d70cb2..f32141272 100644 --- a/crates/matrix-sdk/src/sliding_sync/room.rs +++ b/crates/matrix-sdk/src/sliding_sync/room.rs @@ -3,11 +3,13 @@ use std::{ ops::Not, sync::{ atomic::{AtomicBool, Ordering}, - Arc, + Arc, RwLock as StdRwLock, }, }; -use futures_signals::{signal::Mutable, signal_vec::MutableVec}; +use eyeball::Observable; +use eyeball_im::ObservableVector; +use im::Vector; use matrix_sdk_base::deserialized_responses::SyncTimelineEvent; use ruma::{ api::client::sync::sync_events::{v4, UnreadNotificationsCount}, @@ -29,10 +31,10 @@ pub struct SlidingSyncRoom { client: Client, room_id: OwnedRoomId, inner: v4::SlidingSyncRoom, - is_loading_more: Mutable, + is_loading_more: Arc>>, is_cold: Arc, - prev_batch: Mutable>, - timeline_queue: Arc>, + prev_batch: Arc>>>, + timeline_queue: Arc>>, } impl SlidingSyncRoom { @@ -45,13 +47,16 @@ impl SlidingSyncRoom { // we overwrite to only keep one copy inner.timeline = vec![]; + let mut timeline_queue = ObservableVector::new(); + timeline_queue.append(timeline.into_iter().collect()); + Self { client, room_id, - is_loading_more: Mutable::new(false), + is_loading_more: Arc::new(StdRwLock::new(Observable::new(false))), is_cold: Arc::new(AtomicBool::new(false)), - prev_batch: Mutable::new(inner.prev_batch.clone()), - timeline_queue: Arc::new(MutableVec::new_with_values(timeline)), + prev_batch: Arc::new(StdRwLock::new(Observable::new(inner.prev_batch.clone()))), + timeline_queue: Arc::new(StdRwLock::new(timeline_queue)), inner, } } @@ -63,12 +68,12 @@ impl SlidingSyncRoom { /// Are we currently fetching more timeline events in this room? pub fn is_loading_more(&self) -> bool { - *self.is_loading_more.lock_ref() + **self.is_loading_more.read().unwrap() } /// The `prev_batch` key to fetch more timeline events for this room. pub fn prev_batch(&self) -> Option { - self.prev_batch.lock_ref().clone() + self.prev_batch.read().unwrap().clone() } /// `Timeline` of this room @@ -78,12 +83,12 @@ impl SlidingSyncRoom { fn timeline_builder(&self) -> Option { if let Some(room) = self.client.get_room(&self.room_id) { - Some( - Timeline::builder(&room) - .events(self.prev_batch.get_cloned(), self.timeline_queue.lock_ref().to_vec()), - ) + Some(Timeline::builder(&room).events( + self.prev_batch.read().unwrap().clone(), + self.timeline_queue.read().unwrap().clone(), + )) } else if let Some(invited_room) = self.client.get_invited_room(&self.room_id) { - Some(Timeline::builder(&invited_room).events(None, vec![])) + Some(Timeline::builder(&invited_room).events(None, Vector::new())) } else { error!( room_id = ?self.room_id, @@ -158,7 +163,7 @@ impl SlidingSyncRoom { self.inner.required_state = required_state; if let Some(batch) = prev_batch { - self.prev_batch.lock_mut().replace(batch); + Observable::set(&mut self.prev_batch.write().unwrap(), Some(batch)); } // There is timeline updates. @@ -167,17 +172,26 @@ impl SlidingSyncRoom { // If we come from a cold storage, we overwrite the timeline queue with the // timeline updates. - self.timeline_queue.lock_mut().replace_cloned(timeline_updates); + let mut lock = self.timeline_queue.write().unwrap(); + lock.clear(); + for event in timeline_updates { + lock.push_back(event); + } + self.is_cold.store(false, Ordering::SeqCst); } else if limited { // The server alerted us that we missed items in between. - self.timeline_queue.lock_mut().replace_cloned(timeline_updates); + let mut lock = self.timeline_queue.write().unwrap(); + lock.clear(); + for event in timeline_updates { + lock.push_back(event); + } } else { // It's the hot path. We have new updates that must be added to the existing // timeline queue. - let mut timeline_queue = self.timeline_queue.lock_mut(); + let mut timeline_queue = self.timeline_queue.write().unwrap(); // If the `timeline_queue` contains: // [D, E, F] @@ -243,7 +257,9 @@ impl SlidingSyncRoom { if position == 0 { // No prefix found. - timeline_queue.extend(timeline_updates); + for event in timeline_updates { + timeline_queue.push_back(event); + } } else { // Prefix found. @@ -252,7 +268,7 @@ impl SlidingSyncRoom { if !new_timeline_updates.is_empty() { for (at, update) in new_timeline_updates.iter().cloned().enumerate() { - timeline_queue.insert_cloned(at, update); + timeline_queue.insert(at, update); } } } @@ -263,22 +279,24 @@ impl SlidingSyncRoom { // alert that we are stale. In this case, we should just clear the // existing timeline. - self.timeline_queue.lock_mut().clear(); + self.timeline_queue.write().unwrap().clear(); } } pub(super) fn from_frozen(frozen_room: FrozenSlidingSyncRoom, client: Client) -> Self { - let FrozenSlidingSyncRoom { room_id, inner, prev_batch, timeline_queue: timeline } = - frozen_room; + let FrozenSlidingSyncRoom { room_id, inner, prev_batch, timeline_queue } = frozen_room; + + let mut timeline_queue_ob = ObservableVector::new(); + timeline_queue_ob.append(timeline_queue); Self { client, room_id, inner, - is_loading_more: Mutable::new(false), + is_loading_more: Arc::new(StdRwLock::new(Observable::new(false))), is_cold: Arc::new(AtomicBool::new(true)), - prev_batch: Mutable::new(prev_batch), - timeline_queue: Arc::new(MutableVec::new_with_values(timeline)), + prev_batch: Arc::new(StdRwLock::new(Observable::new(prev_batch))), + timeline_queue: Arc::new(StdRwLock::new(timeline_queue_ob)), } } } @@ -291,12 +309,12 @@ pub(super) struct FrozenSlidingSyncRoom { inner: v4::SlidingSyncRoom, prev_batch: Option, #[serde(rename = "timeline")] - timeline_queue: Vec, + timeline_queue: Vector, } impl From<&SlidingSyncRoom> for FrozenSlidingSyncRoom { fn from(value: &SlidingSyncRoom) -> Self { - let timeline = value.timeline_queue.lock_ref(); + let timeline = value.timeline_queue.read().unwrap(); let timeline_length = timeline.len(); // To not overflow the database, we only freeze the newest 10 items. On doing @@ -307,7 +325,7 @@ impl From<&SlidingSyncRoom> for FrozenSlidingSyncRoom { let pos = timeline_length - 10; (None, timeline.iter().skip(pos).cloned().collect()) } else { - (value.prev_batch.lock_ref().clone(), timeline.to_vec()) + (value.prev_batch.read().unwrap().clone(), timeline.clone()) }; Self { @@ -321,6 +339,7 @@ impl From<&SlidingSyncRoom> for FrozenSlidingSyncRoom { #[cfg(test)] mod tests { + use im::vector; use matrix_sdk_base::deserialized_responses::TimelineEvent; use ruma::{events::room::message::RoomMessageEventContent, RoomId}; use serde_json::json; @@ -333,7 +352,7 @@ mod tests { room_id: <&RoomId>::try_from("!29fhd83h92h0:example.com").unwrap().to_owned(), inner: v4::SlidingSyncRoom::default(), prev_batch: Some("let it go!".to_owned()), - timeline_queue: vec![TimelineEvent { + timeline_queue: vector![TimelineEvent { event: Raw::new(&json! ({ "content": RoomMessageEventContent::text_plain("let it gooo!"), "type": "m.room.message", diff --git a/crates/matrix-sdk/src/sliding_sync/view.rs b/crates/matrix-sdk/src/sliding_sync/view.rs index 42b62c4dc..5caafdffc 100644 --- a/crates/matrix-sdk/src/sliding_sync/view.rs +++ b/crates/matrix-sdk/src/sliding_sync/view.rs @@ -1,16 +1,17 @@ use std::{ collections::BTreeMap, fmt::Debug, + iter, sync::{ atomic::{AtomicBool, Ordering}, - Arc, + Arc, RwLock as StdRwLock, }, }; -use futures_signals::{ - signal::{Mutable, MutableSignalCloned, SignalExt, SignalStream}, - signal_vec::{MutableSignalVec, MutableVec, MutableVecLockMut, SignalVecExt, SignalVecStream}, -}; +use eyeball::Observable; +use eyeball_im::{ObservableVector, VectorDiff}; +use futures_core::Stream; +use im::Vector; use ruma::{ api::client::sync::sync_events::v4, assign, events::StateEventType, OwnedRoomId, RoomId, UInt, }; @@ -64,43 +65,40 @@ pub struct SlidingSyncView { filters: Option, /// The maximum number of timeline events to query for - pub timeline_limit: Mutable>, + pub timeline_limit: Arc>>>, // ----- Public state /// Name of this view to easily recognize them pub name: String, /// The state this view is in - state: Mutable, + state: Arc>>, /// The total known number of rooms, - rooms_count: Mutable>, + rooms_count: Arc>>>, /// The rooms in order - rooms_list: MutableVec, + rooms_list: Arc>>, /// The ranges windows of the view - ranges: Mutable>, + #[allow(clippy::type_complexity)] // temporarily + ranges: Arc>>>, - /// Signaling updates on the room list after processing - rooms_updated_signal: futures_signals::signal::Sender<()>, - - is_cold: Arc, - - /// Get informed if anything in the room changed + /// Get informed if anything in the room changed. /// /// If you only care to know about changes once all of them have applied - /// (including the total) listen to a clone of this signal. - pub rooms_updated_broadcaster: - futures_signals::signal::Broadcaster>, + /// (including the total), subscribe to this observable. + pub rooms_updated_broadcast: Arc>>, + + is_cold: Arc, } #[derive(Serialize, Deserialize)] pub(super) struct FrozenSlidingSyncView { #[serde(default, skip_serializing_if = "Option::is_none")] pub(super) rooms_count: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub(super) rooms_list: Vec, + #[serde(default, skip_serializing_if = "Vector::is_empty")] + pub(super) rooms_list: Vector, #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub(super) rooms: BTreeMap, } @@ -111,8 +109,8 @@ impl FrozenSlidingSyncView { rooms_map: &BTreeMap, ) -> Self { let mut rooms = BTreeMap::new(); - let mut rooms_list = Vec::new(); - for entry in source_view.rooms_list.lock_ref().iter() { + let mut rooms_list = Vector::new(); + for entry in source_view.rooms_list.read().unwrap().iter() { match entry { RoomListEntry::Filled(o) | RoomListEntry::Invalidated(o) => { rooms.insert(o.clone(), rooms_map.get(o).expect("rooms always exists").into()); @@ -120,10 +118,10 @@ impl FrozenSlidingSyncView { _ => {} }; - rooms_list.push(entry.freeze()); + rooms_list.push_back(entry.freeze()); } FrozenSlidingSyncView { - rooms_count: *source_view.rooms_count.lock_ref(), + rooms_count: **source_view.rooms_count.read().unwrap(), rooms_list, rooms, } @@ -134,12 +132,15 @@ impl SlidingSyncView { pub(crate) fn set_from_cold( &mut self, rooms_count: Option, - rooms_list: Vec, + rooms_list: Vector, ) { - self.state.set(SlidingSyncState::Preload); + Observable::set(&mut self.state.write().unwrap(), SlidingSyncState::Preload); self.is_cold.store(true, Ordering::SeqCst); - self.rooms_count.replace(rooms_count); - self.rooms_list.lock_mut().replace_cloned(rooms_list); + Observable::set(&mut self.rooms_count.write().unwrap(), rooms_count); + + let mut lock = self.rooms_list.write().unwrap(); + lock.clear(); + lock.append(rooms_list); } /// Create a new [`SlidingSyncViewBuilder`]. @@ -155,7 +156,7 @@ impl SlidingSyncView { .sort(self.sort.clone()) .required_state(self.required_state.clone()) .batch_size(self.batch_size) - .ranges(self.ranges.read_only().get_cloned()) + .ranges(self.ranges.read().unwrap().clone()) } /// Set the ranges to fetch @@ -163,7 +164,8 @@ impl SlidingSyncView { /// Remember to cancel the existing stream and fetch a new one as this will /// only be applied on the next request. pub fn set_ranges(&self, range: Vec<(u32, u32)>) -> &Self { - *self.ranges.lock_mut() = range.into_iter().map(|(a, b)| (a.into(), b.into())).collect(); + let value = range.into_iter().map(|(a, b)| (a.into(), b.into())).collect(); + Observable::set(&mut self.ranges.write().unwrap(), value); self } @@ -172,7 +174,8 @@ impl SlidingSyncView { /// Remember to cancel the existing stream and fetch a new one as this will /// only be applied on the next request. pub fn set_range(&self, start: u32, end: u32) -> &Self { - *self.ranges.lock_mut() = vec![(start.into(), end.into())]; + let value = vec![(start.into(), end.into())]; + Observable::set(&mut self.ranges.write().unwrap(), value); self } @@ -181,7 +184,9 @@ impl SlidingSyncView { /// Remember to cancel the existing stream and fetch a new one as this will /// only be applied on the next request. pub fn add_range(&self, start: u32, end: u32) -> &Self { - self.ranges.lock_mut().push((start.into(), end.into())); + Observable::update(&mut self.ranges.write().unwrap(), |ranges| { + ranges.push((start.into(), end.into())); + }); self } @@ -195,18 +200,18 @@ impl SlidingSyncView { /// Remember to cancel the existing stream and fetch a new one as this will /// only be applied on the next request. pub fn reset_ranges(&self) -> &Self { - self.ranges.lock_mut().clear(); + Observable::set(&mut self.ranges.write().unwrap(), Vec::new()); self } /// Get the current state. pub fn state(&self) -> SlidingSyncState { - self.state.get_cloned() + self.state.read().unwrap().clone() } /// Get a stream of state. - pub fn state_stream(&self) -> SignalStream> { - self.state.signal_cloned().to_stream() + pub fn state_stream(&self) -> impl Stream { + Observable::subscribe(&self.state.read().unwrap()) } /// Get the current rooms list. @@ -214,22 +219,22 @@ impl SlidingSyncView { where R: for<'a> From<&'a RoomListEntry>, { - self.rooms_list.lock_ref().iter().map(|e| R::from(e)).collect() + self.rooms_list.read().unwrap().iter().map(|e| R::from(e)).collect() } /// Get a stream of rooms list. - pub fn rooms_list_stream(&self) -> SignalVecStream> { - self.rooms_list.signal_vec_cloned().to_stream() + pub fn rooms_list_stream(&self) -> impl Stream> { + ObservableVector::subscribe(&self.rooms_list.read().unwrap()) } /// Get the current rooms count. pub fn rooms_count(&self) -> Option { - self.rooms_count.get_cloned() + **self.rooms_count.read().unwrap() } /// Get a stream of rooms count. - pub fn rooms_count_stream(&self) -> SignalStream>> { - self.rooms_count.signal_cloned().to_stream() + pub fn rooms_count_stream(&self) -> impl Stream> { + Observable::subscribe(&self.rooms_count.read().unwrap()) } /// Find the current valid position of the room in the view room_list. @@ -238,8 +243,8 @@ impl SlidingSyncView { /// Invalid items are ignore. Return the total position the item was /// found in the room_list, return None otherwise. pub fn find_room_in_view(&self, room_id: &RoomId) -> Option { - let ranges = self.ranges.lock_ref(); - let listing = self.rooms_list.lock_ref(); + let ranges = self.ranges.read().unwrap(); + let listing = self.rooms_list.read().unwrap(); for (start_uint, end_uint) in ranges.iter() { let mut cur_pos: usize = (*start_uint).try_into().unwrap(); let end: usize = (*end_uint).try_into().unwrap(); @@ -265,8 +270,8 @@ impl SlidingSyncView { /// Invalid items are ignore. Return the total position the items that were /// found in the room_list, will skip any room not found in the rooms_list. pub fn find_rooms_in_view(&self, room_ids: &[OwnedRoomId]) -> Vec<(usize, OwnedRoomId)> { - let ranges = self.ranges.lock_ref(); - let listing = self.rooms_list.lock_ref(); + let ranges = self.ranges.read().unwrap(); + let listing = self.rooms_list.read().unwrap(); let mut rooms_found = Vec::new(); for (start_uint, end_uint) in ranges.iter() { let mut cur_pos: usize = (*start_uint).try_into().unwrap(); @@ -289,7 +294,11 @@ impl SlidingSyncView { /// Return the room_id at the given index pub fn get_room_id(&self, index: usize) -> Option { - self.rooms_list.lock_ref().get(index).and_then(|e| e.as_room_id().map(ToOwned::to_owned)) + self.rooms_list + .read() + .unwrap() + .get(index) + .and_then(|e| e.as_room_id().map(ToOwned::to_owned)) } #[instrument(skip(self, ops), fields(name = self.name, ops_count = ops.len()))] @@ -300,33 +309,39 @@ impl SlidingSyncView { ranges: &Vec<(usize, usize)>, rooms: &Vec, ) -> Result { - let current_rooms_count = self.rooms_count.get(); + let current_rooms_count = **self.rooms_count.read().unwrap(); if current_rooms_count.is_none() || current_rooms_count == Some(0) || self.is_cold.load(Ordering::SeqCst) { debug!("first run, replacing rooms list"); // first response, we do that slightly differently - let rooms_list = - MutableVec::new_with_values(vec![RoomListEntry::Empty; rooms_count as usize]); + let mut rooms_list = ObservableVector::new(); + rooms_list + .append(iter::repeat(RoomListEntry::Empty).take(rooms_count as usize).collect()); // then we apply it - let mut locked = rooms_list.lock_mut(); - room_ops(&mut locked, ops, ranges)?; - self.rooms_list.lock_mut().replace_cloned(locked.as_slice().to_vec()); - self.rooms_count.set(Some(rooms_count)); + room_ops(&mut rooms_list, ops, ranges)?; + + { + let mut lock = self.rooms_list.write().unwrap(); + lock.clear(); + lock.append(rooms_list.into_inner()); + } + + Observable::set(&mut self.rooms_count.write().unwrap(), Some(rooms_count)); self.is_cold.store(false, Ordering::SeqCst); return Ok(true); } debug!("regular update"); - let mut missing = - rooms_count.checked_sub(self.rooms_list.lock_ref().len() as u32).unwrap_or_default(); + let mut missing = rooms_count + .checked_sub(self.rooms_list.read().unwrap().len() as u32) + .unwrap_or_default(); let mut changed = false; if missing > 0 { - let mut list = self.rooms_list.lock_mut(); - list.reserve_exact(missing as usize); + let mut list = self.rooms_list.write().unwrap(); while missing > 0 { - list.push_cloned(RoomListEntry::Empty); + list.push_back(RoomListEntry::Empty); missing -= 1; } changed = true; @@ -334,7 +349,7 @@ impl SlidingSyncView { { // keep the lock scoped so that the later find_rooms_in_view doesn't deadlock - let mut rooms_list = self.rooms_list.lock_mut(); + let mut rooms_list = self.rooms_list.write().unwrap(); if !ops.is_empty() { room_ops(&mut rooms_list, ops, ranges)?; @@ -344,28 +359,29 @@ impl SlidingSyncView { } } - if self.rooms_count.get() != Some(rooms_count) { - self.rooms_count.set(Some(rooms_count)); - changed = true; + { + let mut lock = self.rooms_count.write().unwrap(); + if **lock != Some(rooms_count) { + Observable::set(&mut lock, Some(rooms_count)); + changed = true; + } } if self.send_updates_for_items && !rooms.is_empty() { let found_views = self.find_rooms_in_view(rooms); if !found_views.is_empty() { debug!("room details found"); - let mut rooms_list = self.rooms_list.lock_mut(); + let mut rooms_list = self.rooms_list.write().unwrap(); for (pos, room_id) in found_views { // trigger an `UpdatedAt` update - rooms_list.set_cloned(pos, RoomListEntry::Filled(room_id)); + rooms_list.set(pos, RoomListEntry::Filled(room_id)); changed = true; } } } if changed { - if let Err(e) = self.rooms_updated_signal.send(()) { - warn!("Could not inform about rooms updated: {:?}", e); - } + Observable::set(&mut self.rooms_updated_broadcast.write().unwrap(), ()); } Ok(changed) @@ -401,7 +417,7 @@ pub struct SlidingSyncViewBuilder { name: Option, state: SlidingSyncState, rooms_count: Option, - rooms_list: Vec, + rooms_list: Vector, ranges: Vec<(UInt, UInt)>, } @@ -422,7 +438,7 @@ impl SlidingSyncViewBuilder { name: None, state: SlidingSyncState::default(), rooms_count: None, - rooms_list: Vec::new(), + rooms_list: Vector::new(), ranges: Vec::new(), } } @@ -520,7 +536,9 @@ impl SlidingSyncViewBuilder { /// Build the view pub fn build(self) -> Result { - let (sender, receiver) = futures_signals::signal::channel(()); + let mut rooms_list = ObservableVector::new(); + rooms_list.append(self.rooms_list); + Ok(SlidingSyncView { sync_mode: self.sync_mode, sort: self.sort, @@ -529,15 +547,14 @@ impl SlidingSyncViewBuilder { send_updates_for_items: self.send_updates_for_items, limit: self.limit, filters: self.filters, - timeline_limit: Mutable::new(self.timeline_limit), + timeline_limit: Arc::new(StdRwLock::new(Observable::new(self.timeline_limit))), name: self.name.ok_or(Error::BuildMissingField("name"))?, - state: Mutable::new(self.state), - rooms_count: Mutable::new(self.rooms_count), - rooms_list: MutableVec::new_with_values(self.rooms_list), - ranges: Mutable::new(self.ranges), + state: Arc::new(StdRwLock::new(Observable::new(self.state))), + rooms_count: Arc::new(StdRwLock::new(Observable::new(self.rooms_count))), + rooms_list: Arc::new(StdRwLock::new(rooms_list)), + ranges: Arc::new(StdRwLock::new(Observable::new(self.ranges))), is_cold: Arc::new(AtomicBool::new(false)), - rooms_updated_signal: sender, - rooms_updated_broadcaster: futures_signals::signal::Broadcaster::new(receiver), + rooms_updated_broadcast: Arc::new(StdRwLock::new(Observable::new(()))), }) } } @@ -560,7 +577,8 @@ impl SlidingSyncViewRequestGenerator { let limit = view.limit; let position = view .ranges - .get_cloned() + .read() + .unwrap() .first() .map(|(_start, end)| u32::try_from(*end).unwrap()) .unwrap_or_default(); @@ -582,7 +600,8 @@ impl SlidingSyncViewRequestGenerator { let limit = view.limit; let position = view .ranges - .get_cloned() + .read() + .unwrap() .first() .map(|(_start, end)| u32::try_from(*end).unwrap()) .unwrap_or_default(); @@ -625,7 +644,7 @@ impl SlidingSyncViewRequestGenerator { fn make_request_for_ranges(&mut self, ranges: Vec<(UInt, UInt)>) -> v4::SyncRequestList { let sort = self.view.sort.clone(); let required_state = self.view.required_state.clone(); - let timeline_limit = self.view.timeline_limit.get_cloned(); + let timeline_limit = **self.view.timeline_limit.read().unwrap(); let filters = self.view.filters.clone(); self.ranges = ranges @@ -651,7 +670,7 @@ impl SlidingSyncViewRequestGenerator { // generate the next live request fn live_request(&mut self) -> v4::SyncRequestList { - let ranges = self.view.ranges.read_only().get_cloned(); + let ranges = self.view.ranges.read().unwrap().clone(); self.make_request_for_ranges(ranges) } @@ -693,21 +712,21 @@ impl SlidingSyncViewRequestGenerator { *position = max; *live = true; - self.view.state.set_if(SlidingSyncState::Live, |before, _now| { - !matches!(before, SlidingSyncState::Live) + Observable::update_eq(&mut self.view.state.write().unwrap(), |state| { + *state = SlidingSyncState::Live; }); } else { *position = end; *live = false; self.view.set_range(0, end); - self.view.state.set_if(SlidingSyncState::CatchingUp, |before, _now| { - !matches!(before, SlidingSyncState::CatchingUp) + Observable::update_eq(&mut self.view.state.write().unwrap(), |state| { + *state = SlidingSyncState::CatchingUp; }); } } InnerSlidingSyncViewRequestGenerator::Live => { - self.view.state.set_if(SlidingSyncState::Live, |before, _now| { - !matches!(before, SlidingSyncState::Live) + Observable::update_eq(&mut self.view.state.write().unwrap(), |state| { + *state = SlidingSyncState::Live; }); } } @@ -744,7 +763,7 @@ impl Iterator for SlidingSyncViewRequestGenerator { #[instrument(skip(ops))] fn room_ops( - rooms_list: &mut MutableVecLockMut<'_, RoomListEntry>, + rooms_list: &mut ObservableVector, ops: &Vec, room_ranges: &Vec<(usize, usize)>, ) -> Result<(), Error> { @@ -769,9 +788,9 @@ fn room_ops( .map(|(i, r)| { let idx = start as usize + i; if idx >= rooms_list.len() { - rooms_list.insert_cloned(idx, RoomListEntry::Filled(r)); + rooms_list.insert(idx, RoomListEntry::Filled(r)); } else { - rooms_list.set_cloned(idx, RoomListEntry::Filled(r)); + rooms_list.set(idx, RoomListEntry::Filled(r)); } }) .count(); @@ -788,7 +807,7 @@ fn room_ops( .map_err(|e| { Error::BadResponse(format!("`index` not a valid int for DELETE: {e:}")) })?; - rooms_list.set_cloned(pos as usize, RoomListEntry::Empty); + rooms_list.set(pos as usize, RoomListEntry::Empty); } v4::SlidingOp::Insert => { let pos: usize = op @@ -802,7 +821,7 @@ fn room_ops( .map_err(|e| { Error::BadResponse(format!("`index` not a valid int for INSERT: {e:}")) })?; - let sliced = rooms_list.as_slice(); + let room = RoomListEntry::Filled(op.room_id.clone().ok_or_else(|| { Error::BadResponse("`room_id` must be present for INSERT operation".to_owned()) })?); @@ -812,7 +831,8 @@ fn room_ops( let (prev_p, prev_overflow) = pos.overflowing_sub(dif); let check_prev = !prev_overflow && index_in_range(prev_p); let (next_p, overflown) = pos.overflowing_add(dif); - let check_after = !overflown && next_p < sliced.len() && index_in_range(next_p); + let check_after = + !overflown && next_p < rooms_list.len() && index_in_range(next_p); if !check_prev && !check_after { return Err(Error::BadResponse( "We were asked to insert but could not find any direction to shift to" @@ -820,11 +840,11 @@ fn room_ops( )); } - if check_prev && sliced[prev_p].empty_or_invalidated() { + if check_prev && rooms_list[prev_p].empty_or_invalidated() { // we only check for previous, if there are items left rooms_list.remove(prev_p); break; - } else if check_after && sliced[next_p].empty_or_invalidated() { + } else if check_after && rooms_list[next_p].empty_or_invalidated() { rooms_list.remove(next_p); break; } else { @@ -832,7 +852,7 @@ fn room_ops( dif += 1; } } - rooms_list.insert_cloned(pos, room); + rooms_list.insert(pos, room); } v4::SlidingOp::Invalidate => { let max_len = rooms_list.len(); @@ -871,9 +891,9 @@ fn room_ops( }; if let Some(b) = entry { - rooms_list.set_cloned(pos as usize, RoomListEntry::Invalidated(b)); + rooms_list.set(pos as usize, RoomListEntry::Invalidated(b)); } else { - rooms_list.set_cloned(pos as usize, RoomListEntry::Empty); + rooms_list.set(pos as usize, RoomListEntry::Empty); } pos += 1; } diff --git a/examples/timeline/Cargo.toml b/examples/timeline/Cargo.toml index 6f4408af5..bc9c979e6 100644 --- a/examples/timeline/Cargo.toml +++ b/examples/timeline/Cargo.toml @@ -12,7 +12,6 @@ test = false anyhow = "1" clap = "4.0.16" futures = "0.3" -futures-signals = { version = "0.3.30", default-features = false } tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread"] } tracing-subscriber = "0.3.15" url = "2.2.2" diff --git a/labs/jack-in/Cargo.toml b/labs/jack-in/Cargo.toml index dd2323a51..bae5ddd48 100644 --- a/labs/jack-in/Cargo.toml +++ b/labs/jack-in/Cargo.toml @@ -13,10 +13,10 @@ app_dirs2 = "2" chrono = "0.4.23" clap = { version = "4.0.29", features = ["derive", "env"] } dialoguer = "0.10.2" +eyeball = { workspace = true } eyeball-im = { workspace = true } 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", "experimental-sliding-sync", "experimental-timeline"], 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" } diff --git a/labs/jack-in/src/app/model.rs b/labs/jack-in/src/app/model.rs index 5a9b93200..26386e16d 100644 --- a/labs/jack-in/src/app/model.rs +++ b/labs/jack-in/src/app/model.rs @@ -2,7 +2,7 @@ //! //! app model -use std::{ops::Deref, time::Duration}; +use std::time::Duration; use futures::executor::block_on; use matrix_sdk::{ruma::events::room::message::RoomMessageEventContent, Client}; @@ -212,7 +212,7 @@ impl Update for Model { None } Msg::SendMessage(m) => { - if let Some(tl) = self.sliding_sync.room_timeline.lock_ref().deref() { + if let Some(tl) = &**self.sliding_sync.room_timeline.read().unwrap() { block_on(async move { // fire and forget tl.send(RoomMessageEventContent::text_plain(m).into(), None).await; diff --git a/labs/jack-in/src/client/mod.rs b/labs/jack-in/src/client/mod.rs index 727d49d8b..485166558 100644 --- a/labs/jack-in/src/client/mod.rs +++ b/labs/jack-in/src/client/mod.rs @@ -95,7 +95,7 @@ pub async fn run_client( while let Some(update) = stream.next().await { { - let selected_room = ssync_state.selected_room.lock_ref().clone(); + let selected_room = ssync_state.selected_room.read().unwrap().clone(); if let Some(room_id) = selected_room { if let Some(prev) = &prev_selected_room { if prev != &room_id { diff --git a/labs/jack-in/src/client/state.rs b/labs/jack-in/src/client/state.rs index 501555420..3ed799d55 100644 --- a/labs/jack-in/src/client/state.rs +++ b/labs/jack-in/src/client/state.rs @@ -1,11 +1,11 @@ use std::{ - sync::Arc, + sync::{Arc, RwLock as StdRwLock}, time::{Duration, Instant}, }; -use eyeball_im::VectorDiff; +use eyeball::Observable; +use eyeball_im::{ObservableVector, VectorDiff}; use futures::{pin_mut, StreamExt}; -use futures_signals::{signal::Mutable, signal_vec::MutableVec}; use matrix_sdk::{ room::timeline::{Timeline, TimelineItem}, ruma::{OwnedRoomId, RoomId}, @@ -29,10 +29,10 @@ pub struct SlidingSyncState { first_render: Option, full_sync: Option, current_state: ViewState, - tl_handle: Mutable>>, - pub selected_room: Mutable>, - pub current_timeline: MutableVec>, - pub room_timeline: Mutable>, + tl_handle: Arc>>>>, + pub selected_room: Arc>>>, + pub current_timeline: Arc>>>, + pub room_timeline: Arc>>>, } impl SlidingSyncState { @@ -56,12 +56,12 @@ impl SlidingSyncState { } pub fn has_selected_room(&self) -> bool { - self.selected_room.lock_ref().is_some() + self.selected_room.read().unwrap().is_some() } pub fn select_room(&self, r: Option) { - self.current_timeline.lock_mut().clear(); - if let Some(c) = self.tl_handle.lock_mut().take() { + self.current_timeline.write().unwrap().clear(); + if let Some(c) = Observable::replace(&mut self.tl_handle.write().unwrap(), None) { c.abort(); } if let Some(room) = r.as_ref().and_then(|room_id| self.get_room(room_id)) { @@ -70,54 +70,53 @@ impl SlidingSyncState { let handle = tokio::spawn(async move { let timeline = room.timeline().await.unwrap(); let (items, listener) = timeline.subscribe().await; - *room_timeline.lock_mut() = Some(timeline); - current_timeline.lock_mut().replace_cloned(items.into_iter().collect()); + Observable::set(&mut room_timeline.write().unwrap(), Some(timeline)); + { + let mut lock = current_timeline.write().unwrap(); + lock.clear(); + lock.append(items.into_iter().collect()); + } pin_mut!(listener); while let Some(diff) = listener.next().await { match diff { VectorDiff::Append { values } => { - let mut lock = current_timeline.lock_mut(); - for v in values { - lock.push_cloned(v); - } + current_timeline.write().unwrap().append(values); } VectorDiff::Clear => { - current_timeline.lock_mut().clear(); + current_timeline.write().unwrap().clear(); } VectorDiff::Insert { index, value } => { - current_timeline.lock_mut().insert_cloned(index, value); + current_timeline.write().unwrap().insert(index, value); } VectorDiff::PopBack => { - current_timeline.lock_mut().pop(); + current_timeline.write().unwrap().pop_back(); } VectorDiff::PopFront => { - current_timeline.lock_mut().remove(0); + current_timeline.write().unwrap().pop_front(); } VectorDiff::PushBack { value } => { - current_timeline.lock_mut().push_cloned(value); + current_timeline.write().unwrap().push_back(value); } VectorDiff::PushFront { value } => { - current_timeline.lock_mut().insert_cloned(0, value); + current_timeline.write().unwrap().push_front(value); } VectorDiff::Remove { index } => { - current_timeline.lock_mut().remove(index); + current_timeline.write().unwrap().remove(index); } VectorDiff::Set { index, value } => { - current_timeline.lock_mut().set_cloned(index, value); + current_timeline.write().unwrap().set(index, value); } VectorDiff::Reset { values } => { - let mut lock = current_timeline.lock_mut(); + let mut lock = current_timeline.write().unwrap(); lock.clear(); - for v in values { - lock.push_cloned(v); - } + lock.append(values); } } } }); - *self.tl_handle.lock_mut() = Some(handle); + Observable::set(&mut self.tl_handle.write().unwrap(), Some(handle)); } - self.selected_room.replace(r); + Observable::set(&mut self.selected_room.write().unwrap(), r); } pub fn time_to_first_render(&self) -> Option { diff --git a/labs/jack-in/src/components/details.rs b/labs/jack-in/src/components/details.rs index a7e160b2b..2f0663c27 100644 --- a/labs/jack-in/src/components/details.rs +++ b/labs/jack-in/src/components/details.rs @@ -46,7 +46,7 @@ impl Details { } pub fn refresh_data(&mut self) { - let Some(room_id) = self.sstate.selected_room.lock_ref().clone() else { return }; + let Some(room_id) = self.sstate.selected_room.read().unwrap().clone() else { return }; let Some(room_data) = self.sstate.get_room(&room_id) else { return; }; @@ -72,7 +72,8 @@ impl Details { let timeline: Vec = self .sstate .current_timeline - .lock_ref() + .read() + .unwrap() .iter() .filter_map(|t| t.as_event()) // we ignore virtual events .map(|e| match e.content() { diff --git a/labs/jack-in/src/main.rs b/labs/jack-in/src/main.rs index 40971216b..cd97b2750 100644 --- a/labs/jack-in/src/main.rs +++ b/labs/jack-in/src/main.rs @@ -7,8 +7,8 @@ use std::{path::Path, time::Duration}; use app_dirs2::{app_root, AppDataType, AppInfo}; use clap::Parser; use dialoguer::{theme::ColorfulTheme, Password}; +use eyeball_im::VectorDiff; use eyre::{eyre, Result}; -use futures_signals::signal_vec::VecDiff; use matrix_sdk::{ config::RequestConfig, room::timeline::TimelineItem, @@ -51,7 +51,7 @@ pub enum Msg { pub enum JackInEvent { Any, // match all SyncUpdate(client::state::SlidingSyncState), - RoomDataUpdate(VecDiff), + RoomDataUpdate(VectorDiff), } impl PartialOrd for JackInEvent { diff --git a/testing/sliding-sync-integration-test/Cargo.toml b/testing/sliding-sync-integration-test/Cargo.toml index e40c26967..49870627c 100644 --- a/testing/sliding-sync-integration-test/Cargo.toml +++ b/testing/sliding-sync-integration-test/Cargo.toml @@ -6,6 +6,7 @@ publish = false [dependencies] anyhow = { workspace = true } +eyeball = { workspace = true } eyeball-im = { workspace = true } matrix-sdk-integration-testing = { path = "../matrix-sdk-integration-testing", features = ["helpers"] } matrix-sdk = { path = "../../crates/matrix-sdk", features = ["experimental-sliding-sync", "testing"] } diff --git a/testing/sliding-sync-integration-test/src/lib.rs b/testing/sliding-sync-integration-test/src/lib.rs index 59fd3995d..1c998f6e3 100644 --- a/testing/sliding-sync-integration-test/src/lib.rs +++ b/testing/sliding-sync-integration-test/src/lib.rs @@ -73,13 +73,14 @@ mod tests { use anyhow::{bail, Context}; use assert_matches::assert_matches; + use eyeball::Observable; use eyeball_im::VectorDiff; use futures::{pin_mut, stream::StreamExt}; use matrix_sdk::{ room::timeline::EventTimelineItem, ruma::{ api::client::error::ErrorKind as RumaError, - events::room::message::RoomMessageEventContent, UInt, + events::room::message::RoomMessageEventContent, uint, }, SlidingSyncMode, SlidingSyncState, SlidingSyncView, }; @@ -237,7 +238,7 @@ mod tests { // Sync to receive messages with a `timeline_limit` set to 20. { - view.timeline_limit.set(Some(UInt::try_from(20u32).unwrap())); + Observable::set(&mut view.timeline_limit.write().unwrap(), Some(uint!(20))); let mut update_summary; @@ -1041,7 +1042,7 @@ mod tests { ); // force the pos to be invalid and thus this being reset internally - sync_proxy.set_pos("100".to_string()); + sync_proxy.set_pos("100".to_owned()); let mut error_seen = false; for _n in 0..2 { From c69d28ef7904bfe93afecdce7497ed5550957b42 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 23 Feb 2023 15:06:09 +0100 Subject: [PATCH 032/166] feat(sdk): Remove clones of large responses in Sliding Sync. I admit this patch is quite tricky. Please try to follow me. So, first off, in `SlidingSyncRoom::new`, we were clearing the timeline, because somehow it exists twice in memory at this step. Which led me to understand how `SlidingSync::handle_response` was working. I've clarified how this part of the code works. We are dealing with 2 kind of responses for a specific reason: `SyncResponse` and `v4::Response`, now it's documented and I hope it's clearer. Then, I notice that we were passing a clone of the entire sliding sync response (`v4::Response`) to `Client::process_sliding_sync`. I thought it was suboptimal, so I've updated the code to take a reference. It led me to update `BaseClient::process_sliding_sync`. It was a little bit tricky, but I reckon we have less clones now than before. And now, back to `SlidingSync::handle_response`, I was able to compute the `timeline` correctly by draining it from the `v4::Response`, or by moving it from `SyncResponse`. So it's no longer necessary to have this clearing code inside `SlidingSyncRoom::new`. Honestly it has nothing to do at this place before. To conclude: We have cleaner code, and less clones. What thing I reckon could be optimized, is that the entire `timeline` (`Vec`) is cloned to be passed to `Client::handle_timeline`. So this timeline exists in 2 places: in Sliding Sync, and somewhere else. I don't believe it's a problem now, that's how it works, but we must be aware of that. --- crates/matrix-sdk-base/src/sliding_sync.rs | 37 +++++++++------ crates/matrix-sdk/src/sliding_sync/client.rs | 3 +- crates/matrix-sdk/src/sliding_sync/mod.rs | 47 ++++++++++++++------ crates/matrix-sdk/src/sliding_sync/room.rs | 3 -- 4 files changed, 59 insertions(+), 31 deletions(-) diff --git a/crates/matrix-sdk-base/src/sliding_sync.rs b/crates/matrix-sdk-base/src/sliding_sync.rs index 4a7dde386..23b36dd06 100644 --- a/crates/matrix-sdk-base/src/sliding_sync.rs +++ b/crates/matrix-sdk-base/src/sliding_sync.rs @@ -26,7 +26,7 @@ impl BaseClient { /// * `response` - The response that we received after a successful sliding /// sync. #[instrument(skip_all, level = "trace")] - pub async fn process_sliding_sync(&self, response: v4::Response) -> Result { + pub async fn process_sliding_sync(&self, response: &v4::Response) -> Result { #[allow(unused_variables)] let v4::Response { // FIXME not yet supported by sliding sync. see @@ -39,6 +39,7 @@ impl BaseClient { //presence, .. } = response; + info!(rooms = rooms.len(), lists = lists.len(), extensions = !extensions.is_empty()); if rooms.is_empty() && extensions.is_empty() { @@ -49,21 +50,31 @@ impl BaseClient { let v4::Extensions { to_device, e2ee, account_data, .. } = extensions; - let to_device_events = to_device.map(|v4| v4.events).unwrap_or_default(); + let to_device_events = to_device.as_ref().map(|v4| v4.events.clone()).unwrap_or_default(); // Destructure the single `None` of the E2EE extension into separate objects - // since that's what the OlmMachine API expects. Passing in the default - // empty maps and vecs for this is completely fine, since the OlmMachine + // since that's what the `OlmMachine` API expects. Passing in the default + // empty maps and vecs for this is completely fine, since the `OlmMachine` // assumes empty maps/vecs mean no change in the one-time key counts. + + // We declare default values that can be referenced hereinbelow. When we try to + // extract values from `e2ee`, that would be unfortunate to clone the + // value just to pass them (to remove them `e2ee`) as a reference later. + let device_one_time_keys_count = Default::default(); + let device_unused_fallback_key_types = Default::default(); + let (device_lists, device_one_time_keys_count, device_unused_fallback_key_types) = e2ee + .as_ref() .map(|e2ee| { ( - e2ee.device_lists, - e2ee.device_one_time_keys_count, - e2ee.device_unused_fallback_key_types, + e2ee.device_lists.clone(), + &e2ee.device_one_time_keys_count, + &e2ee.device_unused_fallback_key_types, ) }) - .unwrap_or_default(); + .unwrap_or_else(|| { + (Default::default(), &device_one_time_keys_count, &device_unused_fallback_key_types) + }); info!( to_device_events = to_device_events.len(), @@ -153,7 +164,7 @@ impl BaseClient { // } let room_account_data = if let Some(inner_account_data) = &account_data { - if let Some(events) = inner_account_data.rooms.get(&room_id) { + 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 { @@ -171,8 +182,8 @@ impl BaseClient { .handle_timeline( &room, room_data.limited, - room_data.timeline, - room_data.prev_batch, + room_data.timeline.clone(), + room_data.prev_batch.clone(), &push_rules, &mut user_ids, &mut room_info, @@ -246,7 +257,7 @@ impl BaseClient { debug!("applied changes"); let device_one_time_keys_count = - device_one_time_keys_count.into_iter().map(|(k, v)| (k, v.into())).collect(); + device_one_time_keys_count.iter().map(|(k, v)| (k.clone(), (*v).into())).collect(); Ok(SyncResponse { rooms: new_rooms, @@ -254,7 +265,7 @@ impl BaseClient { notifications: changes.notifications, // FIXME not yet supported by sliding sync. presence: Default::default(), - account_data: account_data.map(|a| a.global).unwrap_or_default(), + account_data: account_data.as_ref().map(|a| a.global.clone()).unwrap_or_default(), to_device_events, device_lists, device_one_time_keys_count, diff --git a/crates/matrix-sdk/src/sliding_sync/client.rs b/crates/matrix-sdk/src/sliding_sync/client.rs index 6c5ce103a..68bb3ce63 100644 --- a/crates/matrix-sdk/src/sliding_sync/client.rs +++ b/crates/matrix-sdk/src/sliding_sync/client.rs @@ -14,11 +14,12 @@ impl Client { #[instrument(skip(self, response))] pub(crate) async fn process_sliding_sync( &self, - response: v4::Response, + response: &v4::Response, ) -> Result { let response = self.base_client().process_sliding_sync(response).await?; debug!("done processing on base_client"); self.handle_sync_response(&response).await?; + Ok(response) } } diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index a60361367..9a242955d 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -975,28 +975,45 @@ impl SlidingSync { self.rooms.read().unwrap().values().cloned().collect() } + /// Handle the HTTP response. + /// + /// But which response? `v4::Response`, aka the Sliding Sync response, or + /// `SyncResponse` which still relies on `v3`? + /// Well that's tricky. We have both here, because this Sliding Sync + /// implementation is still experimental, and we didn't want to be too + /// invasive. Thus, `SyncResponse` doesn't support Sliding Sync yet. Hence + /// the fact this method handles both at the same time. It's not super + /// annoying but it was important to clarify that. #[instrument(skip_all, fields(views = views.len()))] async fn handle_response( &self, - resp: v4::Response, + sliding_sync_response: v4::Response, extensions: Option, views: &mut BTreeMap, ) -> Result { - let mut processed = self.client.process_sliding_sync(resp.clone()).await?; - debug!("main client processed."); - Observable::set(&mut self.pos.write().unwrap(), Some(resp.pos)); - Observable::set(&mut self.delta_token.write().unwrap(), resp.delta_token); + // We may not need the `sync_response` in the future (once `SyncResponse` will + // move to Sliding Sync, i.e. to `v4::Response`), but processing the + // `sliding_sync_response` is vital, so it must be done somewhere; for now it + // happens here. + let mut sync_response = self.client.process_sliding_sync(&sliding_sync_response).await?; - let update = { + debug!("sliding sync response has been processed"); + + Observable::set(&mut self.pos.write().unwrap(), Some(sliding_sync_response.pos)); + Observable::set(&mut self.delta_token.write().unwrap(), sliding_sync_response.delta_token); + + let update_summary = { let mut rooms = Vec::new(); let mut rooms_map = self.rooms.write().unwrap(); - for (id, mut room_data) in resp.rooms.into_iter() { - let timeline = if let Some(joined_room) = processed.rooms.join.remove(&id) { + + for (id, mut room_data) in sliding_sync_response.rooms.into_iter() { + // `sync_response` contains the rooms with decrypted events if any, so look at + // the timeline events here first if the room exists. + // Otherwise, let's look at the timeline inside the `sliding_sync_response`. + let timeline = if let Some(joined_room) = sync_response.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 + room_data.timeline.drain(..).map(Into::into).collect() }; if let Some(mut room) = rooms_map.remove(&id) { @@ -1018,7 +1035,7 @@ impl SlidingSync { let mut updated_views = Vec::new(); - for (name, updates) in resp.lists { + for (name, updates) in sliding_sync_response.lists { let Some(generator) = views.get_mut(&name) else { error!("Response for view {name} - unknown to us. skipping"); continue @@ -1031,7 +1048,9 @@ impl SlidingSync { } // Update the `to-device` next-batch if found. - if let Some(to_device_since) = resp.extensions.to_device.map(|t| t.next_batch) { + if let Some(to_device_since) = + sliding_sync_response.extensions.to_device.map(|t| t.next_batch) + { self.update_to_device_since(to_device_since) } @@ -1046,7 +1065,7 @@ impl SlidingSync { self.cache_to_storage().await?; - Ok(update) + Ok(update_summary) } async fn sync_once( diff --git a/crates/matrix-sdk/src/sliding_sync/room.rs b/crates/matrix-sdk/src/sliding_sync/room.rs index f32141272..402d48cec 100644 --- a/crates/matrix-sdk/src/sliding_sync/room.rs +++ b/crates/matrix-sdk/src/sliding_sync/room.rs @@ -44,9 +44,6 @@ impl SlidingSyncRoom { mut inner: v4::SlidingSyncRoom, timeline: Vec, ) -> Self { - // we overwrite to only keep one copy - inner.timeline = vec![]; - let mut timeline_queue = ObservableVector::new(); timeline_queue.append(timeline.into_iter().collect()); From 54654aa0c77b949ce7c3ada04c55c52a318ca126 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 23 Feb 2023 16:03:11 +0100 Subject: [PATCH 033/166] chore(sdk): Simplify references, thanks Clippy. --- crates/matrix-sdk-base/src/sliding_sync.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/matrix-sdk-base/src/sliding_sync.rs b/crates/matrix-sdk-base/src/sliding_sync.rs index 23b36dd06..99cdae04c 100644 --- a/crates/matrix-sdk-base/src/sliding_sync.rs +++ b/crates/matrix-sdk-base/src/sliding_sync.rs @@ -91,7 +91,7 @@ impl BaseClient { self.preprocess_to_device_events( to_device_events, &device_lists, - &device_one_time_keys_count, + device_one_time_keys_count, device_unused_fallback_key_types.as_deref(), ) .await? @@ -109,14 +109,14 @@ impl BaseClient { let mut new_rooms = Rooms::default(); - for (room_id, room_data) in rooms.into_iter() { + for (room_id, room_data) in rooms { 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(); room_info.mark_state_partially_synced(); - 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 room_info.mark_state_partially_synced(); @@ -130,7 +130,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 room_info.mark_state_partially_synced(); @@ -164,8 +164,8 @@ impl BaseClient { // } 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; + 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 @@ -199,8 +199,8 @@ 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(); From af6cd85cf43aba75dc618016fc09a16d79dfff3d Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 23 Feb 2023 16:27:19 +0100 Subject: [PATCH 034/166] chore(sdk): `SlidingSyncRoom::new` no longer needs a mutable `inner`. --- crates/matrix-sdk/src/sliding_sync/room.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/sliding_sync/room.rs b/crates/matrix-sdk/src/sliding_sync/room.rs index 402d48cec..3a1c89147 100644 --- a/crates/matrix-sdk/src/sliding_sync/room.rs +++ b/crates/matrix-sdk/src/sliding_sync/room.rs @@ -41,7 +41,7 @@ impl SlidingSyncRoom { pub(super) fn new( client: Client, room_id: OwnedRoomId, - mut inner: v4::SlidingSyncRoom, + inner: v4::SlidingSyncRoom, timeline: Vec, ) -> Self { let mut timeline_queue = ObservableVector::new(); From 3c44f87bee622ad892ee65553eb7b18782e2af36 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 23 Feb 2023 16:39:39 +0100 Subject: [PATCH 035/166] doc(sdk): Simplify one documentation. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 9a242955d..3292ce7ea 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -978,12 +978,8 @@ impl SlidingSync { /// Handle the HTTP response. /// /// But which response? `v4::Response`, aka the Sliding Sync response, or - /// `SyncResponse` which still relies on `v3`? - /// Well that's tricky. We have both here, because this Sliding Sync - /// implementation is still experimental, and we didn't want to be too - /// invasive. Thus, `SyncResponse` doesn't support Sliding Sync yet. Hence - /// the fact this method handles both at the same time. It's not super - /// annoying but it was important to clarify that. + /// `SyncResponse`? We have both because `SyncResponse` doesn't support + /// Sliding Sync yet. #[instrument(skip_all, fields(views = views.len()))] async fn handle_response( &self, From 3b01e4f9a6ab8cd9d54a7acec3ca53c65c6a2729 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 23 Feb 2023 16:39:56 +0100 Subject: [PATCH 036/166] chore(sdk): Use `T::default` instead of `Default::default`. --- crates/matrix-sdk-base/src/sliding_sync.rs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk-base/src/sliding_sync.rs b/crates/matrix-sdk-base/src/sliding_sync.rs index 99cdae04c..5eae74639 100644 --- a/crates/matrix-sdk-base/src/sliding_sync.rs +++ b/crates/matrix-sdk-base/src/sliding_sync.rs @@ -1,12 +1,16 @@ +use std::collections::BTreeMap; #[cfg(feature = "e2e-encryption")] use std::ops::Deref; -use ruma::api::client::sync::sync_events::{ - v3::{self, Ephemeral}, - v4, -}; #[cfg(feature = "e2e-encryption")] use ruma::UserId; +use ruma::{ + api::client::sync::sync_events::{ + v3::{self, Ephemeral}, + v4, DeviceLists, + }, + DeviceKeyAlgorithm, UInt, +}; use tracing::{debug, info, instrument}; use super::BaseClient; @@ -60,8 +64,8 @@ impl BaseClient { // We declare default values that can be referenced hereinbelow. When we try to // extract values from `e2ee`, that would be unfortunate to clone the // value just to pass them (to remove them `e2ee`) as a reference later. - let device_one_time_keys_count = Default::default(); - let device_unused_fallback_key_types = Default::default(); + let device_one_time_keys_count = BTreeMap::::default(); + let device_unused_fallback_key_types = None; let (device_lists, device_one_time_keys_count, device_unused_fallback_key_types) = e2ee .as_ref() @@ -73,7 +77,11 @@ impl BaseClient { ) }) .unwrap_or_else(|| { - (Default::default(), &device_one_time_keys_count, &device_unused_fallback_key_types) + ( + DeviceLists::default(), + &device_one_time_keys_count, + &device_unused_fallback_key_types, + ) }); info!( From fcfdf5c57cde840d80fe82ccb094a8f7347a4609 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 23 Feb 2023 16:09:00 +0100 Subject: [PATCH 037/166] chore(sdk): Rename a variable in `SlidingSync::handle_response`. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 3292ce7ea..7bb5c409f 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -1002,31 +1002,37 @@ impl SlidingSync { let mut rooms = Vec::new(); let mut rooms_map = self.rooms.write().unwrap(); - for (id, mut room_data) in sliding_sync_response.rooms.into_iter() { + for (room_id, mut room_data) in sliding_sync_response.rooms.into_iter() { // `sync_response` contains the rooms with decrypted events if any, so look at // the timeline events here first if the room exists. // Otherwise, let's look at the timeline inside the `sliding_sync_response`. - let timeline = if let Some(joined_room) = sync_response.rooms.join.remove(&id) { + let timeline = if let Some(joined_room) = sync_response.rooms.join.remove(&room_id) + { joined_room.timeline.events } else { room_data.timeline.drain(..).map(Into::into).collect() }; - if let Some(mut room) = rooms_map.remove(&id) { + if let Some(mut room) = rooms_map.remove(&room_id) { // The room existed before, let's update it. room.update(room_data, timeline); - rooms_map.insert(id.clone(), room); + rooms_map.insert(room_id.clone(), room); } else { // First time we need this room, let's create it. rooms_map.insert( - id.clone(), - SlidingSyncRoom::new(self.client.clone(), id.clone(), room_data, timeline), + room_id.clone(), + SlidingSyncRoom::new( + self.client.clone(), + room_id.clone(), + room_data, + timeline, + ), ); } - rooms.push(id); + rooms.push(room_id); } let mut updated_views = Vec::new(); From 23b3e3aa10b2fb4fda8ad5f1727454f6176a3e8a Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 23 Feb 2023 16:34:07 +0100 Subject: [PATCH 038/166] chore(sdk): Format code a little bit. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 7bb5c409f..3a6cdaa32 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -1039,25 +1039,27 @@ impl SlidingSync { for (name, updates) in sliding_sync_response.lists { let Some(generator) = views.get_mut(&name) else { - error!("Response for view {name} - unknown to us. skipping"); + error!("Response for view `{name}` - unknown to us; skipping"); + continue }; + let count: u32 = updates.count.try_into().expect("the list total count convertible into u32"); + if generator.handle_response(count, &updates.ops, &rooms)? { updated_views.push(name.clone()); } } // Update the `to-device` next-batch if found. - if let Some(to_device_since) = - sliding_sync_response.extensions.to_device.map(|t| t.next_batch) - { - self.update_to_device_since(to_device_since) - } + sliding_sync_response + .extensions + .to_device + .map(|to_device| self.update_to_device_since(to_device.next_batch)); - // track the most recently successfully sent extensions (needed for sticky - // semantics) + // Track the most recently successfully sent extensions (needed for sticky + // semantics). if extensions.is_some() { *self.sent_extensions.lock().unwrap() = extensions; } From f803e58e367c7d83c3525ca7a1c94f6ce93e0b84 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 23 Feb 2023 16:53:16 +0100 Subject: [PATCH 039/166] chore(sdk): Keep `to_remove` inside its own scope. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 3a6cdaa32..5fd3da093 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -1077,18 +1077,21 @@ impl SlidingSync { views: &mut BTreeMap, ) -> Result> { let mut requests = BTreeMap::new(); - let mut to_remove = Vec::new(); - for (name, generator) in views.iter_mut() { - if let Some(request) = generator.next() { - requests.insert(name.clone(), request); - } else { - to_remove.push(name.clone()); + { + let mut views_to_remove = Vec::new(); + + for (name, generator) in views.iter_mut() { + if let Some(request) = generator.next() { + requests.insert(name.clone(), request); + } else { + views_to_remove.push(name.clone()); + } } - } - for n in to_remove { - views.remove(&n); + for view_name in views_to_remove { + views.remove(&view_name); + } } if views.is_empty() { From 6465398322853b9848f6ce497de4e5a2fc0e6aa8 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Thu, 23 Feb 2023 16:52:51 +0100 Subject: [PATCH 040/166] feat: expose power_levels.user_can_do on RoomMember --- crates/matrix-sdk-base/src/rooms/members.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-base/src/rooms/members.rs b/crates/matrix-sdk-base/src/rooms/members.rs index e1179997a..86718a731 100644 --- a/crates/matrix-sdk-base/src/rooms/members.rs +++ b/crates/matrix-sdk-base/src/rooms/members.rs @@ -17,7 +17,10 @@ use std::sync::Arc; use ruma::{ events::{ presence::PresenceEvent, - room::{member::MembershipState, power_levels::SyncRoomPowerLevelsEvent}, + room::{ + member::MembershipState, + power_levels::{PowerLevelAction, SyncRoomPowerLevelsEvent}, + }, }, MxcUri, UserId, }; @@ -101,6 +104,15 @@ impl RoomMember { .unwrap_or_else(|| if self.is_room_creator { 100 } else { 0 }) } + /// Whether the given user can do the given action based on the power + /// levels. + pub fn can_do(&self, action: PowerLevelAction) -> bool { + (*self.power_levels) + .as_ref() + .map(|e| e.power_levels().user_can_do(self.user_id(), action)) + .unwrap_or_else(|| self.is_room_creator) + } + /// Is the name that the member uses ambiguous in the room. /// /// A name is considered to be ambiguous if at least one other member shares From e162c99074a921c4d9c02eb8f7325fe3cc5a8cd4 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Thu, 23 Feb 2023 16:53:16 +0100 Subject: [PATCH 041/166] feat: Handy function to update power levels of a room --- crates/matrix-sdk/src/error.rs | 4 +++ crates/matrix-sdk/src/room/joined.rs | 43 ++++++++++++++++++++++++---- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk/src/error.rs b/crates/matrix-sdk/src/error.rs index 687c14867..b73275885 100644 --- a/crates/matrix-sdk/src/error.rs +++ b/crates/matrix-sdk/src/error.rs @@ -171,6 +171,10 @@ pub enum Error { #[error("the queried endpoint requires authentication but was called before logging in")] AuthenticationRequired, + /// This request failed because the local data wasn't sufficient. + #[error("Local cache doesn't contain all necessary data to perform the action.")] + InsufficientData, + /// Attempting to restore a session after the olm-machine has already been /// set up fails #[cfg(feature = "e2e-encryption")] diff --git a/crates/matrix-sdk/src/room/joined.rs b/crates/matrix-sdk/src/room/joined.rs index 96542e882..e1961db4d 100644 --- a/crates/matrix-sdk/src/room/joined.rs +++ b/crates/matrix-sdk/src/room/joined.rs @@ -24,19 +24,22 @@ use ruma::{ }, assign, events::{ - receipt::ReceiptThread, room::message::RoomMessageEventContent, EmptyStateKey, - MessageLikeEventContent, StateEventContent, + receipt::ReceiptThread, + room::{message::RoomMessageEventContent, power_levels::RoomPowerLevelsEventContent}, + EmptyStateKey, MessageLikeEventContent, StateEventContent, }, serde::Raw, - EventId, OwnedEventId, OwnedTransactionId, TransactionId, UserId, + EventId, Int, OwnedEventId, OwnedTransactionId, TransactionId, UserId, }; use serde_json::Value; use tracing::{debug, instrument}; use super::Left; use crate::{ - attachment::AttachmentConfig, error::HttpResult, room::Common, BaseRoom, Client, Result, - RoomType, + attachment::AttachmentConfig, + error::{Error, HttpResult}, + room::Common, + BaseRoom, Client, Result, RoomType, }; #[cfg(feature = "image-proc")] use crate::{ @@ -825,6 +828,36 @@ impl Joined { self.send(RoomMessageEventContent::new(content), config.txn_id.as_deref()).await } + /// Update the power levels of a select set of users of this room. + /// + /// Issue a `power_levels` state event request to the server, changing the + /// given UserId -> Int levels. May fail if the `power_levels` aren't + /// locally known yet or the server rejects the state event update, e.g. + /// because of insufficient permissions. Neither permissions to update + /// nor whether the data might be stale is checked prior to issuing the + /// request. + pub async fn update_power_levels( + &self, + updates: Vec<(&UserId, Int)>, + ) -> Result { + let raw_pl_event = self + .get_state_event_static::() + .await? + .ok_or(Error::InsufficientData)?; + + let mut power_levels = raw_pl_event.deserialize()?.power_levels(); + + for (user_id, new_level) in updates { + if new_level == power_levels.users_default { + power_levels.users.remove(user_id); + } else { + power_levels.users.insert(user_id.to_owned(), new_level); + } + } + + self.send_state_event(RoomPowerLevelsEventContent::from(power_levels)).await + } + /// Send a state event with an empty state key to the homeserver. /// /// For state events with a non-empty state key, see From 32e8cb76b03a3c541796197cb412acd7ab877dc2 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 24 Feb 2023 10:40:50 +0100 Subject: [PATCH 042/166] ci: Remove unused FeatureSet variant --- xtask/src/ci.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs index d2c763651..29e228469 100644 --- a/xtask/src/ci.rs +++ b/xtask/src/ci.rs @@ -61,7 +61,6 @@ enum CiCommand { #[derive(Subcommand, PartialEq, Eq, PartialOrd, Ord)] enum FeatureSet { - Default, NoEncryption, NoSled, NoEncryptionAndSled, From 1783cd77382d93763ddfe9119b5ef3ef098c987c Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 24 Feb 2023 11:49:28 +0100 Subject: [PATCH 043/166] test: Remove unused constant --- crates/matrix-sdk/src/room/timeline/tests/encryption.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/matrix-sdk/src/room/timeline/tests/encryption.rs b/crates/matrix-sdk/src/room/timeline/tests/encryption.rs index daec42f9f..30e56d951 100644 --- a/crates/matrix-sdk/src/room/timeline/tests/encryption.rs +++ b/crates/matrix-sdk/src/room/timeline/tests/encryption.rs @@ -119,7 +119,6 @@ async fn retry_edit_decryption() { W8aUFGxYXuU\n\ -----END MEGOLM SESSION DATA-----"; - const SESSION2_ID: &str = "HSRlM67FgLYl0J0l1luflfGwpnFcLKHnNoRqUuIhQ5Q"; const SESSION2_KEY: &[u8] = b"\ -----BEGIN MEGOLM SESSION DATA-----\n\ AbMgil4w2zS9PcZ25f+vdcBdv0/YVaOg52K49DwCmMUkAAAAChEzP9tvnK3jd0NA+BjFfm0zzHYOiu5EyRK/\ From 70380a6ee4622408734c78681182de4f21bc1158 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 27 Feb 2023 09:30:50 +0100 Subject: [PATCH 044/166] doc(sdk): Improve documentation of `SlidingSync::handle_response`. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 3292ce7ea..5f85b72f5 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -976,10 +976,6 @@ impl SlidingSync { } /// Handle the HTTP response. - /// - /// But which response? `v4::Response`, aka the Sliding Sync response, or - /// `SyncResponse`? We have both because `SyncResponse` doesn't support - /// Sliding Sync yet. #[instrument(skip_all, fields(views = views.len()))] async fn handle_response( &self, @@ -987,6 +983,8 @@ impl SlidingSync { extensions: Option, views: &mut BTreeMap, ) -> Result { + // Handle and transform a Sliding Sync Response to a `SyncResponse`. + // // We may not need the `sync_response` in the future (once `SyncResponse` will // move to Sliding Sync, i.e. to `v4::Response`), but processing the // `sliding_sync_response` is vital, so it must be done somewhere; for now it From f0986643ce73d696256a24f8297c1a9332eb0d76 Mon Sep 17 00:00:00 2001 From: Anderas Date: Mon, 27 Feb 2023 09:59:30 +0000 Subject: [PATCH 045/166] chore(crypto): Log Olm session identifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Damir Jelić --- Cargo.lock | 2 +- Cargo.toml | 2 +- .../src/identities/device.rs | 9 +- crates/matrix-sdk-crypto/src/olm/account.rs | 131 ++++++++++-------- 4 files changed, 85 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c29b9031f..6b022d915 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5654,7 +5654,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vodozemac" version = "0.3.0" -source = "git+https://github.com/matrix-org/vodozemac?rev=12b24e909107c1fac23245376f294eaf48ba186a#12b24e909107c1fac23245376f294eaf48ba186a" +source = "git+https://github.com/matrix-org/vodozemac?rev=fb609ca1e4df5a7a818490ae86ac694119e41e71#fb609ca1e4df5a7a818490ae86ac694119e41e71" dependencies = [ "aes", "arrayvec", diff --git a/Cargo.toml b/Cargo.toml index 3e38fbf58..b5131e157 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ thiserror = "1.0.38" tracing = { version = "0.1.36", default-features = false, features = ["std"] } uniffi = "0.23.0" uniffi_bindgen = "0.23.0" -vodozemac = { git = "https://github.com/matrix-org/vodozemac", rev = "12b24e909107c1fac23245376f294eaf48ba186a" } +vodozemac = { git = "https://github.com/matrix-org/vodozemac", rev = "fb609ca1e4df5a7a818490ae86ac694119e41e71" } zeroize = "1.3.0" # Default release profile, select with `--release` diff --git a/crates/matrix-sdk-crypto/src/identities/device.rs b/crates/matrix-sdk-crypto/src/identities/device.rs index b336559f6..e5ea4899b 100644 --- a/crates/matrix-sdk-crypto/src/identities/device.rs +++ b/crates/matrix-sdk-crypto/src/identities/device.rs @@ -31,7 +31,7 @@ use ruma::{ }; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Value; -use tracing::warn; +use tracing::{trace, warn}; use vodozemac::{olm::SessionConfig, Curve25519PublicKey, Ed25519PublicKey}; use super::{atomic_bool_deserializer, atomic_bool_serializer}; @@ -676,6 +676,13 @@ impl ReadOnlyDevice { let message = session.encrypt(self, event_type, content).await?; + trace!( + user_id = ?self.user_id(), + device_id = ?self.device_id(), + session_id = session.session_id(), + "Successfully encrypted a Megolm session", + ); + Ok((session, message)) } diff --git a/crates/matrix-sdk-crypto/src/olm/account.rs b/crates/matrix-sdk-crypto/src/olm/account.rs index 0dcd10942..07955615f 100644 --- a/crates/matrix-sdk-crypto/src/olm/account.rs +++ b/crates/matrix-sdk-crypto/src/olm/account.rs @@ -36,7 +36,7 @@ use ruma::{ use serde::{Deserialize, Serialize}; use serde_json::{value::RawValue as RawJsonValue, Value}; use sha2::{Digest, Sha256}; -use tracing::{debug, info, trace, warn}; +use tracing::{debug, info, instrument, trace, warn, Span}; use vodozemac::{ olm::{ Account as InnerAccount, AccountPickle, IdentityKeys, OlmMessage, PreKeyMessage, @@ -286,6 +286,7 @@ impl Account { } /// Decrypt an Olm message, creating a new Olm session if possible. + #[instrument(skip(self, message), fields(session_id))] async fn decrypt_olm_message( &self, sender: &UserId, @@ -293,59 +294,72 @@ impl Account { message: &OlmMessage, ) -> OlmResult<(SessionType, DecryptionResult)> { // First try to decrypt using an existing session. - let (session, plaintext) = - if let Some(d) = self.decrypt_with_existing_sessions(sender_key, message).await? { - // Decryption succeeded, de-structure the session/plaintext out of - // the Option. - (SessionType::Existing(d.0), d.1) - } else { - // Decryption failed with every known session, let's try to create a - // new session. - match message { - // A new session can only be created using a pre-key message, - // return with an error if it isn't one. - OlmMessage::Normal(_) => { - warn!( - ?sender_key, - "Failed to decrypt a non-pre-key message with all \ - available sessions", - ); + let (session, plaintext) = if let Some(d) = + self.decrypt_with_existing_sessions(sender_key, message).await? + { + // Decryption succeeded, de-structure the session/plaintext out of + // the Option. + (SessionType::Existing(d.0), d.1) + } else { + // Decryption failed with every known session, let's try to create a + // new session. + match message { + // A new session can only be created using a pre-key message, + // return with an error if it isn't one. + OlmMessage::Normal(_) => { + let session_ids = if let Some(sessions) = + self.store.get_sessions(&sender_key.to_base64()).await? + { + sessions.lock().await.iter().map(|s| s.session_id().to_owned()).collect() + } else { + vec![] + }; - return Err(OlmError::SessionWedged(sender.to_owned(), sender_key)); - } + warn!( + ?session_ids, + "Failed to decrypt a non-pre-key message with all available sessions", + ); - OlmMessage::PreKey(m) => { - // Create the new session. - let result = match self.inner.create_inbound_session(sender_key, m).await { - Ok(r) => r, - Err(e) => { - warn!( - ?sender_key, - session_keys = ?m.session_keys(), - "Failed to create a new Olm session from a \ - pre-key message: {e:?}", - ); - return Err(OlmError::SessionWedged(sender.to_owned(), sender_key)); - } - }; - - // We need to add the new session to the session cache, otherwise - // we might try to create the same session again. - // TODO separate the session cache from the storage so we only add - // it to the cache but don't store it. - let changes = Changes { - account: Some(self.inner.clone()), - sessions: vec![result.session.clone()], - ..Default::default() - }; - self.store.save_changes(changes).await?; - - (SessionType::New(result.session), result.plaintext) - } + return Err(OlmError::SessionWedged(sender.to_owned(), sender_key)); } - }; - trace!(?sender_key, "Successfully decrypted an Olm message"); + OlmMessage::PreKey(m) => { + // Create the new session. + let result = match self.inner.create_inbound_session(sender_key, m).await { + Ok(r) => r, + Err(e) => { + warn!( + session_keys = ?m.session_keys(), + "Failed to create a new Olm session from a pre-key message: {e:?}", + ); + + return Err(OlmError::SessionWedged(sender.to_owned(), sender_key)); + } + }; + + // We need to add the new session to the session cache, otherwise + // we might try to create the same session again. + // TODO: separate the session cache from the storage so we only add + // it to the cache but don't store it. + let changes = Changes { + account: Some(self.inner.clone()), + sessions: vec![result.session.clone()], + ..Default::default() + }; + self.store.save_changes(changes).await?; + + (SessionType::New(result.session), result.plaintext) + } + } + }; + + let session_id = match &session { + SessionType::New(s) => s.session_id(), + SessionType::Existing(s) => s.session_id(), + }; + + Span::current().record("session_id", session_id); + trace!("Successfully decrypted an Olm message"); match self.parse_decrypted_to_device_event(sender, sender_key, plaintext).await { Ok(result) => Ok((session, result)), @@ -368,7 +382,6 @@ impl Account { } warn!( - sender_key = sender_key.to_base64(), error = ?e, "A to-device message was successfully decrypted but \ parsing and checking the event fields failed" @@ -1045,22 +1058,28 @@ impl ReadOnlyAccount { /// /// * `message` - A pre-key Olm message that was sent to us by the other /// account. + #[instrument( + skip_all, + fields( + sender_key = ?their_identity_key, + session_id = message.session_id(), + session_keys = ?message.session_keys(), + ) + )] pub async fn create_inbound_session( &self, their_identity_key: Curve25519PublicKey, message: &PreKeyMessage, ) -> Result { - debug!( - sender_key = ?their_identity_key, - session_keys = ?message.session_keys(), - "Creating a new Olm session from a pre-key message" - ); + debug!("Creating a new Olm session from a pre-key message"); let result = self.inner.lock().await.create_inbound_session(their_identity_key, message)?; let now = SecondsSinceUnixEpoch::now(); let session_id = result.session.session_id(); + trace!(?session_id, "Olm session created successfully"); + let session = Session { user_id: self.user_id.clone(), device_id: self.device_id.clone(), From 98ea3f096b33905b2f6b55fa6c858672df5bba82 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 27 Feb 2023 11:53:15 +0100 Subject: [PATCH 046/166] chore(sdk): Clean up code of `SlidingSync::cache_to_storage`. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 40 +++++++++++++++-------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 5fd3da093..bce751bcb 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -815,32 +815,44 @@ impl From<&SlidingSync> for FrozenSlidingSync { } impl SlidingSync { - async fn cache_to_storage(&self) -> Result<()> { + async fn cache_to_storage(&self) -> Result<(), crate::Error> { let Some(storage_key) = self.storage_key.as_ref() else { return Ok(()) }; trace!(storage_key, "saving to storage for later use"); - let v = serde_json::to_vec(&FrozenSlidingSync::from(self))?; - self.client.store().set_custom_value(storage_key.as_bytes(), v).await?; + + let store = self.client.store(); + + // Write this `SlidingSync` instance, as a `FrozenSlidingSync` instance, inside + // the client store. + store + .set_custom_value( + storage_key.as_bytes(), + serde_json::to_vec(&FrozenSlidingSync::from(self))?, + ) + .await?; + + // Write every `SlidingSyncView` inside the client the store. let frozen_views = { let rooms_lock = self.rooms.read().unwrap(); + self.views .read() .unwrap() .iter() .map(|(name, view)| { - (name.clone(), FrozenSlidingSyncView::freeze(view, &rooms_lock)) + Ok(( + format!("{storage_key}::{name}"), + serde_json::to_vec(&FrozenSlidingSyncView::freeze(view, &rooms_lock))?, + )) }) - .collect::>() + .collect::, crate::Error>>()? }; - for (name, frozen) in frozen_views { - trace!(storage_key, name, "saving to view for later use"); - self.client - .store() - .set_custom_value( - format!("{storage_key}::{name}").as_bytes(), - serde_json::to_vec(&frozen)?, - ) - .await?; // FIXME: parallelize? + + for (storage_key, frozen_view) in frozen_views { + trace!(storage_key, "Saving the frozen Sliding Sync View"); + + store.set_custom_value(storage_key.as_bytes(), frozen_view).await?; } + Ok(()) } From e5c85410a8d09014924001458afae3ad941f091a Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 27 Feb 2023 11:54:48 +0100 Subject: [PATCH 047/166] doc(sdk): Fix documentation, add links etc. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 26 +++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index bce751bcb..d2098f562 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -861,19 +861,16 @@ impl SlidingSync { SlidingSyncBuilder::new() } - /// Generate a new SlidingSyncBuilder with the same inner settings and views - /// but without the current state + /// Generate a new [`SlidingSyncBuilder`] with the same inner settings and + /// views but without the current state. pub fn new_builder_copy(&self) -> SlidingSyncBuilder { let mut builder = Self::builder() .client(self.client.clone()) .subscriptions(self.subscriptions.read().unwrap().to_owned()); - for view in self - .views - .read() - .unwrap() - .values() - .map(|v| v.new_builder().build().expect("builder worked before, builder works now")) - { + + for view in self.views.read().unwrap().values().map(|view| { + view.new_builder().build().expect("builder worked before, builder works now") + }) { builder = builder.add_view(view); } @@ -904,10 +901,11 @@ impl SlidingSync { } } - /// Add the common extensions if not already configured + /// Add the common extensions if not already configured. pub fn add_common_extensions(&self) { let mut lock = self.extensions.lock().unwrap(); 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) })); } @@ -955,7 +953,7 @@ impl SlidingSync { /// /// Note: Remember that this change will only be applicable for any new /// stream created after this. The old stream will still continue to use the - /// previous set of views + /// previous set of views. pub fn pop_view(&self, view_name: &String) -> Option { self.views.write().unwrap().remove(view_name) } @@ -968,7 +966,7 @@ impl SlidingSync { /// /// Note: Remember that this change will only be applicable for any new /// stream created after this. The old stream will still continue to use the - /// previous set of views + /// previous set of views. pub fn add_view(&self, view: SlidingSyncView) -> Option { self.views.write().unwrap().insert(view.name.clone(), view) } @@ -979,6 +977,7 @@ impl SlidingSync { room_ids: I, ) -> Vec> { let rooms = self.rooms.read().unwrap(); + room_ids.map(|room_id| rooms.get(&room_id).cloned()).collect() } @@ -1116,10 +1115,11 @@ impl SlidingSync { let unsubscribe_rooms = mem::take(&mut *self.unsubscribe.write().unwrap()); let timeout = Duration::from_secs(30); - // implement stickiness by only sending extensions if they have + // Implement stickiness by only sending extensions if they have // changed since the last time we sent them let extensions = { let extensions = self.extensions.lock().unwrap(); + if *extensions == *self.sent_extensions.lock().unwrap() { None } else { From 4dbd1a7db4fd7a83a6f5229815dfbc440b61548a Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 27 Feb 2023 11:56:16 +0100 Subject: [PATCH 048/166] fix(sdk): Fix `join` in `SlidingSync::sync_once`. This patch cleans up the `SlidingSync::sync_once` method by renaming variables, adding more comments, simplifying the code by reducing the number of variables etc. This patch also changes `futures_util::join!` to `futures_util::future::join`. It does the same but the macro needs the `async-await-macros` feature to be turned on, while the second works without any features. Finally, this patch improves the log messages by making them more clear for a new reader. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 51 +++++++++++++---------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index d2098f562..4a927a92c 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -1087,14 +1087,14 @@ impl SlidingSync { &self, views: &mut BTreeMap, ) -> Result> { - let mut requests = BTreeMap::new(); + let mut lists_of_requests = BTreeMap::new(); { let mut views_to_remove = Vec::new(); for (name, generator) in views.iter_mut() { if let Some(request) = generator.next() { - requests.insert(name.clone(), request); + lists_of_requests.insert(name.clone(), request); } else { views_to_remove.push(name.clone()); } @@ -1127,40 +1127,49 @@ impl SlidingSync { } }; - let request = assign!(v4::Request::new(), { - lists: requests, - pos, - delta_token, - timeout: Some(timeout), - room_subscriptions, - unsubscribe_rooms, - extensions: extensions.clone().unwrap_or_default(), - }); - debug!("requesting"); + debug!("Sending the sliding sync request"); - // 30s for the long poll + 30s for network delays + // Configure long-polling. We need 30 seconds for the long-poll itself, in + // addition to 30 more extra seconds for the network delays. let request_config = RequestConfig::default().timeout(timeout + Duration::from_secs(30)); + + // Prepare the request. let request = self.client.send_with_homeserver( - request, + assign!(v4::Request::new(), { + lists: lists_of_requests, + pos, + delta_token, + timeout: Some(timeout), + room_subscriptions, + unsubscribe_rooms, + extensions: extensions.clone().unwrap_or_default(), + }), Some(request_config), self.homeserver.as_ref().map(ToString::to_string), ); + // Send the request and get a response with end-to-end encryption support. #[cfg(feature = "e2e-encryption")] let response = { - let (e2ee_uploads, resp) = - futures_util::join!(self.client.send_outgoing_requests(), request); - if let Err(e) = e2ee_uploads { - error!(error = ?e, "Error while sending outgoing E2EE requests"); + let (e2ee_uploads, response) = + futures_util::future::join(self.client.send_outgoing_requests(), request).await; + + if let Err(error) = e2ee_uploads { + error!(?error, "Error while sending outgoing E2EE requests"); } - resp + + response }?; + + // Send the request and get a response _without_ end-to-end encryption support. #[cfg(not(feature = "e2e-encryption"))] let response = request.await?; - debug!("received"); + + debug!("Sliding sync response received"); let updates = self.handle_response(response, extensions, views).await?; - debug!("handled"); + + debug!("Sliding sync response has been handled"); Ok(Some(updates)) } From 16c9845a4784eedf52ca58e3997c0deccfeebb43 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 27 Feb 2023 12:09:14 +0100 Subject: [PATCH 049/166] chore(sdk): Rename `SlidingSync.failure_count` to `.reset_counter`. This patch cleans up the `SlidingSync::stream` method. It renames variables, improves log messages etc. This patch also creates a new `MAXIMUM_SLIDING_SYNC_SESSION_EXPIRATION` constant. This value was previously hardcoded and lost in the code, now it's easier to spot it for further updates. This patch finally renames the `failure_count` field to `reset_counter`, because it doesn't count the number of failure, but the number of `ErrorKind::UnknownPos` exactly, i.e. the number of times we reset the `SlidingSync` state. --- crates/matrix-sdk/src/sliding_sync/builder.rs | 2 +- crates/matrix-sdk/src/sliding_sync/mod.rs | 69 ++++++++++++------- 2 files changed, 46 insertions(+), 25 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/builder.rs b/crates/matrix-sdk/src/sliding_sync/builder.rs index 39a5af3a0..d6cddc8c2 100644 --- a/crates/matrix-sdk/src/sliding_sync/builder.rs +++ b/crates/matrix-sdk/src/sliding_sync/builder.rs @@ -298,7 +298,7 @@ impl SlidingSyncBuilder { extensions: Mutex::new(self.extensions).into(), sent_extensions: Mutex::new(None).into(), - failure_count: Default::default(), + reset_counter: Default::default(), pos: Arc::new(StdRwLock::new(Observable::new(None))), delta_token: Arc::new(StdRwLock::new(Observable::new(delta_token_inner))), diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 4a927a92c..088948e14 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -755,6 +755,15 @@ pub struct UpdateSummary { pub rooms: Vec, } +/// Number of times a Sliding Sync session can expire before raising an error. +/// +/// A Sliding Sync session can expire. In this case, it is reset. However, to +/// avoid entering an infinite loop of “it's expired, let's reset, it's expired, +/// let's reset…” (maybe if the network has an issue, or the server, or anything +/// else), we defined a maximum times a session can expire before +/// raising a proper error. +const MAXIMUM_SLIDING_SYNC_SESSION_EXPIRATION: u8 = 3; + /// The sliding sync instance #[derive(Clone, Debug)] pub struct SlidingSync { @@ -780,8 +789,8 @@ pub struct SlidingSync { subscriptions: Arc>>, unsubscribe: Arc>>, - /// keeping track of retries and failure counts - failure_count: Arc, + /// Number of times a Sliding Session session has been reset. + reset_counter: Arc, /// the intended state of the extensions being supplied to sliding /sync /// calls. May contain the latest next_batch for to_devices, etc. @@ -1174,66 +1183,78 @@ impl SlidingSync { Ok(Some(updates)) } - /// Create the inner stream for the view. + /// Create a _new_ Sliding Sync stream. /// - /// Run this stream to receive new updates from the server. + /// This stream will send requests and will handle responses automatically, + /// hence updating the views. #[instrument(name = "sync_stream", skip_all, parent = &self.client.root_span)] pub fn stream(&self) -> impl Stream> + '_ { + // Collect all the views that needsto be updated. let mut views = { let mut views = BTreeMap::new(); - let views_lock = self.views.read().unwrap(); - for (name, view) in views_lock.iter() { + let lock = self.views.read().unwrap(); + + for (name, view) in lock.iter() { views.insert(name.clone(), view.request_generator()); } + views }; - debug!(?self.extensions, "Setting view stream going"); - let stream_span = Span::current(); + debug!(?self.extensions, "About to run the sync stream"); + + let instrument_span = Span::current(); async_stream::stream! { loop { - let sync_span = info_span!(parent: &stream_span, "sync_once"); + let sync_span = info_span!(parent: &instrument_span, "sync_once"); sync_span.in_scope(|| { - debug!(?self.extensions, "Sync loop running"); + debug!(?self.extensions, "Sync stream loop is running"); }); match self.sync_once(&mut views).instrument(sync_span.clone()).await { Ok(Some(updates)) => { - self.failure_count.store(0, Ordering::SeqCst); + self.reset_counter.store(0, Ordering::SeqCst); - yield Ok(updates) + yield Ok(updates); } Ok(None) => { break; } - Err(e) => { - if e.client_api_error_kind() == Some(&ErrorKind::UnknownPos) { - // session expired, let's reset - if self.failure_count.fetch_add(1, Ordering::SeqCst) >= 3 { - sync_span.in_scope(|| error!("session expired three times in a row")); - yield Err(e.into()); + Err(error) => { + if error.client_api_error_kind() == Some(&ErrorKind::UnknownPos) { + // The session has expired. - break + // Has it expired too many times? + if self.reset_counter.fetch_add(1, Ordering::SeqCst) >= MAXIMUM_SLIDING_SYNC_SESSION_EXPIRATION { + sync_span.in_scope(|| error!("Session expired {MAXIMUM_SLIDING_SYNC_SESSION_EXPIRATION} times in a row")); + + // The session has expired too many times, let's raise an error! + yield Err(error.into()); + + break; } + // Let's reset the Sliding Sync session. sync_span.in_scope(|| { - warn!("Session expired. Restarting sliding sync."); + warn!("Session expired. Restarting Sliding Sync."); + + // To “restart” a Sliding Sync session, we set `pos` to its initial value. Observable::set(&mut self.pos.write().unwrap(), None); - // reset our extensions to the last known good ones. + // We also need to reset our extensions to the last known good ones. *self.extensions.lock().unwrap() = self.sent_extensions.lock().unwrap().take(); - debug!(?self.extensions, "Resetting view stream"); + debug!(?self.extensions, "Sliding Sync has been reset"); }); } - yield Err(e.into()); + yield Err(error.into()); - continue + continue; } } } From f51b51b19e4de42d01542ad25b46d84440ce8bd5 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 27 Feb 2023 12:14:01 +0100 Subject: [PATCH 050/166] chore(sdk): Avoid mapping to a function returning `()`. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 088948e14..005e3d000 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -1072,11 +1072,10 @@ impl SlidingSync { } } - // Update the `to-device` next-batch if found. - sliding_sync_response - .extensions - .to_device - .map(|to_device| self.update_to_device_since(to_device.next_batch)); + // Update the `to-device` next-batch if any. + if let Some(to_device) = sliding_sync_response.extensions.to_device { + self.update_to_device_since(to_device.next_batch); + } // Track the most recently successfully sent extensions (needed for sticky // semantics). From ac1d5afac3bf0ec32dde09e47ddc18201e08899b Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 27 Feb 2023 13:29:03 +0100 Subject: [PATCH 051/166] doc(sdk): Improve inline documentation with link to an issue. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 005e3d000..ba3b6ef6b 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -1157,6 +1157,14 @@ impl SlidingSync { ); // Send the request and get a response with end-to-end encryption support. + // + // Sending the `/sync` request out when end-to-end encryption is enabled means + // that we need to also send out any outgoing e2ee related request out + // coming from the `OlmMachine::outgoing_requests()` method. + // + // FIXME: Processing outgiong requests at the same time while a `/sync` is in + // flight is currently not supported. + // More info: [#1386](https://github.com/matrix-org/matrix-rust-sdk/issues/1386). #[cfg(feature = "e2e-encryption")] let response = { let (e2ee_uploads, response) = From d06e8ccfc5fb55099ab177877d045173d8913403 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 23 Feb 2023 20:13:37 +0100 Subject: [PATCH 052/166] refactor: Upgrade eyeball and use SharedObservable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … but not in sliding sync yet, where some planned refactorings might conflict with this. --- Cargo.lock | 4 +- Cargo.toml | 2 +- crates/matrix-sdk-base/src/store/mod.rs | 10 +-- .../src/verification/qrcode.rs | 44 ++++++------ .../src/verification/requests.rs | 72 ++++++++++--------- .../src/verification/sas/mod.rs | 56 +++++++-------- labs/jack-in/src/app/model.rs | 2 +- labs/jack-in/src/client/mod.rs | 2 +- labs/jack-in/src/client/state.rs | 18 ++--- labs/jack-in/src/components/details.rs | 2 +- 10 files changed, 107 insertions(+), 105 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b022d915..13068671f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1470,9 +1470,9 @@ dependencies = [ [[package]] name = "eyeball" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "705cca477a21fef6fb7359c13ad9008e087787c7a3faca0e2e75507326e15bd7" +checksum = "e7293923c1e4c768a56358441174001e3805a808fc453d3a93b20a5d3ebde347" dependencies = [ "futures-core", "readlock", diff --git a/Cargo.toml b/Cargo.toml index b5131e157..f08b467f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ base64 = "0.21.0" byteorder = "1.4.3" ctor = "0.1.26" dashmap = "5.2.0" -eyeball = "0.1.3" +eyeball = "0.1.4" eyeball-im = "0.1.0" http = "0.2.6" ruma = { version = "0.8.0", features = ["client-api-c"] } diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index 83a72da4a..c921528f1 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -28,10 +28,10 @@ use std::{ pin::Pin, result::Result as StdResult, str::Utf8Error, - sync::{Arc, RwLock as StdRwLock, RwLockReadGuard as StdRwLockReadGuard}, + sync::{Arc, RwLockReadGuard as StdRwLockReadGuard}, }; -use eyeball::Observable; +use eyeball::{Observable, SharedObservable}; use once_cell::sync::OnceCell; #[cfg(any(test, feature = "testing"))] @@ -505,7 +505,7 @@ where pub(crate) struct Store { pub(super) inner: Arc, session_meta: Arc>, - pub(super) session_tokens: Arc>>>, + pub(super) session_tokens: SharedObservable>, /// The current sync token that should be used for the next sync call. pub(super) sync_token: Arc>>, rooms: Arc>, @@ -570,12 +570,12 @@ impl Store { /// The [`SessionTokens`] containing our access token and optional refresh /// token. pub fn session_tokens(&self) -> StdRwLockReadGuard<'_, Observable>> { - self.session_tokens.read().unwrap() + self.session_tokens.read() } /// Set the current [`SessionTokens`]. pub fn set_session_tokens(&self, tokens: SessionTokens) { - Observable::set(&mut self.session_tokens.write().unwrap(), Some(tokens)); + self.session_tokens.set(Some(tokens)); } /// The current [`Session`] containing our user id, device ID, access diff --git a/crates/matrix-sdk-crypto/src/verification/qrcode.rs b/crates/matrix-sdk-crypto/src/verification/qrcode.rs index 3bcdedbc7..721da93ee 100644 --- a/crates/matrix-sdk-crypto/src/verification/qrcode.rs +++ b/crates/matrix-sdk-crypto/src/verification/qrcode.rs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::{Arc, RwLock as StdRwLock}; +use std::sync::Arc; -use eyeball::Observable; +use eyeball::{Observable, SharedObservable}; use futures_core::Stream; use futures_util::StreamExt; use matrix_sdk_qrcode::{ @@ -135,7 +135,7 @@ impl From<&InnerState> for QrVerificationState { pub struct QrVerification { flow_id: FlowId, inner: Arc, - state: Arc>>, + state: SharedObservable, identities: IdentitiesBeingVerified, request_handle: Option, we_started: bool, @@ -157,12 +157,12 @@ impl QrVerification { /// When the verification object is in this state it's required that the /// user confirms that the other side has scanned the QR code. pub fn has_been_scanned(&self) -> bool { - matches!(**self.state.read().unwrap(), InnerState::Scanned(_)) + matches!(**self.state.read(), InnerState::Scanned(_)) } /// Has the scanning of the QR code been confirmed by us. pub fn has_been_confirmed(&self) -> bool { - matches!(**self.state.read().unwrap(), InnerState::Confirmed(_)) + matches!(**self.state.read(), InnerState::Confirmed(_)) } /// Get our own user id. @@ -189,7 +189,7 @@ impl QrVerification { /// Get info about the cancellation if the verification flow has been /// cancelled. pub fn cancel_info(&self) -> Option { - if let InnerState::Cancelled(c) = &**self.state.read().unwrap() { + if let InnerState::Cancelled(c) = &**self.state.read() { Some(c.state.clone().into()) } else { None @@ -198,12 +198,12 @@ impl QrVerification { /// Has the verification flow completed. pub fn is_done(&self) -> bool { - matches!(**self.state.read().unwrap(), InnerState::Done(_)) + matches!(**self.state.read(), InnerState::Done(_)) } /// Has the verification flow been cancelled. pub fn is_cancelled(&self) -> bool { - matches!(**self.state.read().unwrap(), InnerState::Cancelled(_)) + matches!(**self.state.read(), InnerState::Cancelled(_)) } /// Is this a verification that is veryfying one of our own devices @@ -214,7 +214,7 @@ impl QrVerification { /// Have we successfully scanned the QR code and are able to send a /// reciprocation event. pub fn reciprocated(&self) -> bool { - matches!(**self.state.read().unwrap(), InnerState::Reciprocated(_)) + matches!(**self.state.read(), InnerState::Reciprocated(_)) } /// Get the unique ID that identifies this QR code verification flow. @@ -268,7 +268,7 @@ impl QrVerification { /// /// [`cancel()`]: #method.cancel pub fn cancel_with_code(&self, code: CancelCode) -> Option { - let mut state = self.state.write().unwrap(); + let mut state = self.state.write(); if let Some(request) = &self.request_handle { request.cancel_with_code(&code); @@ -296,7 +296,7 @@ impl QrVerification { /// This will return some `OutgoingContent` if the object is in the correct /// state to start the verification flow, otherwise `None`. pub fn reciprocate(&self) -> Option { - match &**self.state.read().unwrap() { + match &**self.state.read() { InnerState::Reciprocated(s) => { Some(self.content_to_request(s.as_content(self.flow_id()))) } @@ -310,7 +310,7 @@ impl QrVerification { /// Confirm that the other side has scanned our QR code. pub fn confirm_scanning(&self) -> Option { - let mut state = self.state.write().unwrap(); + let mut state = self.state.write(); match &**state { InnerState::Scanned(s) => { @@ -366,7 +366,7 @@ impl QrVerification { VerificationResult::SignatureUpload(s) => (None, Some(s)), }; - Observable::set(&mut self.state.write().unwrap(), new_state); + self.state.set(new_state); Ok((content.map(|c| self.content_to_request(c)), request)) } @@ -377,7 +377,7 @@ impl QrVerification { (Option, Option), CryptoStoreError, > { - let state = (*self.state.read().unwrap()).clone(); + let state = self.state.get(); Ok(match state { InnerState::Confirmed(s) => { @@ -430,7 +430,7 @@ impl QrVerification { &self, content: &StartContent<'_>, ) -> Option { - let mut state = self.state.write().unwrap(); + let mut state = self.state.write(); match &**state { InnerState::Created(s) => match s.clone().receive_reciprocate(content) { @@ -454,7 +454,7 @@ impl QrVerification { pub(crate) fn receive_cancel(&self, sender: &UserId, content: &CancelContent<'_>) { if sender == self.other_user_id() { - let mut state = self.state.write().unwrap(); + let mut state = self.state.write(); let new_state = match &**state { InnerState::Created(s) => s.clone().into_cancelled(content), @@ -628,9 +628,9 @@ impl QrVerification { Ok(Self { flow_id, inner: qr_code.into(), - state: Arc::new(StdRwLock::new(Observable::new(InnerState::Reciprocated(QrState { + state: SharedObservable::new(InnerState::Reciprocated(QrState { state: Reciprocated { secret, own_device_id }, - })))), + })), identities, we_started, request_handle, @@ -649,9 +649,9 @@ impl QrVerification { Self { flow_id, inner: inner.into(), - state: Arc::new(StdRwLock::new(Observable::new(InnerState::Created(QrState { + state: SharedObservable::new(InnerState::Created(QrState { state: Created { secret }, - })))), + })), identities, we_started, request_handle, @@ -662,7 +662,7 @@ impl QrVerification { /// /// The changes are presented as a stream of [`QrVerificationState`] values. pub fn changes(&self) -> impl Stream { - Observable::subscribe(&self.state.read().unwrap()).map(|s| (&s).into()) + self.state.subscribe().map(|s| (&s).into()) } /// Get the current state the verification process is in. @@ -670,7 +670,7 @@ impl QrVerification { /// To listen to changes to the [`QrVerificationState`] use the /// [`QrVerification::changes`] method. pub fn state(&self) -> QrVerificationState { - (&**self.state.read().unwrap()).into() + (&**self.state.read()).into() } } diff --git a/crates/matrix-sdk-crypto/src/verification/requests.rs b/crates/matrix-sdk-crypto/src/verification/requests.rs index c15712d79..c2a199139 100644 --- a/crates/matrix-sdk-crypto/src/verification/requests.rs +++ b/crates/matrix-sdk-crypto/src/verification/requests.rs @@ -12,12 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{ - sync::{Arc, RwLock as StdRwLock}, - time::Duration, -}; +use std::{sync::Arc, time::Duration}; -use eyeball::Observable; +use eyeball::{Observable, SharedObservable}; use futures_core::Stream; use futures_util::StreamExt; use matrix_sdk_common::instant::Instant; @@ -139,7 +136,7 @@ pub struct VerificationRequest { account: ReadOnlyAccount, flow_id: Arc, other_user_id: Arc, - inner: Arc>>, + inner: SharedObservable, creation_time: Arc, we_started: bool, recipient_devices: Arc>, @@ -155,20 +152,20 @@ pub struct VerificationRequest { /// `VerificationRequest` object. #[derive(Clone, Debug)] pub(crate) struct RequestHandle { - inner: Arc>>, + inner: SharedObservable, } impl RequestHandle { pub fn cancel_with_code(&self, cancel_code: &CancelCode) { - let mut guard = self.inner.write().unwrap(); + let mut guard = self.inner.write(); if let Some(updated) = guard.cancel(true, cancel_code) { Observable::set(&mut guard, updated); } } } -impl From>>> for RequestHandle { - fn from(inner: Arc>>) -> Self { +impl From> for RequestHandle { + fn from(inner: SharedObservable) -> Self { Self { inner } } } @@ -183,9 +180,13 @@ impl VerificationRequest { methods: Option>, ) -> Self { let account = store.account.clone(); - let inner = Arc::new(StdRwLock::new(Observable::new(InnerRequest::Created( - RequestState::new(cache.clone(), store, other_user, &flow_id, methods), - )))); + let inner = SharedObservable::new(InnerRequest::Created(RequestState::new( + cache.clone(), + store, + other_user, + &flow_id, + methods, + ))); Self { account, @@ -204,7 +205,7 @@ impl VerificationRequest { /// self-verifications and it should be sent to the specific device that we /// want to verify. pub(crate) fn request_to_device(&self) -> ToDeviceRequest { - let inner = self.inner.read().unwrap(); + let inner = self.inner.read(); let methods = if let InnerRequest::Created(c) = &**inner { c.state.our_methods.clone() @@ -262,7 +263,7 @@ impl VerificationRequest { /// The id of the other device that is participating in this verification. pub fn other_device_id(&self) -> Option { - match &**self.inner.read().unwrap() { + match &**self.inner.read() { InnerRequest::Requested(r) => Some(r.state.other_device_id.clone()), InnerRequest::Ready(r) => Some(r.state.other_device_id.clone()), InnerRequest::Created(_) @@ -283,7 +284,7 @@ impl VerificationRequest { /// Get info about the cancellation if the verification request has been /// cancelled. pub fn cancel_info(&self) -> Option { - if let InnerRequest::Cancelled(c) = &**self.inner.read().unwrap() { + if let InnerRequest::Cancelled(c) = &**self.inner.read() { Some(c.state.clone().into()) } else { None @@ -292,12 +293,12 @@ impl VerificationRequest { /// Has the verification request been answered by another device. pub fn is_passive(&self) -> bool { - matches!(**self.inner.read().unwrap(), InnerRequest::Passive(_)) + matches!(**self.inner.read(), InnerRequest::Passive(_)) } /// Is the verification request ready to start a verification flow. pub fn is_ready(&self) -> bool { - matches!(**self.inner.read().unwrap(), InnerRequest::Ready(_)) + matches!(**self.inner.read(), InnerRequest::Ready(_)) } /// Has the verification flow timed out. @@ -310,7 +311,7 @@ impl VerificationRequest { /// Will be present only if the other side requested the verification or if /// we're in the ready state. pub fn their_supported_methods(&self) -> Option> { - match &**self.inner.read().unwrap() { + match &**self.inner.read() { InnerRequest::Requested(r) => Some(r.state.their_methods.clone()), InnerRequest::Ready(r) => Some(r.state.their_methods.clone()), InnerRequest::Created(_) @@ -325,7 +326,7 @@ impl VerificationRequest { /// Will be present only we requested the verification or if we're in the /// ready state. pub fn our_supported_methods(&self) -> Option> { - match &**self.inner.read().unwrap() { + match &**self.inner.read() { InnerRequest::Created(r) => Some(r.state.our_methods.clone()), InnerRequest::Ready(r) => Some(r.state.our_methods.clone()), InnerRequest::Requested(_) @@ -352,20 +353,20 @@ impl VerificationRequest { /// Has the verification flow that was started with this request finished. pub fn is_done(&self) -> bool { - matches!(**self.inner.read().unwrap(), InnerRequest::Done(_)) + matches!(**self.inner.read(), InnerRequest::Done(_)) } /// Has the verification flow that was started with this request been /// cancelled. pub fn is_cancelled(&self) -> bool { - matches!(**self.inner.read().unwrap(), InnerRequest::Cancelled(_)) + matches!(**self.inner.read(), InnerRequest::Cancelled(_)) } /// Generate a QR code that can be used by another client to start a QR code /// based verification. #[cfg(feature = "qrcode")] pub async fn generate_qr_code(&self) -> Result, CryptoStoreError> { - let inner = self.inner.read().unwrap().clone(); + let inner = self.inner.get(); inner.generate_qr_code(self.we_started, self.inner.clone().into()).await } @@ -382,7 +383,7 @@ impl VerificationRequest { &self, data: QrVerificationData, ) -> Result, ScanError> { - let future = if let InnerRequest::Ready(r) = &**self.inner.read().unwrap() { + let future = if let InnerRequest::Ready(r) = &**self.inner.read() { QrVerification::from_scan( r.store.clone(), r.other_user_id.clone(), @@ -396,6 +397,7 @@ impl VerificationRequest { return Ok(None); }; + // await future after self.inner read guard is released let qr_verification = future.await?; // We may have previously started our own QR verification (e.g. two devices @@ -435,9 +437,9 @@ impl VerificationRequest { Self { verification_cache: cache.clone(), - inner: Arc::new(StdRwLock::new(Observable::new(InnerRequest::Requested( + inner: SharedObservable::new(InnerRequest::Requested( RequestState::from_request_event(cache, store, sender, &flow_id, content), - )))), + )), account, other_user_id: sender.into(), flow_id: flow_id.into(), @@ -457,7 +459,7 @@ impl VerificationRequest { &self, methods: Vec, ) -> Option { - let mut guard = self.inner.write().unwrap(); + let mut guard = self.inner.write(); let Some((updated, content)) = guard.accept(methods) else { return None; @@ -503,7 +505,7 @@ impl VerificationRequest { } fn cancel_with_code(&self, cancel_code: CancelCode) -> Option { - let mut guard = self.inner.write().unwrap(); + let mut guard = self.inner.write(); let send_to_everyone = self.we_started() && matches!(**guard, InnerRequest::Created(_)); let other_device = guard.other_device_id(); @@ -628,7 +630,7 @@ impl VerificationRequest { } pub(crate) fn receive_ready(&self, sender: &UserId, content: &ReadyContent<'_>) { - let mut guard = self.inner.write().unwrap(); + let mut guard = self.inner.write(); match &**guard { InnerRequest::Created(s) => { @@ -660,7 +662,7 @@ impl VerificationRequest { sender: &UserId, content: &StartContent<'_>, ) -> Result<(), CryptoStoreError> { - let inner = self.inner.read().unwrap().clone(); + let inner = self.inner.get(); let InnerRequest::Ready(s) = inner else { warn!( @@ -682,7 +684,7 @@ impl VerificationRequest { "Marking a verification request as done" ); - let mut guard = self.inner.write().unwrap(); + let mut guard = self.inner.write(); if let Some(updated) = guard.receive_done(content) { Observable::set(&mut guard, updated); } @@ -699,7 +701,7 @@ impl VerificationRequest { code = content.cancel_code().as_str(), "Cancelling a verification request, other user has cancelled" ); - let mut guard = self.inner.write().unwrap(); + let mut guard = self.inner.write(); if let Some(updated) = guard.cancel(false, content.cancel_code()) { Observable::set(&mut guard, updated); } @@ -717,7 +719,7 @@ impl VerificationRequest { pub async fn start_sas( &self, ) -> Result, CryptoStoreError> { - let inner = self.inner.read().unwrap().clone(); + let inner = self.inner.get(); Ok(match &inner { InnerRequest::Ready(s) => { @@ -771,7 +773,7 @@ impl VerificationRequest { /// The changes are presented as a stream of [`VerificationRequestState`] /// values. pub fn changes(&self) -> impl Stream { - Observable::subscribe(&self.inner.read().unwrap()).map(|s| (&s).into()) + self.inner.subscribe().map(|s| (&s).into()) } /// Get the current state the verification request is in. @@ -779,7 +781,7 @@ impl VerificationRequest { /// To listen to changes to the [`VerificationRequestState`] use the /// [`VerificationRequest::changes`] method. pub fn state(&self) -> VerificationRequestState { - (&**self.inner.read().unwrap()).into() + (&**self.inner.read()).into() } } diff --git a/crates/matrix-sdk-crypto/src/verification/sas/mod.rs b/crates/matrix-sdk-crypto/src/verification/sas/mod.rs index 311bd8419..87f4717f8 100644 --- a/crates/matrix-sdk-crypto/src/verification/sas/mod.rs +++ b/crates/matrix-sdk-crypto/src/verification/sas/mod.rs @@ -16,9 +16,9 @@ mod helpers; mod inner_sas; mod sas_state; -use std::sync::{Arc, RwLock as StdRwLock}; +use std::sync::Arc; -use eyeball::Observable; +use eyeball::{Observable, SharedObservable}; use futures_core::Stream; use futures_util::StreamExt; use inner_sas::InnerSas; @@ -49,7 +49,7 @@ use crate::{ /// Short authentication string object. #[derive(Clone, Debug)] pub struct Sas { - inner: Arc>>, + inner: SharedObservable, account: ReadOnlyAccount, identities_being_verified: IdentitiesBeingVerified, flow_id: Arc, @@ -268,12 +268,12 @@ impl Sas { /// Does this verification flow support displaying emoji for the short /// authentication string. pub fn supports_emoji(&self) -> bool { - self.inner.read().unwrap().supports_emoji() + self.inner.read().supports_emoji() } /// Did this verification flow start from a verification request. pub fn started_from_request(&self) -> bool { - self.inner.read().unwrap().started_from_request() + self.inner.read().started_from_request() } /// Is this a verification that is veryfying one of our own devices. @@ -283,18 +283,18 @@ impl Sas { /// Have we confirmed that the short auth string matches. pub fn have_we_confirmed(&self) -> bool { - self.inner.read().unwrap().have_we_confirmed() + self.inner.read().have_we_confirmed() } /// Has the verification been accepted by both parties. pub fn has_been_accepted(&self) -> bool { - self.inner.read().unwrap().has_been_accepted() + self.inner.read().has_been_accepted() } /// Get info about the cancellation if the verification flow has been /// cancelled. pub fn cancel_info(&self) -> Option { - if let InnerSas::Cancelled(c) = &**self.inner.read().unwrap() { + if let InnerSas::Cancelled(c) = &**self.inner.read() { Some(c.state.as_ref().clone().into()) } else { None @@ -309,7 +309,7 @@ impl Sas { #[cfg(test)] #[allow(dead_code)] pub(crate) fn set_creation_time(&self, time: matrix_sdk_common::instant::Instant) { - Observable::update(&mut self.inner.write().unwrap(), |inner| { + self.inner.update(|inner| { inner.set_creation_time(time); }); } @@ -333,7 +333,7 @@ impl Sas { ( Sas { - inner: Arc::new(StdRwLock::new(Observable::new(inner))), + inner: SharedObservable::new(inner), account, identities_being_verified: identities, flow_id: flow_id.into(), @@ -417,7 +417,7 @@ impl Sas { let account = identities.store.account.clone(); Ok(Sas { - inner: Arc::new(StdRwLock::new(Observable::new(inner))), + inner: SharedObservable::new(inner), account, identities_being_verified: identities, flow_id: flow_id.into(), @@ -447,7 +447,7 @@ impl Sas { let old_state = self.state_debug(); let request = { - let mut guard = self.inner.write().unwrap(); + let mut guard = self.inner.write(); let sas: InnerSas = (*guard).clone(); let methods = settings.allowed_methods; @@ -495,7 +495,7 @@ impl Sas { ) -> Result<(Vec, Option), CryptoStoreError> { let (contents, done) = { - let mut guard = self.inner.write().unwrap(); + let mut guard = self.inner.write(); let sas: InnerSas = (*guard).clone(); let (sas, contents) = sas.confirm(); @@ -567,7 +567,7 @@ impl Sas { /// [`cancel()`]: #method.cancel pub fn cancel_with_code(&self, code: CancelCode) -> Option { let content = { - let mut guard = self.inner.write().unwrap(); + let mut guard = self.inner.write(); if let Some(request) = &self.request_handle { request.cancel_with_code(&code); @@ -600,22 +600,22 @@ impl Sas { /// Has the SAS verification flow timed out. pub fn timed_out(&self) -> bool { - self.inner.read().unwrap().timed_out() + self.inner.read().timed_out() } /// Are we in a state where we can show the short auth string. pub fn can_be_presented(&self) -> bool { - self.inner.read().unwrap().can_be_presented() + self.inner.read().can_be_presented() } /// Is the SAS flow done. pub fn is_done(&self) -> bool { - self.inner.read().unwrap().is_done() + self.inner.read().is_done() } /// Is the SAS flow canceled. pub fn is_cancelled(&self) -> bool { - self.inner.read().unwrap().is_cancelled() + self.inner.read().is_cancelled() } /// Get the emoji version of the short auth string. @@ -623,7 +623,7 @@ impl Sas { /// Returns None if we can't yet present the short auth string, otherwise /// seven tuples containing the emoji and description. pub fn emoji(&self) -> Option<[Emoji; 7]> { - self.inner.read().unwrap().emoji() + self.inner.read().emoji() } /// Get the index of the emoji representing the short auth string @@ -633,7 +633,7 @@ impl Sas { /// converted to an emoji using the /// [relevant spec entry](https://spec.matrix.org/unstable/client-server-api/#sas-method-emoji). pub fn emoji_index(&self) -> Option<[u8; 7]> { - self.inner.read().unwrap().emoji_index() + self.inner.read().emoji_index() } /// Get the decimal version of the short auth string. @@ -642,7 +642,7 @@ impl Sas { /// tuple containing three 4-digit integers that represent the short auth /// string. pub fn decimals(&self) -> Option<(u16, u16, u16)> { - self.inner.read().unwrap().decimals() + self.inner.read().decimals() } /// Listen for changes in the SAS verification process. @@ -734,16 +734,16 @@ impl Sas { /// # anyhow::Ok(()) }); /// ``` pub fn changes(&self) -> impl Stream { - Observable::subscribe(&self.inner.read().unwrap()).map(|s| (&s).into()) + self.inner.subscribe().map(|s| (&s).into()) } /// Get the current state of the verification process. pub fn state(&self) -> SasState { - (&**self.inner.read().unwrap()).into() + (&**self.inner.read()).into() } fn state_debug(&self) -> State { - (&**self.inner.read().unwrap()).into() + (&**self.inner.read()).into() } pub(crate) fn receive_any_event( @@ -754,7 +754,7 @@ impl Sas { let old_state = self.state_debug(); let content = { - let mut guard = self.inner.write().unwrap(); + let mut guard = self.inner.write(); let sas: InnerSas = (*guard).clone(); let (sas, content) = sas.receive_any_event(sender, content); @@ -778,7 +778,7 @@ impl Sas { let old_state = self.state_debug(); { - let mut guard = self.inner.write().unwrap(); + let mut guard = self.inner.write(); let sas: InnerSas = (*guard).clone(); @@ -805,11 +805,11 @@ impl Sas { } pub(crate) fn verified_devices(&self) -> Option> { - self.inner.read().unwrap().verified_devices() + self.inner.read().verified_devices() } pub(crate) fn verified_identities(&self) -> Option> { - self.inner.read().unwrap().verified_identities() + self.inner.read().verified_identities() } pub(crate) fn content_to_request(&self, content: AnyToDeviceEventContent) -> ToDeviceRequest { diff --git a/labs/jack-in/src/app/model.rs b/labs/jack-in/src/app/model.rs index 26386e16d..1c626db60 100644 --- a/labs/jack-in/src/app/model.rs +++ b/labs/jack-in/src/app/model.rs @@ -212,7 +212,7 @@ impl Update for Model { None } Msg::SendMessage(m) => { - if let Some(tl) = &**self.sliding_sync.room_timeline.read().unwrap() { + if let Some(tl) = &**self.sliding_sync.room_timeline.read() { block_on(async move { // fire and forget tl.send(RoomMessageEventContent::text_plain(m).into(), None).await; diff --git a/labs/jack-in/src/client/mod.rs b/labs/jack-in/src/client/mod.rs index 485166558..a66d58d97 100644 --- a/labs/jack-in/src/client/mod.rs +++ b/labs/jack-in/src/client/mod.rs @@ -95,7 +95,7 @@ pub async fn run_client( while let Some(update) = stream.next().await { { - let selected_room = ssync_state.selected_room.read().unwrap().clone(); + let selected_room = ssync_state.selected_room.get(); if let Some(room_id) = selected_room { if let Some(prev) = &prev_selected_room { if prev != &room_id { diff --git a/labs/jack-in/src/client/state.rs b/labs/jack-in/src/client/state.rs index 3ed799d55..4eb41da34 100644 --- a/labs/jack-in/src/client/state.rs +++ b/labs/jack-in/src/client/state.rs @@ -3,7 +3,7 @@ use std::{ time::{Duration, Instant}, }; -use eyeball::Observable; +use eyeball::SharedObservable; use eyeball_im::{ObservableVector, VectorDiff}; use futures::{pin_mut, StreamExt}; use matrix_sdk::{ @@ -29,10 +29,10 @@ pub struct SlidingSyncState { first_render: Option, full_sync: Option, current_state: ViewState, - tl_handle: Arc>>>>, - pub selected_room: Arc>>>, + tl_handle: SharedObservable>>, + pub selected_room: SharedObservable>, pub current_timeline: Arc>>>, - pub room_timeline: Arc>>>, + pub room_timeline: SharedObservable>, } impl SlidingSyncState { @@ -56,12 +56,12 @@ impl SlidingSyncState { } pub fn has_selected_room(&self) -> bool { - self.selected_room.read().unwrap().is_some() + self.selected_room.read().is_some() } pub fn select_room(&self, r: Option) { self.current_timeline.write().unwrap().clear(); - if let Some(c) = Observable::replace(&mut self.tl_handle.write().unwrap(), None) { + if let Some(c) = self.tl_handle.take() { c.abort(); } if let Some(room) = r.as_ref().and_then(|room_id| self.get_room(room_id)) { @@ -70,7 +70,7 @@ impl SlidingSyncState { let handle = tokio::spawn(async move { let timeline = room.timeline().await.unwrap(); let (items, listener) = timeline.subscribe().await; - Observable::set(&mut room_timeline.write().unwrap(), Some(timeline)); + room_timeline.set(Some(timeline)); { let mut lock = current_timeline.write().unwrap(); lock.clear(); @@ -114,9 +114,9 @@ impl SlidingSyncState { } } }); - Observable::set(&mut self.tl_handle.write().unwrap(), Some(handle)); + self.tl_handle.set(Some(handle)); } - Observable::set(&mut self.selected_room.write().unwrap(), r); + self.selected_room.set(r); } pub fn time_to_first_render(&self) -> Option { diff --git a/labs/jack-in/src/components/details.rs b/labs/jack-in/src/components/details.rs index 2f0663c27..351a31c99 100644 --- a/labs/jack-in/src/components/details.rs +++ b/labs/jack-in/src/components/details.rs @@ -46,7 +46,7 @@ impl Details { } pub fn refresh_data(&mut self) { - let Some(room_id) = self.sstate.selected_room.read().unwrap().clone() else { return }; + let Some(room_id) = self.sstate.selected_room.get() else { return }; let Some(room_data) = self.sstate.get_room(&room_id) else { return; }; From 6cec9464015750f9ca8cfb4417b508abe10d65d3 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 27 Feb 2023 13:36:08 +0100 Subject: [PATCH 053/166] chore(sdk): Improve documentation of `SlidingSync`. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index ba3b6ef6b..1d7b2f312 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -760,7 +760,7 @@ pub struct UpdateSummary { /// A Sliding Sync session can expire. In this case, it is reset. However, to /// avoid entering an infinite loop of “it's expired, let's reset, it's expired, /// let's reset…” (maybe if the network has an issue, or the server, or anything -/// else), we defined a maximum times a session can expire before +/// else), we define a maximum times a session can expire before /// raising a proper error. const MAXIMUM_SLIDING_SYNC_SESSION_EXPIRATION: u8 = 3; @@ -770,6 +770,7 @@ pub struct SlidingSync { /// Customize the homeserver for sliding sync only homeserver: Option, + /// The HTTP Matrix client. client: Client, /// The storage key to keep this cache at and load it from @@ -826,7 +827,7 @@ impl From<&SlidingSync> for FrozenSlidingSync { impl SlidingSync { async fn cache_to_storage(&self) -> Result<(), crate::Error> { let Some(storage_key) = self.storage_key.as_ref() else { return Ok(()) }; - trace!(storage_key, "saving to storage for later use"); + trace!(storage_key, "Saving to storage for later use"); let store = self.client.store(); From 49eee146658f3f9fffdea8368b81b88bbdbc930d Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 27 Feb 2023 13:38:09 +0100 Subject: [PATCH 054/166] chore(sdk): Move `SlidingSyncState` into the `sliding_sync::view` module. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 22 ------------------ crates/matrix-sdk/src/sliding_sync/view.rs | 26 +++++++++++++++++++--- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 1d7b2f312..d26526d00 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -665,28 +665,6 @@ pub enum Error { BuildMissingField(&'static str), } -/// The state the [`SlidingSyncView`] is in. -/// -/// The lifetime of a SlidingSync usually starts at a `Preload`, getting a fast -/// response for the first given number of Rooms, then switches into -/// `CatchingUp` during which the view fetches the remaining rooms, usually in -/// order, some times in batches. Once that is ready, it switches into `Live`. -/// -/// If the client has been offline for a while, though, the SlidingSync might -/// return back to `CatchingUp` at any point. -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum SlidingSyncState { - /// Hasn't started yet - #[default] - Cold, - /// We are quickly preloading a preview of the most important rooms - Preload, - /// We are trying to load all remaining rooms, might be in batches - CatchingUp, - /// We are all caught up and now only sync the live responses. - Live, -} - /// The mode by which the the [`SlidingSyncView`] is in fetching the data. #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum SlidingSyncMode { diff --git a/crates/matrix-sdk/src/sliding_sync/view.rs b/crates/matrix-sdk/src/sliding_sync/view.rs index 5caafdffc..3426193a5 100644 --- a/crates/matrix-sdk/src/sliding_sync/view.rs +++ b/crates/matrix-sdk/src/sliding_sync/view.rs @@ -18,9 +18,7 @@ use ruma::{ use serde::{Deserialize, Serialize}; use tracing::{debug, error, instrument, trace, warn}; -use super::{ - Error, FrozenSlidingSyncRoom, RoomListEntry, SlidingSyncMode, SlidingSyncRoom, SlidingSyncState, -}; +use super::{Error, FrozenSlidingSyncRoom, RoomListEntry, SlidingSyncMode, SlidingSyncRoom}; use crate::Result; /// Holding a specific filtered view within the concept of sliding sync. @@ -906,3 +904,25 @@ fn room_ops( Ok(()) } + +/// The state the [`SlidingSyncView`] is in. +/// +/// The lifetime of a SlidingSync usually starts at a `Preload`, getting a fast +/// response for the first given number of Rooms, then switches into +/// `CatchingUp` during which the view fetches the remaining rooms, usually in +/// order, some times in batches. Once that is ready, it switches into `Live`. +/// +/// If the client has been offline for a while, though, the SlidingSync might +/// return back to `CatchingUp` at any point. +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum SlidingSyncState { + /// Hasn't started yet + #[default] + Cold, + /// We are quickly preloading a preview of the most important rooms + Preload, + /// We are trying to load all remaining rooms, might be in batches + CatchingUp, + /// We are all caught up and now only sync the live responses. + Live, +} From b1c9ea9aa32171ad7009c5d65c5d37a75ee0e1db Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 27 Feb 2023 13:38:54 +0100 Subject: [PATCH 055/166] chore(sdk): Move `SlidingSyncMode` into the `sliding_sync::view` module. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 16 ---------------- crates/matrix-sdk/src/sliding_sync/view.rs | 18 +++++++++++++++++- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index d26526d00..33a8b5130 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -665,22 +665,6 @@ pub enum Error { BuildMissingField(&'static str), } -/// The mode by which the the [`SlidingSyncView`] is in fetching the data. -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum SlidingSyncMode { - /// Fully sync all rooms in the background, page by page of `batch_size`, - /// like `0..20`, `21..40`, 41..60` etc. assuming the `batch_size` is 20. - #[serde(alias = "FullSync")] - PagingFullSync, - /// Fully sync all rooms in the background, with a growing window of - /// `batch_size`, like `0..20`, `0..40`, `0..60` etc. assuming the - /// `batch_size` is 20. - GrowingFullSync, - /// Only sync the specific windows defined - #[default] - Selective, -} - /// The Entry in the sliding sync room list per sliding sync view #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub enum RoomListEntry { diff --git a/crates/matrix-sdk/src/sliding_sync/view.rs b/crates/matrix-sdk/src/sliding_sync/view.rs index 3426193a5..29037fbd7 100644 --- a/crates/matrix-sdk/src/sliding_sync/view.rs +++ b/crates/matrix-sdk/src/sliding_sync/view.rs @@ -18,7 +18,7 @@ use ruma::{ use serde::{Deserialize, Serialize}; use tracing::{debug, error, instrument, trace, warn}; -use super::{Error, FrozenSlidingSyncRoom, RoomListEntry, SlidingSyncMode, SlidingSyncRoom}; +use super::{Error, FrozenSlidingSyncRoom, RoomListEntry, SlidingSyncRoom}; use crate::Result; /// Holding a specific filtered view within the concept of sliding sync. @@ -926,3 +926,19 @@ pub enum SlidingSyncState { /// We are all caught up and now only sync the live responses. Live, } + +/// The mode by which the the [`SlidingSyncView`] is in fetching the data. +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum SlidingSyncMode { + /// Fully sync all rooms in the background, page by page of `batch_size`, + /// like `0..20`, `21..40`, 41..60` etc. assuming the `batch_size` is 20. + #[serde(alias = "FullSync")] + PagingFullSync, + /// Fully sync all rooms in the background, with a growing window of + /// `batch_size`, like `0..20`, `0..40`, `0..60` etc. assuming the + /// `batch_size` is 20. + GrowingFullSync, + /// Only sync the specific windows defined + #[default] + Selective, +} From 4d8c3172a4caa66d741b4e2f62ca6584e98925d2 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 27 Feb 2023 13:42:01 +0100 Subject: [PATCH 056/166] chore(sdk): Extract `Error` into its own module. --- crates/matrix-sdk/src/sliding_sync/error.rs | 18 ++++++++++++++++++ crates/matrix-sdk/src/sliding_sync/mod.rs | 18 ++---------------- 2 files changed, 20 insertions(+), 16 deletions(-) create mode 100644 crates/matrix-sdk/src/sliding_sync/error.rs diff --git a/crates/matrix-sdk/src/sliding_sync/error.rs b/crates/matrix-sdk/src/sliding_sync/error.rs new file mode 100644 index 000000000..658e779ea --- /dev/null +++ b/crates/matrix-sdk/src/sliding_sync/error.rs @@ -0,0 +1,18 @@ +//! Sliding Sync errors. + +use thiserror::Error; + +/// Internal representation of errors in Sliding Sync. +#[derive(Error, Debug)] +#[non_exhaustive] +pub enum Error { + /// The response we've received from the server can't be parsed or doesn't + /// match up with the current expectations on the client side. A + /// `sync`-restart might be required. + #[error("The sliding sync response could not be handled: {0}")] + BadResponse(String), + /// Called `.build()` on a builder type, but the given required field was + /// missing. + #[error("Required field missing: `{0}`")] + BuildMissingField(&'static str), +} diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 33a8b5130..1d12d6dc4 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -614,6 +614,7 @@ mod builder; mod client; +mod error; mod room; mod view; @@ -630,6 +631,7 @@ use std::{ pub use builder::*; pub use client::*; +pub use error::*; use eyeball::Observable; use futures_core::stream::Stream; pub use room::*; @@ -643,28 +645,12 @@ use ruma::{ assign, OwnedRoomId, RoomId, }; use serde::{Deserialize, Serialize}; -use thiserror::Error; use tracing::{debug, error, info_span, instrument, trace, warn, Instrument, Span}; use url::Url; pub use view::*; use crate::{config::RequestConfig, Client, Result}; -/// Internal representation of errors in Sliding Sync -#[derive(Error, Debug)] -#[non_exhaustive] -pub enum Error { - /// The response we've received from the server can't be parsed or doesn't - /// match up with the current expectations on the client side. A - /// `sync`-restart might be required. - #[error("The sliding sync response could not be handled: {0}")] - BadResponse(String), - /// Called `.build()` on a builder type, but the given required field was - /// missing. - #[error("Required field missing: `{0}`")] - BuildMissingField(&'static str), -} - /// The Entry in the sliding sync room list per sliding sync view #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub enum RoomListEntry { From 9b062fa782e7d83bfe891b97c93957c607805cf3 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 27 Feb 2023 13:43:40 +0100 Subject: [PATCH 057/166] chore(sdk): Move `RoomListEntry` into the `sliding_sync::view` module. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 43 --------------------- crates/matrix-sdk/src/sliding_sync/view.rs | 45 +++++++++++++++++++++- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 1d12d6dc4..9574c9672 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -651,49 +651,6 @@ pub use view::*; use crate::{config::RequestConfig, Client, Result}; -/// The Entry in the sliding sync room list per sliding sync view -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub enum RoomListEntry { - /// This entry isn't known at this point and thus considered `Empty` - #[default] - Empty, - /// There was `OwnedRoomId` but since the server told us to invalid this - /// entry. it is considered stale - Invalidated(OwnedRoomId), - /// This Entry is followed with `OwnedRoomId` - Filled(OwnedRoomId), -} - -impl RoomListEntry { - /// Is this entry empty or invalidated? - pub fn empty_or_invalidated(&self) -> bool { - matches!(self, RoomListEntry::Empty | RoomListEntry::Invalidated(_)) - } - - /// The inner room_id if given - pub fn as_room_id(&self) -> Option<&RoomId> { - match &self { - RoomListEntry::Empty => None, - RoomListEntry::Invalidated(b) | RoomListEntry::Filled(b) => Some(b.as_ref()), - } - } - - fn freeze(&self) -> RoomListEntry { - match &self { - RoomListEntry::Empty => RoomListEntry::Empty, - RoomListEntry::Invalidated(b) | RoomListEntry::Filled(b) => { - RoomListEntry::Invalidated(b.clone()) - } - } - } -} - -impl<'a> From<&'a RoomListEntry> for RoomListEntry { - fn from(value: &'a RoomListEntry) -> Self { - value.clone() - } -} - /// The Summary of a new SlidingSync Update received #[derive(Debug, Clone)] pub struct UpdateSummary { diff --git a/crates/matrix-sdk/src/sliding_sync/view.rs b/crates/matrix-sdk/src/sliding_sync/view.rs index 29037fbd7..143e2c4d0 100644 --- a/crates/matrix-sdk/src/sliding_sync/view.rs +++ b/crates/matrix-sdk/src/sliding_sync/view.rs @@ -18,7 +18,7 @@ use ruma::{ use serde::{Deserialize, Serialize}; use tracing::{debug, error, instrument, trace, warn}; -use super::{Error, FrozenSlidingSyncRoom, RoomListEntry, SlidingSyncRoom}; +use super::{Error, FrozenSlidingSyncRoom, SlidingSyncRoom}; use crate::Result; /// Holding a specific filtered view within the concept of sliding sync. @@ -942,3 +942,46 @@ pub enum SlidingSyncMode { #[default] Selective, } + +/// The Entry in the sliding sync room list per sliding sync view +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub enum RoomListEntry { + /// This entry isn't known at this point and thus considered `Empty` + #[default] + Empty, + /// There was `OwnedRoomId` but since the server told us to invalid this + /// entry. it is considered stale + Invalidated(OwnedRoomId), + /// This Entry is followed with `OwnedRoomId` + Filled(OwnedRoomId), +} + +impl RoomListEntry { + /// Is this entry empty or invalidated? + pub fn empty_or_invalidated(&self) -> bool { + matches!(self, RoomListEntry::Empty | RoomListEntry::Invalidated(_)) + } + + /// The inner room_id if given + pub fn as_room_id(&self) -> Option<&RoomId> { + match &self { + RoomListEntry::Empty => None, + RoomListEntry::Invalidated(b) | RoomListEntry::Filled(b) => Some(b.as_ref()), + } + } + + fn freeze(&self) -> RoomListEntry { + match &self { + RoomListEntry::Empty => RoomListEntry::Empty, + RoomListEntry::Invalidated(b) | RoomListEntry::Filled(b) => { + RoomListEntry::Invalidated(b.clone()) + } + } + } +} + +impl<'a> From<&'a RoomListEntry> for RoomListEntry { + fn from(value: &'a RoomListEntry) -> Self { + value.clone() + } +} From 4f7186a8402f86278c25d7a5d3dac29aa0718a67 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 27 Feb 2023 13:46:35 +0100 Subject: [PATCH 058/166] chore(sdk): Simplify the implementation of `RoomListEntry`. --- crates/matrix-sdk/src/sliding_sync/view.rs | 26 ++++++++++++---------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/view.rs b/crates/matrix-sdk/src/sliding_sync/view.rs index 143e2c4d0..ae60271e5 100644 --- a/crates/matrix-sdk/src/sliding_sync/view.rs +++ b/crates/matrix-sdk/src/sliding_sync/view.rs @@ -943,38 +943,40 @@ pub enum SlidingSyncMode { Selective, } -/// The Entry in the sliding sync room list per sliding sync view +/// The Entry in the Sliding Sync room list per Sliding Sync view. #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub enum RoomListEntry { - /// This entry isn't known at this point and thus considered `Empty` + /// This entry isn't known at this point and thus considered `Empty`. #[default] Empty, /// There was `OwnedRoomId` but since the server told us to invalid this - /// entry. it is considered stale + /// entry. it is considered stale. Invalidated(OwnedRoomId), - /// This Entry is followed with `OwnedRoomId` + /// This entry is followed with `OwnedRoomId`. Filled(OwnedRoomId), } impl RoomListEntry { /// Is this entry empty or invalidated? pub fn empty_or_invalidated(&self) -> bool { - matches!(self, RoomListEntry::Empty | RoomListEntry::Invalidated(_)) + matches!(self, Self::Empty | Self::Invalidated(_)) } - /// The inner room_id if given + /// Return the inner `room_id` if the entry' state is not empty. pub fn as_room_id(&self) -> Option<&RoomId> { match &self { - RoomListEntry::Empty => None, - RoomListEntry::Invalidated(b) | RoomListEntry::Filled(b) => Some(b.as_ref()), + Self::Empty => None, + Self::Invalidated(room_id) | Self::Filled(room_id) => Some(room_id.as_ref()), } } - fn freeze(&self) -> RoomListEntry { + /// Clone this entry, but freeze it, i.e. if the entry is empty, it remains + /// empty, otherwise it is invalidated. + fn freeze(&self) -> Self { match &self { - RoomListEntry::Empty => RoomListEntry::Empty, - RoomListEntry::Invalidated(b) | RoomListEntry::Filled(b) => { - RoomListEntry::Invalidated(b.clone()) + Self::Empty => Self::Empty, + Self::Invalidated(room_id) | Self::Filled(room_id) => { + Self::Invalidated(room_id.clone()) } } } From 96248248ca922d8fce56097d79ff4968aecc355d Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 27 Feb 2023 13:56:04 +0100 Subject: [PATCH 059/166] chore(sdk): Move `UpdateSummary` at the bottom of the module. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 9574c9672..27521ee19 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -651,15 +651,6 @@ pub use view::*; use crate::{config::RequestConfig, Client, Result}; -/// The Summary of a new SlidingSync Update received -#[derive(Debug, Clone)] -pub struct UpdateSummary { - /// The views (according to their name), which have seen an update - pub views: Vec, - /// The Rooms that have seen updates - pub rooms: Vec, -} - /// Number of times a Sliding Sync session can expire before raising an error. /// /// A Sliding Sync session can expire. In this case, it is reset. However, to @@ -669,7 +660,7 @@ pub struct UpdateSummary { /// raising a proper error. const MAXIMUM_SLIDING_SYNC_SESSION_EXPIRATION: u8 = 3; -/// The sliding sync instance +/// The Sliding Sync instance. #[derive(Clone, Debug)] pub struct SlidingSync { /// Customize the homeserver for sliding sync only @@ -1188,6 +1179,16 @@ impl SlidingSync { } } +/// A summary of the updates received after a sync (like in +/// [`SlidingSync::stream`]). +#[derive(Debug, Clone)] +pub struct UpdateSummary { + /// The names of the views that have seen an update. + pub views: Vec, + /// The rooms that have seen updates + pub rooms: Vec, +} + #[cfg(test)] mod test { use ruma::room_id; From 927c44bec30817ffb96363bc07bbc823eb303b0c Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 27 Feb 2023 15:08:13 +0100 Subject: [PATCH 060/166] doc(sdk): Fix doctests + remove mentions to `futures-signals`. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 44 ++++++----------------- 1 file changed, 11 insertions(+), 33 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 27521ee19..6a5d31107 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -85,10 +85,10 @@ //! are **inclusive**) like so: //! //! ```rust -//! # use matrix_sdk::sliding_sync::{SlidingSyncViewBuilder, SlidingSyncMode}; +//! # use matrix_sdk::sliding_sync::{SlidingSyncView, SlidingSyncMode}; //! use ruma::{assign, api::client::sync::sync_events::v4}; //! -//! let view_builder = SlidingSyncViewBuilder::default() +//! let view_builder = SlidingSyncView::builder() //! .name("main_view") //! .sync_mode(SlidingSyncMode::Selective) //! .filters(Some(assign!( @@ -280,7 +280,6 @@ //! ```no_run //! # use futures::executor::block_on; //! # use futures::{pin_mut, StreamExt}; -//! # use futures_signals::{signal::SignalExt, signal_vec::SignalVecExt}; //! # use matrix_sdk::{ //! # sliding_sync::{SlidingSyncMode, SlidingSyncViewBuilder}, //! # Client, @@ -304,6 +303,7 @@ //! //! // continuously poll for updates //! pin_mut!(stream); +//! //! loop { //! let update = match stream.next().await { //! Some(Ok(u)) => { @@ -349,7 +349,6 @@ //! ```no_run //! # use futures::executor::block_on; //! # use futures::{pin_mut, StreamExt}; -//! # use futures_signals::{signal::SignalExt, signal_vec::SignalVecExt}; //! # use matrix_sdk::{ //! # sliding_sync::{SlidingSyncMode, SlidingSyncViewBuilder, SlidingSync, Error}, //! # Client, @@ -370,7 +369,7 @@ //! # .await?; //! use std::sync::{Arc, atomic::{AtomicBool, Ordering}}; //! -//! struct MyRunner{ lock: Arc, sliding_sync: SlidingSync }; +//! struct MyRunner { lock: Arc, sliding_sync: SlidingSync }; //! //! impl MyRunner { //! pub fn restart_sync(&mut self) { @@ -418,38 +417,20 @@ //! must be applied transparently throughout to the data layer. The simplest //! way to stay up to date on what objects have changed is by checking the //! [`views`](`UpdateSummary.views`) and [`rooms`](`UpdateSummary.rooms`) of -//! each `UpdateSummary` given by each stream iteration and update the local +//! each [`UpdateSummary`] given by each stream iteration and update the local //! copies accordingly. Because of where the loop sits in the stack, that can //! be a bit tedious though, so views and rooms have an additional way of -//! subscribing to updates via [`futures_signals`][]. +//! subscribing to updates via [`eyeball`]. //! -//! The `rooms_list` is of the more specialized -//! [`MutableVec`](`futures_signals::signal_vec::MutableVec`) type. Rather than -//! just signaling the latest state (which can be very inefficient, especially -//! on large lists), its -//! [`MutableSignalVec`](`futures_signals::signal_vec::MutableSignalVec`) will -//! share the modifications made by signalling -//! [`VecDiff`](`futures_signals::signal_vec::VecDiff`) over the stream. This -//! allows for easy and efficient synchronization of exactly those parts that -//! have been changed. If you are keeping a memory copy of the -//! `Vec` for your view for example, you can apply changes that -//! come as `VecDiff` easily by calling -//! [`apply_to_vec`](`futures_signals::signal_vec::VecDiff::apply_to_vec`). -//! -//! The `Timeline` you can receive per room by calling +//! The `Timeline` one can receive per room by calling //! [`.timeline()`][`SlidingSyncRoom::timeline`] will be populated with the //! currently cached timeline events. //! -//! 👉 To learn more about [`future_signals` check out to their excellent -//! tutorial][future-signals-tutorial]. -//! //! ## Caching //! //! All room data, for filled but also _invalidated_ rooms, including the entire //! timeline events as well as all view `room_lists` and `rooms_count` are held -//! in memory (unless one `pop`s the view out). Technically, one can access -//! `rooms_list` and `rooms` directly and mutate them but doing so invalidates -//! further updates received by the server - see [#1474][https://github.com/matrix-org/matrix-rust-sdk/issues/1474]. +//! in memory (unless one `pop`s the view out). //! //! This is a purely in-memory cache layer though. If one wants Sliding Sync to //! persist and load from cold (storage) cache, one needs to set its key with @@ -506,11 +487,10 @@ //! //! ```no_run //! # use futures::executor::block_on; -//! use matrix_sdk::{Client, sliding_sync::{SlidingSyncViewBuilder, SlidingSyncMode}}; +//! use matrix_sdk::{Client, sliding_sync::{SlidingSyncView, SlidingSyncMode}}; //! use ruma::{assign, {api::client::sync::sync_events::v4, events::StateEventType}}; //! use tracing::{warn, error, info, debug}; //! use futures::{StreamExt, pin_mut}; -//! use futures_signals::{signal::SignalExt, signal_vec::SignalVecExt}; //! use url::Url; //! # block_on(async { //! # let homeserver = Url::parse("http://example.com")?; @@ -524,7 +504,7 @@ //! .with_common_extensions() // we want the e2ee and to-device enabled, please //! .cold_cache("example-cache".to_owned()); // we want these to be loaded from and stored into the persistent storage //! -//! let full_sync_view = SlidingSyncViewBuilder::default() +//! let full_sync_view = SlidingSyncView::builder() //! .sync_mode(SlidingSyncMode::GrowingFullSync) // sync up by growing the window //! .name(&full_sync_view_name) // needed to lookup again. //! .sort(vec!["by_recency".to_owned()]) // ordered by most recent @@ -535,7 +515,7 @@ //! .limit(500) // only sync up the top 500 rooms //! .build()?; //! -//! let active_view = SlidingSyncViewBuilder::default() +//! let active_view = SlidingSyncView::builder() //! .name(&active_view_name) // the active window //! .sync_mode(SlidingSyncMode::Selective) // sync up the specific range only //! .set_range(0u32, 9u32) // only the top 10 items @@ -608,9 +588,7 @@ //! //! [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3575 //! [proxy]: https://github.com/matrix-org/sliding-sync -//! [futures_signals]: https://docs.rs/futures-signals/latest/futures_signals/index.html //! [ruma-types]: https://docs.rs/ruma/latest/ruma/api/client/sync/sync_events/v4/index.html -//! [future-signals-tutorial]: https://docs.rs/futures-signals/latest/futures_signals/tutorial/index.html mod builder; mod client; From abab6dcf0fe01ce0a194313a92f10cc61f0625f4 Mon Sep 17 00:00:00 2001 From: Sam Wedgwood <28223854+swedgwood@users.noreply.github.com> Date: Mon, 27 Feb 2023 15:39:01 +0000 Subject: [PATCH 061/166] docs(crypto): Fix typo Signed-off-by: Sam Wedgwood --- crates/matrix-sdk-crypto/src/file_encryption/attachments.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk-crypto/src/file_encryption/attachments.rs b/crates/matrix-sdk-crypto/src/file_encryption/attachments.rs index 361352f9a..9ed02e8a5 100644 --- a/crates/matrix-sdk-crypto/src/file_encryption/attachments.rs +++ b/crates/matrix-sdk-crypto/src/file_encryption/attachments.rs @@ -203,7 +203,7 @@ impl<'a, R: Read + ?Sized + 'a> AttachmentEncryptor<'a, R> { /// /// # Arguments /// - /// * `reader` - The `Reader` that should be wrapped and enrypted. + /// * `reader` - The `Reader` that should be wrapped and encrypted. /// /// # Panics /// From 9531dc195ab0a7028ac4bc55baccbca6ced04ef0 Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 27 Feb 2023 17:05:41 +0000 Subject: [PATCH 062/166] fix(bindings): Fix double quotes when replying to a reply. --- Cargo.lock | 182 +++++++++++++++++++++++++++++ bindings/matrix-sdk-ffi/Cargo.toml | 1 + 2 files changed, 183 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 13068671f..ac127f5f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1603,6 +1603,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures" version = "0.3.26" @@ -1907,6 +1917,20 @@ dependencies = [ "digest 0.10.6", ] +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "http" version = "0.2.8" @@ -2488,12 +2512,32 @@ dependencies = [ "thread-id", ] +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + [[package]] name = "maplit" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf 0.10.1", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + [[package]] name = "matchers" version = "0.1.0" @@ -2791,6 +2835,7 @@ dependencies = [ "once_cell", "opentelemetry", "opentelemetry-otlp", + "ruma", "sanitize-filename-reader-friendly", "serde_json", "thiserror", @@ -3134,6 +3179,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + [[package]] name = "nix" version = "0.24.3" @@ -3548,6 +3599,86 @@ dependencies = [ "indexmap", ] +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + +[[package]] +name = "phf" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" +dependencies = [ + "phf_macros", + "phf_shared 0.11.1", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" +dependencies = [ + "phf_shared 0.11.1", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92aacdc5f16768709a569e913f7451034034178b05bdc8acda226659a3dccc66" +dependencies = [ + "phf_generator 0.11.1", + "phf_shared 0.11.1", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.0.12" @@ -3693,6 +3824,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "prettyplease" version = "0.1.23" @@ -4188,6 +4325,7 @@ dependencies = [ "bytes", "form_urlencoded", "getrandom 0.2.8", + "html5ever", "http", "indexmap", "js-sys", @@ -4195,6 +4333,7 @@ dependencies = [ "js_option", "konst", "percent-encoding", + "phf 0.11.1", "pulldown-cmark", "rand 0.8.5", "regex", @@ -4727,6 +4866,32 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb" +[[package]] +name = "string_cache" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213494b7a2b503146286049378ce02b482200519accc31872ee8be91fa820a08" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot 0.12.1", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.10.0" @@ -4805,6 +4970,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + [[package]] name = "termcolor" version = "1.2.0" @@ -5623,6 +5799,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "uuid" version = "1.3.0" diff --git a/bindings/matrix-sdk-ffi/Cargo.toml b/bindings/matrix-sdk-ffi/Cargo.toml index 5bb8133bf..7a87279ac 100644 --- a/bindings/matrix-sdk-ffi/Cargo.toml +++ b/bindings/matrix-sdk-ffi/Cargo.toml @@ -29,6 +29,7 @@ mime = "0.3.16" once_cell = { workspace = true } opentelemetry = { version = "0.18.0", features = ["rt-tokio"] } opentelemetry-otlp = { version = "0.11.0", features = ["tokio", "reqwest-client", "http-proto"] } +ruma = { workspace = true, features = ["unstable-sanitize"] } sanitize-filename-reader-friendly = "2.2.1" serde_json = { workspace = true } thiserror = { workspace = true } From 1684dbe0a15fd089d89523378eea31575f19423c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Wed, 22 Feb 2023 16:00:13 +0100 Subject: [PATCH 063/166] feat(examples): Add example to persist a session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- Cargo.lock | 14 ++ examples/persist_session/Cargo.toml | 22 ++ examples/persist_session/src/main.rs | 312 +++++++++++++++++++++++++++ 3 files changed, 348 insertions(+) create mode 100644 examples/persist_session/Cargo.toml create mode 100644 examples/persist_session/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index ac127f5f6..a96a5ec4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1444,6 +1444,20 @@ dependencies = [ "url", ] +[[package]] +name = "example-persist-session" +version = "0.1.0" +dependencies = [ + "anyhow", + "dirs", + "matrix-sdk", + "rand 0.8.5", + "serde", + "serde_json", + "tokio", + "tracing-subscriber", +] + [[package]] name = "example-timeline" version = "0.1.0" diff --git a/examples/persist_session/Cargo.toml b/examples/persist_session/Cargo.toml new file mode 100644 index 000000000..2aa9663f7 --- /dev/null +++ b/examples/persist_session/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "example-persist-session" +version = "0.1.0" +edition = "2021" +publish = false + +[[bin]] +name = "example-persist-session" +test = false + +[dependencies] +anyhow = "1" +dirs = "4.0.0" +rand = "0.8.5" +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread"] } +tracing-subscriber = "0.3.15" + +[dependencies.matrix-sdk] +path = "../../crates/matrix-sdk" +version = "0.6.0" diff --git a/examples/persist_session/src/main.rs b/examples/persist_session/src/main.rs new file mode 100644 index 000000000..f54aab31f --- /dev/null +++ b/examples/persist_session/src/main.rs @@ -0,0 +1,312 @@ +use std::{ + io::{self, Write}, + path::{Path, PathBuf}, +}; + +use matrix_sdk::{ + config::SyncSettings, + room::Room, + ruma::{ + api::client::filter::{FilterDefinition, LazyLoadOptions, RoomEventFilter, RoomFilter}, + events::room::message::{MessageType, OriginalSyncRoomMessageEvent}, + }, + Client, Error, LoopCtrl, Session, +}; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use serde::{Deserialize, Serialize}; +use tokio::fs; + +/// The data needed to re-build a client. +#[derive(Debug, Serialize, Deserialize)] +struct ClientSession { + /// The URL of the homeserver of the user. + homeserver: String, + + /// The path of the database. + db_path: PathBuf, + + /// The passphrase of the database. + passphrase: String, +} + +/// The full session to persist. +#[derive(Debug, Serialize, Deserialize)] +struct FullSession { + /// The data to re-build the client. + client_session: ClientSession, + + /// The Matrix user session. + user_session: Session, + + /// The latest sync token. + /// + /// It is only needed to persist it when using `Client::sync_once()` and we + /// want to make our syncs faster by not receiving all the initial sync + /// again. + #[serde(skip_serializing_if = "Option::is_none")] + sync_token: Option, +} + +/// A simple example to show how to persist a client's data to be able to +/// restore it. +/// +/// Restoring a session with encryption without having a persisted store +/// will break the encryption setup and the client will not be able to send or +/// receive encrypted messages, hence the need to persist the session. +/// +/// To use this, just run `cargo run -p example-persist-session`, and everything +/// is interactive after that. You might want to set the `RUST_LOG` environment +/// variable to `warn` to reduce the noise in the logs. The program exits +/// whenever an unexpected error occurs. +/// +/// To reset the login, simply delete the folder containing the session +/// file, the location is shown in the logs. Note that the database must be +/// deleted too as it can't be reused. +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + + // The folder containing this example's data. + let data_dir = dirs::data_dir().expect("no data_dir directory found").join("persist_session"); + // The file where the session is persisted. + let session_file = data_dir.join("session"); + + let (client, sync_token) = if session_file.exists() { + restore_session(&session_file).await? + } else { + (login(&data_dir, &session_file).await?, None) + }; + + sync(client, sync_token, &session_file).await.map_err(Into::into) +} + +/// Restore a previous session. +async fn restore_session(session_file: &Path) -> anyhow::Result<(Client, Option)> { + println!("Previous session found in '{}'", session_file.to_string_lossy()); + + // The session was serialized as JSON in a file. + let serialized_session = fs::read_to_string(session_file).await?; + let FullSession { client_session, user_session, sync_token } = + serde_json::from_str(&serialized_session)?; + + // Build the client with the previous settings from the session. + let client = Client::builder() + .homeserver_url(client_session.homeserver) + .sled_store(client_session.db_path, Some(&client_session.passphrase)) + .build() + .await?; + + println!("Restoring session for {}…", user_session.user_id); + + // Restore the Matrix user session. + client.restore_session(user_session).await?; + + Ok((client, sync_token)) +} + +/// Login with a new device. +async fn login(data_dir: &Path, session_file: &Path) -> anyhow::Result { + println!("No previous session found, logging in…"); + + let (client, client_session) = build_client(data_dir).await?; + + loop { + print!("\nUsername: "); + io::stdout().flush().expect("Unable to write to stdout"); + let mut username = String::new(); + io::stdin().read_line(&mut username).expect("Unable to read user input"); + username = username.trim().to_owned(); + + print!("Password: "); + io::stdout().flush().expect("Unable to write to stdout"); + let mut password = String::new(); + io::stdin().read_line(&mut password).expect("Unable to read user input"); + password = password.trim().to_owned(); + + match client + .login_username(&username, &password) + .initial_device_display_name("persist-session client") + .await + { + Ok(_) => { + println!("Logged in as {username}"); + break; + } + Err(error) => { + println!("Error logging in: {error}"); + println!("Please try again\n"); + } + } + } + + // Persist the session to reuse it later. + // This is not very secure, for simplicity. If the system provides a way of + // storing secrets securely, it should be used instead. + // Note that we could also build the user session from the login response. + let user_session = client.session().expect("A logged-in client should have a session"); + let serialized_session = + serde_json::to_string(&FullSession { client_session, user_session, sync_token: None })?; + fs::write(session_file, serialized_session).await?; + + println!("Session persisted in {}", session_file.to_string_lossy()); + + // After logging in, you might want to verify this session with another one (see + // the `emoji_verification` example), or bootstrap cross-signing if this is your + // first session with encryption, or if you need to reset cross-signing because + // you don't have access to your old sessions (see the + // `cross_signing_bootstrap` example). + + Ok(client) +} + +/// Build a new client. +async fn build_client(data_dir: &Path) -> anyhow::Result<(Client, ClientSession)> { + let mut rng = thread_rng(); + + // Generating a subfolder for the database is not mandatory, but it is useful if + // you allow several clients to run at the same time. Each one must have a + // separate database, which is a different folder with the sled store. + let db_subfolder: String = + (&mut rng).sample_iter(Alphanumeric).take(7).map(char::from).collect(); + let db_path = data_dir.join(db_subfolder); + + // Generate a random passphrase. + let passphrase: String = + (&mut rng).sample_iter(Alphanumeric).take(32).map(char::from).collect(); + + // We create a loop here so the user can retry if an error happens. + loop { + let mut homeserver = String::new(); + + print!("Homeserver URL: "); + io::stdout().flush().expect("Unable to write to stdout"); + io::stdin().read_line(&mut homeserver).expect("Unable to read user input"); + + println!("\nChecking homeserver…"); + + match Client::builder() + .homeserver_url(&homeserver) + // We use the sled store, which is enabled by default. This is the crucial part to + // persist the encryption setup. + // Note that other store backends are available and you an even implement your own. + .sled_store(&db_path, Some(&passphrase)) + .build() + .await + { + Ok(client) => return Ok((client, ClientSession { homeserver, db_path, passphrase })), + Err(error) => match &error { + matrix_sdk::ClientBuildError::AutoDiscovery(_) + | matrix_sdk::ClientBuildError::Url(_) + | matrix_sdk::ClientBuildError::Http(_) => { + println!("Error checking the homeserver: {error}"); + println!("Please try again\n"); + } + _ => { + // Forward other errors, it's unlikely we can retry with a different outcome. + return Err(error.into()); + } + }, + } + } +} + +/// Setup the client to listen to new messages. +async fn sync( + client: Client, + initial_sync_token: Option, + session_file: &Path, +) -> anyhow::Result<()> { + println!("Launching a first sync to ignore past messages…"); + + // Enable room members lazy-loading, it will speed up the initial sync a lot + // with accounts in lots of rooms. + // See . + let mut state_filter = RoomEventFilter::empty(); + state_filter.lazy_load_options = LazyLoadOptions::Enabled { include_redundant_members: false }; + let mut room_filter = RoomFilter::empty(); + room_filter.state = state_filter; + let mut filter = FilterDefinition::empty(); + filter.room = room_filter; + + let mut sync_settings = SyncSettings::default().filter(filter.into()); + + // We restore the sync where we left. + // This is not necessary when not using `sync_once`. The other sync methods get + // the sync token from the store. + if let Some(sync_token) = initial_sync_token { + sync_settings = sync_settings.token(sync_token); + } + + // Let's ignore messages before the program was launched. + // This is a loop in case the initial sync is longer than our timeout. The + // server should cache the response and it will ultimately take less time to + // receive. + loop { + match client.sync_once(sync_settings.clone()).await { + Ok(response) => { + // This is the last time we need to provide this token, the sync method after + // will handle it on its own. + sync_settings = sync_settings.token(response.next_batch.clone()); + persist_sync_token(session_file, response.next_batch).await?; + break; + } + Err(error) => { + println!("An error occurred during initial sync: {error}"); + println!("Trying again…"); + } + } + } + + println!("The client is ready! Listening to new messages…"); + + // Now that we've synced, let's attach a handler for incoming room messages. + client.add_event_handler(on_room_message); + + // This loops until we kill the program or an error happens. + client + .sync_with_result_callback(sync_settings, |sync_result| async move { + let response = sync_result?; + + // We persist the token each time to be able to restore our session + persist_sync_token(session_file, response.next_batch) + .await + .map_err(|err| Error::UnknownError(err.into()))?; + + Ok(LoopCtrl::Continue) + }) + .await?; + + Ok(()) +} + +/// Persist the sync token for a future session. +/// Note that this is needed only when using `sync_once`. Other sync methods get +/// the sync token from the store. +async fn persist_sync_token(session_file: &Path, sync_token: String) -> anyhow::Result<()> { + let serialized_session = fs::read_to_string(session_file).await?; + let mut full_session: FullSession = serde_json::from_str(&serialized_session)?; + + full_session.sync_token = Some(sync_token); + let serialized_session = serde_json::to_string(&full_session)?; + fs::write(session_file, serialized_session).await?; + + Ok(()) +} + +/// Handle room messages. +async fn on_room_message(event: OriginalSyncRoomMessageEvent, room: Room) { + // We only want to log text messages in joined rooms. + let Room::Joined(room) = room else { return }; + let MessageType::Text(text_content) = &event.content.msgtype else { return }; + + let room_name = match room.display_name().await { + Ok(room_name) => room_name.to_string(), + Err(error) => { + println!("Error getting room display name: {error}"); + // Let's fallback to the room ID. + room.room_id().to_string() + } + }; + + println!("[{room_name}] {}: {}", event.sender, text_content.body) +} From 3139204f4b51f214cf3b53a88c229b8e74ab65e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Thu, 23 Feb 2023 12:09:11 +0100 Subject: [PATCH 064/166] fix(examples): Don't use the sled store if the session is not persisted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Running the same example twice borks encryption. Instead point to an example that shows how to do it. Signed-off-by: Kévin Commaille --- examples/autojoin/src/main.rs | 20 ++++---------------- examples/command_bot/src/main.rs | 23 +++++------------------ examples/custom_events/src/main.rs | 4 +--- examples/getting_started/src/main.rs | 9 +++------ examples/timeline/src/main.rs | 6 ++++-- 5 files changed, 17 insertions(+), 45 deletions(-) diff --git a/examples/autojoin/src/main.rs b/examples/autojoin/src/main.rs index bfb9b3b83..3964692bc 100644 --- a/examples/autojoin/src/main.rs +++ b/examples/autojoin/src/main.rs @@ -43,22 +43,10 @@ async fn login_and_sync( username: &str, password: &str, ) -> anyhow::Result<()> { - #[allow(unused_mut)] - let mut client_builder = Client::builder().homeserver_url(homeserver_url); - - #[cfg(feature = "sled")] - { - // The location to save files to - let home = dirs::home_dir().expect("no home directory found").join("autojoin_bot"); - client_builder = client_builder.sled_store(home, None)?; - } - - #[cfg(feature = "indexeddb")] - { - client_builder = client_builder.indexeddb_store("autojoin_bot", None).await?; - } - - let client = client_builder.build().await?; + // Note that when encryption is enabled, you should use a persistent store to be + // able to restore the session with a working encryption setup. + // See the `persist_session` example. + let client = Client::builder().homeserver_url(homeserver_url).build().await?; client.login_username(username, password).initial_device_display_name("autojoin bot").await?; diff --git a/examples/command_bot/src/main.rs b/examples/command_bot/src/main.rs index 52030d336..e5adc376b 100644 --- a/examples/command_bot/src/main.rs +++ b/examples/command_bot/src/main.rs @@ -35,29 +35,16 @@ async fn login_and_sync( username: String, password: String, ) -> anyhow::Result<()> { - #[allow(unused_mut)] - let mut client_builder = Client::builder().homeserver_url(homeserver_url); - - #[cfg(feature = "sled")] - { - // The location to save files to - let home = dirs::home_dir().expect("no home directory found").join("party_bot"); - client_builder = client_builder.sled_store(home, None)?; - } - - #[cfg(feature = "indexeddb")] - { - client_builder = client_builder.indexeddb_store("party_bot", None).await?; - } - - let client = client_builder.build().await.unwrap(); + // Note that when encryption is enabled, you should use a persistent store to be + // able to restore the session with a working encryption setup. + // See the `persist_session` example. + let client = Client::builder().homeserver_url(homeserver_url).build().await.unwrap(); client.login_username(&username, &password).initial_device_display_name("command bot").await?; println!("logged in as {username}"); // An initial sync to set up state and so our bot doesn't respond to old - // messages. If the `StateStore` finds saved state in the location given the - // initial sync will be skipped in favor of loading state from the store + // messages. let response = client.sync_once(SyncSettings::default()).await.unwrap(); // add our CommandBot to be notified of incoming messages, we do this after the // initial sync to avoid responding to messages before the bot was running. diff --git a/examples/custom_events/src/main.rs b/examples/custom_events/src/main.rs index a7000da4d..62f8c724c 100644 --- a/examples/custom_events/src/main.rs +++ b/examples/custom_events/src/main.rs @@ -103,9 +103,7 @@ async fn login_and_sync( username: &str, password: &str, ) -> anyhow::Result<()> { - let home = dirs::data_dir().expect("no home directory found").join("getting_started"); - let client = - Client::builder().homeserver_url(homeserver_url).sled_store(home, None).build().await?; + let client = Client::builder().homeserver_url(homeserver_url).build().await?; client .login_username(username, password) .initial_device_display_name("getting started bot") diff --git a/examples/getting_started/src/main.rs b/examples/getting_started/src/main.rs index f4b8412c4..b5ffc79cd 100644 --- a/examples/getting_started/src/main.rs +++ b/examples/getting_started/src/main.rs @@ -59,15 +59,12 @@ async fn login_and_sync( ) -> anyhow::Result<()> { // First, we set up the client. - let home = dirs::data_dir().expect("no home directory found").join("getting_started"); - + // Note that when encryption is enabled, you should use a persistent store to be + // able to restore the session with a working encryption setup. + // See the `persist_session` example. 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?; diff --git a/examples/timeline/src/main.rs b/examples/timeline/src/main.rs index 8d45720d8..994449833 100644 --- a/examples/timeline/src/main.rs +++ b/examples/timeline/src/main.rs @@ -32,8 +32,10 @@ struct Cli { } async fn login(cli: Cli) -> Result { - let mut builder = - Client::builder().homeserver_url(cli.homeserver).sled_store("./", Some("some password")); + // Note that when encryption is enabled, you should use a persistent store to be + // able to restore the session with a working encryption setup. + // See the `persist_session` example. + let mut builder = Client::builder().homeserver_url(cli.homeserver); if let Some(proxy) = cli.proxy { builder = builder.proxy(proxy); From 6aad2a661d37dcb87576ffaa87fe979f7ee20661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Mon, 6 Feb 2023 13:39:45 +0100 Subject: [PATCH 065/166] refactor(sdk): Rename ProfileProvider to RoomDataProvider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- .../src/room/timeline/event_item.rs | 8 ++-- crates/matrix-sdk/src/room/timeline/inner.rs | 38 +++++++++---------- .../matrix-sdk/src/room/timeline/tests/mod.rs | 10 ++--- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/crates/matrix-sdk/src/room/timeline/event_item.rs b/crates/matrix-sdk/src/room/timeline/event_item.rs index 24d36c195..b40f84993 100644 --- a/crates/matrix-sdk/src/room/timeline/event_item.rs +++ b/crates/matrix-sdk/src/room/timeline/event_item.rs @@ -52,7 +52,7 @@ use ruma::{ OwnedTransactionId, OwnedUserId, TransactionId, UserId, }; -use super::inner::ProfileProvider; +use super::inner::RoomDataProvider; use crate::{Error, Result}; /// An item in the timeline that represents at least one event. @@ -571,9 +571,9 @@ impl RepliedToEvent { &self.sender_profile } - pub(super) async fn try_from_timeline_event( + pub(super) async fn try_from_timeline_event( timeline_event: TimelineEvent, - profile_provider: &P, + room_data_provider: &P, ) -> Result { let event = match timeline_event.event.deserialize() { Ok(AnyTimelineEvent::MessageLike(event)) => event, @@ -593,7 +593,7 @@ impl RepliedToEvent { }; let sender = event.sender().to_owned(); let sender_profile = - TimelineDetails::from_initial_value(profile_provider.profile(&sender).await); + TimelineDetails::from_initial_value(room_data_provider.profile(&sender).await); Ok(Self { message, sender, sender_profile }) } diff --git a/crates/matrix-sdk/src/room/timeline/inner.rs b/crates/matrix-sdk/src/room/timeline/inner.rs index c966a2f76..992f9be53 100644 --- a/crates/matrix-sdk/src/room/timeline/inner.rs +++ b/crates/matrix-sdk/src/room/timeline/inner.rs @@ -58,9 +58,9 @@ use crate::{ }; #[derive(Debug)] -pub(super) struct TimelineInner { +pub(super) struct TimelineInner { state: Mutex, - profile_provider: P, + room_data_provider: P, } #[derive(Debug, Default)] @@ -78,8 +78,8 @@ pub(super) struct TimelineInnerState { pub(super) fully_read_event_in_timeline: bool, } -impl TimelineInner

    { - pub(super) fn new(profile_provider: P) -> Self { +impl TimelineInner

    { + pub(super) fn new(room_data_provider: P) -> Self { let state = TimelineInnerState { // Upstream default capacity is currently 16, which is making // sliding-sync tests with 20 events lag. This should still be @@ -87,7 +87,7 @@ impl TimelineInner

    { items: ObservableVector::with_capacity(32), ..Default::default() }; - Self { state: Mutex::new(state), profile_provider } + Self { state: Mutex::new(state), room_data_provider } } /// Get a copy of the current items in the list. @@ -123,7 +123,7 @@ impl TimelineInner

    { event.encryption_info, TimelineItemPosition::End, state, - &self.profile_provider, + &self.room_data_provider, ) .await; } @@ -152,7 +152,7 @@ impl TimelineInner

    { encryption_info, TimelineItemPosition::End, &mut state, - &self.profile_provider, + &self.room_data_provider, ) .await; } @@ -164,8 +164,8 @@ impl TimelineInner

    { txn_id: OwnedTransactionId, content: AnyMessageLikeEventContent, ) { - let sender = self.profile_provider.own_user_id().to_owned(); - let sender_profile = self.profile_provider.profile(&sender).await; + let sender = self.room_data_provider.own_user_id().to_owned(); + let sender_profile = self.room_data_provider.profile(&sender).await; let event_meta = TimelineEventMetadata { sender, sender_profile, @@ -241,7 +241,7 @@ impl TimelineInner

    { event.encryption_info, TimelineItemPosition::Start, &mut state, - &self.profile_provider, + &self.room_data_provider, ) .await } @@ -387,7 +387,7 @@ impl TimelineInner

    { event.encryption_info, TimelineItemPosition::Update(idx), &mut state, - &self.profile_provider, + &self.room_data_provider, ) .await; @@ -431,7 +431,7 @@ impl TimelineInner

    { Some(event_item) => event_item.sender().to_owned(), None => continue, }; - let maybe_profile = self.profile_provider.profile(&sender).await; + let maybe_profile = self.room_data_provider.profile(&sender).await; assert_eq!(state.items.len(), num_items); @@ -458,7 +458,7 @@ impl TimelineInner

    { impl TimelineInner { pub(super) fn room(&self) -> &room::Common { - &self.profile_provider + &self.room_data_provider } pub(super) async fn fetch_in_reply_to_details( @@ -559,13 +559,13 @@ async fn fetch_replied_to_event( } #[async_trait] -pub(super) trait ProfileProvider { +pub(super) trait RoomDataProvider { fn own_user_id(&self) -> &UserId; async fn profile(&self, user_id: &UserId) -> Option; } #[async_trait] -impl ProfileProvider for room::Common { +impl RoomDataProvider for room::Common { fn own_user_id(&self) -> &UserId { (**self).own_user_id() } @@ -594,12 +594,12 @@ impl ProfileProvider for room::Common { /// Handle a remote event. /// /// Returns the number of timeline updates that were made. -async fn handle_remote_event( +async fn handle_remote_event( raw: Raw, encryption_info: Option, position: TimelineItemPosition, timeline_state: &mut TimelineInnerState, - profile_provider: &P, + room_data_provider: &P, ) -> HandleEventResult { let (event_id, sender, origin_server_ts, txn_id, relations, event_kind) = match raw.deserialize() { @@ -629,8 +629,8 @@ async fn handle_remote_event( }, }; - let is_own_event = sender == profile_provider.own_user_id(); - let sender_profile = profile_provider.profile(&sender).await; + let is_own_event = sender == room_data_provider.own_user_id(); + let sender_profile = room_data_provider.profile(&sender).await; let event_meta = TimelineEventMetadata { sender, sender_profile, is_own_event, relations, encryption_info }; let flow = Flow::Remote { event_id, origin_server_ts, raw_event: raw, txn_id, position }; diff --git a/crates/matrix-sdk/src/room/timeline/tests/mod.rs b/crates/matrix-sdk/src/room/timeline/tests/mod.rs index eb0650f9f..4b0c656f8 100644 --- a/crates/matrix-sdk/src/room/timeline/tests/mod.rs +++ b/crates/matrix-sdk/src/room/timeline/tests/mod.rs @@ -36,7 +36,7 @@ use ruma::{ }; use serde_json::{json, Value as JsonValue}; -use super::{inner::ProfileProvider, Profile, TimelineInner, TimelineItem}; +use super::{inner::RoomDataProvider, Profile, TimelineInner, TimelineItem}; mod basic; mod echo; @@ -48,13 +48,13 @@ static ALICE: Lazy<&UserId> = Lazy::new(|| user_id!("@alice:server.name")); static BOB: Lazy<&UserId> = Lazy::new(|| user_id!("@bob:other.server")); struct TestTimeline { - inner: TimelineInner, + inner: TimelineInner, next_ts: AtomicU64, } impl TestTimeline { fn new() -> Self { - Self { inner: TimelineInner::new(TestProfileProvider), next_ts: AtomicU64::new(0) } + Self { inner: TimelineInner::new(TestRoomDataProvider), next_ts: AtomicU64::new(0) } } async fn subscribe(&self) -> impl Stream>> { @@ -255,10 +255,10 @@ impl TestTimeline { } } -struct TestProfileProvider; +struct TestRoomDataProvider; #[async_trait] -impl ProfileProvider for TestProfileProvider { +impl RoomDataProvider for TestRoomDataProvider { fn own_user_id(&self) -> &UserId { &ALICE } From da8d74ccc1001af63353f2923d08df4bf2286c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Mon, 13 Feb 2023 13:06:50 +0100 Subject: [PATCH 066/166] feat(sdk): Keep track of events read receipts in the timeline API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- crates/matrix-sdk/src/room/common.rs | 2 +- .../matrix-sdk/src/room/timeline/builder.rs | 71 +++++- .../src/room/timeline/event_handler.rs | 60 ++++- .../src/room/timeline/event_item.rs | 8 + crates/matrix-sdk/src/room/timeline/inner.rs | 79 +++++- crates/matrix-sdk/src/room/timeline/mod.rs | 1 + .../src/room/timeline/read_receipts.rs | 228 ++++++++++++++++++ .../matrix-sdk/src/room/timeline/tests/mod.rs | 51 +++- .../src/room/timeline/tests/read_receipts.rs | 91 +++++++ crates/matrix-sdk/src/sliding_sync/room.rs | 2 +- .../tests/integration/room/timeline.rs | 159 +++++++++++- 11 files changed, 716 insertions(+), 36 deletions(-) create mode 100644 crates/matrix-sdk/src/room/timeline/read_receipts.rs create mode 100644 crates/matrix-sdk/src/room/timeline/tests/read_receipts.rs diff --git a/crates/matrix-sdk/src/room/common.rs b/crates/matrix-sdk/src/room/common.rs index 757538050..9d0c19303 100644 --- a/crates/matrix-sdk/src/room/common.rs +++ b/crates/matrix-sdk/src/room/common.rs @@ -272,7 +272,7 @@ impl Common { /// independent events. #[cfg(feature = "experimental-timeline")] pub async fn timeline(&self) -> Timeline { - Timeline::builder(self).track_fully_read().build().await + Timeline::builder(self).track_read_marker_and_receipts().build().await } /// Fetch the event with the given `EventId` in this room. diff --git a/crates/matrix-sdk/src/room/timeline/builder.rs b/crates/matrix-sdk/src/room/timeline/builder.rs index 5ebcd858d..f22a23fb4 100644 --- a/crates/matrix-sdk/src/room/timeline/builder.rs +++ b/crates/matrix-sdk/src/room/timeline/builder.rs @@ -19,7 +19,10 @@ use matrix_sdk_base::{ deserialized_responses::{EncryptionInfo, SyncTimelineEvent}, locks::Mutex, }; -use ruma::events::fully_read::FullyReadEventContent; +use ruma::events::{ + fully_read::FullyReadEventContent, + receipt::{ReceiptThread, ReceiptType, SyncReceiptEvent}, +}; use tracing::error; use super::{ @@ -37,7 +40,7 @@ pub(crate) struct TimelineBuilder { room: room::Common, prev_token: Option, events: Vector, - track_fully_read: bool, + track_read_marker_and_receipts: bool, } impl TimelineBuilder { @@ -46,7 +49,7 @@ impl TimelineBuilder { room: room.clone(), prev_token: None, events: Vector::new(), - track_fully_read: false, + track_read_marker_and_receipts: false, } } @@ -62,18 +65,57 @@ impl TimelineBuilder { self } - /// Enable tracking of the fully-read marker on the timeline. - pub(crate) fn track_fully_read(mut self) -> Self { - self.track_fully_read = true; + /// Enable tracking of the fully-read marker and the read receipts on the + /// timeline. + pub(crate) fn track_read_marker_and_receipts(mut self) -> Self { + self.track_read_marker_and_receipts = true; self } /// Create a [`Timeline`] with the options set on this builder. pub(crate) async fn build(self) -> Timeline { - let Self { room, prev_token, events, track_fully_read } = self; + let Self { room, prev_token, events, track_read_marker_and_receipts } = self; let has_events = !events.is_empty(); - let mut inner = TimelineInner::new(room); + let mut inner = + TimelineInner::new(room).with_read_receipt_tracking(track_read_marker_and_receipts); + + if track_read_marker_and_receipts { + match inner + .room() + .user_receipt( + ReceiptType::Read, + ReceiptThread::Unthreaded, + inner.room().own_user_id(), + ) + .await + { + Ok(Some(read_receipt)) => { + inner.set_initial_user_receipt(ReceiptType::Read, read_receipt); + } + Err(e) => { + error!("Failed to get public read receipt of own user from the store: {e}"); + } + _ => {} + } + match inner + .room() + .user_receipt( + ReceiptType::ReadPrivate, + ReceiptThread::Unthreaded, + inner.room().own_user_id(), + ) + .await + { + Ok(Some(private_read_receipt)) => { + inner.set_initial_user_receipt(ReceiptType::ReadPrivate, private_read_receipt); + } + Err(e) => { + error!("Failed to get private read receipt of own user from the store: {e}"); + } + _ => {} + } + } if has_events { inner.add_initial_events(events).await; @@ -111,7 +153,7 @@ impl TimelineBuilder { forwarded_room_key_handle, ]; - if track_fully_read { + if track_read_marker_and_receipts { match room.account_data_static::().await { Ok(Some(fully_read)) => match fully_read.deserialize() { Ok(fully_read) => { @@ -137,6 +179,17 @@ impl TimelineBuilder { } }); handles.push(fully_read_handle); + + let read_receipts_handle = room.add_event_handler({ + let inner = inner.clone(); + move |read_receipts: SyncReceiptEvent| { + let inner = inner.clone(); + async move { + inner.handle_read_receipts(read_receipts.content).await; + } + } + }); + handles.push(read_receipts_handle); } let client = room.client.clone(); diff --git a/crates/matrix-sdk/src/room/timeline/event_handler.rs b/crates/matrix-sdk/src/room/timeline/event_handler.rs index 8c06e04f3..6e3642cb7 100644 --- a/crates/matrix-sdk/src/room/timeline/event_handler.rs +++ b/crates/matrix-sdk/src/room/timeline/event_handler.rs @@ -21,6 +21,7 @@ use matrix_sdk_base::deserialized_responses::EncryptionInfo; use ruma::{ events::{ reaction::ReactionEventContent, + receipt::{Receipt, ReceiptType}, relation::{Annotation, Replacement}, room::{ encrypted::RoomEncryptedEventContent, @@ -46,8 +47,10 @@ use super::{ MemberProfileChange, OtherState, Profile, RemoteEventTimelineItem, RoomMembershipChange, Sticker, }, - find_read_marker, rfind_event_by_id, rfind_event_item, EventTimelineItem, InReplyToDetails, - Message, ReactionGroup, TimelineDetails, TimelineInnerState, TimelineItem, TimelineItemContent, + find_read_marker, + read_receipts::maybe_add_implicit_read_receipt, + rfind_event_by_id, rfind_event_item, EventTimelineItem, InReplyToDetails, Message, + ReactionGroup, TimelineDetails, TimelineInnerState, TimelineItem, TimelineItemContent, VirtualTimelineItem, }; use crate::{events::SyncTimelineEventWithoutContent, room::timeline::MembershipChange}; @@ -72,6 +75,7 @@ pub(super) struct TimelineEventMetadata { pub(super) is_own_event: bool, pub(super) relations: BundledRelations, pub(super) encryption_info: Option, + pub(super) read_receipts: IndexMap, } #[derive(Clone)] @@ -201,6 +205,9 @@ pub(super) struct TimelineEventHandler<'a> { pending_reactions: &'a mut HashMap>, fully_read_event: &'a mut Option, fully_read_event_in_timeline: &'a mut bool, + track_read_receipts: bool, + users_read_receipts: + &'a mut HashMap>, result: HandleEventResult, } @@ -224,6 +231,7 @@ impl<'a> TimelineEventHandler<'a> { event_meta: TimelineEventMetadata, flow: Flow, state: &'a mut TimelineInnerState, + track_read_receipts: bool, ) -> Self { Self { meta: event_meta, @@ -233,6 +241,8 @@ impl<'a> TimelineEventHandler<'a> { pending_reactions: &mut state.pending_reactions, fully_read_event: &mut state.fully_read_event, fully_read_event_in_timeline: &mut state.fully_read_event_in_timeline, + track_read_receipts, + users_read_receipts: &mut state.users_read_receipts, result: HandleEventResult::default(), } } @@ -532,7 +542,7 @@ impl<'a> TimelineEventHandler<'a> { let sender_profile = TimelineDetails::from_initial_value(self.meta.sender_profile.clone()); let mut reactions = self.pending_reactions().unwrap_or_default(); - let item = match &self.flow { + let mut item = match &self.flow { Flow::Local { txn_id, timestamp } => EventTimelineItem::Local(LocalEventTimelineItem { send_state: EventSendState::NotSentYet, transaction_id: txn_id.to_owned(), @@ -558,13 +568,12 @@ impl<'a> TimelineEventHandler<'a> { reactions, is_own: self.meta.is_own_event, encryption_info: self.meta.encryption_info.clone(), + read_receipts: self.meta.read_receipts.clone(), raw: raw_event.clone(), }) } }; - let item = Arc::new(TimelineItem::Event(item)); - match &self.flow { Flow::Local { timestamp, .. } => { trace!("Adding new local timeline item"); @@ -586,7 +595,7 @@ impl<'a> TimelineEventHandler<'a> { self.items.push_back(Arc::new(TimelineItem::day_divider(*timestamp))); } - self.items.push_back(item); + self.items.push_back(Arc::new(item.into())); } Flow::Remote { @@ -631,7 +640,17 @@ impl<'a> TimelineEventHandler<'a> { .insert(offset, Arc::new(TimelineItem::day_divider(*origin_server_ts))); } - self.items.insert(offset + 1, item); + if self.track_read_receipts { + maybe_add_implicit_read_receipt( + offset, + &mut item, + self.meta.is_own_event, + self.items, + self.users_read_receipts, + ); + } + + self.items.insert(offset + 1, Arc::new(item.into())); } Flow::Remote { @@ -672,8 +691,19 @@ impl<'a> TimelineEventHandler<'a> { { // If the old item is the last one and no day divider // changes need to happen, replace and return early. + + if self.track_read_receipts { + maybe_add_implicit_read_receipt( + idx, + &mut item, + self.meta.is_own_event, + self.items, + self.users_read_receipts, + ); + } + trace!(idx, "Replacing existing event"); - self.items.set(idx, item); + self.items.set(idx, Arc::new(item.into())); return; } else { // In more complex cases, remove the item and day @@ -729,14 +759,24 @@ impl<'a> TimelineEventHandler<'a> { self.items.push_back(Arc::new(TimelineItem::day_divider(*origin_server_ts))); } + if self.track_read_receipts { + maybe_add_implicit_read_receipt( + self.items.len(), + &mut item, + self.meta.is_own_event, + self.items, + self.users_read_receipts, + ); + } + trace!("Adding new remote timeline item at the end"); - self.items.push_back(item); + self.items.push_back(Arc::new(item.into())); } #[cfg(feature = "e2e-encryption")] Flow::Remote { position: TimelineItemPosition::Update(idx), .. } => { trace!("Updating timeline item at position {idx}"); - self.items.set(*idx, item); + self.items.set(*idx, Arc::new(item.into())); } } diff --git a/crates/matrix-sdk/src/room/timeline/event_item.rs b/crates/matrix-sdk/src/room/timeline/event_item.rs index b40f84993..ebf12dfda 100644 --- a/crates/matrix-sdk/src/room/timeline/event_item.rs +++ b/crates/matrix-sdk/src/room/timeline/event_item.rs @@ -22,6 +22,7 @@ use ruma::{ room::PolicyRuleRoomEventContent, server::PolicyRuleServerEventContent, user::PolicyRuleUserEventContent, }, + receipt::Receipt, room::{ aliases::RoomAliasesEventContent, avatar::RoomAvatarEventContent, @@ -294,6 +295,13 @@ pub struct RemoteEventTimelineItem { pub content: TimelineItemContent, /// All bundled reactions about the event. pub reactions: BundledReactions, + /// All read receipts for the event. + /// + /// The key is the ID of a room member and the value are details about the + /// read receipt. + /// + /// Note that currently this ignores threads. + pub read_receipts: IndexMap, /// Whether the event has been sent by the the logged-in user themselves. pub is_own: bool, /// Encryption information. diff --git a/crates/matrix-sdk/src/room/timeline/inner.rs b/crates/matrix-sdk/src/room/timeline/inner.rs index 992f9be53..505d064f0 100644 --- a/crates/matrix-sdk/src/room/timeline/inner.rs +++ b/crates/matrix-sdk/src/room/timeline/inner.rs @@ -20,7 +20,7 @@ use std::{ use async_trait::async_trait; use eyeball_im::{ObservableVector, VectorSubscriber}; use im::Vector; -use indexmap::IndexSet; +use indexmap::{IndexMap, IndexSet}; use matrix_sdk_base::{ crypto::OlmMachine, deserialized_responses::{EncryptionInfo, SyncTimelineEvent, TimelineEvent}, @@ -28,8 +28,10 @@ use matrix_sdk_base::{ }; use ruma::{ events::{ - fully_read::FullyReadEvent, relation::Annotation, AnyMessageLikeEventContent, - AnySyncTimelineEvent, + fully_read::FullyReadEvent, + receipt::{Receipt, ReceiptEventContent, ReceiptThread, ReceiptType}, + relation::Annotation, + AnyMessageLikeEventContent, AnySyncTimelineEvent, }, serde::Raw, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId, RoomId, @@ -48,6 +50,7 @@ use super::{ update_read_marker, Flow, HandleEventResult, TimelineEventHandler, TimelineEventKind, TimelineEventMetadata, TimelineItemPosition, }, + read_receipts::{handle_explicit_read_receipts, load_read_receipts_for_event}, rfind_event_by_id, rfind_event_item, EventSendState, EventTimelineItem, InReplyToDetails, Message, Profile, RepliedToEvent, TimelineDetails, TimelineItem, TimelineItemContent, }; @@ -61,6 +64,7 @@ use crate::{ pub(super) struct TimelineInner { state: Mutex, room_data_provider: P, + track_read_receipts: bool, } #[derive(Debug, Default)] @@ -76,6 +80,9 @@ pub(super) struct TimelineInnerState { /// Whether the event that the fully-ready event _refers to_ is part of the /// timeline. pub(super) fully_read_event_in_timeline: bool, + /// User ID => Receipt type => Read receipt of the user of the given type. + pub(super) users_read_receipts: + HashMap>, } impl TimelineInner

    { @@ -87,7 +94,12 @@ impl TimelineInner

    { items: ObservableVector::with_capacity(32), ..Default::default() }; - Self { state: Mutex::new(state), room_data_provider } + Self { state: Mutex::new(state), room_data_provider, track_read_receipts: false } + } + + pub(super) fn with_read_receipt_tracking(mut self, track_read_receipts: bool) -> Self { + self.track_read_receipts = track_read_receipts; + self } /// Get a copy of the current items in the list. @@ -108,6 +120,20 @@ impl TimelineInner

    { (items, stream) } + pub(super) fn set_initial_user_receipt( + &mut self, + receipt_type: ReceiptType, + receipt: (OwnedEventId, Receipt), + ) { + let own_user_id = self.room_data_provider.own_user_id().to_owned(); + self.state + .get_mut() + .users_read_receipts + .entry(own_user_id) + .or_default() + .insert(receipt_type, receipt); + } + pub(super) async fn add_initial_events(&mut self, events: Vector) { if events.is_empty() { return; @@ -124,6 +150,7 @@ impl TimelineInner

    { TimelineItemPosition::End, state, &self.room_data_provider, + self.track_read_receipts, ) .await; } @@ -153,6 +180,7 @@ impl TimelineInner

    { TimelineItemPosition::End, &mut state, &self.room_data_provider, + self.track_read_receipts, ) .await; } @@ -173,13 +201,15 @@ impl TimelineInner

    { relations: Default::default(), // FIXME: Should we supply something here for encrypted rooms? encryption_info: None, + read_receipts: Default::default(), }; let flow = Flow::Local { txn_id, timestamp: MilliSecondsSinceUnixEpoch::now() }; let kind = TimelineEventKind::Message { content }; let mut state = self.state.lock().await; - TimelineEventHandler::new(event_meta, flow, &mut state).handle_event(kind); + TimelineEventHandler::new(event_meta, flow, &mut state, self.track_read_receipts) + .handle_event(kind); } /// Update the send state of a local event represented by a transaction ID. @@ -242,6 +272,7 @@ impl TimelineInner

    { TimelineItemPosition::Start, &mut state, &self.room_data_provider, + self.track_read_receipts, ) .await } @@ -388,6 +419,7 @@ impl TimelineInner

    { TimelineItemPosition::Update(idx), &mut state, &self.room_data_provider, + self.track_read_receipts, ) .await; @@ -454,6 +486,13 @@ impl TimelineInner

    { } } } + + pub(super) async fn handle_read_receipts(&self, receipt_event_content: ReceiptEventContent) { + let mut state = self.state.lock().await; + let own_user_id = self.room_data_provider.own_user_id(); + + handle_explicit_read_receipts(receipt_event_content, own_user_id, &mut state) + } } impl TimelineInner { @@ -562,6 +601,7 @@ async fn fetch_replied_to_event( pub(super) trait RoomDataProvider { fn own_user_id(&self) -> &UserId; async fn profile(&self, user_id: &UserId) -> Option; + async fn read_receipts_for_event(&self, event_id: &EventId) -> IndexMap; } #[async_trait] @@ -589,6 +629,16 @@ impl RoomDataProvider for room::Common { } } } + + async fn read_receipts_for_event(&self, event_id: &EventId) -> IndexMap { + match self.event_receipts(ReceiptType::Read, ReceiptThread::Unthreaded, event_id).await { + Ok(receipts) => receipts.into_iter().collect(), + Err(e) => { + error!(?event_id, "Failed to get read receipts for event: {e}"); + IndexMap::new() + } + } + } } /// Handle a remote event. @@ -600,6 +650,7 @@ async fn handle_remote_event( position: TimelineItemPosition, timeline_state: &mut TimelineInnerState, room_data_provider: &P, + track_read_receipts: bool, ) -> HandleEventResult { let (event_id, sender, origin_server_ts, txn_id, relations, event_kind) = match raw.deserialize() { @@ -631,9 +682,21 @@ async fn handle_remote_event( let is_own_event = sender == room_data_provider.own_user_id(); let sender_profile = room_data_provider.profile(&sender).await; - let event_meta = - TimelineEventMetadata { sender, sender_profile, is_own_event, relations, encryption_info }; + let read_receipts = if track_read_receipts { + load_read_receipts_for_event(&event_id, timeline_state, room_data_provider).await + } else { + Default::default() + }; + let event_meta = TimelineEventMetadata { + sender, + sender_profile, + is_own_event, + relations, + encryption_info, + read_receipts, + }; let flow = Flow::Remote { event_id, origin_server_ts, raw_event: raw, txn_id, position }; - TimelineEventHandler::new(event_meta, flow, timeline_state).handle_event(event_kind) + TimelineEventHandler::new(event_meta, flow, timeline_state, track_read_receipts) + .handle_event(event_kind) } diff --git a/crates/matrix-sdk/src/room/timeline/mod.rs b/crates/matrix-sdk/src/room/timeline/mod.rs index cde9874b4..fc226c00e 100644 --- a/crates/matrix-sdk/src/room/timeline/mod.rs +++ b/crates/matrix-sdk/src/room/timeline/mod.rs @@ -41,6 +41,7 @@ mod event_handler; mod event_item; mod inner; mod pagination; +mod read_receipts; #[cfg(test)] mod tests; #[cfg(feature = "e2e-encryption")] diff --git a/crates/matrix-sdk/src/room/timeline/read_receipts.rs b/crates/matrix-sdk/src/room/timeline/read_receipts.rs new file mode 100644 index 000000000..2e7cac355 --- /dev/null +++ b/crates/matrix-sdk/src/room/timeline/read_receipts.rs @@ -0,0 +1,228 @@ +// Copyright 2023 Kévin Commaille +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{collections::HashMap, sync::Arc}; + +use eyeball_im::ObservableVector; +use indexmap::IndexMap; +use ruma::{ + events::receipt::{Receipt, ReceiptEventContent, ReceiptThread, ReceiptType}, + EventId, OwnedEventId, OwnedUserId, UserId, +}; +use tracing::error; + +use super::{ + inner::{RoomDataProvider, TimelineInnerState}, + rfind_event_by_id, EventTimelineItem, TimelineItem, +}; + +struct FullReceipt<'a> { + event_id: &'a EventId, + user_id: &'a UserId, + receipt_type: ReceiptType, + receipt: &'a Receipt, +} + +pub(super) fn handle_explicit_read_receipts( + receipt_event_content: ReceiptEventContent, + own_user_id: &UserId, + timeline_state: &mut TimelineInnerState, +) { + for (event_id, receipt_types) in receipt_event_content.0 { + for (receipt_type, receipts) in receipt_types { + // We only care about read receipts here. + if !matches!(receipt_type, ReceiptType::Read | ReceiptType::ReadPrivate) { + continue; + } + + for (user_id, receipt) in receipts { + if receipt.thread != ReceiptThread::Unthreaded { + continue; + } + + let receipt_item_pos = + rfind_event_by_id(&timeline_state.items, &event_id).map(|(pos, _)| pos); + let is_own_user_id = user_id == own_user_id; + let full_receipt = FullReceipt { + event_id: &event_id, + user_id: &user_id, + receipt_type: receipt_type.clone(), + receipt: &receipt, + }; + + let read_receipt_updated = maybe_update_read_receipt( + full_receipt, + receipt_item_pos, + is_own_user_id, + &mut timeline_state.items, + &mut timeline_state.users_read_receipts, + ); + + if read_receipt_updated && !is_own_user_id { + // Update the new item pointed to by the user's read receipt. + let new_receipt_event_item = receipt_item_pos.and_then(|pos| { + let e = timeline_state.items[pos].as_event()?.as_remote()?; + Some((pos, e.clone())) + }); + + if let Some((pos, mut remote_event_item)) = new_receipt_event_item { + remote_event_item.read_receipts.insert(user_id, receipt); + timeline_state + .items + .set(pos, Arc::new(TimelineItem::Event(remote_event_item.into()))); + } + } + } + } + } +} + +/// Add an implicit read receipt to the given event item, if it is more recent +/// than the current read receipt for the sender of the event. +/// +/// According to the spec, read receipts should not point to events sent by our +/// own user, but these events are used to reset the notification count, so we +/// need to handle them locally too. For that we create an "implicit" read +/// receipt, compared to the "explicit" ones sent by the client. +pub(super) fn maybe_add_implicit_read_receipt( + item_pos: usize, + event_item: &mut EventTimelineItem, + is_own_event: bool, + timeline_items: &mut ObservableVector>, + users_read_receipts: &mut HashMap>, +) { + let EventTimelineItem::Remote(remote_event_item) = event_item else { + return; + }; + + let receipt = Receipt::new(remote_event_item.timestamp); + let new_receipt = FullReceipt { + event_id: &remote_event_item.event_id, + user_id: &remote_event_item.sender.clone(), + receipt_type: ReceiptType::Read, + receipt: &receipt, + }; + + let read_receipt_updated = maybe_update_read_receipt( + new_receipt, + Some(item_pos), + is_own_event, + timeline_items, + users_read_receipts, + ); + if read_receipt_updated && !is_own_event { + remote_event_item.read_receipts.insert(remote_event_item.sender.clone(), receipt); + } +} + +/// Update the timeline items with the given read receipt if it is more recent +/// than the current one. +/// +/// In the process, this method removes the corresponding receipt from its old +/// item, if applicable, and updates the `users_read_receipts` map to use the +/// new receipt. +/// +/// Returns true if the read receipt was saved. +/// +/// Currently this method only works reliably if the timeline was started from +/// the end of the timeline. +fn maybe_update_read_receipt( + receipt: FullReceipt<'_>, + new_item_pos: Option, + is_own_user_id: bool, + timeline_items: &mut ObservableVector>, + users_read_receipts: &mut HashMap>, +) -> bool { + let old_event_id = users_read_receipts + .get(receipt.user_id) + .and_then(|receipts| receipts.get(&receipt.receipt_type)) + .map(|(event_id, _)| event_id); + if old_event_id.map_or(false, |id| id == receipt.event_id) { + // Nothing to do. + return false; + } + + let old_item = old_event_id.and_then(|e| { + let (pos, item) = rfind_event_by_id(timeline_items, e)?; + Some((pos, item.as_remote()?)) + }); + + if let Some((old_receipt_pos, old_event_item)) = old_item { + let Some(new_receipt_pos) = new_item_pos else { + // The old receipt is likely more recent since we can't find the event of the + // new receipt in the timeline. Even if it isn't, we wouldn't know where to put + // it. + return false; + }; + + if old_receipt_pos > new_receipt_pos { + // The old receipt is more recent than the new one. + return false; + } + + if !is_own_user_id { + // Remove the read receipt for this user from the old event. + let mut old_event_item = old_event_item.clone(); + if old_event_item.read_receipts.remove(receipt.user_id).is_none() { + error!("inconsistent state: old event item for user's read receipt doesn't have a receipt for the user"); + } + timeline_items + .set(old_receipt_pos, Arc::new(TimelineItem::Event(old_event_item.into()))); + } + } + + // The new receipt is deemed more recent from now on because: + // - If old_receipt_item is Some, we already checked all the cases where it + // wouldn't be more recent. + // - If both old_receipt_item and new_receipt_item are None, they are both + // explicit read receipts so the server should only send us a more recent + // receipt. + // - If old_receipt_item is None and new_receipt_item is Some, the new receipt + // is likely more recent because it has a place in the timeline. + users_read_receipts + .entry(receipt.user_id.to_owned()) + .or_default() + .insert(receipt.receipt_type, (receipt.event_id.to_owned(), receipt.receipt.clone())); + + true +} + +/// Load the read receipts from the store for the given event ID. +pub(super) async fn load_read_receipts_for_event( + event_id: &EventId, + timeline_state: &mut TimelineInnerState, + room_data_provider: &P, +) -> IndexMap { + let read_receipts = room_data_provider.read_receipts_for_event(event_id).await; + + // Filter out receipts for our own user. + let own_user_id = room_data_provider.own_user_id(); + let read_receipts: IndexMap = + read_receipts.into_iter().filter(|(user_id, _)| user_id != own_user_id).collect(); + + // Keep track of the user's read receipt. + for (user_id, receipt) in read_receipts.clone() { + // Only insert the read receipt if the user is not known to avoid conflicts with + // `TimelineInner::handle_read_receipts`. + if !timeline_state.users_read_receipts.contains_key(&user_id) { + timeline_state + .users_read_receipts + .entry(user_id) + .or_default() + .insert(ReceiptType::Read, (event_id.to_owned(), receipt)); + } + } + + read_receipts +} diff --git a/crates/matrix-sdk/src/room/timeline/tests/mod.rs b/crates/matrix-sdk/src/room/timeline/tests/mod.rs index 4b0c656f8..45f125da8 100644 --- a/crates/matrix-sdk/src/room/timeline/tests/mod.rs +++ b/crates/matrix-sdk/src/room/timeline/tests/mod.rs @@ -14,25 +14,30 @@ //! Unit tests (based on private methods) for the timeline API. -use std::sync::{ - atomic::{AtomicU64, Ordering::SeqCst}, - Arc, +use std::{ + collections::BTreeMap, + sync::{ + atomic::{AtomicU64, Ordering::SeqCst}, + Arc, + }, }; use async_trait::async_trait; use eyeball_im::VectorDiff; use futures_core::Stream; +use indexmap::IndexMap; use matrix_sdk_base::deserialized_responses::TimelineEvent; use once_cell::sync::Lazy; use ruma::{ events::{ + receipt::{Receipt, ReceiptEventContent, ReceiptThread, ReceiptType}, AnyMessageLikeEventContent, EmptyStateKey, MessageLikeEventContent, RedactedMessageLikeEventContent, RedactedStateEventContent, StateEventContent, StaticStateEventContent, }, serde::Raw, - server_name, user_id, EventId, MilliSecondsSinceUnixEpoch, OwnedTransactionId, TransactionId, - UserId, + server_name, user_id, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, + OwnedUserId, TransactionId, UserId, }; use serde_json::{json, Value as JsonValue}; @@ -42,6 +47,7 @@ mod basic; mod echo; mod encryption; mod invalid; +mod read_receipts; mod virt; static ALICE: Lazy<&UserId> = Lazy::new(|| user_id!("@alice:server.name")); @@ -57,6 +63,11 @@ impl TestTimeline { Self { inner: TimelineInner::new(TestRoomDataProvider), next_ts: AtomicU64::new(0) } } + fn with_read_receipt_tracking(mut self) -> Self { + self.inner = self.inner.with_read_receipt_tracking(true); + self + } + async fn subscribe(&self) -> impl Stream>> { let (items, stream) = self.inner.subscribe().await; assert_eq!(items.len(), 0, "Please subscribe to TestTimeline before adding items to it"); @@ -156,6 +167,14 @@ impl TestTimeline { self.inner.handle_back_paginated_event(timeline_event).await; } + async fn handle_read_receipts( + &self, + receipts: impl IntoIterator, + ) { + let ev_content = self.make_receipt_event_content(receipts); + self.inner.handle_read_receipts(ev_content).await; + } + /// Set the next server timestamp. /// /// Timestamps will continue to increase by 1 (millisecond) from that value. @@ -245,6 +264,24 @@ impl TestTimeline { }) } + fn make_receipt_event_content( + &self, + receipts: impl IntoIterator, + ) -> ReceiptEventContent { + let mut ev_content = ReceiptEventContent(BTreeMap::new()); + for (event_id, receipt_type, user_id, thread) in receipts { + let event_map = ev_content.entry(event_id).or_default(); + let receipt_map = event_map.entry(receipt_type).or_default(); + + let mut receipt = Receipt::new(self.next_server_ts()); + receipt.thread = thread; + + receipt_map.insert(user_id, receipt); + } + + ev_content + } + fn next_server_ts(&self) -> MilliSecondsSinceUnixEpoch { MilliSecondsSinceUnixEpoch( self.next_ts @@ -266,4 +303,8 @@ impl RoomDataProvider for TestRoomDataProvider { async fn profile(&self, _user_id: &UserId) -> Option { None } + + async fn read_receipts_for_event(&self, _event_id: &EventId) -> IndexMap { + IndexMap::new() + } } diff --git a/crates/matrix-sdk/src/room/timeline/tests/read_receipts.rs b/crates/matrix-sdk/src/room/timeline/tests/read_receipts.rs new file mode 100644 index 000000000..a7a3e37e6 --- /dev/null +++ b/crates/matrix-sdk/src/room/timeline/tests/read_receipts.rs @@ -0,0 +1,91 @@ +// Copyright 2023 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use assert_matches::assert_matches; +use eyeball_im::VectorDiff; +use futures_util::StreamExt; +use matrix_sdk_test::async_test; +use ruma::events::{ + receipt::{ReceiptThread, ReceiptType}, + room::message::RoomMessageEventContent, +}; + +use super::{TestTimeline, ALICE, BOB}; + +#[async_test] +async fn read_receipts_updates() { + let timeline = TestTimeline::new().with_read_receipt_tracking(); + let mut stream = timeline.subscribe().await; + + timeline.handle_live_message_event(*ALICE, RoomMessageEventContent::text_plain("A")).await; + timeline.handle_live_message_event(*BOB, RoomMessageEventContent::text_plain("B")).await; + + let _day_divider = + assert_matches!(stream.next().await, Some(VectorDiff::PushBack { value }) => value); + + // No read receipt for our own user. + let item_a = + assert_matches!(stream.next().await, Some(VectorDiff::PushBack { value }) => value); + let event_a = item_a.as_event().unwrap().as_remote().unwrap(); + assert!(event_a.read_receipts.is_empty()); + + // Implicit read receipt of Bob. + let item_b = + assert_matches!(stream.next().await, Some(VectorDiff::PushBack { value }) => value); + let event_b = item_b.as_event().unwrap().as_remote().unwrap(); + assert_eq!(event_b.read_receipts.len(), 1); + assert!(event_b.read_receipts.get(*BOB).is_some()); + + // Implicit read receipt of Bob is updated. + timeline.handle_live_message_event(*BOB, RoomMessageEventContent::text_plain("C")).await; + + let item_a = + assert_matches!(stream.next().await, Some(VectorDiff::Set { index: 2, value }) => value); + let event_a = item_a.as_event().unwrap().as_remote().unwrap(); + assert!(event_a.read_receipts.is_empty()); + + let item_c = + assert_matches!(stream.next().await, Some(VectorDiff::PushBack { value }) => value); + let event_c = item_c.as_event().unwrap().as_remote().unwrap(); + assert_eq!(event_c.read_receipts.len(), 1); + assert!(event_c.read_receipts.get(*BOB).is_some()); + + timeline.handle_live_message_event(*ALICE, RoomMessageEventContent::text_plain("D")).await; + + let item_d = + assert_matches!(stream.next().await, Some(VectorDiff::PushBack { value }) => value); + let event_d = item_d.as_event().unwrap().as_remote().unwrap(); + assert!(event_d.read_receipts.is_empty()); + + // Explicit read receipt is updated. + timeline + .handle_read_receipts([( + event_d.event_id.clone(), + ReceiptType::Read, + BOB.to_owned(), + ReceiptThread::Unthreaded, + )]) + .await; + + let item_c = + assert_matches!(stream.next().await, Some(VectorDiff::Set { index: 3, value }) => value); + let event_c = item_c.as_event().unwrap().as_remote().unwrap(); + assert!(event_c.read_receipts.is_empty()); + + let item_d = + assert_matches!(stream.next().await, Some(VectorDiff::Set { index: 4, value }) => value); + let event_d = item_d.as_event().unwrap().as_remote().unwrap(); + assert_eq!(event_d.read_receipts.len(), 1); + assert!(event_d.read_receipts.get(*BOB).is_some()); +} diff --git a/crates/matrix-sdk/src/sliding_sync/room.rs b/crates/matrix-sdk/src/sliding_sync/room.rs index 3a1c89147..908d12079 100644 --- a/crates/matrix-sdk/src/sliding_sync/room.rs +++ b/crates/matrix-sdk/src/sliding_sync/room.rs @@ -75,7 +75,7 @@ impl SlidingSyncRoom { /// `Timeline` of this room pub async fn timeline(&self) -> Option { - Some(self.timeline_builder()?.track_fully_read().build().await) + Some(self.timeline_builder()?.track_read_marker_and_receipts().build().await) } fn timeline_builder(&self) -> Option { diff --git a/crates/matrix-sdk/tests/integration/room/timeline.rs b/crates/matrix-sdk/tests/integration/room/timeline.rs index fc447b567..e446bc711 100644 --- a/crates/matrix-sdk/tests/integration/room/timeline.rs +++ b/crates/matrix-sdk/tests/integration/room/timeline.rs @@ -16,8 +16,8 @@ use matrix_sdk::{ }; use matrix_sdk_common::executor::spawn; use matrix_sdk_test::{ - async_test, test_json, EventBuilder, JoinedRoomBuilder, RoomAccountDataTestEvent, - TimelineTestEvent, + async_test, test_json, EphemeralTestEvent, EventBuilder, JoinedRoomBuilder, + RoomAccountDataTestEvent, TimelineTestEvent, }; use ruma::{ event_id, @@ -668,6 +668,9 @@ async fn in_reply_to_details() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; + let _read_receipt_update = + assert_matches!(timeline_stream.next().await, Some(VectorDiff::Set { value, .. }) => value); + let third = assert_matches!(timeline_stream.next().await, Some(VectorDiff::PushBack { value }) => value); let third_event = third.as_event().unwrap().as_remote().unwrap(); let message = @@ -727,3 +730,155 @@ async fn in_reply_to_details() { let message = assert_matches!(third.as_event().unwrap().content(), TimelineItemContent::Message(message) => message); assert_matches!(message.in_reply_to().unwrap().details, TimelineDetails::Ready(_)); } + +#[async_test] +async fn read_receipts_updates() { + 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 alice = user_id!("@alice:localhost"); + let bob = user_id!("@bob:localhost"); + + let second_event_id = event_id!("$e32037280er453l:localhost"); + let third_event_id = event_id!("$Sg2037280074GZr34:localhost"); + + 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().await; + let (items, mut timeline_stream) = timeline.subscribe().await; + + assert!(items.is_empty()); + + ev_builder.add_joined_room( + JoinedRoomBuilder::new(room_id) + .add_timeline_event(TimelineTestEvent::MessageText) + .add_timeline_event(TimelineTestEvent::Custom(json!({ + "content": { + "body": "I'm dancing too", + "msgtype": "m.text" + }, + "event_id": second_event_id, + "origin_server_ts": 152039280, + "sender": alice, + "type": "m.room.message", + }))) + .add_timeline_event(TimelineTestEvent::Custom(json!({ + "content": { + "body": "Viva la macarena!", + "msgtype": "m.text" + }, + "event_id": third_event_id, + "origin_server_ts": 152045280, + "sender": alice, + "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 _day_divider = assert_matches!(timeline_stream.next().await, Some(VectorDiff::PushBack { value }) => value); + + // We don't list the read receipt of our own user on events. + let first_item = assert_matches!(timeline_stream.next().await, Some(VectorDiff::PushBack { value }) => value); + let first_event = first_item.as_event().unwrap().as_remote().unwrap(); + assert!(first_event.read_receipts.is_empty()); + + // Implicit read receipt of @alice:localhost. + let second_item = assert_matches!(timeline_stream.next().await, Some(VectorDiff::PushBack { value }) => value); + let second_event = second_item.as_event().unwrap().as_remote().unwrap(); + assert_eq!(second_event.read_receipts.len(), 1); + + // Read receipt of @alice:localhost is moved to third event. + let second_item = assert_matches!(timeline_stream.next().await, Some(VectorDiff::Set { index: 2, value }) => value); + let second_event = second_item.as_event().unwrap().as_remote().unwrap(); + assert!(second_event.read_receipts.is_empty()); + + let third_item = assert_matches!(timeline_stream.next().await, Some(VectorDiff::PushBack { value }) => value); + let third_event = third_item.as_event().unwrap().as_remote().unwrap(); + assert_eq!(third_event.read_receipts.len(), 1); + + // Read receipt on unknown event is ignored. + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_ephemeral_event( + EphemeralTestEvent::Custom(json!({ + "content": { + "$unknowneventid": { + "m.read": { + alice: { + "ts": 1436453550, + }, + }, + }, + }, + "type": "m.receipt", + })), + )); + + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + // Read receipt on older event is ignored. + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_ephemeral_event( + EphemeralTestEvent::Custom(json!({ + "content": { + second_event_id: { + "m.read": { + alice: { + "ts": 1436451550, + }, + }, + }, + }, + "type": "m.receipt", + })), + )); + + // Read receipt on same event is ignored. + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_ephemeral_event( + EphemeralTestEvent::Custom(json!({ + "content": { + third_event_id: { + "m.read": { + alice: { + "ts": 1436451550, + }, + }, + }, + }, + "type": "m.receipt", + })), + )); + + // New user with explicit read receipt. + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_ephemeral_event( + EphemeralTestEvent::Custom(json!({ + "content": { + third_event_id: { + "m.read": { + bob: { + "ts": 1436451550, + }, + }, + }, + }, + "type": "m.receipt", + })), + )); + + 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 third_item = assert_matches!(timeline_stream.next().await, Some(VectorDiff::Set { index: 3, value }) => value); + let third_event = third_item.as_event().unwrap().as_remote().unwrap(); + assert_eq!(third_event.read_receipts.len(), 2); +} From b936d3e32d2287edef60700fb2dae764a5c8fd64 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 28 Feb 2023 12:22:58 +0100 Subject: [PATCH 067/166] chore: Remove workaround for clippy bug that was fixed --- bindings/matrix-sdk-crypto-ffi/src/lib.rs | 3 --- bindings/matrix-sdk-crypto-js/src/lib.rs | 3 --- bindings/matrix-sdk-ffi/src/lib.rs | 3 --- 3 files changed, 9 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/lib.rs b/bindings/matrix-sdk-crypto-ffi/src/lib.rs index a6fa482f8..18f0b91ae 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/lib.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/lib.rs @@ -5,9 +5,6 @@ #![warn(missing_docs)] #![allow(unused_qualifications)] -// Triggers false positives. -// See . -#![allow(clippy::extra_unused_type_parameters)] mod backup_recovery_key; mod device; diff --git a/bindings/matrix-sdk-crypto-js/src/lib.rs b/bindings/matrix-sdk-crypto-js/src/lib.rs index 31ae7894d..afbacff00 100644 --- a/bindings/matrix-sdk-crypto-js/src/lib.rs +++ b/bindings/matrix-sdk-crypto-js/src/lib.rs @@ -17,9 +17,6 @@ #![warn(missing_docs, missing_debug_implementations)] // triggered by wasm_bindgen code #![allow(clippy::drop_non_drop)] -// Triggers false positives. -// See . -#![allow(clippy::extra_unused_type_parameters)] pub mod attachment; pub mod device; diff --git a/bindings/matrix-sdk-ffi/src/lib.rs b/bindings/matrix-sdk-ffi/src/lib.rs index 16c1b16f0..a8fc4713d 100644 --- a/bindings/matrix-sdk-ffi/src/lib.rs +++ b/bindings/matrix-sdk-ffi/src/lib.rs @@ -1,9 +1,6 @@ // TODO: target-os conditional would be good. #![allow(unused_qualifications, clippy::new_without_default)] -// Triggers false positives. -// See . -#![allow(clippy::extra_unused_type_parameters)] macro_rules! unwrap_or_clone_arc_into_variant { ( From 5c3972938e8105d168dce50bb2c7e6a0ee930f6c Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 28 Feb 2023 16:06:23 +0200 Subject: [PATCH 068/166] chore: Add platform versions for swift CI tests --- bindings/apple/Package.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bindings/apple/Package.swift b/bindings/apple/Package.swift index 05f4003ab..fb04144f0 100644 --- a/bindings/apple/Package.swift +++ b/bindings/apple/Package.swift @@ -5,6 +5,10 @@ import PackageDescription let package = Package( name: "MatrixRustSDK", + platforms: [ + .iOS(.v15), + .macOS(.v12) + ], products: [ .library(name: "MatrixRustSDK", targets: ["MatrixRustSDK"]), From ff5dedbb07779d099ebd60da906ce724c35f4fa0 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 28 Feb 2023 19:32:11 +0200 Subject: [PATCH 069/166] fix(sliding_sync): Prevent PagingFullSync and GrowingFullSync from going over the total room count --- crates/matrix-sdk/src/sliding_sync/view.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/sliding_sync/view.rs b/crates/matrix-sdk/src/sliding_sync/view.rs index ae60271e5..eded7edcc 100644 --- a/crates/matrix-sdk/src/sliding_sync/view.rs +++ b/crates/matrix-sdk/src/sliding_sync/view.rs @@ -631,10 +631,17 @@ impl SlidingSyncViewRequestGenerator { limit: Option, ) -> v4::SyncRequestList { let calc_end = start + batch_size; - let end = match limit { + + let mut end = match limit { Some(l) => std::cmp::min(l, calc_end), _ => calc_end, }; + + end = match self.view.rooms_count() { + Some(total_room_count) => std::cmp::min(end, total_room_count - 1), + _ => end, + }; + self.make_request_for_ranges(vec![(start.into(), end.into())]) } From 452588a18e42b468ae4166c7260e7618c06c2fed Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 08:26:28 +0100 Subject: [PATCH 070/166] chore(indexeddb): Update version of `uuid`. --- crates/matrix-sdk-indexeddb/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk-indexeddb/Cargo.toml b/crates/matrix-sdk-indexeddb/Cargo.toml index dbd3db87d..2fdcfb39b 100644 --- a/crates/matrix-sdk-indexeddb/Cargo.toml +++ b/crates/matrix-sdk-indexeddb/Cargo.toml @@ -45,5 +45,5 @@ matrix-sdk-base = { path = "../matrix-sdk-base", features = ["testing"] } matrix-sdk-common = { path = "../matrix-sdk-common", features = ["js"] } matrix-sdk-crypto = { path = "../matrix-sdk-crypto", features = ["js", "testing"] } matrix-sdk-test = { path = "../../testing/matrix-sdk-test" } -uuid = "1.0.0" +uuid = "1.3.0" wasm-bindgen-test = "0.3.33" From 629b74e4f51b5b31123920c05359170ad716a179 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 11:56:01 +0100 Subject: [PATCH 071/166] =?UTF-8?q?chore(sdk):=20Rename=20the=20`sliding?= =?UTF-8?q?=5Fsync::view`=20module=20to=20`=E2=80=A6::list`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/matrix-sdk/src/sliding_sync/{view.rs => list.rs} | 0 crates/matrix-sdk/src/sliding_sync/mod.rs | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename crates/matrix-sdk/src/sliding_sync/{view.rs => list.rs} (100%) diff --git a/crates/matrix-sdk/src/sliding_sync/view.rs b/crates/matrix-sdk/src/sliding_sync/list.rs similarity index 100% rename from crates/matrix-sdk/src/sliding_sync/view.rs rename to crates/matrix-sdk/src/sliding_sync/list.rs diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 56934e179..81d45c0c9 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -593,8 +593,8 @@ mod builder; mod client; mod error; +mod list; mod room; -mod view; use std::{ collections::BTreeMap, @@ -612,6 +612,7 @@ pub use client::*; pub use error::*; use eyeball::Observable; use futures_core::stream::Stream; +pub use list::*; pub use room::*; use ruma::{ api::client::{ @@ -625,7 +626,6 @@ use ruma::{ use serde::{Deserialize, Serialize}; use tracing::{debug, error, info_span, instrument, trace, warn, Instrument, Span}; use url::Url; -pub use view::*; use crate::{config::RequestConfig, Client, Result}; From 5377bb7ecf52653da2b80e1af49f28bab41bfc9b Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 12:09:34 +0100 Subject: [PATCH 072/166] fix(sdk): Rename `SlidingSyncView` to `SlidingSyncList`. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The specification describes “list”, not “view”. Consistency is important, so let's rename this type! --- bindings/matrix-sdk-ffi/src/api.udl | 30 +++---- bindings/matrix-sdk-ffi/src/lib.rs | 4 +- bindings/matrix-sdk-ffi/src/sliding_sync.rs | 82 ++++++++--------- crates/matrix-sdk/src/lib.rs | 2 +- crates/matrix-sdk/src/sliding_sync/builder.rs | 14 +-- crates/matrix-sdk/src/sliding_sync/list.rs | 89 +++++++++---------- crates/matrix-sdk/src/sliding_sync/mod.rs | 52 +++++------ labs/jack-in/src/client/mod.rs | 4 +- labs/jack-in/src/client/state.rs | 8 +- .../sliding-sync-integration-test/src/lib.rs | 32 +++---- 10 files changed, 158 insertions(+), 159 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/api.udl b/bindings/matrix-sdk-ffi/src/api.udl index 602d6f8e6..96ef2c5cf 100644 --- a/bindings/matrix-sdk-ffi/src/api.udl +++ b/bindings/matrix-sdk-ffi/src/api.udl @@ -53,7 +53,7 @@ enum SlidingSyncMode { "Selective", }; -callback interface SlidingSyncViewStateObserver { +callback interface SlidingSyncListStateObserver { void did_receive_update(SlidingSyncState new_state); }; @@ -65,7 +65,7 @@ interface RoomListEntry { }; [Enum] -interface SlidingSyncViewRoomsListDiff { +interface SlidingSyncListRoomsListDiff { Append(sequence values); Insert(u32 index, RoomListEntry value); Set(u32 index, RoomListEntry value); @@ -78,36 +78,36 @@ interface SlidingSyncViewRoomsListDiff { Reset(sequence values); }; -callback interface SlidingSyncViewRoomListObserver { - void did_receive_update(SlidingSyncViewRoomsListDiff diff); +callback interface SlidingSyncListRoomListObserver { + void did_receive_update(SlidingSyncListRoomsListDiff diff); }; -callback interface SlidingSyncViewRoomsCountObserver { +callback interface SlidingSyncListRoomsCountObserver { void did_receive_update(u32 count); }; -callback interface SlidingSyncViewRoomItemsObserver { +callback interface SlidingSyncListRoomItemsObserver { void did_receive_update(); }; -interface SlidingSyncViewBuilder { +interface SlidingSyncListBuilder { constructor(); [Self=ByArc] - SlidingSyncViewBuilder sync_mode(SlidingSyncMode mode); + SlidingSyncListBuilder sync_mode(SlidingSyncMode mode); [Self=ByArc] - SlidingSyncViewBuilder send_updates_for_items(boolean enable); + SlidingSyncListBuilder send_updates_for_items(boolean enable); [Throws=ClientError, Self=ByArc] - SlidingSyncView build(); + SlidingSyncList build(); }; -interface SlidingSyncView { - TaskHandle observe_room_list(SlidingSyncViewRoomListObserver observer); - TaskHandle observe_rooms_count(SlidingSyncViewRoomsCountObserver observer); - TaskHandle observe_state(SlidingSyncViewStateObserver observer); - TaskHandle observe_room_items(SlidingSyncViewRoomItemsObserver observer); +interface SlidingSyncList { + TaskHandle observe_room_list(SlidingSyncListRoomListObserver observer); + TaskHandle observe_rooms_count(SlidingSyncListRoomsCountObserver observer); + TaskHandle observe_state(SlidingSyncListStateObserver observer); + TaskHandle observe_room_items(SlidingSyncListRoomItemsObserver observer); }; interface SlidingSyncRoom { diff --git a/bindings/matrix-sdk-ffi/src/lib.rs b/bindings/matrix-sdk-ffi/src/lib.rs index a8fc4713d..141205c8b 100644 --- a/bindings/matrix-sdk-ffi/src/lib.rs +++ b/bindings/matrix-sdk-ffi/src/lib.rs @@ -86,8 +86,8 @@ mod uniffi_types { session_verification::{SessionVerificationController, SessionVerificationEmoji}, sliding_sync::{ RequiredState, RoomListEntry, SlidingSync, SlidingSyncBuilder, - SlidingSyncRequestListFilters, SlidingSyncRoom, SlidingSyncView, - SlidingSyncViewBuilder, TaskHandle, UnreadNotificationsCount, + SlidingSyncRequestListFilters, SlidingSyncRoom, SlidingSyncList, + SlidingSyncListBuilder, TaskHandle, UnreadNotificationsCount, }, timeline::{ AudioInfo, AudioMessageContent, EmoteMessageContent, EncryptedMessage, EventSendState, diff --git a/bindings/matrix-sdk-ffi/src/sliding_sync.rs b/bindings/matrix-sdk-ffi/src/sliding_sync.rs index e0367b027..ece014b37 100644 --- a/bindings/matrix-sdk-ffi/src/sliding_sync.rs +++ b/bindings/matrix-sdk-ffi/src/sliding_sync.rs @@ -282,7 +282,7 @@ impl From for UpdateSummary { } } -pub enum SlidingSyncViewRoomsListDiff { +pub enum SlidingSyncListRoomsListDiff { Append { values: Vec }, Insert { index: u32, value: RoomListEntry }, Set { index: u32, value: RoomListEntry }, @@ -295,33 +295,33 @@ pub enum SlidingSyncViewRoomsListDiff { Reset { values: Vec }, } -impl From> for SlidingSyncViewRoomsListDiff { +impl From> for SlidingSyncListRoomsListDiff { fn from(other: VectorDiff) -> Self { match other { - VectorDiff::Append { values } => SlidingSyncViewRoomsListDiff::Append { + VectorDiff::Append { values } => SlidingSyncListRoomsListDiff::Append { values: values.into_iter().map(|e| (&e).into()).collect(), }, VectorDiff::Insert { index, value } => { - SlidingSyncViewRoomsListDiff::Insert { index: index as u32, value: (&value).into() } + SlidingSyncListRoomsListDiff::Insert { index: index as u32, value: (&value).into() } } VectorDiff::Set { index, value } => { - SlidingSyncViewRoomsListDiff::Set { index: index as u32, value: (&value).into() } + SlidingSyncListRoomsListDiff::Set { index: index as u32, value: (&value).into() } } VectorDiff::Remove { index } => { - SlidingSyncViewRoomsListDiff::Remove { index: index as u32 } + SlidingSyncListRoomsListDiff::Remove { index: index as u32 } } VectorDiff::PushBack { value } => { - SlidingSyncViewRoomsListDiff::PushBack { value: (&value).into() } + SlidingSyncListRoomsListDiff::PushBack { value: (&value).into() } } VectorDiff::PushFront { value } => { - SlidingSyncViewRoomsListDiff::PushFront { value: (&value).into() } + SlidingSyncListRoomsListDiff::PushFront { value: (&value).into() } } - VectorDiff::PopBack => SlidingSyncViewRoomsListDiff::PopBack, - VectorDiff::PopFront => SlidingSyncViewRoomsListDiff::PopFront, - VectorDiff::Clear => SlidingSyncViewRoomsListDiff::Clear, + VectorDiff::PopBack => SlidingSyncListRoomsListDiff::PopBack, + VectorDiff::PopFront => SlidingSyncListRoomsListDiff::PopFront, + VectorDiff::Clear => SlidingSyncListRoomsListDiff::Clear, VectorDiff::Reset { values } => { warn!("Room list subscriber lagged behind and was reset"); - SlidingSyncViewRoomsListDiff::Reset { + SlidingSyncListRoomsListDiff::Reset { values: values.into_iter().map(|e| (&e).into()).collect(), } } @@ -348,25 +348,25 @@ impl From<&MatrixRoomEntry> for RoomListEntry { } } -pub trait SlidingSyncViewRoomItemsObserver: Sync + Send { +pub trait SlidingSyncListRoomItemsObserver: Sync + Send { fn did_receive_update(&self); } -pub trait SlidingSyncViewRoomListObserver: Sync + Send { - fn did_receive_update(&self, diff: SlidingSyncViewRoomsListDiff); +pub trait SlidingSyncListRoomListObserver: Sync + Send { + fn did_receive_update(&self, diff: SlidingSyncListRoomsListDiff); } -pub trait SlidingSyncViewRoomsCountObserver: Sync + Send { +pub trait SlidingSyncListRoomsCountObserver: Sync + Send { fn did_receive_update(&self, new_count: u32); } -pub trait SlidingSyncViewStateObserver: Sync + Send { +pub trait SlidingSyncListStateObserver: Sync + Send { fn did_receive_update(&self, new_state: SlidingSyncState); } #[derive(Clone)] -pub struct SlidingSyncViewBuilder { - inner: matrix_sdk::SlidingSyncViewBuilder, +pub struct SlidingSyncListBuilder { + inner: matrix_sdk::SlidingSyncListBuilder, } #[derive(uniffi::Record)] @@ -404,9 +404,9 @@ impl From for SyncRequestListFilters { } } -impl SlidingSyncViewBuilder { +impl SlidingSyncListBuilder { pub fn new() -> Self { - Self { inner: matrix_sdk::SlidingSyncView::builder() } + Self { inner: matrix_sdk::SlidingSyncList::builder() } } pub fn sync_mode(self: Arc, mode: SlidingSyncMode) -> Arc { @@ -427,14 +427,14 @@ impl SlidingSyncViewBuilder { Arc::new(builder) } - pub fn build(self: Arc) -> anyhow::Result> { + pub fn build(self: Arc) -> anyhow::Result> { let builder = unwrap_or_clone_arc(self); Ok(Arc::new(builder.inner.build()?.into())) } } #[uniffi::export] -impl SlidingSyncViewBuilder { +impl SlidingSyncListBuilder { pub fn sort(self: Arc, sort: Vec) -> Arc { let mut builder = unwrap_or_clone_arc(self); builder.inner = builder.inner.sort(sort); @@ -511,20 +511,20 @@ impl SlidingSyncViewBuilder { } #[derive(Clone)] -pub struct SlidingSyncView { - inner: matrix_sdk::SlidingSyncView, +pub struct SlidingSyncList { + inner: matrix_sdk::SlidingSyncList, } -impl From for SlidingSyncView { - fn from(inner: matrix_sdk::SlidingSyncView) -> Self { - SlidingSyncView { inner } +impl From for SlidingSyncList { + fn from(inner: matrix_sdk::SlidingSyncList) -> Self { + SlidingSyncList { inner } } } -impl SlidingSyncView { +impl SlidingSyncList { pub fn observe_state( &self, - observer: Box, + observer: Box, ) -> Arc { let mut state_stream = self.inner.state_stream(); @@ -539,7 +539,7 @@ impl SlidingSyncView { pub fn observe_room_list( &self, - observer: Box, + observer: Box, ) -> Arc { let mut rooms_list_stream = self.inner.rooms_list_stream(); @@ -554,7 +554,7 @@ impl SlidingSyncView { pub fn observe_room_items( &self, - observer: Box, + observer: Box, ) -> Arc { let mut rooms_updated = Observable::subscribe(&self.inner.rooms_updated_broadcast.read().unwrap()); @@ -569,7 +569,7 @@ impl SlidingSyncView { pub fn observe_rooms_count( &self, - observer: Box, + observer: Box, ) -> Arc { let mut rooms_count_stream = self.inner.rooms_count_stream(); @@ -584,7 +584,7 @@ impl SlidingSyncView { } #[uniffi::export] -impl SlidingSyncView { +impl SlidingSyncList { /// Get the current list of rooms pub fn current_rooms_list(&self) -> Vec { self.inner.rooms_list() @@ -709,16 +709,16 @@ impl SlidingSync { #[uniffi::export] impl SlidingSync { #[allow(clippy::significant_drop_in_scrutinee)] - pub fn get_view(&self, name: String) -> Option> { - self.inner.view(&name).map(|inner| Arc::new(SlidingSyncView { inner })) + pub fn get_view(&self, name: String) -> Option> { + self.inner.view(&name).map(|inner| Arc::new(SlidingSyncList { inner })) } - pub fn add_view(&self, view: Arc) -> Option> { - self.inner.add_view(view.inner.clone()).map(|inner| Arc::new(SlidingSyncView { inner })) + pub fn add_view(&self, view: Arc) -> Option> { + self.inner.add_view(view.inner.clone()).map(|inner| Arc::new(SlidingSyncList { inner })) } - pub fn pop_view(&self, name: String) -> Option> { - self.inner.pop_view(&name).map(|inner| Arc::new(SlidingSyncView { inner })) + pub fn pop_view(&self, name: String) -> Option> { + self.inner.pop_view(&name).map(|inner| Arc::new(SlidingSyncList { inner })) } pub fn add_common_extensions(&self) { @@ -810,7 +810,7 @@ impl SlidingSyncBuilder { Arc::new(builder) } - pub fn add_view(self: Arc, v: Arc) -> Arc { + pub fn add_view(self: Arc, v: Arc) -> Arc { let mut builder = unwrap_or_clone_arc(self); let view = unwrap_or_clone_arc(v); builder.inner = builder.inner.add_view(view.inner); diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index ae905703c..e8ce92a5f 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -59,7 +59,7 @@ pub use ruma::{IdParseError, OwnedServerName, ServerName}; #[cfg(feature = "experimental-sliding-sync")] pub use sliding_sync::{ RoomListEntry, SlidingSync, SlidingSyncBuilder, SlidingSyncMode, SlidingSyncRoom, - SlidingSyncState, SlidingSyncView, SlidingSyncViewBuilder, UpdateSummary, + SlidingSyncState, SlidingSyncList, SlidingSyncListBuilder, UpdateSummary, }; #[cfg(any(test, feature = "testing"))] diff --git a/crates/matrix-sdk/src/sliding_sync/builder.rs b/crates/matrix-sdk/src/sliding_sync/builder.rs index d6cddc8c2..799a8f89d 100644 --- a/crates/matrix-sdk/src/sliding_sync/builder.rs +++ b/crates/matrix-sdk/src/sliding_sync/builder.rs @@ -16,8 +16,8 @@ use tracing::trace; use url::Url; use super::{ - Error, FrozenSlidingSync, FrozenSlidingSyncView, SlidingSync, SlidingSyncRoom, SlidingSyncView, - SlidingSyncViewBuilder, + Error, FrozenSlidingSync, FrozenSlidingSyncList, SlidingSync, SlidingSyncRoom, SlidingSyncList, + SlidingSyncListBuilder, }; use crate::{Client, Result}; @@ -30,7 +30,7 @@ pub struct SlidingSyncBuilder { storage_key: Option, homeserver: Option, client: Option, - views: BTreeMap, + views: BTreeMap, extensions: Option, subscriptions: BTreeMap, } @@ -76,7 +76,7 @@ impl SlidingSyncBuilder { /// Convenience function to add a full-sync view to the builder pub fn add_fullsync_view(self) -> Self { self.add_view( - SlidingSyncViewBuilder::default_with_fullsync() + SlidingSyncListBuilder::default_with_fullsync() .build() .expect("Building default full sync view doesn't fail"), ) @@ -103,7 +103,7 @@ impl SlidingSyncBuilder { /// Add the given view to the views. /// /// Replace any view with the name. - pub fn add_view(mut self, v: SlidingSyncView) -> Self { + pub fn add_view(mut self, v: SlidingSyncList) -> Self { self.views.insert(v.name.clone(), v); self } @@ -242,12 +242,12 @@ impl SlidingSyncBuilder { .store() .get_custom_value(format!("{storage_key}::{name}").as_bytes()) .await? - .map(|v| serde_json::from_slice::(&v)) + .map(|v| serde_json::from_slice::(&v)) .transpose()? { trace!(name, "frozen for view found"); - let FrozenSlidingSyncView { rooms_count, rooms_list, rooms } = frozen_view; + let FrozenSlidingSyncList { rooms_count, rooms_list, rooms } = frozen_view; view.set_from_cold(rooms_count, rooms_list); for (key, frozen_room) in rooms.into_iter() { diff --git a/crates/matrix-sdk/src/sliding_sync/list.rs b/crates/matrix-sdk/src/sliding_sync/list.rs index eded7edcc..3110e9f0b 100644 --- a/crates/matrix-sdk/src/sliding_sync/list.rs +++ b/crates/matrix-sdk/src/sliding_sync/list.rs @@ -24,7 +24,6 @@ use crate::Result; /// Holding a specific filtered view within the concept of sliding sync. /// Main entrypoint to the SlidingSync /// -/// /// ```no_run /// # use futures::executor::block_on; /// # use matrix_sdk::Client; @@ -39,7 +38,7 @@ use crate::Result; /// # }); /// ``` #[derive(Clone, Debug)] -pub struct SlidingSyncView { +pub struct SlidingSyncList { /// Which SlidingSyncMode to start this view under sync_mode: SlidingSyncMode, @@ -92,7 +91,7 @@ pub struct SlidingSyncView { } #[derive(Serialize, Deserialize)] -pub(super) struct FrozenSlidingSyncView { +pub(super) struct FrozenSlidingSyncList { #[serde(default, skip_serializing_if = "Option::is_none")] pub(super) rooms_count: Option, #[serde(default, skip_serializing_if = "Vector::is_empty")] @@ -101,9 +100,9 @@ pub(super) struct FrozenSlidingSyncView { pub(super) rooms: BTreeMap, } -impl FrozenSlidingSyncView { +impl FrozenSlidingSyncList { pub(super) fn freeze( - source_view: &SlidingSyncView, + source_view: &SlidingSyncList, rooms_map: &BTreeMap, ) -> Self { let mut rooms = BTreeMap::new(); @@ -118,7 +117,7 @@ impl FrozenSlidingSyncView { rooms_list.push_back(entry.freeze()); } - FrozenSlidingSyncView { + FrozenSlidingSyncList { rooms_count: **source_view.rooms_count.read().unwrap(), rooms_list, rooms, @@ -126,7 +125,7 @@ impl FrozenSlidingSyncView { } } -impl SlidingSyncView { +impl SlidingSyncList { pub(crate) fn set_from_cold( &mut self, rooms_count: Option, @@ -141,13 +140,13 @@ impl SlidingSyncView { lock.append(rooms_list); } - /// Create a new [`SlidingSyncViewBuilder`]. - pub fn builder() -> SlidingSyncViewBuilder { - SlidingSyncViewBuilder::new() + /// Create a new [`SlidingSyncListBuilder`]. + pub fn builder() -> SlidingSyncListBuilder { + SlidingSyncListBuilder::new() } /// Return a builder with the same settings as before - pub fn new_builder(&self) -> SlidingSyncViewBuilder { + pub fn new_builder(&self) -> SlidingSyncListBuilder { Self::builder() .name(&self.name) .sync_mode(self.sync_mode.clone()) @@ -385,15 +384,15 @@ impl SlidingSyncView { Ok(changed) } - pub(super) fn request_generator(&self) -> SlidingSyncViewRequestGenerator { + pub(super) fn request_generator(&self) -> SlidingSyncListRequestGenerator { match &self.sync_mode { SlidingSyncMode::PagingFullSync => { - SlidingSyncViewRequestGenerator::new_with_paging_syncup(self.clone()) + SlidingSyncListRequestGenerator::new_with_paging_syncup(self.clone()) } SlidingSyncMode::GrowingFullSync => { - SlidingSyncViewRequestGenerator::new_with_growing_syncup(self.clone()) + SlidingSyncListRequestGenerator::new_with_growing_syncup(self.clone()) } - SlidingSyncMode::Selective => SlidingSyncViewRequestGenerator::new_live(self.clone()), + SlidingSyncMode::Selective => SlidingSyncListRequestGenerator::new_live(self.clone()), } } } @@ -401,9 +400,9 @@ impl SlidingSyncView { /// the default name for the full sync view pub const FULL_SYNC_VIEW_NAME: &str = "full-sync"; -/// Builder for [`SlidingSyncView`]. +/// Builder for [`SlidingSyncList`]. #[derive(Clone, Debug)] -pub struct SlidingSyncViewBuilder { +pub struct SlidingSyncListBuilder { sync_mode: SlidingSyncMode, sort: Vec, required_state: Vec<(StateEventType, String)>, @@ -419,7 +418,7 @@ pub struct SlidingSyncViewBuilder { ranges: Vec<(UInt, UInt)>, } -impl SlidingSyncViewBuilder { +impl SlidingSyncListBuilder { fn new() -> Self { Self { sync_mode: SlidingSyncMode::default(), @@ -533,11 +532,11 @@ impl SlidingSyncViewBuilder { } /// Build the view - pub fn build(self) -> Result { + pub fn build(self) -> Result { let mut rooms_list = ObservableVector::new(); rooms_list.append(self.rooms_list); - Ok(SlidingSyncView { + Ok(SlidingSyncList { sync_mode: self.sync_mode, sort: self.sort, required_state: self.required_state, @@ -557,20 +556,20 @@ impl SlidingSyncViewBuilder { } } -enum InnerSlidingSyncViewRequestGenerator { +enum InnerSlidingSyncListRequestGenerator { GrowingFullSync { position: u32, batch_size: u32, limit: Option, live: bool }, PagingFullSync { position: u32, batch_size: u32, limit: Option, live: bool }, Live, } -pub(super) struct SlidingSyncViewRequestGenerator { - view: SlidingSyncView, +pub(super) struct SlidingSyncListRequestGenerator { + view: SlidingSyncList, ranges: Vec<(usize, usize)>, - inner: InnerSlidingSyncViewRequestGenerator, + inner: InnerSlidingSyncListRequestGenerator, } -impl SlidingSyncViewRequestGenerator { - fn new_with_paging_syncup(view: SlidingSyncView) -> Self { +impl SlidingSyncListRequestGenerator { + fn new_with_paging_syncup(view: SlidingSyncList) -> Self { let batch_size = view.batch_size; let limit = view.limit; let position = view @@ -581,10 +580,10 @@ impl SlidingSyncViewRequestGenerator { .map(|(_start, end)| u32::try_from(*end).unwrap()) .unwrap_or_default(); - SlidingSyncViewRequestGenerator { + SlidingSyncListRequestGenerator { view, ranges: Default::default(), - inner: InnerSlidingSyncViewRequestGenerator::PagingFullSync { + inner: InnerSlidingSyncListRequestGenerator::PagingFullSync { position, batch_size, limit, @@ -593,7 +592,7 @@ impl SlidingSyncViewRequestGenerator { } } - fn new_with_growing_syncup(view: SlidingSyncView) -> Self { + fn new_with_growing_syncup(view: SlidingSyncList) -> Self { let batch_size = view.batch_size; let limit = view.limit; let position = view @@ -604,10 +603,10 @@ impl SlidingSyncViewRequestGenerator { .map(|(_start, end)| u32::try_from(*end).unwrap()) .unwrap_or_default(); - SlidingSyncViewRequestGenerator { + SlidingSyncListRequestGenerator { view, ranges: Default::default(), - inner: InnerSlidingSyncViewRequestGenerator::GrowingFullSync { + inner: InnerSlidingSyncListRequestGenerator::GrowingFullSync { position, batch_size, limit, @@ -616,11 +615,11 @@ impl SlidingSyncViewRequestGenerator { } } - fn new_live(view: SlidingSyncView) -> Self { - SlidingSyncViewRequestGenerator { + fn new_live(view: SlidingSyncList) -> Self { + SlidingSyncListRequestGenerator { view, ranges: Default::default(), - inner: InnerSlidingSyncViewRequestGenerator::Live, + inner: InnerSlidingSyncListRequestGenerator::Live, } } @@ -702,10 +701,10 @@ impl SlidingSyncViewRequestGenerator { trace!(end, max_index, range_end, name = self.view.name, "updating state"); match &mut self.inner { - InnerSlidingSyncViewRequestGenerator::PagingFullSync { + InnerSlidingSyncListRequestGenerator::PagingFullSync { position, live, limit, .. } - | InnerSlidingSyncViewRequestGenerator::GrowingFullSync { + | InnerSlidingSyncListRequestGenerator::GrowingFullSync { position, live, limit, .. } => { let max = limit.map(|limit| std::cmp::min(limit, max_index)).unwrap_or(max_index); @@ -729,7 +728,7 @@ impl SlidingSyncViewRequestGenerator { }); } } - InnerSlidingSyncViewRequestGenerator::Live => { + InnerSlidingSyncListRequestGenerator::Live => { Observable::update_eq(&mut self.view.state.write().unwrap(), |state| { *state = SlidingSyncState::Live; }); @@ -738,30 +737,30 @@ impl SlidingSyncViewRequestGenerator { } } -impl Iterator for SlidingSyncViewRequestGenerator { +impl Iterator for SlidingSyncListRequestGenerator { type Item = v4::SyncRequestList; fn next(&mut self) -> Option { match self.inner { - InnerSlidingSyncViewRequestGenerator::PagingFullSync { live, .. } - | InnerSlidingSyncViewRequestGenerator::GrowingFullSync { live, .. } + InnerSlidingSyncListRequestGenerator::PagingFullSync { live, .. } + | InnerSlidingSyncListRequestGenerator::GrowingFullSync { live, .. } if live => { Some(self.live_request()) } - InnerSlidingSyncViewRequestGenerator::PagingFullSync { + InnerSlidingSyncListRequestGenerator::PagingFullSync { position, batch_size, limit, .. } => Some(self.prefetch_request(position, batch_size, limit)), - InnerSlidingSyncViewRequestGenerator::GrowingFullSync { + InnerSlidingSyncListRequestGenerator::GrowingFullSync { position, batch_size, limit, .. } => Some(self.prefetch_request(0, position + batch_size, limit)), - InnerSlidingSyncViewRequestGenerator::Live => Some(self.live_request()), + InnerSlidingSyncListRequestGenerator::Live => Some(self.live_request()), } } } @@ -912,7 +911,7 @@ fn room_ops( Ok(()) } -/// The state the [`SlidingSyncView`] is in. +/// The state the [`SlidingSyncList`] is in. /// /// The lifetime of a SlidingSync usually starts at a `Preload`, getting a fast /// response for the first given number of Rooms, then switches into @@ -934,7 +933,7 @@ pub enum SlidingSyncState { Live, } -/// The mode by which the the [`SlidingSyncView`] is in fetching the data. +/// The mode by which the the [`SlidingSyncList`] is in fetching the data. #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum SlidingSyncMode { /// Fully sync all rooms in the background, page by page of `batch_size`, diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 81d45c0c9..515fc0a57 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -85,10 +85,10 @@ //! are **inclusive**) like so: //! //! ```rust -//! # use matrix_sdk::sliding_sync::{SlidingSyncView, SlidingSyncMode}; +//! # use matrix_sdk::sliding_sync::{SlidingSyncList, SlidingSyncMode}; //! use ruma::{assign, api::client::sync::sync_events::v4}; //! -//! let view_builder = SlidingSyncView::builder() +//! let view_builder = SlidingSyncList::builder() //! .name("main_view") //! .sync_mode(SlidingSyncMode::Selective) //! .filters(Some(assign!( @@ -100,7 +100,7 @@ //! //! Please refer to the [specification][MSC], the [Ruma types][ruma-types], //! specifically [`SyncRequestListFilter`](https://docs.rs/ruma/latest/ruma/api/client/sync/sync_events/v4/struct.SyncRequestListFilters.html) and the -//! [`SlidingSyncViewBuilder`] for details on the filters, sort-order and +//! [`SlidingSyncListBuilder`] for details on the filters, sort-order and //! range-options and data one requests to be sent. Once the view is fully //! configured, `build()` it and add the view to the sliding sync session //! by supplying it to [`add_view`][`SlidingSyncBuilder::add_view`]. @@ -110,9 +110,9 @@ //! copy can be retrieved by calling `SlidingSync::view()`, providing the name //! of the view. Next to the configuration settings (like name and //! `timeline_limit`), the view provides the stateful -//! [`rooms_count`](SlidingSyncView::rooms_count), -//! [`rooms_list`](SlidingSyncView::rooms_list) and -//! [`state`](SlidingSyncView::state): +//! [`rooms_count`](SlidingSyncList::rooms_count), +//! [`rooms_list`](SlidingSyncList::rooms_list) and +//! [`state`](SlidingSyncList::state): //! //! - `rooms_count` is the number of rooms _total_ there were found matching //! the filters given. @@ -143,8 +143,8 @@ //! every request till all rooms or until `limit` of rooms are in view. //! //! For both, one should configure -//! [`batch_size`](SlidingSyncViewBuilder::batch_size) and optionally -//! [`limit`](SlidingSyncViewBuilder::limit) on the [`SlidingSyncViewBuilder`]. +//! [`batch_size`](SlidingSyncListBuilder::batch_size) and optionally +//! [`limit`](SlidingSyncListBuilder::limit) on the [`SlidingSyncListBuilder`]. //! Both full-sync views will notice if the number of rooms increased at runtime //! and will attempt to catch up to that (barring the `limit`). //! @@ -281,7 +281,7 @@ //! # use futures::executor::block_on; //! # use futures::{pin_mut, StreamExt}; //! # use matrix_sdk::{ -//! # sliding_sync::{SlidingSyncMode, SlidingSyncViewBuilder}, +//! # sliding_sync::{SlidingSyncMode, SlidingSyncListBuilder}, //! # Client, //! # }; //! # use ruma::{ @@ -350,7 +350,7 @@ //! # use futures::executor::block_on; //! # use futures::{pin_mut, StreamExt}; //! # use matrix_sdk::{ -//! # sliding_sync::{SlidingSyncMode, SlidingSyncViewBuilder, SlidingSync, Error}, +//! # sliding_sync::{SlidingSyncMode, SlidingSyncListBuilder, SlidingSync, Error}, //! # Client, //! # }; //! # use ruma::{ @@ -438,12 +438,12 @@ //! present at `.build()`[`SlidingSyncBuilder::build`] sliding sync will attempt //! to load their latest cached version from storage, as well as some overall //! information of Sliding Sync. If that succeeded the views `state` has been -//! set to [`Preload`][SlidingSyncViewState::Preload]. Only room data of rooms +//! set to [`Preload`][SlidingSyncListState::Preload]. Only room data of rooms //! present in one of the views is loaded from storage. //! //! Once [#1441](https://github.com/matrix-org/matrix-rust-sdk/pull/1441) is merged //! one can disable caching on a per-view basis by setting -//! [`cold_cache(false)`][`SlidingSyncViewBuilder::cold_cache`] when +//! [`cold_cache(false)`][`SlidingSyncListBuilder::cold_cache`] when //! constructing the builder. //! //! Notice that views added after Sliding Sync has been built **will not be @@ -487,7 +487,7 @@ //! //! ```no_run //! # use futures::executor::block_on; -//! use matrix_sdk::{Client, sliding_sync::{SlidingSyncView, SlidingSyncMode}}; +//! use matrix_sdk::{Client, sliding_sync::{SlidingSyncList, SlidingSyncMode}}; //! use ruma::{assign, {api::client::sync::sync_events::v4, events::StateEventType}}; //! use tracing::{warn, error, info, debug}; //! use futures::{StreamExt, pin_mut}; @@ -504,7 +504,7 @@ //! .with_common_extensions() // we want the e2ee and to-device enabled, please //! .cold_cache("example-cache".to_owned()); // we want these to be loaded from and stored into the persistent storage //! -//! let full_sync_view = SlidingSyncView::builder() +//! let full_sync_view = SlidingSyncList::builder() //! .sync_mode(SlidingSyncMode::GrowingFullSync) // sync up by growing the window //! .name(&full_sync_view_name) // needed to lookup again. //! .sort(vec!["by_recency".to_owned()]) // ordered by most recent @@ -515,7 +515,7 @@ //! .limit(500) // only sync up the top 500 rooms //! .build()?; //! -//! let active_view = SlidingSyncView::builder() +//! let active_view = SlidingSyncList::builder() //! .name(&active_view_name) // the active window //! .sync_mode(SlidingSyncMode::Selective) // sync up the specific range only //! .set_range(0u32, 9u32) // only the top 10 items @@ -656,7 +656,7 @@ pub struct SlidingSync { delta_token: Arc>>>, /// The views of this sliding sync instance - views: Arc>>, + views: Arc>>, /// The rooms details rooms: Arc>>, @@ -714,7 +714,7 @@ impl SlidingSync { ) .await?; - // Write every `SlidingSyncView` inside the client the store. + // Write every `SlidingSyncList` inside the client the store. let frozen_views = { let rooms_lock = self.rooms.read().unwrap(); @@ -725,7 +725,7 @@ impl SlidingSync { .map(|(name, view)| { Ok(( format!("{storage_key}::{name}"), - serde_json::to_vec(&FrozenSlidingSyncView::freeze(view, &rooms_lock))?, + serde_json::to_vec(&FrozenSlidingSyncList::freeze(view, &rooms_lock))?, )) }) .collect::, crate::Error>>()? @@ -823,22 +823,22 @@ impl SlidingSync { .since = Some(since); } - /// Get access to the SlidingSyncView named `view_name` + /// Get access to the SlidingSyncList named `view_name` /// /// Note: Remember that this list might have been changed since you started /// listening to the stream and is therefor not necessarily up to date /// with the views used for the stream. - pub fn view(&self, view_name: &str) -> Option { + pub fn view(&self, view_name: &str) -> Option { self.views.read().unwrap().get(view_name).cloned() } - /// Remove the SlidingSyncView named `view_name` from the views list if + /// Remove the SlidingSyncList named `view_name` from the views list if /// found /// /// Note: Remember that this change will only be applicable for any new /// stream created after this. The old stream will still continue to use the /// previous set of views. - pub fn pop_view(&self, view_name: &String) -> Option { + pub fn pop_view(&self, view_name: &String) -> Option { self.views.write().unwrap().remove(view_name) } @@ -851,7 +851,7 @@ impl SlidingSync { /// Note: Remember that this change will only be applicable for any new /// stream created after this. The old stream will still continue to use the /// previous set of views. - pub fn add_view(&self, view: SlidingSyncView) -> Option { + pub fn add_view(&self, view: SlidingSyncList) -> Option { self.views.write().unwrap().insert(view.name.clone(), view) } @@ -876,7 +876,7 @@ impl SlidingSync { &self, sliding_sync_response: v4::Response, extensions: Option, - views: &mut BTreeMap, + views: &mut BTreeMap, ) -> Result { // Handle and transform a Sliding Sync Response to a `SyncResponse`. // @@ -966,7 +966,7 @@ impl SlidingSync { async fn sync_once( &self, - views: &mut BTreeMap, + views: &mut BTreeMap, ) -> Result> { let mut lists_of_requests = BTreeMap::new(); @@ -1175,7 +1175,7 @@ mod test { #[tokio::test] async fn check_find_room_in_view() -> Result<()> { let view = - SlidingSyncView::builder().name("testview").add_range(0u32, 9u32).build().unwrap(); + SlidingSyncList::builder().name("testview").add_range(0u32, 9u32).build().unwrap(); let full_window_update: v4::SyncOp = serde_json::from_value(json! ({ "op": "SYNC", "range": [0, 9], diff --git a/labs/jack-in/src/client/mod.rs b/labs/jack-in/src/client/mod.rs index a66d58d97..f1342cf4a 100644 --- a/labs/jack-in/src/client/mod.rs +++ b/labs/jack-in/src/client/mod.rs @@ -7,7 +7,7 @@ pub mod state; use matrix_sdk::{ ruma::{api::client::error::ErrorKind, OwnedRoomId}, - Client, SlidingSyncState, SlidingSyncViewBuilder, + Client, SlidingSyncState, SlidingSyncListBuilder, }; pub async fn run_client( @@ -17,7 +17,7 @@ pub async fn run_client( ) -> Result<()> { info!("Starting sliding sync now"); let builder = client.sliding_sync().await; - let mut full_sync_view_builder = SlidingSyncViewBuilder::default_with_fullsync() + let mut full_sync_view_builder = SlidingSyncListBuilder::default_with_fullsync() .timeline_limit(10u32) .sync_mode(config.full_sync_mode.into()); if let Some(size) = config.batch_size { diff --git a/labs/jack-in/src/client/state.rs b/labs/jack-in/src/client/state.rs index 4eb41da34..dbddbfb18 100644 --- a/labs/jack-in/src/client/state.rs +++ b/labs/jack-in/src/client/state.rs @@ -9,7 +9,7 @@ use futures::{pin_mut, StreamExt}; use matrix_sdk::{ room::timeline::{Timeline, TimelineItem}, ruma::{OwnedRoomId, RoomId}, - SlidingSync, SlidingSyncRoom, SlidingSyncState as ViewState, SlidingSyncView, + SlidingSync, SlidingSyncRoom, SlidingSyncState as ViewState, SlidingSyncList, }; use tokio::task::JoinHandle; @@ -24,7 +24,7 @@ pub struct CurrentRoomSummary { pub struct SlidingSyncState { started: Instant, syncer: SlidingSync, - view: SlidingSyncView, + view: SlidingSyncList, /// the current list selector for the room first_render: Option, full_sync: Option, @@ -36,7 +36,7 @@ pub struct SlidingSyncState { } impl SlidingSyncState { - pub fn new(syncer: SlidingSync, view: SlidingSyncView) -> Self { + pub fn new(syncer: SlidingSync, view: SlidingSyncList) -> Self { Self { started: Instant::now(), syncer, @@ -142,7 +142,7 @@ impl SlidingSyncState { self.first_render = Some(self.started.elapsed()) } - pub fn view(&self) -> &SlidingSyncView { + pub fn view(&self) -> &SlidingSyncList { &self.view } diff --git a/testing/sliding-sync-integration-test/src/lib.rs b/testing/sliding-sync-integration-test/src/lib.rs index 1c998f6e3..6a73b5bb0 100644 --- a/testing/sliding-sync-integration-test/src/lib.rs +++ b/testing/sliding-sync-integration-test/src/lib.rs @@ -82,7 +82,7 @@ mod tests { api::client::error::ErrorKind as RumaError, events::room::message::RoomMessageEventContent, uint, }, - SlidingSyncMode, SlidingSyncState, SlidingSyncView, + SlidingSyncMode, SlidingSyncState, SlidingSyncList, }; use super::*; @@ -109,7 +109,7 @@ mod tests { let sync = sync_builder .clone() .add_view( - SlidingSyncView::builder() + SlidingSyncList::builder() .sync_mode(SlidingSyncMode::Selective) .add_range(0u32, 1) .timeline_limit(0u32) @@ -170,7 +170,7 @@ mod tests { let sync = sync_builder .clone() .add_view( - SlidingSyncView::builder() + SlidingSyncList::builder() .sync_mode(SlidingSyncMode::Selective) .name("visible_rooms_view") .add_range(0u32, 1) @@ -329,7 +329,7 @@ mod tests { let (client, sync_proxy_builder) = random_setup_with_rooms(20).await?; let build_view = |name| { - SlidingSyncView::builder() + SlidingSyncList::builder() .sync_mode(SlidingSyncMode::Selective) .set_range(0u32, 10u32) .sort(vec!["by_recency".to_owned(), "by_name".to_owned()]) @@ -415,7 +415,7 @@ mod tests { let (client, sync_proxy_builder) = random_setup_with_rooms(20).await?; let build_view = |name| { - SlidingSyncView::builder() + SlidingSyncList::builder() .sync_mode(SlidingSyncMode::Selective) .set_range(0u32, 10u32) .sort(vec!["by_recency".to_owned(), "by_name".to_owned()]) @@ -526,14 +526,14 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn view_goes_live() -> anyhow::Result<()> { let (_client, sync_proxy_builder) = random_setup_with_rooms(21).await?; - let sliding_window_view = SlidingSyncView::builder() + let sliding_window_view = SlidingSyncList::builder() .sync_mode(SlidingSyncMode::Selective) .set_range(0u32, 10u32) .sort(vec!["by_recency".to_owned(), "by_name".to_owned()]) .name("sliding") .build()?; - let full = SlidingSyncView::builder() + let full = SlidingSyncList::builder() .sync_mode(SlidingSyncMode::GrowingFullSync) .batch_size(10u32) .sort(vec!["by_recency".to_owned(), "by_name".to_owned()]) @@ -574,7 +574,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn resizing_sliding_window() -> anyhow::Result<()> { let (_client, sync_proxy_builder) = random_setup_with_rooms(20).await?; - let sliding_window_view = SlidingSyncView::builder() + let sliding_window_view = SlidingSyncList::builder() .sync_mode(SlidingSyncMode::Selective) .set_range(0u32, 10u32) .sort(vec!["by_recency".to_owned(), "by_name".to_owned()]) @@ -678,7 +678,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn moving_out_of_sliding_window() -> anyhow::Result<()> { let (client, sync_proxy_builder) = random_setup_with_rooms(20).await?; - let sliding_window_view = SlidingSyncView::builder() + let sliding_window_view = SlidingSyncList::builder() .sync_mode(SlidingSyncMode::Selective) .set_range(1u32, 10u32) .sort(vec!["by_recency".to_owned(), "by_name".to_owned()]) @@ -828,13 +828,13 @@ mod tests { let (_client, sync_proxy_builder) = random_setup_with_rooms(500).await?; print!("setup took its time"); let build_views = || { - let sliding_window_view = SlidingSyncView::builder() + let sliding_window_view = SlidingSyncList::builder() .sync_mode(SlidingSyncMode::Selective) .set_range(1u32, 10u32) .sort(vec!["by_recency".to_owned(), "by_name".to_owned()]) .name("sliding") .build()?; - let growing_sync = SlidingSyncView::builder() + let growing_sync = SlidingSyncList::builder() .sync_mode(SlidingSyncMode::GrowingFullSync) .limit(100) .sort(vec!["by_recency".to_owned(), "by_name".to_owned()]) @@ -892,7 +892,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn growing_sync_keeps_going() -> anyhow::Result<()> { let (_client, sync_proxy_builder) = random_setup_with_rooms(50).await?; - let growing_sync = SlidingSyncView::builder() + let growing_sync = SlidingSyncList::builder() .sync_mode(SlidingSyncMode::GrowingFullSync) .batch_size(10u32) .sort(vec!["by_recency".to_owned(), "by_name".to_owned()]) @@ -944,7 +944,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn growing_sync_keeps_going_after_restart() -> anyhow::Result<()> { let (_client, sync_proxy_builder) = random_setup_with_rooms(50).await?; - let growing_sync = SlidingSyncView::builder() + let growing_sync = SlidingSyncList::builder() .sync_mode(SlidingSyncMode::GrowingFullSync) .batch_size(10u32) .sort(vec!["by_recency".to_owned(), "by_name".to_owned()]) @@ -1004,7 +1004,7 @@ mod tests { async fn continue_on_reset() -> anyhow::Result<()> { let (_client, sync_proxy_builder) = random_setup_with_rooms(30).await?; print!("setup took its time"); - let growing_sync = SlidingSyncView::builder() + let growing_sync = SlidingSyncList::builder() .sync_mode(SlidingSyncMode::GrowingFullSync) .limit(100) .sort(vec!["by_recency".to_owned(), "by_name".to_owned()]) @@ -1086,7 +1086,7 @@ mod tests { async fn noticing_new_rooms_in_growing() -> anyhow::Result<()> { let (client, sync_proxy_builder) = random_setup_with_rooms(30).await?; print!("setup took its time"); - let growing_sync = SlidingSyncView::builder() + let growing_sync = SlidingSyncList::builder() .sync_mode(SlidingSyncMode::GrowingFullSync) .limit(100) .sort(vec!["by_recency".to_owned(), "by_name".to_owned()]) @@ -1165,7 +1165,7 @@ mod tests { let sync_proxy = sync_proxy_builder .add_view( - SlidingSyncView::builder() + SlidingSyncList::builder() .sync_mode(SlidingSyncMode::Selective) .set_range(0u32, 2u32) .sort(vec!["by_recency".to_owned(), "by_name".to_owned()]) From 575eea470131c7f20a128f907e2d574cb5fe802f Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 12:19:30 +0100 Subject: [PATCH 073/166] fix(sdk): Deal with missing data in Sliding Sync's responses. The Sliding Sync server might not send some parts of the response, because they were sent before and the server wants to save bandwidth. So let's update values only when they exist. --- crates/matrix-sdk/src/sliding_sync/room.rs | 33 +++++++++++++++++----- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/room.rs b/crates/matrix-sdk/src/sliding_sync/room.rs index 908d12079..7da145844 100644 --- a/crates/matrix-sdk/src/sliding_sync/room.rs +++ b/crates/matrix-sdk/src/sliding_sync/room.rs @@ -153,14 +153,33 @@ impl SlidingSyncRoom { } = room_data; self.inner.unread_notifications = unread_notifications; - self.inner.name = name; - self.inner.initial = initial; - self.inner.is_dm = is_dm; - self.inner.invite_state = invite_state; - self.inner.required_state = required_state; - if let Some(batch) = prev_batch { - Observable::set(&mut self.prev_batch.write().unwrap(), Some(batch)); + // The server might not send some parts of the response, because they were sent + // before and the server wants to save bandwidth. So let's update the values + // only when they exist. + + if name.is_some() { + self.inner.name = name; + } + + if initial.is_some() { + self.inner.initial = initial; + } + + if is_dm.is_some() { + self.inner.is_dm = is_dm; + } + + if !invite_state.is_empty() { + self.inner.invite_state = invite_state; + } + + if !required_state.is_empty() { + self.inner.required_state = required_state; + } + + if prev_batch.is_some() { + Observable::set(&mut self.prev_batch.write().unwrap(), prev_batch); } // There is timeline updates. From cf83112bf0b8c596b49ff52644e8df800e9fdbe5 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 13:58:55 +0100 Subject: [PATCH 074/166] chore(sdk): Move `FrozenSlidingSyncList` lower in the file. --- crates/matrix-sdk/src/sliding_sync/list.rs | 71 +++++++++++----------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/list.rs b/crates/matrix-sdk/src/sliding_sync/list.rs index 3110e9f0b..9025be9e3 100644 --- a/crates/matrix-sdk/src/sliding_sync/list.rs +++ b/crates/matrix-sdk/src/sliding_sync/list.rs @@ -64,7 +64,6 @@ pub struct SlidingSyncList { /// The maximum number of timeline events to query for pub timeline_limit: Arc>>>, - // ----- Public state /// Name of this view to easily recognize them pub name: String, @@ -90,41 +89,6 @@ pub struct SlidingSyncList { is_cold: Arc, } -#[derive(Serialize, Deserialize)] -pub(super) struct FrozenSlidingSyncList { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub(super) rooms_count: Option, - #[serde(default, skip_serializing_if = "Vector::is_empty")] - pub(super) rooms_list: Vector, - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub(super) rooms: BTreeMap, -} - -impl FrozenSlidingSyncList { - pub(super) fn freeze( - source_view: &SlidingSyncList, - rooms_map: &BTreeMap, - ) -> Self { - let mut rooms = BTreeMap::new(); - let mut rooms_list = Vector::new(); - for entry in source_view.rooms_list.read().unwrap().iter() { - match entry { - RoomListEntry::Filled(o) | RoomListEntry::Invalidated(o) => { - rooms.insert(o.clone(), rooms_map.get(o).expect("rooms always exists").into()); - } - _ => {} - }; - - rooms_list.push_back(entry.freeze()); - } - FrozenSlidingSyncList { - rooms_count: **source_view.rooms_count.read().unwrap(), - rooms_list, - rooms, - } - } -} - impl SlidingSyncList { pub(crate) fn set_from_cold( &mut self, @@ -397,6 +361,41 @@ impl SlidingSyncList { } } +#[derive(Serialize, Deserialize)] +pub(super) struct FrozenSlidingSyncList { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(super) rooms_count: Option, + #[serde(default, skip_serializing_if = "Vector::is_empty")] + pub(super) rooms_list: Vector, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub(super) rooms: BTreeMap, +} + +impl FrozenSlidingSyncList { + pub(super) fn freeze( + source_view: &SlidingSyncList, + rooms_map: &BTreeMap, + ) -> Self { + let mut rooms = BTreeMap::new(); + let mut rooms_list = Vector::new(); + for entry in source_view.rooms_list.read().unwrap().iter() { + match entry { + RoomListEntry::Filled(o) | RoomListEntry::Invalidated(o) => { + rooms.insert(o.clone(), rooms_map.get(o).expect("rooms always exists").into()); + } + _ => {} + }; + + rooms_list.push_back(entry.freeze()); + } + FrozenSlidingSyncList { + rooms_count: **source_view.rooms_count.read().unwrap(), + rooms_list, + rooms, + } + } +} + /// the default name for the full sync view pub const FULL_SYNC_VIEW_NAME: &str = "full-sync"; From f03767318fa64b8b5faac0cd37c74571646c9b5b Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 14:18:40 +0100 Subject: [PATCH 075/166] chore(sdk): Update all mentions to `view` as `list` in `SlidingSync` and `SlidingSyncBuilder`. --- crates/matrix-sdk/src/sliding_sync/builder.rs | 47 +-- crates/matrix-sdk/src/sliding_sync/list.rs | 6 +- crates/matrix-sdk/src/sliding_sync/mod.rs | 268 +++++++++--------- 3 files changed, 161 insertions(+), 160 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/builder.rs b/crates/matrix-sdk/src/sliding_sync/builder.rs index 799a8f89d..c5a1c53c1 100644 --- a/crates/matrix-sdk/src/sliding_sync/builder.rs +++ b/crates/matrix-sdk/src/sliding_sync/builder.rs @@ -16,8 +16,8 @@ use tracing::trace; use url::Url; use super::{ - Error, FrozenSlidingSync, FrozenSlidingSyncList, SlidingSync, SlidingSyncRoom, SlidingSyncList, - SlidingSyncListBuilder, + Error, FrozenSlidingSync, FrozenSlidingSyncList, SlidingSync, SlidingSyncList, + SlidingSyncListBuilder, SlidingSyncRoom, }; use crate::{Client, Result}; @@ -30,7 +30,7 @@ pub struct SlidingSyncBuilder { storage_key: Option, homeserver: Option, client: Option, - views: BTreeMap, + lists: BTreeMap, extensions: Option, subscriptions: BTreeMap, } @@ -41,7 +41,7 @@ impl SlidingSyncBuilder { storage_key: None, homeserver: None, client: None, - views: BTreeMap::new(), + lists: BTreeMap::new(), extensions: None, subscriptions: BTreeMap::new(), } @@ -73,12 +73,12 @@ impl SlidingSyncBuilder { self } - /// Convenience function to add a full-sync view to the builder - pub fn add_fullsync_view(self) -> Self { - self.add_view( + /// Convenience function to add a full-sync list to the builder + pub fn add_fullsync_list(self) -> Self { + self.add_list( SlidingSyncListBuilder::default_with_fullsync() .build() - .expect("Building default full sync view doesn't fail"), + .expect("Building default full sync list doesn't fail"), ) } @@ -94,17 +94,18 @@ impl SlidingSyncBuilder { self } - /// Reset the views to `None` - pub fn no_views(mut self) -> Self { - self.views.clear(); + /// Reset the lists to `None` + pub fn no_lists(mut self) -> Self { + self.lists.clear(); self } - /// Add the given view to the views. + /// Add the given list to the lists. /// - /// Replace any view with the name. - pub fn add_view(mut self, v: SlidingSyncList) -> Self { - self.views.insert(v.name.clone(), v); + /// Replace any list with the name. + pub fn add_list(mut self, list: SlidingSyncList) -> Self { + self.lists.insert(list.name.clone(), list); + self } @@ -237,18 +238,18 @@ impl SlidingSyncBuilder { if let Some(storage_key) = &self.storage_key { trace!(storage_key, "trying to load from cold"); - for (name, view) in &mut self.views { - if let Some(frozen_view) = client + for (name, list) in &mut self.lists { + if let Some(frozen_list) = client .store() .get_custom_value(format!("{storage_key}::{name}").as_bytes()) .await? .map(|v| serde_json::from_slice::(&v)) .transpose()? { - trace!(name, "frozen for view found"); + trace!(name, "frozen for list found"); - let FrozenSlidingSyncList { rooms_count, rooms_list, rooms } = frozen_view; - view.set_from_cold(rooms_count, rooms_list); + let FrozenSlidingSyncList { rooms_count, rooms_list, rooms } = frozen_list; + list.set_from_cold(rooms_count, rooms_list); for (key, frozen_room) in rooms.into_iter() { rooms_found.entry(key).or_insert_with(|| { @@ -256,7 +257,7 @@ impl SlidingSyncBuilder { }); } } else { - trace!(name, "no frozen state for view found"); + trace!(name, "no frozen state for list found"); } } @@ -286,14 +287,14 @@ impl SlidingSyncBuilder { trace!(len = rooms_found.len(), "rooms unfrozen"); let rooms = Arc::new(StdRwLock::new(rooms_found)); - let views = Arc::new(StdRwLock::new(self.views)); + let lists = Arc::new(StdRwLock::new(self.lists)); Ok(SlidingSync { homeserver: self.homeserver, client, storage_key: self.storage_key, - views, + lists, rooms, extensions: Mutex::new(self.extensions).into(), diff --git a/crates/matrix-sdk/src/sliding_sync/list.rs b/crates/matrix-sdk/src/sliding_sync/list.rs index 9025be9e3..cb44ca22d 100644 --- a/crates/matrix-sdk/src/sliding_sync/list.rs +++ b/crates/matrix-sdk/src/sliding_sync/list.rs @@ -203,7 +203,7 @@ impl SlidingSyncList { /// Only matches against the current ranges and only against filled items. /// Invalid items are ignore. Return the total position the item was /// found in the room_list, return None otherwise. - pub fn find_room_in_view(&self, room_id: &RoomId) -> Option { + pub fn find_room_in_list(&self, room_id: &RoomId) -> Option { let ranges = self.ranges.read().unwrap(); let listing = self.rooms_list.read().unwrap(); for (start_uint, end_uint) in ranges.iter() { @@ -230,7 +230,7 @@ impl SlidingSyncList { /// Only matches against the current ranges and only against filled items. /// Invalid items are ignore. Return the total position the items that were /// found in the room_list, will skip any room not found in the rooms_list. - pub fn find_rooms_in_view(&self, room_ids: &[OwnedRoomId]) -> Vec<(usize, OwnedRoomId)> { + pub fn find_rooms_in_list(&self, room_ids: &[OwnedRoomId]) -> Vec<(usize, OwnedRoomId)> { let ranges = self.ranges.read().unwrap(); let listing = self.rooms_list.read().unwrap(); let mut rooms_found = Vec::new(); @@ -329,7 +329,7 @@ impl SlidingSyncList { } if self.send_updates_for_items && !rooms.is_empty() { - let found_views = self.find_rooms_in_view(rooms); + let found_views = self.find_rooms_in_list(rooms); if !found_views.is_empty() { debug!("room details found"); let mut rooms_list = self.rooms_list.write().unwrap(); diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 515fc0a57..02db2fdec 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -30,10 +30,10 @@ //! connections to stay up to date. On the client side these updates are applied //! and propagated through an [asynchronous reactive API](#reactive-api). //! -//! The protocol is split into three major sections for that: room -//! lists or [views](#views), the [room details](#rooms) and -//! [extensions](#extensions), most notably the end-to-end-encryption and -//! to-device extensions to enable full end-to-end-encryption support. +//! The protocol is split into three major sections for that: [lists][#lists], +//! the [room details](#rooms) and [extensions](#extensions), most notably the +//! end-to-end-encryption and to-device extensions to enable full +//! end-to-end-encryption support. //! //! ## Starting up //! @@ -65,31 +65,31 @@ //! # }); //! ``` //! -//! After the general configuration, one typically wants to add a view via the -//! [`add_view`][`SlidingSyncBuilder::add_view`] function. +//! After the general configuration, one typically wants to add a list via the +//! [`add_list`][`SlidingSyncBuilder::add_list`] function. //! -//! ## Views +//! ## Lists //! -//! A view defines a subset of matching rooms one wants to filter for, and be +//! A list defines a subset of matching rooms one wants to filter for, and be //! kept up about. The [`v4::SyncRequestListFilters`][] allows for a granular //! specification of the exact rooms one wants the server to select and the way -//! one wants them to be ordered before receiving. Secondly each view has a set +//! one wants them to be ordered before receiving. Secondly each list has a set //! of `ranges`: the subset of indexes of the entire list one is interested in //! and a unique name to be identified with. //! //! For example, a user might be part of thousands of rooms, but if the client //! app always starts by showing the most recent direct message conversations, //! loading all rooms is an inefficient approach. Instead with Sliding Sync one -//! defines a view (e.g. named `"main_view"`) filtering for `is_dm`, ordered -//! by recency and select to view the top 10 via `ranges: [ [0,9] ]` (indexes +//! defines a list (e.g. named `"main_list"`) filtering for `is_dm`, ordered +//! by recency and select to list the top 10 via `ranges: [ [0,9] ]` (indexes //! are **inclusive**) like so: //! //! ```rust //! # use matrix_sdk::sliding_sync::{SlidingSyncList, SlidingSyncMode}; //! use ruma::{assign, api::client::sync::sync_events::v4}; //! -//! let view_builder = SlidingSyncList::builder() -//! .name("main_view") +//! let list_builder = SlidingSyncList::builder() +//! .name("main_list") //! .sync_mode(SlidingSyncMode::Selective) //! .filters(Some(assign!( //! v4::SyncRequestListFilters::default(), { is_dm: Some(true)} @@ -101,15 +101,15 @@ //! Please refer to the [specification][MSC], the [Ruma types][ruma-types], //! specifically [`SyncRequestListFilter`](https://docs.rs/ruma/latest/ruma/api/client/sync/sync_events/v4/struct.SyncRequestListFilters.html) and the //! [`SlidingSyncListBuilder`] for details on the filters, sort-order and -//! range-options and data one requests to be sent. Once the view is fully -//! configured, `build()` it and add the view to the sliding sync session -//! by supplying it to [`add_view`][`SlidingSyncBuilder::add_view`]. +//! range-options and data one requests to be sent. Once the list is fully +//! configured, `build()` it and add the list to the sliding sync session +//! by supplying it to [`add_list`][`SlidingSyncBuilder::add_list`]. //! -//! Views are inherently stateful and all updates are applied on the shared -//! view-object. Once a view has been added to [`SlidingSync`], a cloned shared -//! copy can be retrieved by calling `SlidingSync::view()`, providing the name -//! of the view. Next to the configuration settings (like name and -//! `timeline_limit`), the view provides the stateful +//! Lists are inherently stateful and all updates are applied on the shared +//! list-object. Once a list has been added to [`SlidingSync`], a cloned shared +//! copy can be retrieved by calling `SlidingSync::list()`, providing the name +//! of the list. Next to the configuration settings (like name and +//! `timeline_limit`), the list provides the stateful //! [`rooms_count`](SlidingSyncList::rooms_count), //! [`rooms_list`](SlidingSyncList::rooms_list) and //! [`state`](SlidingSyncList::state): @@ -120,58 +120,58 @@ //! current state. `RoomListEntry`'s only hold `the room_id` if given, the //! [Rooms API](#rooms) holds the actual information about each room //! - `state` is a [`SlidingSyncMode`] signalling meta information about the -//! view and its stateful data — whether this is the state loaded from local -//! cache, whether the [full sync](#helper-views) is in progress or whether +//! list and its stateful data — whether this is the state loaded from local +//! cache, whether the [full sync](#helper-lists) is in progress or whether //! this is the current live information //! //! These are updated upon every update received from the server. One can query //! these for their current value at any time, or use the [Reactive API //! to subscribe to changes](#reactive-api). //! -//! ### Helper Views +//! ### Helper lists //! -//! By default views run in the [`Selective` mode](SlidingSyncMode::Selective). +//! By default lists run in the [`Selective` mode](SlidingSyncMode::Selective). //! That means one sets the desired range(s) to see explicitly (as described //! above). Very often, one still wants to load up the entire room list in -//! background though. For that, the client implementation offers to run views +//! background though. For that, the client implementation offers to run lists //! in two additional full-sync-modes, which require additional configuration: //! //! - [`SlidingSyncMode::PagingFullSync`]: Pages through the entire list of //! rooms one request at a time asking for the next `batch_size` number of //! rooms up to the end or `limit` if configured //! - [`SlidingSyncMode::GrowingFullSync`]: Grows the window by `batch_size` on -//! every request till all rooms or until `limit` of rooms are in view. +//! every request till all rooms or until `limit` of rooms are in list. //! //! For both, one should configure //! [`batch_size`](SlidingSyncListBuilder::batch_size) and optionally //! [`limit`](SlidingSyncListBuilder::limit) on the [`SlidingSyncListBuilder`]. -//! Both full-sync views will notice if the number of rooms increased at runtime +//! Both full-sync lists will notice if the number of rooms increased at runtime //! and will attempt to catch up to that (barring the `limit`). //! //! ## Rooms //! //! Next to the room list, the details for rooms are the next important aspect. -//! Each [view](#views) only references the [`OwnedRoomId`][ruma::OwnedRoomId] +//! Each [list](#lists) only references the [`OwnedRoomId`][ruma::OwnedRoomId] //! of the room at the given position. The details (`required_state`s and -//! timeline items) requested by all views are bundled, together with the common +//! timeline items) requested by all lists are bundled, together with the common //! details (e.g. whether it is a `dm` or its calculated name) and made //! available on the Sliding Sync session struct as a [reactive](#reactive-api) //! through [`.rooms`](SlidingSync::rooms), [`get_room`](SlidingSync::get_room) //! and [`get_rooms`](SlidingSync::get_rooms) APIs. //! //! Notably, this map only knows about the rooms that have come down [Sliding -//! Sync protocol][MSC] and if the given room isn't in any active view range, it +//! Sync protocol][MSC] and if the given room isn't in any active list range, it //! may be stale. Additionally to selecting the room data via the room lists, //! the [Sliding Sync protocol][MSC] allows to subscribe to specific rooms via //! the [`subscribe()`](SlidingSync::subscribe). Any room subscribed to will //! receive updates (with the given settings) regardless of whether they are -//! visible in any view. The most common case for using this API is when the +//! visible in any list. The most common case for using this API is when the //! user enters a room - as we want to receive the incoming new messages -//! regardless of whether the room is pushed out of the views room list. +//! regardless of whether the room is pushed out of the lists room list. //! //! ### Room List Entries //! -//! As the room list of each view is a vec of the `rooms_count` len but a room +//! As the room list of each list is a vec of the `rooms_count` len but a room //! may only know of a subset of entries for sure at any given time, these //! entries are wrapped in [`RoomListEntry`][]. This type, in close proximity to //! the [specification][MSC], can be either `Empty`, `Filled` or `Invalidated`, @@ -218,12 +218,12 @@ //! //! ## Timeline events //! -//! Both the view configuration as well as the [room subscription +//! Both the list configuration as well as the [room subscription //! settings](`v4::RoomSubscription`) allow to specify a `timeline_limit` to //! receive timeline events. If that is unset or set to 0, no events are sent by //! the server (which is the default), if multiple limits are found, the highest //! takes precedence. Any positive number indicates that on the first request a -//! room should come into view, up to that count of messages are sent +//! room should come into list, up to that count of messages are sent //! (depending how many the server has in cache). Following, whenever new events //! are found for the matching rooms, the server relays them to the client. //! @@ -239,7 +239,7 @@ //! //! To allow for a quick startup, client might want to request only a very low //! `timeline_limit` (maybe 1 or even 0) at first and update the count later on -//! the view or room subscription (see [reactive api](#reactive-api)), Since +//! the list or room subscription (see [reactive api](#reactive-api)), Since //! `0.99.0-rc1` the [sliding sync proxy][proxy] will then "paginate back" and //! resent the now larger number of events. All this is handled transparently. //! @@ -270,9 +270,9 @@ //! the [reactive structs](#reactive-api) and an //! [`Ok(UpdateSummary)`][`UpdateSummary`] is yielded with the minimum //! information, which data has been refreshed _in this iteration_: names of -//! views and `room_id`s of rooms. Note that, the same way that a view isn't +//! lists and `room_id`s of rooms. Note that, the same way that a list isn't //! reacting if only the room data has changed (but not its position in its -//! list), the view won't be mentioned here either, only the `room_id`. So be +//! list), the list won't be mentioned here either, only the `room_id`. So be //! sure to look at both for all subscribed objects. //! //! In full, this typically looks like this: @@ -295,7 +295,7 @@ //! let sliding_sync = client //! .sliding_sync() //! .await -//! // any views you want are added here. +//! // any lists you want are added here. //! .build() //! .await?; //! @@ -328,7 +328,7 @@ //! A main purpose of [Sliding Sync][MSC] is to provide an API for snappy end //! user applications. Long-polling on the other side means that we wait for the //! server to respond and that can take quite some time, before sending the next -//! request with our updates, for example an update in a view's `range`. +//! request with our updates, for example an update in a list's `range`. //! //! That is a bit unfortunate and leaks through the `stream` API as well. We are //! waiting for a `stream.next().await` call before the next request is sent. @@ -364,7 +364,7 @@ //! # let sliding_sync = client //! # .sliding_sync() //! # .await -//! # // any views you want are added here. +//! # // any lists you want are added here. //! # .build() //! # .await?; //! use std::sync::{Arc, atomic::{AtomicBool, Ordering}}; @@ -416,10 +416,10 @@ //! As the main source of truth is the data coming from the server, all updates //! must be applied transparently throughout to the data layer. The simplest //! way to stay up to date on what objects have changed is by checking the -//! [`views`](`UpdateSummary.views`) and [`rooms`](`UpdateSummary.rooms`) of +//! [`lists`](`UpdateSummary.lists`) and [`rooms`](`UpdateSummary.rooms`) of //! each [`UpdateSummary`] given by each stream iteration and update the local //! copies accordingly. Because of where the loop sits in the stack, that can -//! be a bit tedious though, so views and rooms have an additional way of +//! be a bit tedious though, so lists and rooms have an additional way of //! subscribing to updates via [`eyeball`]. //! //! The `Timeline` one can receive per room by calling @@ -429,26 +429,26 @@ //! ## Caching //! //! All room data, for filled but also _invalidated_ rooms, including the entire -//! timeline events as well as all view `room_lists` and `rooms_count` are held -//! in memory (unless one `pop`s the view out). +//! timeline events as well as all list `room_lists` and `rooms_count` are held +//! in memory (unless one `pop`s the list out). //! //! This is a purely in-memory cache layer though. If one wants Sliding Sync to //! persist and load from cold (storage) cache, one needs to set its key with -//! [`cold_cache(name)`][`SlidingSyncBuilder::cold_cache`] and for each view +//! [`cold_cache(name)`][`SlidingSyncBuilder::cold_cache`] and for each list //! present at `.build()`[`SlidingSyncBuilder::build`] sliding sync will attempt //! to load their latest cached version from storage, as well as some overall -//! information of Sliding Sync. If that succeeded the views `state` has been +//! information of Sliding Sync. If that succeeded the lists `state` has been //! set to [`Preload`][SlidingSyncListState::Preload]. Only room data of rooms -//! present in one of the views is loaded from storage. +//! present in one of the lists is loaded from storage. //! //! Once [#1441](https://github.com/matrix-org/matrix-rust-sdk/pull/1441) is merged -//! one can disable caching on a per-view basis by setting +//! one can disable caching on a per-list basis by setting //! [`cold_cache(false)`][`SlidingSyncListBuilder::cold_cache`] when //! constructing the builder. //! -//! Notice that views added after Sliding Sync has been built **will not be +//! Notice that lists added after Sliding Sync has been built **will not be //! loaded from cache** regardless of their settings (as this could lead to -//! inconsistencies between views). The same goes for any extension: some +//! inconsistencies between lists). The same goes for any extension: some //! extension data (like the to-device-message position) are stored to storage, //! but only retrieved upon `build()` of the `SlidingSyncBuilder`. So if one //! only adds them later, they will not be reading the data from storage (to @@ -495,8 +495,8 @@ //! # block_on(async { //! # let homeserver = Url::parse("http://example.com")?; //! # let client = Client::new(homeserver).await?; -//! let full_sync_view_name = "full-sync".to_owned(); -//! let active_view_name = "active-view".to_owned(); +//! let full_sync_list_name = "full-sync".to_owned(); +//! let active_list_name = "active-list".to_owned(); //! let sliding_sync_builder = client //! .sliding_sync() //! .await @@ -504,9 +504,9 @@ //! .with_common_extensions() // we want the e2ee and to-device enabled, please //! .cold_cache("example-cache".to_owned()); // we want these to be loaded from and stored into the persistent storage //! -//! let full_sync_view = SlidingSyncList::builder() +//! let full_sync_list = SlidingSyncList::builder() //! .sync_mode(SlidingSyncMode::GrowingFullSync) // sync up by growing the window -//! .name(&full_sync_view_name) // needed to lookup again. +//! .name(&full_sync_list_name) // needed to lookup again. //! .sort(vec!["by_recency".to_owned()]) // ordered by most recent //! .required_state(vec![ //! (StateEventType::RoomEncryption, "".to_owned()) @@ -515,8 +515,8 @@ //! .limit(500) // only sync up the top 500 rooms //! .build()?; //! -//! let active_view = SlidingSyncList::builder() -//! .name(&active_view_name) // the active window +//! let active_list = SlidingSyncList::builder() +//! .name(&active_list_name) // the active window //! .sync_mode(SlidingSyncMode::Selective) // sync up the specific range only //! .set_range(0u32, 9u32) // only the top 10 items //! .sort(vec!["by_recency".to_owned()]) // last active @@ -529,27 +529,27 @@ //! .build()?; //! //! let sliding_sync = sliding_sync_builder -//! .add_view(active_view) -//! .add_view(full_sync_view) +//! .add_list(active_view) +//! .add_list(full_sync_list) //! .build() //! .await?; //! -//! // subscribe to the view APIs for updates +//! // subscribe to the list APIs for updates //! -//! let active_view = sliding_sync.view(&active_view_name).unwrap(); -//! let view_state_stream = active_view.state_stream(); -//! let view_count_stream = active_view.rooms_count_stream(); -//! let view_list_stream = active_view.rooms_list_stream(); +//! let active_list = sliding_sync.list(&active_list_name).unwrap(); +//! let list_state_stream = active_list.state_stream(); +//! let list_count_stream = active_list.rooms_count_stream(); +//! let list_list_stream = active_list.rooms_list_stream(); //! //! tokio::spawn(async move { -//! pin_mut!(view_state_stream); -//! while let Some(new_state) = view_state_stream.next().await { -//! info!("active-view switched state to {new_state:?}"); +//! pin_mut!(list_state_stream); +//! while let Some(new_state) = list_state_stream.next().await { +//! info!("active-list switched state to {new_state:?}"); //! } //! }); //! //! tokio::spawn(async move { -//! pin_mut!(view_count_stream); +//! pin_mut!(list_count_stream); //! while let Some(new_count) = view_count_stream.next().await { //! info!("active-view new count: {new_count:?}"); //! } @@ -655,8 +655,8 @@ pub struct SlidingSync { delta_token: Arc>>>, - /// The views of this sliding sync instance - views: Arc>>, + /// The lists of this Sliding Sync instance. + lists: Arc>>, /// The rooms details rooms: Arc>>, @@ -718,7 +718,7 @@ impl SlidingSync { let frozen_views = { let rooms_lock = self.rooms.read().unwrap(); - self.views + self.lists .read() .unwrap() .iter() @@ -752,14 +752,14 @@ impl SlidingSync { .client(self.client.clone()) .subscriptions(self.subscriptions.read().unwrap().to_owned()); - for view in self.views.read().unwrap().values().map(|view| { - view.new_builder().build().expect("builder worked before, builder works now") + for list in self.lists.read().unwrap().values().map(|list| { + list.new_builder().build().expect("builder worked before, builder works now") }) { - builder = builder.add_view(view); + builder = builder.add_list(list); } - if let Some(h) = &self.homeserver { - builder.homeserver(h.clone()) + if let Some(homeserver) = &self.homeserver { + builder.homeserver(homeserver.clone()) } else { builder } @@ -823,36 +823,36 @@ impl SlidingSync { .since = Some(since); } - /// Get access to the SlidingSyncList named `view_name` + /// Get access to the SlidingSyncList named `list_name`. /// /// Note: Remember that this list might have been changed since you started /// listening to the stream and is therefor not necessarily up to date - /// with the views used for the stream. - pub fn view(&self, view_name: &str) -> Option { - self.views.read().unwrap().get(view_name).cloned() + /// with the lists used for the stream. + pub fn list(&self, list_name: &str) -> Option { + self.lists.read().unwrap().get(list_name).cloned() } - /// Remove the SlidingSyncList named `view_name` from the views list if - /// found + /// Remove the SlidingSyncList named `list_name` from the lists list if + /// found. /// /// Note: Remember that this change will only be applicable for any new /// stream created after this. The old stream will still continue to use the - /// previous set of views. - pub fn pop_view(&self, view_name: &String) -> Option { - self.views.write().unwrap().remove(view_name) + /// previous set of lists. + pub fn pop_list(&self, list_name: &String) -> Option { + self.lists.write().unwrap().remove(list_name) } - /// Add the view to the list of views + /// Add the list to the list of lists. /// - /// As views need to have a unique `.name`, if a view with the same name - /// is found the new view will replace the old one and the return it or + /// As lists need to have a unique `.name`, if a list with the same name + /// is found the new list will replace the old one and the return it or /// `None`. /// /// Note: Remember that this change will only be applicable for any new /// stream created after this. The old stream will still continue to use the - /// previous set of views. - pub fn add_view(&self, view: SlidingSyncList) -> Option { - self.views.write().unwrap().insert(view.name.clone(), view) + /// previous set of lists. + pub fn add_list(&self, list: SlidingSyncList) -> Option { + self.lists.write().unwrap().insert(list.name.clone(), list) } /// Lookup a set of rooms @@ -871,12 +871,12 @@ impl SlidingSync { } /// Handle the HTTP response. - #[instrument(skip_all, fields(views = views.len()))] + #[instrument(skip_all, fields(lists = list_generators.len()))] async fn handle_response( &self, sliding_sync_response: v4::Response, extensions: Option, - views: &mut BTreeMap, + list_generators: &mut BTreeMap, ) -> Result { // Handle and transform a Sliding Sync Response to a `SyncResponse`. // @@ -928,11 +928,11 @@ impl SlidingSync { rooms.push(room_id); } - let mut updated_views = Vec::new(); + let mut updated_lists = Vec::new(); for (name, updates) in sliding_sync_response.lists { - let Some(generator) = views.get_mut(&name) else { - error!("Response for view `{name}` - unknown to us; skipping"); + let Some(generator) = list_generators.get_mut(&name) else { + error!("Response for list `{name}` - unknown to us; skipping"); continue }; @@ -941,7 +941,7 @@ impl SlidingSync { updates.count.try_into().expect("the list total count convertible into u32"); if generator.handle_response(count, &updates.ops, &rooms)? { - updated_views.push(name.clone()); + updated_lists.push(name.clone()); } } @@ -956,7 +956,7 @@ impl SlidingSync { *self.sent_extensions.lock().unwrap() = extensions; } - UpdateSummary { views: updated_views, rooms } + UpdateSummary { lists: updated_lists, rooms } }; self.cache_to_storage().await?; @@ -966,27 +966,27 @@ impl SlidingSync { async fn sync_once( &self, - views: &mut BTreeMap, + list_generators: &mut BTreeMap, ) -> Result> { - let mut lists_of_requests = BTreeMap::new(); + let mut lists = BTreeMap::new(); { - let mut views_to_remove = Vec::new(); + let mut lists_to_remove = Vec::new(); - for (name, generator) in views.iter_mut() { + for (name, generator) in list_generators.iter_mut() { if let Some(request) = generator.next() { - lists_of_requests.insert(name.clone(), request); + lists.insert(name.clone(), request); } else { - views_to_remove.push(name.clone()); + lists_to_remove.push(name.clone()); } } - for view_name in views_to_remove { - views.remove(&view_name); + for list_name in lists_to_remove { + list_generators.remove(&list_name); } } - if views.is_empty() { + if list_generators.is_empty() { return Ok(None); } @@ -1017,7 +1017,7 @@ impl SlidingSync { // Prepare the request. let request = self.client.send_with_homeserver( assign!(v4::Request::new(), { - lists: lists_of_requests, + lists, pos, delta_token, timeout: Some(timeout), @@ -1056,7 +1056,7 @@ impl SlidingSync { debug!("Sliding sync response received"); - let updates = self.handle_response(response, extensions, views).await?; + let updates = self.handle_response(response, extensions, list_generators).await?; debug!("Sliding sync response has been handled"); @@ -1066,19 +1066,19 @@ impl SlidingSync { /// Create a _new_ Sliding Sync stream. /// /// This stream will send requests and will handle responses automatically, - /// hence updating the views. + /// hence updating the lists. #[instrument(name = "sync_stream", skip_all, parent = &self.client.root_span)] pub fn stream(&self) -> impl Stream> + '_ { - // Collect all the views that needsto be updated. - let mut views = { - let mut views = BTreeMap::new(); - let lock = self.views.read().unwrap(); + // Collect all the lists that need to be updated. + let mut list_generators = { + let mut list_generators = BTreeMap::new(); + let lock = self.lists.read().unwrap(); - for (name, view) in lock.iter() { - views.insert(name.clone(), view.request_generator()); + for (name, lists) in lock.iter() { + list_generators.insert(name.clone(), lists.request_generator()); } - views + list_generators }; debug!(?self.extensions, "About to run the sync stream"); @@ -1093,7 +1093,7 @@ impl SlidingSync { debug!(?self.extensions, "Sync stream loop is running"); }); - match self.sync_once(&mut views).instrument(sync_span.clone()).await { + match self.sync_once(&mut list_generators).instrument(sync_span.clone()).await { Ok(Some(updates)) => { self.reset_counter.store(0, Ordering::SeqCst); @@ -1159,8 +1159,8 @@ impl SlidingSync { /// [`SlidingSync::stream`]). #[derive(Debug, Clone)] pub struct UpdateSummary { - /// The names of the views that have seen an update. - pub views: Vec, + /// The names of the lists that have seen an update. + pub lists: Vec, /// The rooms that have seen updates pub rooms: Vec, } @@ -1173,9 +1173,9 @@ mod test { use super::*; #[tokio::test] - async fn check_find_room_in_view() -> Result<()> { - let view = - SlidingSyncList::builder().name("testview").add_range(0u32, 9u32).build().unwrap(); + async fn check_find_room_in_list() -> Result<()> { + let list = + SlidingSyncList::builder().name("testlist").add_range(0u32, 9u32).build().unwrap(); let full_window_update: v4::SyncOp = serde_json::from_value(json! ({ "op": "SYNC", "range": [0, 9], @@ -1194,18 +1194,18 @@ mod test { })) .unwrap(); - view.handle_response(10u32, &vec![full_window_update], &vec![(0, 9)], &vec![]).unwrap(); + list.handle_response(10u32, &vec![full_window_update], &vec![(0, 9)], &vec![]).unwrap(); let a02 = room_id!("!A00002:matrix.example").to_owned(); let a05 = room_id!("!A00005:matrix.example").to_owned(); let a09 = room_id!("!A00009:matrix.example").to_owned(); - assert_eq!(view.find_room_in_view(&a02), Some(2)); - assert_eq!(view.find_room_in_view(&a05), Some(5)); - assert_eq!(view.find_room_in_view(&a09), Some(9)); + assert_eq!(list.find_room_in_list(&a02), Some(2)); + assert_eq!(list.find_room_in_list(&a05), Some(5)); + assert_eq!(list.find_room_in_list(&a09), Some(9)); assert_eq!( - view.find_rooms_in_view(&[a02.clone(), a05.clone(), a09.clone()]), + list.find_rooms_in_list(&[a02.clone(), a05.clone(), a09.clone()]), vec![(2, a02.clone()), (5, a05.clone()), (9, a09.clone())] ); @@ -1216,14 +1216,14 @@ mod test { })) .unwrap(); - view.handle_response(10u32, &vec![update], &vec![(0, 3), (8, 9)], &vec![]).unwrap(); + list.handle_response(10u32, &vec![update], &vec![(0, 3), (8, 9)], &vec![]).unwrap(); - assert_eq!(view.find_room_in_view(room_id!("!A00002:matrix.example")), Some(2)); - assert_eq!(view.find_room_in_view(room_id!("!A00005:matrix.example")), None); - assert_eq!(view.find_room_in_view(room_id!("!A00009:matrix.example")), Some(9)); + assert_eq!(list.find_room_in_list(room_id!("!A00002:matrix.example")), Some(2)); + assert_eq!(list.find_room_in_list(room_id!("!A00005:matrix.example")), None); + assert_eq!(list.find_room_in_list(room_id!("!A00009:matrix.example")), Some(9)); assert_eq!( - view.find_rooms_in_view(&[a02.clone(), a05, a09.clone()]), + list.find_rooms_in_list(&[a02.clone(), a05, a09.clone()]), vec![(2, a02), (9, a09)] ); From 4e4c7450320e8e03304843bca78cd23abe9f78d1 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 14:20:44 +0100 Subject: [PATCH 076/166] chore(sdk): Update all mentions to `view` as `list` in `SlidingSyncList`. --- crates/matrix-sdk/src/sliding_sync/list.rs | 112 +++++++++++---------- 1 file changed, 57 insertions(+), 55 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/list.rs b/crates/matrix-sdk/src/sliding_sync/list.rs index cb44ca22d..d6725ba88 100644 --- a/crates/matrix-sdk/src/sliding_sync/list.rs +++ b/crates/matrix-sdk/src/sliding_sync/list.rs @@ -21,7 +21,7 @@ use tracing::{debug, error, instrument, trace, warn}; use super::{Error, FrozenSlidingSyncRoom, SlidingSyncRoom}; use crate::Result; -/// Holding a specific filtered view within the concept of sliding sync. +/// Holding a specific filtered list within the concept of sliding sync. /// Main entrypoint to the SlidingSync /// /// ```no_run @@ -32,14 +32,14 @@ use crate::Result; /// # let homeserver = Url::parse("http://example.com")?; /// let client = Client::new(homeserver).await?; /// let sliding_sync = -/// client.sliding_sync().await.add_fullsync_view().build().await?; +/// client.sliding_sync().await.add_fullsync_list().build().await?; /// /// # anyhow::Ok(()) /// # }); /// ``` #[derive(Clone, Debug)] pub struct SlidingSyncList { - /// Which SlidingSyncMode to start this view under + /// Which SlidingSyncMode to start this list under sync_mode: SlidingSyncMode, /// Sort the rooms list by this @@ -51,7 +51,7 @@ pub struct SlidingSyncList { /// How many rooms request at a time when doing a full-sync catch up batch_size: u32, - /// Whether the view should send `UpdatedAt`-Diff signals for rooms + /// Whether the list should send `UpdatedAt`-Diff signals for rooms /// that have changed send_updates_for_items: bool, @@ -64,10 +64,10 @@ pub struct SlidingSyncList { /// The maximum number of timeline events to query for pub timeline_limit: Arc>>>, - /// Name of this view to easily recognize them + /// Name of this list to easily recognize them pub name: String, - /// The state this view is in + /// The state this list is in state: Arc>>, /// The total known number of rooms, @@ -76,7 +76,7 @@ pub struct SlidingSyncList { /// The rooms in order rooms_list: Arc>>, - /// The ranges windows of the view + /// The ranges windows of the list #[allow(clippy::type_complexity)] // temporarily ranges: Arc>>>, @@ -198,7 +198,7 @@ impl SlidingSyncList { Observable::subscribe(&self.rooms_count.read().unwrap()) } - /// Find the current valid position of the room in the view room_list. + /// Find the current valid position of the room in the list room_list. /// /// Only matches against the current ranges and only against filled items. /// Invalid items are ignore. Return the total position the item was @@ -225,7 +225,7 @@ impl SlidingSyncList { None } - /// Find the current valid position of the rooms in the views room_list. + /// Find the current valid position of the rooms in the lists room_list. /// /// Only matches against the current ranges and only against filled items. /// Invalid items are ignore. Return the total position the items that were @@ -309,7 +309,7 @@ impl SlidingSyncList { } { - // keep the lock scoped so that the later find_rooms_in_view doesn't deadlock + // keep the lock scoped so that the later find_rooms_in_list doesn't deadlock let mut rooms_list = self.rooms_list.write().unwrap(); if !ops.is_empty() { @@ -329,11 +329,13 @@ impl SlidingSyncList { } if self.send_updates_for_items && !rooms.is_empty() { - let found_views = self.find_rooms_in_list(rooms); - if !found_views.is_empty() { + let found_lists = self.find_rooms_in_list(rooms); + + if !found_lists.is_empty() { debug!("room details found"); let mut rooms_list = self.rooms_list.write().unwrap(); - for (pos, room_id) in found_views { + + for (pos, room_id) in found_lists { // trigger an `UpdatedAt` update rooms_list.set(pos, RoomListEntry::Filled(room_id)); changed = true; @@ -373,12 +375,12 @@ pub(super) struct FrozenSlidingSyncList { impl FrozenSlidingSyncList { pub(super) fn freeze( - source_view: &SlidingSyncList, + source_list: &SlidingSyncList, rooms_map: &BTreeMap, ) -> Self { let mut rooms = BTreeMap::new(); let mut rooms_list = Vector::new(); - for entry in source_view.rooms_list.read().unwrap().iter() { + for entry in source_list.rooms_list.read().unwrap().iter() { match entry { RoomListEntry::Filled(o) | RoomListEntry::Invalidated(o) => { rooms.insert(o.clone(), rooms_map.get(o).expect("rooms always exists").into()); @@ -389,15 +391,15 @@ impl FrozenSlidingSyncList { rooms_list.push_back(entry.freeze()); } FrozenSlidingSyncList { - rooms_count: **source_view.rooms_count.read().unwrap(), + rooms_count: **source_list.rooms_count.read().unwrap(), rooms_list, rooms, } } } -/// the default name for the full sync view -pub const FULL_SYNC_VIEW_NAME: &str = "full-sync"; +/// the default name for the full sync list +pub const FULL_SYNC_LIST_NAME: &str = "full-sync"; /// Builder for [`SlidingSyncList`]. #[derive(Clone, Debug)] @@ -441,10 +443,10 @@ impl SlidingSyncListBuilder { /// Create a Builder set up for full sync pub fn default_with_fullsync() -> Self { - Self::new().name(FULL_SYNC_VIEW_NAME).sync_mode(SlidingSyncMode::PagingFullSync) + Self::new().name(FULL_SYNC_LIST_NAME).sync_mode(SlidingSyncMode::PagingFullSync) } - /// Which SlidingSyncMode to start this view under. + /// Which SlidingSyncMode to start this list under. pub fn sync_mode(mut self, value: SlidingSyncMode) -> Self { self.sync_mode = value; self @@ -468,7 +470,7 @@ impl SlidingSyncListBuilder { self } - /// Whether the view should send `UpdatedAt`-Diff signals for rooms that + /// Whether the list should send `UpdatedAt`-Diff signals for rooms that /// have changed. pub fn send_updates_for_items(mut self, value: bool) -> Self { self.send_updates_for_items = value; @@ -500,7 +502,7 @@ impl SlidingSyncListBuilder { self } - /// Set the name of this view, to easily recognize it. + /// Set the name of this list, to easily recognize it. pub fn name(mut self, value: impl Into) -> Self { self.name = Some(value.into()); self @@ -530,7 +532,7 @@ impl SlidingSyncListBuilder { self } - /// Build the view + /// Build the list pub fn build(self) -> Result { let mut rooms_list = ObservableVector::new(); rooms_list.append(self.rooms_list); @@ -562,16 +564,16 @@ enum InnerSlidingSyncListRequestGenerator { } pub(super) struct SlidingSyncListRequestGenerator { - view: SlidingSyncList, + list: SlidingSyncList, ranges: Vec<(usize, usize)>, inner: InnerSlidingSyncListRequestGenerator, } impl SlidingSyncListRequestGenerator { - fn new_with_paging_syncup(view: SlidingSyncList) -> Self { - let batch_size = view.batch_size; - let limit = view.limit; - let position = view + fn new_with_paging_syncup(list: SlidingSyncList) -> Self { + let batch_size = list.batch_size; + let limit = list.limit; + let position = list .ranges .read() .unwrap() @@ -580,7 +582,7 @@ impl SlidingSyncListRequestGenerator { .unwrap_or_default(); SlidingSyncListRequestGenerator { - view, + list, ranges: Default::default(), inner: InnerSlidingSyncListRequestGenerator::PagingFullSync { position, @@ -591,10 +593,10 @@ impl SlidingSyncListRequestGenerator { } } - fn new_with_growing_syncup(view: SlidingSyncList) -> Self { - let batch_size = view.batch_size; - let limit = view.limit; - let position = view + fn new_with_growing_syncup(list: SlidingSyncList) -> Self { + let batch_size = list.batch_size; + let limit = list.limit; + let position = list .ranges .read() .unwrap() @@ -603,7 +605,7 @@ impl SlidingSyncListRequestGenerator { .unwrap_or_default(); SlidingSyncListRequestGenerator { - view, + list, ranges: Default::default(), inner: InnerSlidingSyncListRequestGenerator::GrowingFullSync { position, @@ -614,9 +616,9 @@ impl SlidingSyncListRequestGenerator { } } - fn new_live(view: SlidingSyncList) -> Self { + fn new_live(list: SlidingSyncList) -> Self { SlidingSyncListRequestGenerator { - view, + list, ranges: Default::default(), inner: InnerSlidingSyncListRequestGenerator::Live, } @@ -635,7 +637,7 @@ impl SlidingSyncListRequestGenerator { _ => calc_end, }; - end = match self.view.rooms_count() { + end = match self.list.rooms_count() { Some(total_room_count) => std::cmp::min(end, total_room_count - 1), _ => end, }; @@ -643,12 +645,12 @@ impl SlidingSyncListRequestGenerator { self.make_request_for_ranges(vec![(start.into(), end.into())]) } - #[instrument(skip(self), fields(name = self.view.name))] + #[instrument(skip(self), fields(name = self.list.name))] fn make_request_for_ranges(&mut self, ranges: Vec<(UInt, UInt)>) -> v4::SyncRequestList { - let sort = self.view.sort.clone(); - let required_state = self.view.required_state.clone(); - let timeline_limit = **self.view.timeline_limit.read().unwrap(); - let filters = self.view.filters.clone(); + let sort = self.list.sort.clone(); + let required_state = self.list.required_state.clone(); + let timeline_limit = **self.list.timeline_limit.read().unwrap(); + let filters = self.list.filters.clone(); self.ranges = ranges .iter() @@ -673,18 +675,18 @@ impl SlidingSyncListRequestGenerator { // generate the next live request fn live_request(&mut self) -> v4::SyncRequestList { - let ranges = self.view.ranges.read().unwrap().clone(); + let ranges = self.list.ranges.read().unwrap().clone(); self.make_request_for_ranges(ranges) } - #[instrument(skip_all, fields(name = self.view.name, rooms_count, has_ops = !ops.is_empty()))] + #[instrument(skip_all, fields(name = self.list.name, rooms_count, has_ops = !ops.is_empty()))] pub(super) fn handle_response( &mut self, rooms_count: u32, ops: &Vec, rooms: &Vec, ) -> Result { - let res = self.view.handle_response(rooms_count, ops, &self.ranges, rooms)?; + let res = self.list.handle_response(rooms_count, ops, &self.ranges, rooms)?; self.update_state(rooms_count.saturating_sub(1)); // index is 0 based, count is 1 based Ok(res) } @@ -697,7 +699,7 @@ impl SlidingSyncListRequestGenerator { let end = if &(max_index as usize) < range_end { max_index } else { *range_end as u32 }; - trace!(end, max_index, range_end, name = self.view.name, "updating state"); + trace!(end, max_index, range_end, name = self.list.name, "updating state"); match &mut self.inner { InnerSlidingSyncListRequestGenerator::PagingFullSync { @@ -707,28 +709,28 @@ impl SlidingSyncListRequestGenerator { position, live, limit, .. } => { let max = limit.map(|limit| std::cmp::min(limit, max_index)).unwrap_or(max_index); - trace!(end, max, name = self.view.name, "updating state"); + trace!(end, max, name = self.list.name, "updating state"); if end >= max { - trace!(name = self.view.name, "going live"); + trace!(name = self.list.name, "going live"); // we are switching to live mode - self.view.set_range(0, max); + self.list.set_range(0, max); *position = max; *live = true; - Observable::update_eq(&mut self.view.state.write().unwrap(), |state| { + Observable::update_eq(&mut self.list.state.write().unwrap(), |state| { *state = SlidingSyncState::Live; }); } else { *position = end; *live = false; - self.view.set_range(0, end); - Observable::update_eq(&mut self.view.state.write().unwrap(), |state| { + self.list.set_range(0, end); + Observable::update_eq(&mut self.list.state.write().unwrap(), |state| { *state = SlidingSyncState::CatchingUp; }); } } InnerSlidingSyncListRequestGenerator::Live => { - Observable::update_eq(&mut self.view.state.write().unwrap(), |state| { + Observable::update_eq(&mut self.list.state.write().unwrap(), |state| { *state = SlidingSyncState::Live; }); } @@ -914,7 +916,7 @@ fn room_ops( /// /// The lifetime of a SlidingSync usually starts at a `Preload`, getting a fast /// response for the first given number of Rooms, then switches into -/// `CatchingUp` during which the view fetches the remaining rooms, usually in +/// `CatchingUp` during which the list fetches the remaining rooms, usually in /// order, some times in batches. Once that is ready, it switches into `Live`. /// /// If the client has been offline for a while, though, the SlidingSync might @@ -948,7 +950,7 @@ pub enum SlidingSyncMode { Selective, } -/// The Entry in the Sliding Sync room list per Sliding Sync view. +/// The Entry in the Sliding Sync room list per Sliding Sync list. #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub enum RoomListEntry { /// This entry isn't known at this point and thus considered `Empty`. From 146db59370b97b4a15d15b325233762e5aecddcf Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Mon, 27 Feb 2023 17:39:05 +0100 Subject: [PATCH 077/166] refactor(base): Add an Error type to StateStore trait --- crates/matrix-sdk-base/src/client.rs | 8 +- crates/matrix-sdk-base/src/rooms/normal.rs | 10 +- .../src/store/ambiguity_map.rs | 11 +- .../src/store/integration_tests.rs | 18 +- .../matrix-sdk-base/src/store/memory_store.rs | 2 + crates/matrix-sdk-base/src/store/mod.rs | 391 +---------- crates/matrix-sdk-base/src/store/traits.rs | 607 ++++++++++++++++++ .../matrix-sdk-indexeddb/src/state_store.rs | 2 + crates/matrix-sdk-sled/src/state_store.rs | 2 + crates/matrix-sdk/src/client/mod.rs | 6 +- 10 files changed, 653 insertions(+), 404 deletions(-) create mode 100644 crates/matrix-sdk-base/src/store/traits.rs diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index f3194c7e8..9813a0192 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -63,11 +63,11 @@ use crate::{ error::Result, rooms::{Room, RoomInfo, RoomType}, store::{ - ambiguity_map::AmbiguityCache, Result as StoreResult, StateChanges, StateStoreExt, Store, - StoreConfig, + ambiguity_map::AmbiguityCache, DynStateStore, Result as StoreResult, StateChanges, + StateStoreExt, Store, StoreConfig, }, sync::{JoinedRoom, LeftRoom, Rooms, SyncResponse, Timeline}, - Session, SessionMeta, SessionTokens, StateStore, + Session, SessionMeta, SessionTokens, }; /// A no IO Client implementation. @@ -175,7 +175,7 @@ impl BaseClient { /// Get a reference to the store. #[allow(unknown_lints, clippy::explicit_auto_deref)] - pub fn store(&self) -> &dyn StateStore { + pub fn store(&self) -> &DynStateStore { &*self.store } diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 9bb49fc38..ac86915df 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -40,7 +40,7 @@ use tracing::debug; use super::{BaseRoomInfo, DisplayName, RoomMember}; use crate::{ - store::{Result as StoreResult, StateStore, StateStoreExt}, + store::{DynStateStore, Result as StoreResult, StateStoreExt}, sync::UnreadNotificationsCount, MinimalStateEvent, }; @@ -52,7 +52,7 @@ pub struct Room { room_id: Arc, own_user_id: Arc, inner: Arc>, - store: Arc, + store: Arc, } /// The room summary containing member counts and members that should be used to @@ -83,7 +83,7 @@ pub enum RoomType { impl Room { pub(crate) fn new( own_user_id: &UserId, - store: Arc, + store: Arc, room_id: &RoomId, room_type: RoomType, ) -> Self { @@ -93,7 +93,7 @@ impl Room { pub(crate) fn restore( own_user_id: &UserId, - store: Arc, + store: Arc, room_info: RoomInfo, ) -> Self { Self { @@ -811,7 +811,7 @@ mod test { use super::*; use crate::{ - store::{MemoryStore, StateChanges}, + store::{MemoryStore, StateChanges, StateStore}, MinimalStateEvent, OriginalMinimalStateEvent, }; diff --git a/crates/matrix-sdk-base/src/store/ambiguity_map.rs b/crates/matrix-sdk-base/src/store/ambiguity_map.rs index bba99c617..4b52002d9 100644 --- a/crates/matrix-sdk-base/src/store/ambiguity_map.rs +++ b/crates/matrix-sdk-base/src/store/ambiguity_map.rs @@ -23,15 +23,12 @@ use ruma::{ }; use tracing::trace; -use super::{Result, StateChanges}; -use crate::{ - deserialized_responses::{AmbiguityChange, RawMemberEvent}, - StateStore, -}; +use super::{DynStateStore, Result, StateChanges}; +use crate::deserialized_responses::{AmbiguityChange, RawMemberEvent}; #[derive(Debug)] pub(crate) struct AmbiguityCache { - pub store: Arc, + pub store: Arc, pub cache: BTreeMap>>, pub changes: BTreeMap>, } @@ -72,7 +69,7 @@ impl AmbiguityMap { } impl AmbiguityCache { - pub fn new(store: Arc) -> Self { + pub fn new(store: Arc) -> Self { Self { store, cache: BTreeMap::new(), changes: BTreeMap::new() } } diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index 8e0aba27d..d2b029683 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -155,7 +155,10 @@ macro_rules! statestore_integration_tests { use serde_json::{json, Value as JsonValue}; use $crate::{ - store::{Result as StoreResult, StateChanges, StateStore, StateStoreExt}, + store::{ + DynStateStore, IntoStateStore, Result as StoreResult, StateChanges, StateStore, + StateStoreExt + }, RoomInfo, RoomType, }; @@ -181,7 +184,7 @@ macro_rules! statestore_integration_tests { } /// Populate the given `StateStore`. - pub async fn populate_store(store: Arc) -> StoreResult<()> { + pub async fn populate_store(store: Arc) -> StoreResult<()> { let mut changes = StateChanges::default(); let user_id = user_id(); @@ -372,7 +375,7 @@ macro_rules! statestore_integration_tests { let room_id = room_id(); let inner_store = get_store().await?; - let store = Arc::new(inner_store); + let store = inner_store.into_state_store(); populate_store(store.clone()).await?; assert!(store.get_sync_token().await?.is_some()); @@ -423,7 +426,7 @@ macro_rules! statestore_integration_tests { let user_id = user_id(); let inner_store = get_store().await?; - let store = Arc::new(inner_store); + let store = inner_store.into_state_store(); populate_store(store.clone()).await?; assert!(store.get_sync_token().await?.is_some()); @@ -840,7 +843,8 @@ macro_rules! statestore_integration_tests { async fn test_custom_storage() -> StoreResult<()> { let key = "my_key"; let value = &[0, 1, 2, 3]; - let store = get_store().await?; + let inner_store = get_store().await?; + let store = inner_store.into_state_store(); store.set_custom_value(key.as_bytes(), value.to_vec()).await?; @@ -854,7 +858,7 @@ macro_rules! statestore_integration_tests { #[async_test] async fn test_persist_invited_room() -> StoreResult<()> { let inner_store = get_store().await?; - let store = Arc::new(inner_store); + let store = inner_store.into_state_store(); populate_store(store.clone()).await?; assert_eq!(store.get_stripped_room_infos().await?.len(), 1); @@ -926,7 +930,7 @@ macro_rules! statestore_integration_tests { let inner_store = get_store().await?; let stripped_room_id = stripped_room_id(); - let store = Arc::new(inner_store); + let store = inner_store.into_state_store(); populate_store(store.clone()).await?; store.remove_room(room_id).await?; diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index ebba6faee..20a3972bf 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -594,6 +594,8 @@ impl MemoryStore { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl StateStore for MemoryStore { + type Error = StoreError; + async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<()> { self.save_filter(filter_name, filter_id).await } diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index c921528f1..60e45d1f4 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -21,7 +21,6 @@ //! store. use std::{ - borrow::Borrow, collections::{BTreeMap, BTreeSet}, fmt, ops::Deref, @@ -37,10 +36,10 @@ use once_cell::sync::OnceCell; #[cfg(any(test, feature = "testing"))] #[macro_use] pub mod integration_tests; +mod traits; -use async_trait::async_trait; use dashmap::DashMap; -use matrix_sdk_common::{locks::RwLock, AsyncTraitDeps}; +use matrix_sdk_common::locks::RwLock; #[cfg(feature = "e2e-encryption")] use matrix_sdk_crypto::store::{DynCryptoStore, IntoCryptoStore}; pub use matrix_sdk_store_encryption::Error as StoreEncryptionError; @@ -48,27 +47,22 @@ use ruma::{ api::client::push::get_notifications::v3::Notification, events::{ presence::PresenceEvent, - receipt::{Receipt, ReceiptEventContent, ReceiptThread, ReceiptType}, + receipt::ReceiptEventContent, room::{ member::{StrippedRoomMemberEvent, SyncRoomMemberEvent}, redaction::OriginalSyncRoomRedactionEvent, }, AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent, - AnySyncStateEvent, EmptyStateKey, GlobalAccountDataEvent, GlobalAccountDataEventContent, - GlobalAccountDataEventType, RedactContent, RedactedStateEventContent, RoomAccountDataEvent, - RoomAccountDataEventContent, RoomAccountDataEventType, StateEventType, StaticEventContent, - StaticStateEventContent, SyncStateEvent, + AnySyncStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType, }, serde::Raw, - EventId, MxcUri, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId, + EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId, }; /// BoxStream of owned Types pub type BoxStream = Pin + Send>>; use crate::{ - deserialized_responses::RawMemberEvent, - media::MediaRequest, rooms::{RoomInfo, RoomType}, MinimalRoomMemberEvent, Room, Session, SessionMeta, SessionTokens, }; @@ -76,7 +70,10 @@ use crate::{ pub(crate) mod ambiguity_map; mod memory_store; -pub use self::memory_store::MemoryStore; +pub use self::{ + memory_store::MemoryStore, + traits::{DynStateStore, IntoStateStore, StateStore, StateStoreExt}, +}; /// State store specific error type. #[derive(Debug, thiserror::Error)] @@ -135,375 +132,13 @@ impl StoreError { /// A `StateStore` specific result type. pub type Result = std::result::Result; -/// An abstract state store trait that can be used to implement different stores -/// for the SDK. -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait StateStore: AsyncTraitDeps { - /// Save the given filter id under the given name. - /// - /// # Arguments - /// - /// * `filter_name` - The name that should be used to store the filter id. - /// - /// * `filter_id` - The filter id that should be stored in the state store. - async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<()>; - - /// Save the set of state changes in the store. - async fn save_changes(&self, changes: &StateChanges) -> Result<()>; - - /// Get the filter id that was stored under the given filter name. - /// - /// # Arguments - /// - /// * `filter_name` - The name that was used to store the filter id. - async fn get_filter(&self, filter_name: &str) -> Result>; - - /// Get the last stored sync token. - async fn get_sync_token(&self) -> Result>; - - /// Get the stored presence event for the given user. - /// - /// # Arguments - /// - /// * `user_id` - The id of the user for which we wish to fetch the presence - /// event for. - async fn get_presence_event(&self, user_id: &UserId) -> Result>>; - - /// Get a state event out of the state store. - /// - /// # Arguments - /// - /// * `room_id` - The id of the room the state event was received for. - /// - /// * `event_type` - The event type of the state event. - async fn get_state_event( - &self, - room_id: &RoomId, - event_type: StateEventType, - state_key: &str, - ) -> Result>>; - - /// Get a list of state events for a given room and `StateEventType`. - /// - /// # Arguments - /// - /// * `room_id` - The id of the room to find events for. - /// - /// * `event_type` - The event type. - async fn get_state_events( - &self, - room_id: &RoomId, - event_type: StateEventType, - ) -> Result>>; - - /// Get the current profile for the given user in the given room. - /// - /// # Arguments - /// - /// * `room_id` - The room id the profile is used in. - /// - /// * `user_id` - The id of the user the profile belongs to. - async fn get_profile( - &self, - room_id: &RoomId, - user_id: &UserId, - ) -> Result>; - - /// Get the `MemberEvent` for the given state key in the given room id. - /// - /// # Arguments - /// - /// * `room_id` - The room id the member event belongs to. - /// - /// * `state_key` - The user id that the member event defines the state for. - async fn get_member_event( - &self, - room_id: &RoomId, - state_key: &UserId, - ) -> Result>; - - /// Get all the user ids of members for a given room, for stripped and - /// regular rooms alike. - async fn get_user_ids(&self, room_id: &RoomId) -> Result>; - - /// Get all the user ids of members that are in the invited state for a - /// given room, for stripped and regular rooms alike. - async fn get_invited_user_ids(&self, room_id: &RoomId) -> Result>; - - /// Get all the user ids of members that are in the joined state for a - /// given room, for stripped and regular rooms alike. - async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result>; - - /// Get all the pure `RoomInfo`s the store knows about. - async fn get_room_infos(&self) -> Result>; - - /// Get all the pure `RoomInfo`s the store knows about. - async fn get_stripped_room_infos(&self) -> Result>; - - /// Get all the users that use the given display name in the given room. - /// - /// # Arguments - /// - /// * `room_id` - The id of the room for which the display name users should - /// be fetched for. - /// - /// * `display_name` - The display name that the users use. - async fn get_users_with_display_name( - &self, - room_id: &RoomId, - display_name: &str, - ) -> Result>; - - /// Get an event out of the account data store. - /// - /// # Arguments - /// - /// * `event_type` - The event type of the account data event. - async fn get_account_data_event( - &self, - event_type: GlobalAccountDataEventType, - ) -> Result>>; - - /// Get an event out of the room account data store. - /// - /// # Arguments - /// - /// * `room_id` - The id of the room for which the room account data event - /// should - /// be fetched. - /// - /// * `event_type` - The event type of the room account data event. - async fn get_room_account_data_event( - &self, - room_id: &RoomId, - event_type: RoomAccountDataEventType, - ) -> Result>>; - - /// Get an event out of the user room receipt store. - /// - /// # Arguments - /// - /// * `room_id` - The id of the room for which the receipt should be - /// fetched. - /// - /// * `receipt_type` - The type of the receipt. - /// - /// * `thread` - The thread containing this receipt. - /// - /// * `user_id` - The id of the user for who the receipt should be fetched. - async fn get_user_room_receipt_event( - &self, - room_id: &RoomId, - receipt_type: ReceiptType, - thread: ReceiptThread, - user_id: &UserId, - ) -> Result>; - - /// Get events out of the event room receipt store. - /// - /// # Arguments - /// - /// * `room_id` - The id of the room for which the receipts should be - /// fetched. - /// - /// * `receipt_type` - The type of the receipts. - /// - /// * `thread` - The thread containing this receipt. - /// - /// * `event_id` - The id of the event for which the receipts should be - /// fetched. - async fn get_event_room_receipt_events( - &self, - room_id: &RoomId, - receipt_type: ReceiptType, - thread: ReceiptThread, - event_id: &EventId, - ) -> Result>; - - /// Get arbitrary data from the custom store - /// - /// # Arguments - /// - /// * `key` - The key to fetch data for - async fn get_custom_value(&self, key: &[u8]) -> Result>>; - - /// Put arbitrary data into the custom store - /// - /// # Arguments - /// - /// * `key` - The key to insert data into - /// - /// * `value` - The value to insert - async fn set_custom_value(&self, key: &[u8], value: Vec) -> Result>>; - - /// Add a media file's content in the media store. - /// - /// # Arguments - /// - /// * `request` - The `MediaRequest` of the file. - /// - /// * `content` - The content of the file. - async fn add_media_content(&self, request: &MediaRequest, content: Vec) -> Result<()>; - - /// Get a media file's content out of the media store. - /// - /// # Arguments - /// - /// * `request` - The `MediaRequest` of the file. - async fn get_media_content(&self, request: &MediaRequest) -> Result>>; - - /// Removes a media file's content from the media store. - /// - /// # Arguments - /// - /// * `request` - The `MediaRequest` of the file. - async fn remove_media_content(&self, request: &MediaRequest) -> Result<()>; - - /// Removes all the media files' content associated to an `MxcUri` from the - /// media store. - /// - /// # Arguments - /// - /// * `uri` - The `MxcUri` of the media files. - async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()>; - - /// Removes a room and all elements associated from the state store. - /// - /// # Arguments - /// - /// * `room_id` - The `RoomId` of the room to delete. - async fn remove_room(&self, room_id: &RoomId) -> Result<()>; -} - -/// Convenience functionality for state stores. -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait StateStoreExt: StateStore { - /// Get a specific state event of statically-known type. - /// - /// # Arguments - /// - /// * `room_id` - The id of the room the state event was received for. - async fn get_state_event_static( - &self, - room_id: &RoomId, - ) -> Result>>> - where - C: StaticEventContent + StaticStateEventContent + RedactContent, - C::Redacted: RedactedStateEventContent, - { - Ok(self.get_state_event(room_id, C::TYPE.into(), "").await?.map(Raw::cast)) - } - - /// Get a specific state event of statically-known type. - /// - /// # Arguments - /// - /// * `room_id` - The id of the room the state event was received for. - async fn get_state_event_static_for_key( - &self, - room_id: &RoomId, - state_key: &K, - ) -> Result>>> - where - C: StaticEventContent + StaticStateEventContent + RedactContent, - C::StateKey: Borrow, - C::Redacted: RedactedStateEventContent, - K: AsRef + ?Sized + Sync, - { - Ok(self.get_state_event(room_id, C::TYPE.into(), state_key.as_ref()).await?.map(Raw::cast)) - } - - /// Get a list of state events of a statically-known type for a given room. - /// - /// # Arguments - /// - /// * `room_id` - The id of the room to find events for. - async fn get_state_events_static( - &self, - room_id: &RoomId, - ) -> Result>>> - where - C: StaticEventContent + StaticStateEventContent + RedactContent, - C::Redacted: RedactedStateEventContent, - { - // FIXME: Could be more efficient, if we had streaming store accessor functions - Ok(self - .get_state_events(room_id, C::TYPE.into()) - .await? - .into_iter() - .map(Raw::cast) - .collect()) - } - - /// Get an event of a statically-known type from the account data store. - async fn get_account_data_event_static( - &self, - ) -> Result>>> - where - C: StaticEventContent + GlobalAccountDataEventContent, - { - Ok(self.get_account_data_event(C::TYPE.into()).await?.map(Raw::cast)) - } - - /// Get an event of a statically-known type from the room account data - /// store. - /// - /// # Arguments - /// - /// * `room_id` - The id of the room for which the room account data event - /// should be fetched. - async fn get_room_account_data_event_static( - &self, - room_id: &RoomId, - ) -> Result>>> - where - C: StaticEventContent + RoomAccountDataEventContent, - { - Ok(self.get_room_account_data_event(room_id, C::TYPE.into()).await?.map(Raw::cast)) - } -} - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl StateStoreExt for T {} - -/// A type that can be type-erased into `Arc`. -/// -/// This trait is not meant to be implemented directly outside -/// `matrix-sdk-crypto`, but it is automatically implemented for everything that -/// implements `StateStore`. -pub trait IntoStateStore { - #[doc(hidden)] - fn into_state_store(self) -> Arc; -} - -impl IntoStateStore for T -where - T: StateStore + Sized + 'static, -{ - fn into_state_store(self) -> Arc { - Arc::new(self) - } -} - -impl IntoStateStore for Arc -where - T: StateStore + 'static, -{ - fn into_state_store(self) -> Arc { - self - } -} - /// A state store wrapper for the SDK. /// /// This adds additional higher level store functionality on top of a /// `StateStore` implementation. #[derive(Clone)] pub(crate) struct Store { - pub(super) inner: Arc, + pub(super) inner: Arc, session_meta: Arc>, pub(super) session_tokens: SharedObservable>, /// The current sync token that should be used for the next sync call. @@ -520,7 +155,7 @@ pub(crate) struct Store { impl Store { /// Create a new store, wrapping the given `StateStore` - pub fn new(inner: Arc) -> Self { + pub fn new(inner: Arc) -> Self { Self { inner, session_meta: Default::default(), @@ -662,7 +297,7 @@ impl fmt::Debug for Store { } impl Deref for Store { - type Target = dyn StateStore; + type Target = DynStateStore; fn deref(&self) -> &Self::Target { self.inner.deref() @@ -840,7 +475,7 @@ impl StateChanges { pub struct StoreConfig { #[cfg(feature = "e2e-encryption")] pub(crate) crypto_store: Arc, - pub(crate) state_store: Arc, + pub(crate) state_store: Arc, } #[cfg(not(tarpaulin_include))] diff --git a/crates/matrix-sdk-base/src/store/traits.rs b/crates/matrix-sdk-base/src/store/traits.rs new file mode 100644 index 000000000..d3968db87 --- /dev/null +++ b/crates/matrix-sdk-base/src/store/traits.rs @@ -0,0 +1,607 @@ +// Copyright 2023 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{borrow::Borrow, collections::BTreeSet, fmt, sync::Arc}; + +use async_trait::async_trait; +use matrix_sdk_common::AsyncTraitDeps; +use ruma::{ + events::{ + presence::PresenceEvent, + receipt::{Receipt, ReceiptThread, ReceiptType}, + AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnySyncStateEvent, EmptyStateKey, + GlobalAccountDataEvent, GlobalAccountDataEventContent, GlobalAccountDataEventType, + RedactContent, RedactedStateEventContent, RoomAccountDataEvent, + RoomAccountDataEventContent, RoomAccountDataEventType, StateEventType, StaticEventContent, + StaticStateEventContent, SyncStateEvent, + }, + serde::Raw, + EventId, MxcUri, OwnedEventId, OwnedUserId, RoomId, UserId, +}; + +use super::{StateChanges, StoreError}; +use crate::{ + deserialized_responses::RawMemberEvent, media::MediaRequest, MinimalRoomMemberEvent, RoomInfo, +}; + +/// An abstract state store trait that can be used to implement different stores +/// for the SDK. +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait StateStore: AsyncTraitDeps { + /// The error type used by this state store. + type Error: fmt::Debug + Into; + + /// Save the given filter id under the given name. + /// + /// # Arguments + /// + /// * `filter_name` - The name that should be used to store the filter id. + /// + /// * `filter_id` - The filter id that should be stored in the state store. + async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<(), Self::Error>; + + /// Save the set of state changes in the store. + async fn save_changes(&self, changes: &StateChanges) -> Result<(), Self::Error>; + + /// Get the filter id that was stored under the given filter name. + /// + /// # Arguments + /// + /// * `filter_name` - The name that was used to store the filter id. + async fn get_filter(&self, filter_name: &str) -> Result, Self::Error>; + + /// Get the last stored sync token. + async fn get_sync_token(&self) -> Result, Self::Error>; + + /// Get the stored presence event for the given user. + /// + /// # Arguments + /// + /// * `user_id` - The id of the user for which we wish to fetch the presence + /// event for. + async fn get_presence_event( + &self, + user_id: &UserId, + ) -> Result>, Self::Error>; + + /// Get a state event out of the state store. + /// + /// # Arguments + /// + /// * `room_id` - The id of the room the state event was received for. + /// + /// * `event_type` - The event type of the state event. + async fn get_state_event( + &self, + room_id: &RoomId, + event_type: StateEventType, + state_key: &str, + ) -> Result>, Self::Error>; + + /// Get a list of state events for a given room and `StateEventType`. + /// + /// # Arguments + /// + /// * `room_id` - The id of the room to find events for. + /// + /// * `event_type` - The event type. + async fn get_state_events( + &self, + room_id: &RoomId, + event_type: StateEventType, + ) -> Result>, Self::Error>; + + /// Get the current profile for the given user in the given room. + /// + /// # Arguments + /// + /// * `room_id` - The room id the profile is used in. + /// + /// * `user_id` - The id of the user the profile belongs to. + async fn get_profile( + &self, + room_id: &RoomId, + user_id: &UserId, + ) -> Result, Self::Error>; + + /// Get the `MemberEvent` for the given state key in the given room id. + /// + /// # Arguments + /// + /// * `room_id` - The room id the member event belongs to. + /// + /// * `state_key` - The user id that the member event defines the state for. + async fn get_member_event( + &self, + room_id: &RoomId, + state_key: &UserId, + ) -> Result, Self::Error>; + + /// Get all the user ids of members for a given room, for stripped and + /// regular rooms alike. + async fn get_user_ids(&self, room_id: &RoomId) -> Result, Self::Error>; + + /// Get all the user ids of members that are in the invited state for a + /// given room, for stripped and regular rooms alike. + async fn get_invited_user_ids(&self, room_id: &RoomId) + -> Result, Self::Error>; + + /// Get all the user ids of members that are in the joined state for a + /// given room, for stripped and regular rooms alike. + async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result, Self::Error>; + + /// Get all the pure `RoomInfo`s the store knows about. + async fn get_room_infos(&self) -> Result, Self::Error>; + + /// Get all the pure `RoomInfo`s the store knows about. + async fn get_stripped_room_infos(&self) -> Result, Self::Error>; + + /// Get all the users that use the given display name in the given room. + /// + /// # Arguments + /// + /// * `room_id` - The id of the room for which the display name users should + /// be fetched for. + /// + /// * `display_name` - The display name that the users use. + async fn get_users_with_display_name( + &self, + room_id: &RoomId, + display_name: &str, + ) -> Result, Self::Error>; + + /// Get an event out of the account data store. + /// + /// # Arguments + /// + /// * `event_type` - The event type of the account data event. + async fn get_account_data_event( + &self, + event_type: GlobalAccountDataEventType, + ) -> Result>, Self::Error>; + + /// Get an event out of the room account data store. + /// + /// # Arguments + /// + /// * `room_id` - The id of the room for which the room account data event + /// should + /// be fetched. + /// + /// * `event_type` - The event type of the room account data event. + async fn get_room_account_data_event( + &self, + room_id: &RoomId, + event_type: RoomAccountDataEventType, + ) -> Result>, Self::Error>; + + /// Get an event out of the user room receipt store. + /// + /// # Arguments + /// + /// * `room_id` - The id of the room for which the receipt should be + /// fetched. + /// + /// * `receipt_type` - The type of the receipt. + /// + /// * `thread` - The thread containing this receipt. + /// + /// * `user_id` - The id of the user for who the receipt should be fetched. + async fn get_user_room_receipt_event( + &self, + room_id: &RoomId, + receipt_type: ReceiptType, + thread: ReceiptThread, + user_id: &UserId, + ) -> Result, Self::Error>; + + /// Get events out of the event room receipt store. + /// + /// # Arguments + /// + /// * `room_id` - The id of the room for which the receipts should be + /// fetched. + /// + /// * `receipt_type` - The type of the receipts. + /// + /// * `thread` - The thread containing this receipt. + /// + /// * `event_id` - The id of the event for which the receipts should be + /// fetched. + async fn get_event_room_receipt_events( + &self, + room_id: &RoomId, + receipt_type: ReceiptType, + thread: ReceiptThread, + event_id: &EventId, + ) -> Result, Self::Error>; + + /// Get arbitrary data from the custom store + /// + /// # Arguments + /// + /// * `key` - The key to fetch data for + async fn get_custom_value(&self, key: &[u8]) -> Result>, Self::Error>; + + /// Put arbitrary data into the custom store + /// + /// # Arguments + /// + /// * `key` - The key to insert data into + /// + /// * `value` - The value to insert + async fn set_custom_value( + &self, + key: &[u8], + value: Vec, + ) -> Result>, Self::Error>; + + /// Add a media file's content in the media store. + /// + /// # Arguments + /// + /// * `request` - The `MediaRequest` of the file. + /// + /// * `content` - The content of the file. + async fn add_media_content( + &self, + request: &MediaRequest, + content: Vec, + ) -> Result<(), Self::Error>; + + /// Get a media file's content out of the media store. + /// + /// # Arguments + /// + /// * `request` - The `MediaRequest` of the file. + async fn get_media_content( + &self, + request: &MediaRequest, + ) -> Result>, Self::Error>; + + /// Removes a media file's content from the media store. + /// + /// # Arguments + /// + /// * `request` - The `MediaRequest` of the file. + async fn remove_media_content(&self, request: &MediaRequest) -> Result<(), Self::Error>; + + /// Removes all the media files' content associated to an `MxcUri` from the + /// media store. + /// + /// # Arguments + /// + /// * `uri` - The `MxcUri` of the media files. + async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error>; + + /// Removes a room and all elements associated from the state store. + /// + /// # Arguments + /// + /// * `room_id` - The `RoomId` of the room to delete. + async fn remove_room(&self, room_id: &RoomId) -> Result<(), Self::Error>; +} + +#[repr(transparent)] +struct EraseStateStoreError(T); + +impl fmt::Debug for EraseStateStoreError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl StateStore for EraseStateStoreError { + type Error = StoreError; + + async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<(), Self::Error> { + self.0.save_filter(filter_name, filter_id).await.map_err(Into::into) + } + + async fn save_changes(&self, changes: &StateChanges) -> Result<(), Self::Error> { + self.0.save_changes(changes).await.map_err(Into::into) + } + + async fn get_filter(&self, filter_name: &str) -> Result, Self::Error> { + self.0.get_filter(filter_name).await.map_err(Into::into) + } + + async fn get_sync_token(&self) -> Result, Self::Error> { + self.0.get_sync_token().await.map_err(Into::into) + } + + async fn get_presence_event( + &self, + user_id: &UserId, + ) -> Result>, Self::Error> { + self.0.get_presence_event(user_id).await.map_err(Into::into) + } + + async fn get_state_event( + &self, + room_id: &RoomId, + event_type: StateEventType, + state_key: &str, + ) -> Result>, Self::Error> { + self.0.get_state_event(room_id, event_type, state_key).await.map_err(Into::into) + } + + async fn get_state_events( + &self, + room_id: &RoomId, + event_type: StateEventType, + ) -> Result>, Self::Error> { + self.0.get_state_events(room_id, event_type).await.map_err(Into::into) + } + + async fn get_profile( + &self, + room_id: &RoomId, + user_id: &UserId, + ) -> Result, Self::Error> { + self.0.get_profile(room_id, user_id).await.map_err(Into::into) + } + + async fn get_member_event( + &self, + room_id: &RoomId, + state_key: &UserId, + ) -> Result, Self::Error> { + self.0.get_member_event(room_id, state_key).await.map_err(Into::into) + } + + async fn get_user_ids(&self, room_id: &RoomId) -> Result, Self::Error> { + self.0.get_user_ids(room_id).await.map_err(Into::into) + } + + async fn get_invited_user_ids( + &self, + room_id: &RoomId, + ) -> Result, Self::Error> { + self.0.get_invited_user_ids(room_id).await.map_err(Into::into) + } + + async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result, Self::Error> { + self.0.get_joined_user_ids(room_id).await.map_err(Into::into) + } + + async fn get_room_infos(&self) -> Result, Self::Error> { + self.0.get_room_infos().await.map_err(Into::into) + } + + async fn get_stripped_room_infos(&self) -> Result, Self::Error> { + self.0.get_stripped_room_infos().await.map_err(Into::into) + } + + async fn get_users_with_display_name( + &self, + room_id: &RoomId, + display_name: &str, + ) -> Result, Self::Error> { + self.0.get_users_with_display_name(room_id, display_name).await.map_err(Into::into) + } + + async fn get_account_data_event( + &self, + event_type: GlobalAccountDataEventType, + ) -> Result>, Self::Error> { + self.0.get_account_data_event(event_type).await.map_err(Into::into) + } + + async fn get_room_account_data_event( + &self, + room_id: &RoomId, + event_type: RoomAccountDataEventType, + ) -> Result>, Self::Error> { + self.0.get_room_account_data_event(room_id, event_type).await.map_err(Into::into) + } + + async fn get_user_room_receipt_event( + &self, + room_id: &RoomId, + receipt_type: ReceiptType, + thread: ReceiptThread, + user_id: &UserId, + ) -> Result, Self::Error> { + self.0 + .get_user_room_receipt_event(room_id, receipt_type, thread, user_id) + .await + .map_err(Into::into) + } + + async fn get_event_room_receipt_events( + &self, + room_id: &RoomId, + receipt_type: ReceiptType, + thread: ReceiptThread, + event_id: &EventId, + ) -> Result, Self::Error> { + self.0 + .get_event_room_receipt_events(room_id, receipt_type, thread, event_id) + .await + .map_err(Into::into) + } + + async fn get_custom_value(&self, key: &[u8]) -> Result>, Self::Error> { + self.0.get_custom_value(key).await.map_err(Into::into) + } + + async fn set_custom_value( + &self, + key: &[u8], + value: Vec, + ) -> Result>, Self::Error> { + self.0.set_custom_value(key, value).await.map_err(Into::into) + } + + async fn add_media_content( + &self, + request: &MediaRequest, + content: Vec, + ) -> Result<(), Self::Error> { + self.0.add_media_content(request, content).await.map_err(Into::into) + } + + async fn get_media_content( + &self, + request: &MediaRequest, + ) -> Result>, Self::Error> { + self.0.get_media_content(request).await.map_err(Into::into) + } + + async fn remove_media_content(&self, request: &MediaRequest) -> Result<(), Self::Error> { + self.0.remove_media_content(request).await.map_err(Into::into) + } + + async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error> { + self.0.remove_media_content_for_uri(uri).await.map_err(Into::into) + } + + async fn remove_room(&self, room_id: &RoomId) -> Result<(), Self::Error> { + self.0.remove_room(room_id).await.map_err(Into::into) + } +} + +/// Convenience functionality for state stores. +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait StateStoreExt: StateStore { + /// Get a specific state event of statically-known type. + /// + /// # Arguments + /// + /// * `room_id` - The id of the room the state event was received for. + async fn get_state_event_static( + &self, + room_id: &RoomId, + ) -> Result>>, Self::Error> + where + C: StaticEventContent + StaticStateEventContent + RedactContent, + C::Redacted: RedactedStateEventContent, + { + Ok(self.get_state_event(room_id, C::TYPE.into(), "").await?.map(Raw::cast)) + } + + /// Get a specific state event of statically-known type. + /// + /// # Arguments + /// + /// * `room_id` - The id of the room the state event was received for. + async fn get_state_event_static_for_key( + &self, + room_id: &RoomId, + state_key: &K, + ) -> Result>>, Self::Error> + where + C: StaticEventContent + StaticStateEventContent + RedactContent, + C::StateKey: Borrow, + C::Redacted: RedactedStateEventContent, + K: AsRef + ?Sized + Sync, + { + Ok(self.get_state_event(room_id, C::TYPE.into(), state_key.as_ref()).await?.map(Raw::cast)) + } + + /// Get a list of state events of a statically-known type for a given room. + /// + /// # Arguments + /// + /// * `room_id` - The id of the room to find events for. + async fn get_state_events_static( + &self, + room_id: &RoomId, + ) -> Result>>, Self::Error> + where + C: StaticEventContent + StaticStateEventContent + RedactContent, + C::Redacted: RedactedStateEventContent, + { + // FIXME: Could be more efficient, if we had streaming store accessor functions + Ok(self + .get_state_events(room_id, C::TYPE.into()) + .await? + .into_iter() + .map(Raw::cast) + .collect()) + } + + /// Get an event of a statically-known type from the account data store. + async fn get_account_data_event_static( + &self, + ) -> Result>>, Self::Error> + where + C: StaticEventContent + GlobalAccountDataEventContent, + { + Ok(self.get_account_data_event(C::TYPE.into()).await?.map(Raw::cast)) + } + + /// Get an event of a statically-known type from the room account data + /// store. + /// + /// # Arguments + /// + /// * `room_id` - The id of the room for which the room account data event + /// should be fetched. + async fn get_room_account_data_event_static( + &self, + room_id: &RoomId, + ) -> Result>>, Self::Error> + where + C: StaticEventContent + RoomAccountDataEventContent, + { + Ok(self.get_room_account_data_event(room_id, C::TYPE.into()).await?.map(Raw::cast)) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl StateStoreExt for T {} + +/// A type-erased [`StateStore`]. +pub type DynStateStore = dyn StateStore; + +/// A type that can be type-erased into `Arc`. +/// +/// This trait is not meant to be implemented directly outside +/// `matrix-sdk-crypto`, but it is automatically implemented for everything that +/// implements `StateStore`. +pub trait IntoStateStore { + #[doc(hidden)] + fn into_state_store(self) -> Arc; +} + +impl IntoStateStore for T +where + T: StateStore + Sized + 'static, +{ + fn into_state_store(self) -> Arc { + Arc::new(EraseStateStoreError(self)) + } +} + +// Turns a given `Arc` into `Arc` by attaching the +// StateStore impl vtable of `EraseStateStoreError`. +impl IntoStateStore for Arc +where + T: StateStore + 'static, +{ + fn into_state_store(self) -> Arc { + let ptr: *const T = Arc::into_raw(self); + let ptr_erased = ptr as *const EraseStateStoreError; + // SAFETY: EraseStateStoreError is repr(transparent) so T and + // EraseStateStoreError have the same layout and ABI + unsafe { Arc::from_raw(ptr_erased) } + } +} diff --git a/crates/matrix-sdk-indexeddb/src/state_store.rs b/crates/matrix-sdk-indexeddb/src/state_store.rs index f6cc41fd8..4129cc3d0 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store.rs @@ -1366,6 +1366,8 @@ impl IndexeddbStateStore { #[cfg(target_arch = "wasm32")] #[async_trait(?Send)] impl StateStore for IndexeddbStateStore { + type Error = StoreError; + async fn save_filter(&self, filter_name: &str, filter_id: &str) -> StoreResult<()> { self.save_filter(filter_name, filter_id).await.map_err(|e| e.into()) } diff --git a/crates/matrix-sdk-sled/src/state_store.rs b/crates/matrix-sdk-sled/src/state_store.rs index 0347dc194..dd89b9a81 100644 --- a/crates/matrix-sdk-sled/src/state_store.rs +++ b/crates/matrix-sdk-sled/src/state_store.rs @@ -1442,6 +1442,8 @@ impl SledStateStore { #[async_trait] impl StateStore for SledStateStore { + type Error = StoreError; + async fn save_filter(&self, filter_name: &str, filter_id: &str) -> StoreResult<()> { self.save_filter(filter_name, filter_id).await.map_err(Into::into) } diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 5628da653..f7ad8fc41 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -28,8 +28,8 @@ use eyeball::Observable; use futures_core::Stream; use futures_util::StreamExt; use matrix_sdk_base::{ - BaseClient, RoomType, SendOutsideWasm, Session, SessionMeta, SessionTokens, StateStore, - SyncOutsideWasm, + store::DynStateStore, BaseClient, RoomType, SendOutsideWasm, Session, SessionMeta, + SessionTokens, SyncOutsideWasm, }; use matrix_sdk_common::{ instant::Instant, @@ -503,7 +503,7 @@ impl Client { } /// Get a reference to the state store. - pub fn store(&self) -> &dyn StateStore { + pub fn store(&self) -> &DynStateStore { self.base_client().store() } From 318ede131df6774036d5ddc118a06a0654ff138b Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Mon, 27 Feb 2023 19:15:52 +0100 Subject: [PATCH 078/166] refactor(indexeddb): Use IndexeddbStateStoreError in StateStore impl --- .../matrix-sdk-indexeddb/src/state_store.rs | 398 +++++++----------- 1 file changed, 144 insertions(+), 254 deletions(-) diff --git a/crates/matrix-sdk-indexeddb/src/state_store.rs b/crates/matrix-sdk-indexeddb/src/state_store.rs index 4129cc3d0..8acf8d209 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store.rs @@ -28,7 +28,7 @@ use js_sys::Date as JsDate; use matrix_sdk_base::{ deserialized_responses::RawMemberEvent, media::{MediaRequest, UniqueKey}, - store::{Result as StoreResult, StateChanges, StateStore, StoreError}, + store::{StateChanges, StateStore, StoreError}, MinimalStateEvent, RoomInfo, }; use matrix_sdk_store_encryption::{Error as EncryptionError, StoreCipher}; @@ -541,7 +541,129 @@ impl IndexeddbStateStore { .map_err(|e| IndexeddbStateStoreError::StoreError(StoreError::Backend(anyhow!(e).into()))) } - pub async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<()> { + pub async fn get_user_ids_stream(&self, room_id: &RoomId) -> Result> { + Ok([ + self.get_invited_user_ids_inner(room_id).await?, + self.get_joined_user_ids_inner(room_id).await?, + ] + .concat()) + } + + pub async fn get_invited_user_ids_inner(&self, room_id: &RoomId) -> Result> { + let range = self.encode_to_range(KEYS::INVITED_USER_IDS, room_id)?; + let entries = self + .inner + .transaction_on_one_with_mode(KEYS::INVITED_USER_IDS, IdbTransactionMode::Readonly)? + .object_store(KEYS::INVITED_USER_IDS)? + .get_all_with_key(&range)? + .await? + .iter() + .filter_map(|f| self.deserialize_event::(f).ok()) + .collect::>(); + + Ok(entries) + } + + pub async fn get_joined_user_ids_inner(&self, room_id: &RoomId) -> Result> { + let range = self.encode_to_range(KEYS::JOINED_USER_IDS, room_id)?; + Ok(self + .inner + .transaction_on_one_with_mode(KEYS::JOINED_USER_IDS, IdbTransactionMode::Readonly)? + .object_store(KEYS::JOINED_USER_IDS)? + .get_all_with_key(&range)? + .await? + .iter() + .filter_map(|f| self.deserialize_event::(f).ok()) + .collect::>()) + } + + pub async fn get_stripped_user_ids_stream(&self, room_id: &RoomId) -> Result> { + Ok([ + self.get_stripped_invited_user_ids(room_id).await?, + self.get_stripped_joined_user_ids(room_id).await?, + ] + .concat()) + } + + pub async fn get_stripped_invited_user_ids( + &self, + room_id: &RoomId, + ) -> Result> { + let range = self.encode_to_range(KEYS::STRIPPED_INVITED_USER_IDS, room_id)?; + let entries = self + .inner + .transaction_on_one_with_mode( + KEYS::STRIPPED_INVITED_USER_IDS, + IdbTransactionMode::Readonly, + )? + .object_store(KEYS::STRIPPED_INVITED_USER_IDS)? + .get_all_with_key(&range)? + .await? + .iter() + .filter_map(|f| self.deserialize_event::(f).ok()) + .collect::>(); + + Ok(entries) + } + + pub async fn get_stripped_joined_user_ids(&self, room_id: &RoomId) -> Result> { + let range = self.encode_to_range(KEYS::STRIPPED_JOINED_USER_IDS, room_id)?; + Ok(self + .inner + .transaction_on_one_with_mode( + KEYS::STRIPPED_JOINED_USER_IDS, + IdbTransactionMode::Readonly, + )? + .object_store(KEYS::STRIPPED_JOINED_USER_IDS)? + .get_all_with_key(&range)? + .await? + .iter() + .filter_map(|f| self.deserialize_event::(f).ok()) + .collect::>()) + } + + async fn get_custom_value_for_js(&self, jskey: &JsValue) -> Result>> { + self.inner + .transaction_on_one_with_mode(KEYS::CUSTOM, IdbTransactionMode::Readonly)? + .object_store(KEYS::CUSTOM)? + .get(jskey)? + .await? + .map(|f| self.deserialize_event(f)) + .transpose() + } +} + +// Small hack to have the following macro invocation act as the appropriate +// trait impl block on wasm, but still be compiled on non-wasm as a regular +// impl block otherwise. +// +// The trait impl doesn't compile on non-wasm due to unfulfilled trait bounds, +// this hack allows us to still have most of rust-analyzer's IDE functionality +// within the impl block without having to set it up to check things against +// the wasm target (which would disable many other parts of the codebase). +#[cfg(target_arch = "wasm32")] +macro_rules! impl_state_store { + ( $($body:tt)* ) => { + #[async_trait(?Send)] + impl StateStore for IndexeddbStateStore { + type Error = IndexeddbStateStoreError; + + $($body)* + } + }; +} + +#[cfg(not(target_arch = "wasm32"))] +macro_rules! impl_state_store { + ( $($body:tt)* ) => { + impl IndexeddbStateStore { + $($body)* + } + }; +} + +impl_state_store! { + async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<()> { let tx = self .inner .transaction_on_one_with_mode(KEYS::SESSION, IdbTransactionMode::Readwrite)?; @@ -558,7 +680,7 @@ impl IndexeddbStateStore { Ok(()) } - pub async fn get_filter(&self, filter_name: &str) -> Result> { + async fn get_filter(&self, filter_name: &str) -> Result> { self.inner .transaction_on_one_with_mode(KEYS::SESSION, IdbTransactionMode::Readonly)? .object_store(KEYS::SESSION)? @@ -568,7 +690,7 @@ impl IndexeddbStateStore { .transpose() } - pub async fn get_sync_token(&self) -> Result> { + async fn get_sync_token(&self) -> Result> { self.inner .transaction_on_one_with_mode(KEYS::SYNC_TOKEN, IdbTransactionMode::Readonly)? .object_store(KEYS::SYNC_TOKEN)? @@ -578,7 +700,7 @@ impl IndexeddbStateStore { .transpose() } - pub async fn save_changes(&self, changes: &StateChanges) -> Result<()> { + async fn save_changes(&self, changes: &StateChanges) -> Result<()> { let mut stores: HashSet<&'static str> = [ (changes.sync_token.is_some(), KEYS::SYNC_TOKEN), (changes.session.is_some(), KEYS::SESSION), @@ -960,7 +1082,7 @@ impl IndexeddbStateStore { tx.await.into_result().map_err(|e| e.into()) } - pub async fn get_presence_event(&self, user_id: &UserId) -> Result>> { + async fn get_presence_event(&self, user_id: &UserId) -> Result>> { self.inner .transaction_on_one_with_mode(KEYS::PRESENCE, IdbTransactionMode::Readonly)? .object_store(KEYS::PRESENCE)? @@ -970,7 +1092,7 @@ impl IndexeddbStateStore { .transpose() } - pub async fn get_state_event( + async fn get_state_event( &self, room_id: &RoomId, event_type: StateEventType, @@ -985,7 +1107,7 @@ impl IndexeddbStateStore { .transpose() } - pub async fn get_state_events( + async fn get_state_events( &self, room_id: &RoomId, event_type: StateEventType, @@ -1002,7 +1124,7 @@ impl IndexeddbStateStore { .collect::>()) } - pub async fn get_profile( + async fn get_profile( &self, room_id: &RoomId, user_id: &UserId, @@ -1016,7 +1138,7 @@ impl IndexeddbStateStore { .transpose() } - pub async fn get_member_event( + async fn get_member_event( &self, room_id: &RoomId, state_key: &UserId, @@ -1046,85 +1168,7 @@ impl IndexeddbStateStore { } } - pub async fn get_user_ids_stream(&self, room_id: &RoomId) -> Result> { - Ok([self.get_invited_user_ids(room_id).await?, self.get_joined_user_ids(room_id).await?] - .concat()) - } - - pub async fn get_invited_user_ids(&self, room_id: &RoomId) -> Result> { - let range = self.encode_to_range(KEYS::INVITED_USER_IDS, room_id)?; - let entries = self - .inner - .transaction_on_one_with_mode(KEYS::INVITED_USER_IDS, IdbTransactionMode::Readonly)? - .object_store(KEYS::INVITED_USER_IDS)? - .get_all_with_key(&range)? - .await? - .iter() - .filter_map(|f| self.deserialize_event::(f).ok()) - .collect::>(); - - Ok(entries) - } - - pub async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result> { - let range = self.encode_to_range(KEYS::JOINED_USER_IDS, room_id)?; - Ok(self - .inner - .transaction_on_one_with_mode(KEYS::JOINED_USER_IDS, IdbTransactionMode::Readonly)? - .object_store(KEYS::JOINED_USER_IDS)? - .get_all_with_key(&range)? - .await? - .iter() - .filter_map(|f| self.deserialize_event::(f).ok()) - .collect::>()) - } - - pub async fn get_stripped_user_ids_stream(&self, room_id: &RoomId) -> Result> { - Ok([ - self.get_stripped_invited_user_ids(room_id).await?, - self.get_stripped_joined_user_ids(room_id).await?, - ] - .concat()) - } - - pub async fn get_stripped_invited_user_ids( - &self, - room_id: &RoomId, - ) -> Result> { - let range = self.encode_to_range(KEYS::STRIPPED_INVITED_USER_IDS, room_id)?; - let entries = self - .inner - .transaction_on_one_with_mode( - KEYS::STRIPPED_INVITED_USER_IDS, - IdbTransactionMode::Readonly, - )? - .object_store(KEYS::STRIPPED_INVITED_USER_IDS)? - .get_all_with_key(&range)? - .await? - .iter() - .filter_map(|f| self.deserialize_event::(f).ok()) - .collect::>(); - - Ok(entries) - } - - pub async fn get_stripped_joined_user_ids(&self, room_id: &RoomId) -> Result> { - let range = self.encode_to_range(KEYS::STRIPPED_JOINED_USER_IDS, room_id)?; - Ok(self - .inner - .transaction_on_one_with_mode( - KEYS::STRIPPED_JOINED_USER_IDS, - IdbTransactionMode::Readonly, - )? - .object_store(KEYS::STRIPPED_JOINED_USER_IDS)? - .get_all_with_key(&range)? - .await? - .iter() - .filter_map(|f| self.deserialize_event::(f).ok()) - .collect::>()) - } - - pub async fn get_room_infos(&self) -> Result> { + async fn get_room_infos(&self) -> Result> { let entries: Vec<_> = self .inner .transaction_on_one_with_mode(KEYS::ROOM_INFOS, IdbTransactionMode::Readonly)? @@ -1138,7 +1182,7 @@ impl IndexeddbStateStore { Ok(entries) } - pub async fn get_stripped_room_infos(&self) -> Result> { + async fn get_stripped_room_infos(&self) -> Result> { let entries = self .inner .transaction_on_one_with_mode(KEYS::STRIPPED_ROOM_INFOS, IdbTransactionMode::Readonly)? @@ -1152,7 +1196,7 @@ impl IndexeddbStateStore { Ok(entries) } - pub async fn get_users_with_display_name( + async fn get_users_with_display_name( &self, room_id: &RoomId, display_name: &str, @@ -1166,7 +1210,7 @@ impl IndexeddbStateStore { .unwrap_or_else(|| Ok(Default::default())) } - pub async fn get_account_data_event( + async fn get_account_data_event( &self, event_type: GlobalAccountDataEventType, ) -> Result>> { @@ -1179,7 +1223,7 @@ impl IndexeddbStateStore { .transpose() } - pub async fn get_room_account_data_event( + async fn get_room_account_data_event( &self, room_id: &RoomId, event_type: RoomAccountDataEventType, @@ -1272,16 +1316,6 @@ impl IndexeddbStateStore { self.get_custom_value_for_js(jskey).await } - async fn get_custom_value_for_js(&self, jskey: &JsValue) -> Result>> { - self.inner - .transaction_on_one_with_mode(KEYS::CUSTOM, IdbTransactionMode::Readonly)? - .object_store(KEYS::CUSTOM)? - .get(jskey)? - .await? - .map(|f| self.deserialize_event(f)) - .transpose() - } - async fn set_custom_value(&self, key: &[u8], value: Vec) -> Result>> { let jskey = JsValue::from_str(core::str::from_utf8(key).map_err(StoreError::Codec)?); @@ -1361,174 +1395,29 @@ impl IndexeddbStateStore { } tx.await.into_result().map_err(|e| e.into()) } -} -#[cfg(target_arch = "wasm32")] -#[async_trait(?Send)] -impl StateStore for IndexeddbStateStore { - type Error = StoreError; - - async fn save_filter(&self, filter_name: &str, filter_id: &str) -> StoreResult<()> { - self.save_filter(filter_name, filter_id).await.map_err(|e| e.into()) - } - - async fn save_changes(&self, changes: &StateChanges) -> StoreResult<()> { - self.save_changes(changes).await.map_err(|e| e.into()) - } - - async fn get_filter(&self, filter_id: &str) -> StoreResult> { - self.get_filter(filter_id).await.map_err(|e| e.into()) - } - - async fn get_sync_token(&self) -> StoreResult> { - self.get_sync_token().await.map_err(|e| e.into()) - } - - async fn get_presence_event( - &self, - user_id: &UserId, - ) -> StoreResult>> { - self.get_presence_event(user_id).await.map_err(|e| e.into()) - } - - async fn get_state_event( - &self, - room_id: &RoomId, - event_type: StateEventType, - state_key: &str, - ) -> StoreResult>> { - self.get_state_event(room_id, event_type, state_key).await.map_err(|e| e.into()) - } - - async fn get_state_events( - &self, - room_id: &RoomId, - event_type: StateEventType, - ) -> StoreResult>> { - self.get_state_events(room_id, event_type).await.map_err(|e| e.into()) - } - - async fn get_profile( - &self, - room_id: &RoomId, - user_id: &UserId, - ) -> StoreResult>> { - self.get_profile(room_id, user_id).await.map_err(|e| e.into()) - } - - async fn get_member_event( - &self, - room_id: &RoomId, - state_key: &UserId, - ) -> StoreResult> { - self.get_member_event(room_id, state_key).await.map_err(|e| e.into()) - } - - async fn get_user_ids(&self, room_id: &RoomId) -> StoreResult> { + async fn get_user_ids(&self, room_id: &RoomId) -> Result> { let ids: Vec = self.get_stripped_user_ids_stream(room_id).await?; if !ids.is_empty() { return Ok(ids); } - self.get_user_ids_stream(room_id).await.map_err(|e| e.into()) + self.get_user_ids_stream(room_id).await } - async fn get_invited_user_ids(&self, room_id: &RoomId) -> StoreResult> { + async fn get_invited_user_ids(&self, room_id: &RoomId) -> Result> { let ids: Vec = self.get_stripped_invited_user_ids(room_id).await?; if !ids.is_empty() { return Ok(ids); } - self.get_invited_user_ids(room_id).await.map_err(|e| e.into()) + self.get_invited_user_ids_inner(room_id).await } - async fn get_joined_user_ids(&self, room_id: &RoomId) -> StoreResult> { + async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result> { let ids: Vec = self.get_stripped_joined_user_ids(room_id).await?; if !ids.is_empty() { return Ok(ids); } - self.get_joined_user_ids(room_id).await.map_err(|e| e.into()) - } - - async fn get_room_infos(&self) -> StoreResult> { - self.get_room_infos().await.map_err(|e| e.into()) - } - - async fn get_stripped_room_infos(&self) -> StoreResult> { - self.get_stripped_room_infos().await.map_err(|e| e.into()) - } - - async fn get_users_with_display_name( - &self, - room_id: &RoomId, - display_name: &str, - ) -> StoreResult> { - self.get_users_with_display_name(room_id, display_name).await.map_err(|e| e.into()) - } - - async fn get_account_data_event( - &self, - event_type: GlobalAccountDataEventType, - ) -> StoreResult>> { - self.get_account_data_event(event_type).await.map_err(|e| e.into()) - } - - async fn get_room_account_data_event( - &self, - room_id: &RoomId, - event_type: RoomAccountDataEventType, - ) -> StoreResult>> { - self.get_room_account_data_event(room_id, event_type).await.map_err(|e| e.into()) - } - - async fn get_user_room_receipt_event( - &self, - room_id: &RoomId, - receipt_type: ReceiptType, - thread: ReceiptThread, - user_id: &UserId, - ) -> StoreResult> { - self.get_user_room_receipt_event(room_id, receipt_type, thread, user_id) - .await - .map_err(|e| e.into()) - } - - async fn get_event_room_receipt_events( - &self, - room_id: &RoomId, - receipt_type: ReceiptType, - thread: ReceiptThread, - event_id: &EventId, - ) -> StoreResult> { - self.get_event_room_receipt_events(room_id, receipt_type, thread, event_id) - .await - .map_err(|e| e.into()) - } - - async fn get_custom_value(&self, key: &[u8]) -> StoreResult>> { - self.get_custom_value(key).await.map_err(|e| e.into()) - } - - async fn set_custom_value(&self, key: &[u8], value: Vec) -> StoreResult>> { - self.set_custom_value(key, value).await.map_err(|e| e.into()) - } - - async fn add_media_content(&self, request: &MediaRequest, data: Vec) -> StoreResult<()> { - self.add_media_content(request, data).await.map_err(|e| e.into()) - } - - async fn get_media_content(&self, request: &MediaRequest) -> StoreResult>> { - self.get_media_content(request).await.map_err(|e| e.into()) - } - - async fn remove_media_content(&self, request: &MediaRequest) -> StoreResult<()> { - self.remove_media_content(request).await.map_err(|e| e.into()) - } - - async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> StoreResult<()> { - self.remove_media_content_for_uri(uri).await.map_err(|e| e.into()) - } - - async fn remove_room(&self, room_id: &RoomId) -> StoreResult<()> { - self.remove_room(room_id).await.map_err(|e| e.into()) + self.get_joined_user_ids_inner(room_id).await } } @@ -1574,6 +1463,7 @@ mod migration_tests { wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); use indexed_db_futures::prelude::*; + use matrix_sdk_base::StateStore; use matrix_sdk_test::async_test; use ruma::{ events::{AnySyncStateEvent, StateEventType}, From c366eadf33028dc3f9b45ecaa411c0b64a83254c Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 14:35:46 +0100 Subject: [PATCH 079/166] =?UTF-8?q?chore(sdk):=20Rename=20variables,=20cle?= =?UTF-8?q?an=20up=20code,=20improve=20documentation=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/matrix-sdk/src/sliding_sync/list.rs | 93 +++++++++++++++------- 1 file changed, 63 insertions(+), 30 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/list.rs b/crates/matrix-sdk/src/sliding_sync/list.rs index d6725ba88..feefdee90 100644 --- a/crates/matrix-sdk/src/sliding_sync/list.rs +++ b/crates/matrix-sdk/src/sliding_sync/list.rs @@ -120,13 +120,14 @@ impl SlidingSyncList { .ranges(self.ranges.read().unwrap().clone()) } - /// Set the ranges to fetch + /// Set the ranges to fetch. /// /// Remember to cancel the existing stream and fetch a new one as this will /// only be applied on the next request. pub fn set_ranges(&self, range: Vec<(u32, u32)>) -> &Self { let value = range.into_iter().map(|(a, b)| (a.into(), b.into())).collect(); Observable::set(&mut self.ranges.write().unwrap(), value); + self } @@ -137,6 +138,7 @@ impl SlidingSyncList { pub fn set_range(&self, start: u32, end: u32) -> &Self { let value = vec![(start.into(), end.into())]; Observable::set(&mut self.ranges.write().unwrap(), value); + self } @@ -148,6 +150,7 @@ impl SlidingSyncList { Observable::update(&mut self.ranges.write().unwrap(), |ranges| { ranges.push((start.into(), end.into())); }); + self } @@ -162,6 +165,7 @@ impl SlidingSyncList { /// only be applied on the next request. pub fn reset_ranges(&self) -> &Self { Observable::set(&mut self.ranges.write().unwrap(), Vec::new()); + self } @@ -198,7 +202,7 @@ impl SlidingSyncList { Observable::subscribe(&self.rooms_count.read().unwrap()) } - /// Find the current valid position of the room in the list room_list. + /// Find the current valid position of the room in the list `room_list`. /// /// Only matches against the current ranges and only against filled items. /// Invalid items are ignore. Return the total position the item was @@ -206,60 +210,71 @@ impl SlidingSyncList { pub fn find_room_in_list(&self, room_id: &RoomId) -> Option { let ranges = self.ranges.read().unwrap(); let listing = self.rooms_list.read().unwrap(); + for (start_uint, end_uint) in ranges.iter() { - let mut cur_pos: usize = (*start_uint).try_into().unwrap(); + let mut current_position: usize = (*start_uint).try_into().unwrap(); let end: usize = (*end_uint).try_into().unwrap(); - let iterator = listing.iter().skip(cur_pos); - for n in iterator { - if let RoomListEntry::Filled(r) = n { - if room_id == r { - return Some(cur_pos); + let room_list_entries = listing.iter().skip(current_position); + + for room_list_entry in room_list_entries { + if let RoomListEntry::Filled(this_room_id) = room_list_entry { + if room_id == this_room_id { + return Some(current_position); } } - if cur_pos == end { + + if current_position == end { break; } - cur_pos += 1; + + current_position += 1; } } + None } - /// Find the current valid position of the rooms in the lists room_list. + /// Find the current valid position of the rooms in the lists `room_list`. /// /// Only matches against the current ranges and only against filled items. /// Invalid items are ignore. Return the total position the items that were - /// found in the room_list, will skip any room not found in the rooms_list. + /// found in the `room_list`, will skip any room not found in the + /// `rooms_list`. pub fn find_rooms_in_list(&self, room_ids: &[OwnedRoomId]) -> Vec<(usize, OwnedRoomId)> { let ranges = self.ranges.read().unwrap(); let listing = self.rooms_list.read().unwrap(); let mut rooms_found = Vec::new(); + for (start_uint, end_uint) in ranges.iter() { - let mut cur_pos: usize = (*start_uint).try_into().unwrap(); + let mut current_position: usize = (*start_uint).try_into().unwrap(); let end: usize = (*end_uint).try_into().unwrap(); - let iterator = listing.iter().skip(cur_pos); - for n in iterator { - if let RoomListEntry::Filled(r) = n { - if room_ids.contains(r) { - rooms_found.push((cur_pos, r.clone())); + let room_list_entries = listing.iter().skip(current_position); + + for room_list_entry in room_list_entries { + if let RoomListEntry::Filled(room_id) = room_list_entry { + if room_ids.contains(room_id) { + rooms_found.push((current_position, room_id.clone())); } } - if cur_pos == end { + + if current_position == end { break; } - cur_pos += 1; + + current_position += 1; } } + rooms_found } - /// Return the room_id at the given index + /// Return the `room_id` at the given index. pub fn get_room_id(&self, index: usize) -> Option { self.rooms_list .read() .unwrap() .get(index) - .and_then(|e| e.as_room_id().map(ToOwned::to_owned)) + .and_then(|room_list_entry| room_list_entry.as_room_id().map(ToOwned::to_owned)) } #[instrument(skip(self, ops), fields(name = self.name, ops_count = ops.len()))] @@ -271,15 +286,18 @@ impl SlidingSyncList { rooms: &Vec, ) -> Result { let current_rooms_count = **self.rooms_count.read().unwrap(); + if current_rooms_count.is_none() || current_rooms_count == Some(0) || self.is_cold.load(Ordering::SeqCst) { debug!("first run, replacing rooms list"); + // first response, we do that slightly differently let mut rooms_list = ObservableVector::new(); rooms_list .append(iter::repeat(RoomListEntry::Empty).take(rooms_count as usize).collect()); + // then we apply it room_ops(&mut rooms_list, ops, ranges)?; @@ -291,25 +309,30 @@ impl SlidingSyncList { Observable::set(&mut self.rooms_count.write().unwrap(), Some(rooms_count)); self.is_cold.store(false, Ordering::SeqCst); + return Ok(true); } debug!("regular update"); + let mut missing = rooms_count .checked_sub(self.rooms_list.read().unwrap().len() as u32) .unwrap_or_default(); let mut changed = false; + if missing > 0 { let mut list = self.rooms_list.write().unwrap(); + while missing > 0 { list.push_back(RoomListEntry::Empty); missing -= 1; } + changed = true; } { - // keep the lock scoped so that the later find_rooms_in_list doesn't deadlock + // keep the lock scoped so that the later `find_rooms_in_list` doesn't deadlock let mut rooms_list = self.rooms_list.write().unwrap(); if !ops.is_empty() { @@ -322,6 +345,7 @@ impl SlidingSyncList { { let mut lock = self.rooms_count.write().unwrap(); + if **lock != Some(rooms_count) { Observable::set(&mut lock, Some(rooms_count)); changed = true; @@ -355,9 +379,11 @@ impl SlidingSyncList { SlidingSyncMode::PagingFullSync => { SlidingSyncListRequestGenerator::new_with_paging_syncup(self.clone()) } + SlidingSyncMode::GrowingFullSync => { SlidingSyncListRequestGenerator::new_with_growing_syncup(self.clone()) } + SlidingSyncMode::Selective => SlidingSyncListRequestGenerator::new_live(self.clone()), } } @@ -380,16 +406,22 @@ impl FrozenSlidingSyncList { ) -> Self { let mut rooms = BTreeMap::new(); let mut rooms_list = Vector::new(); - for entry in source_list.rooms_list.read().unwrap().iter() { - match entry { - RoomListEntry::Filled(o) | RoomListEntry::Invalidated(o) => { - rooms.insert(o.clone(), rooms_map.get(o).expect("rooms always exists").into()); + + for room_list_entry in source_list.rooms_list.read().unwrap().iter() { + match room_list_entry { + RoomListEntry::Filled(room_id) | RoomListEntry::Invalidated(room_id) => { + rooms.insert( + room_id.clone(), + rooms_map.get(room_id).expect("room doesn't exist").into(), + ); } + _ => {} }; - rooms_list.push_back(entry.freeze()); + rooms_list.push_back(room_list_entry.freeze()); } + FrozenSlidingSyncList { rooms_count: **source_list.rooms_count.read().unwrap(), rooms_list, @@ -686,9 +718,10 @@ impl SlidingSyncListRequestGenerator { ops: &Vec, rooms: &Vec, ) -> Result { - let res = self.list.handle_response(rooms_count, ops, &self.ranges, rooms)?; + let response = self.list.handle_response(rooms_count, ops, &self.ranges, rooms)?; self.update_state(rooms_count.saturating_sub(1)); // index is 0 based, count is 1 based - Ok(res) + + Ok(response) } fn update_state(&mut self, max_index: u32) { From 50a248c18bf5c14c098d0844d2f395e0d60cf50b Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 14:38:26 +0100 Subject: [PATCH 080/166] chore(sdk): Continue the renaming from view to list for Sliding Sync. --- crates/matrix-sdk/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index e8ce92a5f..ab07b473d 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -58,8 +58,8 @@ pub use media::Media; pub use ruma::{IdParseError, OwnedServerName, ServerName}; #[cfg(feature = "experimental-sliding-sync")] pub use sliding_sync::{ - RoomListEntry, SlidingSync, SlidingSyncBuilder, SlidingSyncMode, SlidingSyncRoom, - SlidingSyncState, SlidingSyncList, SlidingSyncListBuilder, UpdateSummary, + RoomListEntry, SlidingSync, SlidingSyncBuilder, SlidingSyncList, SlidingSyncListBuilder, + SlidingSyncMode, SlidingSyncRoom, SlidingSyncState, UpdateSummary, }; #[cfg(any(test, feature = "testing"))] From d829fd3376c7298080fe71ef81a4079c02f62817 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 14:38:43 +0100 Subject: [PATCH 081/166] chore(bindings): Continue the renaming from view to list for Sliding Sync. --- bindings/matrix-sdk-ffi/src/sliding_sync.rs | 86 ++++++++++----------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/sliding_sync.rs b/bindings/matrix-sdk-ffi/src/sliding_sync.rs index ece014b37..4ccd544e2 100644 --- a/bindings/matrix-sdk-ffi/src/sliding_sync.rs +++ b/bindings/matrix-sdk-ffi/src/sliding_sync.rs @@ -276,13 +276,13 @@ impl From for RumaRoomSubscription { impl From for UpdateSummary { fn from(other: matrix_sdk::UpdateSummary) -> UpdateSummary { UpdateSummary { - views: other.views, + views: other.lists, rooms: other.rooms.into_iter().map(|r| r.as_str().to_owned()).collect(), } } } -pub enum SlidingSyncListRoomsListDiff { +pub enum SlidingSyncViewRoomsListDiff { Append { values: Vec }, Insert { index: u32, value: RoomListEntry }, Set { index: u32, value: RoomListEntry }, @@ -295,33 +295,33 @@ pub enum SlidingSyncListRoomsListDiff { Reset { values: Vec }, } -impl From> for SlidingSyncListRoomsListDiff { +impl From> for SlidingSyncViewRoomsListDiff { fn from(other: VectorDiff) -> Self { match other { - VectorDiff::Append { values } => SlidingSyncListRoomsListDiff::Append { + VectorDiff::Append { values } => SlidingSyncViewRoomsListDiff::Append { values: values.into_iter().map(|e| (&e).into()).collect(), }, VectorDiff::Insert { index, value } => { - SlidingSyncListRoomsListDiff::Insert { index: index as u32, value: (&value).into() } + SlidingSyncViewRoomsListDiff::Insert { index: index as u32, value: (&value).into() } } VectorDiff::Set { index, value } => { - SlidingSyncListRoomsListDiff::Set { index: index as u32, value: (&value).into() } + SlidingSyncViewRoomsListDiff::Set { index: index as u32, value: (&value).into() } } VectorDiff::Remove { index } => { - SlidingSyncListRoomsListDiff::Remove { index: index as u32 } + SlidingSyncViewRoomsListDiff::Remove { index: index as u32 } } VectorDiff::PushBack { value } => { - SlidingSyncListRoomsListDiff::PushBack { value: (&value).into() } + SlidingSyncViewRoomsListDiff::PushBack { value: (&value).into() } } VectorDiff::PushFront { value } => { - SlidingSyncListRoomsListDiff::PushFront { value: (&value).into() } + SlidingSyncViewRoomsListDiff::PushFront { value: (&value).into() } } - VectorDiff::PopBack => SlidingSyncListRoomsListDiff::PopBack, - VectorDiff::PopFront => SlidingSyncListRoomsListDiff::PopFront, - VectorDiff::Clear => SlidingSyncListRoomsListDiff::Clear, + VectorDiff::PopBack => SlidingSyncViewRoomsListDiff::PopBack, + VectorDiff::PopFront => SlidingSyncViewRoomsListDiff::PopFront, + VectorDiff::Clear => SlidingSyncViewRoomsListDiff::Clear, VectorDiff::Reset { values } => { warn!("Room list subscriber lagged behind and was reset"); - SlidingSyncListRoomsListDiff::Reset { + SlidingSyncViewRoomsListDiff::Reset { values: values.into_iter().map(|e| (&e).into()).collect(), } } @@ -348,25 +348,25 @@ impl From<&MatrixRoomEntry> for RoomListEntry { } } -pub trait SlidingSyncListRoomItemsObserver: Sync + Send { +pub trait SlidingSyncViewRoomItemsObserver: Sync + Send { fn did_receive_update(&self); } -pub trait SlidingSyncListRoomListObserver: Sync + Send { - fn did_receive_update(&self, diff: SlidingSyncListRoomsListDiff); +pub trait SlidingSyncViewRoomListObserver: Sync + Send { + fn did_receive_update(&self, diff: SlidingSyncViewRoomsListDiff); } -pub trait SlidingSyncListRoomsCountObserver: Sync + Send { +pub trait SlidingSyncViewRoomsCountObserver: Sync + Send { fn did_receive_update(&self, new_count: u32); } -pub trait SlidingSyncListStateObserver: Sync + Send { +pub trait SlidingSyncViewStateObserver: Sync + Send { fn did_receive_update(&self, new_state: SlidingSyncState); } #[derive(Clone)] -pub struct SlidingSyncListBuilder { - inner: matrix_sdk::SlidingSyncListBuilder, +pub struct SlidingSyncViewBuilder { + inner: matrix_sdk::SlidingSyncViewBuilder, } #[derive(uniffi::Record)] @@ -404,7 +404,7 @@ impl From for SyncRequestListFilters { } } -impl SlidingSyncListBuilder { +impl SlidingSyncViewBuilder { pub fn new() -> Self { Self { inner: matrix_sdk::SlidingSyncList::builder() } } @@ -427,14 +427,14 @@ impl SlidingSyncListBuilder { Arc::new(builder) } - pub fn build(self: Arc) -> anyhow::Result> { + pub fn build(self: Arc) -> anyhow::Result> { let builder = unwrap_or_clone_arc(self); Ok(Arc::new(builder.inner.build()?.into())) } } #[uniffi::export] -impl SlidingSyncListBuilder { +impl SlidingSyncViewBuilder { pub fn sort(self: Arc, sort: Vec) -> Arc { let mut builder = unwrap_or_clone_arc(self); builder.inner = builder.inner.sort(sort); @@ -511,20 +511,20 @@ impl SlidingSyncListBuilder { } #[derive(Clone)] -pub struct SlidingSyncList { +pub struct SlidingSyncView { inner: matrix_sdk::SlidingSyncList, } -impl From for SlidingSyncList { +impl From for SlidingSyncView { fn from(inner: matrix_sdk::SlidingSyncList) -> Self { - SlidingSyncList { inner } + SlidingSyncView { inner } } } -impl SlidingSyncList { +impl SlidingSyncView { pub fn observe_state( &self, - observer: Box, + observer: Box, ) -> Arc { let mut state_stream = self.inner.state_stream(); @@ -539,7 +539,7 @@ impl SlidingSyncList { pub fn observe_room_list( &self, - observer: Box, + observer: Box, ) -> Arc { let mut rooms_list_stream = self.inner.rooms_list_stream(); @@ -554,7 +554,7 @@ impl SlidingSyncList { pub fn observe_room_items( &self, - observer: Box, + observer: Box, ) -> Arc { let mut rooms_updated = Observable::subscribe(&self.inner.rooms_updated_broadcast.read().unwrap()); @@ -569,7 +569,7 @@ impl SlidingSyncList { pub fn observe_rooms_count( &self, - observer: Box, + observer: Box, ) -> Arc { let mut rooms_count_stream = self.inner.rooms_count_stream(); @@ -584,7 +584,7 @@ impl SlidingSyncList { } #[uniffi::export] -impl SlidingSyncList { +impl SlidingSyncView { /// Get the current list of rooms pub fn current_rooms_list(&self) -> Vec { self.inner.rooms_list() @@ -709,16 +709,16 @@ impl SlidingSync { #[uniffi::export] impl SlidingSync { #[allow(clippy::significant_drop_in_scrutinee)] - pub fn get_view(&self, name: String) -> Option> { - self.inner.view(&name).map(|inner| Arc::new(SlidingSyncList { inner })) + pub fn get_view(&self, name: String) -> Option> { + self.inner.list(&name).map(|inner| Arc::new(SlidingSyncView { inner })) } - pub fn add_view(&self, view: Arc) -> Option> { - self.inner.add_view(view.inner.clone()).map(|inner| Arc::new(SlidingSyncList { inner })) + pub fn add_view(&self, view: Arc) -> Option> { + self.inner.add_list(view.inner.clone()).map(|inner| Arc::new(SlidingSyncView { inner })) } - pub fn pop_view(&self, name: String) -> Option> { - self.inner.pop_view(&name).map(|inner| Arc::new(SlidingSyncList { inner })) + pub fn pop_view(&self, name: String) -> Option> { + self.inner.pop_list(&name).map(|inner| Arc::new(SlidingSyncView { inner })) } pub fn add_common_extensions(&self) { @@ -794,13 +794,13 @@ impl SlidingSyncBuilder { impl SlidingSyncBuilder { pub fn add_fullsync_view(self: Arc) -> Arc { let mut builder = unwrap_or_clone_arc(self); - builder.inner = builder.inner.add_fullsync_view(); + builder.inner = builder.inner.add_fullsync_list(); Arc::new(builder) } pub fn no_views(self: Arc) -> Arc { let mut builder = unwrap_or_clone_arc(self); - builder.inner = builder.inner.no_views(); + builder.inner = builder.inner.no_lists(); Arc::new(builder) } @@ -810,10 +810,10 @@ impl SlidingSyncBuilder { Arc::new(builder) } - pub fn add_view(self: Arc, v: Arc) -> Arc { + pub fn add_view(self: Arc, v: Arc) -> Arc { let mut builder = unwrap_or_clone_arc(self); let view = unwrap_or_clone_arc(v); - builder.inner = builder.inner.add_view(view.inner); + builder.inner = builder.inner.add_list(view.inner); Arc::new(builder) } @@ -864,7 +864,7 @@ impl Client { pub fn full_sliding_sync(&self) -> anyhow::Result> { RUNTIME.block_on(async move { let builder = self.client.sliding_sync().await; - let inner = builder.add_fullsync_view().build().await?; + let inner = builder.add_fullsync_list().build().await?; Ok(Arc::new(SlidingSync::new(inner, self.clone()))) }) } From c5d5deba8e12cd9ff80a8d96de9384211ed3f7a2 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 14:39:09 +0100 Subject: [PATCH 082/166] chore: Continue the renaming from view to list for Sliding Sync. --- labs/jack-in/src/client/mod.rs | 6 +- labs/jack-in/src/client/state.rs | 2 +- .../sliding-sync-integration-test/src/lib.rs | 126 +++++++++--------- 3 files changed, 67 insertions(+), 67 deletions(-) diff --git a/labs/jack-in/src/client/mod.rs b/labs/jack-in/src/client/mod.rs index f1342cf4a..d411211ed 100644 --- a/labs/jack-in/src/client/mod.rs +++ b/labs/jack-in/src/client/mod.rs @@ -7,7 +7,7 @@ pub mod state; use matrix_sdk::{ ruma::{api::client::error::ErrorKind, OwnedRoomId}, - Client, SlidingSyncState, SlidingSyncListBuilder, + Client, SlidingSyncListBuilder, SlidingSyncState, }; pub async fn run_client( @@ -35,13 +35,13 @@ pub async fn run_client( let syncer = builder .homeserver(config.proxy.parse().wrap_err("can't parse sync proxy")?) - .add_view(full_sync_view) + .add_list(full_sync_view) .with_common_extensions() .cold_cache("jack-in-default") .build() .await?; let stream = syncer.stream(); - let view = syncer.view("full-sync").expect("we have the full syncer there").clone(); + let view = syncer.list("full-sync").expect("we have the full syncer there").clone(); let mut ssync_state = state::SlidingSyncState::new(syncer.clone(), view.clone()); tx.send(ssync_state.clone()).await?; diff --git a/labs/jack-in/src/client/state.rs b/labs/jack-in/src/client/state.rs index dbddbfb18..01ddd9bed 100644 --- a/labs/jack-in/src/client/state.rs +++ b/labs/jack-in/src/client/state.rs @@ -9,7 +9,7 @@ use futures::{pin_mut, StreamExt}; use matrix_sdk::{ room::timeline::{Timeline, TimelineItem}, ruma::{OwnedRoomId, RoomId}, - SlidingSync, SlidingSyncRoom, SlidingSyncState as ViewState, SlidingSyncList, + SlidingSync, SlidingSyncList, SlidingSyncRoom, SlidingSyncState as ViewState, }; use tokio::task::JoinHandle; diff --git a/testing/sliding-sync-integration-test/src/lib.rs b/testing/sliding-sync-integration-test/src/lib.rs index 6a73b5bb0..2c194f05e 100644 --- a/testing/sliding-sync-integration-test/src/lib.rs +++ b/testing/sliding-sync-integration-test/src/lib.rs @@ -82,7 +82,7 @@ mod tests { api::client::error::ErrorKind as RumaError, events::room::message::RoomMessageEventContent, uint, }, - SlidingSyncMode, SlidingSyncState, SlidingSyncList, + SlidingSyncList, SlidingSyncMode, SlidingSyncState, }; use super::*; @@ -90,7 +90,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn it_works_smoke_test() -> anyhow::Result<()> { let (_client, sync_proxy_builder) = setup("odo".to_owned(), false).await?; - let sync_proxy = sync_proxy_builder.add_fullsync_view().build().await?; + let sync_proxy = sync_proxy_builder.add_fullsync_list().build().await?; let stream = sync_proxy.stream(); pin_mut!(stream); let room_summary = @@ -108,7 +108,7 @@ mod tests { let room_id = { let sync = sync_builder .clone() - .add_view( + .add_list( SlidingSyncList::builder() .sync_mode(SlidingSyncMode::Selective) .add_range(0u32, 1) @@ -124,7 +124,7 @@ mod tests { pin_mut!(stream); // Get the view to all rooms to check the view' state. - let view = sync.view("init_view").context("View `init_view` isn't found")?; + let view = sync.list("init_view").context("View `init_view` isn't found")?; assert_eq!(view.state(), SlidingSyncState::Cold); // Send the request and wait for a response. @@ -169,7 +169,7 @@ mod tests { let sync = sync_builder .clone() - .add_view( + .add_list( SlidingSyncList::builder() .sync_mode(SlidingSyncMode::Selective) .name("visible_rooms_view") @@ -186,7 +186,7 @@ mod tests { // Get the view. let view = - sync.view("visible_rooms_view").context("View `visible_rooms_view` isn't found")?; + sync.list("visible_rooms_view").context("View `visible_rooms_view` isn't found")?; let mut all_event_ids = Vec::new(); @@ -337,14 +337,14 @@ mod tests { .build() }; let sync_proxy = sync_proxy_builder - .add_view(build_view(view_name_1)?) - .add_view(build_view(view_name_2)?) + .add_list(build_view(view_name_1)?) + .add_list(build_view(view_name_2)?) .build() .await?; - let view1 = sync_proxy.view(view_name_1).context("but we just added that view!")?; - let _view2 = sync_proxy.view(view_name_2).context("but we just added that view!")?; + let view1 = sync_proxy.list(view_name_1).context("but we just added that view!")?; + let _view2 = sync_proxy.list(view_name_2).context("but we just added that view!")?; - assert!(sync_proxy.view(view_name_3).is_none()); + assert!(sync_proxy.list(view_name_3).is_none()); let stream = sync_proxy.stream(); pin_mut!(stream); @@ -352,9 +352,9 @@ mod tests { stream.next().await.context("No room summary found, loop ended unsuccessfully")?; let summary = room_summary?; // we only heard about the ones we had asked for - assert_eq!(summary.views, [view_name_1, view_name_2]); + assert_eq!(summary.lists, [view_name_1, view_name_2]); - assert!(sync_proxy.add_view(build_view(view_name_3)?).is_none()); + assert!(sync_proxy.add_list(build_view(view_name_3)?).is_none()); // we need to restart the stream after every view listing update let stream = sync_proxy.stream(); @@ -365,9 +365,9 @@ mod tests { let room_summary = stream.next().await.context("sync has closed unexpectedly")?; let summary = room_summary?; // we only heard about the ones we had asked for - if !summary.views.is_empty() { + if !summary.lists.is_empty() { // only if we saw an update come through - assert_eq!(summary.views, [view_name_3]); + assert_eq!(summary.lists, [view_name_3]); // we didn't update the other views, so only no 2 should se an update saw_update = true; break; @@ -390,9 +390,9 @@ mod tests { let room_summary = stream.next().await.context("sync has closed unexpectedly")?; let summary = room_summary?; // we only heard about the ones we had asked for - if !summary.views.is_empty() { + if !summary.lists.is_empty() { // only if we saw an update come through - assert_eq!(summary.views, [view_name_1, view_name_2, view_name_3,]); + assert_eq!(summary.lists, [view_name_1, view_name_2, view_name_3,]); // notice that our view 2 is now the last view, but all have seen updates saw_update = true; break; @@ -423,19 +423,19 @@ mod tests { .build() }; let sync_proxy = sync_proxy_builder - .add_view(build_view(view_name_1)?) - .add_view(build_view(view_name_2)?) - .add_view(build_view(view_name_3)?) + .add_list(build_view(view_name_1)?) + .add_list(build_view(view_name_2)?) + .add_list(build_view(view_name_3)?) .build() .await?; - let Some(view1 )= sync_proxy.view(view_name_1) else { + let Some(view1 )= sync_proxy.list(view_name_1) else { bail!("but we just added that view!"); }; - let Some(_view2 )= sync_proxy.view(view_name_2) else { + let Some(_view2 )= sync_proxy.list(view_name_2) else { bail!("but we just added that view!"); }; - let Some(_view3 )= sync_proxy.view(view_name_3) else { + let Some(_view3 )= sync_proxy.list(view_name_3) else { bail!("but we just added that view!"); }; @@ -446,9 +446,9 @@ mod tests { }; let summary = room_summary?; // we only heard about the ones we had asked for - assert_eq!(summary.views, [view_name_1, view_name_2, view_name_3]); + assert_eq!(summary.lists, [view_name_1, view_name_2, view_name_3]); - let Some(view_2) = sync_proxy.pop_view(&view_name_2.to_owned()) else { + let Some(view_2) = sync_proxy.pop_list(&view_name_2.to_owned()) else { bail!("Room exists"); }; @@ -476,9 +476,9 @@ mod tests { }; let summary = room_summary?; // we only heard about the ones we had asked for - if !summary.views.is_empty() { + if !summary.lists.is_empty() { // only if we saw an update come through - assert_eq!(summary.views, [view_name_1, view_name_3]); + assert_eq!(summary.lists, [view_name_1, view_name_3]); saw_update = true; break; } @@ -486,7 +486,7 @@ mod tests { assert!(saw_update, "We didn't see the update come through the pipe"); - assert!(sync_proxy.add_view(view_2).is_none()); + assert!(sync_proxy.add_list(view_2).is_none()); // we need to restart the stream after every view listing update let stream = sync_proxy.stream(); @@ -510,9 +510,9 @@ mod tests { }; let summary = room_summary?; // we only heard about the ones we had asked for - if !summary.views.is_empty() { + if !summary.lists.is_empty() { // only if we saw an update come through - assert_eq!(summary.views, [view_name_1, view_name_2, view_name_3]); // all views are visible again + assert_eq!(summary.lists, [view_name_1, view_name_2, view_name_3]); // all views are visible again saw_update = true; break; } @@ -540,10 +540,10 @@ mod tests { .name("full") .build()?; let sync_proxy = - sync_proxy_builder.add_view(sliding_window_view).add_view(full).build().await?; + sync_proxy_builder.add_list(sliding_window_view).add_list(full).build().await?; - let view = sync_proxy.view("sliding").context("but we just added that view!")?; - let full_view = sync_proxy.view("full").context("but we just added that view!")?; + let view = sync_proxy.list("sliding").context("but we just added that view!")?; + let full_view = sync_proxy.list("full").context("but we just added that view!")?; assert_eq!(view.state(), SlidingSyncState::Cold, "view isn't cold"); assert_eq!(full_view.state(), SlidingSyncState::Cold, "full isn't cold"); @@ -580,8 +580,8 @@ mod tests { .sort(vec!["by_recency".to_owned(), "by_name".to_owned()]) .name("sliding") .build()?; - let sync_proxy = sync_proxy_builder.add_view(sliding_window_view).build().await?; - let view = sync_proxy.view("sliding").context("but we just added that view!")?; + let sync_proxy = sync_proxy_builder.add_list(sliding_window_view).build().await?; + let view = sync_proxy.list("sliding").context("but we just added that view!")?; let stream = sync_proxy.stream(); pin_mut!(stream); let room_summary = @@ -611,7 +611,7 @@ mod tests { let room_summary = stream.next().await.context("sync has closed unexpectedly")?; let summary = room_summary?; // we only heard about the ones we had asked for - if summary.views.iter().any(|s| s == "sliding") { + if summary.lists.iter().any(|s| s == "sliding") { break; } } @@ -633,7 +633,7 @@ mod tests { let room_summary = stream.next().await.context("sync has closed unexpectedly")?; let summary = room_summary?; // we only heard about the ones we had asked for - if summary.views.iter().any(|s| s == "sliding") { + if summary.lists.iter().any(|s| s == "sliding") { break; } } @@ -657,7 +657,7 @@ mod tests { let room_summary = stream.next().await.context("sync has closed unexpectedly")?; let summary = room_summary?; // we only heard about the ones we had asked for - if summary.views.iter().any(|s| s == "sliding") { + if summary.lists.iter().any(|s| s == "sliding") { break; } } @@ -684,8 +684,8 @@ mod tests { .sort(vec!["by_recency".to_owned(), "by_name".to_owned()]) .name("sliding") .build()?; - let sync_proxy = sync_proxy_builder.add_view(sliding_window_view).build().await?; - let view = sync_proxy.view("sliding").context("but we just added that view!")?; + let sync_proxy = sync_proxy_builder.add_list(sliding_window_view).build().await?; + let view = sync_proxy.list("sliding").context("but we just added that view!")?; let stream = sync_proxy.stream(); pin_mut!(stream); let room_summary = @@ -714,7 +714,7 @@ mod tests { let room_summary = stream.next().await.context("sync has closed unexpectedly")?; let summary = room_summary?; // we only heard about the ones we had asked for - if summary.views.iter().any(|s| s == "sliding") { + if summary.lists.iter().any(|s| s == "sliding") { break; } } @@ -737,7 +737,7 @@ mod tests { let room_summary = stream.next().await.context("sync has closed unexpectedly")?; let summary = room_summary?; // we only heard about the ones we had asked for - if summary.views.iter().any(|s| s == "sliding") { + if summary.lists.iter().any(|s| s == "sliding") { break; } } @@ -768,7 +768,7 @@ mod tests { let room_summary = stream.next().await.context("sync has closed unexpectedly")?; let summary = room_summary?; // we only heard about the ones we had asked for - if summary.views.iter().any(|s| s == "sliding") { + if summary.lists.iter().any(|s| s == "sliding") { break; } } @@ -797,7 +797,7 @@ mod tests { let room_summary = stream.next().await.context("sync has closed unexpectedly")?; let summary = room_summary?; // we only heard about the ones we had asked for - if summary.views.iter().any(|s| s == "sliding") { + if summary.lists.iter().any(|s| s == "sliding") { break; } } @@ -851,12 +851,12 @@ mod tests { let sync_proxy = sync_proxy_builder .clone() .cold_cache("sliding_sync") - .add_view(sliding_window_view) - .add_view(growing_sync) + .add_list(sliding_window_view) + .add_list(growing_sync) .build() .await?; let growing_sync = - sync_proxy.view("growing").context("but we just added that view!")?; // let's catch it up fully. + sync_proxy.list("growing").context("but we just added that view!")?; // let's catch it up fully. let stream = sync_proxy.stream(); pin_mut!(stream); while growing_sync.state() != SlidingSyncState::Live { @@ -878,8 +878,8 @@ mod tests { let _sync_proxy = sync_proxy_builder .clone() .cold_cache("sliding_sync") - .add_view(sliding_window_view) - .add_view(growing_sync) + .add_list(sliding_window_view) + .add_list(growing_sync) .build() .await?; let duration = start.elapsed(); @@ -899,8 +899,8 @@ mod tests { .name("growing") .build()?; - let sync_proxy = sync_proxy_builder.clone().add_view(growing_sync).build().await?; - let view = sync_proxy.view("growing").context("but we just added that view!")?; + let sync_proxy = sync_proxy_builder.clone().add_list(growing_sync).build().await?; + let view = sync_proxy.list("growing").context("but we just added that view!")?; let stream = sync_proxy.stream(); pin_mut!(stream); @@ -951,8 +951,8 @@ mod tests { .name("growing") .build()?; - let sync_proxy = sync_proxy_builder.clone().add_view(growing_sync).build().await?; - let view = sync_proxy.view("growing").context("but we just added that view!")?; + let sync_proxy = sync_proxy_builder.clone().add_list(growing_sync).build().await?; + let view = sync_proxy.list("growing").context("but we just added that view!")?; let stream = sync_proxy.stream(); pin_mut!(stream); @@ -1015,17 +1015,17 @@ mod tests { let sync_proxy = sync_proxy_builder .clone() .cold_cache("sliding_sync") - .add_view(growing_sync) + .add_list(growing_sync) .build() .await?; - let view = sync_proxy.view("growing").context("but we just added that view!")?; // let's catch it up fully. + let view = sync_proxy.list("growing").context("but we just added that view!")?; // let's catch it up fully. let stream = sync_proxy.stream(); pin_mut!(stream); for _n in 0..2 { let room_summary = stream.next().await.context("sync has closed unexpectedly")?; let summary = room_summary?; - if summary.views.iter().any(|s| s == "growing") { + if summary.lists.iter().any(|s| s == "growing") { break; } } @@ -1061,7 +1061,7 @@ mod tests { None => anyhow::bail!("Stream ended unexpectedly."), }; // we only heard about the ones we had asked for - if summary.views.iter().any(|s| s == "growing") { + if summary.lists.iter().any(|s| s == "growing") { break; } } @@ -1097,10 +1097,10 @@ mod tests { let sync_proxy = sync_proxy_builder .clone() .cold_cache("sliding_sync") - .add_view(growing_sync) + .add_list(growing_sync) .build() .await?; - let view = sync_proxy.view("growing").context("but we just added that view!")?; // let's catch it up fully. + let view = sync_proxy.list("growing").context("but we just added that view!")?; // let's catch it up fully. let stream = sync_proxy.stream(); pin_mut!(stream); while view.state() != SlidingSyncState::Live { @@ -1133,7 +1133,7 @@ mod tests { let room_summary = stream.next().await.context("sync has closed unexpectedly")?; let summary = room_summary?; // we only heard about the ones we had asked for - if summary.views.iter().any(|s| s == "growing") + if summary.lists.iter().any(|s| s == "growing") && view.rooms_count().unwrap_or_default() == 32 { if seen { @@ -1164,7 +1164,7 @@ mod tests { let (client, sync_proxy_builder) = random_setup_with_rooms(3).await?; let sync_proxy = sync_proxy_builder - .add_view( + .add_list( SlidingSyncList::builder() .sync_mode(SlidingSyncMode::Selective) .set_range(0u32, 2u32) @@ -1175,7 +1175,7 @@ mod tests { .build() .await?; - let view = sync_proxy.view("sliding_view").context("View `sliding_view` isn't found")?; + let view = sync_proxy.list("sliding_view").context("View `sliding_view` isn't found")?; let stream = sync_proxy.stream(); pin_mut!(stream); @@ -1203,7 +1203,7 @@ mod tests { let room_summary = stream.next().await.context("sync has closed unexpectedly")??; // we only heard about the ones we had asked for - if room_summary.views.iter().any(|s| s == "sliding_view") { + if room_summary.lists.iter().any(|s| s == "sliding_view") { break; } } From f5cc6feccd058591769fe0c1fc4bad89b091e87f Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 14:43:58 +0100 Subject: [PATCH 083/166] chore(sdk): Split the `list.rs` module into `list/mod.rs` and `list/builder.rs`. --- .../src/sliding_sync/list/builder.rs | 173 ++++++++++++++++++ .../src/sliding_sync/{list.rs => list/mod.rs} | 162 +--------------- 2 files changed, 176 insertions(+), 159 deletions(-) create mode 100644 crates/matrix-sdk/src/sliding_sync/list/builder.rs rename crates/matrix-sdk/src/sliding_sync/{list.rs => list/mod.rs} (85%) diff --git a/crates/matrix-sdk/src/sliding_sync/list/builder.rs b/crates/matrix-sdk/src/sliding_sync/list/builder.rs new file mode 100644 index 000000000..91012f18d --- /dev/null +++ b/crates/matrix-sdk/src/sliding_sync/list/builder.rs @@ -0,0 +1,173 @@ +//! Builder for [`SlidingSyncList`]. + +use std::{ + fmt::Debug, + sync::{atomic::AtomicBool, Arc, RwLock as StdRwLock}, +}; + +use eyeball::Observable; +use eyeball_im::ObservableVector; +use im::Vector; +use ruma::{api::client::sync::sync_events::v4, events::StateEventType, UInt}; + +use super::{Error, RoomListEntry, SlidingSyncList, SlidingSyncMode, SlidingSyncState}; +use crate::Result; + +/// The default name for the full sync list. +pub const FULL_SYNC_LIST_NAME: &str = "full-sync"; + +/// Builder for [`SlidingSyncList`]. +#[derive(Clone, Debug)] +pub struct SlidingSyncListBuilder { + sync_mode: SlidingSyncMode, + sort: Vec, + required_state: Vec<(StateEventType, String)>, + batch_size: u32, + send_updates_for_items: bool, + limit: Option, + filters: Option, + timeline_limit: Option, + name: Option, + state: SlidingSyncState, + rooms_count: Option, + rooms_list: Vector, + ranges: Vec<(UInt, UInt)>, +} + +impl SlidingSyncListBuilder { + pub(super) fn new() -> Self { + Self { + sync_mode: SlidingSyncMode::default(), + sort: vec!["by_recency".to_owned(), "by_name".to_owned()], + required_state: vec![ + (StateEventType::RoomEncryption, "".to_owned()), + (StateEventType::RoomTombstone, "".to_owned()), + ], + batch_size: 20, + send_updates_for_items: false, + limit: None, + filters: None, + timeline_limit: None, + name: None, + state: SlidingSyncState::default(), + rooms_count: None, + rooms_list: Vector::new(), + ranges: Vec::new(), + } + } + + /// Create a Builder set up for full sync + pub fn default_with_fullsync() -> Self { + Self::new().name(FULL_SYNC_LIST_NAME).sync_mode(SlidingSyncMode::PagingFullSync) + } + + /// Which SlidingSyncMode to start this list under. + pub fn sync_mode(mut self, value: SlidingSyncMode) -> Self { + self.sync_mode = value; + self + } + + /// Sort the rooms list by this. + pub fn sort(mut self, value: Vec) -> Self { + self.sort = value; + self + } + + /// Required states to return per room. + pub fn required_state(mut self, value: Vec<(StateEventType, String)>) -> Self { + self.required_state = value; + self + } + + /// How many rooms request at a time when doing a full-sync catch up. + pub fn batch_size(mut self, value: u32) -> Self { + self.batch_size = value; + self + } + + /// Whether the list should send `UpdatedAt`-Diff signals for rooms that + /// have changed. + pub fn send_updates_for_items(mut self, value: bool) -> Self { + self.send_updates_for_items = value; + self + } + + /// How many rooms request a total hen doing a full-sync catch up. + pub fn limit(mut self, value: impl Into>) -> Self { + self.limit = value.into(); + self + } + + /// Any filters to apply to the query. + pub fn filters(mut self, value: Option) -> Self { + self.filters = value; + self + } + + /// Set the limit of regular events to fetch for the timeline. + pub fn timeline_limit>(mut self, timeline_limit: U) -> Self { + self.timeline_limit = Some(timeline_limit.into()); + self + } + + /// Reset the limit of regular events to fetch for the timeline. It is left + /// to the server to decide how many to send back + pub fn no_timeline_limit(mut self) -> Self { + self.timeline_limit = Default::default(); + self + } + + /// Set the name of this list, to easily recognize it. + pub fn name(mut self, value: impl Into) -> Self { + self.name = Some(value.into()); + self + } + + /// Set the ranges to fetch + pub fn ranges>(mut self, range: Vec<(U, U)>) -> Self { + self.ranges = range.into_iter().map(|(a, b)| (a.into(), b.into())).collect(); + self + } + + /// Set a single range fetch + pub fn set_range>(mut self, from: U, to: U) -> Self { + self.ranges = vec![(from.into(), to.into())]; + self + } + + /// Set the ranges to fetch + pub fn add_range>(mut self, from: U, to: U) -> Self { + self.ranges.push((from.into(), to.into())); + self + } + + /// Set the ranges to fetch + pub fn reset_ranges(mut self) -> Self { + self.ranges = Default::default(); + self + } + + /// Build the list + pub fn build(self) -> Result { + let mut rooms_list = ObservableVector::new(); + rooms_list.append(self.rooms_list); + + Ok(SlidingSyncList { + sync_mode: self.sync_mode, + sort: self.sort, + required_state: self.required_state, + batch_size: self.batch_size, + send_updates_for_items: self.send_updates_for_items, + limit: self.limit, + filters: self.filters, + timeline_limit: Arc::new(StdRwLock::new(Observable::new(self.timeline_limit))), + name: self.name.ok_or(Error::BuildMissingField("name"))?, + state: Arc::new(StdRwLock::new(Observable::new(self.state))), + rooms_count: Arc::new(StdRwLock::new(Observable::new(self.rooms_count))), + rooms_list: Arc::new(StdRwLock::new(rooms_list)), + ranges: Arc::new(StdRwLock::new(Observable::new(self.ranges))), + is_cold: Arc::new(AtomicBool::new(false)), + rooms_updated_broadcast: Arc::new(StdRwLock::new(Observable::new(()))), + }) + } +} diff --git a/crates/matrix-sdk/src/sliding_sync/list.rs b/crates/matrix-sdk/src/sliding_sync/list/mod.rs similarity index 85% rename from crates/matrix-sdk/src/sliding_sync/list.rs rename to crates/matrix-sdk/src/sliding_sync/list/mod.rs index feefdee90..752122dd3 100644 --- a/crates/matrix-sdk/src/sliding_sync/list.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/mod.rs @@ -1,3 +1,5 @@ +mod builder; + use std::{ collections::BTreeMap, fmt::Debug, @@ -8,6 +10,7 @@ use std::{ }, }; +pub use builder::*; use eyeball::Observable; use eyeball_im::{ObservableVector, VectorDiff}; use futures_core::Stream; @@ -430,165 +433,6 @@ impl FrozenSlidingSyncList { } } -/// the default name for the full sync list -pub const FULL_SYNC_LIST_NAME: &str = "full-sync"; - -/// Builder for [`SlidingSyncList`]. -#[derive(Clone, Debug)] -pub struct SlidingSyncListBuilder { - sync_mode: SlidingSyncMode, - sort: Vec, - required_state: Vec<(StateEventType, String)>, - batch_size: u32, - send_updates_for_items: bool, - limit: Option, - filters: Option, - timeline_limit: Option, - name: Option, - state: SlidingSyncState, - rooms_count: Option, - rooms_list: Vector, - ranges: Vec<(UInt, UInt)>, -} - -impl SlidingSyncListBuilder { - fn new() -> Self { - Self { - sync_mode: SlidingSyncMode::default(), - sort: vec!["by_recency".to_owned(), "by_name".to_owned()], - required_state: vec![ - (StateEventType::RoomEncryption, "".to_owned()), - (StateEventType::RoomTombstone, "".to_owned()), - ], - batch_size: 20, - send_updates_for_items: false, - limit: None, - filters: None, - timeline_limit: None, - name: None, - state: SlidingSyncState::default(), - rooms_count: None, - rooms_list: Vector::new(), - ranges: Vec::new(), - } - } - - /// Create a Builder set up for full sync - pub fn default_with_fullsync() -> Self { - Self::new().name(FULL_SYNC_LIST_NAME).sync_mode(SlidingSyncMode::PagingFullSync) - } - - /// Which SlidingSyncMode to start this list under. - pub fn sync_mode(mut self, value: SlidingSyncMode) -> Self { - self.sync_mode = value; - self - } - - /// Sort the rooms list by this. - pub fn sort(mut self, value: Vec) -> Self { - self.sort = value; - self - } - - /// Required states to return per room. - pub fn required_state(mut self, value: Vec<(StateEventType, String)>) -> Self { - self.required_state = value; - self - } - - /// How many rooms request at a time when doing a full-sync catch up. - pub fn batch_size(mut self, value: u32) -> Self { - self.batch_size = value; - self - } - - /// Whether the list should send `UpdatedAt`-Diff signals for rooms that - /// have changed. - pub fn send_updates_for_items(mut self, value: bool) -> Self { - self.send_updates_for_items = value; - self - } - - /// How many rooms request a total hen doing a full-sync catch up. - pub fn limit(mut self, value: impl Into>) -> Self { - self.limit = value.into(); - self - } - - /// Any filters to apply to the query. - pub fn filters(mut self, value: Option) -> Self { - self.filters = value; - self - } - - /// Set the limit of regular events to fetch for the timeline. - pub fn timeline_limit>(mut self, timeline_limit: U) -> Self { - self.timeline_limit = Some(timeline_limit.into()); - self - } - - /// Reset the limit of regular events to fetch for the timeline. It is left - /// to the server to decide how many to send back - pub fn no_timeline_limit(mut self) -> Self { - self.timeline_limit = Default::default(); - self - } - - /// Set the name of this list, to easily recognize it. - pub fn name(mut self, value: impl Into) -> Self { - self.name = Some(value.into()); - self - } - - /// Set the ranges to fetch - pub fn ranges>(mut self, range: Vec<(U, U)>) -> Self { - self.ranges = range.into_iter().map(|(a, b)| (a.into(), b.into())).collect(); - self - } - - /// Set a single range fetch - pub fn set_range>(mut self, from: U, to: U) -> Self { - self.ranges = vec![(from.into(), to.into())]; - self - } - - /// Set the ranges to fetch - pub fn add_range>(mut self, from: U, to: U) -> Self { - self.ranges.push((from.into(), to.into())); - self - } - - /// Set the ranges to fetch - pub fn reset_ranges(mut self) -> Self { - self.ranges = Default::default(); - self - } - - /// Build the list - pub fn build(self) -> Result { - let mut rooms_list = ObservableVector::new(); - rooms_list.append(self.rooms_list); - - Ok(SlidingSyncList { - sync_mode: self.sync_mode, - sort: self.sort, - required_state: self.required_state, - batch_size: self.batch_size, - send_updates_for_items: self.send_updates_for_items, - limit: self.limit, - filters: self.filters, - timeline_limit: Arc::new(StdRwLock::new(Observable::new(self.timeline_limit))), - name: self.name.ok_or(Error::BuildMissingField("name"))?, - state: Arc::new(StdRwLock::new(Observable::new(self.state))), - rooms_count: Arc::new(StdRwLock::new(Observable::new(self.rooms_count))), - rooms_list: Arc::new(StdRwLock::new(rooms_list)), - ranges: Arc::new(StdRwLock::new(Observable::new(self.ranges))), - is_cold: Arc::new(AtomicBool::new(false)), - rooms_updated_broadcast: Arc::new(StdRwLock::new(Observable::new(()))), - }) - } -} - enum InnerSlidingSyncListRequestGenerator { GrowingFullSync { position: u32, batch_size: u32, limit: Option, live: bool }, PagingFullSync { position: u32, batch_size: u32, limit: Option, live: bool }, From 6d2055449dbbe9d40c6c99a6892a138767842935 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 14:51:34 +0100 Subject: [PATCH 084/166] chore(bindings): Continue to rename view to list in Sliding Sync. --- bindings/matrix-sdk-ffi/src/api.udl | 2 +- bindings/matrix-sdk-ffi/src/sliding_sync.rs | 90 ++++++++++----------- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/api.udl b/bindings/matrix-sdk-ffi/src/api.udl index 96ef2c5cf..c8ce0483d 100644 --- a/bindings/matrix-sdk-ffi/src/api.udl +++ b/bindings/matrix-sdk-ffi/src/api.udl @@ -25,7 +25,7 @@ dictionary RoomSubscription { }; dictionary UpdateSummary { - sequence views; + sequence lists; sequence rooms; }; diff --git a/bindings/matrix-sdk-ffi/src/sliding_sync.rs b/bindings/matrix-sdk-ffi/src/sliding_sync.rs index 4ccd544e2..272ad0025 100644 --- a/bindings/matrix-sdk-ffi/src/sliding_sync.rs +++ b/bindings/matrix-sdk-ffi/src/sliding_sync.rs @@ -246,8 +246,8 @@ pub struct SlidingSyncSubscribeResult { } pub struct UpdateSummary { - /// The views (according to their name), which have seen an update - pub views: Vec, + /// The lists (according to their name), which have seen an update + pub lists: Vec, pub rooms: Vec, } @@ -276,13 +276,13 @@ impl From for RumaRoomSubscription { impl From for UpdateSummary { fn from(other: matrix_sdk::UpdateSummary) -> UpdateSummary { UpdateSummary { - views: other.lists, + lists: other.lists, rooms: other.rooms.into_iter().map(|r| r.as_str().to_owned()).collect(), } } } -pub enum SlidingSyncViewRoomsListDiff { +pub enum SlidingSyncListRoomsListDiff { Append { values: Vec }, Insert { index: u32, value: RoomListEntry }, Set { index: u32, value: RoomListEntry }, @@ -295,33 +295,33 @@ pub enum SlidingSyncViewRoomsListDiff { Reset { values: Vec }, } -impl From> for SlidingSyncViewRoomsListDiff { +impl From> for SlidingSyncListRoomsListDiff { fn from(other: VectorDiff) -> Self { match other { - VectorDiff::Append { values } => SlidingSyncViewRoomsListDiff::Append { + VectorDiff::Append { values } => SlidingSyncListRoomsListDiff::Append { values: values.into_iter().map(|e| (&e).into()).collect(), }, VectorDiff::Insert { index, value } => { - SlidingSyncViewRoomsListDiff::Insert { index: index as u32, value: (&value).into() } + SlidingSyncListRoomsListDiff::Insert { index: index as u32, value: (&value).into() } } VectorDiff::Set { index, value } => { - SlidingSyncViewRoomsListDiff::Set { index: index as u32, value: (&value).into() } + SlidingSyncListRoomsListDiff::Set { index: index as u32, value: (&value).into() } } VectorDiff::Remove { index } => { - SlidingSyncViewRoomsListDiff::Remove { index: index as u32 } + SlidingSyncListRoomsListDiff::Remove { index: index as u32 } } VectorDiff::PushBack { value } => { - SlidingSyncViewRoomsListDiff::PushBack { value: (&value).into() } + SlidingSyncListRoomsListDiff::PushBack { value: (&value).into() } } VectorDiff::PushFront { value } => { - SlidingSyncViewRoomsListDiff::PushFront { value: (&value).into() } + SlidingSyncListRoomsListDiff::PushFront { value: (&value).into() } } - VectorDiff::PopBack => SlidingSyncViewRoomsListDiff::PopBack, - VectorDiff::PopFront => SlidingSyncViewRoomsListDiff::PopFront, - VectorDiff::Clear => SlidingSyncViewRoomsListDiff::Clear, + VectorDiff::PopBack => SlidingSyncListRoomsListDiff::PopBack, + VectorDiff::PopFront => SlidingSyncListRoomsListDiff::PopFront, + VectorDiff::Clear => SlidingSyncListRoomsListDiff::Clear, VectorDiff::Reset { values } => { warn!("Room list subscriber lagged behind and was reset"); - SlidingSyncViewRoomsListDiff::Reset { + SlidingSyncListRoomsListDiff::Reset { values: values.into_iter().map(|e| (&e).into()).collect(), } } @@ -348,25 +348,25 @@ impl From<&MatrixRoomEntry> for RoomListEntry { } } -pub trait SlidingSyncViewRoomItemsObserver: Sync + Send { +pub trait SlidingSyncListRoomItemsObserver: Sync + Send { fn did_receive_update(&self); } -pub trait SlidingSyncViewRoomListObserver: Sync + Send { - fn did_receive_update(&self, diff: SlidingSyncViewRoomsListDiff); +pub trait SlidingSyncListRoomListObserver: Sync + Send { + fn did_receive_update(&self, diff: SlidingSyncListRoomsListDiff); } -pub trait SlidingSyncViewRoomsCountObserver: Sync + Send { +pub trait SlidingSyncListRoomsCountObserver: Sync + Send { fn did_receive_update(&self, new_count: u32); } -pub trait SlidingSyncViewStateObserver: Sync + Send { +pub trait SlidingSyncListStateObserver: Sync + Send { fn did_receive_update(&self, new_state: SlidingSyncState); } #[derive(Clone)] -pub struct SlidingSyncViewBuilder { - inner: matrix_sdk::SlidingSyncViewBuilder, +pub struct SlidingSyncListBuilder { + inner: matrix_sdk::SlidingSyncListBuilder, } #[derive(uniffi::Record)] @@ -404,7 +404,7 @@ impl From for SyncRequestListFilters { } } -impl SlidingSyncViewBuilder { +impl SlidingSyncListBuilder { pub fn new() -> Self { Self { inner: matrix_sdk::SlidingSyncList::builder() } } @@ -427,14 +427,14 @@ impl SlidingSyncViewBuilder { Arc::new(builder) } - pub fn build(self: Arc) -> anyhow::Result> { + pub fn build(self: Arc) -> anyhow::Result> { let builder = unwrap_or_clone_arc(self); Ok(Arc::new(builder.inner.build()?.into())) } } #[uniffi::export] -impl SlidingSyncViewBuilder { +impl SlidingSyncListBuilder { pub fn sort(self: Arc, sort: Vec) -> Arc { let mut builder = unwrap_or_clone_arc(self); builder.inner = builder.inner.sort(sort); @@ -511,20 +511,20 @@ impl SlidingSyncViewBuilder { } #[derive(Clone)] -pub struct SlidingSyncView { +pub struct SlidingSyncList { inner: matrix_sdk::SlidingSyncList, } -impl From for SlidingSyncView { +impl From for SlidingSyncList { fn from(inner: matrix_sdk::SlidingSyncList) -> Self { - SlidingSyncView { inner } + SlidingSyncList { inner } } } -impl SlidingSyncView { +impl SlidingSyncList { pub fn observe_state( &self, - observer: Box, + observer: Box, ) -> Arc { let mut state_stream = self.inner.state_stream(); @@ -539,7 +539,7 @@ impl SlidingSyncView { pub fn observe_room_list( &self, - observer: Box, + observer: Box, ) -> Arc { let mut rooms_list_stream = self.inner.rooms_list_stream(); @@ -554,7 +554,7 @@ impl SlidingSyncView { pub fn observe_room_items( &self, - observer: Box, + observer: Box, ) -> Arc { let mut rooms_updated = Observable::subscribe(&self.inner.rooms_updated_broadcast.read().unwrap()); @@ -569,7 +569,7 @@ impl SlidingSyncView { pub fn observe_rooms_count( &self, - observer: Box, + observer: Box, ) -> Arc { let mut rooms_count_stream = self.inner.rooms_count_stream(); @@ -584,7 +584,7 @@ impl SlidingSyncView { } #[uniffi::export] -impl SlidingSyncView { +impl SlidingSyncList { /// Get the current list of rooms pub fn current_rooms_list(&self) -> Vec { self.inner.rooms_list() @@ -709,16 +709,16 @@ impl SlidingSync { #[uniffi::export] impl SlidingSync { #[allow(clippy::significant_drop_in_scrutinee)] - pub fn get_view(&self, name: String) -> Option> { - self.inner.list(&name).map(|inner| Arc::new(SlidingSyncView { inner })) + pub fn get_list(&self, name: String) -> Option> { + self.inner.list(&name).map(|inner| Arc::new(SlidingSyncList { inner })) } - pub fn add_view(&self, view: Arc) -> Option> { - self.inner.add_list(view.inner.clone()).map(|inner| Arc::new(SlidingSyncView { inner })) + pub fn add_list(&self, list: Arc) -> Option> { + self.inner.add_list(list.inner.clone()).map(|inner| Arc::new(SlidingSyncList { inner })) } - pub fn pop_view(&self, name: String) -> Option> { - self.inner.pop_list(&name).map(|inner| Arc::new(SlidingSyncView { inner })) + pub fn pop_list(&self, name: String) -> Option> { + self.inner.pop_list(&name).map(|inner| Arc::new(SlidingSyncList { inner })) } pub fn add_common_extensions(&self) { @@ -792,13 +792,13 @@ impl SlidingSyncBuilder { #[uniffi::export] impl SlidingSyncBuilder { - pub fn add_fullsync_view(self: Arc) -> Arc { + pub fn add_fullsync_list(self: Arc) -> Arc { let mut builder = unwrap_or_clone_arc(self); builder.inner = builder.inner.add_fullsync_list(); Arc::new(builder) } - pub fn no_views(self: Arc) -> Arc { + pub fn no_lists(self: Arc) -> Arc { let mut builder = unwrap_or_clone_arc(self); builder.inner = builder.inner.no_lists(); Arc::new(builder) @@ -810,10 +810,10 @@ impl SlidingSyncBuilder { Arc::new(builder) } - pub fn add_view(self: Arc, v: Arc) -> Arc { + pub fn add_list(self: Arc, v: Arc) -> Arc { let mut builder = unwrap_or_clone_arc(self); - let view = unwrap_or_clone_arc(v); - builder.inner = builder.inner.add_list(view.inner); + let list = unwrap_or_clone_arc(v); + builder.inner = builder.inner.add_list(list.inner); Arc::new(builder) } From 1f5c4feafcb563fe289598a89374ade192236fd3 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 15:01:02 +0100 Subject: [PATCH 085/166] chore(sdk): Extract Sliding Sync request generators into their own module. --- .../src/sliding_sync/list/builder.rs | 2 +- .../matrix-sdk/src/sliding_sync/list/mod.rs | 218 +----------------- .../sliding_sync/list/request_generator.rs | 215 +++++++++++++++++ 3 files changed, 220 insertions(+), 215 deletions(-) create mode 100644 crates/matrix-sdk/src/sliding_sync/list/request_generator.rs diff --git a/crates/matrix-sdk/src/sliding_sync/list/builder.rs b/crates/matrix-sdk/src/sliding_sync/list/builder.rs index 91012f18d..36cd52f2e 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/builder.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/builder.rs @@ -56,7 +56,7 @@ impl SlidingSyncListBuilder { } } - /// Create a Builder set up for full sync + /// Create a Builder set up for full sync. pub fn default_with_fullsync() -> Self { Self::new().name(FULL_SYNC_LIST_NAME).sync_mode(SlidingSyncMode::PagingFullSync) } diff --git a/crates/matrix-sdk/src/sliding_sync/list/mod.rs b/crates/matrix-sdk/src/sliding_sync/list/mod.rs index 752122dd3..e0cef3eab 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/mod.rs @@ -1,4 +1,5 @@ mod builder; +mod request_generator; use std::{ collections::BTreeMap, @@ -15,11 +16,10 @@ use eyeball::Observable; use eyeball_im::{ObservableVector, VectorDiff}; use futures_core::Stream; use im::Vector; -use ruma::{ - api::client::sync::sync_events::v4, assign, events::StateEventType, OwnedRoomId, RoomId, UInt, -}; +pub(super) use request_generator::*; +use ruma::{api::client::sync::sync_events::v4, events::StateEventType, OwnedRoomId, RoomId, UInt}; use serde::{Deserialize, Serialize}; -use tracing::{debug, error, instrument, trace, warn}; +use tracing::{debug, instrument, warn}; use super::{Error, FrozenSlidingSyncRoom, SlidingSyncRoom}; use crate::Result; @@ -433,216 +433,6 @@ impl FrozenSlidingSyncList { } } -enum InnerSlidingSyncListRequestGenerator { - GrowingFullSync { position: u32, batch_size: u32, limit: Option, live: bool }, - PagingFullSync { position: u32, batch_size: u32, limit: Option, live: bool }, - Live, -} - -pub(super) struct SlidingSyncListRequestGenerator { - list: SlidingSyncList, - ranges: Vec<(usize, usize)>, - inner: InnerSlidingSyncListRequestGenerator, -} - -impl SlidingSyncListRequestGenerator { - fn new_with_paging_syncup(list: SlidingSyncList) -> Self { - let batch_size = list.batch_size; - let limit = list.limit; - let position = list - .ranges - .read() - .unwrap() - .first() - .map(|(_start, end)| u32::try_from(*end).unwrap()) - .unwrap_or_default(); - - SlidingSyncListRequestGenerator { - list, - ranges: Default::default(), - inner: InnerSlidingSyncListRequestGenerator::PagingFullSync { - position, - batch_size, - limit, - live: false, - }, - } - } - - fn new_with_growing_syncup(list: SlidingSyncList) -> Self { - let batch_size = list.batch_size; - let limit = list.limit; - let position = list - .ranges - .read() - .unwrap() - .first() - .map(|(_start, end)| u32::try_from(*end).unwrap()) - .unwrap_or_default(); - - SlidingSyncListRequestGenerator { - list, - ranges: Default::default(), - inner: InnerSlidingSyncListRequestGenerator::GrowingFullSync { - position, - batch_size, - limit, - live: false, - }, - } - } - - fn new_live(list: SlidingSyncList) -> Self { - SlidingSyncListRequestGenerator { - list, - ranges: Default::default(), - inner: InnerSlidingSyncListRequestGenerator::Live, - } - } - - fn prefetch_request( - &mut self, - start: u32, - batch_size: u32, - limit: Option, - ) -> v4::SyncRequestList { - let calc_end = start + batch_size; - - let mut end = match limit { - Some(l) => std::cmp::min(l, calc_end), - _ => calc_end, - }; - - end = match self.list.rooms_count() { - Some(total_room_count) => std::cmp::min(end, total_room_count - 1), - _ => end, - }; - - self.make_request_for_ranges(vec![(start.into(), end.into())]) - } - - #[instrument(skip(self), fields(name = self.list.name))] - fn make_request_for_ranges(&mut self, ranges: Vec<(UInt, UInt)>) -> v4::SyncRequestList { - let sort = self.list.sort.clone(); - let required_state = self.list.required_state.clone(); - let timeline_limit = **self.list.timeline_limit.read().unwrap(); - let filters = self.list.filters.clone(); - - self.ranges = ranges - .iter() - .map(|(a, b)| { - ( - usize::try_from(*a).expect("range is a valid u32"), - usize::try_from(*b).expect("range is a valid u32"), - ) - }) - .collect(); - - assign!(v4::SyncRequestList::default(), { - ranges: ranges, - room_details: assign!(v4::RoomDetailsConfig::default(), { - required_state, - timeline_limit, - }), - sort, - filters, - }) - } - - // generate the next live request - fn live_request(&mut self) -> v4::SyncRequestList { - let ranges = self.list.ranges.read().unwrap().clone(); - self.make_request_for_ranges(ranges) - } - - #[instrument(skip_all, fields(name = self.list.name, rooms_count, has_ops = !ops.is_empty()))] - pub(super) fn handle_response( - &mut self, - rooms_count: u32, - ops: &Vec, - rooms: &Vec, - ) -> Result { - let response = self.list.handle_response(rooms_count, ops, &self.ranges, rooms)?; - self.update_state(rooms_count.saturating_sub(1)); // index is 0 based, count is 1 based - - Ok(response) - } - - fn update_state(&mut self, max_index: u32) { - let Some((_start, range_end)) = self.ranges.first() else { - error!("Why don't we have any ranges?"); - return - }; - - let end = if &(max_index as usize) < range_end { max_index } else { *range_end as u32 }; - - trace!(end, max_index, range_end, name = self.list.name, "updating state"); - - match &mut self.inner { - InnerSlidingSyncListRequestGenerator::PagingFullSync { - position, live, limit, .. - } - | InnerSlidingSyncListRequestGenerator::GrowingFullSync { - position, live, limit, .. - } => { - let max = limit.map(|limit| std::cmp::min(limit, max_index)).unwrap_or(max_index); - trace!(end, max, name = self.list.name, "updating state"); - if end >= max { - trace!(name = self.list.name, "going live"); - // we are switching to live mode - self.list.set_range(0, max); - *position = max; - *live = true; - - Observable::update_eq(&mut self.list.state.write().unwrap(), |state| { - *state = SlidingSyncState::Live; - }); - } else { - *position = end; - *live = false; - self.list.set_range(0, end); - Observable::update_eq(&mut self.list.state.write().unwrap(), |state| { - *state = SlidingSyncState::CatchingUp; - }); - } - } - InnerSlidingSyncListRequestGenerator::Live => { - Observable::update_eq(&mut self.list.state.write().unwrap(), |state| { - *state = SlidingSyncState::Live; - }); - } - } - } -} - -impl Iterator for SlidingSyncListRequestGenerator { - type Item = v4::SyncRequestList; - - fn next(&mut self) -> Option { - match self.inner { - InnerSlidingSyncListRequestGenerator::PagingFullSync { live, .. } - | InnerSlidingSyncListRequestGenerator::GrowingFullSync { live, .. } - if live => - { - Some(self.live_request()) - } - InnerSlidingSyncListRequestGenerator::PagingFullSync { - position, - batch_size, - limit, - .. - } => Some(self.prefetch_request(position, batch_size, limit)), - InnerSlidingSyncListRequestGenerator::GrowingFullSync { - position, - batch_size, - limit, - .. - } => Some(self.prefetch_request(0, position + batch_size, limit)), - InnerSlidingSyncListRequestGenerator::Live => Some(self.live_request()), - } - } -} - #[instrument(skip(ops))] fn room_ops( rooms_list: &mut ObservableVector, diff --git a/crates/matrix-sdk/src/sliding_sync/list/request_generator.rs b/crates/matrix-sdk/src/sliding_sync/list/request_generator.rs new file mode 100644 index 000000000..58c0808d3 --- /dev/null +++ b/crates/matrix-sdk/src/sliding_sync/list/request_generator.rs @@ -0,0 +1,215 @@ +use eyeball::Observable; +use ruma::{api::client::sync::sync_events::v4, assign, OwnedRoomId, UInt}; +use tracing::{error, instrument, trace}; + +use super::{Error, SlidingSyncList, SlidingSyncState}; + +enum InnerSlidingSyncListRequestGenerator { + GrowingFullSync { position: u32, batch_size: u32, limit: Option, live: bool }, + PagingFullSync { position: u32, batch_size: u32, limit: Option, live: bool }, + Live, +} + +pub(in super::super) struct SlidingSyncListRequestGenerator { + list: SlidingSyncList, + ranges: Vec<(usize, usize)>, + inner: InnerSlidingSyncListRequestGenerator, +} + +impl SlidingSyncListRequestGenerator { + pub(super) fn new_with_paging_syncup(list: SlidingSyncList) -> Self { + let batch_size = list.batch_size; + let limit = list.limit; + let position = list + .ranges + .read() + .unwrap() + .first() + .map(|(_start, end)| u32::try_from(*end).unwrap()) + .unwrap_or_default(); + + SlidingSyncListRequestGenerator { + list, + ranges: Default::default(), + inner: InnerSlidingSyncListRequestGenerator::PagingFullSync { + position, + batch_size, + limit, + live: false, + }, + } + } + + pub(super) fn new_with_growing_syncup(list: SlidingSyncList) -> Self { + let batch_size = list.batch_size; + let limit = list.limit; + let position = list + .ranges + .read() + .unwrap() + .first() + .map(|(_start, end)| u32::try_from(*end).unwrap()) + .unwrap_or_default(); + + SlidingSyncListRequestGenerator { + list, + ranges: Default::default(), + inner: InnerSlidingSyncListRequestGenerator::GrowingFullSync { + position, + batch_size, + limit, + live: false, + }, + } + } + + pub(super) fn new_live(list: SlidingSyncList) -> Self { + SlidingSyncListRequestGenerator { + list, + ranges: Default::default(), + inner: InnerSlidingSyncListRequestGenerator::Live, + } + } + + fn prefetch_request( + &mut self, + start: u32, + batch_size: u32, + limit: Option, + ) -> v4::SyncRequestList { + let calc_end = start + batch_size; + + let mut end = match limit { + Some(l) => std::cmp::min(l, calc_end), + _ => calc_end, + }; + + end = match self.list.rooms_count() { + Some(total_room_count) => std::cmp::min(end, total_room_count - 1), + _ => end, + }; + + self.make_request_for_ranges(vec![(start.into(), end.into())]) + } + + #[instrument(skip(self), fields(name = self.list.name))] + fn make_request_for_ranges(&mut self, ranges: Vec<(UInt, UInt)>) -> v4::SyncRequestList { + let sort = self.list.sort.clone(); + let required_state = self.list.required_state.clone(); + let timeline_limit = **self.list.timeline_limit.read().unwrap(); + let filters = self.list.filters.clone(); + + self.ranges = ranges + .iter() + .map(|(a, b)| { + ( + usize::try_from(*a).expect("range is a valid u32"), + usize::try_from(*b).expect("range is a valid u32"), + ) + }) + .collect(); + + assign!(v4::SyncRequestList::default(), { + ranges: ranges, + room_details: assign!(v4::RoomDetailsConfig::default(), { + required_state, + timeline_limit, + }), + sort, + filters, + }) + } + + // generate the next live request + fn live_request(&mut self) -> v4::SyncRequestList { + let ranges = self.list.ranges.read().unwrap().clone(); + self.make_request_for_ranges(ranges) + } + + #[instrument(skip_all, fields(name = self.list.name, rooms_count, has_ops = !ops.is_empty()))] + pub(in super::super) fn handle_response( + &mut self, + rooms_count: u32, + ops: &Vec, + rooms: &Vec, + ) -> Result { + let response = self.list.handle_response(rooms_count, ops, &self.ranges, rooms)?; + self.update_state(rooms_count.saturating_sub(1)); // index is 0 based, count is 1 based + + Ok(response) + } + + fn update_state(&mut self, max_index: u32) { + let Some((_start, range_end)) = self.ranges.first() else { + error!("Why don't we have any ranges?"); + return + }; + + let end = if &(max_index as usize) < range_end { max_index } else { *range_end as u32 }; + + trace!(end, max_index, range_end, name = self.list.name, "updating state"); + + match &mut self.inner { + InnerSlidingSyncListRequestGenerator::PagingFullSync { + position, live, limit, .. + } + | InnerSlidingSyncListRequestGenerator::GrowingFullSync { + position, live, limit, .. + } => { + let max = limit.map(|limit| std::cmp::min(limit, max_index)).unwrap_or(max_index); + trace!(end, max, name = self.list.name, "updating state"); + if end >= max { + trace!(name = self.list.name, "going live"); + // we are switching to live mode + self.list.set_range(0, max); + *position = max; + *live = true; + + Observable::update_eq(&mut self.list.state.write().unwrap(), |state| { + *state = SlidingSyncState::Live; + }); + } else { + *position = end; + *live = false; + self.list.set_range(0, end); + Observable::update_eq(&mut self.list.state.write().unwrap(), |state| { + *state = SlidingSyncState::CatchingUp; + }); + } + } + InnerSlidingSyncListRequestGenerator::Live => { + Observable::update_eq(&mut self.list.state.write().unwrap(), |state| { + *state = SlidingSyncState::Live; + }); + } + } + } +} + +impl Iterator for SlidingSyncListRequestGenerator { + type Item = v4::SyncRequestList; + + fn next(&mut self) -> Option { + match self.inner { + InnerSlidingSyncListRequestGenerator::PagingFullSync { live, .. } + | InnerSlidingSyncListRequestGenerator::GrowingFullSync { live, .. } + if live => + { + Some(self.live_request()) + } + InnerSlidingSyncListRequestGenerator::PagingFullSync { + position, + batch_size, + limit, + .. + } => Some(self.prefetch_request(position, batch_size, limit)), + InnerSlidingSyncListRequestGenerator::GrowingFullSync { + position, + batch_size, + limit, + .. + } => Some(self.prefetch_request(0, position + batch_size, limit)), + InnerSlidingSyncListRequestGenerator::Live => Some(self.live_request()), + } + } +} From c03ba55f248bc4b616101c17d7476ee5d7c9d23f Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 15:04:37 +0100 Subject: [PATCH 086/166] chore(sdk): Rename `InnerSlidingSyncListRequestGenerator` to `GeneratorKind`. --- .../sliding_sync/list/request_generator.rs | 66 ++++++------------- 1 file changed, 21 insertions(+), 45 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/list/request_generator.rs b/crates/matrix-sdk/src/sliding_sync/list/request_generator.rs index 58c0808d3..cad0812c5 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/request_generator.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/request_generator.rs @@ -4,7 +4,7 @@ use tracing::{error, instrument, trace}; use super::{Error, SlidingSyncList, SlidingSyncState}; -enum InnerSlidingSyncListRequestGenerator { +enum GeneratorKind { GrowingFullSync { position: u32, batch_size: u32, limit: Option, live: bool }, PagingFullSync { position: u32, batch_size: u32, limit: Option, live: bool }, Live, @@ -13,7 +13,7 @@ enum InnerSlidingSyncListRequestGenerator { pub(in super::super) struct SlidingSyncListRequestGenerator { list: SlidingSyncList, ranges: Vec<(usize, usize)>, - inner: InnerSlidingSyncListRequestGenerator, + kind: GeneratorKind, } impl SlidingSyncListRequestGenerator { @@ -28,15 +28,10 @@ impl SlidingSyncListRequestGenerator { .map(|(_start, end)| u32::try_from(*end).unwrap()) .unwrap_or_default(); - SlidingSyncListRequestGenerator { + Self { list, ranges: Default::default(), - inner: InnerSlidingSyncListRequestGenerator::PagingFullSync { - position, - batch_size, - limit, - live: false, - }, + kind: GeneratorKind::PagingFullSync { position, batch_size, limit, live: false }, } } @@ -51,24 +46,15 @@ impl SlidingSyncListRequestGenerator { .map(|(_start, end)| u32::try_from(*end).unwrap()) .unwrap_or_default(); - SlidingSyncListRequestGenerator { + Self { list, ranges: Default::default(), - inner: InnerSlidingSyncListRequestGenerator::GrowingFullSync { - position, - batch_size, - limit, - live: false, - }, + kind: GeneratorKind::GrowingFullSync { position, batch_size, limit, live: false }, } } pub(super) fn new_live(list: SlidingSyncList) -> Self { - SlidingSyncListRequestGenerator { - list, - ranges: Default::default(), - inner: InnerSlidingSyncListRequestGenerator::Live, - } + Self { list, ranges: Default::default(), kind: GeneratorKind::Live } } fn prefetch_request( @@ -149,13 +135,9 @@ impl SlidingSyncListRequestGenerator { trace!(end, max_index, range_end, name = self.list.name, "updating state"); - match &mut self.inner { - InnerSlidingSyncListRequestGenerator::PagingFullSync { - position, live, limit, .. - } - | InnerSlidingSyncListRequestGenerator::GrowingFullSync { - position, live, limit, .. - } => { + match &mut self.kind { + GeneratorKind::PagingFullSync { position, live, limit, .. } + | GeneratorKind::GrowingFullSync { position, live, limit, .. } => { let max = limit.map(|limit| std::cmp::min(limit, max_index)).unwrap_or(max_index); trace!(end, max, name = self.list.name, "updating state"); if end >= max { @@ -177,7 +159,7 @@ impl SlidingSyncListRequestGenerator { }); } } - InnerSlidingSyncListRequestGenerator::Live => { + GeneratorKind::Live => { Observable::update_eq(&mut self.list.state.write().unwrap(), |state| { *state = SlidingSyncState::Live; }); @@ -190,26 +172,20 @@ impl Iterator for SlidingSyncListRequestGenerator { type Item = v4::SyncRequestList; fn next(&mut self) -> Option { - match self.inner { - InnerSlidingSyncListRequestGenerator::PagingFullSync { live, .. } - | InnerSlidingSyncListRequestGenerator::GrowingFullSync { live, .. } + match self.kind { + GeneratorKind::PagingFullSync { live, .. } + | GeneratorKind::GrowingFullSync { live, .. } if live => { Some(self.live_request()) } - InnerSlidingSyncListRequestGenerator::PagingFullSync { - position, - batch_size, - limit, - .. - } => Some(self.prefetch_request(position, batch_size, limit)), - InnerSlidingSyncListRequestGenerator::GrowingFullSync { - position, - batch_size, - limit, - .. - } => Some(self.prefetch_request(0, position + batch_size, limit)), - InnerSlidingSyncListRequestGenerator::Live => Some(self.live_request()), + GeneratorKind::PagingFullSync { position, batch_size, limit, .. } => { + Some(self.prefetch_request(position, batch_size, limit)) + } + GeneratorKind::GrowingFullSync { position, batch_size, limit, .. } => { + Some(self.prefetch_request(0, position + batch_size, limit)) + } + GeneratorKind::Live => Some(self.live_request()), } } } From 866c91ffbc1543751735bf6914d80092b605ec8b Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 15:17:31 +0100 Subject: [PATCH 087/166] chore(sdk): Simplify code of Sliding Sync request generator. --- .../sliding_sync/list/request_generator.rs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/list/request_generator.rs b/crates/matrix-sdk/src/sliding_sync/list/request_generator.rs index cad0812c5..c04b916a1 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/request_generator.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/request_generator.rs @@ -1,3 +1,5 @@ +use std::cmp::min; + use eyeball::Observable; use ruma::{api::client::sync::sync_events::v4, assign, OwnedRoomId, UInt}; use tracing::{error, instrument, trace}; @@ -63,15 +65,15 @@ impl SlidingSyncListRequestGenerator { batch_size: u32, limit: Option, ) -> v4::SyncRequestList { - let calc_end = start + batch_size; + let calculated_end = start + batch_size; let mut end = match limit { - Some(l) => std::cmp::min(l, calc_end), - _ => calc_end, + Some(limit) => min(limit, calculated_end), + _ => calculated_end, }; end = match self.list.rooms_count() { - Some(total_room_count) => std::cmp::min(end, total_room_count - 1), + Some(total_room_count) => min(end, total_room_count - 1), _ => end, }; @@ -128,7 +130,8 @@ impl SlidingSyncListRequestGenerator { fn update_state(&mut self, max_index: u32) { let Some((_start, range_end)) = self.ranges.first() else { error!("Why don't we have any ranges?"); - return + + return; }; let end = if &(max_index as usize) < range_end { max_index } else { *range_end as u32 }; @@ -138,11 +141,15 @@ impl SlidingSyncListRequestGenerator { match &mut self.kind { GeneratorKind::PagingFullSync { position, live, limit, .. } | GeneratorKind::GrowingFullSync { position, live, limit, .. } => { - let max = limit.map(|limit| std::cmp::min(limit, max_index)).unwrap_or(max_index); + let max = limit.map(|limit| min(limit, max_index)).unwrap_or(max_index); + trace!(end, max, name = self.list.name, "updating state"); + if end >= max { + // Switching to live mode. + trace!(name = self.list.name, "going live"); - // we are switching to live mode + self.list.set_range(0, max); *position = max; *live = true; @@ -154,11 +161,13 @@ impl SlidingSyncListRequestGenerator { *position = end; *live = false; self.list.set_range(0, end); + Observable::update_eq(&mut self.list.state.write().unwrap(), |state| { *state = SlidingSyncState::CatchingUp; }); } } + GeneratorKind::Live => { Observable::update_eq(&mut self.list.state.write().unwrap(), |state| { *state = SlidingSyncState::Live; From 3bad2a84ad28a2e813dd11dc28a2862040c29a7c Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 15:17:53 +0100 Subject: [PATCH 088/166] chore(sdk): Remove `SlidingSyncListRequestGenerator::live_request`. --- .../sliding_sync/list/request_generator.rs | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/list/request_generator.rs b/crates/matrix-sdk/src/sliding_sync/list/request_generator.rs index c04b916a1..b7dc95b15 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/request_generator.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/request_generator.rs @@ -108,12 +108,6 @@ impl SlidingSyncListRequestGenerator { }) } - // generate the next live request - fn live_request(&mut self) -> v4::SyncRequestList { - let ranges = self.list.ranges.read().unwrap().clone(); - self.make_request_for_ranges(ranges) - } - #[instrument(skip_all, fields(name = self.list.name, rooms_count, has_ops = !ops.is_empty()))] pub(in super::super) fn handle_response( &mut self, @@ -182,19 +176,21 @@ impl Iterator for SlidingSyncListRequestGenerator { fn next(&mut self) -> Option { match self.kind { - GeneratorKind::PagingFullSync { live, .. } - | GeneratorKind::GrowingFullSync { live, .. } - if live => - { - Some(self.live_request()) + GeneratorKind::PagingFullSync { live: true, .. } + | GeneratorKind::GrowingFullSync { live: true, .. } + | GeneratorKind::Live => { + let ranges = self.list.ranges.read().unwrap().clone(); + + Some(self.make_request_for_ranges(ranges)) } + GeneratorKind::PagingFullSync { position, batch_size, limit, .. } => { Some(self.prefetch_request(position, batch_size, limit)) } + GeneratorKind::GrowingFullSync { position, batch_size, limit, .. } => { Some(self.prefetch_request(0, position + batch_size, limit)) } - GeneratorKind::Live => Some(self.live_request()), } } } From bfb55d782f2ee0233ec9b9fe37a400f0be9b116a Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 15:38:15 +0100 Subject: [PATCH 089/166] chore(sdk): Clean up `rooms_ops`. --- .../matrix-sdk/src/sliding_sync/list/mod.rs | 124 ++++++++++-------- 1 file changed, 71 insertions(+), 53 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/list/mod.rs b/crates/matrix-sdk/src/sliding_sync/list/mod.rs index e0cef3eab..d221a7530 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/mod.rs @@ -433,17 +433,18 @@ impl FrozenSlidingSyncList { } } -#[instrument(skip(ops))] +#[instrument(skip(operations))] fn room_ops( rooms_list: &mut ObservableVector, - ops: &Vec, + operations: &Vec, room_ranges: &Vec<(usize, usize)>, ) -> Result<(), Error> { let index_in_range = |idx| room_ranges.iter().any(|(start, end)| idx >= *start && idx <= *end); - for op in ops { - match &op.op { + + for operation in operations { + match &operation.op { v4::SlidingOp::Sync => { - let start: u32 = op + let start: u32 = operation .range .ok_or_else(|| { Error::BadResponse( @@ -452,13 +453,17 @@ fn room_ops( })? .0 .try_into() - .map_err(|e| Error::BadResponse(format!("`range` not a valid int: {e:}")))?; - let room_ids = op.room_ids.clone(); + .map_err(|error| { + Error::BadResponse(format!("`range` not a valid int: {error}")) + })?; + let room_ids = operation.room_ids.clone(); + room_ids .into_iter() .enumerate() .map(|(i, r)| { let idx = start as usize + i; + if idx >= rooms_list.len() { rooms_list.insert(idx, RoomListEntry::Filled(r)); } else { @@ -467,8 +472,9 @@ fn room_ops( }) .count(); } + v4::SlidingOp::Delete => { - let pos: u32 = op + let position: u32 = operation .index .ok_or_else(|| { Error::BadResponse( @@ -476,13 +482,14 @@ fn room_ops( ) })? .try_into() - .map_err(|e| { - Error::BadResponse(format!("`index` not a valid int for DELETE: {e:}")) + .map_err(|error| { + Error::BadResponse(format!("`index` not a valid int for DELETE: {error}")) })?; - rooms_list.set(pos as usize, RoomListEntry::Empty); + rooms_list.set(position as usize, RoomListEntry::Empty); } + v4::SlidingOp::Insert => { - let pos: usize = op + let position: usize = operation .index .ok_or_else(|| { Error::BadResponse( @@ -490,51 +497,59 @@ fn room_ops( ) })? .try_into() - .map_err(|e| { - Error::BadResponse(format!("`index` not a valid int for INSERT: {e:}")) + .map_err(|error| { + Error::BadResponse(format!("`index` not a valid int for INSERT: {error}")) })?; - let room = RoomListEntry::Filled(op.room_id.clone().ok_or_else(|| { + let room = RoomListEntry::Filled(operation.room_id.clone().ok_or_else(|| { Error::BadResponse("`room_id` must be present for INSERT operation".to_owned()) })?); - let mut dif = 0usize; + let mut offset = 0usize; + loop { - // find the next empty slot and drop it - let (prev_p, prev_overflow) = pos.overflowing_sub(dif); - let check_prev = !prev_overflow && index_in_range(prev_p); - let (next_p, overflown) = pos.overflowing_add(dif); - let check_after = - !overflown && next_p < rooms_list.len() && index_in_range(next_p); - if !check_prev && !check_after { + // Find the next empty slot and drop it. + let (previous_position, overflow) = position.overflowing_sub(offset); + let check_previous = !overflow && index_in_range(previous_position); + + let (next_position, overflow) = position.overflowing_add(offset); + let check_next = !overflow + && next_position < rooms_list.len() + && index_in_range(next_position); + + if !check_previous && !check_next { return Err(Error::BadResponse( "We were asked to insert but could not find any direction to shift to" .to_owned(), )); } - if check_prev && rooms_list[prev_p].empty_or_invalidated() { + if check_previous && rooms_list[previous_position].is_empty_or_invalidated() { // we only check for previous, if there are items left - rooms_list.remove(prev_p); + rooms_list.remove(previous_position); + break; - } else if check_after && rooms_list[next_p].empty_or_invalidated() { - rooms_list.remove(next_p); + } else if check_next && rooms_list[next_position].is_empty_or_invalidated() { + rooms_list.remove(next_position); + break; } else { - // let's check the next position; - dif += 1; + // Let's check the next position. + offset += 1; } } - rooms_list.insert(pos, room); + + rooms_list.insert(position, room); } + v4::SlidingOp::Invalidate => { let max_len = rooms_list.len(); - let (mut pos, end): (u32, u32) = if let Some(range) = op.range { + let (mut position, end): (usize, usize) = if let Some(range) = operation.range { ( - range.0.try_into().map_err(|e| { - Error::BadResponse(format!("`range.0` not a valid int: {e:}")) + range.0.try_into().map_err(|error| { + Error::BadResponse(format!("`range.0` not a valid int: {error}")) })?, - range.1.try_into().map_err(|e| { - Error::BadResponse(format!("`range.1` not a valid int: {e:}")) + range.1.try_into().map_err(|error| { + Error::BadResponse(format!("`range.1` not a valid int: {error}")) })?, ) } else { @@ -543,35 +558,38 @@ fn room_ops( )); }; - if pos > end { + if position > end { return Err(Error::BadResponse( "Invalid invalidation, end smaller than start".to_owned(), )); } - // ranges are inclusive up to the last index. e.g. `[0, 10]`; `[0, 0]`. - // ensure we pick them all up - while pos <= end { - if pos as usize >= max_len { + // Ranges are inclusive up to the last index. e.g. `[0, 10]`; `[0, 0]`. + // ensure we pick them all up. + while position <= end { + if position >= max_len { break; // how does this happen? } - let idx = pos as usize; - let entry = if let Some(RoomListEntry::Filled(b)) = rooms_list.get(idx) { - Some(b.clone()) - } else { - None - }; - if let Some(b) = entry { - rooms_list.set(pos as usize, RoomListEntry::Invalidated(b)); + let room_id = + if let Some(RoomListEntry::Filled(room_id)) = rooms_list.get(position) { + Some(room_id.clone()) + } else { + None + }; + + if let Some(room_id) = room_id { + rooms_list.set(position, RoomListEntry::Invalidated(room_id)); } else { - rooms_list.set(pos as usize, RoomListEntry::Empty); + rooms_list.set(position, RoomListEntry::Empty); } - pos += 1; + + position += 1; } } - s => { - warn!("Unknown operation occurred: {:?}", s); + + unknown_operation => { + warn!("Unknown operation occurred: {unknown_operation:?}"); } } } @@ -632,7 +650,7 @@ pub enum RoomListEntry { impl RoomListEntry { /// Is this entry empty or invalidated? - pub fn empty_or_invalidated(&self) -> bool { + pub fn is_empty_or_invalidated(&self) -> bool { matches!(self, Self::Empty | Self::Invalidated(_)) } From b4bf544676a47bb23c665a324a29a48c29360867 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 15:43:59 +0100 Subject: [PATCH 090/166] test: Continue the move from view to list in Sliding Sync. --- .../sliding-sync-integration-test/src/lib.rs | 252 +++++++++--------- 1 file changed, 126 insertions(+), 126 deletions(-) diff --git a/testing/sliding-sync-integration-test/src/lib.rs b/testing/sliding-sync-integration-test/src/lib.rs index 2c194f05e..4b4465bed 100644 --- a/testing/sliding-sync-integration-test/src/lib.rs +++ b/testing/sliding-sync-integration-test/src/lib.rs @@ -113,7 +113,7 @@ mod tests { .sync_mode(SlidingSyncMode::Selective) .add_range(0u32, 1) .timeline_limit(0u32) - .name("init_view") + .name("init_list") .build()?, ) .build() @@ -123,9 +123,9 @@ mod tests { let stream = sync.stream(); pin_mut!(stream); - // Get the view to all rooms to check the view' state. - let view = sync.list("init_view").context("View `init_view` isn't found")?; - assert_eq!(view.state(), SlidingSyncState::Cold); + // Get the list to all rooms to check the list' state. + let list = sync.list("init_list").context("list `init_list` isn't found")?; + assert_eq!(list.state(), SlidingSyncState::Cold); // Send the request and wait for a response. let update_summary = stream @@ -134,7 +134,7 @@ mod tests { .context("No room summary found, loop ended unsuccessfully")??; // Check the state has switched to `Live`. - assert_eq!(view.state(), SlidingSyncState::Live); + assert_eq!(list.state(), SlidingSyncState::Live); // One room has received an update. assert_eq!(update_summary.rooms.len(), 1); @@ -142,8 +142,8 @@ mod tests { // Let's fetch the room ID then. let room_id = update_summary.rooms[0].clone(); - // Let's fetch the room ID from the view too. - assert_matches!(view.rooms_list().get(0), Some(RoomListEntry::Filled(same_room_id)) => { + // Let's fetch the room ID from the list too. + assert_matches!(list.rooms_list().get(0), Some(RoomListEntry::Filled(same_room_id)) => { assert_eq!(same_room_id, &room_id); }); @@ -172,7 +172,7 @@ mod tests { .add_list( SlidingSyncList::builder() .sync_mode(SlidingSyncMode::Selective) - .name("visible_rooms_view") + .name("visible_rooms_list") .add_range(0u32, 1) .timeline_limit(1u32) .build()?, @@ -184,9 +184,9 @@ mod tests { let stream = sync.stream(); pin_mut!(stream); - // Get the view. - let view = - sync.list("visible_rooms_view").context("View `visible_rooms_view` isn't found")?; + // Get the list. + let list = + sync.list("visible_rooms_list").context("list `visible_rooms_list` isn't found")?; let mut all_event_ids = Vec::new(); @@ -238,7 +238,7 @@ mod tests { // Sync to receive messages with a `timeline_limit` set to 20. { - Observable::set(&mut view.timeline_limit.write().unwrap(), Some(uint!(20))); + Observable::set(&mut list.timeline_limit.write().unwrap(), Some(uint!(20))); let mut update_summary; @@ -258,8 +258,8 @@ mod tests { assert_eq!(update_summary.rooms.len(), 1); assert_eq!(room_id, update_summary.rooms[0]); - // Let's fetch the room ID from the view too. - assert_matches!(view.rooms_list().get(0), Some(RoomListEntry::Filled(same_room_id)) => { + // Let's fetch the room ID from the list too. + assert_matches!(list.rooms_list().get(0), Some(RoomListEntry::Filled(same_room_id)) => { assert_eq!(same_room_id, &room_id); }); @@ -322,13 +322,13 @@ mod tests { } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] - async fn adding_view_later() -> anyhow::Result<()> { - let view_name_1 = "sliding1"; - let view_name_2 = "sliding2"; - let view_name_3 = "sliding3"; + async fn adding_list_later() -> anyhow::Result<()> { + let list_name_1 = "sliding1"; + let list_name_2 = "sliding2"; + let list_name_3 = "sliding3"; let (client, sync_proxy_builder) = random_setup_with_rooms(20).await?; - let build_view = |name| { + let build_list = |name| { SlidingSyncList::builder() .sync_mode(SlidingSyncMode::Selective) .set_range(0u32, 10u32) @@ -337,14 +337,14 @@ mod tests { .build() }; let sync_proxy = sync_proxy_builder - .add_list(build_view(view_name_1)?) - .add_list(build_view(view_name_2)?) + .add_list(build_list(list_name_1)?) + .add_list(build_list(list_name_2)?) .build() .await?; - let view1 = sync_proxy.list(view_name_1).context("but we just added that view!")?; - let _view2 = sync_proxy.list(view_name_2).context("but we just added that view!")?; + let list1 = sync_proxy.list(list_name_1).context("but we just added that list!")?; + let _list2 = sync_proxy.list(list_name_2).context("but we just added that list!")?; - assert!(sync_proxy.list(view_name_3).is_none()); + assert!(sync_proxy.list(list_name_3).is_none()); let stream = sync_proxy.stream(); pin_mut!(stream); @@ -352,11 +352,11 @@ mod tests { stream.next().await.context("No room summary found, loop ended unsuccessfully")?; let summary = room_summary?; // we only heard about the ones we had asked for - assert_eq!(summary.lists, [view_name_1, view_name_2]); + assert_eq!(summary.lists, [list_name_1, list_name_2]); - assert!(sync_proxy.add_list(build_view(view_name_3)?).is_none()); + assert!(sync_proxy.add_list(build_list(list_name_3)?).is_none()); - // we need to restart the stream after every view listing update + // we need to restart the stream after every list listing update let stream = sync_proxy.stream(); pin_mut!(stream); @@ -367,8 +367,8 @@ mod tests { // we only heard about the ones we had asked for if !summary.lists.is_empty() { // only if we saw an update come through - assert_eq!(summary.lists, [view_name_3]); - // we didn't update the other views, so only no 2 should se an update + assert_eq!(summary.lists, [list_name_3]); + // we didn't update the other lists, so only no 2 should se an update saw_update = true; break; } @@ -376,8 +376,8 @@ mod tests { assert!(saw_update, "We didn't see the update come through the pipe"); - // and let's update the order of all views again - let room_id = assert_matches!(view1.rooms_list().get(4), Some(RoomListEntry::Filled(room_id)) => room_id.clone()); + // and let's update the order of all lists again + let room_id = assert_matches!(list1.rooms_list().get(4), Some(RoomListEntry::Filled(room_id)) => room_id.clone()); let room = client.get_joined_room(&room_id).context("No joined room {room_id}")?; @@ -392,8 +392,8 @@ mod tests { // we only heard about the ones we had asked for if !summary.lists.is_empty() { // only if we saw an update come through - assert_eq!(summary.lists, [view_name_1, view_name_2, view_name_3,]); - // notice that our view 2 is now the last view, but all have seen updates + assert_eq!(summary.lists, [list_name_1, list_name_2, list_name_3,]); + // notice that our list 2 is now the last list, but all have seen updates saw_update = true; break; } @@ -404,17 +404,17 @@ mod tests { Ok(()) } - // index-based views don't support removing views. Leaving this test for an API + // index-based lists don't support removing lists. Leaving this test for an API // update later. // #[tokio::test(flavor = "multi_thread", worker_threads = 4)] - async fn live_views() -> anyhow::Result<()> { - let view_name_1 = "sliding1"; - let view_name_2 = "sliding2"; - let view_name_3 = "sliding3"; + async fn live_lists() -> anyhow::Result<()> { + let list_name_1 = "sliding1"; + let list_name_2 = "sliding2"; + let list_name_3 = "sliding3"; let (client, sync_proxy_builder) = random_setup_with_rooms(20).await?; - let build_view = |name| { + let build_list = |name| { SlidingSyncList::builder() .sync_mode(SlidingSyncMode::Selective) .set_range(0u32, 10u32) @@ -423,20 +423,20 @@ mod tests { .build() }; let sync_proxy = sync_proxy_builder - .add_list(build_view(view_name_1)?) - .add_list(build_view(view_name_2)?) - .add_list(build_view(view_name_3)?) + .add_list(build_list(list_name_1)?) + .add_list(build_list(list_name_2)?) + .add_list(build_list(list_name_3)?) .build() .await?; - let Some(view1 )= sync_proxy.list(view_name_1) else { - bail!("but we just added that view!"); + let Some(list1 )= sync_proxy.list(list_name_1) else { + bail!("but we just added that list!"); }; - let Some(_view2 )= sync_proxy.list(view_name_2) else { - bail!("but we just added that view!"); + let Some(_list2 )= sync_proxy.list(list_name_2) else { + bail!("but we just added that list!"); }; - let Some(_view3 )= sync_proxy.list(view_name_3) else { - bail!("but we just added that view!"); + let Some(_list3 )= sync_proxy.list(list_name_3) else { + bail!("but we just added that list!"); }; let stream = sync_proxy.stream(); @@ -446,20 +446,20 @@ mod tests { }; let summary = room_summary?; // we only heard about the ones we had asked for - assert_eq!(summary.lists, [view_name_1, view_name_2, view_name_3]); + assert_eq!(summary.lists, [list_name_1, list_name_2, list_name_3]); - let Some(view_2) = sync_proxy.pop_list(&view_name_2.to_owned()) else { + let Some(list_2) = sync_proxy.pop_list(&list_name_2.to_owned()) else { bail!("Room exists"); }; - // we need to restart the stream after every view listing update + // we need to restart the stream after every list listing update let stream = sync_proxy.stream(); pin_mut!(stream); // Let's trigger an update by sending a message to room pos=3, making it move to // pos 0 - let room_id = assert_matches!(view1.rooms_list().get(3), Some(RoomListEntry::Filled(room_id)) => room_id.clone()); + let room_id = assert_matches!(list1.rooms_list().get(3), Some(RoomListEntry::Filled(room_id)) => room_id.clone()); let Some(room) = client.get_joined_room(&room_id) else { bail!("No joined room {room_id}"); @@ -478,7 +478,7 @@ mod tests { // we only heard about the ones we had asked for if !summary.lists.is_empty() { // only if we saw an update come through - assert_eq!(summary.lists, [view_name_1, view_name_3]); + assert_eq!(summary.lists, [list_name_1, list_name_3]); saw_update = true; break; } @@ -486,14 +486,14 @@ mod tests { assert!(saw_update, "We didn't see the update come through the pipe"); - assert!(sync_proxy.add_list(view_2).is_none()); + assert!(sync_proxy.add_list(list_2).is_none()); - // we need to restart the stream after every view listing update + // we need to restart the stream after every list listing update let stream = sync_proxy.stream(); pin_mut!(stream); - // and let's update the order of all views again - let room_id = assert_matches!(view1.rooms_list().get(4), Some(RoomListEntry::Filled(room_id)) => room_id.clone()); + // and let's update the order of all lists again + let room_id = assert_matches!(list1.rooms_list().get(4), Some(RoomListEntry::Filled(room_id)) => room_id.clone()); let Some(room) = client.get_joined_room(&room_id) else { bail!("No joined room {room_id}"); @@ -512,7 +512,7 @@ mod tests { // we only heard about the ones we had asked for if !summary.lists.is_empty() { // only if we saw an update come through - assert_eq!(summary.lists, [view_name_1, view_name_2, view_name_3]); // all views are visible again + assert_eq!(summary.lists, [list_name_1, list_name_2, list_name_3]); // all lists are visible again saw_update = true; break; } @@ -524,9 +524,9 @@ mod tests { } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] - async fn view_goes_live() -> anyhow::Result<()> { + async fn list_goes_live() -> anyhow::Result<()> { let (_client, sync_proxy_builder) = random_setup_with_rooms(21).await?; - let sliding_window_view = SlidingSyncList::builder() + let sliding_window_list = SlidingSyncList::builder() .sync_mode(SlidingSyncMode::Selective) .set_range(0u32, 10u32) .sort(vec!["by_recency".to_owned(), "by_name".to_owned()]) @@ -540,12 +540,12 @@ mod tests { .name("full") .build()?; let sync_proxy = - sync_proxy_builder.add_list(sliding_window_view).add_list(full).build().await?; + sync_proxy_builder.add_list(sliding_window_list).add_list(full).build().await?; - let view = sync_proxy.list("sliding").context("but we just added that view!")?; - let full_view = sync_proxy.list("full").context("but we just added that view!")?; - assert_eq!(view.state(), SlidingSyncState::Cold, "view isn't cold"); - assert_eq!(full_view.state(), SlidingSyncState::Cold, "full isn't cold"); + let list = sync_proxy.list("sliding").context("but we just added that list!")?; + let full_list = sync_proxy.list("full").context("but we just added that list!")?; + assert_eq!(list.state(), SlidingSyncState::Cold, "list isn't cold"); + assert_eq!(full_list.state(), SlidingSyncState::Cold, "full isn't cold"); let stream = sync_proxy.stream(); pin_mut!(stream); @@ -556,17 +556,17 @@ mod tests { // we only heard about the ones we had asked for assert_eq!(room_summary.rooms.len(), 11); - assert_eq!(view.state(), SlidingSyncState::Live, "view isn't live"); - assert_eq!(full_view.state(), SlidingSyncState::CatchingUp, "full isn't preloading"); + assert_eq!(list.state(), SlidingSyncState::Live, "list isn't live"); + assert_eq!(full_list.state(), SlidingSyncState::CatchingUp, "full isn't preloading"); // doing another two requests 0-20; 0-21 should bring full live, too let _room_summary = stream.next().await.context("No room summary found, loop ended unsuccessfully")??; - let rooms_list = full_view.rooms_list::(); + let rooms_list = full_list.rooms_list::(); assert_eq!(rooms_list, repeat(RoomListEntryEasy::Filled).take(21).collect::>()); - assert_eq!(full_view.state(), SlidingSyncState::Live, "full isn't live yet"); + assert_eq!(full_list.state(), SlidingSyncState::Live, "full isn't live yet"); Ok(()) } @@ -574,14 +574,14 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn resizing_sliding_window() -> anyhow::Result<()> { let (_client, sync_proxy_builder) = random_setup_with_rooms(20).await?; - let sliding_window_view = SlidingSyncList::builder() + let sliding_window_list = SlidingSyncList::builder() .sync_mode(SlidingSyncMode::Selective) .set_range(0u32, 10u32) .sort(vec!["by_recency".to_owned(), "by_name".to_owned()]) .name("sliding") .build()?; - let sync_proxy = sync_proxy_builder.add_list(sliding_window_view).build().await?; - let view = sync_proxy.list("sliding").context("but we just added that view!")?; + let sync_proxy = sync_proxy_builder.add_list(sliding_window_list).build().await?; + let list = sync_proxy.list("sliding").context("but we just added that list!")?; let stream = sync_proxy.stream(); pin_mut!(stream); let room_summary = @@ -590,7 +590,7 @@ mod tests { // we only heard about the ones we had asked for assert_eq!(summary.rooms.len(), 11); - let collection_simple = view.rooms_list::(); + let collection_simple = list.rooms_list::(); assert_eq!( collection_simple, @@ -600,11 +600,11 @@ mod tests { .collect::>() ); - let _signal = view.rooms_list_stream(); + let _signal = list.rooms_list_stream(); // let's move the window - view.set_range(1, 10); + list.set_range(1, 10); // Ensure 0-0 invalidation ranges work. for _n in 0..2 { @@ -616,7 +616,7 @@ mod tests { } } - let collection_simple = view.rooms_list::(); + let collection_simple = list.rooms_list::(); assert_eq!( collection_simple, @@ -627,7 +627,7 @@ mod tests { .collect::>() ); - view.set_range(5, 10); + list.set_range(5, 10); for _n in 0..2 { let room_summary = stream.next().await.context("sync has closed unexpectedly")?; @@ -638,7 +638,7 @@ mod tests { } } - let collection_simple = view.rooms_list::(); + let collection_simple = list.rooms_list::(); assert_eq!( collection_simple, @@ -651,7 +651,7 @@ mod tests { // let's move the window - view.set_range(5, 15); + list.set_range(5, 15); for _n in 0..2 { let room_summary = stream.next().await.context("sync has closed unexpectedly")?; @@ -662,7 +662,7 @@ mod tests { } } - let collection_simple = view.rooms_list::(); + let collection_simple = list.rooms_list::(); assert_eq!( collection_simple, @@ -678,14 +678,14 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn moving_out_of_sliding_window() -> anyhow::Result<()> { let (client, sync_proxy_builder) = random_setup_with_rooms(20).await?; - let sliding_window_view = SlidingSyncList::builder() + let sliding_window_list = SlidingSyncList::builder() .sync_mode(SlidingSyncMode::Selective) .set_range(1u32, 10u32) .sort(vec!["by_recency".to_owned(), "by_name".to_owned()]) .name("sliding") .build()?; - let sync_proxy = sync_proxy_builder.add_list(sliding_window_view).build().await?; - let view = sync_proxy.list("sliding").context("but we just added that view!")?; + let sync_proxy = sync_proxy_builder.add_list(sliding_window_list).build().await?; + let list = sync_proxy.list("sliding").context("but we just added that list!")?; let stream = sync_proxy.stream(); pin_mut!(stream); let room_summary = @@ -693,7 +693,7 @@ mod tests { let summary = room_summary?; // we only heard about the ones we had asked for assert_eq!(summary.rooms.len(), 10); - let collection_simple = view.rooms_list::(); + let collection_simple = list.rooms_list::(); assert_eq!( collection_simple, @@ -704,11 +704,11 @@ mod tests { .collect::>() ); - let _signal = view.rooms_list_stream(); + let _signal = list.rooms_list_stream(); // let's move the window - view.set_range(0, 10); + list.set_range(0, 10); for _n in 0..2 { let room_summary = stream.next().await.context("sync has closed unexpectedly")?; @@ -719,7 +719,7 @@ mod tests { } } - let collection_simple = view.rooms_list::(); + let collection_simple = list.rooms_list::(); assert_eq!( collection_simple, @@ -731,7 +731,7 @@ mod tests { // let's move the window again - view.set_range(2, 12); + list.set_range(2, 12); for _n in 0..2 { let room_summary = stream.next().await.context("sync has closed unexpectedly")?; @@ -742,7 +742,7 @@ mod tests { } } - let collection_simple = view.rooms_list::(); + let collection_simple = list.rooms_list::(); assert_eq!( collection_simple, @@ -756,7 +756,7 @@ mod tests { // now we "move" the room of pos 3 to pos 0; // this is a bordering case - let room_id = assert_matches!(view.rooms_list().get(3), Some(RoomListEntry::Filled(room_id)) => room_id.clone()); + let room_id = assert_matches!(list.rooms_list().get(3), Some(RoomListEntry::Filled(room_id)) => room_id.clone()); let room = client.get_joined_room(&room_id).context("No joined room {room_id}")?; @@ -773,7 +773,7 @@ mod tests { } } - let collection_simple = view.rooms_list::(); + let collection_simple = list.rooms_list::(); assert_eq!( collection_simple, @@ -786,12 +786,12 @@ mod tests { // items has moved, thus we shouldn't find it where it was assert!( - view.rooms_list::().get(3).unwrap().as_room_id().unwrap() != room_id + list.rooms_list::().get(3).unwrap().as_room_id().unwrap() != room_id ); // let's move the window again - view.set_range(0, 10); + list.set_range(0, 10); for _n in 0..2 { let room_summary = stream.next().await.context("sync has closed unexpectedly")?; @@ -802,7 +802,7 @@ mod tests { } } - let collection_simple = view.rooms_list::(); + let collection_simple = list.rooms_list::(); assert_eq!( collection_simple, @@ -815,7 +815,7 @@ mod tests { // and check that our room move has been accepted properly, too. assert_eq!( - view.rooms_list::().get(0).unwrap().as_room_id().unwrap(), + list.rooms_list::().get(0).unwrap().as_room_id().unwrap(), &room_id ); @@ -827,8 +827,8 @@ mod tests { async fn fast_unfreeze() -> anyhow::Result<()> { let (_client, sync_proxy_builder) = random_setup_with_rooms(500).await?; print!("setup took its time"); - let build_views = || { - let sliding_window_view = SlidingSyncList::builder() + let build_lists = || { + let sliding_window_list = SlidingSyncList::builder() .sync_mode(SlidingSyncMode::Selective) .set_range(1u32, 10u32) .sort(vec!["by_recency".to_owned(), "by_name".to_owned()]) @@ -840,23 +840,23 @@ mod tests { .sort(vec!["by_recency".to_owned(), "by_name".to_owned()]) .name("growing") .build()?; - anyhow::Ok((sliding_window_view, growing_sync)) + anyhow::Ok((sliding_window_list, growing_sync)) }; println!("starting the sliding sync setup"); { // SETUP - let (sliding_window_view, growing_sync) = build_views()?; + let (sliding_window_list, growing_sync) = build_lists()?; let sync_proxy = sync_proxy_builder .clone() .cold_cache("sliding_sync") - .add_list(sliding_window_view) + .add_list(sliding_window_list) .add_list(growing_sync) .build() .await?; let growing_sync = - sync_proxy.list("growing").context("but we just added that view!")?; // let's catch it up fully. + sync_proxy.list("growing").context("but we just added that list!")?; // let's catch it up fully. let stream = sync_proxy.stream(); pin_mut!(stream); while growing_sync.state() != SlidingSyncState::Live { @@ -871,14 +871,14 @@ mod tests { println!("starting from cold"); // recover from frozen state. - let (sliding_window_view, growing_sync) = build_views()?; + let (sliding_window_list, growing_sync) = build_lists()?; // we recover only the window. this should be quick! let start = Instant::now(); let _sync_proxy = sync_proxy_builder .clone() .cold_cache("sliding_sync") - .add_list(sliding_window_view) + .add_list(sliding_window_list) .add_list(growing_sync) .build() .await?; @@ -900,7 +900,7 @@ mod tests { .build()?; let sync_proxy = sync_proxy_builder.clone().add_list(growing_sync).build().await?; - let view = sync_proxy.list("growing").context("but we just added that view!")?; + let list = sync_proxy.list("growing").context("but we just added that list!")?; let stream = sync_proxy.stream(); pin_mut!(stream); @@ -912,7 +912,7 @@ mod tests { let _summary = room_summary?; } - let collection_simple = view.rooms_list::(); + let collection_simple = list.rooms_list::(); assert_eq!( collection_simple, @@ -928,7 +928,7 @@ mod tests { let _summary = room_summary?; } - let collection_simple = view.rooms_list::(); + let collection_simple = list.rooms_list::(); assert_eq!( collection_simple, @@ -952,7 +952,7 @@ mod tests { .build()?; let sync_proxy = sync_proxy_builder.clone().add_list(growing_sync).build().await?; - let view = sync_proxy.list("growing").context("but we just added that view!")?; + let list = sync_proxy.list("growing").context("but we just added that list!")?; let stream = sync_proxy.stream(); pin_mut!(stream); @@ -964,7 +964,7 @@ mod tests { let _summary = room_summary?; } - let collection_simple = view.rooms_list::(); + let collection_simple = list.rooms_list::(); assert_eq!( collection_simple.iter().fold(0, |acc, i| if *i == RoomListEntryEasy::Filled { @@ -986,7 +986,7 @@ mod tests { let _summary = room_summary?; } - let collection_simple = view.rooms_list::(); + let collection_simple = list.rooms_list::(); assert_eq!( collection_simple.iter().fold(0, |acc, i| if *i == RoomListEntryEasy::Filled { @@ -1018,7 +1018,7 @@ mod tests { .add_list(growing_sync) .build() .await?; - let view = sync_proxy.list("growing").context("but we just added that view!")?; // let's catch it up fully. + let list = sync_proxy.list("growing").context("but we just added that list!")?; // let's catch it up fully. let stream = sync_proxy.stream(); pin_mut!(stream); @@ -1030,7 +1030,7 @@ mod tests { } } - let collection_simple = view.rooms_list::(); + let collection_simple = list.rooms_list::(); assert_eq!( collection_simple.iter().fold(0, |acc, i| if *i == RoomListEntryEasy::Filled { @@ -1068,7 +1068,7 @@ mod tests { assert!(error_seen, "We have not seen the UnknownPos error"); - let collection_simple = view.rooms_list::(); + let collection_simple = list.rooms_list::(); assert_eq!( collection_simple.iter().fold(0, |acc, i| if *i == RoomListEntryEasy::Filled { @@ -1100,10 +1100,10 @@ mod tests { .add_list(growing_sync) .build() .await?; - let view = sync_proxy.list("growing").context("but we just added that view!")?; // let's catch it up fully. + let list = sync_proxy.list("growing").context("but we just added that list!")?; // let's catch it up fully. let stream = sync_proxy.stream(); pin_mut!(stream); - while view.state() != SlidingSyncState::Live { + while list.state() != SlidingSyncState::Live { // we wait until growing sync is all done, too println!("awaiting"); let _room_summary = stream @@ -1112,7 +1112,7 @@ mod tests { .context("No room summary found, loop ended unsuccessfully")??; } - let collection_simple = view.rooms_list::(); + let collection_simple = list.rooms_list::(); assert_eq!( collection_simple.iter().fold(0, |acc, i| if *i == RoomListEntryEasy::Filled { @@ -1134,7 +1134,7 @@ mod tests { let summary = room_summary?; // we only heard about the ones we had asked for if summary.lists.iter().any(|s| s == "growing") - && view.rooms_count().unwrap_or_default() == 32 + && list.rooms_count().unwrap_or_default() == 32 { if seen { // once we saw 32, we give it another loop to catch up! @@ -1145,7 +1145,7 @@ mod tests { } } - let collection_simple = view.rooms_list::(); + let collection_simple = list.rooms_list::(); assert_eq!( collection_simple.iter().fold(0, |acc, i| if *i == RoomListEntryEasy::Filled { @@ -1169,13 +1169,13 @@ mod tests { .sync_mode(SlidingSyncMode::Selective) .set_range(0u32, 2u32) .sort(vec!["by_recency".to_owned(), "by_name".to_owned()]) - .name("sliding_view") + .name("sliding_list") .build()?, ) .build() .await?; - let view = sync_proxy.list("sliding_view").context("View `sliding_view` isn't found")?; + let list = sync_proxy.list("sliding_list").context("list `sliding_list` isn't found")?; let stream = sync_proxy.stream(); pin_mut!(stream); @@ -1186,29 +1186,29 @@ mod tests { // we only heard about the ones we had asked for assert_eq!(room_summary.rooms.len(), 3); - let collection_simple = view.rooms_list::(); + let collection_simple = list.rooms_list::(); assert_eq!( collection_simple, repeat(RoomListEntryEasy::Filled).take(3).collect::>() ); - let _signal = view.rooms_list_stream(); + let _signal = list.rooms_list_stream(); // let's move the window - view.set_range(1, 2); + list.set_range(1, 2); for _n in 0..2 { let room_summary = stream.next().await.context("sync has closed unexpectedly")??; // we only heard about the ones we had asked for - if room_summary.lists.iter().any(|s| s == "sliding_view") { + if room_summary.lists.iter().any(|s| s == "sliding_list") { break; } } - let collection_simple = view.rooms_list::(); + let collection_simple = list.rooms_list::(); assert_eq!( collection_simple, @@ -1220,7 +1220,7 @@ mod tests { // let's get that first entry - let room_id = assert_matches!(view.rooms_list().get(0), Some(RoomListEntry::Invalidated(room_id)) => room_id.clone()); + let room_id = assert_matches!(list.rooms_list().get(0), Some(RoomListEntry::Invalidated(room_id)) => room_id.clone()); // send a message @@ -1305,7 +1305,7 @@ mod tests { let sliding_sync_room = sync_proxy.get_room(&room_id).expect("Slidin Sync room not found"); let event = sliding_sync_room.latest_event().await.expect("No even found"); - let collection_simple = view.rooms_list::(); + let collection_simple = list.rooms_list::(); assert_eq!( collection_simple, From a341caa7733e35d998288a0d813fe470cde4f373 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 15:46:40 +0100 Subject: [PATCH 091/166] chore(sdk): Continue the move from view to list in Sliding Sync. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 28 +++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 02db2fdec..af02f876a 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -529,7 +529,7 @@ //! .build()?; //! //! let sliding_sync = sliding_sync_builder -//! .add_list(active_view) +//! .add_list(active_list) //! .add_list(full_sync_list) //! .build() //! .await?; @@ -539,7 +539,7 @@ //! let active_list = sliding_sync.list(&active_list_name).unwrap(); //! let list_state_stream = active_list.state_stream(); //! let list_count_stream = active_list.rooms_count_stream(); -//! let list_list_stream = active_list.rooms_list_stream(); +//! let list_stream = active_list.rooms_list_stream(); //! //! tokio::spawn(async move { //! pin_mut!(list_state_stream); @@ -550,15 +550,15 @@ //! //! tokio::spawn(async move { //! pin_mut!(list_count_stream); -//! while let Some(new_count) = view_count_stream.next().await { -//! info!("active-view new count: {new_count:?}"); +//! while let Some(new_count) = list_count_stream.next().await { +//! info!("active-list new count: {new_count:?}"); //! } //! }); //! //! tokio::spawn(async move { -//! pin_mut!(view_list_stream); -//! while let Some(v_diff) = view_list_stream.next().await { -//! info!("active-view rooms view diff update: {v_diff:?}"); +//! pin_mut!(list_stream); +//! while let Some(v_diff) = list_stream.next().await { +//! info!("active-list rooms list diff update: {v_diff:?}"); //! } //! }); //! @@ -715,26 +715,26 @@ impl SlidingSync { .await?; // Write every `SlidingSyncList` inside the client the store. - let frozen_views = { + let frozen_lists = { let rooms_lock = self.rooms.read().unwrap(); self.lists .read() .unwrap() .iter() - .map(|(name, view)| { + .map(|(name, list)| { Ok(( format!("{storage_key}::{name}"), - serde_json::to_vec(&FrozenSlidingSyncList::freeze(view, &rooms_lock))?, + serde_json::to_vec(&FrozenSlidingSyncList::freeze(list, &rooms_lock))?, )) }) .collect::, crate::Error>>()? }; - for (storage_key, frozen_view) in frozen_views { - trace!(storage_key, "Saving the frozen Sliding Sync View"); + for (storage_key, frozen_list) in frozen_lists { + trace!(storage_key, "Saving the frozen Sliding Sync list"); - store.set_custom_value(storage_key.as_bytes(), frozen_view).await?; + store.set_custom_value(storage_key.as_bytes(), frozen_list).await?; } Ok(()) @@ -746,7 +746,7 @@ impl SlidingSync { } /// Generate a new [`SlidingSyncBuilder`] with the same inner settings and - /// views but without the current state. + /// lists but without the current state. pub fn new_builder_copy(&self) -> SlidingSyncBuilder { let mut builder = Self::builder() .client(self.client.clone()) From 9b1c5079e68066a60c043d5cd78c9448b3f72c57 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 15:58:22 +0100 Subject: [PATCH 092/166] chore: `cargo fmt`. --- bindings/matrix-sdk-ffi/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/lib.rs b/bindings/matrix-sdk-ffi/src/lib.rs index 141205c8b..002d77039 100644 --- a/bindings/matrix-sdk-ffi/src/lib.rs +++ b/bindings/matrix-sdk-ffi/src/lib.rs @@ -85,9 +85,9 @@ mod uniffi_types { room::{Membership, MembershipState, Room, RoomMember}, session_verification::{SessionVerificationController, SessionVerificationEmoji}, sliding_sync::{ - RequiredState, RoomListEntry, SlidingSync, SlidingSyncBuilder, - SlidingSyncRequestListFilters, SlidingSyncRoom, SlidingSyncList, - SlidingSyncListBuilder, TaskHandle, UnreadNotificationsCount, + RequiredState, RoomListEntry, SlidingSync, SlidingSyncBuilder, SlidingSyncList, + SlidingSyncListBuilder, SlidingSyncRequestListFilters, SlidingSyncRoom, TaskHandle, + UnreadNotificationsCount, }, timeline::{ AudioInfo, AudioMessageContent, EmoteMessageContent, EncryptedMessage, EventSendState, From 4212980f32a79be13bdf83ba123766df1b9b2eb5 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 16:42:09 +0100 Subject: [PATCH 093/166] chore(bindings): Remove a `From for TaskHandle` impl. This implementation is never used. --- bindings/matrix-sdk-ffi/src/sliding_sync.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/sliding_sync.rs b/bindings/matrix-sdk-ffi/src/sliding_sync.rs index 272ad0025..92ca32441 100644 --- a/bindings/matrix-sdk-ffi/src/sliding_sync.rs +++ b/bindings/matrix-sdk-ffi/src/sliding_sync.rs @@ -50,12 +50,6 @@ impl TaskHandle { } } -impl From> for TaskHandle { - fn from(value: JoinHandle<()>) -> Self { - Self::with_handle(value) - } -} - #[uniffi::export] impl TaskHandle { pub fn cancel(&self) { From ccd9ad7b497de88e2dc26710b7dab7979578b0ad Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 16:42:47 +0100 Subject: [PATCH 094/166] chore(bindings): Simplify the code. Rust is a beautiful language. Let's use all the great constructions it gives to us to write pretty code. --- bindings/matrix-sdk-ffi/src/sliding_sync.rs | 58 ++++++++++----------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/sliding_sync.rs b/bindings/matrix-sdk-ffi/src/sliding_sync.rs index 92ca32441..3a278c0ff 100644 --- a/bindings/matrix-sdk-ffi/src/sliding_sync.rs +++ b/bindings/matrix-sdk-ffi/src/sliding_sync.rs @@ -54,16 +54,20 @@ impl TaskHandle { impl TaskHandle { pub fn cancel(&self) { debug!("stoppable.cancel() called"); + if let Some(handle) = &self.handle { handle.abort(); } + if let Some(callback) = self.callback.write().unwrap().take() { callback(); } } + /// Check wether a handle-based `TaskHandle` is finished; will return + /// `false` for callback-based `TaskHandle`. pub fn is_finished(&self) -> bool { - self.handle.as_ref().map(|h| h.is_finished()).unwrap_or_default() + self.handle.as_ref().map(|handle| handle.is_finished()).unwrap_or_default() } } @@ -163,6 +167,7 @@ impl SlidingSyncRoom { listener: Box, ) -> anyhow::Result { let (items, stoppable_spawn) = self.add_timeline_listener_inner(listener)?; + Ok(SlidingSyncSubscribeResult { items, task_handle: Arc::new(stoppable_spawn) }) } @@ -173,9 +178,12 @@ impl SlidingSyncRoom { ) -> anyhow::Result { let (items, mut stoppable_spawn) = self.add_timeline_listener_inner(listener)?; let room_id = self.inner.room_id().clone(); + self.runner.subscribe(room_id.clone(), settings.map(Into::into)); + let runner = self.runner.clone(); stoppable_spawn.set_callback(Box::new(move || runner.unsubscribe(room_id))); + Ok(SlidingSyncSubscribeResult { items, task_handle: Arc::new(stoppable_spawn) }) } @@ -230,6 +238,7 @@ impl SlidingSyncRoom { let task_handle = TaskHandle::with_handle(RUNTIME.spawn(async move { join(handle_events, handle_sliding_sync_reset).await; })); + Ok((items, task_handle)) } } @@ -268,8 +277,8 @@ impl From for RumaRoomSubscription { } impl From for UpdateSummary { - fn from(other: matrix_sdk::UpdateSummary) -> UpdateSummary { - UpdateSummary { + fn from(other: matrix_sdk::UpdateSummary) -> Self { + Self { lists: other.lists, rooms: other.rooms.into_iter().map(|r| r.as_str().to_owned()).collect(), } @@ -292,32 +301,24 @@ pub enum SlidingSyncListRoomsListDiff { impl From> for SlidingSyncListRoomsListDiff { fn from(other: VectorDiff) -> Self { match other { - VectorDiff::Append { values } => SlidingSyncListRoomsListDiff::Append { - values: values.into_iter().map(|e| (&e).into()).collect(), - }, + VectorDiff::Append { values } => { + Self::Append { values: values.into_iter().map(|e| (&e).into()).collect() } + } VectorDiff::Insert { index, value } => { - SlidingSyncListRoomsListDiff::Insert { index: index as u32, value: (&value).into() } + Self::Insert { index: index as u32, value: (&value).into() } } VectorDiff::Set { index, value } => { - SlidingSyncListRoomsListDiff::Set { index: index as u32, value: (&value).into() } + Self::Set { index: index as u32, value: (&value).into() } } - VectorDiff::Remove { index } => { - SlidingSyncListRoomsListDiff::Remove { index: index as u32 } - } - VectorDiff::PushBack { value } => { - SlidingSyncListRoomsListDiff::PushBack { value: (&value).into() } - } - VectorDiff::PushFront { value } => { - SlidingSyncListRoomsListDiff::PushFront { value: (&value).into() } - } - VectorDiff::PopBack => SlidingSyncListRoomsListDiff::PopBack, - VectorDiff::PopFront => SlidingSyncListRoomsListDiff::PopFront, - VectorDiff::Clear => SlidingSyncListRoomsListDiff::Clear, + VectorDiff::Remove { index } => Self::Remove { index: index as u32 }, + VectorDiff::PushBack { value } => Self::PushBack { value: (&value).into() }, + VectorDiff::PushFront { value } => Self::PushFront { value: (&value).into() }, + VectorDiff::PopBack => Self::PopBack, + VectorDiff::PopFront => Self::PopFront, + VectorDiff::Clear => Self::Clear, VectorDiff::Reset { values } => { warn!("Room list subscriber lagged behind and was reset"); - SlidingSyncListRoomsListDiff::Reset { - values: values.into_iter().map(|e| (&e).into()).collect(), - } + Self::Reset { values: values.into_iter().map(|e| (&e).into()).collect() } } } } @@ -333,11 +334,9 @@ pub enum RoomListEntry { impl From<&MatrixRoomEntry> for RoomListEntry { fn from(other: &MatrixRoomEntry) -> Self { match other { - MatrixRoomEntry::Empty => RoomListEntry::Empty, - MatrixRoomEntry::Filled(b) => RoomListEntry::Filled { room_id: b.to_string() }, - MatrixRoomEntry::Invalidated(b) => { - RoomListEntry::Invalidated { room_id: b.to_string() } - } + MatrixRoomEntry::Empty => Self::Empty, + MatrixRoomEntry::Filled(b) => Self::Filled { room_id: b.to_string() }, + MatrixRoomEntry::Invalidated(b) => Self::Invalidated { room_id: b.to_string() }, } } } @@ -392,6 +391,7 @@ impl From for SyncRequestListFilters { tags, not_tags, } = value; + assign!(SyncRequestListFilters::default(), { is_dm, spaces, is_encrypted, is_invite, is_tombstoned, room_types, not_room_types, room_name_like, tags, not_tags, }) @@ -640,7 +640,7 @@ pub struct SlidingSync { impl SlidingSync { fn new(inner: matrix_sdk::SlidingSync, client: Client) -> Self { - SlidingSync { inner, client, observer: Default::default() } + Self { inner, client, observer: Default::default() } } pub fn set_observer(&self, observer: Option>) { From daa3b80870058c0dce4f0f26de858f98d85959cb Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 1 Mar 2023 14:10:29 +0100 Subject: [PATCH 095/166] Re-export and document LocalEventTimelineItem, RemoteEventTimelineItem --- crates/matrix-sdk/src/room/timeline/event_item.rs | 3 +++ crates/matrix-sdk/src/room/timeline/mod.rs | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk/src/room/timeline/event_item.rs b/crates/matrix-sdk/src/room/timeline/event_item.rs index ebf12dfda..71c3ce577 100644 --- a/crates/matrix-sdk/src/room/timeline/event_item.rs +++ b/crates/matrix-sdk/src/room/timeline/event_item.rs @@ -242,6 +242,8 @@ pub enum EventSendState { }, } +/// An item for an event that was created locally and not yet echoed back by the +/// homeserver. #[derive(Debug, Clone)] pub struct LocalEventTimelineItem { /// The send state of this local event. @@ -281,6 +283,7 @@ impl From for EventTimelineItem { } } +/// An item for an event that was received from the homeserver. #[derive(Clone)] pub struct RemoteEventTimelineItem { /// The event ID. diff --git a/crates/matrix-sdk/src/room/timeline/mod.rs b/crates/matrix-sdk/src/room/timeline/mod.rs index fc226c00e..fa92d594b 100644 --- a/crates/matrix-sdk/src/room/timeline/mod.rs +++ b/crates/matrix-sdk/src/room/timeline/mod.rs @@ -53,9 +53,9 @@ use self::inner::{TimelineInner, TimelineInnerState}; pub use self::{ event_item::{ AnyOtherFullStateEventContent, BundledReactions, EncryptedMessage, EventSendState, - EventTimelineItem, InReplyToDetails, MemberProfileChange, MembershipChange, Message, - OtherState, Profile, ReactionGroup, RepliedToEvent, RoomMembershipChange, Sticker, - TimelineDetails, TimelineItemContent, + EventTimelineItem, InReplyToDetails, LocalEventTimelineItem, MemberProfileChange, + MembershipChange, Message, OtherState, Profile, ReactionGroup, RemoteEventTimelineItem, + RepliedToEvent, RoomMembershipChange, Sticker, TimelineDetails, TimelineItemContent, }, pagination::{PaginationOptions, PaginationOutcome}, virtual_item::VirtualTimelineItem, From 1452a2124f08f09ef31807aac013939daaed95e0 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 1 Mar 2023 14:16:01 +0100 Subject: [PATCH 096/166] Move LocalEventTimelineItem into its own module --- .../src/room/timeline/event_item/local.rs | 39 +++++++++++++++++++ .../{event_item.rs => event_item/mod.rs} | 39 ++----------------- 2 files changed, 43 insertions(+), 35 deletions(-) create mode 100644 crates/matrix-sdk/src/room/timeline/event_item/local.rs rename crates/matrix-sdk/src/room/timeline/{event_item.rs => event_item/mod.rs} (96%) diff --git a/crates/matrix-sdk/src/room/timeline/event_item/local.rs b/crates/matrix-sdk/src/room/timeline/event_item/local.rs new file mode 100644 index 000000000..5997c1eab --- /dev/null +++ b/crates/matrix-sdk/src/room/timeline/event_item/local.rs @@ -0,0 +1,39 @@ +use ruma::{EventId, MilliSecondsSinceUnixEpoch, OwnedTransactionId, OwnedUserId}; + +use super::{EventSendState, Profile, TimelineDetails, TimelineItemContent}; + +/// An item for an event that was created locally and not yet echoed back by +/// the homeserver. +#[derive(Debug, Clone)] +pub struct LocalEventTimelineItem { + /// The send state of this local event. + pub send_state: EventSendState, + /// The transaction ID. + pub transaction_id: OwnedTransactionId, + /// The sender of the event. + pub sender: OwnedUserId, + /// The sender's profile of the event. + pub sender_profile: TimelineDetails, + /// The timestamp of the event. + pub timestamp: MilliSecondsSinceUnixEpoch, + /// The content of the event. + pub content: TimelineItemContent, +} + +impl LocalEventTimelineItem { + /// Get the event ID of this item. + /// + /// Will be `Some` if and only if `send_state` is + /// `EventSendState::Sent`. + pub fn event_id(&self) -> Option<&EventId> { + match &self.send_state { + EventSendState::Sent { event_id } => Some(event_id), + _ => None, + } + } + + /// Clone the current event item, and update its `send_state`. + pub(in crate::room::timeline) fn with_send_state(&self, send_state: EventSendState) -> Self { + Self { send_state, ..self.clone() } + } +} diff --git a/crates/matrix-sdk/src/room/timeline/event_item.rs b/crates/matrix-sdk/src/room/timeline/event_item/mod.rs similarity index 96% rename from crates/matrix-sdk/src/room/timeline/event_item.rs rename to crates/matrix-sdk/src/room/timeline/event_item/mod.rs index 71c3ce577..773af1550 100644 --- a/crates/matrix-sdk/src/room/timeline/event_item.rs +++ b/crates/matrix-sdk/src/room/timeline/event_item/mod.rs @@ -56,6 +56,10 @@ use ruma::{ use super::inner::RoomDataProvider; use crate::{Error, Result}; +mod local; + +pub use self::local::LocalEventTimelineItem; + /// An item in the timeline that represents at least one event. /// /// There is always one main event that gives the `EventTimelineItem` its @@ -242,41 +246,6 @@ pub enum EventSendState { }, } -/// An item for an event that was created locally and not yet echoed back by the -/// homeserver. -#[derive(Debug, Clone)] -pub struct LocalEventTimelineItem { - /// The send state of this local event. - pub send_state: EventSendState, - /// The transaction ID. - pub transaction_id: OwnedTransactionId, - /// The sender of the event. - pub sender: OwnedUserId, - /// The sender's profile of the event. - pub sender_profile: TimelineDetails, - /// The timestamp of the event. - pub timestamp: MilliSecondsSinceUnixEpoch, - /// The content of the event. - pub content: TimelineItemContent, -} - -impl LocalEventTimelineItem { - /// Get the event ID of this item. - /// - /// Will be `Some` if and only if `send_state` is `EventSendState::Sent`. - pub fn event_id(&self) -> Option<&EventId> { - match &self.send_state { - EventSendState::Sent { event_id } => Some(event_id), - _ => None, - } - } - - /// Clone the current event item, and update its `send_state`. - pub(super) fn with_send_state(&self, send_state: EventSendState) -> Self { - Self { send_state, ..self.clone() } - } -} - impl From for EventTimelineItem { fn from(value: LocalEventTimelineItem) -> Self { Self::Local(value) From 770a67678ccfe988d30d2b2c88fda0cb9dab0da6 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 1 Mar 2023 14:18:35 +0100 Subject: [PATCH 097/166] Move RemoteEventTimelineItem into its own module --- .../src/room/timeline/event_item/mod.rs | 82 +---------------- .../src/room/timeline/event_item/remote.rs | 87 +++++++++++++++++++ 2 files changed, 90 insertions(+), 79 deletions(-) create mode 100644 crates/matrix-sdk/src/room/timeline/event_item/remote.rs diff --git a/crates/matrix-sdk/src/room/timeline/event_item/mod.rs b/crates/matrix-sdk/src/room/timeline/event_item/mod.rs index 773af1550..9a85cd2a6 100644 --- a/crates/matrix-sdk/src/room/timeline/event_item/mod.rs +++ b/crates/matrix-sdk/src/room/timeline/event_item/mod.rs @@ -15,14 +15,13 @@ use std::{fmt, ops::Deref, sync::Arc}; use indexmap::IndexMap; -use matrix_sdk_base::deserialized_responses::{EncryptionInfo, TimelineEvent}; +use matrix_sdk_base::deserialized_responses::TimelineEvent; use ruma::{ events::{ policy::rule::{ room::PolicyRuleRoomEventContent, server::PolicyRuleServerEventContent, user::PolicyRuleUserEventContent, }, - receipt::Receipt, room::{ aliases::RoomAliasesEventContent, avatar::RoomAvatarEventContent, @@ -57,8 +56,9 @@ use super::inner::RoomDataProvider; use crate::{Error, Result}; mod local; +mod remote; -pub use self::local::LocalEventTimelineItem; +pub use self::{local::LocalEventTimelineItem, remote::RemoteEventTimelineItem}; /// An item in the timeline that represents at least one event. /// @@ -252,88 +252,12 @@ impl From for EventTimelineItem { } } -/// An item for an event that was received from the homeserver. -#[derive(Clone)] -pub struct RemoteEventTimelineItem { - /// The event ID. - pub event_id: OwnedEventId, - /// The sender of the event. - pub sender: OwnedUserId, - /// The sender's profile of the event. - pub sender_profile: TimelineDetails, - /// The timestamp of the event. - pub timestamp: MilliSecondsSinceUnixEpoch, - /// The content of the event. - pub content: TimelineItemContent, - /// All bundled reactions about the event. - pub reactions: BundledReactions, - /// All read receipts for the event. - /// - /// The key is the ID of a room member and the value are details about the - /// read receipt. - /// - /// Note that currently this ignores threads. - pub read_receipts: IndexMap, - /// Whether the event has been sent by the the logged-in user themselves. - pub is_own: bool, - /// Encryption information. - pub encryption_info: Option, - // FIXME: Expose the raw JSON of aggregated events somehow - pub raw: Raw, -} - -impl RemoteEventTimelineItem { - /// Clone the current event item, and update its `reactions`. - pub(super) fn with_reactions(&self, reactions: BundledReactions) -> Self { - Self { reactions, ..self.clone() } - } - - /// Clone the current event item, and update its `content`. - pub(super) fn with_content(&self, content: TimelineItemContent) -> Self { - Self { content, ..self.clone() } - } - - /// Clone the current event item, change its `content` to - /// [`TimelineItemContent::RedactedMessage`], and reset its `reactions`. - pub(super) fn to_redacted(&self) -> Self { - Self { - // FIXME: Change when we support state events - content: TimelineItemContent::RedactedMessage, - reactions: BundledReactions::default(), - ..self.clone() - } - } - - /// Get the reactions of this item. - pub fn reactions(&self) -> &BundledReactions { - // FIXME: Find out the state of incomplete bundled reactions, adjust - // Ruma if necessary, return the whole BundledReactions field - &self.reactions - } -} - impl From for EventTimelineItem { fn from(value: RemoteEventTimelineItem) -> Self { Self::Remote(value) } } -#[cfg(not(tarpaulin_include))] -impl fmt::Debug for RemoteEventTimelineItem { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("RemoteEventTimelineItem") - .field("event_id", &self.event_id) - .field("sender", &self.sender) - .field("timestamp", &self.timestamp) - .field("content", &self.content) - .field("reactions", &self.reactions) - .field("is_own", &self.is_own) - .field("encryption_info", &self.encryption_info) - // skip raw, too noisy - .finish_non_exhaustive() - } -} - /// The display name and avatar URL of a room member. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Profile { diff --git a/crates/matrix-sdk/src/room/timeline/event_item/remote.rs b/crates/matrix-sdk/src/room/timeline/event_item/remote.rs new file mode 100644 index 000000000..99d8424dc --- /dev/null +++ b/crates/matrix-sdk/src/room/timeline/event_item/remote.rs @@ -0,0 +1,87 @@ +use std::fmt; + +use indexmap::IndexMap; +use matrix_sdk_base::deserialized_responses::EncryptionInfo; +use ruma::{ + events::{receipt::Receipt, AnySyncTimelineEvent}, + serde::Raw, + MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, +}; + +use super::{BundledReactions, Profile, TimelineDetails, TimelineItemContent}; + +/// An item for an event that was received from the homeserver. +#[derive(Clone)] +pub struct RemoteEventTimelineItem { + /// The event ID. + pub event_id: OwnedEventId, + /// The sender of the event. + pub sender: OwnedUserId, + /// The sender's profile of the event. + pub sender_profile: TimelineDetails, + /// The timestamp of the event. + pub timestamp: MilliSecondsSinceUnixEpoch, + /// The content of the event. + pub content: TimelineItemContent, + /// All bundled reactions about the event. + pub reactions: BundledReactions, + /// All read receipts for the event. + /// + /// The key is the ID of a room member and the value are details about the + /// read receipt. + /// + /// Note that currently this ignores threads. + pub read_receipts: IndexMap, + /// Whether the event has been sent by the the logged-in user themselves. + pub is_own: bool, + /// Encryption information. + pub encryption_info: Option, + // FIXME: Expose the raw JSON of aggregated events somehow + pub raw: Raw, +} + +impl RemoteEventTimelineItem { + /// Clone the current event item, and update its `reactions`. + pub(in crate::room::timeline) fn with_reactions(&self, reactions: BundledReactions) -> Self { + Self { reactions, ..self.clone() } + } + + /// Clone the current event item, and update its `content`. + pub(in crate::room::timeline) fn with_content(&self, content: TimelineItemContent) -> Self { + Self { content, ..self.clone() } + } + + /// Clone the current event item, change its `content` to + /// [`TimelineItemContent::RedactedMessage`], and reset its `reactions`. + pub(in crate::room::timeline) fn to_redacted(&self) -> Self { + Self { + // FIXME: Change when we support state events + content: TimelineItemContent::RedactedMessage, + reactions: BundledReactions::default(), + ..self.clone() + } + } + + /// Get the reactions of this item. + pub fn reactions(&self) -> &BundledReactions { + // FIXME: Find out the state of incomplete bundled reactions, adjust + // Ruma if necessary, return the whole BundledReactions field + &self.reactions + } +} + +#[cfg(not(tarpaulin_include))] +impl fmt::Debug for RemoteEventTimelineItem { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RemoteEventTimelineItem") + .field("event_id", &self.event_id) + .field("sender", &self.sender) + .field("timestamp", &self.timestamp) + .field("content", &self.content) + .field("reactions", &self.reactions) + .field("is_own", &self.is_own) + .field("encryption_info", &self.encryption_info) + // skip raw, too noisy + .finish_non_exhaustive() + } +} From aac9d9c29ba28ed20b768836aa78878858d25ca4 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 1 Mar 2023 14:28:28 +0100 Subject: [PATCH 098/166] Move TimelineItemContent and related types into a new submodule --- .../src/room/timeline/event_item/content.rs | 588 +++++++++++++++++ .../src/room/timeline/event_item/mod.rs | 594 +----------------- 2 files changed, 602 insertions(+), 580 deletions(-) create mode 100644 crates/matrix-sdk/src/room/timeline/event_item/content.rs diff --git a/crates/matrix-sdk/src/room/timeline/event_item/content.rs b/crates/matrix-sdk/src/room/timeline/event_item/content.rs new file mode 100644 index 000000000..277f5e9a9 --- /dev/null +++ b/crates/matrix-sdk/src/room/timeline/event_item/content.rs @@ -0,0 +1,588 @@ +use std::{fmt, ops::Deref, sync::Arc}; + +use indexmap::IndexMap; +use matrix_sdk_base::deserialized_responses::TimelineEvent; +use ruma::{ + events::{ + policy::rule::{ + room::PolicyRuleRoomEventContent, server::PolicyRuleServerEventContent, + user::PolicyRuleUserEventContent, + }, + room::{ + aliases::RoomAliasesEventContent, + avatar::RoomAvatarEventContent, + canonical_alias::RoomCanonicalAliasEventContent, + create::RoomCreateEventContent, + encrypted::{EncryptedEventScheme, MegolmV1AesSha2Content, RoomEncryptedEventContent}, + encryption::RoomEncryptionEventContent, + guest_access::RoomGuestAccessEventContent, + history_visibility::RoomHistoryVisibilityEventContent, + join_rules::RoomJoinRulesEventContent, + member::{Change, RoomMemberEventContent}, + message::{self, MessageType, Relation}, + name::RoomNameEventContent, + pinned_events::RoomPinnedEventsEventContent, + power_levels::RoomPowerLevelsEventContent, + server_acl::RoomServerAclEventContent, + third_party_invite::RoomThirdPartyInviteEventContent, + tombstone::RoomTombstoneEventContent, + topic::RoomTopicEventContent, + }, + space::{child::SpaceChildEventContent, parent::SpaceParentEventContent}, + sticker::StickerEventContent, + AnyFullStateEventContent, AnyMessageLikeEventContent, AnyTimelineEvent, + FullStateEventContent, MessageLikeEventType, StateEventType, + }, + OwnedDeviceId, OwnedEventId, OwnedMxcUri, OwnedTransactionId, OwnedUserId, UserId, +}; + +use super::{Profile, TimelineDetails}; +use crate::{ + room::timeline::{inner::RoomDataProvider, Error as TimelineError}, + Result, +}; + +/// 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.sticker` event. + Sticker(Sticker), + + /// An `m.room.encrypted` event that could not be decrypted. + UnableToDecrypt(EncryptedMessage), + + /// A room membership change. + MembershipChange(RoomMembershipChange), + + /// A room member profile change. + ProfileChange(MemberProfileChange), + + /// Another state event. + OtherState(OtherState), + + /// A message-like event that failed to deserialize. + FailedToParseMessageLike { + /// The event `type`. + event_type: MessageLikeEventType, + + /// The deserialization error. + error: Arc, + }, + + /// A state event that failed to deserialize. + FailedToParseState { + /// The event `type`. + event_type: StateEventType, + + /// The state key. + state_key: String, + + /// The deserialization error. + error: Arc, + }, +} + +impl TimelineItemContent { + /// If `self` is of the [`Message`][Self::Message] variant, return the inner + /// [`Message`]. + pub fn as_message(&self) -> Option<&Message> { + match self { + Self::Message(v) => Some(v), + _ => None, + } + } + + /// If `self` is of the [`UnableToDecrypt`][Self::UnableToDecrypt] variant, + /// return the inner [`EncryptedMessage`]. + pub fn as_unable_to_decrypt(&self) -> Option<&EncryptedMessage> { + match self { + Self::UnableToDecrypt(v) => Some(v), + _ => None, + } + } +} + +/// An `m.room.message` event or extensible event, including edits. +#[derive(Clone)] +pub struct Message { + pub(in crate::room::timeline) msgtype: MessageType, + pub(in crate::room::timeline) in_reply_to: Option, + pub(in crate::room::timeline) edited: bool, +} + +impl Message { + /// Get the `msgtype`-specific data of this message. + pub fn msgtype(&self) -> &MessageType { + &self.msgtype + } + + /// Get a reference to the message body. + /// + /// Shorthand for `.msgtype().body()`. + pub fn body(&self) -> &str { + self.msgtype.body() + } + + /// Get the event this message is replying to, if any. + pub fn in_reply_to(&self) -> Option<&InReplyToDetails> { + self.in_reply_to.as_ref() + } + + /// Get the edit state of this message (has been edited: `true` / `false`). + pub fn is_edited(&self) -> bool { + self.edited + } + + pub(in crate::room::timeline) fn with_in_reply_to( + &self, + in_reply_to: InReplyToDetails, + ) -> Self { + Self { in_reply_to: Some(in_reply_to), ..self.clone() } + } +} + +#[cfg(not(tarpaulin_include))] +impl fmt::Debug for Message { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // since timeline items are logged, don't include all fields here so + // people don't leak personal data in bug reports + f.debug_struct("Message").field("edited", &self.edited).finish_non_exhaustive() + } +} + +/// Details about an event being replied to. +#[derive(Clone, Debug)] +pub struct InReplyToDetails { + /// The ID of the event. + pub event_id: OwnedEventId, + + /// The details of the event. + /// + /// Use [`Timeline::fetch_item_details`] to fetch the data if it is + /// unavailable. The `replies_nesting_level` field in + /// [`TimelineDetailsSettings`] decides if this should be fetched. + /// + /// [`Timeline::fetch_item_details`]: super::Timeline::fetch_item_details + /// [`TimelineDetailsSettings`]: super::TimelineDetailsSettings + pub details: TimelineDetails>, +} + +impl InReplyToDetails { + pub(in crate::room::timeline) fn from_relation(relation: Relation) -> Option { + match relation { + message::Relation::Reply { in_reply_to } => { + Some(Self { event_id: in_reply_to.event_id, details: TimelineDetails::Unavailable }) + } + _ => None, + } + } +} + +/// An event that is replied to. +#[derive(Clone, Debug)] +pub struct RepliedToEvent { + pub(in crate::room::timeline) message: Message, + pub(in crate::room::timeline) sender: OwnedUserId, + pub(in crate::room::timeline) sender_profile: TimelineDetails, +} + +impl RepliedToEvent { + /// Get the message of this event. + pub fn message(&self) -> &Message { + &self.message + } + + /// Get the sender of this event. + pub fn sender(&self) -> &UserId { + &self.sender + } + + /// Get the profile of the sender. + pub fn sender_profile(&self) -> &TimelineDetails { + &self.sender_profile + } + + pub(in crate::room::timeline) async fn try_from_timeline_event( + timeline_event: TimelineEvent, + room_data_provider: &P, + ) -> Result { + let event = match timeline_event.event.deserialize() { + Ok(AnyTimelineEvent::MessageLike(event)) => event, + _ => { + return Err(TimelineError::UnsupportedEvent.into()); + } + }; + + let Some(AnyMessageLikeEventContent::RoomMessage(c)) = event.original_content() else { + return Err(TimelineError::UnsupportedEvent.into()); + }; + + let message = Message { + msgtype: c.msgtype, + in_reply_to: c.relates_to.and_then(InReplyToDetails::from_relation), + edited: event.relations().replace.is_some(), + }; + let sender = event.sender().to_owned(); + let sender_profile = + TimelineDetails::from_initial_value(room_data_provider.profile(&sender).await); + + Ok(Self { message, sender, sender_profile }) + } +} + +/// Metadata about an `m.room.encrypted` event that could not be decrypted. +#[derive(Clone, Debug)] +pub enum EncryptedMessage { + /// Metadata about an event using the `m.olm.v1.curve25519-aes-sha2` + /// algorithm. + OlmV1Curve25519AesSha2 { + /// The Curve25519 key of the sender. + sender_key: String, + }, + /// Metadata about an event using the `m.megolm.v1.aes-sha2` algorithm. + MegolmV1AesSha2 { + /// The Curve25519 key of the sender. + #[deprecated = "this field still needs to be sent but should not be used when received"] + #[doc(hidden)] // Included for Debug formatting only + sender_key: String, + + /// The ID of the sending device. + #[deprecated = "this field still needs to be sent but should not be used when received"] + #[doc(hidden)] // Included for Debug formatting only + device_id: OwnedDeviceId, + + /// The ID of the session used to encrypt the message. + session_id: String, + }, + /// No metadata because the event uses an unknown algorithm. + Unknown, +} + +impl From for EncryptedMessage { + fn from(c: RoomEncryptedEventContent) -> Self { + match c.scheme { + EncryptedEventScheme::OlmV1Curve25519AesSha2(s) => { + Self::OlmV1Curve25519AesSha2 { sender_key: s.sender_key } + } + #[allow(deprecated)] + EncryptedEventScheme::MegolmV1AesSha2(s) => { + let MegolmV1AesSha2Content { sender_key, device_id, session_id, .. } = s; + Self::MegolmV1AesSha2 { sender_key, device_id, session_id } + } + _ => Self::Unknown, + } + } +} + +/// The reactions grouped by key. +/// +/// Key: The reaction, usually an emoji.\ +/// Value: The group of reactions. +pub type BundledReactions = IndexMap; + +/// A group of reaction events on the same event with the same key. +/// +/// This is a map of the event ID or transaction ID of the reactions to the ID +/// of the sender of the reaction. +#[derive(Clone, Debug, Default)] +pub struct ReactionGroup( + pub(in crate::room::timeline) IndexMap<(Option, Option), OwnedUserId>, +); + +impl ReactionGroup { + /// The senders of the reactions in this group. + pub fn senders(&self) -> impl Iterator { + self.values().map(AsRef::as_ref) + } +} + +impl Deref for ReactionGroup { + type Target = IndexMap<(Option, Option), OwnedUserId>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// An `m.sticker` event. +#[derive(Clone, Debug)] +pub struct Sticker { + pub(in crate::room::timeline) content: StickerEventContent, +} + +impl Sticker { + /// Get the data of this sticker. + pub fn content(&self) -> &StickerEventContent { + &self.content + } +} + +/// An event changing a room membership. +#[derive(Clone, Debug)] +pub struct RoomMembershipChange { + pub(in crate::room::timeline) user_id: OwnedUserId, + pub(in crate::room::timeline) content: FullStateEventContent, + pub(in crate::room::timeline) change: Option, +} + +impl RoomMembershipChange { + /// The ID of the user whose membership changed. + pub fn user_id(&self) -> &UserId { + &self.user_id + } + + /// The full content of the event. + pub fn content(&self) -> &FullStateEventContent { + &self.content + } + + /// The membership change induced by this event. + /// + /// If this returns `None`, it doesn't mean that there was no change, but + /// that the change could not be computed. This is currently always the case + /// with redacted events. + // FIXME: Fetch the prev_content when missing so we can compute this with + // redacted events? + pub fn change(&self) -> Option { + self.change + } +} + +/// An enum over all the possible room membership changes. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MembershipChange { + /// No change. + None, + + /// Must never happen. + Error, + + /// User joined the room. + Joined, + + /// User left the room. + Left, + + /// User was banned. + Banned, + + /// User was unbanned. + Unbanned, + + /// User was kicked. + Kicked, + + /// User was invited. + Invited, + + /// User was kicked and banned. + KickedAndBanned, + + /// User accepted the invite. + InvitationAccepted, + + /// User rejected the invite. + InvitationRejected, + + /// User had their invite revoked. + InvitationRevoked, + + /// User knocked. + Knocked, + + /// User had their knock accepted. + KnockAccepted, + + /// User retracted their knock. + KnockRetracted, + + /// User had their knock denied. + KnockDenied, + + /// Not implemented. + NotImplemented, +} + +/// An event changing a member's profile. +/// +/// Note that profile changes only occur in the timeline when the user's +/// membership is already `join`. +#[derive(Clone, Debug)] +pub struct MemberProfileChange { + pub(in crate::room::timeline) user_id: OwnedUserId, + pub(in crate::room::timeline) displayname_change: Option>>, + pub(in crate::room::timeline) avatar_url_change: Option>>, +} + +impl MemberProfileChange { + /// The ID of the user whose profile changed. + pub fn user_id(&self) -> &UserId { + &self.user_id + } + + /// The display name change induced by this event. + pub fn displayname_change(&self) -> Option<&Change>> { + self.displayname_change.as_ref() + } + + /// The avatar URL change induced by this event. + pub fn avatar_url_change(&self) -> Option<&Change>> { + self.avatar_url_change.as_ref() + } +} + +/// An enum over all the full state event contents that don't have their own +/// `TimelineItemContent` variant. +#[derive(Clone, Debug)] +pub enum AnyOtherFullStateEventContent { + /// m.policy.rule.room + PolicyRuleRoom(FullStateEventContent), + + /// m.policy.rule.server + PolicyRuleServer(FullStateEventContent), + + /// m.policy.rule.user + PolicyRuleUser(FullStateEventContent), + + /// m.room.aliases + RoomAliases(FullStateEventContent), + + /// m.room.avatar + RoomAvatar(FullStateEventContent), + + /// m.room.canonical_alias + RoomCanonicalAlias(FullStateEventContent), + + /// m.room.create + RoomCreate(FullStateEventContent), + + /// m.room.encryption + RoomEncryption(FullStateEventContent), + + /// m.room.guest_access + RoomGuestAccess(FullStateEventContent), + + /// m.room.history_visibility + RoomHistoryVisibility(FullStateEventContent), + + /// m.room.join_rules + RoomJoinRules(FullStateEventContent), + + /// m.room.name + RoomName(FullStateEventContent), + + /// m.room.pinned_events + RoomPinnedEvents(FullStateEventContent), + + /// m.room.power_levels + RoomPowerLevels(FullStateEventContent), + + /// m.room.server_acl + RoomServerAcl(FullStateEventContent), + + /// m.room.third_party_invite + RoomThirdPartyInvite(FullStateEventContent), + + /// m.room.tombstone + RoomTombstone(FullStateEventContent), + + /// m.room.topic + RoomTopic(FullStateEventContent), + + /// m.space.child + SpaceChild(FullStateEventContent), + + /// m.space.parent + SpaceParent(FullStateEventContent), + + #[doc(hidden)] + _Custom { event_type: String }, +} + +impl AnyOtherFullStateEventContent { + /// Create an `AnyOtherFullStateEventContent` from an + /// `AnyFullStateEventContent`. + /// + /// Panics if the event content does not match one of the variants. + // This could be a `From` implementation but we don't want it in the public API. + pub(crate) fn with_event_content(content: AnyFullStateEventContent) -> Self { + let event_type = content.event_type(); + + match content { + AnyFullStateEventContent::PolicyRuleRoom(c) => Self::PolicyRuleRoom(c), + AnyFullStateEventContent::PolicyRuleServer(c) => Self::PolicyRuleServer(c), + AnyFullStateEventContent::PolicyRuleUser(c) => Self::PolicyRuleUser(c), + AnyFullStateEventContent::RoomAliases(c) => Self::RoomAliases(c), + AnyFullStateEventContent::RoomAvatar(c) => Self::RoomAvatar(c), + AnyFullStateEventContent::RoomCanonicalAlias(c) => Self::RoomCanonicalAlias(c), + AnyFullStateEventContent::RoomCreate(c) => Self::RoomCreate(c), + AnyFullStateEventContent::RoomEncryption(c) => Self::RoomEncryption(c), + AnyFullStateEventContent::RoomGuestAccess(c) => Self::RoomGuestAccess(c), + AnyFullStateEventContent::RoomHistoryVisibility(c) => Self::RoomHistoryVisibility(c), + AnyFullStateEventContent::RoomJoinRules(c) => Self::RoomJoinRules(c), + AnyFullStateEventContent::RoomName(c) => Self::RoomName(c), + AnyFullStateEventContent::RoomPinnedEvents(c) => Self::RoomPinnedEvents(c), + AnyFullStateEventContent::RoomPowerLevels(c) => Self::RoomPowerLevels(c), + AnyFullStateEventContent::RoomServerAcl(c) => Self::RoomServerAcl(c), + AnyFullStateEventContent::RoomThirdPartyInvite(c) => Self::RoomThirdPartyInvite(c), + AnyFullStateEventContent::RoomTombstone(c) => Self::RoomTombstone(c), + AnyFullStateEventContent::RoomTopic(c) => Self::RoomTopic(c), + AnyFullStateEventContent::SpaceChild(c) => Self::SpaceChild(c), + AnyFullStateEventContent::SpaceParent(c) => Self::SpaceParent(c), + AnyFullStateEventContent::RoomMember(_) => unreachable!(), + _ => Self::_Custom { event_type: event_type.to_string() }, + } + } + + /// Get the event's type, like `m.room.create`. + pub fn event_type(&self) -> StateEventType { + match self { + Self::PolicyRuleRoom(c) => c.event_type(), + Self::PolicyRuleServer(c) => c.event_type(), + Self::PolicyRuleUser(c) => c.event_type(), + Self::RoomAliases(c) => c.event_type(), + Self::RoomAvatar(c) => c.event_type(), + Self::RoomCanonicalAlias(c) => c.event_type(), + Self::RoomCreate(c) => c.event_type(), + Self::RoomEncryption(c) => c.event_type(), + Self::RoomGuestAccess(c) => c.event_type(), + Self::RoomHistoryVisibility(c) => c.event_type(), + Self::RoomJoinRules(c) => c.event_type(), + Self::RoomName(c) => c.event_type(), + Self::RoomPinnedEvents(c) => c.event_type(), + Self::RoomPowerLevels(c) => c.event_type(), + Self::RoomServerAcl(c) => c.event_type(), + Self::RoomThirdPartyInvite(c) => c.event_type(), + Self::RoomTombstone(c) => c.event_type(), + Self::RoomTopic(c) => c.event_type(), + Self::SpaceChild(c) => c.event_type(), + Self::SpaceParent(c) => c.event_type(), + Self::_Custom { event_type } => event_type.as_str().into(), + } + } +} + +/// A state event that doesn't have its own variant. +#[derive(Clone, Debug)] +pub struct OtherState { + pub(in crate::room::timeline) state_key: String, + pub(in crate::room::timeline) content: AnyOtherFullStateEventContent, +} + +impl OtherState { + /// The state key of the event. + pub fn state_key(&self) -> &str { + &self.state_key + } + + /// The content of the event. + pub fn content(&self) -> &AnyOtherFullStateEventContent { + &self.content + } +} diff --git a/crates/matrix-sdk/src/room/timeline/event_item/mod.rs b/crates/matrix-sdk/src/room/timeline/event_item/mod.rs index 9a85cd2a6..508eda266 100644 --- a/crates/matrix-sdk/src/room/timeline/event_item/mod.rs +++ b/crates/matrix-sdk/src/room/timeline/event_item/mod.rs @@ -12,53 +12,29 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{fmt, ops::Deref, sync::Arc}; +use std::sync::Arc; -use indexmap::IndexMap; -use matrix_sdk_base::deserialized_responses::TimelineEvent; use ruma::{ - events::{ - policy::rule::{ - room::PolicyRuleRoomEventContent, server::PolicyRuleServerEventContent, - user::PolicyRuleUserEventContent, - }, - room::{ - aliases::RoomAliasesEventContent, - avatar::RoomAvatarEventContent, - canonical_alias::RoomCanonicalAliasEventContent, - create::RoomCreateEventContent, - encrypted::{EncryptedEventScheme, MegolmV1AesSha2Content, RoomEncryptedEventContent}, - encryption::RoomEncryptionEventContent, - guest_access::RoomGuestAccessEventContent, - history_visibility::RoomHistoryVisibilityEventContent, - join_rules::RoomJoinRulesEventContent, - member::{Change, RoomMemberEventContent}, - message::{self, MessageType, Relation}, - name::RoomNameEventContent, - pinned_events::RoomPinnedEventsEventContent, - power_levels::RoomPowerLevelsEventContent, - server_acl::RoomServerAclEventContent, - third_party_invite::RoomThirdPartyInviteEventContent, - tombstone::RoomTombstoneEventContent, - topic::RoomTopicEventContent, - }, - space::{child::SpaceChildEventContent, parent::SpaceParentEventContent}, - sticker::StickerEventContent, - AnyFullStateEventContent, AnyMessageLikeEventContent, AnySyncTimelineEvent, - AnyTimelineEvent, FullStateEventContent, MessageLikeEventType, StateEventType, - }, + events::{room::message::MessageType, AnySyncTimelineEvent}, serde::Raw, - EventId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedEventId, OwnedMxcUri, - OwnedTransactionId, OwnedUserId, TransactionId, UserId, + EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, TransactionId, UserId, }; -use super::inner::RoomDataProvider; -use crate::{Error, Result}; +use crate::Error; +mod content; mod local; mod remote; -pub use self::{local::LocalEventTimelineItem, remote::RemoteEventTimelineItem}; +pub use self::{ + content::{ + AnyOtherFullStateEventContent, BundledReactions, EncryptedMessage, InReplyToDetails, + MemberProfileChange, MembershipChange, Message, OtherState, ReactionGroup, RepliedToEvent, + RoomMembershipChange, Sticker, TimelineItemContent, + }, + local::LocalEventTimelineItem, + remote::RemoteEventTimelineItem, +}; /// An item in the timeline that represents at least one event. /// @@ -311,545 +287,3 @@ impl TimelineDetails { matches!(self, Self::Ready(v) if v == value) } } - -/// 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.sticker` event. - Sticker(Sticker), - - /// An `m.room.encrypted` event that could not be decrypted. - UnableToDecrypt(EncryptedMessage), - - /// A room membership change. - MembershipChange(RoomMembershipChange), - - /// A room member profile change. - ProfileChange(MemberProfileChange), - - /// Another state event. - OtherState(OtherState), - - /// A message-like event that failed to deserialize. - FailedToParseMessageLike { - /// The event `type`. - event_type: MessageLikeEventType, - - /// The deserialization error. - error: Arc, - }, - - /// A state event that failed to deserialize. - FailedToParseState { - /// The event `type`. - event_type: StateEventType, - - /// The state key. - state_key: String, - - /// The deserialization error. - error: Arc, - }, -} - -impl TimelineItemContent { - /// If `self` is of the [`Message`][Self::Message] variant, return the inner - /// [`Message`]. - pub fn as_message(&self) -> Option<&Message> { - match self { - Self::Message(v) => Some(v), - _ => None, - } - } - - /// If `self` is of the [`UnableToDecrypt`][Self::UnableToDecrypt] variant, - /// return the inner [`EncryptedMessage`]. - pub fn as_unable_to_decrypt(&self) -> Option<&EncryptedMessage> { - match self { - Self::UnableToDecrypt(v) => Some(v), - _ => None, - } - } -} - -/// An `m.room.message` event or extensible event, including edits. -#[derive(Clone)] -pub struct Message { - pub(super) msgtype: MessageType, - 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 a reference to the message body. - /// - /// Shorthand for `.msgtype().body()`. - pub fn body(&self) -> &str { - self.msgtype.body() - } - - /// Get the event this message is replying to, if any. - pub fn in_reply_to(&self) -> Option<&InReplyToDetails> { - self.in_reply_to.as_ref() - } - - /// Get the edit state of this message (has been edited: `true` / `false`). - pub fn is_edited(&self) -> bool { - self.edited - } - - pub(super) fn with_in_reply_to(&self, in_reply_to: InReplyToDetails) -> Self { - Self { in_reply_to: Some(in_reply_to), ..self.clone() } - } -} - -#[cfg(not(tarpaulin_include))] -impl fmt::Debug for Message { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // since timeline items are logged, don't include all fields here so - // people don't leak personal data in bug reports - f.debug_struct("Message").field("edited", &self.edited).finish_non_exhaustive() - } -} - -/// Details about an event being replied to. -#[derive(Clone, Debug)] -pub struct InReplyToDetails { - /// The ID of the event. - pub event_id: OwnedEventId, - - /// The details of the event. - /// - /// Use [`Timeline::fetch_item_details`] to fetch the data if it is - /// unavailable. The `replies_nesting_level` field in - /// [`TimelineDetailsSettings`] decides if this should be fetched. - /// - /// [`Timeline::fetch_item_details`]: super::Timeline::fetch_item_details - /// [`TimelineDetailsSettings`]: super::TimelineDetailsSettings - pub details: TimelineDetails>, -} - -impl InReplyToDetails { - pub(super) fn from_relation(relation: Relation) -> Option { - match relation { - message::Relation::Reply { in_reply_to } => { - Some(Self { event_id: in_reply_to.event_id, details: TimelineDetails::Unavailable }) - } - _ => None, - } - } -} - -/// An event that is replied to. -#[derive(Clone, Debug)] -pub struct RepliedToEvent { - pub(super) message: Message, - pub(super) sender: OwnedUserId, - pub(super) sender_profile: TimelineDetails, -} - -impl RepliedToEvent { - /// Get the message of this event. - pub fn message(&self) -> &Message { - &self.message - } - - /// Get the sender of this event. - pub fn sender(&self) -> &UserId { - &self.sender - } - - /// Get the profile of the sender. - pub fn sender_profile(&self) -> &TimelineDetails { - &self.sender_profile - } - - pub(super) async fn try_from_timeline_event( - timeline_event: TimelineEvent, - room_data_provider: &P, - ) -> Result { - let event = match timeline_event.event.deserialize() { - Ok(AnyTimelineEvent::MessageLike(event)) => event, - _ => { - return Err(super::Error::UnsupportedEvent.into()); - } - }; - - let Some(AnyMessageLikeEventContent::RoomMessage(c)) = event.original_content() else { - return Err(super::Error::UnsupportedEvent.into()); - }; - - let message = Message { - msgtype: c.msgtype, - in_reply_to: c.relates_to.and_then(InReplyToDetails::from_relation), - edited: event.relations().replace.is_some(), - }; - let sender = event.sender().to_owned(); - let sender_profile = - TimelineDetails::from_initial_value(room_data_provider.profile(&sender).await); - - Ok(Self { message, sender, sender_profile }) - } -} - -/// Metadata about an `m.room.encrypted` event that could not be decrypted. -#[derive(Clone, Debug)] -pub enum EncryptedMessage { - /// Metadata about an event using the `m.olm.v1.curve25519-aes-sha2` - /// algorithm. - OlmV1Curve25519AesSha2 { - /// The Curve25519 key of the sender. - sender_key: String, - }, - /// Metadata about an event using the `m.megolm.v1.aes-sha2` algorithm. - MegolmV1AesSha2 { - /// The Curve25519 key of the sender. - #[deprecated = "this field still needs to be sent but should not be used when received"] - #[doc(hidden)] // Included for Debug formatting only - sender_key: String, - - /// The ID of the sending device. - #[deprecated = "this field still needs to be sent but should not be used when received"] - #[doc(hidden)] // Included for Debug formatting only - device_id: OwnedDeviceId, - - /// The ID of the session used to encrypt the message. - session_id: String, - }, - /// No metadata because the event uses an unknown algorithm. - Unknown, -} - -impl From for EncryptedMessage { - fn from(c: RoomEncryptedEventContent) -> Self { - match c.scheme { - EncryptedEventScheme::OlmV1Curve25519AesSha2(s) => { - Self::OlmV1Curve25519AesSha2 { sender_key: s.sender_key } - } - #[allow(deprecated)] - EncryptedEventScheme::MegolmV1AesSha2(s) => { - let MegolmV1AesSha2Content { sender_key, device_id, session_id, .. } = s; - Self::MegolmV1AesSha2 { sender_key, device_id, session_id } - } - _ => Self::Unknown, - } - } -} - -/// The reactions grouped by key. -/// -/// Key: The reaction, usually an emoji.\ -/// Value: The group of reactions. -pub type BundledReactions = IndexMap; - -/// A group of reaction events on the same event with the same key. -/// -/// This is a map of the event ID or transaction ID of the reactions to the ID -/// of the sender of the reaction. -#[derive(Clone, Debug, Default)] -pub struct ReactionGroup( - pub(super) IndexMap<(Option, Option), OwnedUserId>, -); - -impl ReactionGroup { - /// The senders of the reactions in this group. - pub fn senders(&self) -> impl Iterator { - self.values().map(AsRef::as_ref) - } -} - -impl Deref for ReactionGroup { - type Target = IndexMap<(Option, Option), OwnedUserId>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -/// An `m.sticker` event. -#[derive(Clone, Debug)] -pub struct Sticker { - pub(super) content: StickerEventContent, -} - -impl Sticker { - /// Get the data of this sticker. - pub fn content(&self) -> &StickerEventContent { - &self.content - } -} - -/// An event changing a room membership. -#[derive(Clone, Debug)] -pub struct RoomMembershipChange { - pub(super) user_id: OwnedUserId, - pub(super) content: FullStateEventContent, - pub(super) change: Option, -} - -impl RoomMembershipChange { - /// The ID of the user whose membership changed. - pub fn user_id(&self) -> &UserId { - &self.user_id - } - - /// The full content of the event. - pub fn content(&self) -> &FullStateEventContent { - &self.content - } - - /// The membership change induced by this event. - /// - /// If this returns `None`, it doesn't mean that there was no change, but - /// that the change could not be computed. This is currently always the case - /// with redacted events. - // FIXME: Fetch the prev_content when missing so we can compute this with - // redacted events? - pub fn change(&self) -> Option { - self.change - } -} - -/// An enum over all the possible room membership changes. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum MembershipChange { - /// No change. - None, - - /// Must never happen. - Error, - - /// User joined the room. - Joined, - - /// User left the room. - Left, - - /// User was banned. - Banned, - - /// User was unbanned. - Unbanned, - - /// User was kicked. - Kicked, - - /// User was invited. - Invited, - - /// User was kicked and banned. - KickedAndBanned, - - /// User accepted the invite. - InvitationAccepted, - - /// User rejected the invite. - InvitationRejected, - - /// User had their invite revoked. - InvitationRevoked, - - /// User knocked. - Knocked, - - /// User had their knock accepted. - KnockAccepted, - - /// User retracted their knock. - KnockRetracted, - - /// User had their knock denied. - KnockDenied, - - /// Not implemented. - NotImplemented, -} - -/// An event changing a member's profile. -/// -/// Note that profile changes only occur in the timeline when the user's -/// membership is already `join`. -#[derive(Clone, Debug)] -pub struct MemberProfileChange { - pub(super) user_id: OwnedUserId, - pub(super) displayname_change: Option>>, - pub(super) avatar_url_change: Option>>, -} - -impl MemberProfileChange { - /// The ID of the user whose profile changed. - pub fn user_id(&self) -> &UserId { - &self.user_id - } - - /// The display name change induced by this event. - pub fn displayname_change(&self) -> Option<&Change>> { - self.displayname_change.as_ref() - } - - /// The avatar URL change induced by this event. - pub fn avatar_url_change(&self) -> Option<&Change>> { - self.avatar_url_change.as_ref() - } -} - -/// An enum over all the full state event contents that don't have their own -/// `TimelineItemContent` variant. -#[derive(Clone, Debug)] -pub enum AnyOtherFullStateEventContent { - /// m.policy.rule.room - PolicyRuleRoom(FullStateEventContent), - - /// m.policy.rule.server - PolicyRuleServer(FullStateEventContent), - - /// m.policy.rule.user - PolicyRuleUser(FullStateEventContent), - - /// m.room.aliases - RoomAliases(FullStateEventContent), - - /// m.room.avatar - RoomAvatar(FullStateEventContent), - - /// m.room.canonical_alias - RoomCanonicalAlias(FullStateEventContent), - - /// m.room.create - RoomCreate(FullStateEventContent), - - /// m.room.encryption - RoomEncryption(FullStateEventContent), - - /// m.room.guest_access - RoomGuestAccess(FullStateEventContent), - - /// m.room.history_visibility - RoomHistoryVisibility(FullStateEventContent), - - /// m.room.join_rules - RoomJoinRules(FullStateEventContent), - - /// m.room.name - RoomName(FullStateEventContent), - - /// m.room.pinned_events - RoomPinnedEvents(FullStateEventContent), - - /// m.room.power_levels - RoomPowerLevels(FullStateEventContent), - - /// m.room.server_acl - RoomServerAcl(FullStateEventContent), - - /// m.room.third_party_invite - RoomThirdPartyInvite(FullStateEventContent), - - /// m.room.tombstone - RoomTombstone(FullStateEventContent), - - /// m.room.topic - RoomTopic(FullStateEventContent), - - /// m.space.child - SpaceChild(FullStateEventContent), - - /// m.space.parent - SpaceParent(FullStateEventContent), - - #[doc(hidden)] - _Custom { event_type: String }, -} - -impl AnyOtherFullStateEventContent { - /// Create an `AnyOtherFullStateEventContent` from an - /// `AnyFullStateEventContent`. - /// - /// Panics if the event content does not match one of the variants. - // This could be a `From` implementation but we don't want it in the public API. - pub(crate) fn with_event_content(content: AnyFullStateEventContent) -> Self { - let event_type = content.event_type(); - - match content { - AnyFullStateEventContent::PolicyRuleRoom(c) => Self::PolicyRuleRoom(c), - AnyFullStateEventContent::PolicyRuleServer(c) => Self::PolicyRuleServer(c), - AnyFullStateEventContent::PolicyRuleUser(c) => Self::PolicyRuleUser(c), - AnyFullStateEventContent::RoomAliases(c) => Self::RoomAliases(c), - AnyFullStateEventContent::RoomAvatar(c) => Self::RoomAvatar(c), - AnyFullStateEventContent::RoomCanonicalAlias(c) => Self::RoomCanonicalAlias(c), - AnyFullStateEventContent::RoomCreate(c) => Self::RoomCreate(c), - AnyFullStateEventContent::RoomEncryption(c) => Self::RoomEncryption(c), - AnyFullStateEventContent::RoomGuestAccess(c) => Self::RoomGuestAccess(c), - AnyFullStateEventContent::RoomHistoryVisibility(c) => Self::RoomHistoryVisibility(c), - AnyFullStateEventContent::RoomJoinRules(c) => Self::RoomJoinRules(c), - AnyFullStateEventContent::RoomName(c) => Self::RoomName(c), - AnyFullStateEventContent::RoomPinnedEvents(c) => Self::RoomPinnedEvents(c), - AnyFullStateEventContent::RoomPowerLevels(c) => Self::RoomPowerLevels(c), - AnyFullStateEventContent::RoomServerAcl(c) => Self::RoomServerAcl(c), - AnyFullStateEventContent::RoomThirdPartyInvite(c) => Self::RoomThirdPartyInvite(c), - AnyFullStateEventContent::RoomTombstone(c) => Self::RoomTombstone(c), - AnyFullStateEventContent::RoomTopic(c) => Self::RoomTopic(c), - AnyFullStateEventContent::SpaceChild(c) => Self::SpaceChild(c), - AnyFullStateEventContent::SpaceParent(c) => Self::SpaceParent(c), - AnyFullStateEventContent::RoomMember(_) => unreachable!(), - _ => Self::_Custom { event_type: event_type.to_string() }, - } - } - - /// Get the event's type, like `m.room.create`. - pub fn event_type(&self) -> StateEventType { - match self { - Self::PolicyRuleRoom(c) => c.event_type(), - Self::PolicyRuleServer(c) => c.event_type(), - Self::PolicyRuleUser(c) => c.event_type(), - Self::RoomAliases(c) => c.event_type(), - Self::RoomAvatar(c) => c.event_type(), - Self::RoomCanonicalAlias(c) => c.event_type(), - Self::RoomCreate(c) => c.event_type(), - Self::RoomEncryption(c) => c.event_type(), - Self::RoomGuestAccess(c) => c.event_type(), - Self::RoomHistoryVisibility(c) => c.event_type(), - Self::RoomJoinRules(c) => c.event_type(), - Self::RoomName(c) => c.event_type(), - Self::RoomPinnedEvents(c) => c.event_type(), - Self::RoomPowerLevels(c) => c.event_type(), - Self::RoomServerAcl(c) => c.event_type(), - Self::RoomThirdPartyInvite(c) => c.event_type(), - Self::RoomTombstone(c) => c.event_type(), - Self::RoomTopic(c) => c.event_type(), - Self::SpaceChild(c) => c.event_type(), - Self::SpaceParent(c) => c.event_type(), - Self::_Custom { event_type } => event_type.as_str().into(), - } - } -} - -/// A state event that doesn't have its own variant. -#[derive(Clone, Debug)] -pub struct OtherState { - pub(super) state_key: String, - pub(super) content: AnyOtherFullStateEventContent, -} - -impl OtherState { - /// The state key of the event. - pub fn state_key(&self) -> &str { - &self.state_key - } - - /// The content of the event. - pub fn content(&self) -> &AnyOtherFullStateEventContent { - &self.content - } -} From 400879c819a8bf6f3a3dcc01fe3d037710f0386d Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 1 Mar 2023 14:47:07 +0100 Subject: [PATCH 099/166] Make LocalEventTimelineItem fields private, provide methods instead --- bindings/matrix-sdk-ffi/src/timeline.rs | 2 +- .../src/room/timeline/event_handler.rs | 18 +++-- .../src/room/timeline/event_item/local.rs | 76 +++++++++++++++++-- .../src/room/timeline/event_item/mod.rs | 28 +++---- crates/matrix-sdk/src/room/timeline/inner.rs | 2 +- .../src/room/timeline/tests/echo.rs | 10 +-- .../tests/integration/room/timeline.rs | 6 +- 7 files changed, 100 insertions(+), 42 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline.rs b/bindings/matrix-sdk-ffi/src/timeline.rs index fbb840198..f9598cea3 100644 --- a/bindings/matrix-sdk-ffi/src/timeline.rs +++ b/bindings/matrix-sdk-ffi/src/timeline.rs @@ -312,7 +312,7 @@ impl EventTimelineItem { use matrix_sdk::room::timeline::EventTimelineItem::*; match &self.0 { - Local(local_event) => Some((&local_event.send_state).into()), + Local(local_event) => Some(local_event.send_state().into()), Remote(_) => None, } } diff --git a/crates/matrix-sdk/src/room/timeline/event_handler.rs b/crates/matrix-sdk/src/room/timeline/event_handler.rs index 6e3642cb7..027a35592 100644 --- a/crates/matrix-sdk/src/room/timeline/event_handler.rs +++ b/crates/matrix-sdk/src/room/timeline/event_handler.rs @@ -543,14 +543,16 @@ impl<'a> TimelineEventHandler<'a> { let mut reactions = self.pending_reactions().unwrap_or_default(); let mut item = match &self.flow { - Flow::Local { txn_id, timestamp } => EventTimelineItem::Local(LocalEventTimelineItem { - send_state: EventSendState::NotSentYet, - transaction_id: txn_id.to_owned(), - sender, - sender_profile, - timestamp: *timestamp, - content, - }), + Flow::Local { txn_id, timestamp } => { + EventTimelineItem::Local(LocalEventTimelineItem::new( + EventSendState::NotSentYet, + txn_id.to_owned(), + sender, + sender_profile, + *timestamp, + content, + )) + } Flow::Remote { event_id, origin_server_ts, raw_event, .. } => { // Drop pending reactions if the message is redacted. if let TimelineItemContent::RedactedMessage = content { diff --git a/crates/matrix-sdk/src/room/timeline/event_item/local.rs b/crates/matrix-sdk/src/room/timeline/event_item/local.rs index 5997c1eab..b1a58ae79 100644 --- a/crates/matrix-sdk/src/room/timeline/event_item/local.rs +++ b/crates/matrix-sdk/src/room/timeline/event_item/local.rs @@ -1,4 +1,6 @@ -use ruma::{EventId, MilliSecondsSinceUnixEpoch, OwnedTransactionId, OwnedUserId}; +use ruma::{ + EventId, MilliSecondsSinceUnixEpoch, OwnedTransactionId, OwnedUserId, TransactionId, UserId, +}; use super::{EventSendState, Profile, TimelineDetails, TimelineItemContent}; @@ -7,20 +9,36 @@ use super::{EventSendState, Profile, TimelineDetails, TimelineItemContent}; #[derive(Debug, Clone)] pub struct LocalEventTimelineItem { /// The send state of this local event. - pub send_state: EventSendState, + send_state: EventSendState, /// The transaction ID. - pub transaction_id: OwnedTransactionId, + transaction_id: OwnedTransactionId, /// The sender of the event. - pub sender: OwnedUserId, + sender: OwnedUserId, /// The sender's profile of the event. - pub sender_profile: TimelineDetails, + sender_profile: TimelineDetails, /// The timestamp of the event. - pub timestamp: MilliSecondsSinceUnixEpoch, + timestamp: MilliSecondsSinceUnixEpoch, /// The content of the event. - pub content: TimelineItemContent, + content: TimelineItemContent, } impl LocalEventTimelineItem { + pub(in crate::room::timeline) fn new( + send_state: EventSendState, + transaction_id: OwnedTransactionId, + sender: OwnedUserId, + sender_profile: TimelineDetails, + timestamp: MilliSecondsSinceUnixEpoch, + content: TimelineItemContent, + ) -> Self { + Self { send_state, transaction_id, sender, sender_profile, timestamp, content } + } + + /// Get the event's send state. + pub fn send_state(&self) -> &EventSendState { + &self.send_state + } + /// Get the event ID of this item. /// /// Will be `Some` if and only if `send_state` is @@ -32,8 +50,52 @@ impl LocalEventTimelineItem { } } + /// Get the transaction ID of the event. + pub fn transaction_id(&self) -> &TransactionId { + &self.transaction_id + } + + /// Get the sender of the event. + /// + /// This is always the user's own user ID. + pub(crate) fn sender(&self) -> &UserId { + &self.sender + } + + /// Get the profile of the event's sender. + /// + /// Since `LocalEventTimelineItem`s are always sent by the user that is + /// logged in with the client that created the timeline, this effectively + /// gives the sender's own (possibly room-specific) profile. + pub fn sender_profile(&self) -> &TimelineDetails { + &self.sender_profile + } + + /// Get the timestamp when the event was created locally. + pub fn timestamp(&self) -> MilliSecondsSinceUnixEpoch { + self.timestamp + } + + /// Get the content of the event. + pub fn content(&self) -> &TimelineItemContent { + &self.content + } + /// Clone the current event item, and update its `send_state`. pub(in crate::room::timeline) fn with_send_state(&self, send_state: EventSendState) -> Self { Self { send_state, ..self.clone() } } + + /// Clone the current event item, and update its `sender_profile`. + pub(in crate::room::timeline) fn with_sender_profile( + &self, + sender_profile: TimelineDetails, + ) -> Self { + Self { sender_profile, ..self.clone() } + } + + /// Clone the current event item, and update its `content`. + pub(in crate::room::timeline) fn with_content(&self, content: TimelineItemContent) -> Self { + Self { content, ..self.clone() } + } } diff --git a/crates/matrix-sdk/src/room/timeline/event_item/mod.rs b/crates/matrix-sdk/src/room/timeline/event_item/mod.rs index 508eda266..c099b8e3b 100644 --- a/crates/matrix-sdk/src/room/timeline/event_item/mod.rs +++ b/crates/matrix-sdk/src/room/timeline/event_item/mod.rs @@ -72,12 +72,10 @@ impl EventTimelineItem { /// case of a remote event. pub fn unique_identifier(&self) -> String { match self { - Self::Local(LocalEventTimelineItem { transaction_id, send_state, .. }) => { - match send_state { - EventSendState::Sent { event_id } => event_id.to_string(), - _ => transaction_id.to_string(), - } - } + Self::Local(item) => match item.send_state() { + EventSendState::Sent { event_id } => event_id.to_string(), + _ => item.transaction_id().to_string(), + }, Self::Remote(RemoteEventTimelineItem { event_id, .. }) => event_id.to_string(), } @@ -91,7 +89,7 @@ impl EventTimelineItem { /// discarded. pub fn transaction_id(&self) -> Option<&TransactionId> { match self { - Self::Local(local) => Some(&local.transaction_id), + Self::Local(local) => Some(local.transaction_id()), Self::Remote(_) => None, } } @@ -115,7 +113,7 @@ impl EventTimelineItem { /// Get the sender of this item. pub fn sender(&self) -> &UserId { match self { - Self::Local(local_event) => &local_event.sender, + Self::Local(local_event) => local_event.sender(), Self::Remote(remote_event) => &remote_event.sender, } } @@ -123,7 +121,7 @@ impl EventTimelineItem { /// Get the profile of the sender. pub fn sender_profile(&self) -> &TimelineDetails { match self { - Self::Local(local_event) => &local_event.sender_profile, + Self::Local(local_event) => local_event.sender_profile(), Self::Remote(remote_event) => &remote_event.sender_profile, } } @@ -131,7 +129,7 @@ impl EventTimelineItem { /// Get the content of this item. pub fn content(&self) -> &TimelineItemContent { match self { - Self::Local(local_event) => &local_event.content, + Self::Local(local_event) => local_event.content(), Self::Remote(remote_event) => &remote_event.content, } } @@ -143,7 +141,7 @@ impl EventTimelineItem { /// server timestamp. pub fn timestamp(&self) -> MilliSecondsSinceUnixEpoch { match self { - Self::Local(local_event) => local_event.timestamp, + Self::Local(local_event) => local_event.timestamp(), Self::Remote(remote_event) => remote_event.timestamp, } } @@ -182,9 +180,7 @@ impl EventTimelineItem { /// Clone the current event item, and update its `content`. pub(super) fn with_content(&self, content: TimelineItemContent) -> Self { match self { - Self::Local(local_event_item) => { - Self::Local(LocalEventTimelineItem { content, ..local_event_item.clone() }) - } + Self::Local(local_event_item) => Self::Local(local_event_item.with_content(content)), Self::Remote(remote_event_item) => { Self::Remote(RemoteEventTimelineItem { content, ..remote_event_item.clone() }) } @@ -194,9 +190,7 @@ impl EventTimelineItem { /// Clone the current event item, and update its `sender_profile`. pub(super) fn with_sender_profile(&self, sender_profile: TimelineDetails) -> Self { match self { - EventTimelineItem::Local(item) => { - Self::Local(LocalEventTimelineItem { sender_profile, ..item.clone() }) - } + EventTimelineItem::Local(item) => Self::Local(item.with_sender_profile(sender_profile)), EventTimelineItem::Remote(item) => { Self::Remote(RemoteEventTimelineItem { sender_profile, ..item.clone() }) } diff --git a/crates/matrix-sdk/src/room/timeline/inner.rs b/crates/matrix-sdk/src/room/timeline/inner.rs index 505d064f0..4b5cd300f 100644 --- a/crates/matrix-sdk/src/room/timeline/inner.rs +++ b/crates/matrix-sdk/src/room/timeline/inner.rs @@ -248,7 +248,7 @@ impl TimelineInner

    { // The event was already marked as sent, that's a broken state, let's // emit an error but also override to the given sent state. - if let EventSendState::Sent { event_id: existing_event_id } = &item.send_state { + if let EventSendState::Sent { event_id: existing_event_id } = item.send_state() { let new_event_id = new_event_id.map(debug); error!(?existing_event_id, ?new_event_id, "Local echo already marked as sent"); } diff --git a/crates/matrix-sdk/src/room/timeline/tests/echo.rs b/crates/matrix-sdk/src/room/timeline/tests/echo.rs index fbde67cbe..04afb758c 100644 --- a/crates/matrix-sdk/src/room/timeline/tests/echo.rs +++ b/crates/matrix-sdk/src/room/timeline/tests/echo.rs @@ -36,7 +36,7 @@ async fn remote_echo_full_trip() { let item = assert_matches!(stream.next().await, Some(VectorDiff::PushBack { value }) => value); let event = item.as_event().unwrap().as_local().unwrap(); - assert_matches!(event.send_state, EventSendState::NotSentYet); + assert_matches!(event.send_state(), EventSendState::NotSentYet); } // Scenario 2: The local event has not been sent to the server successfully, it @@ -56,7 +56,7 @@ async fn remote_echo_full_trip() { Some(VectorDiff::Set { value, index: 1 }) => value ); let event = item.as_event().unwrap().as_local().unwrap(); - assert_matches!(event.send_state, EventSendState::SendingFailed { .. }); + assert_matches!(event.send_state(), EventSendState::SendingFailed { .. }); } // Scenario 3: The local event has been sent successfully to the server and an @@ -76,9 +76,9 @@ async fn remote_echo_full_trip() { Some(VectorDiff::Set { value, index: 1 }) => value ); let event_item = item.as_event().unwrap().as_local().unwrap(); - assert_matches!(event_item.send_state, EventSendState::Sent { .. }); + assert_matches!(event_item.send_state(), EventSendState::Sent { .. }); - event_item.timestamp + event_item.timestamp() }; // Now, a sync has been run against the server, and an event with the same ID @@ -119,7 +119,7 @@ async fn remote_echo_new_position() { let item = assert_matches!(stream.next().await, Some(VectorDiff::PushBack { value }) => value); let txn_id_from_event = item.as_event().unwrap().as_local().unwrap(); - assert_eq!(txn_id, *txn_id_from_event.transaction_id); + assert_eq!(txn_id, *txn_id_from_event.transaction_id()); // … and another event that comes back before the remote echo timeline.handle_live_message_event(&BOB, RoomMessageEventContent::text_plain("test")).await; diff --git a/crates/matrix-sdk/tests/integration/room/timeline.rs b/crates/matrix-sdk/tests/integration/room/timeline.rs index e446bc711..9fa365abf 100644 --- a/crates/matrix-sdk/tests/integration/room/timeline.rs +++ b/crates/matrix-sdk/tests/integration/room/timeline.rs @@ -189,9 +189,9 @@ async fn echo() { let _day_divider = assert_matches!(timeline_stream.next().await, Some(VectorDiff::PushBack { value }) => value); let local_echo = assert_matches!(timeline_stream.next().await, Some(VectorDiff::PushBack { value }) => value); let item = local_echo.as_event().unwrap().as_local().unwrap(); - assert_matches!(&item.send_state, EventSendState::NotSentYet); + assert_matches!(item.send_state(), EventSendState::NotSentYet); - let msg = assert_matches!(&item.content, TimelineItemContent::Message(msg) => msg); + 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!"); @@ -203,7 +203,7 @@ async fn echo() { Some(VectorDiff::Set { index: 1, value }) => value ); let item = sent_confirmation.as_event().unwrap().as_local().unwrap(); - assert_matches!(&item.send_state, EventSendState::Sent { .. }); + assert_matches!(item.send_state(), EventSendState::Sent { .. }); ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event( TimelineTestEvent::Custom(json!({ From c9e6d3e2dc6acd309068b6d3fc283b81713e5b32 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 1 Mar 2023 16:02:19 +0100 Subject: [PATCH 100/166] Make RemoteEventTimelineItem fields private, provide methods instead --- .../src/room/timeline/event_handler.rs | 22 +-- .../src/room/timeline/event_item/content.rs | 8 +- .../src/room/timeline/event_item/mod.rs | 31 ++-- .../src/room/timeline/event_item/remote.rs | 140 +++++++++++++++--- crates/matrix-sdk/src/room/timeline/inner.rs | 21 ++- .../src/room/timeline/read_receipts.rs | 17 ++- .../src/room/timeline/tests/basic.rs | 4 +- .../src/room/timeline/tests/encryption.rs | 8 +- .../src/room/timeline/tests/invalid.rs | 20 +-- .../src/room/timeline/tests/read_receipts.rs | 22 +-- .../tests/integration/room/timeline.rs | 28 ++-- .../sliding-sync-integration-test/src/lib.rs | 18 +-- 12 files changed, 222 insertions(+), 117 deletions(-) diff --git a/crates/matrix-sdk/src/room/timeline/event_handler.rs b/crates/matrix-sdk/src/room/timeline/event_handler.rs index 027a35592..76722ab48 100644 --- a/crates/matrix-sdk/src/room/timeline/event_handler.rs +++ b/crates/matrix-sdk/src/room/timeline/event_handler.rs @@ -407,11 +407,11 @@ impl<'a> TimelineEventHandler<'a> { // Handling of reactions on redacted events is an open question. // For now, ignore reactions on redacted events like Element does. - if let TimelineItemContent::RedactedMessage = remote_event_item.content { + if let TimelineItemContent::RedactedMessage = remote_event_item.content() { debug!("Ignoring reaction on redacted event"); return; } else { - let mut reactions = remote_event_item.reactions.clone(); + let mut reactions = remote_event_item.reactions().clone(); let reaction_group = reactions.entry(c.relates_to.key.clone()).or_default(); if let Some(txn_id) = old_txn_id { @@ -474,7 +474,7 @@ impl<'a> TimelineEventHandler<'a> { return None; }; - let mut reactions = remote_event_item.reactions.clone(); + let mut reactions = remote_event_item.reactions().clone(); let count = { let Entry::Occupied(mut group_entry) = reactions.entry(rel.key.clone()) else { @@ -561,18 +561,18 @@ impl<'a> TimelineEventHandler<'a> { } } - EventTimelineItem::Remote(RemoteEventTimelineItem { - event_id: event_id.clone(), + EventTimelineItem::Remote(RemoteEventTimelineItem::new( + event_id.clone(), sender, sender_profile, - timestamp: *origin_server_ts, + *origin_server_ts, content, reactions, - is_own: self.meta.is_own_event, - encryption_info: self.meta.encryption_info.clone(), - read_receipts: self.meta.read_receipts.clone(), - raw: raw_event.clone(), - }) + self.meta.read_receipts.clone(), + self.meta.is_own_event, + self.meta.encryption_info.clone(), + raw_event.clone(), + )) } }; diff --git a/crates/matrix-sdk/src/room/timeline/event_item/content.rs b/crates/matrix-sdk/src/room/timeline/event_item/content.rs index 277f5e9a9..6d239b07a 100644 --- a/crates/matrix-sdk/src/room/timeline/event_item/content.rs +++ b/crates/matrix-sdk/src/room/timeline/event_item/content.rs @@ -286,14 +286,16 @@ impl From for EncryptedMessage { /// Value: The group of reactions. pub type BundledReactions = IndexMap; +// The long type after a long visibility specified trips up rustfmt currently. +// This works around. Report: https://github.com/rust-lang/rustfmt/issues/5703 +type ReactionGroupInner = IndexMap<(Option, Option), OwnedUserId>; + /// A group of reaction events on the same event with the same key. /// /// This is a map of the event ID or transaction ID of the reactions to the ID /// of the sender of the reaction. #[derive(Clone, Debug, Default)] -pub struct ReactionGroup( - pub(in crate::room::timeline) IndexMap<(Option, Option), OwnedUserId>, -); +pub struct ReactionGroup(pub(in crate::room::timeline) ReactionGroupInner); impl ReactionGroup { /// The senders of the reactions in this group. diff --git a/crates/matrix-sdk/src/room/timeline/event_item/mod.rs b/crates/matrix-sdk/src/room/timeline/event_item/mod.rs index c099b8e3b..662a71180 100644 --- a/crates/matrix-sdk/src/room/timeline/event_item/mod.rs +++ b/crates/matrix-sdk/src/room/timeline/event_item/mod.rs @@ -76,8 +76,7 @@ impl EventTimelineItem { EventSendState::Sent { event_id } => event_id.to_string(), _ => item.transaction_id().to_string(), }, - - Self::Remote(RemoteEventTimelineItem { event_id, .. }) => event_id.to_string(), + Self::Remote(item) => item.event_id().to_string(), } } @@ -106,7 +105,7 @@ impl EventTimelineItem { pub fn event_id(&self) -> Option<&EventId> { match self { Self::Local(local_event) => local_event.event_id(), - Self::Remote(remote_event) => Some(&remote_event.event_id), + Self::Remote(remote_event) => Some(remote_event.event_id()), } } @@ -114,7 +113,7 @@ impl EventTimelineItem { pub fn sender(&self) -> &UserId { match self { Self::Local(local_event) => local_event.sender(), - Self::Remote(remote_event) => &remote_event.sender, + Self::Remote(remote_event) => remote_event.sender(), } } @@ -122,7 +121,7 @@ impl EventTimelineItem { pub fn sender_profile(&self) -> &TimelineDetails { match self { Self::Local(local_event) => local_event.sender_profile(), - Self::Remote(remote_event) => &remote_event.sender_profile, + Self::Remote(remote_event) => remote_event.sender_profile(), } } @@ -130,7 +129,7 @@ impl EventTimelineItem { pub fn content(&self) -> &TimelineItemContent { match self { Self::Local(local_event) => local_event.content(), - Self::Remote(remote_event) => &remote_event.content, + Self::Remote(remote_event) => remote_event.content(), } } @@ -142,7 +141,7 @@ impl EventTimelineItem { pub fn timestamp(&self) -> MilliSecondsSinceUnixEpoch { match self { Self::Local(local_event) => local_event.timestamp(), - Self::Remote(remote_event) => remote_event.timestamp, + Self::Remote(remote_event) => remote_event.timestamp(), } } @@ -150,7 +149,7 @@ impl EventTimelineItem { pub fn is_own(&self) -> bool { match self { Self::Local(_) => true, - Self::Remote(remote_event) => remote_event.is_own, + Self::Remote(remote_event) => remote_event.is_own(), } } @@ -173,26 +172,26 @@ impl EventTimelineItem { pub fn raw(&self) -> Option<&Raw> { match self { Self::Local(_local_event) => None, - Self::Remote(remote_event) => Some(&remote_event.raw), + Self::Remote(remote_event) => Some(remote_event.raw()), } } /// Clone the current event item, and update its `content`. pub(super) fn with_content(&self, content: TimelineItemContent) -> Self { match self { - Self::Local(local_event_item) => Self::Local(local_event_item.with_content(content)), - Self::Remote(remote_event_item) => { - Self::Remote(RemoteEventTimelineItem { content, ..remote_event_item.clone() }) - } + Self::Local(local_event) => Self::Local(local_event.with_content(content)), + Self::Remote(remote_event) => Self::Remote(remote_event.with_content(content)), } } /// Clone the current event item, and update its `sender_profile`. pub(super) fn with_sender_profile(&self, sender_profile: TimelineDetails) -> Self { match self { - EventTimelineItem::Local(item) => Self::Local(item.with_sender_profile(sender_profile)), - EventTimelineItem::Remote(item) => { - Self::Remote(RemoteEventTimelineItem { sender_profile, ..item.clone() }) + EventTimelineItem::Local(local_event) => { + Self::Local(local_event.with_sender_profile(sender_profile)) + } + EventTimelineItem::Remote(remote_event) => { + Self::Remote(remote_event.with_sender_profile(sender_profile)) } } } diff --git a/crates/matrix-sdk/src/room/timeline/event_item/remote.rs b/crates/matrix-sdk/src/room/timeline/event_item/remote.rs index 99d8424dc..e3feb991e 100644 --- a/crates/matrix-sdk/src/room/timeline/event_item/remote.rs +++ b/crates/matrix-sdk/src/room/timeline/event_item/remote.rs @@ -5,7 +5,7 @@ use matrix_sdk_base::deserialized_responses::EncryptionInfo; use ruma::{ events::{receipt::Receipt, AnySyncTimelineEvent}, serde::Raw, - MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, + EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, UserId, }; use super::{BundledReactions, Profile, TimelineDetails, TimelineItemContent}; @@ -14,33 +14,136 @@ use super::{BundledReactions, Profile, TimelineDetails, TimelineItemContent}; #[derive(Clone)] pub struct RemoteEventTimelineItem { /// The event ID. - pub event_id: OwnedEventId, + event_id: OwnedEventId, /// The sender of the event. - pub sender: OwnedUserId, + sender: OwnedUserId, /// The sender's profile of the event. - pub sender_profile: TimelineDetails, + sender_profile: TimelineDetails, /// The timestamp of the event. - pub timestamp: MilliSecondsSinceUnixEpoch, + timestamp: MilliSecondsSinceUnixEpoch, /// The content of the event. - pub content: TimelineItemContent, + content: TimelineItemContent, /// All bundled reactions about the event. - pub reactions: BundledReactions, + reactions: BundledReactions, /// All read receipts for the event. /// /// The key is the ID of a room member and the value are details about the /// read receipt. /// /// Note that currently this ignores threads. - pub read_receipts: IndexMap, + read_receipts: IndexMap, /// Whether the event has been sent by the the logged-in user themselves. - pub is_own: bool, + is_own: bool, /// Encryption information. - pub encryption_info: Option, + encryption_info: Option, // FIXME: Expose the raw JSON of aggregated events somehow - pub raw: Raw, + raw: Raw, } impl RemoteEventTimelineItem { + #[allow(clippy::too_many_arguments)] // Would be nice to fix, but unclear how + pub(in crate::room::timeline) fn new( + event_id: OwnedEventId, + sender: OwnedUserId, + sender_profile: TimelineDetails, + timestamp: MilliSecondsSinceUnixEpoch, + content: TimelineItemContent, + reactions: BundledReactions, + read_receipts: IndexMap, + is_own: bool, + encryption_info: Option, + raw: Raw, + ) -> Self { + Self { + event_id, + sender, + sender_profile, + timestamp, + content, + reactions, + read_receipts, + is_own, + encryption_info, + raw, + } + } + + /// Get the ID of the event. + pub fn event_id(&self) -> &EventId { + &self.event_id + } + + /// Get the sender of the event. + pub fn sender(&self) -> &UserId { + &self.sender + } + + /// Get the profile of the event's sender. + pub fn sender_profile(&self) -> &TimelineDetails { + &self.sender_profile + } + + /// Get the event timestamp as set by the homeserver that created the event. + pub fn timestamp(&self) -> MilliSecondsSinceUnixEpoch { + self.timestamp + } + + /// Get the content of the event. + pub fn content(&self) -> &TimelineItemContent { + &self.content + } + + /// Get the reactions of this item. + pub fn reactions(&self) -> &BundledReactions { + // FIXME: Find out the state of incomplete bundled reactions, adjust + // Ruma if necessary, return the whole BundledReactions field + &self.reactions + } + + /// Get the read receipts of this item. + /// + /// The key is the ID of a room member and the value are details about the + /// read receipt. + /// + /// Note that currently this ignores threads. + pub fn read_receipts(&self) -> &IndexMap { + &self.read_receipts + } + + /// Whether the event has been sent by the the logged-in user themselves. + pub fn is_own(&self) -> bool { + self.is_own + } + + /// Get the encryption information for the event. + pub fn encryption_info(&self) -> Option<&EncryptionInfo> { + self.encryption_info.as_ref() + } + + /// Get the raw JSON representation of the primary event. + pub fn raw(&self) -> &Raw { + &self.raw + } + + pub(in crate::room::timeline) fn set_content(&mut self, content: TimelineItemContent) { + self.content = content; + } + + pub(in crate::room::timeline) fn add_read_receipt( + &mut self, + user_id: OwnedUserId, + receipt: Receipt, + ) { + self.read_receipts.insert(user_id, receipt); + } + + /// Remove the read receipt for the given user. + /// + /// Returns `true` if there was one, `false` if not. + pub(in crate::room::timeline) fn remove_read_receipt(&mut self, user_id: &UserId) -> bool { + self.read_receipts.remove(user_id).is_some() + } + /// Clone the current event item, and update its `reactions`. pub(in crate::room::timeline) fn with_reactions(&self, reactions: BundledReactions) -> Self { Self { reactions, ..self.clone() } @@ -51,6 +154,14 @@ impl RemoteEventTimelineItem { Self { content, ..self.clone() } } + /// Clone the current event item, and update its `sender_profile`. + pub(in crate::room::timeline) fn with_sender_profile( + &self, + sender_profile: TimelineDetails, + ) -> Self { + Self { sender_profile, ..self.clone() } + } + /// Clone the current event item, change its `content` to /// [`TimelineItemContent::RedactedMessage`], and reset its `reactions`. pub(in crate::room::timeline) fn to_redacted(&self) -> Self { @@ -61,13 +172,6 @@ impl RemoteEventTimelineItem { ..self.clone() } } - - /// Get the reactions of this item. - pub fn reactions(&self) -> &BundledReactions { - // FIXME: Find out the state of incomplete bundled reactions, adjust - // Ruma if necessary, return the whole BundledReactions field - &self.reactions - } } #[cfg(not(tarpaulin_include))] diff --git a/crates/matrix-sdk/src/room/timeline/inner.rs b/crates/matrix-sdk/src/room/timeline/inner.rs index 4b5cd300f..c73f968c4 100644 --- a/crates/matrix-sdk/src/room/timeline/inner.rs +++ b/crates/matrix-sdk/src/room/timeline/inner.rs @@ -373,16 +373,14 @@ impl TimelineInner

    { tracing::Span::current().record("session_id", session_id); - let EventTimelineItem::Remote( - RemoteEventTimelineItem { event_id, raw, .. }, - ) = event_item else { + let EventTimelineItem::Remote(remote_event) = event_item else { error!("Key for unable-to-decrypt timeline item is not an event ID"); return None; }; - tracing::Span::current().record("event_id", debug(event_id)); + tracing::Span::current().record("event_id", debug(remote_event.event_id())); - let raw = raw.cast_ref(); + let raw = remote_event.raw().cast_ref(); match olm_machine.decrypt_room_event(raw, room_id).await { Ok(event) => { trace!("Successfully decrypted event that previously failed to decrypt"); @@ -509,7 +507,7 @@ impl TimelineInner { .and_then(|(pos, item)| item.as_remote().map(|item| (pos, item.clone()))) .ok_or(super::Error::RemoteEventNotInTimeline)?; - let TimelineItemContent::Message(message) = item.content.clone() else { + let TimelineItemContent::Message(message) = item.content().clone() else { return Ok(item); }; let Some(in_reply_to) = message.in_reply_to() else { @@ -529,23 +527,22 @@ impl TimelineInner { // We need to be sure to have the latest position of the event as it might have // changed while waiting for the request. let mut state = self.state.lock().await; - let (index, mut item) = rfind_event_by_id(&state.items, &item.event_id) + let (index, mut item) = rfind_event_by_id(&state.items, item.event_id()) .and_then(|(pos, item)| item.as_remote().map(|item| (pos, item.clone()))) .ok_or(super::Error::RemoteEventNotInTimeline)?; // Check the state of the event again, it might have been redacted while // the request was in-flight. - let TimelineItemContent::Message(message) = item.content.clone() else { + let TimelineItemContent::Message(message) = item.content().clone() else { return Ok(item); }; let Some(in_reply_to) = message.in_reply_to() else { return Ok(item); }; - item.content = TimelineItemContent::Message(message.with_in_reply_to(InReplyToDetails { - event_id: in_reply_to.event_id.clone(), - details, - })); + item.set_content(TimelineItemContent::Message(message.with_in_reply_to( + InReplyToDetails { event_id: in_reply_to.event_id.clone(), details }, + ))); state.items.set(index, Arc::new(TimelineItem::Event(item.clone().into()))); Ok(item) diff --git a/crates/matrix-sdk/src/room/timeline/read_receipts.rs b/crates/matrix-sdk/src/room/timeline/read_receipts.rs index 2e7cac355..59d609be7 100644 --- a/crates/matrix-sdk/src/room/timeline/read_receipts.rs +++ b/crates/matrix-sdk/src/room/timeline/read_receipts.rs @@ -77,7 +77,7 @@ pub(super) fn handle_explicit_read_receipts( }); if let Some((pos, mut remote_event_item)) = new_receipt_event_item { - remote_event_item.read_receipts.insert(user_id, receipt); + remote_event_item.add_read_receipt(user_id, receipt); timeline_state .items .set(pos, Arc::new(TimelineItem::Event(remote_event_item.into()))); @@ -106,10 +106,10 @@ pub(super) fn maybe_add_implicit_read_receipt( return; }; - let receipt = Receipt::new(remote_event_item.timestamp); + let receipt = Receipt::new(remote_event_item.timestamp()); let new_receipt = FullReceipt { - event_id: &remote_event_item.event_id, - user_id: &remote_event_item.sender.clone(), + event_id: remote_event_item.event_id(), + user_id: remote_event_item.sender(), receipt_type: ReceiptType::Read, receipt: &receipt, }; @@ -122,7 +122,7 @@ pub(super) fn maybe_add_implicit_read_receipt( users_read_receipts, ); if read_receipt_updated && !is_own_event { - remote_event_item.read_receipts.insert(remote_event_item.sender.clone(), receipt); + remote_event_item.add_read_receipt(remote_event_item.sender().to_owned(), receipt); } } @@ -174,8 +174,11 @@ fn maybe_update_read_receipt( if !is_own_user_id { // Remove the read receipt for this user from the old event. let mut old_event_item = old_event_item.clone(); - if old_event_item.read_receipts.remove(receipt.user_id).is_none() { - error!("inconsistent state: old event item for user's read receipt doesn't have a receipt for the user"); + if !old_event_item.remove_read_receipt(receipt.user_id) { + error!( + "inconsistent state: old event item for user's read \ + receipt doesn't have a receipt for the user" + ); } timeline_items .set(old_receipt_pos, Arc::new(TimelineItem::Event(old_event_item.into()))); diff --git a/crates/matrix-sdk/src/room/timeline/tests/basic.rs b/crates/matrix-sdk/src/room/timeline/tests/basic.rs index d68f6cea2..99ce74850 100644 --- a/crates/matrix-sdk/src/room/timeline/tests/basic.rs +++ b/crates/matrix-sdk/src/room/timeline/tests/basic.rs @@ -70,7 +70,7 @@ async fn reaction_redaction() { let event = item.as_event().unwrap().as_remote().unwrap(); assert_eq!(event.reactions().len(), 0); - let msg_event_id = &event.event_id; + let msg_event_id = event.event_id(); let rel = Annotation::new(msg_event_id.to_owned(), "+1".to_owned()); timeline.handle_live_message_event(&BOB, ReactionEventContent::new(rel)).await; @@ -81,7 +81,7 @@ async fn reaction_redaction() { // TODO: After adding raw timeline items, check for one here - let reaction_event_id = event.event_id.as_ref(); + let reaction_event_id = event.event_id(); timeline.handle_live_redaction(&BOB, reaction_event_id).await; let item = diff --git a/crates/matrix-sdk/src/room/timeline/tests/encryption.rs b/crates/matrix-sdk/src/room/timeline/tests/encryption.rs index 30e56d951..3deba7770 100644 --- a/crates/matrix-sdk/src/room/timeline/tests/encryption.rs +++ b/crates/matrix-sdk/src/room/timeline/tests/encryption.rs @@ -98,8 +98,8 @@ async fn retry_message_decryption() { let item = assert_matches!(stream.next().await, Some(VectorDiff::Set { index: 1, value }) => value); let event = item.as_event().unwrap().as_remote().unwrap(); - assert_matches!(&event.encryption_info, Some(_)); - let text = assert_matches!(&event.content, TimelineItemContent::Message(msg) => msg.body()); + assert_matches!(event.encryption_info(), Some(_)); + let text = assert_matches!(event.content(), TimelineItemContent::Message(msg) => msg.body()); assert_eq!(text, "It's a secret to everybody"); } @@ -200,8 +200,8 @@ async fn retry_edit_decryption() { let item = items[1].as_event().unwrap().as_remote().unwrap(); - assert_matches!(&item.encryption_info, Some(_)); - let msg = assert_matches!(&item.content, TimelineItemContent::Message(msg) => msg); + assert_matches!(item.encryption_info(), Some(_)); + let msg = assert_matches!(item.content(), TimelineItemContent::Message(msg) => msg); assert!(msg.is_edited()); assert_eq!(msg.body(), "This is Error"); } diff --git a/crates/matrix-sdk/src/room/timeline/tests/invalid.rs b/crates/matrix-sdk/src/room/timeline/tests/invalid.rs index e7825d4fd..c3cd412a3 100644 --- a/crates/matrix-sdk/src/room/timeline/tests/invalid.rs +++ b/crates/matrix-sdk/src/room/timeline/tests/invalid.rs @@ -26,10 +26,10 @@ async fn invalid_edit() { assert_matches!(stream.next().await, Some(VectorDiff::PushBack { value }) => value); let item = assert_matches!(stream.next().await, Some(VectorDiff::PushBack { value }) => value); let event = item.as_event().unwrap().as_remote().unwrap(); - let msg = event.content.as_message().unwrap(); + let msg = event.content().as_message().unwrap(); assert_eq!(msg.body(), "test"); - let msg_event_id = &event.event_id; + let msg_event_id = event.event_id(); let edit = assign!(RoomMessageEventContent::text_plain(" * fake"), { relates_to: Some(message::Relation::Replacement(Replacement::new( @@ -66,11 +66,11 @@ async fn invalid_event_content() { assert_matches!(stream.next().await, Some(VectorDiff::PushBack { value }) => value); let item = assert_matches!(stream.next().await, Some(VectorDiff::PushBack { value }) => value); let event_item = item.as_event().unwrap().as_remote().unwrap(); - assert_eq!(event_item.sender, "@alice:example.org"); - assert_eq!(event_item.event_id, event_id!("$eeG0HA0FAZ37wP8kXlNkxx3I").to_owned()); - assert_eq!(event_item.timestamp, MilliSecondsSinceUnixEpoch(uint!(10))); + assert_eq!(event_item.sender(), "@alice:example.org"); + assert_eq!(event_item.event_id(), event_id!("$eeG0HA0FAZ37wP8kXlNkxx3I").to_owned()); + assert_eq!(event_item.timestamp(), MilliSecondsSinceUnixEpoch(uint!(10))); let event_type = assert_matches!( - &event_item.content, + event_item.content(), TimelineItemContent::FailedToParseMessageLike { event_type, .. } => event_type ); assert_eq!(*event_type, MessageLikeEventType::RoomMessage); @@ -90,11 +90,11 @@ async fn invalid_event_content() { let item = assert_matches!(stream.next().await, Some(VectorDiff::PushBack { value }) => value); let event_item = item.as_event().unwrap().as_remote().unwrap(); - assert_eq!(event_item.sender, "@alice:example.org"); - assert_eq!(event_item.event_id, event_id!("$d5G0HA0FAZ37wP8kXlNkxx3I").to_owned()); - assert_eq!(event_item.timestamp, MilliSecondsSinceUnixEpoch(uint!(2179))); + assert_eq!(event_item.sender(), "@alice:example.org"); + assert_eq!(event_item.event_id(), event_id!("$d5G0HA0FAZ37wP8kXlNkxx3I").to_owned()); + assert_eq!(event_item.timestamp(), MilliSecondsSinceUnixEpoch(uint!(2179))); let (event_type, state_key) = assert_matches!( - &event_item.content, + event_item.content(), TimelineItemContent::FailedToParseState { event_type, state_key, diff --git a/crates/matrix-sdk/src/room/timeline/tests/read_receipts.rs b/crates/matrix-sdk/src/room/timeline/tests/read_receipts.rs index a7a3e37e6..a8fb13a87 100644 --- a/crates/matrix-sdk/src/room/timeline/tests/read_receipts.rs +++ b/crates/matrix-sdk/src/room/timeline/tests/read_receipts.rs @@ -38,14 +38,14 @@ async fn read_receipts_updates() { let item_a = assert_matches!(stream.next().await, Some(VectorDiff::PushBack { value }) => value); let event_a = item_a.as_event().unwrap().as_remote().unwrap(); - assert!(event_a.read_receipts.is_empty()); + assert!(event_a.read_receipts().is_empty()); // Implicit read receipt of Bob. let item_b = assert_matches!(stream.next().await, Some(VectorDiff::PushBack { value }) => value); let event_b = item_b.as_event().unwrap().as_remote().unwrap(); - assert_eq!(event_b.read_receipts.len(), 1); - assert!(event_b.read_receipts.get(*BOB).is_some()); + assert_eq!(event_b.read_receipts().len(), 1); + assert!(event_b.read_receipts().get(*BOB).is_some()); // Implicit read receipt of Bob is updated. timeline.handle_live_message_event(*BOB, RoomMessageEventContent::text_plain("C")).await; @@ -53,25 +53,25 @@ async fn read_receipts_updates() { let item_a = assert_matches!(stream.next().await, Some(VectorDiff::Set { index: 2, value }) => value); let event_a = item_a.as_event().unwrap().as_remote().unwrap(); - assert!(event_a.read_receipts.is_empty()); + assert!(event_a.read_receipts().is_empty()); let item_c = assert_matches!(stream.next().await, Some(VectorDiff::PushBack { value }) => value); let event_c = item_c.as_event().unwrap().as_remote().unwrap(); - assert_eq!(event_c.read_receipts.len(), 1); - assert!(event_c.read_receipts.get(*BOB).is_some()); + assert_eq!(event_c.read_receipts().len(), 1); + assert!(event_c.read_receipts().get(*BOB).is_some()); timeline.handle_live_message_event(*ALICE, RoomMessageEventContent::text_plain("D")).await; let item_d = assert_matches!(stream.next().await, Some(VectorDiff::PushBack { value }) => value); let event_d = item_d.as_event().unwrap().as_remote().unwrap(); - assert!(event_d.read_receipts.is_empty()); + assert!(event_d.read_receipts().is_empty()); // Explicit read receipt is updated. timeline .handle_read_receipts([( - event_d.event_id.clone(), + event_d.event_id().to_owned(), ReceiptType::Read, BOB.to_owned(), ReceiptThread::Unthreaded, @@ -81,11 +81,11 @@ async fn read_receipts_updates() { let item_c = assert_matches!(stream.next().await, Some(VectorDiff::Set { index: 3, value }) => value); let event_c = item_c.as_event().unwrap().as_remote().unwrap(); - assert!(event_c.read_receipts.is_empty()); + assert!(event_c.read_receipts().is_empty()); let item_d = assert_matches!(stream.next().await, Some(VectorDiff::Set { index: 4, value }) => value); let event_d = item_d.as_event().unwrap().as_remote().unwrap(); - assert_eq!(event_d.read_receipts.len(), 1); - assert!(event_d.read_receipts.get(*BOB).is_some()); + assert_eq!(event_d.read_receipts().len(), 1); + assert!(event_d.read_receipts().get(*BOB).is_some()); } diff --git a/crates/matrix-sdk/tests/integration/room/timeline.rs b/crates/matrix-sdk/tests/integration/room/timeline.rs index 9fa365abf..b06fa4eb1 100644 --- a/crates/matrix-sdk/tests/integration/room/timeline.rs +++ b/crates/matrix-sdk/tests/integration/room/timeline.rs @@ -241,8 +241,8 @@ async fn echo() { Some(VectorDiff::PushBack { value }) => value ); let item = remote_echo.as_event().unwrap().as_remote().unwrap(); - assert!(item.is_own); - assert_eq!(item.timestamp, MilliSecondsSinceUnixEpoch(uint!(152038280))); + assert!(item.is_own()); + assert_eq!(item.timestamp(), MilliSecondsSinceUnixEpoch(uint!(152038280))); } #[async_test] @@ -421,7 +421,7 @@ async fn reaction() { Some(VectorDiff::Set { index: 1, value }) => value ); let event_item = updated_message.as_event().unwrap().as_remote().unwrap(); - let msg = assert_matches!(&event_item.content, TimelineItemContent::Message(msg) => msg); + let msg = assert_matches!(event_item.content(), TimelineItemContent::Message(msg) => msg); assert!(!msg.is_edited()); assert_eq!(event_item.reactions().len(), 1); let group = &event_item.reactions()["👍"]; @@ -451,7 +451,7 @@ async fn reaction() { Some(VectorDiff::Set { index: 1, value }) => value ); let event_item = updated_message.as_event().unwrap().as_remote().unwrap(); - let msg = assert_matches!(&event_item.content, TimelineItemContent::Message(msg) => msg); + let msg = assert_matches!(event_item.content(), TimelineItemContent::Message(msg) => msg); assert!(!msg.is_edited()); assert_eq!(event_item.reactions().len(), 0); } @@ -634,13 +634,13 @@ async fn in_reply_to_details() { let second = assert_matches!(timeline_stream.next().await, Some(VectorDiff::PushBack { value }) => value); let second_event = second.as_event().unwrap().as_remote().unwrap(); let message = - assert_matches!(&second_event.content, TimelineItemContent::Message(message) => message); + assert_matches!(second_event.content(), TimelineItemContent::Message(message) => message); let in_reply_to = message.in_reply_to().unwrap(); assert_eq!(in_reply_to.event_id, event_id!("$event1")); assert_matches!(in_reply_to.details, TimelineDetails::Unavailable); // Fetch details locally first. - timeline.fetch_event_details(&second_event.event_id).await.unwrap(); + timeline.fetch_event_details(second_event.event_id()).await.unwrap(); let second = assert_matches!(timeline_stream.next().await, Some(VectorDiff::Set { index: 2, value }) => value); let message = assert_matches!(second.as_event().unwrap().content(), TimelineItemContent::Message(message) => message); @@ -674,7 +674,7 @@ async fn in_reply_to_details() { let third = assert_matches!(timeline_stream.next().await, Some(VectorDiff::PushBack { value }) => value); let third_event = third.as_event().unwrap().as_remote().unwrap(); let message = - assert_matches!(&third_event.content, TimelineItemContent::Message(message) => message); + assert_matches!(third_event.content(), TimelineItemContent::Message(message) => message); let in_reply_to = message.in_reply_to().unwrap(); assert_eq!(in_reply_to.event_id, event_id!("$remoteevent")); assert_matches!(in_reply_to.details, TimelineDetails::Unavailable); @@ -691,7 +691,7 @@ async fn in_reply_to_details() { .await; // Fetch details remotely if we can't find them locally. - timeline.fetch_event_details(&third_event.event_id).await.unwrap(); + timeline.fetch_event_details(third_event.event_id()).await.unwrap(); server.reset().await; let third = assert_matches!(timeline_stream.next().await, Some(VectorDiff::Set { index: 3, value }) => value); @@ -720,7 +720,7 @@ async fn in_reply_to_details() { .mount(&server) .await; - timeline.fetch_event_details(&third_event.event_id).await.unwrap(); + timeline.fetch_event_details(third_event.event_id()).await.unwrap(); let third = assert_matches!(timeline_stream.next().await, Some(VectorDiff::Set { index: 3, value }) => value); let message = assert_matches!(third.as_event().unwrap().content(), TimelineItemContent::Message(message) => message); @@ -790,21 +790,21 @@ async fn read_receipts_updates() { // We don't list the read receipt of our own user on events. let first_item = assert_matches!(timeline_stream.next().await, Some(VectorDiff::PushBack { value }) => value); let first_event = first_item.as_event().unwrap().as_remote().unwrap(); - assert!(first_event.read_receipts.is_empty()); + assert!(first_event.read_receipts().is_empty()); // Implicit read receipt of @alice:localhost. let second_item = assert_matches!(timeline_stream.next().await, Some(VectorDiff::PushBack { value }) => value); let second_event = second_item.as_event().unwrap().as_remote().unwrap(); - assert_eq!(second_event.read_receipts.len(), 1); + assert_eq!(second_event.read_receipts().len(), 1); // Read receipt of @alice:localhost is moved to third event. let second_item = assert_matches!(timeline_stream.next().await, Some(VectorDiff::Set { index: 2, value }) => value); let second_event = second_item.as_event().unwrap().as_remote().unwrap(); - assert!(second_event.read_receipts.is_empty()); + assert!(second_event.read_receipts().is_empty()); let third_item = assert_matches!(timeline_stream.next().await, Some(VectorDiff::PushBack { value }) => value); let third_event = third_item.as_event().unwrap().as_remote().unwrap(); - assert_eq!(third_event.read_receipts.len(), 1); + assert_eq!(third_event.read_receipts().len(), 1); // Read receipt on unknown event is ignored. ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_ephemeral_event( @@ -880,5 +880,5 @@ async fn read_receipts_updates() { let third_item = assert_matches!(timeline_stream.next().await, Some(VectorDiff::Set { index: 3, value }) => value); let third_event = third_item.as_event().unwrap().as_remote().unwrap(); - assert_eq!(third_event.read_receipts.len(), 2); + assert_eq!(third_event.read_receipts().len(), 2); } diff --git a/testing/sliding-sync-integration-test/src/lib.rs b/testing/sliding-sync-integration-test/src/lib.rs index 1c998f6e3..f8cadf5da 100644 --- a/testing/sliding-sync-integration-test/src/lib.rs +++ b/testing/sliding-sync-integration-test/src/lib.rs @@ -225,12 +225,12 @@ mod tests { timeline_items[1].as_event(), Some(EventTimelineItem::Remote(remote_event)) => remote_event ); - all_event_ids.push(latest_remote_event.event_id.clone()); + all_event_ids.push(latest_remote_event.event_id().to_owned()); // Test the room to see the last event. assert_matches!(room.latest_event().await, Some(EventTimelineItem::Remote(remote_event)) => { - assert_eq!(remote_event.event_id, latest_remote_event.event_id, "Unexpected latest event"); - assert_eq!(remote_event.content.as_message().unwrap().body(), "Message #19"); + assert_eq!(remote_event.event_id(), latest_remote_event.event_id(), "Unexpected latest event"); + assert_eq!(remote_event.content().as_message().unwrap().body(), "Message #19"); }); (room, timeline, timeline_stream) @@ -275,11 +275,11 @@ mod tests { // Check messages arrived in the correct order. assert_eq!( - remote_event.content.as_message().expect("Received event is not a message").body(), + remote_event.content().as_message().expect("Received event is not a message").body(), format!("Message #{nth}"), ); - all_event_ids.push(remote_event.event_id.clone()); + all_event_ids.push(remote_event.event_id().to_owned()); }); } @@ -296,16 +296,16 @@ mod tests { value.as_event(), Some(EventTimelineItem::Remote(remote_event)) => remote_event ); - assert_eq!(remote_event.content.as_message().unwrap().body(), "Message #19"); - assert_eq!(remote_event.event_id.clone(), all_event_ids[0]); + assert_eq!(remote_event.content().as_message().unwrap().body(), "Message #19"); + assert_eq!(remote_event.event_id(), all_event_ids[0]); remote_event.clone() }); // Test the room to see the last event. assert_matches!(room.latest_event().await, Some(EventTimelineItem::Remote(remote_event)) => { - assert_eq!(remote_event.content.as_message().unwrap().body(), "Message #19"); - assert_eq!(remote_event.event_id, latest_remote_event.event_id, "Unexpected latest event"); + assert_eq!(remote_event.content().as_message().unwrap().body(), "Message #19"); + assert_eq!(remote_event.event_id(), latest_remote_event.event_id(), "Unexpected latest event"); }); // Ensure there is no event ID duplication. From 15513b0ada3ebc26a3e72414e4eb8078dea0ea2e Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 1 Mar 2023 15:50:31 +0000 Subject: [PATCH 101/166] Clean up the way we build `xtask` in CI (#1600) Currently, the cache of the xtask binary isn't working terribly well: * we seem to build it on each run anyway, presumably because we don't cache any of the intermediate build artifacts. Running the binary directly rather than indirecting via "cargo" prevents this. * There is no sharing of the cache between the "rust" and "bindings" CI, because we use different cache keys. This PR addresses both problems, and hopefully speeds up CI a bit as a result. --- .github/workflows/bindings_ci.yml | 71 +++++--------------------- .github/workflows/ci.yml | 72 +++++++++++---------------- .github/workflows/xtask.yml | 82 +++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 104 deletions(-) create mode 100644 .github/workflows/xtask.yml diff --git a/.github/workflows/bindings_ci.yml b/.github/workflows/bindings_ci.yml index c468b6755..2d4113f2d 100644 --- a/.github/workflows/bindings_ci.yml +++ b/.github/workflows/bindings_ci.yml @@ -18,36 +18,12 @@ env: MATRIX_SDK_CRYPTO_JS_PATH: bindings/matrix-sdk-crypto-js jobs: - xtask-linux: - runs-on: ubuntu-latest - steps: - - name: Checkout repo - uses: actions/checkout@v2 - - - name: Install Protoc - uses: arduino/setup-protoc@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Check xtask cache - uses: actions/cache@v3 - id: xtask-cache - with: - path: target/debug/xtask - key: xtask-linux-${{ hashFiles('Cargo.toml', 'xtask/**') }} - - - name: Install rust stable toolchain - if: steps.xtask-cache.outputs.cache-hit != 'true' - uses: dtolnay/rust-toolchain@stable - - - name: Build - if: steps.xtask-cache.outputs.cache-hit != 'true' - run: | - cargo build -p xtask + xtask: + uses: ./.github/workflows/xtask.yml test-uniffi-codegen: name: Test UniFFI bindings generation - needs: xtask-linux + needs: xtask if: github.event_name == 'push' || !github.event.pull_request.draft runs-on: ubuntu-latest @@ -67,10 +43,11 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Get xtask - uses: actions/cache@v3 + uses: actions/cache/restore@v3 with: path: target/debug/xtask - key: xtask-linux-${{ hashFiles('Cargo.toml', 'xtask/**') }} + key: "${{ needs.xtask.outputs.cachekey-linux }}" + fail-on-cache-miss: true - name: Build library & generate bindings run: target/debug/xtask ci bindings @@ -211,36 +188,9 @@ jobs: working-directory: ${{ env.MATRIX_SDK_CRYPTO_JS_PATH }} run: npm run doc - xtask-macos: - runs-on: macos-12 - steps: - - name: Checkout repo - uses: actions/checkout@v2 - - - name: Install Protoc - uses: arduino/setup-protoc@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Check xtask cache - uses: actions/cache@v3 - id: xtask-cache - with: - path: target/debug/xtask - key: xtask-macos-${{ hashFiles('Cargo.toml', 'xtask/**') }} - - - name: Install rust stable toolchain - if: steps.xtask-cache.outputs.cache-hit != 'true' - uses: dtolnay/rust-toolchain@stable - - - name: Build - if: steps.xtask-cache.outputs.cache-hit != 'true' - run: | - cargo build -p xtask - test-apple: name: matrix-rust-components-swift - needs: xtask-macos + needs: xtask runs-on: macos-12 if: github.event_name == 'push' || !github.event.pull_request.draft @@ -263,10 +213,11 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Get xtask - uses: actions/cache@v3 + uses: actions/cache/restore@v3 with: path: target/debug/xtask - key: xtask-macos-${{ hashFiles('Cargo.toml', 'xtask/**') }} + key: "${{ needs.xtask.outputs.cachekey-macos }}" + fail-on-cache-miss: true - name: Build library & bindings run: target/debug/xtask swift build-library @@ -276,4 +227,4 @@ jobs: run: swift test - name: Build Framework - run: cargo xtask swift build-framework --only-target=aarch64-apple-ios + run: target/debug/xtask swift build-framework --only-target=aarch64-apple-ios diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06db21d8d..dd6f282ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,31 +17,7 @@ env: jobs: xtask: - runs-on: ubuntu-latest - steps: - - name: Checkout repo - uses: actions/checkout@v2 - - - name: Install Protoc - uses: arduino/setup-protoc@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Check xtask cache - uses: actions/cache@v3 - id: xtask-cache - with: - path: target/debug/xtask - key: xtask-${{ hashFiles('xtask/**') }} - - - name: Install rust stable toolchain - if: steps.xtask-cache.outputs.cache-hit != 'true' - uses: dtolnay/rust-toolchain@stable - - - name: Build - if: steps.xtask-cache.outputs.cache-hit != 'true' - run: | - cargo build -p xtask + uses: ./.github/workflows/xtask.yml test-matrix-sdk-features: name: 🐧 [m], ${{ matrix.name }} @@ -76,14 +52,15 @@ jobs: uses: taiki-e/install-action@nextest - name: Get xtask - uses: actions/cache@v3 + uses: actions/cache/restore@v3 with: path: target/debug/xtask - key: xtask-${{ hashFiles('xtask/**') }} + key: "${{ needs.xtask.outputs.cachekey-linux }}" + fail-on-cache-miss: true - name: Test run: | - cargo run -p xtask -- ci test-features ${{ matrix.name }} + target/debug/xtask ci test-features ${{ matrix.name }} test-matrix-sdk-examples: name: 🐧 [m]-examples @@ -105,14 +82,15 @@ jobs: uses: taiki-e/install-action@nextest - name: Get xtask - uses: actions/cache@v3 + uses: actions/cache/restore@v3 with: path: target/debug/xtask - key: xtask-${{ hashFiles('xtask/**') }} + key: "${{ needs.xtask.outputs.cachekey-linux }}" + fail-on-cache-miss: true - name: Test run: | - cargo run -p xtask -- ci examples + target/debug/xtask ci examples test-matrix-sdk-crypto: name: 🐧 [m]-crypto @@ -134,14 +112,15 @@ jobs: uses: taiki-e/install-action@nextest - name: Get xtask - uses: actions/cache@v3 + uses: actions/cache/restore@v3 with: path: target/debug/xtask - key: xtask-${{ hashFiles('xtask/**') }} + key: "${{ needs.xtask.outputs.cachekey-linux }}" + fail-on-cache-miss: true - name: Test run: | - cargo run -p xtask -- ci test-crypto + target/debug/xtask ci test-crypto test-all-crates: name: ${{ matrix.name }} @@ -261,18 +240,19 @@ jobs: uses: taiki-e/install-action@nextest - name: Get xtask - uses: actions/cache@v3 + uses: actions/cache/restore@v3 with: path: target/debug/xtask - key: xtask-${{ hashFiles('xtask/**') }} + key: "${{ needs.xtask.outputs.cachekey-linux }}" + fail-on-cache-miss: true - name: Rust Check run: | - cargo run -p xtask -- ci wasm ${{ matrix.cmd }} + target/debug/xtask ci wasm ${{ matrix.cmd }} - name: Wasm-Pack test run: | - cargo run -p xtask -- ci wasm-pack ${{ matrix.cmd }} + target/debug/xtask ci wasm-pack ${{ matrix.cmd }} test-appservice: name: ${{ matrix.os-name }} [m]-appservice @@ -286,9 +266,11 @@ jobs: include: - os: ubuntu-latest os-name: 🐧 + xtask-cachekey: "${{ needs.xtask.outputs.cachekey-linux }}" - os: macos-latest os-name: 🍏 + xtask-cachekey: "${{ needs.xtask.outputs.cachekey-macos }}" steps: - name: Checkout @@ -304,14 +286,15 @@ jobs: uses: taiki-e/install-action@nextest - name: Get xtask - uses: actions/cache@v3 + uses: actions/cache/restore@v3 with: path: target/debug/xtask - key: xtask-${{ hashFiles('xtask/**') }} + key: "${{ matrix.xtask-cachekey }}" + fail-on-cache-miss: true - name: Run checks run: | - cargo run -p xtask -- ci test-appservice + target/debug/xtask ci test-appservice formatting: name: Check Formatting @@ -367,14 +350,15 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Get xtask - uses: actions/cache@v3 + uses: actions/cache/restore@v3 with: path: target/debug/xtask - key: xtask-${{ hashFiles('xtask/**') }} + key: "${{ needs.xtask.outputs.cachekey-linux }}" + fail-on-cache-miss: true - name: Clippy run: | - cargo run -p xtask -- ci clippy + target/debug/xtask ci clippy integration-tests: name: Integration test diff --git a/.github/workflows/xtask.yml b/.github/workflows/xtask.yml new file mode 100644 index 000000000..fd9b95566 --- /dev/null +++ b/.github/workflows/xtask.yml @@ -0,0 +1,82 @@ +# A reusable github actions workflow that will build xtask, if it is not +# already cached. +# +# It will create a pair of GHA cache entries, if they do not already exist. +# The cache keys take the form `xtask-{os}-{hash}`, where "{os}" is "linux" +# or "macos", and "{hash}" is the hash of the xtask# directory. +# +# The cache keys are written to output variables named "cachekey-{os}". +# + +name: Build xtask if necessary + +on: + workflow_call: + outputs: + cachekey-linux: + description: "The cache key for the linux build artifact" + value: "${{ jobs.xtask.outputs.cachekey-linux }}" + cachekey-macos: + description: "The cache key for the macos build artifact" + value: "${{ jobs.xtask.outputs.cachekey-macos }}" + +env: + CARGO_TERM_COLOR: always + +jobs: + xtask: + name: "xtask-${{ matrix.os-name }}" + + strategy: + fail-fast: true + matrix: + include: + - os: ubuntu-latest + os-name: 🐧 + cachekey-id: linux + + - os: macos-12 + os-name: 🍏 + cachekey-id: macos + + runs-on: "${{ matrix.os }}" + + steps: + - name: Checkout repo + uses: actions/checkout@v2 + + - name: Calculate cache key + id: cachekey + # set a step output variable "cachekey-{os}" that can be referenced in + # the job outputs below. + run: | + echo "cachekey-${{ matrix.cachekey-id }}=xtask-${{ matrix.cachekey-id }}-${{ hashFiles('Cargo.toml', 'xtask/**') }}" >> $GITHUB_OUTPUT + + - name: Check xtask cache + uses: actions/cache@v3 + id: xtask-cache + with: + path: target/debug/xtask + # use the cache key calculated in the step above. Bit of an awkard + # syntax + key: | + ${{ steps.cachekey.outputs[format('cachekey-{0}', matrix.cachekey-id)] }} + + - name: Install Protoc + if: steps.xtask-cache.outputs.cache-hit != 'true' + uses: arduino/setup-protoc@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install rust stable toolchain + if: steps.xtask-cache.outputs.cache-hit != 'true' + uses: dtolnay/rust-toolchain@stable + + - name: Build + if: steps.xtask-cache.outputs.cache-hit != 'true' + run: | + cargo build -p xtask + + outputs: + "cachekey-linux": "${{ steps.cachekey.outputs.cachekey-linux }}" + "cachekey-macos": "${{ steps.cachekey.outputs.cachekey-macos }}" From 1dccea87355d864faf181f3e57a445a14e4b53fd Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 19:59:55 +0100 Subject: [PATCH 102/166] doc: Fix a typo. --- bindings/matrix-sdk-ffi/src/sliding_sync.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-ffi/src/sliding_sync.rs b/bindings/matrix-sdk-ffi/src/sliding_sync.rs index 3a278c0ff..a8c8ad0fe 100644 --- a/bindings/matrix-sdk-ffi/src/sliding_sync.rs +++ b/bindings/matrix-sdk-ffi/src/sliding_sync.rs @@ -64,7 +64,7 @@ impl TaskHandle { } } - /// Check wether a handle-based `TaskHandle` is finished; will return + /// Check whether a handle-based `TaskHandle` is finished; will return /// `false` for callback-based `TaskHandle`. pub fn is_finished(&self) -> bool { self.handle.as_ref().map(|handle| handle.is_finished()).unwrap_or_default() From bdb9d274cea37e311e998b6462111ee8e6c7ec08 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 1 Mar 2023 22:17:36 +0000 Subject: [PATCH 103/166] Skip installing `protoc` where it is unneeded (#1604) This seems to have been cargo-culted to lots of places where it is redundant. --- .github/workflows/bindings_ci.yml | 5 ----- .github/workflows/ci.yml | 5 ----- .github/workflows/xtask.yml | 6 ------ 3 files changed, 16 deletions(-) diff --git a/.github/workflows/bindings_ci.yml b/.github/workflows/bindings_ci.yml index 2d4113f2d..8a94844b5 100644 --- a/.github/workflows/bindings_ci.yml +++ b/.github/workflows/bindings_ci.yml @@ -198,11 +198,6 @@ jobs: - name: Checkout uses: actions/checkout@v1 - - name: Install Protoc - uses: arduino/setup-protoc@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install Rust uses: dtolnay/rust-toolchain@nightly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd6f282ca..d1dd0b502 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -336,11 +336,6 @@ jobs: - name: Checkout the repo uses: actions/checkout@v3 - - name: Install Protoc - uses: arduino/setup-protoc@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install Rust uses: dtolnay/rust-toolchain@nightly with: diff --git a/.github/workflows/xtask.yml b/.github/workflows/xtask.yml index fd9b95566..c31f720d5 100644 --- a/.github/workflows/xtask.yml +++ b/.github/workflows/xtask.yml @@ -62,12 +62,6 @@ jobs: key: | ${{ steps.cachekey.outputs[format('cachekey-{0}', matrix.cachekey-id)] }} - - name: Install Protoc - if: steps.xtask-cache.outputs.cache-hit != 'true' - uses: arduino/setup-protoc@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install rust stable toolchain if: steps.xtask-cache.outputs.cache-hit != 'true' uses: dtolnay/rust-toolchain@stable From 2fe08a90c5ad6357f64375a589f742b0d65b235a Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 2 Mar 2023 12:47:58 +0000 Subject: [PATCH 104/166] Reinstate protoc for a couple of GHA jobs (#1607) Looks like #1604 broke the build :( --- .github/workflows/bindings_ci.yml | 6 ++++++ .github/workflows/ci.yml | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/.github/workflows/bindings_ci.yml b/.github/workflows/bindings_ci.yml index 8a94844b5..f02eb6dd6 100644 --- a/.github/workflows/bindings_ci.yml +++ b/.github/workflows/bindings_ci.yml @@ -198,6 +198,12 @@ jobs: - name: Checkout uses: actions/checkout@v1 + # install protoc in case we end up rebuilding opentelemetry-proto + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install Rust uses: dtolnay/rust-toolchain@nightly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1dd0b502..dd6f282ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -336,6 +336,11 @@ jobs: - name: Checkout the repo uses: actions/checkout@v3 + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install Rust uses: dtolnay/rust-toolchain@nightly with: From e84a43dbaa0db6f67b35f96642837d14082e7cb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= <76261501+zecakeh@users.noreply.github.com> Date: Thu, 2 Mar 2023 14:35:51 +0100 Subject: [PATCH 105/166] Split StateStore integration tests into a trait MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To declare the tests, the macro is still used but the content of the tests is moved in the StateStoreIntegrationTests trait. Allows to get the proper line reported when a panic occurs, and have linting, formatting and IDE suggestions. Signed-off-by: Kévin Commaille --- crates/matrix-sdk-base/Cargo.toml | 3 +- .../src/store/integration_tests.rs | 1820 +++++++++-------- crates/matrix-sdk-base/src/store/mod.rs | 2 + 3 files changed, 921 insertions(+), 904 deletions(-) diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index 64c4f84dc..b3cf57ee9 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -23,7 +23,7 @@ qrcode = ["matrix-sdk-crypto?/qrcode"] experimental-sliding-sync = ["ruma/unstable-msc3575"] # helpers for testing features build upon this -testing = ["dep:http"] +testing = ["dep:http", "dep:matrix-sdk-test"] [dependencies] async-stream = { workspace = true } @@ -36,6 +36,7 @@ http = { workspace = true, optional = true } 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" } +matrix-sdk-test = { version = "0.6.0", path = "../../testing/matrix-sdk-test", optional = true } once_cell = { workspace = true } ruma = { workspace = true, features = ["canonical-json"] } serde = { workspace = true, features = ["rc"] } diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index d2b029683..87a6195a5 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -1,4 +1,825 @@ -//! Macro of integration tests for StateStore implementations. +//! Trait and macro of integration tests for StateStore implementations. + +use std::collections::{BTreeMap, BTreeSet}; + +use async_trait::async_trait; +use matrix_sdk_test::test_json; +use ruma::{ + api::client::media::get_content_thumbnail::v3::Method, + event_id, + events::{ + presence::PresenceEvent, + receipt::{ReceiptThread, ReceiptType}, + room::{ + member::{ + MembershipState, RoomMemberEventContent, StrippedRoomMemberEvent, + SyncRoomMemberEvent, + }, + power_levels::RoomPowerLevelsEventContent, + topic::{OriginalRoomTopicEvent, RedactedRoomTopicEvent, RoomTopicEventContent}, + MediaSource, + }, + AnyEphemeralRoomEventContent, AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, + AnyStrippedStateEvent, AnySyncEphemeralRoomEvent, AnySyncStateEvent, + GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType, + }, + mxc_uri, room_id, + serde::Raw, + uint, user_id, EventId, OwnedEventId, RoomId, UserId, +}; +use serde_json::{json, value::Value as JsonValue}; + +use super::DynStateStore; +use crate::{ + deserialized_responses::MemberEvent, + media::{MediaFormat, MediaRequest, MediaThumbnailSize}, + store::{Result, StateStoreExt}, + RoomInfo, RoomType, StateChanges, +}; + +/// `StateStore` integration tests. +/// +/// This trait is not meant to be used directly, but will be used with the [``] +/// macro. +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait StateStoreIntegrationTests { + /// Populate the given `StateStore`. + async fn populate(&self) -> Result<()>; + /// Test media content storage. + async fn test_media_content(&self); + /// Test room topic redaction. + async fn test_topic_redaction(&self) -> Result<()>; + /// Test populating the store. + async fn test_populate_store(&self) -> Result<()>; + /// Test room member saving. + async fn test_member_saving(&self); + /// Test filter saving. + async fn test_filter_saving(&self); + /// Test sync token saving. + async fn test_sync_token_saving(&self); + /// Test stripped room member saving. + async fn test_stripped_member_saving(&self); + /// Test room power levels saving. + async fn test_power_level_saving(&self); + /// Test user receipts saving. + async fn test_receipts_saving(&self); + /// Test custom storage. + async fn test_custom_storage(&self) -> Result<()>; + /// Test invited room saving. + async fn test_persist_invited_room(&self) -> Result<()>; + /// Test stripped and non-stripped room member saving. + async fn test_stripped_non_stripped(&self) -> Result<()>; + /// Test room removal. + async fn test_room_removal(&self) -> Result<()>; +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl StateStoreIntegrationTests for DynStateStore { + async fn populate(&self) -> Result<()> { + let mut changes = StateChanges::default(); + + let user_id = user_id(); + let invited_user_id = invited_user_id(); + let room_id = room_id(); + let stripped_room_id = stripped_room_id(); + + changes.sync_token = Some("t392-516_47314_0_7_1_1_1_11444_1".to_owned()); + + let presence_json: &JsonValue = &test_json::PRESENCE; + let presence_raw = + serde_json::from_value::>(presence_json.clone()).unwrap(); + let presence_event = presence_raw.deserialize().unwrap(); + changes.add_presence_event(presence_event, presence_raw); + + let pushrules_json: &JsonValue = &test_json::PUSH_RULES; + let pushrules_raw = + serde_json::from_value::>(pushrules_json.clone()) + .unwrap(); + let pushrules_event = pushrules_raw.deserialize().unwrap(); + changes.add_account_data(pushrules_event, pushrules_raw); + + let mut room = RoomInfo::new(room_id, RoomType::Joined); + room.mark_as_left(); + + let tag_json: &JsonValue = &test_json::TAG; + let tag_raw = + serde_json::from_value::>(tag_json.clone()).unwrap(); + let tag_event = tag_raw.deserialize().unwrap(); + changes.add_room_account_data(room_id, tag_event, tag_raw); + + let name_json: &JsonValue = &test_json::NAME; + let name_raw = serde_json::from_value::>(name_json.clone()).unwrap(); + let name_event = name_raw.deserialize().unwrap(); + room.handle_state_event(&name_event); + changes.add_state_event(room_id, name_event, name_raw); + + let topic_json: &JsonValue = &test_json::TOPIC; + let topic_raw = serde_json::from_value::>(topic_json.clone()) + .expect("can create sync-state-event for topic"); + let topic_event = topic_raw.deserialize().expect("can deserialize raw topic"); + room.handle_state_event(&topic_event); + changes.add_state_event(room_id, topic_event, topic_raw); + + let mut room_ambiguity_map = BTreeMap::new(); + let mut room_profiles = BTreeMap::new(); + let mut room_members = BTreeMap::new(); + + let member_json: &JsonValue = &test_json::MEMBER; + let member_event: SyncRoomMemberEvent = + serde_json::from_value(member_json.clone()).unwrap(); + let displayname = member_event.as_original().unwrap().content.displayname.clone().unwrap(); + room_ambiguity_map.insert(displayname.clone(), BTreeSet::from([user_id.to_owned()])); + room_profiles.insert(user_id.to_owned(), (&member_event).into()); + room_members.insert(user_id.to_owned(), Raw::new(&member_json).unwrap().cast()); + + let member_state_raw = + serde_json::from_value::>(member_json.clone()).unwrap(); + let member_state_event = member_state_raw.deserialize().unwrap(); + changes.add_state_event(room_id, member_state_event, member_state_raw); + + let invited_member_json: &JsonValue = &test_json::MEMBER_INVITE; + // FIXME: Should be stripped room member event + let invited_member_event: SyncRoomMemberEvent = + serde_json::from_value(invited_member_json.clone()).unwrap(); + room_ambiguity_map.entry(displayname).or_default().insert(invited_user_id.to_owned()); + room_profiles.insert(invited_user_id.to_owned(), (&invited_member_event).into()); + room_members + .insert(invited_user_id.to_owned(), Raw::new(&invited_member_json).unwrap().cast()); + + let invited_member_state_raw = + serde_json::from_value::>(invited_member_json.clone()).unwrap(); + let invited_member_state_event = invited_member_state_raw.deserialize().unwrap(); + changes.add_state_event(room_id, invited_member_state_event, invited_member_state_raw); + + let receipt_json: &JsonValue = &test_json::READ_RECEIPT; + let receipt_event = + serde_json::from_value::(receipt_json.clone()).unwrap(); + let receipt_content = match receipt_event.content() { + AnyEphemeralRoomEventContent::Receipt(content) => content, + _ => panic!(), + }; + changes.add_receipts(room_id, receipt_content); + + changes.ambiguity_maps.insert(room_id.to_owned(), room_ambiguity_map); + changes.profiles.insert(room_id.to_owned(), room_profiles); + changes.members.insert(room_id.to_owned(), room_members); + changes.add_room(room); + + let mut stripped_room = RoomInfo::new(stripped_room_id, RoomType::Invited); + + let stripped_name_json: &JsonValue = &test_json::NAME_STRIPPED; + let stripped_name_raw = + serde_json::from_value::>(stripped_name_json.clone()) + .unwrap(); + let stripped_name_event = stripped_name_raw.deserialize().unwrap(); + stripped_room.handle_stripped_state_event(&stripped_name_event); + changes.stripped_state.insert( + stripped_room_id.to_owned(), + BTreeMap::from([( + stripped_name_event.event_type(), + BTreeMap::from([( + stripped_name_event.state_key().to_owned(), + stripped_name_raw.clone(), + )]), + )]), + ); + + changes.add_stripped_room(stripped_room); + + let stripped_member_json: &JsonValue = &test_json::MEMBER_STRIPPED; + let stripped_member_event = Raw::new(&stripped_member_json.clone()).unwrap().cast(); + changes.add_stripped_member(stripped_room_id, user_id, stripped_member_event); + + self.save_changes(&changes).await?; + + Ok(()) + } + + async fn test_media_content(&self) { + let uri = mxc_uri!("mxc://localhost/media"); + let content: Vec = "somebinarydata".into(); + + let request_file = + MediaRequest { source: MediaSource::Plain(uri.to_owned()), format: MediaFormat::File }; + + let request_thumbnail = MediaRequest { + source: MediaSource::Plain(uri.to_owned()), + format: MediaFormat::Thumbnail(MediaThumbnailSize { + method: Method::Crop, + width: uint!(100), + height: uint!(100), + }), + }; + + assert!( + self.get_media_content(&request_file).await.unwrap().is_none(), + "unexpected media found" + ); + assert!( + self.get_media_content(&request_thumbnail).await.unwrap().is_none(), + "media not found" + ); + + self.add_media_content(&request_file, content.clone()).await.expect("adding media failed"); + assert!( + self.get_media_content(&request_file).await.unwrap().is_some(), + "media not found though added" + ); + + self.remove_media_content(&request_file).await.expect("removing media failed"); + assert!( + self.get_media_content(&request_file).await.unwrap().is_none(), + "media still there after removing" + ); + + self.add_media_content(&request_file, content.clone()) + .await + .expect("adding media again failed"); + assert!( + self.get_media_content(&request_file).await.unwrap().is_some(), + "media not found after adding again" + ); + + self.add_media_content(&request_thumbnail, content.clone()) + .await + .expect("adding thumbnail failed"); + assert!( + self.get_media_content(&request_thumbnail).await.unwrap().is_some(), + "thumbnail not found" + ); + + self.remove_media_content_for_uri(uri).await.expect("removing all media for uri failed"); + assert!( + self.get_media_content(&request_file).await.unwrap().is_none(), + "media wasn't removed" + ); + assert!( + self.get_media_content(&request_thumbnail).await.unwrap().is_none(), + "thumbnail wasn't removed" + ); + } + + async fn test_topic_redaction(&self) -> Result<()> { + let room_id = room_id(); + self.populate().await?; + + assert!(self.get_sync_token().await?.is_some()); + assert_eq!( + self.get_state_event_static::(room_id) + .await? + .expect("room topic found before redaction") + .deserialize_as::() + .expect("can deserialize room topic before redaction") + .content + .topic, + "😀" + ); + + let mut changes = StateChanges::default(); + + let redaction_json: &JsonValue = &test_json::TOPIC_REDACTION; + let redaction_evt: Raw<_> = serde_json::from_value(redaction_json.clone()) + .expect("topic redaction event making works"); + let redacted_event_id: OwnedEventId = redaction_evt.get_field("redacts").unwrap().unwrap(); + + changes.add_redaction(room_id, &redacted_event_id, redaction_evt); + self.save_changes(&changes).await?; + + match self + .get_state_event_static::(room_id) + .await? + .expect("room topic found before redaction") + .deserialize_as::() + { + Err(_) => {} // as expected + Ok(_) => panic!("Topic has not been redacted"), + } + + let _ = self + .get_state_event_static::(room_id) + .await? + .expect("room topic found after redaction") + .deserialize_as::() + .expect("can deserialize room topic after redaction"); + + Ok(()) + } + + async fn test_populate_store(&self) -> Result<()> { + let room_id = room_id(); + let user_id = user_id(); + self.populate().await?; + + assert!(self.get_sync_token().await?.is_some()); + assert!(self.get_presence_event(user_id).await?.is_some()); + assert_eq!(self.get_room_infos().await?.len(), 1, "Expected to find 1 room info"); + assert_eq!( + self.get_stripped_room_infos().await?.len(), + 1, + "Expected to find 1 stripped room info" + ); + assert!(self + .get_account_data_event(GlobalAccountDataEventType::PushRules) + .await? + .is_some()); + + assert!(self.get_state_event(room_id, StateEventType::RoomName, "").await?.is_some()); + assert_eq!( + self.get_state_events(room_id, StateEventType::RoomTopic).await?.len(), + 1, + "Expected to find 1 room topic" + ); + assert!(self.get_profile(room_id, user_id).await?.is_some()); + assert!(self.get_member_event(room_id, user_id).await?.is_some()); + assert_eq!( + self.get_user_ids(room_id).await?.len(), + 2, + "Expected to find 2 members for room" + ); + assert_eq!( + self.get_invited_user_ids(room_id).await?.len(), + 1, + "Expected to find 1 invited user ids" + ); + assert_eq!( + self.get_joined_user_ids(room_id).await?.len(), + 1, + "Expected to find 1 joined user ids" + ); + assert_eq!( + self.get_users_with_display_name(room_id, "example").await?.len(), + 2, + "Expected to find 2 display names for room" + ); + assert!(self + .get_room_account_data_event(room_id, RoomAccountDataEventType::Tag) + .await? + .is_some()); + assert!(self + .get_user_room_receipt_event( + room_id, + ReceiptType::Read, + ReceiptThread::Unthreaded, + user_id + ) + .await? + .is_some()); + assert_eq!( + self.get_event_room_receipt_events( + room_id, + ReceiptType::Read, + ReceiptThread::Unthreaded, + first_receipt_event_id() + ) + .await? + .len(), + 1, + "Expected to find 1 read receipt" + ); + Ok(()) + } + + async fn test_member_saving(&self) { + let room_id = room_id!("!test_member_saving:localhost"); + let user_id = user_id(); + + assert!(self.get_member_event(room_id, user_id).await.unwrap().is_none()); + let mut changes = StateChanges::default(); + changes + .members + .entry(room_id.to_owned()) + .or_default() + .insert(user_id.to_owned(), membership_event()); + + self.save_changes(&changes).await.unwrap(); + assert!(self.get_member_event(room_id, user_id).await.unwrap().is_some()); + + let members = self.get_user_ids(room_id).await.unwrap(); + assert!(!members.is_empty(), "We expected to find members for the room") + } + + async fn test_filter_saving(&self) { + let test_name = "filter_name"; + let filter_id = "filter_id_1234"; + assert_eq!(self.get_filter(test_name).await.unwrap(), None); + self.save_filter(test_name, filter_id).await.unwrap(); + assert_eq!(self.get_filter(test_name).await.unwrap(), Some(filter_id.to_owned())); + } + + async fn test_sync_token_saving(&self) { + let mut changes = StateChanges::default(); + let sync_token = "t392-516_47314_0_7_1".to_owned(); + + changes.sync_token = Some(sync_token.clone()); + assert_eq!(self.get_sync_token().await.unwrap(), None); + self.save_changes(&changes).await.unwrap(); + assert_eq!(self.get_sync_token().await.unwrap(), Some(sync_token)); + } + + async fn test_stripped_member_saving(&self) { + let room_id = room_id!("!test_stripped_member_saving:localhost"); + let user_id = user_id(); + + assert!(self.get_member_event(room_id, user_id).await.unwrap().is_none()); + let mut changes = StateChanges::default(); + changes + .stripped_members + .entry(room_id.to_owned()) + .or_default() + .insert(user_id.to_owned(), stripped_membership_event()); + + self.save_changes(&changes).await.unwrap(); + assert!(self.get_member_event(room_id, user_id).await.unwrap().is_some()); + + let members = self.get_user_ids(room_id).await.unwrap(); + assert!(!members.is_empty(), "We expected to find members for the room") + } + + async fn test_power_level_saving(&self) { + let room_id = room_id!("!test_power_level_saving:localhost"); + + let raw_event = power_level_event(); + let event = raw_event.deserialize().unwrap(); + + assert!(self + .get_state_event(room_id, StateEventType::RoomPowerLevels, "") + .await + .unwrap() + .is_none()); + let mut changes = StateChanges::default(); + changes.add_state_event(room_id, event, raw_event); + + self.save_changes(&changes).await.unwrap(); + assert!(self + .get_state_event(room_id, StateEventType::RoomPowerLevels, "") + .await + .unwrap() + .is_some()); + } + + async fn test_receipts_saving(&self) { + let room_id = room_id!("!test_receipts_saving:localhost"); + + let first_event_id = event_id!("$1435641916114394fHBLK:matrix.org"); + let second_event_id = event_id!("$fHBLK1435641916114394:matrix.org"); + + let first_receipt_ts = uint!(1436451550); + let second_receipt_ts = uint!(1436451653); + let third_receipt_ts = uint!(1436474532); + + let first_receipt_event = serde_json::from_value(json!({ + first_event_id: { + "m.read": { + user_id(): { + "ts": first_receipt_ts, + } + } + } + })) + .expect("json creation failed"); + + let second_receipt_event = serde_json::from_value(json!({ + second_event_id: { + "m.read": { + user_id(): { + "ts": second_receipt_ts, + } + } + } + })) + .expect("json creation failed"); + + let third_receipt_event = serde_json::from_value(json!({ + second_event_id: { + "m.read": { + user_id(): { + "ts": third_receipt_ts, + "thread_id": "main", + } + } + } + })) + .expect("json creation failed"); + + assert!(self + .get_user_room_receipt_event( + room_id, + ReceiptType::Read, + ReceiptThread::Unthreaded, + user_id() + ) + .await + .expect("failed to read unthreaded user room receipt") + .is_none()); + assert!(self + .get_event_room_receipt_events( + room_id, + ReceiptType::Read, + ReceiptThread::Unthreaded, + first_event_id + ) + .await + .expect("failed to read unthreaded event room receipt for 1") + .is_empty()); + assert!(self + .get_event_room_receipt_events( + room_id, + ReceiptType::Read, + ReceiptThread::Unthreaded, + second_event_id + ) + .await + .expect("failed to read unthreaded event room receipt for 2") + .is_empty()); + + let mut changes = StateChanges::default(); + changes.add_receipts(room_id, first_receipt_event); + + self.save_changes(&changes).await.expect("writing changes fauked"); + let (unthreaded_user_receipt_event_id, unthreaded_user_receipt) = self + .get_user_room_receipt_event( + room_id, + ReceiptType::Read, + ReceiptThread::Unthreaded, + user_id(), + ) + .await + .expect("failed to read unthreaded user room receipt after save") + .unwrap(); + assert_eq!(unthreaded_user_receipt_event_id, first_event_id); + assert_eq!(unthreaded_user_receipt.ts.unwrap().0, first_receipt_ts); + let first_event_unthreaded_receipts = self + .get_event_room_receipt_events( + room_id, + ReceiptType::Read, + ReceiptThread::Unthreaded, + first_event_id, + ) + .await + .expect("failed to read unthreaded event room receipt for 1 after save"); + assert_eq!( + first_event_unthreaded_receipts.len(), + 1, + "Found a wrong number of unthreaded receipts for 1 after save" + ); + assert_eq!(first_event_unthreaded_receipts[0].0, user_id()); + assert_eq!(first_event_unthreaded_receipts[0].1.ts.unwrap().0, first_receipt_ts); + assert!(self + .get_event_room_receipt_events( + room_id, + ReceiptType::Read, + ReceiptThread::Unthreaded, + second_event_id + ) + .await + .expect("failed to read unthreaded event room receipt for 2 after save") + .is_empty()); + + let mut changes = StateChanges::default(); + changes.add_receipts(room_id, second_receipt_event); + + self.save_changes(&changes).await.expect("Saving works"); + let (unthreaded_user_receipt_event_id, unthreaded_user_receipt) = self + .get_user_room_receipt_event( + room_id, + ReceiptType::Read, + ReceiptThread::Unthreaded, + user_id(), + ) + .await + .expect("Getting unthreaded user room receipt after save failed") + .unwrap(); + assert_eq!(unthreaded_user_receipt_event_id, second_event_id); + assert_eq!(unthreaded_user_receipt.ts.unwrap().0, second_receipt_ts); + assert!(self + .get_event_room_receipt_events( + room_id, + ReceiptType::Read, + ReceiptThread::Unthreaded, + first_event_id + ) + .await + .expect("Getting unthreaded event room receipt events for first event failed") + .is_empty()); + let second_event_unthreaded_receipts = self + .get_event_room_receipt_events( + room_id, + ReceiptType::Read, + ReceiptThread::Unthreaded, + second_event_id, + ) + .await + .expect("Getting unthreaded event room receipt events for second event failed"); + assert_eq!( + second_event_unthreaded_receipts.len(), + 1, + "Found a wrong number of unthreaded receipts for second event after save" + ); + assert_eq!(second_event_unthreaded_receipts[0].0, user_id()); + assert_eq!(second_event_unthreaded_receipts[0].1.ts.unwrap().0, second_receipt_ts); + + assert!(self + .get_user_room_receipt_event(room_id, ReceiptType::Read, ReceiptThread::Main, user_id()) + .await + .expect("failed to read threaded user room receipt") + .is_none()); + assert!(self + .get_event_room_receipt_events( + room_id, + ReceiptType::Read, + ReceiptThread::Main, + second_event_id + ) + .await + .expect("Getting threaded event room receipts for 2 failed") + .is_empty()); + + let mut changes = StateChanges::default(); + changes.add_receipts(room_id, third_receipt_event); + + self.save_changes(&changes).await.expect("Saving works"); + // Unthreaded receipts should not have changed. + let (unthreaded_user_receipt_event_id, unthreaded_user_receipt) = self + .get_user_room_receipt_event( + room_id, + ReceiptType::Read, + ReceiptThread::Unthreaded, + user_id(), + ) + .await + .expect("Getting unthreaded user room receipt after save failed") + .unwrap(); + assert_eq!(unthreaded_user_receipt_event_id, second_event_id); + assert_eq!(unthreaded_user_receipt.ts.unwrap().0, second_receipt_ts); + let second_event_unthreaded_receipts = self + .get_event_room_receipt_events( + room_id, + ReceiptType::Read, + ReceiptThread::Unthreaded, + second_event_id, + ) + .await + .expect("Getting unthreaded event room receipt events for second event failed"); + assert_eq!( + second_event_unthreaded_receipts.len(), + 1, + "Found a wrong number of unthreaded receipts for second event after save" + ); + assert_eq!(second_event_unthreaded_receipts[0].0, user_id()); + assert_eq!(second_event_unthreaded_receipts[0].1.ts.unwrap().0, second_receipt_ts); + // Threaded receipts should have changed + let (threaded_user_receipt_event_id, threaded_user_receipt) = self + .get_user_room_receipt_event(room_id, ReceiptType::Read, ReceiptThread::Main, user_id()) + .await + .expect("Getting threaded user room receipt after save failed") + .unwrap(); + assert_eq!(threaded_user_receipt_event_id, second_event_id); + assert_eq!(threaded_user_receipt.ts.unwrap().0, third_receipt_ts); + let second_event_threaded_receipts = self + .get_event_room_receipt_events( + room_id, + ReceiptType::Read, + ReceiptThread::Main, + second_event_id, + ) + .await + .expect("Getting threaded event room receipt events for second event failed"); + assert_eq!( + second_event_threaded_receipts.len(), + 1, + "Found a wrong number of threaded receipts for second event after save" + ); + assert_eq!(second_event_threaded_receipts[0].0, user_id()); + assert_eq!(second_event_threaded_receipts[0].1.ts.unwrap().0, third_receipt_ts); + } + + async fn test_custom_storage(&self) -> Result<()> { + let key = "my_key"; + let value = &[0, 1, 2, 3]; + + self.set_custom_value(key.as_bytes(), value.to_vec()).await?; + + let read = self.get_custom_value(key.as_bytes()).await?; + + assert_eq!(Some(value.as_ref()), read.as_deref()); + + Ok(()) + } + + async fn test_persist_invited_room(&self) -> Result<()> { + self.populate().await?; + + assert_eq!(self.get_stripped_room_infos().await?.len(), 1); + + Ok(()) + } + + async fn test_stripped_non_stripped(&self) -> Result<()> { + let room_id = room_id!("!test_stripped_non_stripped:localhost"); + let user_id = user_id(); + + assert!(self.get_member_event(room_id, user_id).await.unwrap().is_none()); + assert_eq!(self.get_room_infos().await.unwrap().len(), 0); + assert_eq!(self.get_stripped_room_infos().await.unwrap().len(), 0); + + let mut changes = StateChanges::default(); + changes + .members + .entry(room_id.to_owned()) + .or_default() + .insert(user_id.to_owned(), membership_event()); + changes.add_room(RoomInfo::new(room_id, RoomType::Left)); + self.save_changes(&changes).await.unwrap(); + + let member_event = + self.get_member_event(room_id, user_id).await.unwrap().unwrap().deserialize().unwrap(); + assert!(matches!(member_event, MemberEvent::Sync(_))); + assert_eq!(self.get_room_infos().await.unwrap().len(), 1); + assert_eq!(self.get_stripped_room_infos().await.unwrap().len(), 0); + + let members = self.get_user_ids(room_id).await.unwrap(); + assert_eq!(members, vec![user_id.to_owned()]); + + let mut changes = StateChanges::default(); + changes.add_stripped_member(room_id, user_id, custom_stripped_membership_event(user_id)); + changes.add_stripped_room(RoomInfo::new(room_id, RoomType::Invited)); + self.save_changes(&changes).await.unwrap(); + + let member_event = + self.get_member_event(room_id, user_id).await.unwrap().unwrap().deserialize().unwrap(); + assert!(matches!(member_event, MemberEvent::Stripped(_))); + assert_eq!(self.get_room_infos().await.unwrap().len(), 0); + assert_eq!(self.get_stripped_room_infos().await.unwrap().len(), 1); + + let members = self.get_user_ids(room_id).await.unwrap(); + assert_eq!(members, vec![user_id.to_owned()]); + + Ok(()) + } + + async fn test_room_removal(&self) -> Result<()> { + let room_id = room_id(); + let user_id = user_id(); + let stripped_room_id = stripped_room_id(); + + self.populate().await?; + + self.remove_room(room_id).await?; + + assert!(self.get_room_infos().await?.is_empty(), "room is still there"); + assert_eq!(self.get_stripped_room_infos().await?.len(), 1); + + assert!(self.get_state_event(room_id, StateEventType::RoomName, "").await?.is_none()); + assert!( + self.get_state_events(room_id, StateEventType::RoomTopic).await?.is_empty(), + "still state events found" + ); + assert!(self.get_profile(room_id, user_id).await?.is_none()); + assert!(self.get_member_event(room_id, user_id).await?.is_none()); + assert!(self.get_user_ids(room_id).await?.is_empty(), "still user ids found"); + assert!( + self.get_invited_user_ids(room_id).await?.is_empty(), + "still invited user ids found" + ); + assert!(self.get_joined_user_ids(room_id).await?.is_empty(), "still joined users found"); + assert!( + self.get_users_with_display_name(room_id, "example").await?.is_empty(), + "still display names found" + ); + assert!(self + .get_room_account_data_event(room_id, RoomAccountDataEventType::Tag) + .await? + .is_none()); + assert!(self + .get_user_room_receipt_event( + room_id, + ReceiptType::Read, + ReceiptThread::Unthreaded, + user_id + ) + .await? + .is_none()); + assert!( + self.get_event_room_receipt_events( + room_id, + ReceiptType::Read, + ReceiptThread::Unthreaded, + first_receipt_event_id() + ) + .await? + .is_empty(), + "still event recepts in the store" + ); + + self.remove_room(stripped_room_id).await?; + + assert!(self.get_room_infos().await?.is_empty(), "still room info found"); + assert!(self.get_stripped_room_infos().await?.is_empty(), "still stripped room info found"); + Ok(()) + } +} /// Macro building to allow your StateStore implementation to run the entire /// tests suite locally. @@ -32,89 +853,10 @@ macro_rules! statestore_integration_tests { mod statestore_integration_tests { $crate::statestore_integration_tests!(@inner); - use ruma::{ - api::client::media::get_content_thumbnail::v3::Method, - events::room::MediaSource, - mxc_uri, - }; - - use $crate::media::{MediaFormat, MediaRequest, MediaThumbnailSize}; - #[async_test] async fn test_media_content() { - let store = get_store().await.unwrap(); - - let uri = mxc_uri!("mxc://localhost/media"); - let content: Vec = "somebinarydata".into(); - - let request_file = MediaRequest { - source: MediaSource::Plain(uri.to_owned()), - format: MediaFormat::File, - }; - - let request_thumbnail = MediaRequest { - source: MediaSource::Plain(uri.to_owned()), - format: MediaFormat::Thumbnail(MediaThumbnailSize { - method: Method::Crop, - width: uint!(100), - height: uint!(100), - }), - }; - - assert!( - store.get_media_content(&request_file).await.unwrap().is_none(), - "unexpected media found" - ); - assert!( - store.get_media_content(&request_thumbnail).await.unwrap().is_none(), - "media not found" - ); - - store - .add_media_content(&request_file, content.clone()) - .await - .expect("adding media failed"); - assert!( - store.get_media_content(&request_file).await.unwrap().is_some(), - "media not found though added" - ); - - store.remove_media_content(&request_file).await.expect("removing media failed"); - assert!( - store.get_media_content(&request_file).await.unwrap().is_none(), - "media still there after removing" - ); - - store - .add_media_content(&request_file, content.clone()) - .await - .expect("adding media again failed"); - assert!( - store.get_media_content(&request_file).await.unwrap().is_some(), - "media not found after adding again" - ); - - store - .add_media_content(&request_thumbnail, content.clone()) - .await - .expect("adding thumbnail failed"); - assert!( - store.get_media_content(&request_thumbnail).await.unwrap().is_some(), - "thumbnail not found" - ); - - store - .remove_media_content_for_uri(uri) - .await - .expect("removing all media for uri failed"); - assert!( - store.get_media_content(&request_file).await.unwrap().is_none(), - "media wasn't removed" - ); - assert!( - store.get_media_content(&request_thumbnail).await.unwrap().is_none(), - "thumbnail wasn't removed" - ); + let store = get_store().await.unwrap().into_state_store(); + store.test_media_content().await; } } }; @@ -124,877 +866,149 @@ macro_rules! statestore_integration_tests { } }; (@inner) => { - use std::{ - collections::{BTreeMap, BTreeSet}, - sync::Arc, - }; + use matrix_sdk_test::async_test; - use matrix_sdk_test::{async_test, test_json}; - use ruma::{ - event_id, - events::{ - presence::PresenceEvent, - receipt::{ReceiptType, ReceiptThread}, - room::{ - member::{ - MembershipState, RoomMemberEventContent, StrippedRoomMemberEvent, - SyncRoomMemberEvent, - }, - power_levels::RoomPowerLevelsEventContent, - topic::{RoomTopicEventContent, OriginalRoomTopicEvent, RedactedRoomTopicEvent}, - }, - AnyEphemeralRoomEventContent, AnyGlobalAccountDataEvent, - AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncEphemeralRoomEvent, - AnySyncStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType, - StateEventType, - }, - room_id, - serde::Raw, - uint, user_id, EventId, OwnedEventId, RoomId, UserId, - }; - use serde_json::{json, Value as JsonValue}; - - use $crate::{ - store::{ - DynStateStore, IntoStateStore, Result as StoreResult, StateChanges, StateStore, - StateStoreExt - }, - RoomInfo, RoomType, - }; + use $crate::store::{IntoStateStore, Result as StoreResult, StateStoreIntegrationTests}; use super::get_store; - fn user_id() -> &'static UserId { - user_id!("@example:localhost") - } - pub(crate) fn invited_user_id() -> &'static UserId { - user_id!("@invited:localhost") - } - - pub(crate) fn room_id() -> &'static RoomId { - room_id!("!test:localhost") - } - - pub(crate) fn stripped_room_id() -> &'static RoomId { - room_id!("!stripped:localhost") - } - - pub(crate) fn first_receipt_event_id() -> &'static EventId { - event_id!("$example") - } - - /// Populate the given `StateStore`. - pub async fn populate_store(store: Arc) -> StoreResult<()> { - let mut changes = StateChanges::default(); - - let user_id = user_id(); - let invited_user_id = invited_user_id(); - let room_id = room_id(); - let stripped_room_id = stripped_room_id(); - - changes.sync_token = Some("t392-516_47314_0_7_1_1_1_11444_1".to_owned()); - - let presence_json: &JsonValue = &test_json::PRESENCE; - let presence_raw = - serde_json::from_value::>(presence_json.clone()).unwrap(); - let presence_event = presence_raw.deserialize().unwrap(); - changes.add_presence_event(presence_event, presence_raw); - - let pushrules_json: &JsonValue = &test_json::PUSH_RULES; - let pushrules_raw = serde_json::from_value::>( - pushrules_json.clone(), - ) - .unwrap(); - let pushrules_event = pushrules_raw.deserialize().unwrap(); - changes.add_account_data(pushrules_event, pushrules_raw); - - let mut room = RoomInfo::new(room_id, RoomType::Joined); - room.mark_as_left(); - - let tag_json: &JsonValue = &test_json::TAG; - let tag_raw = - serde_json::from_value::>(tag_json.clone()) - .unwrap(); - let tag_event = tag_raw.deserialize().unwrap(); - changes.add_room_account_data(room_id, tag_event, tag_raw); - - let name_json: &JsonValue = &test_json::NAME; - let name_raw = - serde_json::from_value::>(name_json.clone()).unwrap(); - let name_event = name_raw.deserialize().unwrap(); - room.handle_state_event(&name_event); - changes.add_state_event(room_id, name_event, name_raw); - - let topic_json: &JsonValue = &test_json::TOPIC; - let topic_raw = - serde_json::from_value::>(topic_json.clone()).expect("can create sync-state-event for topic"); - let topic_event = topic_raw.deserialize().expect("can deserialize raw topic"); - room.handle_state_event(&topic_event); - changes.add_state_event(room_id, topic_event, topic_raw); - - let mut room_ambiguity_map = BTreeMap::new(); - let mut room_profiles = BTreeMap::new(); - let mut room_members = BTreeMap::new(); - - let member_json: &JsonValue = &test_json::MEMBER; - let member_event: SyncRoomMemberEvent = - serde_json::from_value(member_json.clone()).unwrap(); - let displayname = - member_event.as_original().unwrap().content.displayname.clone().unwrap(); - room_ambiguity_map - .insert(displayname.clone(), BTreeSet::from([user_id.to_owned()])); - room_profiles.insert(user_id.to_owned(), (&member_event).into()); - room_members.insert(user_id.to_owned(), Raw::new(&member_json).unwrap().cast()); - - let member_state_raw = - serde_json::from_value::>(member_json.clone()).unwrap(); - let member_state_event = member_state_raw.deserialize().unwrap(); - changes.add_state_event(room_id, member_state_event, member_state_raw); - - let invited_member_json: &JsonValue = &test_json::MEMBER_INVITE; - // FIXME: Should be stripped room member event - let invited_member_event: SyncRoomMemberEvent = - serde_json::from_value(invited_member_json.clone()).unwrap(); - room_ambiguity_map - .entry(displayname) - .or_default() - .insert(invited_user_id.to_owned()); - room_profiles.insert(invited_user_id.to_owned(), (&invited_member_event).into()); - room_members.insert( - invited_user_id.to_owned(), - Raw::new(&invited_member_json).unwrap().cast(), - ); - - let invited_member_state_raw = - serde_json::from_value::>(invited_member_json.clone()) - .unwrap(); - let invited_member_state_event = invited_member_state_raw.deserialize().unwrap(); - changes.add_state_event( - room_id, - invited_member_state_event, - invited_member_state_raw, - ); - - let receipt_json: &JsonValue = &test_json::READ_RECEIPT; - let receipt_event = - serde_json::from_value::(receipt_json.clone()) - .unwrap(); - let receipt_content = match receipt_event.content() { - AnyEphemeralRoomEventContent::Receipt(content) => content, - _ => panic!(), - }; - changes.add_receipts(room_id, receipt_content); - - changes.ambiguity_maps.insert(room_id.to_owned(), room_ambiguity_map); - changes.profiles.insert(room_id.to_owned(), room_profiles); - changes.members.insert(room_id.to_owned(), room_members); - changes.add_room(room); - - let mut stripped_room = RoomInfo::new(stripped_room_id, RoomType::Invited); - - let stripped_name_json: &JsonValue = &test_json::NAME_STRIPPED; - let stripped_name_raw = serde_json::from_value::>( - stripped_name_json.clone(), - ) - .unwrap(); - let stripped_name_event = stripped_name_raw.deserialize().unwrap(); - stripped_room.handle_stripped_state_event(&stripped_name_event); - changes.stripped_state.insert( - stripped_room_id.to_owned(), - BTreeMap::from([( - stripped_name_event.event_type(), - BTreeMap::from([( - stripped_name_event.state_key().to_owned(), - stripped_name_raw.clone(), - )]), - )]), - ); - - changes.add_stripped_room(stripped_room); - - let stripped_member_json: &JsonValue = &test_json::MEMBER_STRIPPED; - let stripped_member_event = Raw::new(&stripped_member_json.clone()).unwrap().cast(); - changes.add_stripped_member(stripped_room_id, user_id, stripped_member_event); - - store.save_changes(&changes).await?; - Ok(()) - } - - fn power_level_event() -> Raw { - let content = RoomPowerLevelsEventContent::default(); - - let event = json!({ - "event_id": "$h29iv0s8:example.com", - "content": content, - "sender": user_id(), - "type": "m.room.power_levels", - "origin_server_ts": 0u64, - "state_key": "", - }); - - serde_json::from_value(event).unwrap() - } - - fn stripped_membership_event() -> Raw { - custom_stripped_membership_event(user_id()) - } - - fn custom_stripped_membership_event(user_id: &UserId) -> Raw { - let ev_json = json!({ - "type": "m.room.member", - "content": RoomMemberEventContent::new(MembershipState::Join), - "sender": user_id, - "state_key": user_id, - }); - - Raw::new(&ev_json).unwrap().cast() - } - - fn membership_event() -> Raw { - custom_membership_event(user_id(), event_id!("$h29iv0s8:example.com").to_owned()) - } - - fn custom_membership_event( - user_id: &UserId, - event_id: OwnedEventId, - ) -> Raw { - let ev_json = json!({ - "type": "m.room.member", - "content": RoomMemberEventContent::new(MembershipState::Join), - "event_id": event_id, - "origin_server_ts": 198, - "sender": user_id, - "state_key": user_id, - }); - - Raw::new(&ev_json).unwrap().cast() - } - #[async_test] async fn test_topic_redaction() -> StoreResult<()> { - let room_id = room_id(); - let inner_store = get_store().await?; - - let store = inner_store.into_state_store(); - populate_store(store.clone()).await?; - - assert!(store.get_sync_token().await?.is_some()); - assert_eq!( - store - .get_state_event_static::(room_id) - .await? - .expect("room topic found before redaction") - .deserialize_as::() - .expect("can deserialize room topic before redaction") - .content - .topic, - "😀" - ); - - let mut changes = StateChanges::default(); - - let redaction_json: &JsonValue = &test_json::TOPIC_REDACTION; - let redaction_evt: Raw<_> = serde_json::from_value(redaction_json.clone()).expect("topic redaction event making works"); - let redacted_event_id: OwnedEventId = redaction_evt.get_field("redacts").unwrap().unwrap(); - - changes.add_redaction(room_id, &redacted_event_id, redaction_evt); - store.save_changes(&changes).await?; - - match store - .get_state_event_static::(room_id) - .await? - .expect("room topic found before redaction") - .deserialize_as::() - { - Err(_) => { } // as expected - Ok(_) => panic!("Topic has not been redacted") - } - - let _ = store - .get_state_event_static::(room_id) - .await? - .expect("room topic found after redaction") - .deserialize_as::() - .expect("can deserialize room topic after redaction"); - - Ok(()) + let store = get_store().await?.into_state_store(); + store.test_topic_redaction().await } #[async_test] async fn test_populate_store() -> StoreResult<()> { - let room_id = room_id(); - let user_id = user_id(); - let inner_store = get_store().await?; - - let store = inner_store.into_state_store(); - populate_store(store.clone()).await?; - - assert!(store.get_sync_token().await?.is_some()); - assert!(store.get_presence_event(user_id).await?.is_some()); - assert_eq!(store.get_room_infos().await?.len(), 1, "Expected to find 1 room info"); - assert_eq!( - store.get_stripped_room_infos().await?.len(), - 1, - "Expected to find 1 stripped room info" - ); - assert!(store - .get_account_data_event(GlobalAccountDataEventType::PushRules) - .await? - .is_some()); - - assert!(store - .get_state_event(room_id, StateEventType::RoomName, "") - .await? - .is_some()); - assert_eq!( - store.get_state_events(room_id, StateEventType::RoomTopic).await?.len(), - 1, - "Expected to find 1 room topic" - ); - assert!(store.get_profile(room_id, user_id).await?.is_some()); - assert!(store.get_member_event(room_id, user_id).await?.is_some()); - assert_eq!( - store.get_user_ids(room_id).await?.len(), - 2, - "Expected to find 2 members for room" - ); - assert_eq!( - store.get_invited_user_ids(room_id).await?.len(), - 1, - "Expected to find 1 invited user ids" - ); - assert_eq!( - store.get_joined_user_ids(room_id).await?.len(), - 1, - "Expected to find 1 joined user ids" - ); - assert_eq!( - store.get_users_with_display_name(room_id, "example").await?.len(), - 2, - "Expected to find 2 display names for room" - ); - assert!(store - .get_room_account_data_event(room_id, RoomAccountDataEventType::Tag) - .await? - .is_some()); - assert!(store - .get_user_room_receipt_event( - room_id, - ReceiptType::Read, - ReceiptThread::Unthreaded, - user_id - ) - .await? - .is_some()); - assert_eq!( - store - .get_event_room_receipt_events( - room_id, - ReceiptType::Read, - ReceiptThread::Unthreaded, - first_receipt_event_id() - ) - .await? - .len(), - 1, - "Expected to find 1 read receipt" - ); - Ok(()) + let store = get_store().await?.into_state_store(); + store.test_populate_store().await } #[async_test] async fn test_member_saving() { - let store = get_store().await.unwrap(); - let room_id = room_id!("!test_member_saving:localhost"); - let user_id = user_id(); - - assert!(store.get_member_event(room_id, user_id).await.unwrap().is_none()); - let mut changes = StateChanges::default(); - changes - .members - .entry(room_id.to_owned()) - .or_default() - .insert(user_id.to_owned(), membership_event()); - - store.save_changes(&changes).await.unwrap(); - assert!(store.get_member_event(room_id, user_id).await.unwrap().is_some()); - - let members = store.get_user_ids(room_id).await.unwrap(); - assert!(!members.is_empty(), "We expected to find members for the room") + let store = get_store().await.unwrap().into_state_store(); + store.test_member_saving().await } #[async_test] async fn test_filter_saving() { - let store = get_store().await.unwrap(); - let test_name = "filter_name"; - let filter_id = "filter_id_1234"; - assert_eq!(store.get_filter(test_name).await.unwrap(), None); - store.save_filter(test_name, filter_id).await.unwrap(); - assert_eq!(store.get_filter(test_name).await.unwrap(), Some(filter_id.to_owned())); + let store = get_store().await.unwrap().into_state_store(); + store.test_filter_saving().await } #[async_test] async fn test_sync_token_saving() { - let mut changes = StateChanges::default(); - let store = get_store().await.unwrap(); - let sync_token = "t392-516_47314_0_7_1".to_owned(); - - changes.sync_token = Some(sync_token.clone()); - assert_eq!(store.get_sync_token().await.unwrap(), None); - store.save_changes(&changes).await.unwrap(); - assert_eq!(store.get_sync_token().await.unwrap(), Some(sync_token)); + let store = get_store().await.unwrap().into_state_store(); + store.test_sync_token_saving().await } #[async_test] async fn test_stripped_member_saving() { - let store = get_store().await.unwrap(); - let room_id = room_id!("!test_stripped_member_saving:localhost"); - let user_id = user_id(); - - assert!(store.get_member_event(room_id, user_id).await.unwrap().is_none()); - let mut changes = StateChanges::default(); - changes - .stripped_members - .entry(room_id.to_owned()) - .or_default() - .insert(user_id.to_owned(), stripped_membership_event()); - - store.save_changes(&changes).await.unwrap(); - assert!(store.get_member_event(room_id, user_id).await.unwrap().is_some()); - - let members = store.get_user_ids(room_id).await.unwrap(); - assert!(!members.is_empty(), "We expected to find members for the room") + let store = get_store().await.unwrap().into_state_store(); + store.test_stripped_member_saving().await } #[async_test] async fn test_power_level_saving() { - let store = get_store().await.unwrap(); - let room_id = room_id!("!test_power_level_saving:localhost"); - - let raw_event = power_level_event(); - let event = raw_event.deserialize().unwrap(); - - assert!(store - .get_state_event(room_id, StateEventType::RoomPowerLevels, "") - .await - .unwrap() - .is_none()); - let mut changes = StateChanges::default(); - changes.add_state_event(room_id, event, raw_event); - - store.save_changes(&changes).await.unwrap(); - assert!(store - .get_state_event(room_id, StateEventType::RoomPowerLevels, "") - .await - .unwrap() - .is_some()); + let store = get_store().await.unwrap().into_state_store(); + store.test_power_level_saving().await } #[async_test] async fn test_receipts_saving() { - let store = get_store().await.expect("creating store failed"); - - let room_id = room_id!("!test_receipts_saving:localhost"); - - let first_event_id = event_id!("$1435641916114394fHBLK:matrix.org"); - let second_event_id = event_id!("$fHBLK1435641916114394:matrix.org"); - - let first_receipt_ts = uint!(1436451550); - let second_receipt_ts = uint!(1436451653); - let third_receipt_ts = uint!(1436474532); - - let first_receipt_event = serde_json::from_value(json!({ - first_event_id: { - "m.read": { - user_id(): { - "ts": first_receipt_ts, - } - } - } - })) - .expect("json creation failed"); - - let second_receipt_event = serde_json::from_value(json!({ - second_event_id: { - "m.read": { - user_id(): { - "ts": second_receipt_ts, - } - } - } - })) - .expect("json creation failed"); - - let third_receipt_event = serde_json::from_value(json!({ - second_event_id: { - "m.read": { - user_id(): { - "ts": third_receipt_ts, - "thread_id": "main", - } - } - } - })) - .expect("json creation failed"); - - assert!(store - .get_user_room_receipt_event( - room_id, - ReceiptType::Read, - ReceiptThread::Unthreaded, - user_id() - ) - .await - .expect("failed to read unthreaded user room receipt") - .is_none()); - assert!(store - .get_event_room_receipt_events( - room_id, - ReceiptType::Read, - ReceiptThread::Unthreaded, - &first_event_id - ) - .await - .expect("failed to read unthreaded event room receipt for 1") - .is_empty()); - assert!(store - .get_event_room_receipt_events( - room_id, - ReceiptType::Read, - ReceiptThread::Unthreaded, - &second_event_id - ) - .await - .expect("failed to read unthreaded event room receipt for 2") - .is_empty()); - - let mut changes = StateChanges::default(); - changes.add_receipts(room_id, first_receipt_event); - - store.save_changes(&changes).await.expect("writing changes fauked"); - let (unthreaded_user_receipt_event_id, unthreaded_user_receipt) = store - .get_user_room_receipt_event( - room_id, - ReceiptType::Read, - ReceiptThread::Unthreaded, - user_id() - ) - .await - .expect("failed to read unthreaded user room receipt after save") - .unwrap(); - assert_eq!(unthreaded_user_receipt_event_id, first_event_id); - assert_eq!(unthreaded_user_receipt.ts.unwrap().0, first_receipt_ts); - let first_event_unthreaded_receipts = store - .get_event_room_receipt_events( - room_id, - ReceiptType::Read, - ReceiptThread::Unthreaded, - &first_event_id - ) - .await - .expect("failed to read unthreaded event room receipt for 1 after save"); - assert_eq!( - first_event_unthreaded_receipts.len(), - 1, - "Found a wrong number of unthreaded receipts for 1 after save" - ); - assert_eq!(first_event_unthreaded_receipts[0].0, user_id()); - assert_eq!(first_event_unthreaded_receipts[0].1.ts.unwrap().0, first_receipt_ts); - assert!(store - .get_event_room_receipt_events( - room_id, - ReceiptType::Read, - ReceiptThread::Unthreaded, - &second_event_id - ) - .await - .expect("failed to read unthreaded event room receipt for 2 after save") - .is_empty()); - - let mut changes = StateChanges::default(); - changes.add_receipts(room_id, second_receipt_event); - - store.save_changes(&changes).await.expect("Saving works"); - let (unthreaded_user_receipt_event_id, unthreaded_user_receipt) = store - .get_user_room_receipt_event( - room_id, - ReceiptType::Read, - ReceiptThread::Unthreaded, - user_id() - ) - .await - .expect("Getting unthreaded user room receipt after save failed") - .unwrap(); - assert_eq!(unthreaded_user_receipt_event_id, second_event_id); - assert_eq!(unthreaded_user_receipt.ts.unwrap().0, second_receipt_ts); - assert!(store - .get_event_room_receipt_events( - room_id, - ReceiptType::Read, - ReceiptThread::Unthreaded, - &first_event_id - ) - .await - .expect("Getting unthreaded event room receipt events for first event failed") - .is_empty()); - let second_event_unthreaded_receipts = store - .get_event_room_receipt_events( - room_id, - ReceiptType::Read, - ReceiptThread::Unthreaded, - &second_event_id - ) - .await - .expect("Getting unthreaded event room receipt events for second event failed"); - assert_eq!( - second_event_unthreaded_receipts.len(), - 1, - "Found a wrong number of unthreaded receipts for second event after save" - ); - assert_eq!(second_event_unthreaded_receipts[0].0, user_id()); - assert_eq!(second_event_unthreaded_receipts[0].1.ts.unwrap().0, second_receipt_ts); - - assert!(store - .get_user_room_receipt_event( - room_id, - ReceiptType::Read, - ReceiptThread::Main, - user_id() - ) - .await - .expect("failed to read threaded user room receipt") - .is_none()); - assert!(store - .get_event_room_receipt_events( - room_id, - ReceiptType::Read, - ReceiptThread::Main, - &second_event_id - ) - .await - .expect("Getting threaded event room receipts for 2 failed") - .is_empty()); - - let mut changes = StateChanges::default(); - changes.add_receipts(room_id, third_receipt_event); - - store.save_changes(&changes).await.expect("Saving works"); - // Unthreaded receipts should not have changed. - let (unthreaded_user_receipt_event_id, unthreaded_user_receipt) = store - .get_user_room_receipt_event( - room_id, - ReceiptType::Read, - ReceiptThread::Unthreaded, - user_id() - ) - .await - .expect("Getting unthreaded user room receipt after save failed") - .unwrap(); - assert_eq!(unthreaded_user_receipt_event_id, second_event_id); - assert_eq!(unthreaded_user_receipt.ts.unwrap().0, second_receipt_ts); - let second_event_unthreaded_receipts = store - .get_event_room_receipt_events( - room_id, - ReceiptType::Read, - ReceiptThread::Unthreaded, - &second_event_id - ) - .await - .expect("Getting unthreaded event room receipt events for second event failed"); - assert_eq!( - second_event_unthreaded_receipts.len(), - 1, - "Found a wrong number of unthreaded receipts for second event after save" - ); - assert_eq!(second_event_unthreaded_receipts[0].0, user_id()); - assert_eq!(second_event_unthreaded_receipts[0].1.ts.unwrap().0, second_receipt_ts); - // Threaded receipts should have changed - let (threaded_user_receipt_event_id, threaded_user_receipt) = store - .get_user_room_receipt_event( - room_id, - ReceiptType::Read, - ReceiptThread::Main, - user_id() - ) - .await - .expect("Getting threaded user room receipt after save failed") - .unwrap(); - assert_eq!(threaded_user_receipt_event_id, second_event_id); - assert_eq!(threaded_user_receipt.ts.unwrap().0, third_receipt_ts); - let second_event_threaded_receipts = store - .get_event_room_receipt_events( - room_id, - ReceiptType::Read, - ReceiptThread::Main, - &second_event_id - ) - .await - .expect("Getting threaded event room receipt events for second event failed"); - assert_eq!( - second_event_threaded_receipts.len(), - 1, - "Found a wrong number of threaded receipts for second event after save" - ); - assert_eq!(second_event_threaded_receipts[0].0, user_id()); - assert_eq!(second_event_threaded_receipts[0].1.ts.unwrap().0, third_receipt_ts); + let store = get_store().await.expect("creating store failed").into_state_store(); + store.test_receipts_saving().await; } #[async_test] async fn test_custom_storage() -> StoreResult<()> { - let key = "my_key"; - let value = &[0, 1, 2, 3]; - let inner_store = get_store().await?; - let store = inner_store.into_state_store(); - - store.set_custom_value(key.as_bytes(), value.to_vec()).await?; - - let read = store.get_custom_value(key.as_bytes()).await?; - - assert_eq!(Some(value.as_ref()), read.as_deref()); - - Ok(()) + let store = get_store().await?.into_state_store(); + store.test_custom_storage().await } #[async_test] async fn test_persist_invited_room() -> StoreResult<()> { - let inner_store = get_store().await?; - let store = inner_store.into_state_store(); - populate_store(store.clone()).await?; - - assert_eq!(store.get_stripped_room_infos().await?.len(), 1); - - Ok(()) + let store = get_store().await?.into_state_store(); + store.test_persist_invited_room().await } #[async_test] async fn test_stripped_non_stripped() -> StoreResult<()> { - let store = get_store().await.unwrap(); - let room_id = room_id!("!test_stripped_non_stripped:localhost"); - let user_id = user_id(); - - assert!(store.get_member_event(room_id, user_id).await.unwrap().is_none()); - assert_eq!(store.get_room_infos().await.unwrap().len(), 0); - assert_eq!(store.get_stripped_room_infos().await.unwrap().len(), 0); - - let mut changes = StateChanges::default(); - changes - .members - .entry(room_id.to_owned()) - .or_default() - .insert(user_id.to_owned(), membership_event()); - changes.add_room(RoomInfo::new(room_id, RoomType::Left)); - store.save_changes(&changes).await.unwrap(); - - let member_event = store - .get_member_event(room_id, user_id) - .await - .unwrap() - .unwrap() - .deserialize() - .unwrap(); - assert!(matches!(member_event, $crate::deserialized_responses::MemberEvent::Sync(_))); - assert_eq!(store.get_room_infos().await.unwrap().len(), 1); - assert_eq!(store.get_stripped_room_infos().await.unwrap().len(), 0); - - let members = store.get_user_ids(room_id).await.unwrap(); - assert_eq!(members, vec![user_id.to_owned()]); - - let mut changes = StateChanges::default(); - changes.add_stripped_member(room_id, user_id, custom_stripped_membership_event(user_id)); - changes.add_stripped_room(RoomInfo::new(room_id, RoomType::Invited)); - store.save_changes(&changes).await.unwrap(); - - let member_event = store - .get_member_event(room_id, user_id) - .await - .unwrap() - .unwrap() - .deserialize() - .unwrap(); - assert!( - matches!(member_event, $crate::deserialized_responses::MemberEvent::Stripped(_)) - ); - assert_eq!(store.get_room_infos().await.unwrap().len(), 0); - assert_eq!(store.get_stripped_room_infos().await.unwrap().len(), 1); - - let members = store.get_user_ids(room_id).await.unwrap(); - assert_eq!(members, vec![user_id.to_owned()]); - - Ok(()) + let store = get_store().await.unwrap().into_state_store(); + store.test_stripped_non_stripped().await } #[async_test] async fn test_room_removal() -> StoreResult<()> { - let room_id = room_id(); - let user_id = user_id(); - let inner_store = get_store().await?; - let stripped_room_id = stripped_room_id(); - - let store = inner_store.into_state_store(); - populate_store(store.clone()).await?; - - store.remove_room(room_id).await?; - - assert!(store.get_room_infos().await?.is_empty(), "room is still there"); - assert_eq!(store.get_stripped_room_infos().await?.len(), 1); - - assert!(store - .get_state_event(room_id, StateEventType::RoomName, "") - .await? - .is_none()); - assert!( - store.get_state_events(room_id, StateEventType::RoomTopic).await?.is_empty(), - "still state events found" - ); - assert!(store.get_profile(room_id, user_id).await?.is_none()); - assert!(store.get_member_event(room_id, user_id).await?.is_none()); - assert!(store.get_user_ids(room_id).await?.is_empty(), "still user ids found"); - assert!( - store.get_invited_user_ids(room_id).await?.is_empty(), - "still invited user ids found" - ); - assert!( - store.get_joined_user_ids(room_id).await?.is_empty(), - "still joined users found" - ); - assert!( - store.get_users_with_display_name(room_id, "example").await?.is_empty(), - "still display names found" - ); - assert!(store - .get_room_account_data_event(room_id, RoomAccountDataEventType::Tag) - .await? - .is_none()); - assert!(store - .get_user_room_receipt_event( - room_id, - ReceiptType::Read, - ReceiptThread::Unthreaded, - user_id - ) - .await? - .is_none()); - assert!( - store - .get_event_room_receipt_events( - room_id, - ReceiptType::Read, - ReceiptThread::Unthreaded, - first_receipt_event_id() - ) - .await? - .is_empty(), - "still event recepts in the store" - ); - - store.remove_room(stripped_room_id).await?; - - assert!(store.get_room_infos().await?.is_empty(), "still room info found"); - assert!( - store.get_stripped_room_infos().await?.is_empty(), - "still stripped room info found" - ); - Ok(()) + let store = get_store().await?.into_state_store(); + store.test_room_removal().await } }; } + +fn user_id() -> &'static UserId { + user_id!("@example:localhost") +} + +fn invited_user_id() -> &'static UserId { + user_id!("@invited:localhost") +} + +fn room_id() -> &'static RoomId { + room_id!("!test:localhost") +} + +fn stripped_room_id() -> &'static RoomId { + room_id!("!stripped:localhost") +} + +fn first_receipt_event_id() -> &'static EventId { + event_id!("$example") +} + +fn power_level_event() -> Raw { + let content = RoomPowerLevelsEventContent::default(); + + let event = json!({ + "event_id": "$h29iv0s8:example.com", + "content": content, + "sender": user_id(), + "type": "m.room.power_levels", + "origin_server_ts": 0u64, + "state_key": "", + }); + + serde_json::from_value(event).unwrap() +} + +fn stripped_membership_event() -> Raw { + custom_stripped_membership_event(user_id()) +} + +fn custom_stripped_membership_event(user_id: &UserId) -> Raw { + let ev_json = json!({ + "type": "m.room.member", + "content": RoomMemberEventContent::new(MembershipState::Join), + "sender": user_id, + "state_key": user_id, + }); + + Raw::new(&ev_json).unwrap().cast() +} + +fn membership_event() -> Raw { + custom_membership_event(user_id(), event_id!("$h29iv0s8:example.com").to_owned()) +} + +fn custom_membership_event(user_id: &UserId, event_id: OwnedEventId) -> Raw { + let ev_json = json!({ + "type": "m.room.member", + "content": RoomMemberEventContent::new(MembershipState::Join), + "event_id": event_id, + "origin_server_ts": 198, + "sender": user_id, + "state_key": user_id, + }); + + Raw::new(&ev_json).unwrap().cast() +} diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index 60e45d1f4..2bbb93a41 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -70,6 +70,8 @@ use crate::{ pub(crate) mod ambiguity_map; mod memory_store; +#[cfg(any(test, feature = "testing"))] +pub use self::integration_tests::StateStoreIntegrationTests; pub use self::{ memory_store::MemoryStore, traits::{DynStateStore, IntoStateStore, StateStore, StateStoreExt}, From 5c3d02cb893ab17312f7e07acc2690c13ea5a3e7 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 2 Mar 2023 13:53:11 +0000 Subject: [PATCH 106/166] sliding-sync: process receipt extension response Needs some kind of tests, but this should do the trick. --- crates/matrix-sdk-base/src/sliding_sync.rs | 26 ++++++++++++---------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/crates/matrix-sdk-base/src/sliding_sync.rs b/crates/matrix-sdk-base/src/sliding_sync.rs index 5eae74639..253e6eff6 100644 --- a/crates/matrix-sdk-base/src/sliding_sync.rs +++ b/crates/matrix-sdk-base/src/sliding_sync.rs @@ -9,6 +9,7 @@ use ruma::{ v3::{self, Ephemeral}, v4, DeviceLists, }, + events::AnyEphemeralRoomEvent, DeviceKeyAlgorithm, UInt, }; use tracing::{debug, info, instrument}; @@ -52,7 +53,7 @@ impl BaseClient { return Ok(SyncResponse::default()); }; - let v4::Extensions { to_device, e2ee, account_data, .. } = extensions; + let v4::Extensions { to_device, e2ee, account_data, receipt, .. } = extensions; let to_device_events = to_device.as_ref().map(|v4| v4.events.clone()).unwrap_or_default(); @@ -160,17 +161,6 @@ impl BaseClient { Default::default() }; - // FIXME not yet supported by sliding sync. see - // https://github.com/matrix-org/matrix-rust-sdk/issues/1014 - // if let Some(event) = - // room_data.ephemeral.events.iter().find_map(|e| match e.deserialize() { - // Ok(AnySyncEphemeralRoomEvent::Receipt(event)) => Some(event.content), - // _ => None, - // }) - // { - // changes.add_receipts(&room_id, event); - // } - 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; @@ -238,6 +228,18 @@ impl BaseClient { } } + // Process receipts now we have rooms + if let Some(receipts) = receipt.as_ref() { + for (room_id, receipt_edu) in &receipts.rooms { + if let Some(content) = match receipt_edu.deserialize() { + Ok(AnyEphemeralRoomEvent::Receipt(event)) => Some(event.content), + _ => None, + } { + changes.add_receipts(&room_id, content); + } + } + } + // 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 From a86754a317a000d9f6ef2e5150f6db17d2749f68 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 2 Mar 2023 14:04:59 +0000 Subject: [PATCH 107/166] Minor tweaks to the github actions configurations (#1606) * Replace custom cancellation action with `concurrency` * Improve step names ... so don't have three steps with the same name * Bump version of checkout action checkout@v2 uses an old version of nodejs, which is deprecated. --- .github/workflows/bindings_ci.yml | 8 ++++++-- .github/workflows/cancel_others.yml | 13 ------------- .github/workflows/ci.yml | 16 ++++++++++------ .github/workflows/coverage.yml | 4 ++++ .github/workflows/documentation.yml | 4 ++++ .github/workflows/xtask.yml | 2 +- 6 files changed, 25 insertions(+), 22 deletions(-) delete mode 100644 .github/workflows/cancel_others.yml diff --git a/.github/workflows/bindings_ci.yml b/.github/workflows/bindings_ci.yml index f02eb6dd6..1c6e22fce 100644 --- a/.github/workflows/bindings_ci.yml +++ b/.github/workflows/bindings_ci.yml @@ -12,6 +12,10 @@ on: - synchronize - ready_for_review +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + env: CARGO_TERM_COLOR: always MATRIX_SDK_CRYPTO_NODEJS_PATH: bindings/matrix-sdk-crypto-nodejs @@ -29,7 +33,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v3 - name: Install Protoc uses: arduino/setup-protoc@v1 @@ -196,7 +200,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v3 # install protoc in case we end up rebuilding opentelemetry-proto - name: Install Protoc diff --git a/.github/workflows/cancel_others.yml b/.github/workflows/cancel_others.yml deleted file mode 100644 index 0f1227f06..000000000 --- a/.github/workflows/cancel_others.yml +++ /dev/null @@ -1,13 +0,0 @@ -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 dd6f282ca..643e3db68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,10 @@ on: - synchronize - ready_for_review +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + env: CARGO_TERM_COLOR: always @@ -40,7 +44,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v3 - name: Install Rust uses: dtolnay/rust-toolchain@stable @@ -145,7 +149,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v3 - name: Install Protoc uses: arduino/setup-protoc@v1 @@ -153,15 +157,15 @@ jobs: repo-token: ${{ secrets.GITHUB_TOKEN }} # Can't use `${{ matrix.* }}` inside uses - - name: Install Rust + - name: Install Rust stable if: matrix.rust == 'stable' uses: dtolnay/rust-toolchain@stable - - name: Install Rust + - name: Install Rust beta if: matrix.rust == 'beta' uses: dtolnay/rust-toolchain@beta - - name: Install Rust + - name: Install Rust nightly if: matrix.rust == 'nightly' uses: dtolnay/rust-toolchain@nightly @@ -274,7 +278,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v3 - name: Install Rust uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 70fa8f056..8680117ef 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -6,6 +6,10 @@ on: pull_request: branches: [main] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + env: CARGO_TERM_COLOR: always diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 393b190fa..b6bad5b95 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -5,6 +5,10 @@ on: branches: [main] pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: docs: name: All crates diff --git a/.github/workflows/xtask.yml b/.github/workflows/xtask.yml index c31f720d5..aa4303f6d 100644 --- a/.github/workflows/xtask.yml +++ b/.github/workflows/xtask.yml @@ -43,7 +43,7 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Calculate cache key id: cachekey From a2da63758c6f7ed22eb2d595b18934cd149e0a5d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 2 Mar 2023 14:10:53 +0000 Subject: [PATCH 108/166] Use a separate GHA cache for each matrix build (#1605) You can't update a GHA cache once you create it, so if we use the same cache for each job in a matrix build, then we end up populating it for the first job that completes, so any slower jobs don't get their dependencies cached. On the other hand, if we create 20 500MB cache items on each build, we're going to exhaust the cache storage as soon as we do a build. So, instead, let's just do the caching for the main branch, and hope that other branches can still benefit from it. --- .github/workflows/ci.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 643e3db68..6cbfa7743 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,6 +51,14 @@ jobs: - name: Load cache uses: Swatinem/rust-cache@v2 + with: + # use a separate cache for each job to work around + # https://github.com/Swatinem/rust-cache/issues/124 + key: "${{ matrix.name }}" + + # ... but only save the cache on the main branch + # cf https://github.com/Swatinem/rust-cache/issues/95 + save-if: ${{ github.ref == 'refs/head/main' }} - name: Install nextest uses: taiki-e/install-action@nextest @@ -239,6 +247,14 @@ jobs: - name: Load cache uses: Swatinem/rust-cache@v2 + with: + # use a separate cache for each job to work around + # https://github.com/Swatinem/rust-cache/issues/124 + key: "${{ matrix.cmd }}" + + # ... but only save the cache on the main branch + # cf https://github.com/Swatinem/rust-cache/issues/95 + save-if: ${{ github.ref == 'refs/head/main' }} - name: Install nextest uses: taiki-e/install-action@nextest From e261d377566700e77eb7380e531833ccbe626f09 Mon Sep 17 00:00:00 2001 From: kegsay Date: Thu, 2 Mar 2023 14:48:00 +0000 Subject: [PATCH 109/166] Apply suggestions from code review Co-authored-by: Jonas Platte --- crates/matrix-sdk-base/src/sliding_sync.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk-base/src/sliding_sync.rs b/crates/matrix-sdk-base/src/sliding_sync.rs index 253e6eff6..2439cf246 100644 --- a/crates/matrix-sdk-base/src/sliding_sync.rs +++ b/crates/matrix-sdk-base/src/sliding_sync.rs @@ -229,13 +229,10 @@ impl BaseClient { } // Process receipts now we have rooms - if let Some(receipts) = receipt.as_ref() { + if let Some(receipts) = &receipt { for (room_id, receipt_edu) in &receipts.rooms { - if let Some(content) = match receipt_edu.deserialize() { - Ok(AnyEphemeralRoomEvent::Receipt(event)) => Some(event.content), - _ => None, - } { - changes.add_receipts(&room_id, content); + if let Ok(AnyEphemeralRoomEvent::Receipt(event)) = receipt_edu.deserialize() { + changes.add_receipts(&room_id, event.content); } } } From 5881503a6ae7eb098424359a7a7426214ee5ed58 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 2 Mar 2023 14:49:16 +0000 Subject: [PATCH 110/166] Clippy --- crates/matrix-sdk-base/src/sliding_sync.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk-base/src/sliding_sync.rs b/crates/matrix-sdk-base/src/sliding_sync.rs index 2439cf246..f649dbac6 100644 --- a/crates/matrix-sdk-base/src/sliding_sync.rs +++ b/crates/matrix-sdk-base/src/sliding_sync.rs @@ -232,7 +232,7 @@ impl BaseClient { if let Some(receipts) = &receipt { for (room_id, receipt_edu) in &receipts.rooms { if let Ok(AnyEphemeralRoomEvent::Receipt(event)) = receipt_edu.deserialize() { - changes.add_receipts(&room_id, event.content); + changes.add_receipts(room_id, event.content); } } } From 0da29057f8966d07df19c1b5769a63a6c8a8244e Mon Sep 17 00:00:00 2001 From: Sam Wedgwood <28223854+swedgwood@users.noreply.github.com> Date: Fri, 3 Mar 2023 09:27:10 +0000 Subject: [PATCH 111/166] feat(sdk): Expose `prepare_encrypted_file` function Signed-off-by: Sam Wedgwood --- crates/matrix-sdk/src/encryption/mod.rs | 113 +++++++++++++++--------- 1 file changed, 70 insertions(+), 43 deletions(-) diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index c93734a4d..dea2e59aa 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -108,53 +108,46 @@ impl Client { Ok(response) } - /// Encrypt and upload the file to be read from `reader` and construct an - /// attachment message with `body`, `content_type`, `info` and `thumbnail`. + /// Construct a [`EncryptedFile`][ruma::events::room::EncryptedFile] by + /// encrypting and uploading a provided reader. + /// + /// # Arguments + /// * `content_type` - The content type of the file. + /// * `reader` - The reader that should be encrypted and uploaded. + /// + /// # Example + /// ```no_run + /// # use futures::executor::block_on; + /// # use matrix_sdk::Client; + /// # use url::Url; + /// # use matrix_sdk::ruma::{room_id, OwnedRoomId}; + /// use serde::{Deserialize, Serialize}; + /// use matrix_sdk::ruma::events::macros::EventContent; + /// + /// #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] + /// #[ruma_event(type = "com.example.custom", kind = MessageLike)] + /// struct CustomEventContent { + /// encrypted_file: matrix_sdk::ruma::events::room::EncryptedFile, + /// } + /// # block_on(async { + /// # let homeserver = Url::parse("http://example.com")?; + /// # let client = Client::new(homeserver).await?; + /// # let room = client.get_joined_room(&room_id!("!test:example.com")).unwrap(); + /// + /// let mut reader = std::io::Cursor::new(b"Hello, world!"); + /// let encrypted_file = client.prepare_encrypted_file(&mime::TEXT_PLAIN, &mut reader).await?; + /// + /// room.send(CustomEventContent { encrypted_file }, None).await?; + /// # anyhow::Ok(()) }); + /// ``` #[cfg(feature = "e2e-encryption")] - pub(crate) async fn prepare_encrypted_attachment_message( + pub async fn prepare_encrypted_file<'a, R: Read + ?Sized + 'a>( &self, - body: &str, content_type: &mime::Mime, - data: Vec, - info: Option, - thumbnail: Option, - ) -> Result { - let (thumbnail_source, thumbnail_info) = if let Some(thumbnail) = thumbnail { - let mut cursor = Cursor::new(thumbnail.data); - let mut encryptor = matrix_sdk_base::crypto::AttachmentEncryptor::new(&mut cursor); + reader: &'a mut R, + ) -> Result { + let mut encryptor = matrix_sdk_base::crypto::AttachmentEncryptor::new(reader); - let mut buf = Vec::new(); - encryptor.read_to_end(&mut buf)?; - - let response = self.media().upload(&thumbnail.content_type, buf).await?; - - let file: ruma::events::room::EncryptedFile = { - let keys = encryptor.finish(); - ruma::events::room::EncryptedFileInit { - url: response.content_uri, - key: keys.key, - iv: keys.iv, - hashes: keys.hashes, - v: keys.version, - } - .into() - }; - - use ruma::events::room::ThumbnailInfo; - - #[rustfmt::skip] - let thumbnail_info = - assign!(thumbnail.info.map(ThumbnailInfo::from).unwrap_or_default(), { - mimetype: Some(thumbnail.content_type.as_ref().to_owned()) - }); - - (Some(MediaSource::Encrypted(Box::new(file))), Some(Box::new(thumbnail_info))) - } else { - (None, None) - }; - - let mut cursor = Cursor::new(data); - let mut encryptor = matrix_sdk_base::crypto::AttachmentEncryptor::new(&mut cursor); let mut buf = Vec::new(); encryptor.read_to_end(&mut buf)?; @@ -172,6 +165,40 @@ impl Client { .into() }; + Ok(file) + } + + /// Encrypt and upload the file to be read from `reader` and construct an + /// attachment message with `body`, `content_type`, `info` and `thumbnail`. + #[cfg(feature = "e2e-encryption")] + pub(crate) async fn prepare_encrypted_attachment_message( + &self, + body: &str, + content_type: &mime::Mime, + data: Vec, + info: Option, + thumbnail: Option, + ) -> Result { + let (thumbnail_source, thumbnail_info) = if let Some(thumbnail) = thumbnail { + let mut cursor = Cursor::new(thumbnail.data); + + let file = self.prepare_encrypted_file(content_type, &mut cursor).await?; + use ruma::events::room::ThumbnailInfo; + + #[rustfmt::skip] + let thumbnail_info = + assign!(thumbnail.info.map(ThumbnailInfo::from).unwrap_or_default(), { + mimetype: Some(thumbnail.content_type.as_ref().to_owned()) + }); + + (Some(MediaSource::Encrypted(Box::new(file))), Some(Box::new(thumbnail_info))) + } else { + (None, None) + }; + + let mut cursor = Cursor::new(data); + let file = self.prepare_encrypted_file(content_type, &mut cursor).await?; + use std::io::Cursor; use ruma::events::room::{self, message, MediaSource}; From e550c16c146d5d80ab96b16bbce6b5fe1eaefc7b Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 3 Mar 2023 13:13:26 +0200 Subject: [PATCH 112/166] Broadcast UnknownToken API client errors, handle them on the FFI client side and send a `did_receive_auth_error` delegate call --- bindings/matrix-sdk-ffi/src/client.rs | 38 ++++++++++++++++++------- crates/matrix-sdk/src/client/builder.rs | 4 +++ crates/matrix-sdk/src/client/mod.rs | 35 +++++++++++++++++++++-- crates/matrix-sdk/src/lib.rs | 2 +- 4 files changed, 65 insertions(+), 14 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 29f889f34..c59116236 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -19,7 +19,7 @@ use matrix_sdk::{ }, Client as MatrixClient, Error, LoopCtrl, }; -use tokio::sync::broadcast; +use tokio::sync::broadcast::{self, error::RecvError}; use tracing::{debug, warn}; use super::{ @@ -73,14 +73,31 @@ impl Client { let (sliding_sync_reset_broadcast_tx, _) = broadcast::channel(1); - Client { + let client = Client { client, state: Arc::new(RwLock::new(state)), delegate: Arc::new(RwLock::new(None)), session_verification_controller, sliding_sync_proxy: Arc::new(RwLock::new(None)), sliding_sync_reset_broadcast_tx, - } + }; + + let mut unknown_token_error_receiver = client.subscribe_to_unknown_token_errors(); + let client_clone = client.clone(); + RUNTIME.spawn(async move { + loop { + match unknown_token_error_receiver.recv().await { + Ok(unknown_token) => client_clone.process_unknown_token_error(unknown_token), + Err(receive_error) => { + if let RecvError::Closed = receive_error { + break; + } + } + } + } + }); + + client } /// Login using a username and password. @@ -347,14 +364,6 @@ impl Client { pub(crate) fn process_sync_error(&self, sync_error: Error) -> LoopCtrl { let client_api_error_kind = sync_error.client_api_error_kind(); match client_api_error_kind { - Some(ErrorKind::UnknownToken { soft_logout }) => { - self.state.write().unwrap().is_soft_logout = *soft_logout; - if let Some(delegate) = &*self.delegate.read().unwrap() { - delegate.did_update_restore_token(); - delegate.did_receive_auth_error(*soft_logout); - } - LoopCtrl::Break - } Some(ErrorKind::UnknownPos) => { let _ = self.sliding_sync_reset_broadcast_tx.send(()); LoopCtrl::Continue @@ -365,6 +374,13 @@ impl Client { } } } + + fn process_unknown_token_error(&self, unknown_token: matrix_sdk::UnknownToken) { + if let Some(delegate) = &*self.delegate.read().unwrap() { + delegate.did_update_restore_token(); + delegate.did_receive_auth_error(unknown_token.soft_logout); + } + } } #[uniffi::export] diff --git a/crates/matrix-sdk/src/client/builder.rs b/crates/matrix-sdk/src/client/builder.rs index bf4790688..98d4e411e 100644 --- a/crates/matrix-sdk/src/client/builder.rs +++ b/crates/matrix-sdk/src/client/builder.rs @@ -27,6 +27,7 @@ use ruma::{ OwnedServerName, ServerName, }; use thiserror::Error; +use tokio::sync::broadcast; #[cfg(not(target_arch = "wasm32"))] use tokio::sync::OnceCell; use tracing::{ @@ -419,6 +420,8 @@ impl ClientBuilder { #[cfg(feature = "experimental-sliding-sync")] let sliding_sync_proxy = sliding_sync_proxy.map(RwLock::new); + let (unknown_token_error_sender, _) = broadcast::channel(1); + let inner = Arc::new(ClientInner { homeserver, authentication_issuer, @@ -441,6 +444,7 @@ impl ClientBuilder { sync_beat: event_listener::Event::new(), handle_refresh_tokens: self.handle_refresh_tokens, refresh_token_lock: Mutex::new(Ok(())), + unknown_token_error_sender, }); debug!("Done building the Client"); diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index f7ad8fc41..de4f1b6e2 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -68,6 +68,7 @@ use ruma::{ ServerName, UInt, UserId, }; use serde::de::DeserializeOwned; +use tokio::sync::broadcast; #[cfg(not(target_arch = "wasm32"))] use tokio::sync::OnceCell; #[cfg(feature = "e2e-encryption")] @@ -125,6 +126,13 @@ pub enum LoopCtrl { Break, } +/// Wrapper struct for ErrorKind::UnknownToken +#[derive(Debug, Clone)] +pub struct UnknownToken { + /// Whether or not the session was soft logged out + pub soft_logout: bool, +} + /// An async/await enabled Matrix client. /// /// All of the state is held in an `Arc` so the `Client` can be cloned freely. @@ -181,6 +189,9 @@ pub(crate) struct ClientInner { /// wait for the sync to get the data to fetch a room object from the state /// store. pub(crate) sync_beat: event_listener::Event, + /// Client API UnknownToken error publisher. Allows the subscriber logout + /// the user when any request fails because of an invalid access token + pub(crate) unknown_token_error_sender: broadcast::Sender, } #[cfg(not(tarpaulin_include))] @@ -1843,7 +1854,8 @@ impl Client { None => self.homeserver().await.to_string(), }; - self.inner + let response = self + .inner .http_client .send( request, @@ -1853,7 +1865,20 @@ impl Client { self.user_id(), self.server_versions().await?, ) - .await + .await; + + if let Err(http_error) = &response { + if let Some(ErrorKind::UnknownToken { soft_logout }) = + http_error.client_api_error_kind() + { + _ = self + .inner + .unknown_token_error_sender + .send(UnknownToken { soft_logout: *soft_logout }); + } + } + + response } async fn request_server_versions(&self) -> HttpResult> { @@ -2441,6 +2466,12 @@ impl Client { let request = logout::v3::Request::new(); self.send(request, None).await } + + /// Subscribes a new receiver to client UnknownToken errors + pub fn subscribe_to_unknown_token_errors(&self) -> broadcast::Receiver { + let broadcast = &self.inner.unknown_token_error_sender; + broadcast.subscribe() + } } // The http mocking library is not supported for wasm32 diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index ab07b473d..fa6776d32 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -49,7 +49,7 @@ mod events; pub use account::Account; #[cfg(feature = "sso-login")] pub use client::SsoLoginBuilder; -pub use client::{Client, ClientBuildError, ClientBuilder, LoginBuilder, LoopCtrl}; +pub use client::{Client, ClientBuildError, ClientBuilder, LoginBuilder, LoopCtrl, UnknownToken}; #[cfg(feature = "image-proc")] pub use error::ImageError; pub use error::{Error, HttpError, HttpResult, RefreshTokenError, Result, RumaApiError}; From d513cb9a47005d668c88e3e9750908193738f87d Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 3 Mar 2023 08:40:47 +0200 Subject: [PATCH 113/166] chore(ffi): remove now unused `start_sync` method and related properties --- bindings/matrix-sdk-ffi/src/api.udl | 1 - bindings/matrix-sdk-ffi/src/client.rs | 70 +-------------------------- bindings/matrix-sdk-ffi/src/lib.rs | 3 -- 3 files changed, 1 insertion(+), 73 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/api.udl b/bindings/matrix-sdk-ffi/src/api.udl index c8ce0483d..7af7bd7ed 100644 --- a/bindings/matrix-sdk-ffi/src/api.udl +++ b/bindings/matrix-sdk-ffi/src/api.udl @@ -9,7 +9,6 @@ interface ClientError { }; callback interface ClientDelegate { - void did_receive_sync_update(); void did_receive_auth_error(boolean is_soft_logout); void did_update_restore_token(); }; diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index c59116236..3dae4bd86 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -2,16 +2,11 @@ use std::sync::{Arc, RwLock}; 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, + account::whoami, error::ErrorKind, media::get_content_thumbnail::v3::Method, session::get_login_types, - sync::sync_events::v3::Filter, }, events::{room::MediaSource, AnyToDeviceEvent}, serde::Raw, @@ -34,7 +29,6 @@ impl std::ops::Deref for Client { } pub trait ClientDelegate: Sync + Send { - fn did_receive_sync_update(&self); fn did_receive_auth_error(&self, is_soft_logout: bool); fn did_update_restore_token(&self); } @@ -390,17 +384,6 @@ impl Client { RUNTIME.block_on(async move { self.async_homeserver().await }) } - /// Indication whether we've received a first sync response since - /// establishing the client (in memory) - pub fn has_first_synced(&self) -> bool { - self.state.read().unwrap().has_first_synced - } - - /// Indication whether we are currently syncing - pub fn is_syncing(&self) -> bool { - self.state.read().unwrap().is_syncing - } - /// Flag indicating whether the session is in soft logout mode pub fn is_soft_logout(&self) -> bool { self.state.read().unwrap().is_soft_logout @@ -409,57 +392,6 @@ impl Client { pub fn rooms(&self) -> Vec> { self.client.rooms().into_iter().map(|room| Arc::new(Room::new(room))).collect() } - - pub fn start_sync(&self, timeline_limit: Option) { - let client = self.client.clone(); - let state = self.state.clone(); - let delegate = self.delegate.clone(); - let local_self = self.clone(); - RUNTIME.spawn(async move { - let mut filter = FilterDefinition::default(); - let mut room_filter = RoomFilter::default(); - let mut event_filter = RoomEventFilter::default(); - let mut timeline_filter = RoomEventFilter::default(); - - event_filter.lazy_load_options = - LazyLoadOptions::Enabled { include_redundant_members: false }; - room_filter.state = event_filter; - filter.room = room_filter; - - timeline_filter.limit = timeline_limit.map(|limit| limit.into()); - filter.room.timeline = timeline_filter; - - let filter_id = client.get_or_upload_filter("sync", filter).await.unwrap(); - - let sync_settings = SyncSettings::new().filter(Filter::FilterId(filter_id)); - - client - .sync_with_result_callback(sync_settings, |result| async { - Ok(if result.is_ok() { - if !state.read().unwrap().has_first_synced { - state.write().unwrap().has_first_synced = true; - } - - if state.read().unwrap().should_stop_syncing { - state.write().unwrap().is_syncing = false; - return Ok(LoopCtrl::Break); - } else if !state.read().unwrap().is_syncing { - state.write().unwrap().is_syncing = true; - } - - if let Some(delegate) = &*delegate.read().unwrap() { - delegate.did_receive_sync_update() - } - - LoopCtrl::Continue - } else { - local_self.process_sync_error(result.err().unwrap()) - }) - }) - .await - .unwrap(); - }); - } } pub struct Session { diff --git a/bindings/matrix-sdk-ffi/src/lib.rs b/bindings/matrix-sdk-ffi/src/lib.rs index 002d77039..2f2201d71 100644 --- a/bindings/matrix-sdk-ffi/src/lib.rs +++ b/bindings/matrix-sdk-ffi/src/lib.rs @@ -53,9 +53,6 @@ pub use self::{ #[derive(Default, Debug)] pub struct ClientState { - has_first_synced: bool, - is_syncing: bool, - should_stop_syncing: bool, is_soft_logout: bool, } From 0dd2b12141c21f1795e55d4428e3ba730bbb04c5 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 3 Mar 2023 08:47:41 +0200 Subject: [PATCH 114/166] chore(ffi): Remove now unnecessarly stored `is_soft_logout` session and client state property. The `did_receive_auth_error(is_soft_logout)` client delegate is enough. --- bindings/matrix-sdk-ffi/src/api.udl | 2 -- bindings/matrix-sdk-ffi/src/client.rs | 22 ++----------------- bindings/matrix-sdk-ffi/src/client_builder.rs | 4 ++-- bindings/matrix-sdk-ffi/src/lib.rs | 5 ----- 4 files changed, 4 insertions(+), 29 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/api.udl b/bindings/matrix-sdk-ffi/src/api.udl index 7af7bd7ed..c9f34913b 100644 --- a/bindings/matrix-sdk-ffi/src/api.udl +++ b/bindings/matrix-sdk-ffi/src/api.udl @@ -10,7 +10,6 @@ interface ClientError { callback interface ClientDelegate { void did_receive_auth_error(boolean is_soft_logout); - void did_update_restore_token(); }; dictionary RequiredState { @@ -208,7 +207,6 @@ dictionary Session { string user_id; string device_id; string homeserver_url; - boolean is_soft_logout; string? sliding_sync_proxy; }; diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 3dae4bd86..5bc2bf738 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -17,9 +17,7 @@ use matrix_sdk::{ use tokio::sync::broadcast::{self, error::RecvError}; use tracing::{debug, warn}; -use super::{ - room::Room, session_verification::SessionVerificationController, ClientState, RUNTIME, -}; +use super::{room::Room, session_verification::SessionVerificationController, RUNTIME}; impl std::ops::Deref for Client { type Target = MatrixClient; @@ -30,13 +28,11 @@ impl std::ops::Deref for Client { pub trait ClientDelegate: Sync + Send { fn did_receive_auth_error(&self, is_soft_logout: bool); - fn did_update_restore_token(&self); } #[derive(Clone)] pub struct Client { pub(crate) client: MatrixClient, - state: Arc>, delegate: Arc>>>, session_verification_controller: Arc>>, @@ -48,7 +44,7 @@ pub struct Client { } impl Client { - pub fn new(client: MatrixClient, state: ClientState) -> Self { + pub fn new(client: MatrixClient) -> Self { let session_verification_controller: Arc< matrix_sdk::locks::RwLock>, > = Default::default(); @@ -69,7 +65,6 @@ impl Client { let client = Client { client, - state: Arc::new(RwLock::new(state)), delegate: Arc::new(RwLock::new(None)), session_verification_controller, sliding_sync_proxy: Arc::new(RwLock::new(None)), @@ -123,12 +118,9 @@ impl Client { user_id, device_id, homeserver_url: _, - is_soft_logout, sliding_sync_proxy, } = session; - // update the client state - self.state.write().unwrap().is_soft_logout = is_soft_logout; *self.sliding_sync_proxy.write().unwrap() = sliding_sync_proxy; let session = matrix_sdk::Session { @@ -195,8 +187,6 @@ impl Client { let matrix_sdk::Session { access_token, refresh_token, user_id, device_id } = self.client.session().context("Missing session")?; let homeserver_url = self.client.homeserver().await.into(); - let state = self.state.read().unwrap(); - let is_soft_logout = state.is_soft_logout; let sliding_sync_proxy = self.sliding_sync_proxy.read().unwrap().clone(); Ok(Session { @@ -205,7 +195,6 @@ impl Client { user_id: user_id.to_string(), device_id: device_id.to_string(), homeserver_url, - is_soft_logout, sliding_sync_proxy, }) }) @@ -371,7 +360,6 @@ impl Client { fn process_unknown_token_error(&self, unknown_token: matrix_sdk::UnknownToken) { if let Some(delegate) = &*self.delegate.read().unwrap() { - delegate.did_update_restore_token(); delegate.did_receive_auth_error(unknown_token.soft_logout); } } @@ -384,11 +372,6 @@ impl Client { RUNTIME.block_on(async move { self.async_homeserver().await }) } - /// Flag indicating whether the session is in soft logout mode - pub fn is_soft_logout(&self) -> bool { - self.state.read().unwrap().is_soft_logout - } - pub fn rooms(&self) -> Vec> { self.client.rooms().into_iter().map(|room| Arc::new(Room::new(room))).collect() } @@ -409,7 +392,6 @@ pub struct Session { // FFI-only fields (for now) pub homeserver_url: String, - pub is_soft_logout: bool, pub sliding_sync_proxy: Option, } diff --git a/bindings/matrix-sdk-ffi/src/client_builder.rs b/bindings/matrix-sdk-ffi/src/client_builder.rs index 6cccd0b9d..3200c7e03 100644 --- a/bindings/matrix-sdk-ffi/src/client_builder.rs +++ b/bindings/matrix-sdk-ffi/src/client_builder.rs @@ -11,7 +11,7 @@ use matrix_sdk::{ use sanitize_filename_reader_friendly::sanitize; use zeroize::Zeroizing; -use super::{client::Client, ClientState, RUNTIME}; +use super::{client::Client, RUNTIME}; use crate::helpers::unwrap_or_clone_arc; #[derive(Clone)] @@ -135,7 +135,7 @@ impl ClientBuilder { RUNTIME.block_on(async move { let client = inner_builder.build().await?; - let c = Client::new(client, ClientState::default()); + let c = Client::new(client); c.set_sliding_sync_proxy(builder.sliding_sync_proxy); Ok(Arc::new(c)) }) diff --git a/bindings/matrix-sdk-ffi/src/lib.rs b/bindings/matrix-sdk-ffi/src/lib.rs index 2f2201d71..062cd0047 100644 --- a/bindings/matrix-sdk-ffi/src/lib.rs +++ b/bindings/matrix-sdk-ffi/src/lib.rs @@ -51,11 +51,6 @@ pub use self::{ timeline::*, }; -#[derive(Default, Debug)] -pub struct ClientState { - is_soft_logout: bool, -} - #[derive(thiserror::Error, Debug)] pub enum ClientError { #[error("client error: {msg}")] From f03f7c8d1adc457e0ec248ff86fa6024110abfc2 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 6 Mar 2023 09:29:23 +0100 Subject: [PATCH 115/166] Fix a typo in the integration tests readme --- testing/matrix-sdk-integration-testing/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/matrix-sdk-integration-testing/README.md b/testing/matrix-sdk-integration-testing/README.md index 36c4c8ad7..dd572a7c1 100644 --- a/testing/matrix-sdk-integration-testing/README.md +++ b/testing/matrix-sdk-integration-testing/README.md @@ -28,7 +28,7 @@ To drop the database of your docker-compose run: ```bash docker-compose -f assets/docker-compose.yml stop -docker volume rm -f assets_marix-rust-sdk-ci-data +docker volume rm -f assets_matrix-rust-sdk-ci-data ``` or simply: From 7ec98ff72150420e674b1a97277effc03d1b9cd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 1 Mar 2023 18:17:32 +0100 Subject: [PATCH 116/166] Remove a race condition when fetching members and sending out room keys --- crates/matrix-sdk/src/client/mod.rs | 5 +- crates/matrix-sdk/src/room/common.rs | 15 +++--- crates/matrix-sdk/src/room/joined.rs | 20 +++---- crates/matrix-sdk/tests/integration/main.rs | 16 +++++- .../tests/integration/room/joined.rs | 54 ++++++++++++++++--- 5 files changed, 81 insertions(+), 29 deletions(-) diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index de4f1b6e2..1696b7f4d 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -15,6 +15,7 @@ // limitations under the License. use std::{ + collections::BTreeMap, fmt::{self, Debug}, future::Future, pin::Pin, @@ -159,11 +160,11 @@ pub(crate) struct ClientInner { /// Locks making sure we only have one group session sharing request in /// flight per room. #[cfg(feature = "e2e-encryption")] - pub(crate) group_session_locks: DashMap>>, + pub(crate) group_session_locks: Mutex>>>, /// Lock making sure we're only doing one key claim request at a time. #[cfg(feature = "e2e-encryption")] pub(crate) key_claim_lock: Mutex<()>, - pub(crate) members_request_locks: DashMap>>, + pub(crate) members_request_locks: Mutex>>>, /// Locks for requests on the encryption state of rooms. pub(crate) encryption_state_request_locks: DashMap>>, pub(crate) typing_notice_times: DashMap, diff --git a/crates/matrix-sdk/src/room/common.rs b/crates/matrix-sdk/src/room/common.rs index 9d0c19303..eeb7a27da 100644 --- a/crates/matrix-sdk/src/room/common.rs +++ b/crates/matrix-sdk/src/room/common.rs @@ -299,22 +299,21 @@ impl Common { } pub(crate) async fn request_members(&self) -> Result> { - if let Some(mutex) = - self.client.inner.members_request_locks.get(self.inner.room_id()).map(|m| m.clone()) - { + let mut map = self.client.inner.members_request_locks.lock().await; + + if let Some(mutex) = map.get(self.inner.room_id()).cloned() { // If a member request is already going on, await the release of // the lock. + drop(map); _ = mutex.lock().await; Ok(None) } else { let mutex = Arc::new(Mutex::new(())); - self.client - .inner - .members_request_locks - .insert(self.inner.room_id().to_owned(), mutex.clone()); + map.insert(self.inner.room_id().to_owned(), mutex.clone()); let _guard = mutex.lock().await; + drop(map); let request = get_member_events::v3::Request::new(self.inner.room_id().to_owned()); let response = self.client.send(request, None).await?; @@ -322,7 +321,7 @@ impl Common { let response = self.client.base_client().receive_members(self.inner.room_id(), &response).await?; - self.client.inner.members_request_locks.remove(self.inner.room_id()); + self.client.inner.members_request_locks.lock().await.remove(self.inner.room_id()); Ok(Some(response)) } diff --git a/crates/matrix-sdk/src/room/joined.rs b/crates/matrix-sdk/src/room/joined.rs index e1961db4d..b3c2af503 100644 --- a/crates/matrix-sdk/src/room/joined.rs +++ b/crates/matrix-sdk/src/room/joined.rs @@ -356,25 +356,25 @@ impl Joined { /// room if necessary and share a room key that can be shared with them. /// /// Does nothing if no room key needs to be shared. + // TODO: expose this publicly so people can pre-share a group session if + // e.g. a user starts to type a message for a room. #[cfg(feature = "e2e-encryption")] #[instrument(skip_all, fields(room_id = ?self.room_id()))] async fn preshare_room_key(&self) -> Result<()> { - // TODO: expose this publicly so people can pre-share a group session if - // e.g. a user starts to type a message for a room. - if let Some(mutex) = - self.client.inner.group_session_locks.get(self.inner.room_id()).map(|m| m.clone()) - { + let mut map = self.client.inner.group_session_locks.lock().await; + + if let Some(mutex) = map.get(self.inner.room_id()).cloned() { // If a group session share request is already going on, await the // release of the lock. + drop(map); _ = mutex.lock().await; } else { // Otherwise create a new lock and share the group // session. let mutex = Arc::new(Mutex::new(())); - self.client - .inner - .group_session_locks - .insert(self.inner.room_id().to_owned(), mutex.clone()); + map.insert(self.inner.room_id().to_owned(), mutex.clone()); + + drop(map); let _guard = mutex.lock().await; @@ -388,7 +388,7 @@ impl Joined { let response = self.share_room_key().await; - self.client.inner.group_session_locks.remove(self.inner.room_id()); + self.client.inner.group_session_locks.lock().await.remove(self.inner.room_id()); // If one of the responses failed invalidate the group // session as using it would end up in undecryptable diff --git a/crates/matrix-sdk/tests/integration/main.rs b/crates/matrix-sdk/tests/integration/main.rs index 4be34961a..2619a4c0a 100644 --- a/crates/matrix-sdk/tests/integration/main.rs +++ b/crates/matrix-sdk/tests/integration/main.rs @@ -1,7 +1,10 @@ // The http mocking library is not supported for wasm32 #![cfg(not(target_arch = "wasm32"))] -use matrix_sdk::{config::RequestConfig, Client, ClientBuilder, Session}; +use matrix_sdk::{ + config::{RequestConfig, SyncSettings}, + Client, ClientBuilder, Session, +}; use matrix_sdk_test::test_json; use ruma::{api::MatrixVersion, device_id, user_id}; use serde::Serialize; @@ -51,6 +54,17 @@ async fn logged_in_client() -> (Client, MockServer) { (client, server) } +async fn synced_client() -> (Client, MockServer) { + let (client, server) = logged_in_client().await; + mock_sync(&server, &*test_json::SYNC, None).await; + + let sync_settings = SyncSettings::new(); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + (client, server) +} + /// Mount a Mock on the given server to handle the `GET /sync` endpoint with /// an optional `since` param that returns a 200 status code with the given /// response body. diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index 6e64a4b99..9affccf8e 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -1,5 +1,6 @@ use std::time::Duration; +use futures::future::join_all; use matrix_sdk::{ attachment::{ AttachmentConfig, AttachmentInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo, @@ -21,7 +22,7 @@ use wiremock::{ Mock, ResponseTemplate, }; -use crate::{logged_in_client, mock_encryption_state, mock_sync}; +use crate::{logged_in_client, mock_encryption_state, mock_sync, synced_client}; #[async_test] async fn invite_user_by_id() { @@ -496,7 +497,7 @@ async fn room_attachment_send_info_thumbnail() { #[async_test] async fn room_redact() { - let (client, server) = logged_in_client().await; + let (client, server) = synced_client().await; Mock::given(method("PUT")) .and(path_regex(r"^/_matrix/client/r0/rooms/.*/redact/.*?/.*?")) @@ -505,12 +506,6 @@ async fn room_redact() { .mount(&server) .await; - mock_sync(&server, &*test_json::SYNC, None).await; - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - let room = client.get_joined_room(&test_json::DEFAULT_SYNC_ROOM_ID).unwrap(); let event_id = event_id!("$xxxxxxxx:example.com"); @@ -521,3 +516,46 @@ async fn room_redact() { assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) } + +#[cfg(not(target_arch = "wasm32"))] +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn fetch_members_deduplication() { + let (client, server) = synced_client().await; + + // We don't need any members, we're just checking if we're correctly + // deduplicating calls to the method. + let response_body = json!({ + "chunk": [], + }); + + Mock::given(method("GET")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/members")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(response_body)) + // Expect that we're only going to send the request out once. + .expect(1..=1) + .mount(&server) + .await; + + let room = client.get_joined_room(&test_json::DEFAULT_SYNC_ROOM_ID).unwrap(); + + let mut tasks = Vec::new(); + + // Create N tasks that try to fetch the members. + for _ in 0..5 { + let task = tokio::spawn({ + let room = room.clone(); + async move { room.sync_members().await } + }); + + tasks.push(task); + } + + // Wait on all of them at once. + let results = join_all(tasks).await; + + // See how many of them sent a request and thus have a response. + let response_count = + results.iter().filter(|r| r.as_ref().unwrap().as_ref().unwrap().is_some()).count(); + assert_eq!(response_count, 1); +} From 071ba2376eec7dc543391cac66ab45255ed96347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 1 Mar 2023 18:38:14 +0100 Subject: [PATCH 117/166] Make the majority of fields of the RoomInfo private --- crates/matrix-sdk-base/src/rooms/normal.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index ac86915df..f9e9314e8 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -515,21 +515,21 @@ pub struct RoomInfo { /// The unique room id of the room. pub(crate) room_id: Arc, /// The type of the room. - pub(crate) room_type: RoomType, + room_type: RoomType, /// The unread notifications counts. - pub(crate) notification_counts: UnreadNotificationsCount, + notification_counts: UnreadNotificationsCount, /// The summary of this room. - pub(crate) summary: RoomSummary, + summary: RoomSummary, /// Flag remembering if the room members are synced. - pub(crate) members_synced: bool, + members_synced: bool, /// The prev batch of this room we received during the last sync. pub(crate) last_prev_batch: Option, /// How much we know about this room. #[serde(default = "SyncInfo::complete")] // see fn docs for why we use this default - pub(crate) sync_info: SyncInfo, + sync_info: SyncInfo, /// Whether or not the encryption info was been synced. #[serde(default = "encryption_state_default")] // see fn docs for why we use this default - pub(crate) encryption_state_synced: bool, + encryption_state_synced: bool, /// Base room info which holds some basic event contents important for the /// room state. pub(crate) base_info: BaseRoomInfo, From 8550eaeed02e53c6703727f53d8a3e3598da037b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 1 Mar 2023 18:38:50 +0100 Subject: [PATCH 118/166] Make sure that syncing members is only happening if they aren't already synced --- crates/matrix-sdk/src/room/common.rs | 15 ++++++++------- crates/matrix-sdk/src/room/joined.rs | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/matrix-sdk/src/room/common.rs b/crates/matrix-sdk/src/room/common.rs index eeb7a27da..035fd62f2 100644 --- a/crates/matrix-sdk/src/room/common.rs +++ b/crates/matrix-sdk/src/room/common.rs @@ -391,16 +391,16 @@ impl Common { } } - pub(crate) async fn ensure_members(&self) -> Result<()> { + pub(crate) async fn ensure_members(&self) -> Result> { if !self.are_events_visible() { - return Ok(()); + return Ok(None); } if !self.are_members_synced() { - self.request_members().await?; + self.request_members().await + } else { + Ok(None) } - - Ok(()) } fn are_events_visible(&self) -> bool { @@ -417,9 +417,10 @@ impl Common { /// Sync the member list with the server. /// /// This method will de-duplicate requests if it is called multiple times in - /// quick succession, in that case the return value will be `None`. + /// quick succession, in that case the return value will be `None`. This + /// method does nothing if the members are already synced. pub async fn sync_members(&self) -> Result> { - self.request_members().await + self.ensure_members().await } /// Get active members for this room, includes invited, joined members. diff --git a/crates/matrix-sdk/src/room/joined.rs b/crates/matrix-sdk/src/room/joined.rs index b3c2af503..8fdd6d171 100644 --- a/crates/matrix-sdk/src/room/joined.rs +++ b/crates/matrix-sdk/src/room/joined.rs @@ -621,7 +621,7 @@ impl Joined { ); if !self.are_members_synced() { - self.request_members().await?; + self.ensure_members().await?; // TODO query keys here? } From d8465a2c1e3cec2caddfb1bd09c501d43f6ecd66 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 1 Mar 2023 14:51:23 +0100 Subject: [PATCH 119/166] Add a very basic changelog to crates/matrix-sdk --- crates/matrix-sdk/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 crates/matrix-sdk/CHANGELOG.md diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md new file mode 100644 index 000000000..35c8a151f --- /dev/null +++ b/crates/matrix-sdk/CHANGELOG.md @@ -0,0 +1,7 @@ +# 0.6.2 + +- Fix the access token being printed in tracing span fields. + +# 0.6.1 + +- Fixes a bug where the access token used for Matrix requests was added as a field to a tracing span. From 792b60c5017753d3f6c30ee7c844a8b957ebd627 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 1 Mar 2023 14:53:33 +0100 Subject: [PATCH 120/166] Remove most mentions of conventional commits --- CONVENTIONAL_COMMITS.md | 124 ------------------- bindings/matrix-sdk-crypto-js/cliff.toml | 61 --------- bindings/matrix-sdk-crypto-nodejs/.npmignore | 1 - bindings/matrix-sdk-crypto-nodejs/cliff.toml | 61 --------- 4 files changed, 247 deletions(-) delete mode 100644 CONVENTIONAL_COMMITS.md delete mode 100644 bindings/matrix-sdk-crypto-js/cliff.toml delete mode 100644 bindings/matrix-sdk-crypto-nodejs/cliff.toml diff --git a/CONVENTIONAL_COMMITS.md b/CONVENTIONAL_COMMITS.md deleted file mode 100644 index e08252feb..000000000 --- a/CONVENTIONAL_COMMITS.md +++ /dev/null @@ -1,124 +0,0 @@ -# Conventional Commits - -This project uses [Conventional -Commits](https://www.conventionalcommits.org/). Read the -[Summary](https://www.conventionalcommits.org/en/v1.0.0/#summary) or -the [Full -Specification](https://www.conventionalcommits.org/en/v1.0.0/#specification) -to learn more. - -## Types - -Conventional Commits defines _type_ (as in `type(scope): -message`). This section aims at listing the types used inside this -project: - -| Type | Definition | -|-|-| -| `feat` | About a new feature. | -| `fix` | About a bug fix. | -| `test` | About a test (suite, case, runner…). | -| `docs` | About a documentation modification. | -| `refactor` | About a refactoring. | -| `ci` | About a Continuous Integration modification. | -| `chore` | About some cleanup, or regular tasks. | - -## Scopes - -Conventional Commits defines _scope_ (as in `type(scope): message`). This -section aims at listing all the scopes used inside this project: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    GroupScopeDefinition
    CratessdkAbout the matrix-sdk crate.
    appserviceAbout the matrix-sdk-appservice crate.
    baseAbout the matrix-sdk-base crate.
    commonAbout the matrix-sdk-common crate.
    cryptoAbout the matrix-sdk-crypto crate.
    indexeddbAbout the matrix-sdk-indexeddb crate.
    qrcodeAbout the matrix-sdk-qrcode crate.
    sledAbout the matrix-sdk-sled crate.
    store-encryptionAbout the matrix-sdk-store-encryption crate.
    testAbout the matrix-sdk-test and matrix-sdk-test-macros crate.
    BindingsappleAbout the matrix-rust-components-swift binding.
    crypto-nodejsAbout the matrix-sdk-crypto-nodejs binding.
    crypto-jsAbout the matrix-sdk-crypto-js binding.
    crypto-ffiAbout the matrix-sdk-crypto-ffi binding.
    ffiAbout the matrix-sdk-ffi binding.
    Labsjack-inAbout the jack-in project.
    Continuous IntegrationxtaskAbout the xtask project.
    - -## Generating `CHANGELOG.md` - -The [`git-cliff`](https://github.com/orhun/git-cliff) project is used -to generate `CHANGELOG.md` automatically. Hence the various -`cliff.toml` files that are present in this project, or the -`package.metadata.git-cliff` sections in various `Cargo.toml` files. - -Its companion, -[`git-cliff-action`](https://github.com/orhun/git-cliff-action) -project, is used inside Github Action workflows. diff --git a/bindings/matrix-sdk-crypto-js/cliff.toml b/bindings/matrix-sdk-crypto-js/cliff.toml deleted file mode 100644 index 26f33b838..000000000 --- a/bindings/matrix-sdk-crypto-js/cliff.toml +++ /dev/null @@ -1,61 +0,0 @@ -# configuration file for git-cliff (0.1.0) - -[changelog] -# changelog header -header = """ -# Matrix SDK Crypto JavaScript Changelog\n -All notable changes to this project will be documented in this file.\n -""" -# template for the changelog body -# https://tera.netlify.app/docs/#introduction -body = """ -{% if version %}\ - ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} -{% else %}\ - ## [unreleased] -{% endif %}\ -{% for group, commits in commits | filter(attribute="scope", value="crypto-js") | group_by(attribute="group") %} - ### {{ group | upper_first }} - {% for commit in commits %} - - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\ - {% endfor %} -{% endfor %}\n -""" -# remove the leading and trailing whitespace from the template -trim = true -# changelog footer -footer = """ -""" - -[git] -# parse the commits based on https://www.conventionalcommits.org -conventional_commits = true -# filter out the commits that are not conventional -filter_unconventional = true -# regex for preprocessing the commit messages -commit_preprocessors = [ - { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/issues/${2}))"}, -] -# regex for parsing and grouping commits -commit_parsers = [ - { message = "^feat", group = "Features"}, - { message = "^fix", group = "Bug Fixes"}, - { message = "^test", group = "Testing"}, - { message = "^doc", group = "Documentation"}, - { message = "^refactor", group = "Refactoring"}, - { message = "^ci", group = "Continuous Integration"}, - { message = "^chore", group = "Miscellaneous Tasks"}, - { body = ".*security", group = "Security"}, -] -# filter out the commits that are not matched by commit parsers -filter_commits = false -# glob pattern for matching git tags -tag_pattern = "v[0-9]*" -# regex for skipping tags -skip_tags = "" -# regex for ignoring tags -ignore_tags = "" -# sort the tags chronologically -date_order = false -# sort the commits inside sections by oldest/newest order -sort_commits = "oldest" diff --git a/bindings/matrix-sdk-crypto-nodejs/.npmignore b/bindings/matrix-sdk-crypto-nodejs/.npmignore index 3e3d54025..cebcc362e 100644 --- a/bindings/matrix-sdk-crypto-nodejs/.npmignore +++ b/bindings/matrix-sdk-crypto-nodejs/.npmignore @@ -5,4 +5,3 @@ build.rs *.node *.tgz tsconfig.json -cliff.toml \ No newline at end of file diff --git a/bindings/matrix-sdk-crypto-nodejs/cliff.toml b/bindings/matrix-sdk-crypto-nodejs/cliff.toml deleted file mode 100644 index f2c005219..000000000 --- a/bindings/matrix-sdk-crypto-nodejs/cliff.toml +++ /dev/null @@ -1,61 +0,0 @@ -# configuration file for git-cliff (0.1.0) - -[changelog] -# changelog header -header = """ -# Matrix SDK Crypto Node.js Changelog\n -All notable changes to this project will be documented in this file.\n -""" -# template for the changelog body -# https://tera.netlify.app/docs/#introduction -body = """ -{% if version %}\ - ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} -{% else %}\ - ## [unreleased] -{% endif %}\ -{% for group, commits in commits | filter(attribute="scope", value="crypto-nodejs") | group_by(attribute="group") %} - ### {{ group | upper_first }} - {% for commit in commits %} - - {{ commit.id | truncate(length=7, end="") }}{% if commit.breaking %} [**breaking**] {% endif %}: {{ commit.message | upper_first }}\ - {% endfor %} -{% endfor %}\n -""" -# remove the leading and trailing whitespace from the template -trim = true -# changelog footer -footer = """ -""" - -[git] -# parse the commits based on https://www.conventionalcommits.org -conventional_commits = true -# filter out the commits that are not conventional -filter_unconventional = true -# regex for preprocessing the commit messages -commit_preprocessors = [ - { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/issues/${2}))"}, -] -# regex for parsing and grouping commits -commit_parsers = [ - { message = "^feat", group = "Features"}, - { message = "^fix", group = "Bug Fixes"}, - { message = "^test", group = "Testing"}, - { message = "^doc", group = "Documentation"}, - { message = "^refactor", group = "Refactoring"}, - { message = "^ci", group = "Continuous Integration"}, - { message = "^chore", group = "Miscellaneous Tasks"}, - { body = ".*security", group = "Security"}, -] -# filter out the commits that are not matched by commit parsers -filter_commits = true -# glob pattern for matching git tags -tag_pattern = "v[0-9]*" -# regex for skipping tags -skip_tags = "v0.1.0-beta.1" -# regex for ignoring tags -ignore_tags = "" -# sort the tags chronologically -date_order = false -# sort the commits inside sections by oldest/newest order -sort_commits = "oldest" From 50114f4e74370723db0ef37954995fee1fc5e185 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 1 Mar 2023 14:54:48 +0100 Subject: [PATCH 121/166] Mention changelogs in PR template --- .github/pull_request_template.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e8304a455..d49fe3fc4 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,5 +1,7 @@ +- [ ] Public API changes documented in changelogs (optional) + Signed-off-by: From 01a66d4874f255fb381391d2c74f1610598010d1 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Mon, 6 Mar 2023 17:56:04 +0100 Subject: [PATCH 122/166] Fix unnecessary cfg(all()) --- crates/matrix-sdk-common/src/timeout.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk-common/src/timeout.rs b/crates/matrix-sdk-common/src/timeout.rs index afbade986..2bc4b53fd 100644 --- a/crates/matrix-sdk-common/src/timeout.rs +++ b/crates/matrix-sdk-common/src/timeout.rs @@ -44,7 +44,7 @@ where } } -#[cfg(all(test))] +#[cfg(test)] pub(crate) mod tests { use std::{future, time::Duration}; From 79ee4b59ee35b40f68ed5fe9207be60ebf25641b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 6 Mar 2023 18:25:47 +0000 Subject: [PATCH 123/166] matrix-sdk-crypto: enable tracing for in-crate tests --- Cargo.lock | 2 ++ crates/matrix-sdk-crypto/Cargo.toml | 2 ++ crates/matrix-sdk-crypto/src/lib.rs | 11 +++++++++++ 3 files changed, 15 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index a96a5ec4c..409c9b0be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2734,6 +2734,7 @@ dependencies = [ "bs58", "byteorder", "cfg-if", + "ctor", "ctr", "dashmap", "event-listener", @@ -2758,6 +2759,7 @@ dependencies = [ "thiserror", "tokio", "tracing", + "tracing-subscriber", "vodozemac", "zeroize", ] diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index 146093242..3f3421193 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -60,6 +60,7 @@ tokio = { version = "1.24", default-features = false, features = ["time"] } [dev-dependencies] anyhow = { workspace = true } assert_matches = "1.5.0" +ctor.workspace = true futures = { version = "0.3.21", default-features = false, features = ["executor"] } http = { workspace = true } indoc = "1.0.4" @@ -67,3 +68,4 @@ matrix-sdk-test = { version = "0.6.0", path = "../../testing/matrix-sdk-test" } proptest = { version = "1.0.0", default-features = false, features = ["std"] } # required for async_test macro tokio = { version = "1.24.2", default-features = false, features = ["macros", "rt-multi-thread"] } +tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index 0d2b781ce..fb58a2dcd 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -109,3 +109,14 @@ pub mod vodozemac { /// The version of the matrix-sdk-cypto crate being used pub static VERSION: &str = env!("CARGO_PKG_VERSION"); + +// Enable tracing for tests in this crate +#[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(); +} From 2f974c11f775a4e940e5765893f2585f801b5c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 7 Mar 2023 08:48:05 +0100 Subject: [PATCH 124/166] Simplify handling of sliding sync extensions Simplify handling of sliding sync extensions This patch simplifies the logic for handling sliding sync extensions. These extensions are sticky, meaning that once enabled in one request, they do not need to be repeatedly enabled for subsequent requests. However, the previous logic tried to accommodate the case where the extensions were initially disabled to reduce the size and processing time of the initial response. This resulted in complex and error-prone code. With this patch, we have removed support for disabling to-device events and streamlined the handling of sync extensions. Co-authored-by: Jonas Platte --- crates/matrix-sdk/src/sliding_sync/builder.rs | 1 - crates/matrix-sdk/src/sliding_sync/mod.rs | 117 +++++++++++++----- 2 files changed, 89 insertions(+), 29 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/builder.rs b/crates/matrix-sdk/src/sliding_sync/builder.rs index c5a1c53c1..0eb35a262 100644 --- a/crates/matrix-sdk/src/sliding_sync/builder.rs +++ b/crates/matrix-sdk/src/sliding_sync/builder.rs @@ -298,7 +298,6 @@ impl SlidingSyncBuilder { rooms, extensions: Mutex::new(self.extensions).into(), - sent_extensions: Mutex::new(None).into(), reset_counter: Default::default(), pos: Arc::new(StdRwLock::new(Observable::new(None))), diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index af02f876a..779dfcdc3 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -670,10 +670,6 @@ pub struct SlidingSync { /// the intended state of the extensions being supplied to sliding /sync /// calls. May contain the latest next_batch for to_devices, etc. extensions: Arc>>, - - /// the last extensions known to be successfully sent to the server. - /// if the current extensions match this, we can avoid sending them again. - sent_extensions: Arc>>, } #[derive(Serialize, Deserialize)] @@ -814,6 +810,8 @@ impl SlidingSync { } fn update_to_device_since(&self, since: String) { + // FIXME: Find a better place where the to-device since token should be + // persisted. self.extensions .lock() .unwrap() @@ -870,12 +868,52 @@ impl SlidingSync { self.rooms.read().unwrap().values().cloned().collect() } + fn prepare_extension_config(&self, pos: Option<&str>) -> ExtensionsConfig { + if pos.is_none() { + // The pos is `None`, it's either our initial sync or the proxy forgot about us + // and sent us an `UnknownPos` error. We need to send out the config for our + // extensions. + let mut extensions = self.extensions.lock().unwrap().clone().unwrap_or_default(); + + // Always enable to-device events and the e2ee-extension on the initial request, + // no matter what the caller wants. + // + // The to-device `since` parameter is either `None` or guaranteed to be set + // because the `update_to_device_since()` method updates the + // self.extensions field and they get persisted to the store using the + // `cache_to_storage()` method. + // + // The token is also loaded from storage in the `SlidingSyncBuilder::build()` + // method. + let mut to_device = extensions.to_device.unwrap_or_default(); + to_device.enabled = Some(true); + + extensions.to_device = Some(to_device); + extensions.e2ee = Some(assign!(E2EEConfig::default(), { enabled: Some(true) })); + + extensions + } else { + // We already enabled all the things, just fetch out the to-device since token + // out of self.extensions and set it in a new, and empty, `ExtensionsConfig`. + let since = self + .extensions + .lock() + .unwrap() + .as_ref() + .and_then(|e| e.to_device.as_ref()?.since.to_owned()); + + let mut extensions: ExtensionsConfig = Default::default(); + extensions.to_device = Some(assign!(ToDeviceConfig::default(), { since })); + + extensions + } + } + /// Handle the HTTP response. #[instrument(skip_all, fields(lists = list_generators.len()))] async fn handle_response( &self, sliding_sync_response: v4::Response, - extensions: Option, list_generators: &mut BTreeMap, ) -> Result { // Handle and transform a Sliding Sync Response to a `SyncResponse`. @@ -950,12 +988,6 @@ impl SlidingSync { self.update_to_device_since(to_device.next_batch); } - // Track the most recently successfully sent extensions (needed for sticky - // semantics). - if extensions.is_some() { - *self.sent_extensions.lock().unwrap() = extensions; - } - UpdateSummary { lists: updated_lists, rooms } }; @@ -995,18 +1027,7 @@ impl SlidingSync { let room_subscriptions = self.subscriptions.read().unwrap().clone(); let unsubscribe_rooms = mem::take(&mut *self.unsubscribe.write().unwrap()); let timeout = Duration::from_secs(30); - - // Implement stickiness by only sending extensions if they have - // changed since the last time we sent them - let extensions = { - let extensions = self.extensions.lock().unwrap(); - - if *extensions == *self.sent_extensions.lock().unwrap() { - None - } else { - extensions.clone() - } - }; + let extensions = self.prepare_extension_config(pos.as_deref()); debug!("Sending the sliding sync request"); @@ -1023,7 +1044,7 @@ impl SlidingSync { timeout: Some(timeout), room_subscriptions, unsubscribe_rooms, - extensions: extensions.clone().unwrap_or_default(), + extensions, }), Some(request_config), self.homeserver.as_ref().map(ToString::to_string), @@ -1056,7 +1077,7 @@ impl SlidingSync { debug!("Sliding sync response received"); - let updates = self.handle_response(response, extensions, list_generators).await?; + let updates = self.handle_response(response, list_generators).await?; debug!("Sliding sync response has been handled"); @@ -1125,9 +1146,6 @@ impl SlidingSync { // To “restart” a Sliding Sync session, we set `pos` to its initial value. Observable::set(&mut self.pos.write().unwrap(), None); - // We also need to reset our extensions to the last known good ones. - *self.extensions.lock().unwrap() = self.sent_extensions.lock().unwrap().take(); - debug!(?self.extensions, "Sliding Sync has been reset"); }); } @@ -1167,10 +1185,13 @@ pub struct UpdateSummary { #[cfg(test)] mod test { + use assert_matches::assert_matches; use ruma::room_id; use serde_json::json; + use wiremock::MockServer; use super::*; + use crate::test_utils::logged_in_client; #[tokio::test] async fn check_find_room_in_list() -> Result<()> { @@ -1229,4 +1250,44 @@ mod test { Ok(()) } + + #[tokio::test] + async fn to_device_is_enabled_when_pos_is_none() -> Result<()> { + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; + + let sync = client.sliding_sync().await.build().await?; + let extensions = sync.prepare_extension_config(None); + + // If the user doesn't provide any extension config, we enable to-device and + // e2ee anyways. + assert_matches!( + extensions.to_device, + Some(ToDeviceConfig { enabled: Some(true), since: None, .. }) + ); + assert_matches!(extensions.e2ee, Some(E2EEConfig { enabled: Some(true), .. })); + + let some_since = "some_since".to_owned(); + sync.update_to_device_since(some_since.to_owned()); + let extensions = sync.prepare_extension_config(Some("foo")); + + // If there's a `pos` and to-device `since` token, we make sure we put the token + // into the extension config. The rest doesn't need to be re-enabled due to + // stickyness. + assert_matches!( + extensions.to_device, + Some(ToDeviceConfig { enabled: None, since: Some(since), .. }) if since == some_since + ); + assert_matches!(extensions.e2ee, None); + + let extensions = sync.prepare_extension_config(None); + // Even if there isn't a `pos`, if we have a to-device `since` token, we put it + // into the request. + assert_matches!( + extensions.to_device, + Some(ToDeviceConfig { enabled: Some(true), since: Some(since), .. }) if since == some_since + ); + + Ok(()) + } } From 2aec3d050274ddb5685a29125fa9d61ee4b7be24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Fri, 3 Mar 2023 13:17:48 +0100 Subject: [PATCH 125/166] indexeddb: Move the state store migrations to a separate file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- .../src/state_store/migrations.rs | 461 ++++++++++++++++++ .../{state_store.rs => state_store/mod.rs} | 440 +---------------- 2 files changed, 471 insertions(+), 430 deletions(-) create mode 100644 crates/matrix-sdk-indexeddb/src/state_store/migrations.rs rename crates/matrix-sdk-indexeddb/src/{state_store.rs => state_store/mod.rs} (74%) diff --git a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs new file mode 100644 index 000000000..860bf1758 --- /dev/null +++ b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs @@ -0,0 +1,461 @@ +// Copyright 2021 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::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; + +use gloo_utils::format::JsValueSerdeExt; +use indexed_db_futures::{prelude::*, request::OpenDbRequest, IdbDatabase, IdbVersionChangeEvent}; +use js_sys::Date as JsDate; +use matrix_sdk_store_encryption::StoreCipher; +use serde::{Deserialize, Serialize}; +use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue}; +use wasm_bindgen::JsValue; +use web_sys::IdbTransactionMode; + +use super::{deserialize_event, serialize_event, Result, ALL_STORES, KEYS}; +use crate::IndexeddbStateStoreError; + +const CURRENT_DB_VERSION: f64 = 1.2; +const CURRENT_META_DB_VERSION: f64 = 2.0; + +/// Sometimes Migrations can't proceed without having to drop existing +/// data. This allows you to configure, how these cases should be handled. +#[allow(dead_code)] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum MigrationConflictStrategy { + /// Just drop the data, we don't care that we have to sync again + Drop, + /// Raise a [`IndexeddbStateStoreError::MigrationConflict`] error with the + /// path to the DB in question. The caller then has to take care about + /// what they want to do and try again after. + Raise, + /// Default. + BackupAndDrop, +} + +#[derive(Clone, Serialize, Deserialize)] +struct StoreKeyWrapper(Vec); + +pub async fn upgrade_meta_db( + meta_name: &str, + passphrase: Option<&str>, +) -> Result<(IdbDatabase, Option>)> { + // Meta database. + let mut db_req: OpenDbRequest = IdbDatabase::open_f64(meta_name, CURRENT_META_DB_VERSION)?; + db_req.set_on_upgrade_needed(Some(|evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { + let db = evt.db(); + if evt.old_version() < 1.0 { + // migrating to version 1 + + db.create_object_store(KEYS::INTERNAL_STATE)?; + db.create_object_store(KEYS::BACKUPS_META)?; + } else if evt.old_version() < 2.0 { + db.create_object_store(KEYS::BACKUPS_META)?; + } + Ok(()) + })); + + let meta_db: IdbDatabase = db_req.into_future().await?; + + let store_cipher = if let Some(passphrase) = passphrase { + let tx: IdbTransaction<'_> = meta_db + .transaction_on_one_with_mode(KEYS::INTERNAL_STATE, IdbTransactionMode::Readwrite)?; + let ob = tx.object_store(KEYS::INTERNAL_STATE)?; + + let cipher = if let Some(StoreKeyWrapper(inner)) = ob + .get(&JsValue::from_str(KEYS::STORE_KEY))? + .await? + .map(|v| v.into_serde()) + .transpose()? + { + StoreCipher::import(passphrase, &inner)? + } else { + let cipher = StoreCipher::new()?; + #[cfg(not(test))] + let export = cipher.export(passphrase)?; + #[cfg(test)] + let export = cipher._insecure_export_fast_for_testing(passphrase)?; + ob.put_key_val( + &JsValue::from_str(KEYS::STORE_KEY), + &JsValue::from_serde(&StoreKeyWrapper(export))?, + )?; + cipher + }; + + tx.await.into_result()?; + Some(Arc::new(cipher)) + } else { + None + }; + + Ok((meta_db, store_cipher)) +} + +pub async fn upgrade_inner_db( + name: &str, + store_cipher: Option<&StoreCipher>, + migration_strategy: MigrationConflictStrategy, + meta_db: &IdbDatabase, +) -> Result { + let mut recreate_stores = false; + { + // checkup up in a separate call, whether we have to backup or do anything else + // to the db. Unfortunately the set_on_upgrade_needed doesn't allow async fn + // which we need to execute the backup. + let has_store_cipher = store_cipher.is_some(); + let mut db_req: OpenDbRequest = IdbDatabase::open_f64(name, 1.0)?; + let created = Arc::new(AtomicBool::new(false)); + let created_inner = created.clone(); + + db_req.set_on_upgrade_needed(Some( + move |evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { + // in case this is a fresh db, we dont't want to trigger + // further migrations other than just creating the full + // schema. + if evt.old_version() < 1.0 { + create_stores(evt.db())?; + created_inner.store(true, Ordering::Relaxed); + } + Ok(()) + }, + )); + + let pre_db = db_req.into_future().await?; + let old_version = pre_db.version(); + + if created.load(Ordering::Relaxed) { + // this is a fresh DB, nothing to do + } else if old_version == 1.0 && has_store_cipher { + match migration_strategy { + MigrationConflictStrategy::BackupAndDrop => { + backup(&pre_db, meta_db).await?; + recreate_stores = true; + } + MigrationConflictStrategy::Drop => { + recreate_stores = true; + } + MigrationConflictStrategy::Raise => { + return Err(IndexeddbStateStoreError::MigrationConflict { + name: name.to_owned(), + old_version, + new_version: CURRENT_DB_VERSION, + }) + } + } + } else if old_version < 1.2 { + migrate_to_v1_2(&pre_db, store_cipher).await?; + } else { + // Nothing to be done + } + } + + let mut db_req: OpenDbRequest = IdbDatabase::open_f64(name, CURRENT_DB_VERSION)?; + db_req.set_on_upgrade_needed(Some(move |evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { + // changing the format can only happen in the upgrade procedure + if recreate_stores { + drop_stores(evt.db())?; + create_stores(evt.db())?; + } + Ok(()) + })); + + Ok(db_req.into_future().await?) +} + +fn drop_stores(db: &IdbDatabase) -> Result<(), JsValue> { + for name in ALL_STORES { + db.delete_object_store(name)?; + } + Ok(()) +} + +fn create_stores(db: &IdbDatabase) -> Result<(), JsValue> { + for name in ALL_STORES { + db.create_object_store(name)?; + } + Ok(()) +} + +async fn backup(source: &IdbDatabase, meta: &IdbDatabase) -> Result<()> { + let now = JsDate::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> { + // migrating to version 1 + let db = evt.db(); + for name in ALL_STORES { + db.create_object_store(name)?; + } + Ok(()) + })); + let target = db_req.into_future().await?; + + for name in ALL_STORES { + let tx = target.transaction_on_one_with_mode(name, IdbTransactionMode::Readwrite)?; + + let obj = tx.object_store(name)?; + + if let Some(curs) = source + .transaction_on_one_with_mode(name, IdbTransactionMode::Readonly)? + .object_store(name)? + .open_cursor()? + .await? + { + while let Some(key) = curs.key() { + obj.put_key_val(&key, &curs.value())?; + + curs.continue_cursor()?.await?; + } + } + + tx.await.into_result()?; + } + + let tx = + meta.transaction_on_one_with_mode(KEYS::BACKUPS_META, IdbTransactionMode::Readwrite)?; + let backup_store = tx.object_store(KEYS::BACKUPS_META)?; + backup_store.put_key_val(&JsValue::from_f64(now), &JsValue::from_str(&backup_name))?; + + tx.await; + + Ok(()) +} + +async fn v1_2_fix_store( + store: &IdbObjectStore<'_>, + store_cipher: Option<&StoreCipher>, +) -> Result<()> { + fn maybe_fix_json(raw_json: &RawJsonValue) -> Result> { + let json = raw_json.get(); + + if json.contains(r#""content":null"#) { + let mut value: JsonValue = serde_json::from_str(json)?; + if let Some(content) = value.get_mut("content") { + if matches!(content, JsonValue::Null) { + *content = JsonValue::Object(Default::default()); + return Ok(Some(value)); + } + } + } + + Ok(None) + } + + let cursor = store.open_cursor()?.await?; + + if let Some(cursor) = cursor { + loop { + let raw_json: Box = deserialize_event(store_cipher, cursor.value())?; + + if let Some(fixed_json) = maybe_fix_json(&raw_json)? { + cursor.update(&serialize_event(store_cipher, &fixed_json)?)?.await?; + } + + if !cursor.continue_cursor()?.await? { + break; + } + } + } + + Ok(()) +} + +async fn migrate_to_v1_2(db: &IdbDatabase, store_cipher: Option<&StoreCipher>) -> Result<()> { + let tx = db.transaction_on_multi_with_mode( + &[KEYS::ROOM_STATE, KEYS::ROOM_INFOS], + IdbTransactionMode::Readwrite, + )?; + + v1_2_fix_store(&tx.object_store(KEYS::ROOM_STATE)?, store_cipher).await?; + v1_2_fix_store(&tx.object_store(KEYS::ROOM_INFOS)?, store_cipher).await?; + + tx.await.into_result().map_err(|e| e.into()) +} + +#[cfg(all(test, target_arch = "wasm32"))] +mod tests { + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + + use indexed_db_futures::prelude::*; + use matrix_sdk_base::StateStore; + use matrix_sdk_test::async_test; + use ruma::{ + events::{AnySyncStateEvent, StateEventType}, + room_id, + }; + use serde_json::json; + use uuid::Uuid; + use wasm_bindgen::JsValue; + + use super::{serialize_event, MigrationConflictStrategy, Result, ALL_STORES, KEYS}; + use crate::{safe_encode::SafeEncode, IndexeddbStateStore, IndexeddbStateStoreError}; + + pub async fn create_fake_db(name: &str, version: f64) -> Result { + let mut db_req: OpenDbRequest = IdbDatabase::open_f64(name, version)?; + db_req.set_on_upgrade_needed(Some( + move |evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { + // migrating to version 1 + let db = evt.db(); + for name in ALL_STORES { + db.create_object_store(name)?; + } + Ok(()) + }, + )); + db_req.into_future().await.map_err(Into::into) + } + + #[async_test] + pub async fn test_no_upgrade() -> Result<()> { + let name = format!("simple-1.1-no-cipher-{}", Uuid::new_v4().as_hyphenated().to_string()); + + // this transparently migrates to the latest version + let store = IndexeddbStateStore::builder().name(name).build().await?; + // this didn't create any backup + assert_eq!(store.has_backups().await?, false); + // simple check that the layout exists. + assert_eq!(store.get_sync_token().await?, None); + Ok(()) + } + + #[async_test] + pub async fn test_migrating_v1_to_1_1_plain() -> Result<()> { + let name = + format!("migrating-1.1-no-cipher-{}", Uuid::new_v4().as_hyphenated().to_string()); + create_fake_db(&name, 1.0).await?; + + // this transparently migrates to the latest version + let store = IndexeddbStateStore::builder().name(name).build().await?; + // this didn't create any backup + assert_eq!(store.has_backups().await?, false); + assert_eq!(store.get_sync_token().await?, None); + Ok(()) + } + + #[async_test] + pub async fn test_migrating_v1_to_1_1_with_pw() -> Result<()> { + let name = + format!("migrating-1.1-with-cipher-{}", Uuid::new_v4().as_hyphenated().to_string()); + let passphrase = "somepassphrase".to_owned(); + create_fake_db(&name, 1.0).await?; + + // this transparently migrates to the latest version + let store = + IndexeddbStateStore::builder().name(name).passphrase(passphrase).build().await?; + // this creates a backup by default + assert_eq!(store.has_backups().await?, true); + assert!(store.latest_backup().await?.is_some(), "No backup_found"); + assert_eq!(store.get_sync_token().await?, None); + Ok(()) + } + + #[async_test] + pub async fn test_migrating_v1_to_1_1_with_pw_drops() -> Result<()> { + let name = format!( + "migrating-1.1-with-cipher-drops-{}", + Uuid::new_v4().as_hyphenated().to_string() + ); + let passphrase = "some-other-passphrase".to_owned(); + create_fake_db(&name, 1.0).await?; + + // this transparently migrates to the latest version + let store = IndexeddbStateStore::builder() + .name(name) + .passphrase(passphrase) + .migration_conflict_strategy(MigrationConflictStrategy::Drop) + .build() + .await?; + // this creates a backup by default + assert_eq!(store.has_backups().await?, false); + assert_eq!(store.get_sync_token().await?, None); + Ok(()) + } + + #[async_test] + pub async fn test_migrating_v1_to_1_1_with_pw_raise() -> Result<()> { + let name = format!( + "migrating-1.1-with-cipher-raises-{}", + Uuid::new_v4().as_hyphenated().to_string() + ); + let passphrase = "some-other-passphrase".to_owned(); + create_fake_db(&name, 1.0).await?; + + // this transparently migrates to the latest version + let store_res = IndexeddbStateStore::builder() + .name(name) + .passphrase(passphrase) + .migration_conflict_strategy(MigrationConflictStrategy::Raise) + .build() + .await; + + if let Err(IndexeddbStateStoreError::MigrationConflict { .. }) = store_res { + // all fine! + } else { + assert!(false, "Conflict didn't raise: {:?}", store_res) + } + Ok(()) + } + + #[async_test] + pub async fn test_migrating_to_v1_2() -> Result<()> { + let name = format!("migrating-1.2-{}", Uuid::new_v4().as_hyphenated().to_string()); + // An event that fails to deserialize. + let wrong_redacted_state_event = json!({ + "content": null, + "event_id": "$wrongevent", + "origin_server_ts": 1673887516047_u64, + "sender": "@example:localhost", + "state_key": "", + "type": "m.room.topic", + "unsigned": { + "redacted_because": { + "type": "m.room.redaction", + "sender": "@example:localhost", + "content": {}, + "redacts": "$wrongevent", + "origin_server_ts": 1673893816047_u64, + "unsigned": {}, + "event_id": "$redactionevent", + }, + }, + }); + serde_json::from_value::(wrong_redacted_state_event.clone()) + .unwrap_err(); + + let room_id = room_id!("!some_room:localhost"); + + // Populate DB with wrong event. + { + let db = create_fake_db(&name, 1.1).await?; + let tx = + db.transaction_on_one_with_mode(KEYS::ROOM_STATE, IdbTransactionMode::Readwrite)?; + let state = tx.object_store(KEYS::ROOM_STATE)?; + let key = (room_id, StateEventType::RoomTopic, "").encode(); + state.put_key_val(&key, &serialize_event(None, &wrong_redacted_state_event)?)?; + tx.await.into_result()?; + } + + // this transparently migrates to the latest version + let store = IndexeddbStateStore::builder().name(name).build().await?; + let event = + store.get_state_event(room_id, StateEventType::RoomTopic, "").await.unwrap().unwrap(); + event.deserialize().unwrap(); + + Ok(()) + } +} diff --git a/crates/matrix-sdk-indexeddb/src/state_store.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs similarity index 74% rename from crates/matrix-sdk-indexeddb/src/state_store.rs rename to crates/matrix-sdk-indexeddb/src/state_store/mod.rs index 8acf8d209..15f72da7b 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -14,17 +14,13 @@ use std::{ collections::{BTreeSet, HashSet}, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, + sync::Arc, }; use anyhow::anyhow; use async_trait::async_trait; use gloo_utils::format::JsValueSerdeExt; use indexed_db_futures::prelude::*; -use js_sys::Date as JsDate; use matrix_sdk_base::{ deserialized_responses::RawMemberEvent, media::{MediaRequest, UniqueKey}, @@ -44,16 +40,16 @@ use ruma::{ serde::Raw, CanonicalJsonObject, EventId, MxcUri, OwnedEventId, OwnedUserId, RoomId, RoomVersionId, UserId, }; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue}; +use serde::{de::DeserializeOwned, Serialize}; use tracing::{debug, warn}; use wasm_bindgen::JsValue; use web_sys::IdbKeyRange; -use crate::safe_encode::SafeEncode; +mod migrations; -#[derive(Clone, Serialize, Deserialize)] -struct StoreKeyWrapper(Vec); +pub use self::migrations::MigrationConflictStrategy; +use self::migrations::{upgrade_inner_db, upgrade_meta_db}; +use crate::safe_encode::SafeEncode; #[derive(Debug, thiserror::Error)] pub enum IndexeddbStateStoreError { @@ -69,21 +65,6 @@ pub enum IndexeddbStateStoreError { MigrationConflict { name: String, old_version: f64, new_version: f64 }, } -/// Sometimes Migrations can't proceed without having to drop existing -/// data. This allows you to configure, how these cases should be handled. -#[allow(dead_code)] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum MigrationConflictStrategy { - /// Just drop the data, we don't care that we have to sync again - Drop, - /// Raise a [`IndexeddbStateStoreError::MigrationConflict`] error with the - /// path to the DB in question. The caller then has to take care about - /// what they want to do and try again after. - Raise, - /// Default. - BackupAndDrop, -} - impl From for IndexeddbStateStoreError { fn from(frm: indexed_db_futures::web_sys::DomException) -> IndexeddbStateStoreError { IndexeddbStateStoreError::DomException { @@ -109,9 +90,6 @@ impl From for StoreError { mod KEYS { // STORES - pub const CURRENT_DB_VERSION: f64 = 1.2; - pub const CURRENT_META_DB_VERSION: f64 = 2.0; - pub const INTERNAL_STATE: &str = "matrix-sdk-state"; pub const BACKUPS_META: &str = "backups"; @@ -177,66 +155,6 @@ mod KEYS { pub use KEYS::ALL_STORES; -fn drop_stores(db: &IdbDatabase) -> Result<(), JsValue> { - for name in ALL_STORES { - db.delete_object_store(name)?; - } - Ok(()) -} - -fn create_stores(db: &IdbDatabase) -> Result<(), JsValue> { - for name in ALL_STORES { - db.create_object_store(name)?; - } - Ok(()) -} - -async fn backup(source: &IdbDatabase, meta: &IdbDatabase) -> Result<()> { - let now = JsDate::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> { - // migrating to version 1 - let db = evt.db(); - for name in ALL_STORES { - db.create_object_store(name)?; - } - Ok(()) - })); - let target = db_req.into_future().await?; - - for name in ALL_STORES { - let tx = target.transaction_on_one_with_mode(name, IdbTransactionMode::Readwrite)?; - - let obj = tx.object_store(name)?; - - if let Some(curs) = source - .transaction_on_one_with_mode(name, IdbTransactionMode::Readonly)? - .object_store(name)? - .open_cursor()? - .await? - { - while let Some(key) = curs.key() { - obj.put_key_val(&key, &curs.value())?; - - curs.continue_cursor()?.await?; - } - } - - tx.await.into_result()?; - } - - let tx = - meta.transaction_on_one_with_mode(KEYS::BACKUPS_META, IdbTransactionMode::Readwrite)?; - let backup_store = tx.object_store(KEYS::BACKUPS_META)?; - backup_store.put_key_val(&JsValue::from_f64(now), &JsValue::from_str(&backup_name))?; - - tx.await; - - Ok(()) -} - fn serialize_event(store_cipher: Option<&StoreCipher>, event: &impl Serialize) -> Result { Ok(match store_cipher { Some(cipher) => JsValue::from_serde(&cipher.encrypt_value_typed(event)?)?, @@ -254,57 +172,6 @@ fn deserialize_event( } } -async fn v1_2_fix_store( - store: &IdbObjectStore<'_>, - store_cipher: Option<&StoreCipher>, -) -> Result<()> { - fn maybe_fix_json(raw_json: &RawJsonValue) -> Result> { - let json = raw_json.get(); - - if json.contains(r#""content":null"#) { - let mut value: JsonValue = serde_json::from_str(json)?; - if let Some(content) = value.get_mut("content") { - if matches!(content, JsonValue::Null) { - *content = JsonValue::Object(Default::default()); - return Ok(Some(value)); - } - } - } - - Ok(None) - } - - let cursor = store.open_cursor()?.await?; - - if let Some(cursor) = cursor { - loop { - let raw_json: Box = deserialize_event(store_cipher, cursor.value())?; - - if let Some(fixed_json) = maybe_fix_json(&raw_json)? { - cursor.update(&serialize_event(store_cipher, &fixed_json)?)?.await?; - } - - if !cursor.continue_cursor()?.await? { - break; - } - } - } - - Ok(()) -} - -async fn migrate_to_v1_2(db: &IdbDatabase, store_cipher: Option<&StoreCipher>) -> Result<()> { - let tx = db.transaction_on_multi_with_mode( - &[KEYS::ROOM_STATE, KEYS::ROOM_INFOS], - IdbTransactionMode::Readwrite, - )?; - - v1_2_fix_store(&tx.object_store(KEYS::ROOM_STATE)?, store_cipher).await?; - v1_2_fix_store(&tx.object_store(KEYS::ROOM_INFOS)?, store_cipher).await?; - - tx.await.into_result().map_err(|e| e.into()) -} - /// Builder for [`IndexeddbStateStore`]. #[derive(Debug)] pub struct IndexeddbStateStoreBuilder { @@ -350,122 +217,11 @@ impl IndexeddbStateStoreBuilder { let meta_name = format!("{name}::{}", KEYS::INTERNAL_STATE); - let mut db_req: OpenDbRequest = - IdbDatabase::open_f64(&meta_name, KEYS::CURRENT_META_DB_VERSION)?; - db_req.set_on_upgrade_needed(Some(|evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { - let db = evt.db(); - if evt.old_version() < 1.0 { - // migrating to version 1 + let (meta, store_cipher) = upgrade_meta_db(&meta_name, self.passphrase.as_deref()).await?; + let inner = + upgrade_inner_db(&name, store_cipher.as_deref(), migration_strategy, &meta).await?; - db.create_object_store(KEYS::INTERNAL_STATE)?; - db.create_object_store(KEYS::BACKUPS_META)?; - } else if evt.old_version() < 2.0 { - db.create_object_store(KEYS::BACKUPS_META)?; - } - Ok(()) - })); - - let meta_db: IdbDatabase = db_req.into_future().await?; - - let store_cipher = if let Some(passphrase) = &self.passphrase { - let tx: IdbTransaction<'_> = meta_db.transaction_on_one_with_mode( - KEYS::INTERNAL_STATE, - IdbTransactionMode::Readwrite, - )?; - let ob = tx.object_store(KEYS::INTERNAL_STATE)?; - - let cipher = if let Some(StoreKeyWrapper(inner)) = ob - .get(&JsValue::from_str(KEYS::STORE_KEY))? - .await? - .map(|v| v.into_serde()) - .transpose()? - { - StoreCipher::import(passphrase, &inner)? - } else { - let cipher = StoreCipher::new()?; - #[cfg(not(test))] - let export = cipher.export(passphrase)?; - #[cfg(test)] - let export = cipher._insecure_export_fast_for_testing(passphrase)?; - ob.put_key_val( - &JsValue::from_str(KEYS::STORE_KEY), - &JsValue::from_serde(&StoreKeyWrapper(export))?, - )?; - cipher - }; - - tx.await.into_result()?; - Some(Arc::new(cipher)) - } else { - None - }; - - let mut recreate_stores = false; - { - // checkup up in a separate call, whether we have to backup or do anything else - // to the db. Unfortunately the set_on_upgrade_needed doesn't allow async fn - // which we need to execute the backup. - let has_store_cipher = store_cipher.is_some(); - let mut db_req: OpenDbRequest = IdbDatabase::open_f64(&name, 1.0)?; - let created = Arc::new(AtomicBool::new(false)); - let created_inner = created.clone(); - - db_req.set_on_upgrade_needed(Some( - move |evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { - // in case this is a fresh db, we dont't want to trigger - // further migrations other than just creating the full - // schema. - if evt.old_version() < 1.0 { - create_stores(evt.db())?; - created_inner.store(true, Ordering::Relaxed); - } - Ok(()) - }, - )); - - let pre_db = db_req.into_future().await?; - let old_version = pre_db.version(); - - if created.load(Ordering::Relaxed) { - // this is a fresh DB, nothing to do - } else if old_version == 1.0 && has_store_cipher { - match migration_strategy { - MigrationConflictStrategy::BackupAndDrop => { - backup(&pre_db, &meta_db).await?; - recreate_stores = true; - } - MigrationConflictStrategy::Drop => { - recreate_stores = true; - } - MigrationConflictStrategy::Raise => { - return Err(IndexeddbStateStoreError::MigrationConflict { - name, - old_version, - new_version: KEYS::CURRENT_DB_VERSION, - }) - } - } - } else if old_version < 1.2 { - migrate_to_v1_2(&pre_db, store_cipher.as_deref()).await?; - } else { - // Nothing to be done - } - } - - let mut db_req: OpenDbRequest = IdbDatabase::open_f64(&name, KEYS::CURRENT_DB_VERSION)?; - db_req.set_on_upgrade_needed(Some( - move |evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { - // changing the format can only happen in the upgrade procedure - if recreate_stores { - drop_stores(evt.db())?; - create_stores(evt.db())?; - } - Ok(()) - }, - )); - - let db = db_req.into_future().await?; - Ok(IndexeddbStateStore { name, inner: db, meta: meta_db, store_cipher }) + Ok(IndexeddbStateStore { name, inner, meta, store_cipher }) } } @@ -1457,179 +1213,3 @@ mod encrypted_tests { statestore_integration_tests!(with_media_tests); } - -#[cfg(all(test, target_arch = "wasm32"))] -mod migration_tests { - wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - - use indexed_db_futures::prelude::*; - use matrix_sdk_base::StateStore; - use matrix_sdk_test::async_test; - use ruma::{ - events::{AnySyncStateEvent, StateEventType}, - room_id, - }; - use serde_json::json; - use uuid::Uuid; - use wasm_bindgen::JsValue; - - use super::{ - serialize_event, IndexeddbStateStore, IndexeddbStateStoreError, MigrationConflictStrategy, - Result, ALL_STORES, KEYS, - }; - use crate::safe_encode::SafeEncode; - - pub async fn create_fake_db(name: &str, version: f64) -> Result { - let mut db_req: OpenDbRequest = IdbDatabase::open_f64(name, version)?; - db_req.set_on_upgrade_needed(Some( - move |evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { - // migrating to version 1 - let db = evt.db(); - for name in ALL_STORES { - db.create_object_store(name)?; - } - Ok(()) - }, - )); - db_req.into_future().await.map_err(Into::into) - } - - #[async_test] - pub async fn test_no_upgrade() -> Result<()> { - let name = format!("simple-1.1-no-cipher-{}", Uuid::new_v4().as_hyphenated().to_string()); - - // this transparently migrates to the latest version - let store = IndexeddbStateStore::builder().name(name).build().await?; - // this didn't create any backup - assert_eq!(store.has_backups().await?, false); - // simple check that the layout exists. - assert_eq!(store.get_sync_token().await?, None); - Ok(()) - } - - #[async_test] - pub async fn test_migrating_v1_to_1_1_plain() -> Result<()> { - let name = - format!("migrating-1.1-no-cipher-{}", Uuid::new_v4().as_hyphenated().to_string()); - create_fake_db(&name, 1.0).await?; - - // this transparently migrates to the latest version - let store = IndexeddbStateStore::builder().name(name).build().await?; - // this didn't create any backup - assert_eq!(store.has_backups().await?, false); - assert_eq!(store.get_sync_token().await?, None); - Ok(()) - } - - #[async_test] - pub async fn test_migrating_v1_to_1_1_with_pw() -> Result<()> { - let name = - format!("migrating-1.1-with-cipher-{}", Uuid::new_v4().as_hyphenated().to_string()); - let passphrase = "somepassphrase".to_owned(); - create_fake_db(&name, 1.0).await?; - - // this transparently migrates to the latest version - let store = - IndexeddbStateStore::builder().name(name).passphrase(passphrase).build().await?; - // this creates a backup by default - assert_eq!(store.has_backups().await?, true); - assert!(store.latest_backup().await?.is_some(), "No backup_found"); - assert_eq!(store.get_sync_token().await?, None); - Ok(()) - } - - #[async_test] - pub async fn test_migrating_v1_to_1_1_with_pw_drops() -> Result<()> { - let name = format!( - "migrating-1.1-with-cipher-drops-{}", - Uuid::new_v4().as_hyphenated().to_string() - ); - let passphrase = "some-other-passphrase".to_owned(); - create_fake_db(&name, 1.0).await?; - - // this transparently migrates to the latest version - let store = IndexeddbStateStore::builder() - .name(name) - .passphrase(passphrase) - .migration_conflict_strategy(MigrationConflictStrategy::Drop) - .build() - .await?; - // this creates a backup by default - assert_eq!(store.has_backups().await?, false); - assert_eq!(store.get_sync_token().await?, None); - Ok(()) - } - - #[async_test] - pub async fn test_migrating_v1_to_1_1_with_pw_raise() -> Result<()> { - let name = format!( - "migrating-1.1-with-cipher-raises-{}", - Uuid::new_v4().as_hyphenated().to_string() - ); - let passphrase = "some-other-passphrase".to_owned(); - create_fake_db(&name, 1.0).await?; - - // this transparently migrates to the latest version - let store_res = IndexeddbStateStore::builder() - .name(name) - .passphrase(passphrase) - .migration_conflict_strategy(MigrationConflictStrategy::Raise) - .build() - .await; - - if let Err(IndexeddbStateStoreError::MigrationConflict { .. }) = store_res { - // all fine! - } else { - assert!(false, "Conflict didn't raise: {:?}", store_res) - } - Ok(()) - } - - #[async_test] - pub async fn test_migrating_to_v1_2() -> Result<()> { - let name = format!("migrating-1.2-{}", Uuid::new_v4().as_hyphenated().to_string()); - // An event that fails to deserialize. - let wrong_redacted_state_event = json!({ - "content": null, - "event_id": "$wrongevent", - "origin_server_ts": 1673887516047_u64, - "sender": "@example:localhost", - "state_key": "", - "type": "m.room.topic", - "unsigned": { - "redacted_because": { - "type": "m.room.redaction", - "sender": "@example:localhost", - "content": {}, - "redacts": "$wrongevent", - "origin_server_ts": 1673893816047_u64, - "unsigned": {}, - "event_id": "$redactionevent", - }, - }, - }); - serde_json::from_value::(wrong_redacted_state_event.clone()) - .unwrap_err(); - - let room_id = room_id!("!some_room:localhost"); - - // Populate DB with wrong event. - { - let db = create_fake_db(&name, 1.1).await?; - let tx = - db.transaction_on_one_with_mode(KEYS::ROOM_STATE, IdbTransactionMode::Readwrite)?; - let state = tx.object_store(KEYS::ROOM_STATE)?; - let key = (room_id, StateEventType::RoomTopic, "").encode(); - state.put_key_val(&key, &serialize_event(None, &wrong_redacted_state_event)?)?; - tx.await.into_result()?; - } - - // this transparently migrates to the latest version - let store = IndexeddbStateStore::builder().name(name).build().await?; - let event = - store.get_state_event(room_id, StateEventType::RoomTopic, "").await.unwrap().unwrap(); - event.deserialize().unwrap(); - - Ok(()) - } -} From e09ec389d15a92e14342f9abc007f44ebbd3a221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Sat, 4 Mar 2023 18:04:26 +0100 Subject: [PATCH 126/166] indexeddb: Fix migration of state store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The IndexedDB API expects an unsigned integer for the version. The web-sys bindings expose it as a f64 so versions like 1.1 and 1.2 were used. The were converted back to uint in JS which means the version was always 1.0 and no real upgrade was processed Signed-off-by: Kévin Commaille --- Cargo.lock | 1 + crates/matrix-sdk-indexeddb/Cargo.toml | 1 + .../src/state_store/migrations.rs | 296 ++++++++++++------ .../src/state_store/mod.rs | 12 +- 4 files changed, 210 insertions(+), 100 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 409c9b0be..9f3b1018f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2871,6 +2871,7 @@ name = "matrix-sdk-indexeddb" version = "0.2.0" dependencies = [ "anyhow", + "assert_matches", "async-trait", "base64 0.21.0", "dashmap", diff --git a/crates/matrix-sdk-indexeddb/Cargo.toml b/crates/matrix-sdk-indexeddb/Cargo.toml index 2fdcfb39b..84247d624 100644 --- a/crates/matrix-sdk-indexeddb/Cargo.toml +++ b/crates/matrix-sdk-indexeddb/Cargo.toml @@ -41,6 +41,7 @@ web-sys = { version = "0.3.57", features = ["IdbKeyRange"] } getrandom = { version = "0.2.6", features = ["js"] } [dev-dependencies] +assert_matches = "1.5.0" matrix-sdk-base = { path = "../matrix-sdk-base", features = ["testing"] } matrix-sdk-common = { path = "../matrix-sdk-common", features = ["js"] } matrix-sdk-crypto = { path = "../matrix-sdk-crypto", features = ["js", "testing"] } diff --git a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs index 860bf1758..f6d68d3ce 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs @@ -12,10 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, -}; +use std::sync::Arc; use gloo_utils::format::JsValueSerdeExt; use indexed_db_futures::{prelude::*, request::OpenDbRequest, IdbDatabase, IdbVersionChangeEvent}; @@ -29,8 +26,8 @@ use web_sys::IdbTransactionMode; use super::{deserialize_event, serialize_event, Result, ALL_STORES, KEYS}; use crate::IndexeddbStateStoreError; -const CURRENT_DB_VERSION: f64 = 1.2; -const CURRENT_META_DB_VERSION: f64 = 2.0; +const CURRENT_DB_VERSION: u32 = 3; +const CURRENT_META_DB_VERSION: u32 = 2; /// Sometimes Migrations can't proceed without having to drop existing /// data. This allows you to configure, how these cases should be handled. @@ -55,17 +52,19 @@ pub async fn upgrade_meta_db( passphrase: Option<&str>, ) -> Result<(IdbDatabase, Option>)> { // Meta database. - let mut db_req: OpenDbRequest = IdbDatabase::open_f64(meta_name, CURRENT_META_DB_VERSION)?; + let mut db_req: OpenDbRequest = IdbDatabase::open_u32(meta_name, CURRENT_META_DB_VERSION)?; db_req.set_on_upgrade_needed(Some(|evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { let db = evt.db(); - if evt.old_version() < 1.0 { - // migrating to version 1 + let old_version = evt.old_version() as u32; + if old_version < 1 { db.create_object_store(KEYS::INTERNAL_STATE)?; - db.create_object_store(KEYS::BACKUPS_META)?; - } else if evt.old_version() < 2.0 { + } + + if old_version < 2 { db.create_object_store(KEYS::BACKUPS_META)?; } + Ok(()) })); @@ -111,65 +110,72 @@ pub async fn upgrade_inner_db( migration_strategy: MigrationConflictStrategy, meta_db: &IdbDatabase, ) -> Result { - let mut recreate_stores = false; + let mut should_create_stores = false; + let mut should_drop_stores = false; { - // checkup up in a separate call, whether we have to backup or do anything else - // to the db. Unfortunately the set_on_upgrade_needed doesn't allow async fn - // which we need to execute the backup. + // This is a hack, we need to open the database a first time to get the current + // version. + // The indexed_db_futures crate doesn't let us access the transaction so we + // can't migrate data inside the `onupgradeneeded` callback. Instead we see if + // we need to migrate some data before the upgrade, then let the store process + // the upgrade. + // See let has_store_cipher = store_cipher.is_some(); - let mut db_req: OpenDbRequest = IdbDatabase::open_f64(name, 1.0)?; - let created = Arc::new(AtomicBool::new(false)); - let created_inner = created.clone(); + let pre_db = IdbDatabase::open(name)?.into_future().await?; - db_req.set_on_upgrade_needed(Some( - move |evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { - // in case this is a fresh db, we dont't want to trigger - // further migrations other than just creating the full - // schema. - if evt.old_version() < 1.0 { - create_stores(evt.db())?; - created_inner.store(true, Ordering::Relaxed); - } - Ok(()) - }, - )); + // Even if the web-sys bindings expose the version as a f64, the IndexedDB API + // works with an unsigned integer. + // See + let mut old_version = pre_db.version() as u32; - let pre_db = db_req.into_future().await?; - let old_version = pre_db.version(); + // Inside the `onupgradeneeded` callback we would know whether it's a new DB + // because the old version would be set to 0, here it is already set to 1 so we + // check if the stores exist. + if old_version == 1 && pre_db.object_store_names().next().is_none() { + old_version = 0; + } - if created.load(Ordering::Relaxed) { - // this is a fresh DB, nothing to do - } else if old_version == 1.0 && has_store_cipher { + // Upgrades to v1 and v2 (re)create empty stores, while the other upgrades + // change data that is already in the stores, so we use exclusive branches here. + if old_version == 0 { + should_create_stores = true; + } else if old_version < 2 && has_store_cipher { match migration_strategy { MigrationConflictStrategy::BackupAndDrop => { - backup(&pre_db, meta_db).await?; - recreate_stores = true; + backup_v1(&pre_db, meta_db).await?; + should_drop_stores = true; + should_create_stores = true; } MigrationConflictStrategy::Drop => { - recreate_stores = true; + should_drop_stores = true; + should_create_stores = true; } MigrationConflictStrategy::Raise => { return Err(IndexeddbStateStoreError::MigrationConflict { name: name.to_owned(), old_version, new_version: CURRENT_DB_VERSION, - }) + }); } } - } else if old_version < 1.2 { - migrate_to_v1_2(&pre_db, store_cipher).await?; - } else { - // Nothing to be done + } else if old_version < 3 { + migrate_to_v3(&pre_db, store_cipher).await?; } + + pre_db.close(); } - let mut db_req: OpenDbRequest = IdbDatabase::open_f64(name, CURRENT_DB_VERSION)?; + let mut db_req: OpenDbRequest = IdbDatabase::open_u32(name, CURRENT_DB_VERSION)?; db_req.set_on_upgrade_needed(Some(move |evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { - // changing the format can only happen in the upgrade procedure - if recreate_stores { + // Changing the format can only happen in the upgrade procedure + if should_drop_stores { drop_stores(evt.db())?; + } + + if should_create_stores { create_stores(evt.db())?; } + Ok(()) })); @@ -190,7 +196,7 @@ fn create_stores(db: &IdbDatabase) -> Result<(), JsValue> { Ok(()) } -async fn backup(source: &IdbDatabase, meta: &IdbDatabase) -> Result<()> { +async fn backup_v1(source: &IdbDatabase, meta: &IdbDatabase) -> Result<()> { let now = JsDate::now(); let backup_name = format!("backup-{}-{now}", source.name()); @@ -206,24 +212,24 @@ async fn backup(source: &IdbDatabase, meta: &IdbDatabase) -> Result<()> { let target = db_req.into_future().await?; for name in ALL_STORES { - let tx = target.transaction_on_one_with_mode(name, IdbTransactionMode::Readwrite)?; - - let obj = tx.object_store(name)?; - - if let Some(curs) = source - .transaction_on_one_with_mode(name, IdbTransactionMode::Readonly)? - .object_store(name)? + let source_tx = source.transaction_on_one_with_mode(name, IdbTransactionMode::Readonly)?; + let source_obj = source_tx.object_store(name)?; + let Some(curs) = source_obj .open_cursor()? - .await? - { - while let Some(key) = curs.key() { - obj.put_key_val(&key, &curs.value())?; + .await? else { + continue; + }; - curs.continue_cursor()?.await?; - } + let data = curs.into_vec(0).await?; + + let target_tx = target.transaction_on_one_with_mode(name, IdbTransactionMode::Readwrite)?; + let target_obj = target_tx.object_store(name)?; + + for kv in data { + target_obj.put_key_val(kv.key(), kv.value())?; } - tx.await.into_result()?; + target_tx.await.into_result()?; } let tx = @@ -236,7 +242,7 @@ async fn backup(source: &IdbDatabase, meta: &IdbDatabase) -> Result<()> { Ok(()) } -async fn v1_2_fix_store( +async fn v3_fix_store( store: &IdbObjectStore<'_>, store_cipher: Option<&StoreCipher>, ) -> Result<()> { @@ -275,14 +281,15 @@ async fn v1_2_fix_store( Ok(()) } -async fn migrate_to_v1_2(db: &IdbDatabase, store_cipher: Option<&StoreCipher>) -> Result<()> { +/// Fix serialized redacted state events. +async fn migrate_to_v3(db: &IdbDatabase, store_cipher: Option<&StoreCipher>) -> Result<()> { let tx = db.transaction_on_multi_with_mode( &[KEYS::ROOM_STATE, KEYS::ROOM_INFOS], IdbTransactionMode::Readwrite, )?; - v1_2_fix_store(&tx.object_store(KEYS::ROOM_STATE)?, store_cipher).await?; - v1_2_fix_store(&tx.object_store(KEYS::ROOM_INFOS)?, store_cipher).await?; + v3_fix_store(&tx.object_store(KEYS::ROOM_STATE)?, store_cipher).await?; + v3_fix_store(&tx.object_store(KEYS::ROOM_INFOS)?, store_cipher).await?; tx.await.into_result().map_err(|e| e.into()) } @@ -291,8 +298,9 @@ async fn migrate_to_v1_2(db: &IdbDatabase, store_cipher: Option<&StoreCipher>) - mod tests { wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + use assert_matches::assert_matches; use indexed_db_futures::prelude::*; - use matrix_sdk_base::StateStore; + use matrix_sdk_base::{StateStore, StoreError}; use matrix_sdk_test::async_test; use ruma::{ events::{AnySyncStateEvent, StateEventType}, @@ -302,18 +310,27 @@ mod tests { use uuid::Uuid; use wasm_bindgen::JsValue; - use super::{serialize_event, MigrationConflictStrategy, Result, ALL_STORES, KEYS}; - use crate::{safe_encode::SafeEncode, IndexeddbStateStore, IndexeddbStateStoreError}; + use super::{MigrationConflictStrategy, CURRENT_DB_VERSION, CURRENT_META_DB_VERSION}; + use crate::{ + safe_encode::SafeEncode, + state_store::{serialize_event, Result, ALL_STORES, KEYS}, + IndexeddbStateStore, IndexeddbStateStoreError, + }; - pub async fn create_fake_db(name: &str, version: f64) -> Result { - let mut db_req: OpenDbRequest = IdbDatabase::open_f64(name, version)?; + const CUSTOM_DATA_KEY: &[u8] = b"custom_data_key"; + const CUSTOM_DATA: &[u8] = b"some_custom_data"; + + pub async fn create_fake_db(name: &str, version: u32) -> Result { + let mut db_req: OpenDbRequest = IdbDatabase::open_u32(name, version)?; db_req.set_on_upgrade_needed(Some( move |evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { - // migrating to version 1 let db = evt.db(); + + // Initialize stores. for name in ALL_STORES { db.create_object_store(name)?; } + Ok(()) }, )); @@ -321,38 +338,78 @@ mod tests { } #[async_test] - pub async fn test_no_upgrade() -> Result<()> { - let name = format!("simple-1.1-no-cipher-{}", Uuid::new_v4().as_hyphenated().to_string()); + pub async fn test_new_store() -> Result<()> { + let name = format!("new-store-no-cipher-{}", Uuid::new_v4().as_hyphenated().to_string()); // this transparently migrates to the latest version let store = IndexeddbStateStore::builder().name(name).build().await?; // this didn't create any backup assert_eq!(store.has_backups().await?, false); // simple check that the layout exists. - assert_eq!(store.get_sync_token().await?, None); + assert_eq!(store.get_custom_value(CUSTOM_DATA_KEY).await?, None); + + // Check versions. + assert_eq!(store.version(), CURRENT_DB_VERSION); + assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION); + Ok(()) } #[async_test] - pub async fn test_migrating_v1_to_1_1_plain() -> Result<()> { - let name = - format!("migrating-1.1-no-cipher-{}", Uuid::new_v4().as_hyphenated().to_string()); - create_fake_db(&name, 1.0).await?; + pub async fn test_migrating_v1_to_v2_plain() -> Result<()> { + let name = format!("migrating-v2-no-cipher-{}", Uuid::new_v4().as_hyphenated().to_string()); + + // Create and populate db. + { + let db = create_fake_db(&name, 1).await?; + let tx = + db.transaction_on_one_with_mode(KEYS::CUSTOM, IdbTransactionMode::Readwrite)?; + let custom = tx.object_store(KEYS::CUSTOM)?; + let jskey = JsValue::from_str( + core::str::from_utf8(CUSTOM_DATA_KEY).map_err(StoreError::Codec)?, + ); + custom.put_key_val(&jskey, &serialize_event(None, &CUSTOM_DATA)?)?; + tx.await.into_result()?; + db.close(); + } // this transparently migrates to the latest version let store = IndexeddbStateStore::builder().name(name).build().await?; // this didn't create any backup assert_eq!(store.has_backups().await?, false); - assert_eq!(store.get_sync_token().await?, None); + // Custom data is still there. + let stored_data = assert_matches!( + store.get_custom_value(CUSTOM_DATA_KEY).await?, + Some(d) => d + ); + assert_eq!(stored_data, CUSTOM_DATA); + + // Check versions. + assert_eq!(store.version(), CURRENT_DB_VERSION); + assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION); + Ok(()) } #[async_test] - pub async fn test_migrating_v1_to_1_1_with_pw() -> Result<()> { + pub async fn test_migrating_v1_to_v2_with_pw() -> Result<()> { let name = - format!("migrating-1.1-with-cipher-{}", Uuid::new_v4().as_hyphenated().to_string()); + format!("migrating-v2-with-cipher-{}", Uuid::new_v4().as_hyphenated().to_string()); let passphrase = "somepassphrase".to_owned(); - create_fake_db(&name, 1.0).await?; + + // Create and populate db. + { + let db = create_fake_db(&name, 1).await?; + let tx = + db.transaction_on_one_with_mode(KEYS::CUSTOM, IdbTransactionMode::Readwrite)?; + let custom = tx.object_store(KEYS::CUSTOM)?; + let jskey = JsValue::from_str( + core::str::from_utf8(CUSTOM_DATA_KEY).map_err(StoreError::Codec)?, + ); + custom.put_key_val(&jskey, &serialize_event(None, &CUSTOM_DATA)?)?; + tx.await.into_result()?; + db.close(); + } // this transparently migrates to the latest version let store = @@ -360,18 +417,37 @@ mod tests { // this creates a backup by default assert_eq!(store.has_backups().await?, true); assert!(store.latest_backup().await?.is_some(), "No backup_found"); - assert_eq!(store.get_sync_token().await?, None); + // the data is gone + assert_eq!(store.get_custom_value(CUSTOM_DATA_KEY).await?, None); + + // Check versions. + assert_eq!(store.version(), CURRENT_DB_VERSION); + assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION); + Ok(()) } #[async_test] - pub async fn test_migrating_v1_to_1_1_with_pw_drops() -> Result<()> { + pub async fn test_migrating_v1_to_v2_with_pw_drops() -> Result<()> { let name = format!( - "migrating-1.1-with-cipher-drops-{}", + "migrating-v2-with-cipher-drops-{}", Uuid::new_v4().as_hyphenated().to_string() ); let passphrase = "some-other-passphrase".to_owned(); - create_fake_db(&name, 1.0).await?; + + // Create and populate db. + { + let db = create_fake_db(&name, 1).await?; + let tx = + db.transaction_on_one_with_mode(KEYS::CUSTOM, IdbTransactionMode::Readwrite)?; + let custom = tx.object_store(KEYS::CUSTOM)?; + let jskey = JsValue::from_str( + core::str::from_utf8(CUSTOM_DATA_KEY).map_err(StoreError::Codec)?, + ); + custom.put_key_val(&jskey, &serialize_event(None, &CUSTOM_DATA)?)?; + tx.await.into_result()?; + db.close(); + } // this transparently migrates to the latest version let store = IndexeddbStateStore::builder() @@ -380,20 +456,39 @@ mod tests { .migration_conflict_strategy(MigrationConflictStrategy::Drop) .build() .await?; - // this creates a backup by default + // this doesn't create a backup assert_eq!(store.has_backups().await?, false); - assert_eq!(store.get_sync_token().await?, None); + // the data is gone + assert_eq!(store.get_custom_value(CUSTOM_DATA_KEY).await?, None); + + // Check versions. + assert_eq!(store.version(), CURRENT_DB_VERSION); + assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION); + Ok(()) } #[async_test] - pub async fn test_migrating_v1_to_1_1_with_pw_raise() -> Result<()> { + pub async fn test_migrating_v1_to_v2_with_pw_raise() -> Result<()> { let name = format!( - "migrating-1.1-with-cipher-raises-{}", + "migrating-v2-with-cipher-raises-{}", Uuid::new_v4().as_hyphenated().to_string() ); let passphrase = "some-other-passphrase".to_owned(); - create_fake_db(&name, 1.0).await?; + + // Create and populate db. + { + let db = create_fake_db(&name, 1).await?; + let tx = + db.transaction_on_one_with_mode(KEYS::CUSTOM, IdbTransactionMode::Readwrite)?; + let custom = tx.object_store(KEYS::CUSTOM)?; + let jskey = JsValue::from_str( + core::str::from_utf8(CUSTOM_DATA_KEY).map_err(StoreError::Codec)?, + ); + custom.put_key_val(&jskey, &serialize_event(None, &CUSTOM_DATA)?)?; + tx.await.into_result()?; + db.close(); + } // this transparently migrates to the latest version let store_res = IndexeddbStateStore::builder() @@ -403,17 +498,15 @@ mod tests { .build() .await; - if let Err(IndexeddbStateStoreError::MigrationConflict { .. }) = store_res { - // all fine! - } else { - assert!(false, "Conflict didn't raise: {:?}", store_res) - } + assert_matches!(store_res, Err(IndexeddbStateStoreError::MigrationConflict { .. })); + Ok(()) } #[async_test] - pub async fn test_migrating_to_v1_2() -> Result<()> { - let name = format!("migrating-1.2-{}", Uuid::new_v4().as_hyphenated().to_string()); + pub async fn test_migrating_to_v3() -> Result<()> { + let name = format!("migrating-v3-{}", Uuid::new_v4().as_hyphenated().to_string()); + // An event that fails to deserialize. let wrong_redacted_state_event = json!({ "content": null, @@ -441,13 +534,14 @@ mod tests { // Populate DB with wrong event. { - let db = create_fake_db(&name, 1.1).await?; + let db = create_fake_db(&name, 2).await?; let tx = db.transaction_on_one_with_mode(KEYS::ROOM_STATE, IdbTransactionMode::Readwrite)?; let state = tx.object_store(KEYS::ROOM_STATE)?; let key = (room_id, StateEventType::RoomTopic, "").encode(); state.put_key_val(&key, &serialize_event(None, &wrong_redacted_state_event)?)?; tx.await.into_result()?; + db.close(); } // this transparently migrates to the latest version @@ -456,6 +550,10 @@ mod tests { store.get_state_event(room_id, StateEventType::RoomTopic, "").await.unwrap().unwrap(); event.deserialize().unwrap(); + // Check versions. + assert_eq!(store.version(), CURRENT_DB_VERSION); + assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION); + Ok(()) } } diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index 15f72da7b..982129918 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -62,7 +62,7 @@ pub enum IndexeddbStateStoreError { #[error(transparent)] StoreError(#[from] StoreError), #[error("Can't migrate {name} from {old_version} to {new_version} without deleting data. See MigrationConflictStrategy for ways to configure.")] - MigrationConflict { name: String, old_version: f64, new_version: f64 }, + MigrationConflict { name: String, old_version: u32, new_version: u32 }, } impl From for IndexeddbStateStoreError { @@ -246,6 +246,16 @@ impl IndexeddbStateStore { IndexeddbStateStoreBuilder::new() } + /// The version of the database containing the data. + pub fn version(&self) -> u32 { + self.inner.version() as u32 + } + + /// The version of the database containing the metadata. + pub fn meta_version(&self) -> u32 { + self.meta.version() as u32 + } + /// Whether this database has any migration backups pub async fn has_backups(&self) -> Result { Ok(self From 99cbf3122b8aef77f633756e817b96fa27b9b461 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 7 Mar 2023 10:39:23 +0100 Subject: [PATCH 127/166] ci: Work around frequent codecov upload errors --- .github/workflows/coverage.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 8680117ef..07355e53e 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -55,6 +55,9 @@ jobs: - name: Upload to codecov.io uses: codecov/codecov-action@v3 with: + # Work around frequent upload errors, for runs inside the main repo (not PRs from forks). + # Otherwise not required for public repos. + token: ${{ secrets.CODECOV_UPLOAD_TOKEN }} # The upload sometimes fails due to https://github.com/codecov/codecov-action/issues/837. # To make sure that the failure gets flagged clearly in the UI, fail the action. fail_ci_if_error: true From 3ff1844da4fe4861a74364fd46128846c196b490 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 7 Mar 2023 12:32:20 +0100 Subject: [PATCH 128/166] base: Remove unused Deserialize implementation --- crates/matrix-sdk-base/src/deserialized_responses.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-base/src/deserialized_responses.rs b/crates/matrix-sdk-base/src/deserialized_responses.rs index f96ca21fe..0ccc71f17 100644 --- a/crates/matrix-sdk-base/src/deserialized_responses.rs +++ b/crates/matrix-sdk-base/src/deserialized_responses.rs @@ -25,7 +25,7 @@ use ruma::{ serde::Raw, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UserId, }; -use serde::{Deserialize, Serialize}; +use serde::Serialize; /// A change in ambiguity of room members that an `m.room.member` event /// triggers. @@ -83,7 +83,7 @@ impl RawMemberEvent { /// Wrapper around both MemberEvent-Types #[allow(clippy::large_enum_variant)] -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug)] pub enum MemberEvent { /// A member event from a room in joined or left state. Sync(SyncRoomMemberEvent), From 6ce51b589087537b1564531933b635b979f3469c Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 7 Mar 2023 12:33:04 +0100 Subject: [PATCH 129/166] Remove unnecessary allow attribute --- crates/matrix-sdk-base/src/deserialized_responses.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/matrix-sdk-base/src/deserialized_responses.rs b/crates/matrix-sdk-base/src/deserialized_responses.rs index 0ccc71f17..8d09b617f 100644 --- a/crates/matrix-sdk-base/src/deserialized_responses.rs +++ b/crates/matrix-sdk-base/src/deserialized_responses.rs @@ -82,7 +82,6 @@ impl RawMemberEvent { } /// Wrapper around both MemberEvent-Types -#[allow(clippy::large_enum_variant)] #[derive(Clone, Debug)] pub enum MemberEvent { /// A member event from a room in joined or left state. From 0c4c73084db8bf5ac111819aad05043d3aac6f74 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 7 Mar 2023 12:33:17 +0100 Subject: [PATCH 130/166] Appease clippy --- xtask/src/kotlin.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xtask/src/kotlin.rs b/xtask/src/kotlin.rs index d9055da34..4a49946e0 100644 --- a/xtask/src/kotlin.rs +++ b/xtask/src/kotlin.rs @@ -160,7 +160,7 @@ fn build_for_android_target( // The builtin dev profile has its files stored under target/debug, all // other targets have matching directory names let profile_dir_name = if profile == "dev" { "debug" } else { profile }; - let package_camel = package_name.replace("-", "_"); + let package_camel = package_name.replace('-', "_"); let lib_name = format!("lib{package_camel}.so"); Ok(workspace::target_path()?.join(target).join(profile_dir_name).join(lib_name)) } From 9324702b0426c7feb76c210a9192a2d09d503768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 30 Jan 2023 16:15:20 +0100 Subject: [PATCH 131/166] feat(bindings): Add a method to partially migrate data --- bindings/matrix-sdk-crypto-ffi/src/lib.rs | 278 +++++++++++++++------ bindings/matrix-sdk-crypto-ffi/src/olm.udl | 18 ++ 2 files changed, 217 insertions(+), 79 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/lib.rs b/bindings/matrix-sdk-crypto-ffi/src/lib.rs index 18f0b91ae..3925fdfc2 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/lib.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/lib.rs @@ -31,6 +31,8 @@ pub use machine::{KeyRequestPair, OlmMachine, SignatureVerification}; use matrix_sdk_common::deserialized_responses::VerificationState; use matrix_sdk_crypto::{ backups::SignatureState, + olm::{IdentityKeys, InboundGroupSession, Session}, + store::{Changes, CryptoStore}, types::{EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey}, EncryptionSettings as RustEncryptionSettings, LocalTrust, }; @@ -41,15 +43,17 @@ pub use responses::{ }; use ruma::{ events::room::history_visibility::HistoryVisibility as RustHistoryVisibility, DeviceId, - DeviceKeyAlgorithm, OwnedUserId, RoomId, SecondsSinceUnixEpoch, UserId, + DeviceKeyAlgorithm, OwnedDeviceId, OwnedUserId, RoomId, SecondsSinceUnixEpoch, UserId, }; use serde::{Deserialize, Serialize}; +use tokio::runtime::Runtime; pub use users::UserIdentity; pub use verification::{ CancelInfo, ConfirmVerificationResult, QrCode, QrCodeListener, QrCodeState, RequestVerificationResult, Sas, SasListener, SasState, ScanResult, StartSasResult, Verification, VerificationRequest, VerificationRequestListener, VerificationRequestState, }; +use vodozemac::{Curve25519PublicKey, Ed25519PublicKey}; /// Struct collecting data that is important to migrate to the rust-sdk #[derive(Deserialize, Serialize)] @@ -72,6 +76,24 @@ pub struct MigrationData { tracked_users: Vec, } +/// Struct collecting data that is important to migrate sessions to the rust-sdk +pub struct SessionMigrationData { + /// The user id that the data belongs to. + user_id: String, + /// The device id that the data belongs to. + device_id: String, + /// The Curve25519 public key of the Account that owns this data. + curve25519_key: String, + /// The Ed25519 public key of the Account that owns this data. + ed25519_key: String, + /// The list of pickleds Olm Sessions. + sessions: Vec, + /// The list of pickled Megolm inbound group sessions. + inbound_group_sessions: Vec, + /// The Olm pickle key that was used to pickle all the Olm objects. + pickle_key: Vec, +} + /// A pickled version of an `Account`. /// /// Holds all the information that needs to be stored in a database to restore @@ -149,17 +171,17 @@ impl From for MigrationError { } } -/// Migrate a libolm based setup to a vodozemac based setup stored in a Sled +/// Migrate a libolm based setup to a vodozemac based setup stored in a SQLite /// store. /// /// # Arguments /// -/// * `data` - The data that should be migrated over to the Sled store. +/// * `data` - The data that should be migrated over to the SQLite store. /// -/// * `path` - The path where the Sled store should be created. +/// * `path` - The path where the SQLite store should be created. /// /// * `passphrase` - The passphrase that should be used to encrypt the data at -/// rest in the Sled store. **Warning**, if no passphrase is given, the store +/// rest in the SQLite store. **Warning**, if no passphrase is given, the store /// and all its data will remain unencrypted. /// /// * `progress_listener` - A callback that can be used to introspect the @@ -170,7 +192,6 @@ pub fn migrate( passphrase: Option, progress_listener: Box, ) -> anyhow::Result<()> { - use tokio::runtime::Runtime; let runtime = Runtime::new()?; runtime.block_on(async move { migrate_data(data, path, passphrase, progress_listener).await?; @@ -184,15 +205,8 @@ async fn migrate_data( passphrase: Option, progress_listener: Box, ) -> anyhow::Result<()> { - use matrix_sdk_crypto::{ - olm::PrivateCrossSigningIdentity, - store::{Changes as RustChanges, CryptoStore, RecoveryKey}, - }; - use vodozemac::{ - megolm::InboundGroupSession, - olm::{Account, Session}, - Curve25519PublicKey, - }; + use matrix_sdk_crypto::{olm::PrivateCrossSigningIdentity, store::RecoveryKey}; + use vodozemac::olm::Account; use zeroize::Zeroize; // The total steps here include all the sessions/inbound group sessions and @@ -238,69 +252,17 @@ async fn migrate_data( processed_steps += 1; listener(processed_steps, total_steps); - let mut sessions = Vec::new(); - - for session_pickle in data.sessions { - let pickle = - Session::from_libolm_pickle(&session_pickle.pickle, &data.pickle_key)?.pickle(); - - let creation_time = SecondsSinceUnixEpoch(UInt::from_str(&session_pickle.creation_time)?); - let last_use_time = SecondsSinceUnixEpoch(UInt::from_str(&session_pickle.last_use_time)?); - - let pickle = matrix_sdk_crypto::olm::PickledSession { - pickle, - sender_key: Curve25519PublicKey::from_base64(&session_pickle.sender_key)?, - created_using_fallback_key: session_pickle.created_using_fallback_key, - creation_time, - last_use_time, - }; - - let session = matrix_sdk_crypto::olm::Session::from_pickle( - user_id.clone(), - device_id.clone(), - identity_keys.clone(), - pickle, - ); - - sessions.push(session); - processed_steps += 1; - listener(processed_steps, total_steps); - } - - let mut inbound_group_sessions = Vec::new(); - - for session in data.inbound_group_sessions { - let pickle = - InboundGroupSession::from_libolm_pickle(&session.pickle, &data.pickle_key)?.pickle(); - - let sender_key = Curve25519PublicKey::from_base64(&session.sender_key)?; - - let pickle = matrix_sdk_crypto::olm::PickledInboundGroupSession { - pickle, - sender_key, - signing_key: session - .signing_key - .into_iter() - .map(|(k, v)| { - let algorithm = DeviceKeyAlgorithm::try_from(k)?; - let key = SigningKey::from_parts(&algorithm, v)?; - - Ok((algorithm, key)) - }) - .collect::>()?, - room_id: RoomId::parse(session.room_id)?, - imported: session.imported, - backed_up: session.backed_up, - history_visibility: None, - algorithm: RustEventEncryptionAlgorithm::MegolmV1AesSha2, - }; - - let session = matrix_sdk_crypto::olm::InboundGroupSession::from_pickle(pickle)?; - - inbound_group_sessions.push(session); - processed_steps += 1; - listener(processed_steps, total_steps); - } + let (sessions, inbound_group_sessions) = collect_sessions( + processed_steps, + total_steps, + &listener, + &data.pickle_key, + user_id.clone(), + device_id, + identity_keys, + data.sessions, + data.inbound_group_sessions, + )?; let recovery_key = data.backup_recovery_key.map(|k| RecoveryKey::from_base58(k.as_str())).transpose()?; @@ -333,7 +295,7 @@ async fn migrate_data( processed_steps += 1; listener(processed_steps, total_steps); - let changes = RustChanges { + let changes = Changes { account: Some(account), private_identity: Some(cross_signing), sessions, @@ -342,6 +304,17 @@ async fn migrate_data( backup_version: data.backup_version, ..Default::default() }; + + save_changes(processed_steps, total_steps, &listener, changes, &store).await +} + +async fn save_changes( + mut processed_steps: usize, + total_steps: usize, + listener: &dyn Fn(usize, usize), + changes: Changes, + store: &SqliteCryptoStore, +) -> anyhow::Result<()> { store.save_changes(changes).await?; processed_steps += 1; @@ -350,6 +323,153 @@ async fn migrate_data( Ok(()) } +/// Migrate sessions and group sessions of a libolm based setup to a vodozemac +/// based setup stored in a SQLite store. +/// +/// This method allows you to migrate a subset of the data, it should only be +/// used after the [`migrate()`] method has been already used. +/// +/// # Arguments +/// +/// * `data` - The data that should be migrated over to the SQLite store. +/// +/// * `path` - The path where the SQLite store should be created. +/// +/// * `passphrase` - The passphrase that should be used to encrypt the data at +/// rest in the SQLite store. **Warning**, if no passphrase is given, the store +/// and all its data will remain unencrypted. +/// +/// * `progress_listener` - A callback that can be used to introspect the +/// progress of the migration. +pub fn migrate_sessions( + data: SessionMigrationData, + path: &str, + passphrase: Option, + progress_listener: Box, +) -> anyhow::Result<()> { + let runtime = Runtime::new()?; + runtime.block_on(migrate_session_data(data, path, passphrase, progress_listener)) +} + +async fn migrate_session_data( + data: SessionMigrationData, + path: &str, + passphrase: Option, + progress_listener: Box, +) -> anyhow::Result<()> { + let store = SqliteCryptoStore::open(path, passphrase.as_deref()).await?; + + let listener = |progress: usize, total: usize| { + progress_listener.on_progress(progress as i32, total as i32) + }; + + let total_steps = 1 + data.sessions.len() + data.inbound_group_sessions.len(); + let processed_steps = 0; + + let user_id = UserId::parse(data.user_id)?.into(); + let device_id: OwnedDeviceId = data.device_id.into(); + + let identity_keys = IdentityKeys { + ed25519: Ed25519PublicKey::from_base64(&data.ed25519_key)?, + curve25519: Curve25519PublicKey::from_base64(&data.curve25519_key)?, + } + .into(); + + let (sessions, inbound_group_sessions) = collect_sessions( + processed_steps, + total_steps, + &listener, + &data.pickle_key, + user_id, + device_id.into(), + identity_keys, + data.sessions, + data.inbound_group_sessions, + )?; + + let changes = Changes { sessions, inbound_group_sessions, ..Default::default() }; + save_changes(processed_steps, total_steps, &listener, changes, &store).await +} + +#[allow(clippy::too_many_arguments)] +fn collect_sessions( + mut processed_steps: usize, + total_steps: usize, + listener: &dyn Fn(usize, usize), + pickle_key: &[u8], + user_id: Arc, + device_id: Arc, + identity_keys: Arc, + session_pickles: Vec, + group_session_pickles: Vec, +) -> anyhow::Result<(Vec, Vec)> { + let mut sessions = Vec::new(); + + for session_pickle in session_pickles { + let pickle = + vodozemac::olm::Session::from_libolm_pickle(&session_pickle.pickle, pickle_key)? + .pickle(); + + let creation_time = SecondsSinceUnixEpoch(UInt::from_str(&session_pickle.creation_time)?); + let last_use_time = SecondsSinceUnixEpoch(UInt::from_str(&session_pickle.last_use_time)?); + + let pickle = matrix_sdk_crypto::olm::PickledSession { + pickle, + sender_key: Curve25519PublicKey::from_base64(&session_pickle.sender_key)?, + created_using_fallback_key: session_pickle.created_using_fallback_key, + creation_time, + last_use_time, + }; + + let session = + Session::from_pickle(user_id.clone(), device_id.clone(), identity_keys.clone(), pickle); + + sessions.push(session); + processed_steps += 1; + listener(processed_steps, total_steps); + } + + let mut inbound_group_sessions = Vec::new(); + + for session in group_session_pickles { + let pickle = vodozemac::megolm::InboundGroupSession::from_libolm_pickle( + &session.pickle, + pickle_key, + )? + .pickle(); + + let sender_key = Curve25519PublicKey::from_base64(&session.sender_key)?; + + let pickle = matrix_sdk_crypto::olm::PickledInboundGroupSession { + pickle, + sender_key, + signing_key: session + .signing_key + .into_iter() + .map(|(k, v)| { + let algorithm = DeviceKeyAlgorithm::try_from(k)?; + let key = SigningKey::from_parts(&algorithm, v)?; + + Ok((algorithm, key)) + }) + .collect::>()?, + room_id: RoomId::parse(session.room_id)?, + imported: session.imported, + backed_up: session.backed_up, + history_visibility: None, + algorithm: RustEventEncryptionAlgorithm::MegolmV1AesSha2, + }; + + let session = matrix_sdk_crypto::olm::InboundGroupSession::from_pickle(pickle)?; + + inbound_group_sessions.push(session); + processed_steps += 1; + listener(processed_steps, total_steps); + } + + Ok((sessions, inbound_group_sessions)) +} + /// Callback that will be passed over the FFI to report progress pub trait ProgressListener { /// The callback that should be called on the Rust side diff --git a/bindings/matrix-sdk-crypto-ffi/src/olm.udl b/bindings/matrix-sdk-crypto-ffi/src/olm.udl index 5a3155755..7be41032f 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/olm.udl +++ b/bindings/matrix-sdk-crypto-ffi/src/olm.udl @@ -7,6 +7,14 @@ namespace matrix_sdk_crypto_ffi { string? passphrase, ProgressListener progress_listener ); + [Throws=MigrationError] + void migrate_sessions( + SessionMigrationData data, + [ByRef] string path, + string? passphrase, + ProgressListener progress_listener + ); + }; [Error] @@ -504,6 +512,16 @@ dictionary MigrationData { sequence tracked_users; }; +dictionary SessionMigrationData { + string user_id; + string device_id; + string curve25519_key; + string ed25519_key; + sequence sessions; + sequence inbound_group_sessions; + sequence pickle_key; +}; + dictionary PickledAccount { string user_id; string device_id; From 4d1363e6836d6474253cb575f8cbeccc63d9d7e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Fri, 3 Mar 2023 14:42:49 +0100 Subject: [PATCH 132/166] indexeddb: Merge session and sync_token stores into new kv store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- .../src/state_store/migrations.rs | 245 +++++++++++++++--- .../src/state_store/mod.rs | 67 +++-- 2 files changed, 249 insertions(+), 63 deletions(-) diff --git a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs index f6d68d3ce..fbee5c291 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::Arc; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; use gloo_utils::format::JsValueSerdeExt; use indexed_db_futures::{prelude::*, request::OpenDbRequest, IdbDatabase, IdbVersionChangeEvent}; @@ -23,10 +26,12 @@ use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue}; use wasm_bindgen::JsValue; use web_sys::IdbTransactionMode; -use super::{deserialize_event, serialize_event, Result, ALL_STORES, KEYS}; +use super::{ + deserialize_event, encode_key, encode_to_range, serialize_event, Result, ALL_STORES, KEYS, +}; use crate::IndexeddbStateStoreError; -const CURRENT_DB_VERSION: u32 = 3; +const CURRENT_DB_VERSION: u32 = 4; const CURRENT_META_DB_VERSION: u32 = 2; /// Sometimes Migrations can't proceed without having to drop existing @@ -47,6 +52,13 @@ pub enum MigrationConflictStrategy { #[derive(Clone, Serialize, Deserialize)] struct StoreKeyWrapper(Vec); +#[allow(non_snake_case)] +mod OLD_KEYS { + // Old stores + pub const SESSION: &str = "session"; + pub const SYNC_TOKEN: &str = "sync_token"; +} + pub async fn upgrade_meta_db( meta_name: &str, passphrase: Option<&str>, @@ -104,14 +116,37 @@ pub async fn upgrade_meta_db( Ok((meta_db, store_cipher)) } +// Helper struct for upgrading the inner DB. +#[derive(Debug, Clone, Default)] +pub struct OngoingMigration { + // Names of stores to drop. + drop_stores: HashSet<&'static str>, + // Names of stores to create. + create_stores: HashSet<&'static str>, + // Store name => key-value data to add. + data: HashMap<&'static str, Vec<(JsValue, JsValue)>>, +} + +impl OngoingMigration { + /// Merge this migration with the given one. + fn merge(&mut self, other: OngoingMigration) { + self.drop_stores.extend(other.drop_stores); + self.create_stores.extend(other.create_stores); + + for (store, data) in other.data { + let entry = self.data.entry(store).or_default(); + entry.extend(data); + } + } +} + pub async fn upgrade_inner_db( name: &str, store_cipher: Option<&StoreCipher>, migration_strategy: MigrationConflictStrategy, meta_db: &IdbDatabase, ) -> Result { - let mut should_create_stores = false; - let mut should_drop_stores = false; + let mut migration = OngoingMigration::default(); { // This is a hack, we need to open the database a first time to get the current // version. @@ -138,17 +173,17 @@ pub async fn upgrade_inner_db( // Upgrades to v1 and v2 (re)create empty stores, while the other upgrades // change data that is already in the stores, so we use exclusive branches here. if old_version == 0 { - should_create_stores = true; + migration.create_stores.extend(ALL_STORES); } else if old_version < 2 && has_store_cipher { match migration_strategy { MigrationConflictStrategy::BackupAndDrop => { backup_v1(&pre_db, meta_db).await?; - should_drop_stores = true; - should_create_stores = true; + migration.drop_stores.extend(V1_STORES); + migration.create_stores.extend(ALL_STORES); } MigrationConflictStrategy::Drop => { - should_drop_stores = true; - should_create_stores = true; + migration.drop_stores.extend(V1_STORES); + migration.create_stores.extend(ALL_STORES); } MigrationConflictStrategy::Raise => { return Err(IndexeddbStateStoreError::MigrationConflict { @@ -158,8 +193,13 @@ pub async fn upgrade_inner_db( }); } } - } else if old_version < 3 { - migrate_to_v3(&pre_db, store_cipher).await?; + } else { + if old_version < 3 { + migrate_to_v3(&pre_db, store_cipher).await?; + } + if old_version < 4 { + migration.merge(migrate_to_v4(&pre_db, store_cipher).await?); + } } pre_db.close(); @@ -168,33 +208,59 @@ pub async fn upgrade_inner_db( let mut db_req: OpenDbRequest = IdbDatabase::open_u32(name, CURRENT_DB_VERSION)?; db_req.set_on_upgrade_needed(Some(move |evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { // Changing the format can only happen in the upgrade procedure - if should_drop_stores { - drop_stores(evt.db())?; + for store in &migration.drop_stores { + evt.db().delete_object_store(store)?; } - - if should_create_stores { - create_stores(evt.db())?; + for store in &migration.create_stores { + evt.db().create_object_store(store)?; } Ok(()) })); - Ok(db_req.into_future().await?) + let db = db_req.into_future().await?; + + // Finally, we can add data to the newly created tables if needed. + if !migration.data.is_empty() { + let stores: Vec<_> = migration.data.keys().copied().collect(); + let tx = db.transaction_on_multi_with_mode(&stores, IdbTransactionMode::Readwrite)?; + + for (name, data) in migration.data { + let store = tx.object_store(name)?; + for (key, value) in data { + store.put_key_val(&key, &value)?; + } + } + + tx.await.into_result()?; + } + + Ok(db) } -fn drop_stores(db: &IdbDatabase) -> Result<(), JsValue> { - for name in ALL_STORES { - db.delete_object_store(name)?; - } - Ok(()) -} - -fn create_stores(db: &IdbDatabase) -> Result<(), JsValue> { - for name in ALL_STORES { - db.create_object_store(name)?; - } - Ok(()) -} +pub const V1_STORES: &[&str] = &[ + OLD_KEYS::SESSION, + KEYS::ACCOUNT_DATA, + KEYS::MEMBERS, + KEYS::PROFILES, + KEYS::DISPLAY_NAMES, + KEYS::JOINED_USER_IDS, + KEYS::INVITED_USER_IDS, + KEYS::ROOM_STATE, + KEYS::ROOM_INFOS, + KEYS::PRESENCE, + KEYS::ROOM_ACCOUNT_DATA, + KEYS::STRIPPED_ROOM_INFOS, + KEYS::STRIPPED_MEMBERS, + KEYS::STRIPPED_ROOM_STATE, + KEYS::STRIPPED_JOINED_USER_IDS, + KEYS::STRIPPED_INVITED_USER_IDS, + KEYS::ROOM_USER_RECEIPTS, + KEYS::ROOM_EVENT_RECEIPTS, + KEYS::MEDIA, + KEYS::CUSTOM, + OLD_KEYS::SYNC_TOKEN, +]; async fn backup_v1(source: &IdbDatabase, meta: &IdbDatabase) -> Result<()> { let now = JsDate::now(); @@ -204,14 +270,14 @@ async fn backup_v1(source: &IdbDatabase, meta: &IdbDatabase) -> Result<()> { db_req.set_on_upgrade_needed(Some(move |evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { // migrating to version 1 let db = evt.db(); - for name in ALL_STORES { + for name in V1_STORES { db.create_object_store(name)?; } Ok(()) })); let target = db_req.into_future().await?; - for name in ALL_STORES { + for name in V1_STORES { let source_tx = source.transaction_on_one_with_mode(name, IdbTransactionMode::Readonly)?; let source_obj = source_tx.object_store(name)?; let Some(curs) = source_obj @@ -294,6 +360,50 @@ async fn migrate_to_v3(db: &IdbDatabase, store_cipher: Option<&StoreCipher>) -> tx.await.into_result().map_err(|e| e.into()) } +/// Move the content of the SYNC_TOKEN and SESSION stores to the new KV store. +async fn migrate_to_v4( + db: &IdbDatabase, + store_cipher: Option<&StoreCipher>, +) -> Result { + let tx = db.transaction_on_multi_with_mode( + &[OLD_KEYS::SYNC_TOKEN, OLD_KEYS::SESSION], + IdbTransactionMode::Readonly, + )?; + let mut values = Vec::new(); + + // Sync token + let sync_token_store = tx.object_store(OLD_KEYS::SYNC_TOKEN)?; + let sync_token = sync_token_store.get(&JsValue::from_str(OLD_KEYS::SYNC_TOKEN))?.await?; + + if let Some(sync_token) = sync_token { + values.push((encode_key(store_cipher, KEYS::SYNC_TOKEN, KEYS::SYNC_TOKEN), sync_token)); + } + + // Filters + let session_store = tx.object_store(OLD_KEYS::SESSION)?; + let range = encode_to_range(store_cipher, KEYS::FILTER, KEYS::FILTER)?; + if let Some(cursor) = session_store.open_cursor_with_range(&range)?.await? { + while let Some(key) = cursor.key() { + let value = cursor.value(); + values.push((key, value)); + cursor.continue_cursor()?.await?; + } + } + + tx.await.into_result()?; + + let mut data = HashMap::new(); + if !values.is_empty() { + data.insert(KEYS::KV, values); + } + + Ok(OngoingMigration { + drop_stores: [OLD_KEYS::SYNC_TOKEN, OLD_KEYS::SESSION].into_iter().collect(), + create_stores: [KEYS::KV].into_iter().collect(), + data, + }) +} + #[cfg(all(test, target_arch = "wasm32"))] mod tests { wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -310,10 +420,12 @@ mod tests { use uuid::Uuid; use wasm_bindgen::JsValue; - use super::{MigrationConflictStrategy, CURRENT_DB_VERSION, CURRENT_META_DB_VERSION}; + use super::{ + MigrationConflictStrategy, CURRENT_DB_VERSION, CURRENT_META_DB_VERSION, OLD_KEYS, V1_STORES, + }; use crate::{ safe_encode::SafeEncode, - state_store::{serialize_event, Result, ALL_STORES, KEYS}, + state_store::{encode_key, serialize_event, Result, ALL_STORES, KEYS}, IndexeddbStateStore, IndexeddbStateStoreError, }; @@ -327,8 +439,14 @@ mod tests { let db = evt.db(); // Initialize stores. - for name in ALL_STORES { - db.create_object_store(name)?; + if version < 4 { + for name in V1_STORES { + db.create_object_store(name)?; + } + } else { + for name in ALL_STORES { + db.create_object_store(name)?; + } } Ok(()) @@ -556,4 +674,57 @@ mod tests { Ok(()) } + + #[async_test] + pub async fn test_migrating_to_v4() -> Result<()> { + let name = format!("migrating-v4-{}", Uuid::new_v4().as_hyphenated().to_string()); + + let sync_token = "a_very_unique_string"; + let filter_1 = "filter_1"; + let filter_1_id = "filter_1_id"; + let filter_2 = "filter_2"; + let filter_2_id = "filter_2_id"; + + // Populate DB with old table. + { + let db = create_fake_db(&name, 3).await?; + let tx = db.transaction_on_multi_with_mode( + &[OLD_KEYS::SYNC_TOKEN, OLD_KEYS::SESSION], + IdbTransactionMode::Readwrite, + )?; + + let sync_token_store = tx.object_store(OLD_KEYS::SYNC_TOKEN)?; + sync_token_store.put_key_val( + &JsValue::from_str(OLD_KEYS::SYNC_TOKEN), + &serialize_event(None, &sync_token)?, + )?; + + let session_store = tx.object_store(OLD_KEYS::SESSION)?; + session_store.put_key_val( + &encode_key(None, KEYS::FILTER, (KEYS::FILTER, filter_1)), + &serialize_event(None, &filter_1_id)?, + )?; + session_store.put_key_val( + &encode_key(None, KEYS::FILTER, (KEYS::FILTER, filter_2)), + &serialize_event(None, &filter_2_id)?, + )?; + + tx.await.into_result()?; + db.close(); + } + + // this transparently migrates to the latest version + let store = IndexeddbStateStore::builder().name(name).build().await?; + + let stored_sync_token = store.get_sync_token().await.unwrap().unwrap(); + assert_eq!(stored_sync_token, sync_token); + + let stored_filter_1_id = store.get_filter(filter_1).await.unwrap().unwrap(); + assert_eq!(stored_filter_1_id, filter_1_id); + + let stored_filter_2_id = store.get_filter(filter_2).await.unwrap().unwrap(); + assert_eq!(stored_filter_2_id, filter_2_id); + + Ok(()) + } } diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index 982129918..2f4fe133a 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -93,7 +93,6 @@ mod KEYS { pub const INTERNAL_STATE: &str = "matrix-sdk-state"; pub const BACKUPS_META: &str = "backups"; - pub const SESSION: &str = "session"; pub const ACCOUNT_DATA: &str = "account_data"; pub const MEMBERS: &str = "members"; @@ -119,12 +118,10 @@ mod KEYS { pub const MEDIA: &str = "media"; pub const CUSTOM: &str = "custom"; + pub const KV: &str = "kv"; - pub const SYNC_TOKEN: &str = "sync_token"; - - /// All names of the state stores for convenience. + /// All names of the current state stores for convenience. pub const ALL_STORES: &[&str] = &[ - SESSION, ACCOUNT_DATA, MEMBERS, PROFILES, @@ -144,13 +141,14 @@ mod KEYS { ROOM_EVENT_RECEIPTS, MEDIA, CUSTOM, - SYNC_TOKEN, + KV, ]; // static keys pub const STORE_KEY: &str = "store_key"; pub const FILTER: &str = "filter"; + pub const SYNC_TOKEN: &str = "sync_token"; } pub use KEYS::ALL_STORES; @@ -172,6 +170,31 @@ fn deserialize_event( } } +fn encode_key(store_cipher: Option<&StoreCipher>, table_name: &str, key: T) -> JsValue +where + T: SafeEncode, +{ + match store_cipher { + Some(cipher) => key.encode_secure(table_name, cipher), + None => key.encode(), + } +} + +fn encode_to_range( + store_cipher: Option<&StoreCipher>, + table_name: &str, + key: T, +) -> Result +where + T: SafeEncode, +{ + match store_cipher { + Some(cipher) => key.encode_to_range_secure(table_name, cipher), + None => key.encode_to_range(), + } + .map_err(|e| IndexeddbStateStoreError::StoreError(StoreError::Backend(anyhow!(e).into()))) +} + /// Builder for [`IndexeddbStateStore`]. #[derive(Debug)] pub struct IndexeddbStateStoreBuilder { @@ -290,21 +313,14 @@ impl IndexeddbStateStore { where T: SafeEncode, { - match &self.store_cipher { - Some(cipher) => key.encode_secure(table_name, cipher), - None => key.encode(), - } + encode_key(self.store_cipher.as_deref(), table_name, key) } fn encode_to_range(&self, table_name: &str, key: T) -> Result where T: SafeEncode, { - match &self.store_cipher { - Some(cipher) => key.encode_to_range_secure(table_name, cipher), - None => key.encode_to_range(), - } - .map_err(|e| IndexeddbStateStoreError::StoreError(StoreError::Backend(anyhow!(e).into()))) + encode_to_range(self.store_cipher.as_deref(), table_name, key) } pub async fn get_user_ids_stream(&self, room_id: &RoomId) -> Result> { @@ -432,9 +448,9 @@ impl_state_store! { async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<()> { let tx = self .inner - .transaction_on_one_with_mode(KEYS::SESSION, IdbTransactionMode::Readwrite)?; + .transaction_on_one_with_mode(KEYS::KV, IdbTransactionMode::Readwrite)?; - let obj = tx.object_store(KEYS::SESSION)?; + let obj = tx.object_store(KEYS::KV)?; obj.put_key_val( &self.encode_key(KEYS::FILTER, (KEYS::FILTER, filter_name)), @@ -448,8 +464,8 @@ impl_state_store! { async fn get_filter(&self, filter_name: &str) -> Result> { self.inner - .transaction_on_one_with_mode(KEYS::SESSION, IdbTransactionMode::Readonly)? - .object_store(KEYS::SESSION)? + .transaction_on_one_with_mode(KEYS::KV, IdbTransactionMode::Readonly)? + .object_store(KEYS::KV)? .get(&self.encode_key(KEYS::FILTER, (KEYS::FILTER, filter_name)))? .await? .map(|f| self.deserialize_event(f)) @@ -458,9 +474,9 @@ impl_state_store! { async fn get_sync_token(&self) -> Result> { self.inner - .transaction_on_one_with_mode(KEYS::SYNC_TOKEN, IdbTransactionMode::Readonly)? - .object_store(KEYS::SYNC_TOKEN)? - .get(&JsValue::from_str(KEYS::SYNC_TOKEN))? + .transaction_on_one_with_mode(KEYS::KV, IdbTransactionMode::Readonly)? + .object_store(KEYS::KV)? + .get(&self.encode_key(KEYS::SYNC_TOKEN, KEYS::SYNC_TOKEN))? .await? .map(|f| self.deserialize_event(f)) .transpose() @@ -468,8 +484,7 @@ impl_state_store! { async fn save_changes(&self, changes: &StateChanges) -> Result<()> { let mut stores: HashSet<&'static str> = [ - (changes.sync_token.is_some(), KEYS::SYNC_TOKEN), - (changes.session.is_some(), KEYS::SESSION), + (changes.sync_token.is_some(), KEYS::KV), (!changes.ambiguity_maps.is_empty(), KEYS::DISPLAY_NAMES), (!changes.account_data.is_empty(), KEYS::ACCOUNT_DATA), (!changes.presence.is_empty(), KEYS::PRESENCE), @@ -528,8 +543,8 @@ impl_state_store! { self.inner.transaction_on_multi_with_mode(&stores, IdbTransactionMode::Readwrite)?; if let Some(s) = &changes.sync_token { - tx.object_store(KEYS::SYNC_TOKEN)? - .put_key_val(&JsValue::from_str(KEYS::SYNC_TOKEN), &self.serialize_event(s)?)?; + tx.object_store(KEYS::KV)? + .put_key_val(&self.encode_key(KEYS::SYNC_TOKEN, KEYS::SYNC_TOKEN), &self.serialize_event(s)?)?; } if !changes.ambiguity_maps.is_empty() { From f538b5e595042afcafbd87d85a84bc4aaa666aca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Fri, 3 Mar 2023 17:25:52 +0100 Subject: [PATCH 133/166] sled: Move the state store migrations to a separate file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- .../src/state_store/migrations.rs | 312 ++++++++++++++++++ .../{state_store.rs => state_store/mod.rs} | 285 +--------------- 2 files changed, 315 insertions(+), 282 deletions(-) create mode 100644 crates/matrix-sdk-sled/src/state_store/migrations.rs rename crates/matrix-sdk-sled/src/{state_store.rs => state_store/mod.rs} (85%) diff --git a/crates/matrix-sdk-sled/src/state_store/migrations.rs b/crates/matrix-sdk-sled/src/state_store/migrations.rs new file mode 100644 index 000000000..f4798a7fc --- /dev/null +++ b/crates/matrix-sdk-sled/src/state_store/migrations.rs @@ -0,0 +1,312 @@ +// Copyright 2021 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use matrix_sdk_base::store::{Result as StoreResult, StoreError}; +use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue}; +use sled::{transaction::TransactionError, Batch, Transactional, Tree}; +use tracing::debug; + +use super::{Result, SledStateStore, SledStoreError, ALL_DB_STORES}; + +const DATABASE_VERSION: u8 = 3; + +const VERSION_KEY: &str = "state-store-version"; +const ALL_GLOBAL_KEYS: &[&str] = &[VERSION_KEY]; + +/// Sometimes Migrations can't proceed without having to drop existing +/// data. This allows you to configure, how these cases should be handled. +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum MigrationConflictStrategy { + /// Just drop the data, we don't care that we have to sync again + Drop, + /// Raise a `SledStoreError::MigrationConflict` error with the path to the + /// DB in question. The caller then has to take care about what they want + /// to do and try again after. + Raise, + /// _Default_: The _entire_ database is backed up under + /// `$path.$timestamp.backup` (this includes the crypto store if they + /// are linked), before the state tables are dropped. + BackupAndDrop, +} + +impl SledStateStore { + pub(super) fn upgrade(&mut self) -> Result<()> { + let old_version = self.db_version()?; + + if old_version == 0 { + // we are fresh, let's write the current version + return self.set_db_version(DATABASE_VERSION); + } + if old_version == DATABASE_VERSION { + // current, we don't have to do anything + return Ok(()); + }; + + debug!(old_version, new_version = DATABASE_VERSION, "Upgrading the Sled state store"); + + if old_version == 1 && self.store_cipher.is_some() { + // we stored some fields un-encrypted. Drop them to force re-creation + return Err(SledStoreError::MigrationConflict { + path: self.path.take().expect("Path must exist for a migration to fail"), + old_version: old_version.into(), + new_version: DATABASE_VERSION.into(), + }); + } + + if old_version < 3 { + self.migrate_to_v3()?; + return Ok(()); + } + + // FUTURE UPGRADE CODE GOES HERE + + // can't upgrade from that version to the new one + Err(SledStoreError::MigrationConflict { + path: self.path.take().expect("Path must exist for a migration to fail"), + old_version: old_version.into(), + new_version: DATABASE_VERSION.into(), + }) + } + + /// Get the version of the database. + /// + /// Returns `0` for a new database. + fn db_version(&self) -> Result { + Ok(self + .inner + .get(VERSION_KEY)? + .map(|v| { + let (version_bytes, _) = v.split_at(std::mem::size_of::()); + u8::from_be_bytes(version_bytes.try_into().unwrap_or_default()) + }) + .unwrap_or_default()) + } + + fn set_db_version(&self, version: u8) -> Result<()> { + self.inner.insert(VERSION_KEY, version.to_be_bytes().as_ref())?; + self.inner.flush()?; + Ok(()) + } + + pub fn drop_tables(self) -> StoreResult<()> { + for name in ALL_DB_STORES { + self.inner.drop_tree(name).map_err(StoreError::backend)?; + } + for name in ALL_GLOBAL_KEYS { + self.inner.remove(name).map_err(StoreError::backend)?; + } + + Ok(()) + } + + fn v3_fix_tree(&self, tree: &Tree, batch: &mut Batch) -> Result<()> { + fn maybe_fix_json(raw_json: &RawJsonValue) -> Result> { + let json = raw_json.get(); + + if json.contains(r#""content":null"#) { + let mut value: JsonValue = serde_json::from_str(json)?; + if let Some(content) = value.get_mut("content") { + if matches!(content, JsonValue::Null) { + *content = JsonValue::Object(Default::default()); + return Ok(Some(value)); + } + } + } + + Ok(None) + } + + for entry in tree.iter() { + let (key, value) = entry?; + let raw_json: Box = self.deserialize_value(&value)?; + + if let Some(fixed_json) = maybe_fix_json(&raw_json)? { + batch.insert(key, self.serialize_value(&fixed_json)?); + } + } + + Ok(()) + } + + fn migrate_to_v3(&self) -> Result<()> { + let mut room_info_batch = sled::Batch::default(); + self.v3_fix_tree(&self.room_info, &mut room_info_batch)?; + + let mut room_state_batch = sled::Batch::default(); + self.v3_fix_tree(&self.room_state, &mut room_state_batch)?; + + let ret: Result<(), TransactionError> = (&self.room_info, &self.room_state) + .transaction(|(room_info, room_state)| { + room_info.apply_batch(&room_info_batch)?; + room_state.apply_batch(&room_state_batch)?; + + Ok(()) + }); + ret?; + + self.set_db_version(3u8) + } +} + +#[cfg(test)] +mod test { + use matrix_sdk_test::async_test; + use ruma::{ + events::{AnySyncStateEvent, StateEventType}, + room_id, + }; + use serde_json::json; + use tempfile::TempDir; + + use super::MigrationConflictStrategy; + use crate::state_store::{Result, SledStateStore, SledStoreError, ROOM_STATE}; + + #[async_test] + pub async fn migrating_v1_to_2_plain() -> Result<()> { + let folder = TempDir::new()?; + + let store = SledStateStore::builder().path(folder.path().to_path_buf()).build()?; + + store.set_db_version(1u8)?; + drop(store); + + // this transparently migrates to the latest version + let _store = SledStateStore::builder().path(folder.path().to_path_buf()).build()?; + Ok(()) + } + + #[async_test] + pub async fn migrating_v1_to_2_with_pw_backed_up() -> Result<()> { + let folder = TempDir::new()?; + + let store = SledStateStore::builder() + .path(folder.path().to_path_buf()) + .passphrase("something".to_owned()) + .build()?; + + store.set_db_version(1u8)?; + drop(store); + + // this transparently creates a backup and a fresh db + let _store = SledStateStore::builder() + .path(folder.path().to_path_buf()) + .passphrase("something".to_owned()) + .build()?; + assert_eq!(std::fs::read_dir(folder.path())?.count(), 2); + Ok(()) + } + + #[async_test] + pub async fn migrating_v1_to_2_with_pw_drop() -> Result<()> { + let folder = TempDir::new()?; + + let store = SledStateStore::builder() + .path(folder.path().to_path_buf()) + .passphrase("other thing".to_owned()) + .build()?; + + store.set_db_version(1u8)?; + drop(store); + + // this transparently creates a backup and a fresh db + let _store = SledStateStore::builder() + .path(folder.path().to_path_buf()) + .passphrase("other thing".to_owned()) + .migration_conflict_strategy(MigrationConflictStrategy::Drop) + .build()?; + assert_eq!(std::fs::read_dir(folder.path())?.count(), 1); + Ok(()) + } + + #[async_test] + pub async fn migrating_v1_to_2_with_pw_raises() -> Result<()> { + let folder = TempDir::new()?; + + let store = SledStateStore::builder() + .path(folder.path().to_path_buf()) + .passphrase("secret".to_owned()) + .build()?; + + store.set_db_version(1u8)?; + drop(store); + + // this transparently creates a backup and a fresh db + let res = SledStateStore::builder() + .path(folder.path().to_path_buf()) + .passphrase("secret".to_owned()) + .migration_conflict_strategy(MigrationConflictStrategy::Raise) + .build(); + if let Err(SledStoreError::MigrationConflict { .. }) = res { + // all good + } else { + panic!("Didn't raise the expected error: {res:?}"); + } + assert_eq!(std::fs::read_dir(folder.path())?.count(), 1); + Ok(()) + } + + #[async_test] + pub async fn migrating_v2_to_v3() { + // An event that fails to deserialize. + let wrong_redacted_state_event = json!({ + "content": null, + "event_id": "$wrongevent", + "origin_server_ts": 1673887516047_u64, + "sender": "@example:localhost", + "state_key": "", + "type": "m.room.topic", + "unsigned": { + "redacted_because": { + "type": "m.room.redaction", + "sender": "@example:localhost", + "content": {}, + "redacts": "$wrongevent", + "origin_server_ts": 1673893816047_u64, + "unsigned": {}, + "event_id": "$redactionevent", + }, + }, + }); + serde_json::from_value::(wrong_redacted_state_event.clone()) + .unwrap_err(); + + let room_id = room_id!("!some_room:localhost"); + let folder = TempDir::new().unwrap(); + + let store = SledStateStore::builder() + .path(folder.path().to_path_buf()) + .passphrase("secret".to_owned()) + .build() + .unwrap(); + + store + .room_state + .insert( + store.encode_key(ROOM_STATE, (room_id, StateEventType::RoomTopic, "")), + store.serialize_value(&wrong_redacted_state_event).unwrap(), + ) + .unwrap(); + store.set_db_version(2u8).unwrap(); + drop(store); + + let store = SledStateStore::builder() + .path(folder.path().to_path_buf()) + .passphrase("secret".to_owned()) + .build() + .unwrap(); + let event = + store.get_state_event(room_id, StateEventType::RoomTopic, "").await.unwrap().unwrap(); + event.deserialize().unwrap(); + } +} diff --git a/crates/matrix-sdk-sled/src/state_store.rs b/crates/matrix-sdk-sled/src/state_store/mod.rs similarity index 85% rename from crates/matrix-sdk-sled/src/state_store.rs rename to crates/matrix-sdk-sled/src/state_store/mod.rs index dd89b9a81..c3766307a 100644 --- a/crates/matrix-sdk-sled/src/state_store.rs +++ b/crates/matrix-sdk-sled/src/state_store/mod.rs @@ -43,7 +43,6 @@ use ruma::{ RoomVersionId, UserId, }; use serde::{de::DeserializeOwned, Serialize}; -use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue}; use sled::{ transaction::{ConflictableTransactionError, TransactionError}, Config, Db, Transactional, Tree, @@ -51,6 +50,9 @@ use sled::{ use tokio::task::spawn_blocking; use tracing::{debug, info, warn}; +mod migrations; + +pub use self::migrations::MigrationConflictStrategy; #[cfg(feature = "crypto-store")] use super::OpenStoreError; use crate::encode_key::{EncodeKey, EncodeUnchecked}; @@ -79,22 +81,6 @@ pub enum SledStoreError { MigrationConflict { path: PathBuf, old_version: usize, new_version: usize }, } -/// Sometimes Migrations can't proceed without having to drop existing -/// data. This allows you to configure, how these cases should be handled. -#[derive(PartialEq, Eq, Clone, Debug)] -pub enum MigrationConflictStrategy { - /// Just drop the data, we don't care that we have to sync again - Drop, - /// Raise a `SledStoreError::MigrationConflict` error with the path to the - /// DB in question. The caller then has to take care about what they want - /// to do and try again after. - Raise, - /// _Default_: The _entire_ database is backed up under - /// `$path.$timestamp.backup` (this includes the crypto store if they - /// are linked), before the state tables are dropped. - BackupAndDrop, -} - impl From> for SledStoreError { fn from(e: TransactionError) -> Self { match e { @@ -115,9 +101,6 @@ impl From for StoreError { } } } -const DATABASE_VERSION: u8 = 3; - -const VERSION_KEY: &str = "state-store-version"; const ACCOUNT_DATA: &str = "account-data"; const CUSTOM: &str = "custom"; @@ -166,7 +149,6 @@ const ALL_DB_STORES: &[&str] = &[ STRIPPED_ROOM_STATE, CUSTOM, ]; -const ALL_GLOBAL_KEYS: &[&str] = &[VERSION_KEY]; type Result = std::result::Result; @@ -413,115 +395,6 @@ impl SledStateStore { SledStateStoreBuilder::new() } - fn drop_tables(self) -> StoreResult<()> { - for name in ALL_DB_STORES { - self.inner.drop_tree(name).map_err(StoreError::backend)?; - } - for name in ALL_GLOBAL_KEYS { - self.inner.remove(name).map_err(StoreError::backend)?; - } - - Ok(()) - } - - fn set_db_version(&self, version: u8) -> Result<()> { - self.inner.insert(VERSION_KEY, version.to_be_bytes().as_ref())?; - self.inner.flush()?; - Ok(()) - } - - fn upgrade(&mut self) -> Result<()> { - let db_version = self.inner.get(VERSION_KEY)?.map(|v| { - let (version_bytes, _) = v.split_at(std::mem::size_of::()); - u8::from_be_bytes(version_bytes.try_into().unwrap_or_default()) - }); - - let old_version = match db_version { - None => { - // we are fresh, let's write the current version - return self.set_db_version(DATABASE_VERSION); - } - Some(version) if version == DATABASE_VERSION => { - // current, we don't have to do anything - return Ok(()); - } - Some(version) => version, - }; - - debug!(old_version, new_version = DATABASE_VERSION, "Upgrading the Sled state store"); - - if old_version == 1 && self.store_cipher.is_some() { - // we stored some fields un-encrypted. Drop them to force re-creation - return Err(SledStoreError::MigrationConflict { - path: self.path.take().expect("Path must exist for a migration to fail"), - old_version: old_version.into(), - new_version: DATABASE_VERSION.into(), - }); - } - - if old_version < 3 { - self.migrate_to_v3()?; - return Ok(()); - } - - // FUTURE UPGRADE CODE GOES HERE - - // can't upgrade from that version to the new one - Err(SledStoreError::MigrationConflict { - path: self.path.take().expect("Path must exist for a migration to fail"), - old_version: old_version.into(), - new_version: DATABASE_VERSION.into(), - }) - } - - fn v3_fix_tree(&self, tree: &Tree, batch: &mut sled::Batch) -> Result<()> { - fn maybe_fix_json(raw_json: &RawJsonValue) -> Result> { - let json = raw_json.get(); - - if json.contains(r#""content":null"#) { - let mut value: JsonValue = serde_json::from_str(json)?; - if let Some(content) = value.get_mut("content") { - if matches!(content, JsonValue::Null) { - *content = JsonValue::Object(Default::default()); - return Ok(Some(value)); - } - } - } - - Ok(None) - } - - for entry in tree.iter() { - let (key, value) = entry?; - let raw_json: Box = self.deserialize_value(&value)?; - - if let Some(fixed_json) = maybe_fix_json(&raw_json)? { - batch.insert(key, self.serialize_value(&fixed_json)?); - } - } - - Ok(()) - } - - fn migrate_to_v3(&self) -> Result<()> { - let mut room_info_batch = sled::Batch::default(); - self.v3_fix_tree(&self.room_info, &mut room_info_batch)?; - - let mut room_state_batch = sled::Batch::default(); - self.v3_fix_tree(&self.room_state, &mut room_state_batch)?; - - let ret: Result<(), TransactionError> = (&self.room_info, &self.room_state) - .transaction(|(room_info, room_state)| { - room_info.apply_batch(&room_info_batch)?; - room_state.apply_batch(&room_state_batch)?; - - Ok(()) - }); - ret?; - - self.set_db_version(3u8) - } - /// Open a `SledCryptoStore` that uses the same database as this store. /// /// The given passphrase will be used to encrypt private data. @@ -1646,155 +1519,3 @@ mod encrypted_tests { statestore_integration_tests!(with_media_tests); } - -#[cfg(test)] -mod migration { - use matrix_sdk_test::async_test; - use ruma::{ - events::{AnySyncStateEvent, StateEventType}, - room_id, - }; - use serde_json::json; - use tempfile::TempDir; - - use super::{MigrationConflictStrategy, Result, SledStateStore, SledStoreError}; - use crate::state_store::ROOM_STATE; - - #[async_test] - pub async fn migrating_v1_to_2_plain() -> Result<()> { - let folder = TempDir::new()?; - - let store = SledStateStore::builder().path(folder.path().to_path_buf()).build()?; - - store.set_db_version(1u8)?; - drop(store); - - // this transparently migrates to the latest version - let _store = SledStateStore::builder().path(folder.path().to_path_buf()).build()?; - Ok(()) - } - - #[async_test] - pub async fn migrating_v1_to_2_with_pw_backed_up() -> Result<()> { - let folder = TempDir::new()?; - - let store = SledStateStore::builder() - .path(folder.path().to_path_buf()) - .passphrase("something".to_owned()) - .build()?; - - store.set_db_version(1u8)?; - drop(store); - - // this transparently creates a backup and a fresh db - let _store = SledStateStore::builder() - .path(folder.path().to_path_buf()) - .passphrase("something".to_owned()) - .build()?; - assert_eq!(std::fs::read_dir(folder.path())?.count(), 2); - Ok(()) - } - - #[async_test] - pub async fn migrating_v1_to_2_with_pw_drop() -> Result<()> { - let folder = TempDir::new()?; - - let store = SledStateStore::builder() - .path(folder.path().to_path_buf()) - .passphrase("other thing".to_owned()) - .build()?; - - store.set_db_version(1u8)?; - drop(store); - - // this transparently creates a backup and a fresh db - let _store = SledStateStore::builder() - .path(folder.path().to_path_buf()) - .passphrase("other thing".to_owned()) - .migration_conflict_strategy(MigrationConflictStrategy::Drop) - .build()?; - assert_eq!(std::fs::read_dir(folder.path())?.count(), 1); - Ok(()) - } - - #[async_test] - pub async fn migrating_v1_to_2_with_pw_raises() -> Result<()> { - let folder = TempDir::new()?; - - let store = SledStateStore::builder() - .path(folder.path().to_path_buf()) - .passphrase("secret".to_owned()) - .build()?; - - store.set_db_version(1u8)?; - drop(store); - - // this transparently creates a backup and a fresh db - let res = SledStateStore::builder() - .path(folder.path().to_path_buf()) - .passphrase("secret".to_owned()) - .migration_conflict_strategy(MigrationConflictStrategy::Raise) - .build(); - if let Err(SledStoreError::MigrationConflict { .. }) = res { - // all good - } else { - panic!("Didn't raise the expected error: {res:?}"); - } - assert_eq!(std::fs::read_dir(folder.path())?.count(), 1); - Ok(()) - } - - #[async_test] - pub async fn migrating_v2_to_v3() { - // An event that fails to deserialize. - let wrong_redacted_state_event = json!({ - "content": null, - "event_id": "$wrongevent", - "origin_server_ts": 1673887516047_u64, - "sender": "@example:localhost", - "state_key": "", - "type": "m.room.topic", - "unsigned": { - "redacted_because": { - "type": "m.room.redaction", - "sender": "@example:localhost", - "content": {}, - "redacts": "$wrongevent", - "origin_server_ts": 1673893816047_u64, - "unsigned": {}, - "event_id": "$redactionevent", - }, - }, - }); - serde_json::from_value::(wrong_redacted_state_event.clone()) - .unwrap_err(); - - let room_id = room_id!("!some_room:localhost"); - let folder = TempDir::new().unwrap(); - - let store = SledStateStore::builder() - .path(folder.path().to_path_buf()) - .passphrase("secret".to_owned()) - .build() - .unwrap(); - - store - .room_state - .insert( - store.encode_key(ROOM_STATE, (room_id, StateEventType::RoomTopic, "")), - store.serialize_value(&wrong_redacted_state_event).unwrap(), - ) - .unwrap(); - store.set_db_version(2u8).unwrap(); - drop(store); - - let store = SledStateStore::builder() - .path(folder.path().to_path_buf()) - .passphrase("secret".to_owned()) - .build() - .unwrap(); - let event = - store.get_state_event(room_id, StateEventType::RoomTopic, "").await.unwrap().unwrap(); - event.deserialize().unwrap(); - } -} From 61796a2725b85cbe6d15534bc9d4ac35f259daf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Fri, 3 Mar 2023 17:30:12 +0100 Subject: [PATCH 134/166] sled: Move state store keys into keys module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- .../src/state_store/migrations.rs | 4 +- crates/matrix-sdk-sled/src/state_store/mod.rs | 345 ++++++++++-------- 2 files changed, 189 insertions(+), 160 deletions(-) diff --git a/crates/matrix-sdk-sled/src/state_store/migrations.rs b/crates/matrix-sdk-sled/src/state_store/migrations.rs index f4798a7fc..2fcd109b9 100644 --- a/crates/matrix-sdk-sled/src/state_store/migrations.rs +++ b/crates/matrix-sdk-sled/src/state_store/migrations.rs @@ -170,7 +170,7 @@ mod test { use tempfile::TempDir; use super::MigrationConflictStrategy; - use crate::state_store::{Result, SledStateStore, SledStoreError, ROOM_STATE}; + use crate::state_store::{keys, Result, SledStateStore, SledStoreError}; #[async_test] pub async fn migrating_v1_to_2_plain() -> Result<()> { @@ -293,7 +293,7 @@ mod test { store .room_state .insert( - store.encode_key(ROOM_STATE, (room_id, StateEventType::RoomTopic, "")), + store.encode_key(keys::ROOM_STATE, (room_id, StateEventType::RoomTopic, "")), store.serialize_value(&wrong_redacted_state_event).unwrap(), ) .unwrap(); diff --git a/crates/matrix-sdk-sled/src/state_store/mod.rs b/crates/matrix-sdk-sled/src/state_store/mod.rs index c3766307a..83bcb532a 100644 --- a/crates/matrix-sdk-sled/src/state_store/mod.rs +++ b/crates/matrix-sdk-sled/src/state_store/mod.rs @@ -102,53 +102,58 @@ impl From for StoreError { } } -const ACCOUNT_DATA: &str = "account-data"; -const CUSTOM: &str = "custom"; -const SYNC_TOKEN: &str = "sync_token"; -const DISPLAY_NAME: &str = "display-name"; -const INVITED_USER_ID: &str = "invited-user-id"; -const JOINED_USER_ID: &str = "joined-user-id"; -const MEDIA: &str = "media"; -const MEMBER: &str = "member"; -const PRESENCE: &str = "presence"; -const PROFILE: &str = "profile"; -const ROOM_ACCOUNT_DATA: &str = "room-account-data"; -const ROOM_EVENT_RECEIPT: &str = "room-event-receipt"; -const ROOM_INFO: &str = "room-info"; -const ROOM_STATE: &str = "room-state"; -const ROOM_USER_RECEIPT: &str = "room-user-receipt"; -const ROOM: &str = "room"; -const SESSION: &str = "session"; -const STRIPPED_INVITED_USER_ID: &str = "stripped-invited-user-id"; -const STRIPPED_JOINED_USER_ID: &str = "stripped-joined-user-id"; -const STRIPPED_ROOM_INFO: &str = "stripped-room-info"; -const STRIPPED_ROOM_MEMBER: &str = "stripped-room-member"; -const STRIPPED_ROOM_STATE: &str = "stripped-room-state"; +mod keys { + // Stores + pub const ACCOUNT_DATA: &str = "account-data"; + pub const CUSTOM: &str = "custom"; + pub const SYNC_TOKEN: &str = "sync_token"; + pub const DISPLAY_NAME: &str = "display-name"; + pub const INVITED_USER_ID: &str = "invited-user-id"; + pub const JOINED_USER_ID: &str = "joined-user-id"; + pub const MEDIA: &str = "media"; + pub const MEMBER: &str = "member"; + pub const PRESENCE: &str = "presence"; + pub const PROFILE: &str = "profile"; + pub const ROOM_ACCOUNT_DATA: &str = "room-account-data"; + pub const ROOM_EVENT_RECEIPT: &str = "room-event-receipt"; + pub const ROOM_INFO: &str = "room-info"; + pub const ROOM_STATE: &str = "room-state"; + pub const ROOM_USER_RECEIPT: &str = "room-user-receipt"; + pub const ROOM: &str = "room"; + pub const SESSION: &str = "session"; + pub const STRIPPED_INVITED_USER_ID: &str = "stripped-invited-user-id"; + pub const STRIPPED_JOINED_USER_ID: &str = "stripped-joined-user-id"; + pub const STRIPPED_ROOM_INFO: &str = "stripped-room-info"; + pub const STRIPPED_ROOM_MEMBER: &str = "stripped-room-member"; + pub const STRIPPED_ROOM_STATE: &str = "stripped-room-state"; -const ALL_DB_STORES: &[&str] = &[ - ACCOUNT_DATA, - SYNC_TOKEN, - DISPLAY_NAME, - INVITED_USER_ID, - JOINED_USER_ID, - MEDIA, - MEMBER, - PRESENCE, - PROFILE, - ROOM_ACCOUNT_DATA, - ROOM_EVENT_RECEIPT, - ROOM_INFO, - ROOM_STATE, - ROOM_USER_RECEIPT, - ROOM, - SESSION, - STRIPPED_INVITED_USER_ID, - STRIPPED_JOINED_USER_ID, - STRIPPED_ROOM_INFO, - STRIPPED_ROOM_MEMBER, - STRIPPED_ROOM_STATE, - CUSTOM, -]; + pub const ALL_DB_STORES: &[&str] = &[ + ACCOUNT_DATA, + SYNC_TOKEN, + DISPLAY_NAME, + INVITED_USER_ID, + JOINED_USER_ID, + MEDIA, + MEMBER, + PRESENCE, + PROFILE, + ROOM_ACCOUNT_DATA, + ROOM_EVENT_RECEIPT, + ROOM_INFO, + ROOM_STATE, + ROOM_USER_RECEIPT, + ROOM, + SESSION, + STRIPPED_INVITED_USER_ID, + STRIPPED_JOINED_USER_ID, + STRIPPED_ROOM_INFO, + STRIPPED_ROOM_MEMBER, + STRIPPED_ROOM_STATE, + CUSTOM, + ]; +} + +use keys::ALL_DB_STORES; type Result = std::result::Result; @@ -336,32 +341,32 @@ impl SledStateStore { path: Option, store_cipher: Option>, ) -> Result { - let session = db.open_tree(SESSION)?; - let account_data = db.open_tree(ACCOUNT_DATA)?; + let session = db.open_tree(keys::SESSION)?; + let account_data = db.open_tree(keys::ACCOUNT_DATA)?; - let members = db.open_tree(MEMBER)?; - let profiles = db.open_tree(PROFILE)?; - let display_names = db.open_tree(DISPLAY_NAME)?; - let joined_user_ids = db.open_tree(JOINED_USER_ID)?; - let invited_user_ids = db.open_tree(INVITED_USER_ID)?; + let members = db.open_tree(keys::MEMBER)?; + let profiles = db.open_tree(keys::PROFILE)?; + let display_names = db.open_tree(keys::DISPLAY_NAME)?; + let joined_user_ids = db.open_tree(keys::JOINED_USER_ID)?; + let invited_user_ids = db.open_tree(keys::INVITED_USER_ID)?; - let room_state = db.open_tree(ROOM_STATE)?; - let room_info = db.open_tree(ROOM_INFO)?; - let presence = db.open_tree(PRESENCE)?; - let room_account_data = db.open_tree(ROOM_ACCOUNT_DATA)?; + let room_state = db.open_tree(keys::ROOM_STATE)?; + let room_info = db.open_tree(keys::ROOM_INFO)?; + let presence = db.open_tree(keys::PRESENCE)?; + let room_account_data = db.open_tree(keys::ROOM_ACCOUNT_DATA)?; - let stripped_joined_user_ids = db.open_tree(STRIPPED_JOINED_USER_ID)?; - let stripped_invited_user_ids = db.open_tree(STRIPPED_INVITED_USER_ID)?; - let stripped_room_infos = db.open_tree(STRIPPED_ROOM_INFO)?; - let stripped_members = db.open_tree(STRIPPED_ROOM_MEMBER)?; - let stripped_room_state = db.open_tree(STRIPPED_ROOM_STATE)?; + let stripped_joined_user_ids = db.open_tree(keys::STRIPPED_JOINED_USER_ID)?; + let stripped_invited_user_ids = db.open_tree(keys::STRIPPED_INVITED_USER_ID)?; + let stripped_room_infos = db.open_tree(keys::STRIPPED_ROOM_INFO)?; + let stripped_members = db.open_tree(keys::STRIPPED_ROOM_MEMBER)?; + let stripped_room_state = db.open_tree(keys::STRIPPED_ROOM_STATE)?; - let room_user_receipts = db.open_tree(ROOM_USER_RECEIPT)?; - let room_event_receipts = db.open_tree(ROOM_EVENT_RECEIPT)?; + let room_user_receipts = db.open_tree(keys::ROOM_USER_RECEIPT)?; + let room_event_receipts = db.open_tree(keys::ROOM_EVENT_RECEIPT)?; - let media = db.open_tree(MEDIA)?; + let media = db.open_tree(keys::MEDIA)?; - let custom = db.open_tree(CUSTOM)?; + let custom = db.open_tree(keys::CUSTOM)?; Ok(Self { path, @@ -431,7 +436,7 @@ impl SledStateStore { pub async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<()> { self.session.insert( - self.encode_key(SESSION, ("filter", filter_name)), + self.encode_key(keys::SESSION, ("filter", filter_name)), self.serialize_value(&filter_id)?, )?; Ok(()) @@ -439,13 +444,13 @@ impl SledStateStore { pub async fn get_filter(&self, filter_name: &str) -> Result> { self.session - .get(self.encode_key(SESSION, ("filter", filter_name)))? + .get(self.encode_key(keys::SESSION, ("filter", filter_name)))? .map(|f| self.deserialize_value(&f)) .transpose() } pub async fn get_sync_token(&self) -> Result> { - self.session.get(SYNC_TOKEN.encode())?.map(|t| self.deserialize_value(&t)).transpose() + self.session.get(keys::SYNC_TOKEN.encode())?.map(|t| self.deserialize_value(&t)).transpose() } pub async fn save_changes(&self, changes: &StateChanges) -> Result<()> { @@ -500,45 +505,46 @@ impl SledStateStore { let key = (room, event.state_key()); stripped_joined - .remove(self.encode_key(STRIPPED_JOINED_USER_ID, key))?; + .remove(self.encode_key(keys::STRIPPED_JOINED_USER_ID, key))?; stripped_invited - .remove(self.encode_key(STRIPPED_INVITED_USER_ID, key))?; + .remove(self.encode_key(keys::STRIPPED_INVITED_USER_ID, key))?; match event.membership() { MembershipState::Join => { joined.insert( - self.encode_key(JOINED_USER_ID, key), + self.encode_key(keys::JOINED_USER_ID, key), self.serialize_value(event.state_key()) .map_err(ConflictableTransactionError::Abort)?, )?; - invited.remove(self.encode_key(INVITED_USER_ID, key))?; + invited.remove(self.encode_key(keys::INVITED_USER_ID, key))?; } MembershipState::Invite => { invited.insert( - self.encode_key(INVITED_USER_ID, key), + self.encode_key(keys::INVITED_USER_ID, key), self.serialize_value(event.state_key()) .map_err(ConflictableTransactionError::Abort)?, )?; - joined.remove(self.encode_key(JOINED_USER_ID, key))?; + joined.remove(self.encode_key(keys::JOINED_USER_ID, key))?; } _ => { - joined.remove(self.encode_key(JOINED_USER_ID, key))?; - invited.remove(self.encode_key(INVITED_USER_ID, key))?; + joined.remove(self.encode_key(keys::JOINED_USER_ID, key))?; + invited.remove(self.encode_key(keys::INVITED_USER_ID, key))?; } } members.insert( - self.encode_key(MEMBER, key), + self.encode_key(keys::MEMBER, key), self.serialize_value(&raw_event) .map_err(ConflictableTransactionError::Abort)?, )?; - stripped_members.remove(self.encode_key(STRIPPED_ROOM_MEMBER, key))?; + stripped_members + .remove(self.encode_key(keys::STRIPPED_ROOM_MEMBER, key))?; if let Some(profile) = profile_changes.and_then(|p| p.get(event.state_key())) { profiles.insert( - self.encode_key(PROFILE, key), + self.encode_key(keys::PROFILE, key), self.serialize_value(&profile) .map_err(ConflictableTransactionError::Abort)?, )?; @@ -549,7 +555,7 @@ impl SledStateStore { for (room_id, ambiguity_maps) in &changes.ambiguity_maps { for (display_name, map) in ambiguity_maps { display_names.insert( - self.encode_key(DISPLAY_NAME, (room_id, display_name)), + self.encode_key(keys::DISPLAY_NAME, (room_id, display_name)), self.serialize_value(&map) .map_err(ConflictableTransactionError::Abort)?, )?; @@ -559,7 +565,7 @@ impl SledStateStore { for (room, events) in &changes.room_account_data { for (event_type, event) in events { room_account_data.insert( - self.encode_key(ROOM_ACCOUNT_DATA, (room, event_type)), + self.encode_key(keys::ROOM_ACCOUNT_DATA, (room, event_type)), self.serialize_value(&event) .map_err(ConflictableTransactionError::Abort)?, )?; @@ -570,12 +576,15 @@ impl SledStateStore { for (event_type, events) in event_types { for (state_key, event) in events { state.insert( - self.encode_key(ROOM_STATE, (room, event_type, state_key)), + self.encode_key( + keys::ROOM_STATE, + (room, event_type, state_key), + ), self.serialize_value(&event) .map_err(ConflictableTransactionError::Abort)?, )?; stripped_state.remove(self.encode_key( - STRIPPED_ROOM_STATE, + keys::STRIPPED_ROOM_STATE, (room, event_type, state_key), ))?; } @@ -584,20 +593,21 @@ impl SledStateStore { for (room_id, room_info) in &changes.room_infos { rooms.insert( - self.encode_key(ROOM, room_id), + self.encode_key(keys::ROOM, room_id), self.serialize_value(room_info) .map_err(ConflictableTransactionError::Abort)?, )?; - stripped_rooms.remove(self.encode_key(STRIPPED_ROOM_INFO, room_id))?; + stripped_rooms + .remove(self.encode_key(keys::STRIPPED_ROOM_INFO, room_id))?; } for (room_id, info) in &changes.stripped_room_infos { stripped_rooms.insert( - self.encode_key(STRIPPED_ROOM_INFO, room_id), + self.encode_key(keys::STRIPPED_ROOM_INFO, room_id), self.serialize_value(&info) .map_err(ConflictableTransactionError::Abort)?, )?; - rooms.remove(self.encode_key(ROOM, room_id))?; + rooms.remove(self.encode_key(keys::ROOM, room_id))?; } for (room, raw_events) in &changes.stripped_members { @@ -620,31 +630,35 @@ impl SledStateStore { match event.content.membership { MembershipState::Join => { stripped_joined.insert( - self.encode_key(STRIPPED_JOINED_USER_ID, key), + self.encode_key(keys::STRIPPED_JOINED_USER_ID, key), self.serialize_value(&event.state_key) .map_err(ConflictableTransactionError::Abort)?, )?; - stripped_invited - .remove(self.encode_key(STRIPPED_INVITED_USER_ID, key))?; + stripped_invited.remove( + self.encode_key(keys::STRIPPED_INVITED_USER_ID, key), + )?; } MembershipState::Invite => { stripped_invited.insert( - self.encode_key(STRIPPED_INVITED_USER_ID, key), + self.encode_key(keys::STRIPPED_INVITED_USER_ID, key), self.serialize_value(&event.state_key) .map_err(ConflictableTransactionError::Abort)?, )?; - stripped_joined - .remove(self.encode_key(STRIPPED_JOINED_USER_ID, key))?; + stripped_joined.remove( + self.encode_key(keys::STRIPPED_JOINED_USER_ID, key), + )?; } _ => { - stripped_joined - .remove(self.encode_key(STRIPPED_JOINED_USER_ID, key))?; - stripped_invited - .remove(self.encode_key(STRIPPED_INVITED_USER_ID, key))?; + stripped_joined.remove( + self.encode_key(keys::STRIPPED_JOINED_USER_ID, key), + )?; + stripped_invited.remove( + self.encode_key(keys::STRIPPED_INVITED_USER_ID, key), + )?; } } stripped_members.insert( - self.encode_key(STRIPPED_ROOM_MEMBER, key), + self.encode_key(keys::STRIPPED_ROOM_MEMBER, key), self.serialize_value(&raw_event) .map_err(ConflictableTransactionError::Abort)?, )?; @@ -656,7 +670,7 @@ impl SledStateStore { for (state_key, event) in events { stripped_state.insert( self.encode_key( - STRIPPED_ROOM_STATE, + keys::STRIPPED_ROOM_STATE, (room, event_type.to_string(), state_key), ), self.serialize_value(&event) @@ -678,7 +692,7 @@ impl SledStateStore { let make_room_version = |room_id| { self.room_info - .get(self.encode_key(ROOM, room_id)) + .get(self.encode_key(keys::ROOM, room_id)) .ok() .flatten() .map(|r| self.deserialize_value::(&r)) @@ -693,7 +707,7 @@ impl SledStateStore { }; for (room_id, redactions) in &changes.redactions { - let key_prefix = self.encode_key(ROOM_STATE, room_id); + let key_prefix = self.encode_key(keys::ROOM_STATE, room_id); let mut room_version = None; // iterate through all saved state events and check whether they are among the @@ -731,11 +745,11 @@ impl SledStateStore { // Add the receipt to the room user receipts let key = match receipt.thread.as_str() { Some(thread_id) => self.encode_key( - ROOM_USER_RECEIPT, + keys::ROOM_USER_RECEIPT, (room, receipt_type, thread_id, user_id), ), None => self.encode_key( - ROOM_USER_RECEIPT, + keys::ROOM_USER_RECEIPT, (room, receipt_type, user_id), ), }; @@ -750,11 +764,11 @@ impl SledStateStore { .map_err(ConflictableTransactionError::Abort)?; let key = match receipt.thread.as_str() { Some(thread_id) => self.encode_key( - ROOM_EVENT_RECEIPT, + keys::ROOM_EVENT_RECEIPT, (room, receipt_type, thread_id, old_event, user_id), ), None => self.encode_key( - ROOM_EVENT_RECEIPT, + keys::ROOM_EVENT_RECEIPT, (room, receipt_type, old_event, user_id), ), }; @@ -764,11 +778,11 @@ impl SledStateStore { // Add the receipt to the room event receipts let key = match receipt.thread.as_str() { Some(thread_id) => self.encode_key( - ROOM_EVENT_RECEIPT, + keys::ROOM_EVENT_RECEIPT, (room, receipt_type, thread_id, event_id, user_id), ), None => self.encode_key( - ROOM_EVENT_RECEIPT, + keys::ROOM_EVENT_RECEIPT, (room, receipt_type, event_id, user_id), ), }; @@ -784,7 +798,7 @@ impl SledStateStore { for (sender, event) in &changes.presence { presence.insert( - self.encode_key(PRESENCE, sender), + self.encode_key(keys::PRESENCE, sender), self.serialize_value(&event) .map_err(ConflictableTransactionError::Abort)?, )?; @@ -801,14 +815,14 @@ impl SledStateStore { .transaction(|(session, account_data)| { if let Some(s) = &changes.sync_token { session.insert( - SYNC_TOKEN.encode(), + keys::SYNC_TOKEN.encode(), self.serialize_value(s).map_err(ConflictableTransactionError::Abort)?, )?; } for (event_type, event) in &changes.account_data { account_data.insert( - self.encode_key(ACCOUNT_DATA, event_type), + self.encode_key(keys::ACCOUNT_DATA, event_type), self.serialize_value(&event) .map_err(ConflictableTransactionError::Abort)?, )?; @@ -828,7 +842,7 @@ impl SledStateStore { pub async fn get_presence_event(&self, user_id: &UserId) -> Result>> { let db = self.clone(); - let key = self.encode_key(PRESENCE, user_id); + let key = self.encode_key(keys::PRESENCE, user_id); spawn_blocking(move || db.presence.get(key)?.map(|e| db.deserialize_value(&e)).transpose()) .await? } @@ -840,7 +854,7 @@ impl SledStateStore { state_key: &str, ) -> Result>> { let db = self.clone(); - let key = self.encode_key(ROOM_STATE, (room_id, event_type.to_string(), state_key)); + let key = self.encode_key(keys::ROOM_STATE, (room_id, event_type.to_string(), state_key)); spawn_blocking(move || { db.room_state.get(key)?.map(|e| db.deserialize_value(&e)).transpose() }) @@ -853,7 +867,7 @@ impl SledStateStore { event_type: StateEventType, ) -> Result>> { let db = self.clone(); - let key = self.encode_key(ROOM_STATE, (room_id, event_type.to_string())); + let key = self.encode_key(keys::ROOM_STATE, (room_id, event_type.to_string())); spawn_blocking(move || { db.room_state .scan_prefix(key) @@ -869,7 +883,7 @@ impl SledStateStore { user_id: &UserId, ) -> Result>> { let db = self.clone(); - let key = self.encode_key(PROFILE, (room_id, user_id)); + let key = self.encode_key(keys::PROFILE, (room_id, user_id)); spawn_blocking(move || db.profiles.get(key)?.map(|p| db.deserialize_value(&p)).transpose()) .await? } @@ -880,8 +894,8 @@ impl SledStateStore { state_key: &UserId, ) -> Result> { let db = self.clone(); - let key = self.encode_key(MEMBER, (room_id, state_key)); - let stripped_key = self.encode_key(STRIPPED_ROOM_MEMBER, (room_id, state_key)); + let key = self.encode_key(keys::MEMBER, (room_id, state_key)); + let stripped_key = self.encode_key(keys::STRIPPED_ROOM_MEMBER, (room_id, state_key)); spawn_blocking(move || { if let Some(e) = db .stripped_members @@ -925,7 +939,7 @@ impl SledStateStore { room_id: &RoomId, ) -> StoreResult>> { let db = self.clone(); - let key = self.encode_key(INVITED_USER_ID, room_id); + let key = self.encode_key(keys::INVITED_USER_ID, room_id); spawn_blocking(move || { stream::iter(db.invited_user_ids.scan_prefix(key).map(move |u| { db.deserialize_value(&u.map_err(StoreError::backend)?.1) @@ -941,7 +955,7 @@ impl SledStateStore { room_id: &RoomId, ) -> StoreResult>> { let db = self.clone(); - let key = self.encode_key(JOINED_USER_ID, room_id); + let key = self.encode_key(keys::JOINED_USER_ID, room_id); spawn_blocking(move || { stream::iter(db.joined_user_ids.scan_prefix(key).map(move |u| { db.deserialize_value(&u.map_err(StoreError::backend)?.1) @@ -957,7 +971,7 @@ impl SledStateStore { room_id: &RoomId, ) -> StoreResult>> { let db = self.clone(); - let key = self.encode_key(STRIPPED_INVITED_USER_ID, room_id); + let key = self.encode_key(keys::STRIPPED_INVITED_USER_ID, room_id); spawn_blocking(move || { stream::iter(db.stripped_invited_user_ids.scan_prefix(key).map(move |u| { db.deserialize_value(&u.map_err(StoreError::backend)?.1) @@ -973,7 +987,7 @@ impl SledStateStore { room_id: &RoomId, ) -> StoreResult>> { let db = self.clone(); - let key = self.encode_key(STRIPPED_JOINED_USER_ID, room_id); + let key = self.encode_key(keys::STRIPPED_JOINED_USER_ID, room_id); spawn_blocking(move || { stream::iter(db.stripped_joined_user_ids.scan_prefix(key).map(move |u| { db.deserialize_value(&u.map_err(StoreError::backend)?.1) @@ -1008,7 +1022,7 @@ impl SledStateStore { display_name: &str, ) -> Result> { let db = self.clone(); - let key = self.encode_key(DISPLAY_NAME, (room_id, display_name)); + let key = self.encode_key(keys::DISPLAY_NAME, (room_id, display_name)); spawn_blocking(move || { Ok(db .display_names @@ -1025,7 +1039,7 @@ impl SledStateStore { event_type: GlobalAccountDataEventType, ) -> Result>> { let db = self.clone(); - let key = self.encode_key(ACCOUNT_DATA, event_type); + let key = self.encode_key(keys::ACCOUNT_DATA, event_type); spawn_blocking(move || { db.account_data.get(key)?.map(|m| db.deserialize_value(&m)).transpose() }) @@ -1038,7 +1052,7 @@ impl SledStateStore { event_type: RoomAccountDataEventType, ) -> Result>> { let db = self.clone(); - let key = self.encode_key(ROOM_ACCOUNT_DATA, (room_id, event_type)); + let key = self.encode_key(keys::ROOM_ACCOUNT_DATA, (room_id, event_type)); spawn_blocking(move || { db.room_account_data.get(key)?.map(|m| db.deserialize_value(&m)).transpose() }) @@ -1054,10 +1068,9 @@ impl SledStateStore { ) -> Result> { let db = self.clone(); let key = match thread.as_str() { - Some(thread_id) => { - self.encode_key(ROOM_USER_RECEIPT, (room_id, receipt_type, thread_id, user_id)) - } - None => self.encode_key(ROOM_USER_RECEIPT, (room_id, receipt_type, user_id)), + Some(thread_id) => self + .encode_key(keys::ROOM_USER_RECEIPT, (room_id, receipt_type, thread_id, user_id)), + None => self.encode_key(keys::ROOM_USER_RECEIPT, (room_id, receipt_type, user_id)), }; spawn_blocking(move || { db.room_user_receipts.get(key)?.map(|m| db.deserialize_value(&m)).transpose() @@ -1074,10 +1087,9 @@ impl SledStateStore { ) -> StoreResult> { let db = self.clone(); let key = match thread.as_str() { - Some(thread_id) => { - self.encode_key(ROOM_EVENT_RECEIPT, (room_id, receipt_type, thread_id, event_id)) - } - None => self.encode_key(ROOM_EVENT_RECEIPT, (room_id, receipt_type, event_id)), + Some(thread_id) => self + .encode_key(keys::ROOM_EVENT_RECEIPT, (room_id, receipt_type, thread_id, event_id)), + None => self.encode_key(keys::ROOM_EVENT_RECEIPT, (room_id, receipt_type, event_id)), }; spawn_blocking(move || { db.room_event_receipts @@ -1095,7 +1107,10 @@ impl SledStateStore { async fn add_media_content(&self, request: &MediaRequest, data: Vec) -> Result<()> { self.media.insert( - self.encode_key(MEDIA, (request.source.unique_key(), request.format.unique_key())), + self.encode_key( + keys::MEDIA, + (request.source.unique_key(), request.format.unique_key()), + ), self.serialize_value(&data)?, )?; @@ -1106,8 +1121,8 @@ impl SledStateStore { async fn get_media_content(&self, request: &MediaRequest) -> Result>> { let db = self.clone(); - let key = - self.encode_key(MEDIA, (request.source.unique_key(), request.format.unique_key())); + let key = self + .encode_key(keys::MEDIA, (request.source.unique_key(), request.format.unique_key())); spawn_blocking(move || { db.media.get(key)?.map(move |m| db.deserialize_value(&m)).transpose() @@ -1118,13 +1133,13 @@ impl SledStateStore { async fn get_custom_value(&self, key: &[u8]) -> Result>> { let custom = self.custom.clone(); let me = self.clone(); - let key = self.encode_key(CUSTOM, EncodeUnchecked::from(key)); + let key = self.encode_key(keys::CUSTOM, EncodeUnchecked::from(key)); spawn_blocking(move || custom.get(key)?.map(move |v| me.deserialize_value(&v)).transpose()) .await? } async fn set_custom_value(&self, key: &[u8], value: Vec) -> Result>> { - let key = self.encode_key(CUSTOM, EncodeUnchecked::from(key)); + let key = self.encode_key(keys::CUSTOM, EncodeUnchecked::from(key)); let me = self.clone(); let ret = self .custom @@ -1138,14 +1153,17 @@ impl SledStateStore { async fn remove_media_content(&self, request: &MediaRequest) -> Result<()> { self.media.remove( - self.encode_key(MEDIA, (request.source.unique_key(), request.format.unique_key())), + self.encode_key( + keys::MEDIA, + (request.source.unique_key(), request.format.unique_key()), + ), )?; Ok(()) } async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> { - let keys = self.media.scan_prefix(self.encode_key(MEDIA, uri)).keys(); + let keys = self.media.scan_prefix(self.encode_key(keys::MEDIA, uri)).keys(); let mut batch = sled::Batch::default(); for key in keys { @@ -1157,29 +1175,34 @@ impl SledStateStore { async fn remove_room(&self, room_id: &RoomId) -> Result<()> { let mut members_batch = sled::Batch::default(); - for key in self.members.scan_prefix(self.encode_key(MEMBER, room_id)).keys() { + for key in self.members.scan_prefix(self.encode_key(keys::MEMBER, room_id)).keys() { members_batch.remove(key?); } let mut stripped_members_batch = sled::Batch::default(); - for key in - self.stripped_members.scan_prefix(self.encode_key(STRIPPED_ROOM_MEMBER, room_id)).keys() + for key in self + .stripped_members + .scan_prefix(self.encode_key(keys::STRIPPED_ROOM_MEMBER, room_id)) + .keys() { stripped_members_batch.remove(key?); } let mut profiles_batch = sled::Batch::default(); - for key in self.profiles.scan_prefix(self.encode_key(PROFILE, room_id)).keys() { + for key in self.profiles.scan_prefix(self.encode_key(keys::PROFILE, room_id)).keys() { profiles_batch.remove(key?); } let mut display_names_batch = sled::Batch::default(); - for key in self.display_names.scan_prefix(self.encode_key(DISPLAY_NAME, room_id)).keys() { + for key in + self.display_names.scan_prefix(self.encode_key(keys::DISPLAY_NAME, room_id)).keys() + { display_names_batch.remove(key?); } let mut joined_user_ids_batch = sled::Batch::default(); - for key in self.joined_user_ids.scan_prefix(self.encode_key(JOINED_USER_ID, room_id)).keys() + for key in + self.joined_user_ids.scan_prefix(self.encode_key(keys::JOINED_USER_ID, room_id)).keys() { joined_user_ids_batch.remove(key?); } @@ -1187,15 +1210,17 @@ impl SledStateStore { let mut stripped_joined_user_ids_batch = sled::Batch::default(); for key in self .stripped_joined_user_ids - .scan_prefix(self.encode_key(STRIPPED_JOINED_USER_ID, room_id)) + .scan_prefix(self.encode_key(keys::STRIPPED_JOINED_USER_ID, room_id)) .keys() { stripped_joined_user_ids_batch.remove(key?); } let mut invited_user_ids_batch = sled::Batch::default(); - for key in - self.invited_user_ids.scan_prefix(self.encode_key(INVITED_USER_ID, room_id)).keys() + for key in self + .invited_user_ids + .scan_prefix(self.encode_key(keys::INVITED_USER_ID, room_id)) + .keys() { invited_user_ids_batch.remove(key?); } @@ -1203,29 +1228,31 @@ impl SledStateStore { let mut stripped_invited_user_ids_batch = sled::Batch::default(); for key in self .stripped_invited_user_ids - .scan_prefix(self.encode_key(STRIPPED_INVITED_USER_ID, room_id)) + .scan_prefix(self.encode_key(keys::STRIPPED_INVITED_USER_ID, room_id)) .keys() { stripped_invited_user_ids_batch.remove(key?); } let mut room_state_batch = sled::Batch::default(); - for key in self.room_state.scan_prefix(self.encode_key(ROOM_STATE, room_id)).keys() { + for key in self.room_state.scan_prefix(self.encode_key(keys::ROOM_STATE, room_id)).keys() { room_state_batch.remove(key?); } let mut stripped_room_state_batch = sled::Batch::default(); for key in self .stripped_room_state - .scan_prefix(self.encode_key(STRIPPED_ROOM_STATE, room_id)) + .scan_prefix(self.encode_key(keys::STRIPPED_ROOM_STATE, room_id)) .keys() { stripped_room_state_batch.remove(key?); } let mut room_account_data_batch = sled::Batch::default(); - for key in - self.room_account_data.scan_prefix(self.encode_key(ROOM_ACCOUNT_DATA, room_id)).keys() + for key in self + .room_account_data + .scan_prefix(self.encode_key(keys::ROOM_ACCOUNT_DATA, room_id)) + .keys() { room_account_data_batch.remove(key?); } @@ -1261,8 +1288,8 @@ impl SledStateStore { stripped_state, room_account_data, )| { - rooms.remove(self.encode_key(ROOM, room_id))?; - stripped_rooms.remove(self.encode_key(STRIPPED_ROOM_INFO, room_id))?; + rooms.remove(self.encode_key(keys::ROOM, room_id))?; + stripped_rooms.remove(self.encode_key(keys::STRIPPED_ROOM_INFO, room_id))?; members.apply_batch(&members_batch)?; stripped_members.apply_batch(&stripped_members_batch)?; @@ -1282,8 +1309,10 @@ impl SledStateStore { ret?; let mut room_user_receipts_batch = sled::Batch::default(); - for key in - self.room_user_receipts.scan_prefix(self.encode_key(ROOM_USER_RECEIPT, room_id)).keys() + for key in self + .room_user_receipts + .scan_prefix(self.encode_key(keys::ROOM_USER_RECEIPT, room_id)) + .keys() { room_user_receipts_batch.remove(key?); } @@ -1291,7 +1320,7 @@ impl SledStateStore { let mut room_event_receipts_batch = sled::Batch::default(); for key in self .room_event_receipts - .scan_prefix(self.encode_key(ROOM_EVENT_RECEIPT, room_id)) + .scan_prefix(self.encode_key(keys::ROOM_EVENT_RECEIPT, room_id)) .keys() { room_event_receipts_batch.remove(key?); From 5b28c69cfc4c81e37038d3726506dc3dbe04e26a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Fri, 3 Mar 2023 18:18:29 +0100 Subject: [PATCH 135/166] sled: Rename session store to kv MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- .../src/state_store/migrations.rs | 132 ++++++++++++++++-- crates/matrix-sdk-sled/src/state_store/mod.rs | 61 +++----- 2 files changed, 141 insertions(+), 52 deletions(-) diff --git a/crates/matrix-sdk-sled/src/state_store/migrations.rs b/crates/matrix-sdk-sled/src/state_store/migrations.rs index 2fcd109b9..2c996e8a6 100644 --- a/crates/matrix-sdk-sled/src/state_store/migrations.rs +++ b/crates/matrix-sdk-sled/src/state_store/migrations.rs @@ -17,12 +17,12 @@ use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue}; use sled::{transaction::TransactionError, Batch, Transactional, Tree}; use tracing::debug; -use super::{Result, SledStateStore, SledStoreError, ALL_DB_STORES}; +use super::{keys, Result, SledStateStore, SledStoreError}; +use crate::encode_key::EncodeKey; -const DATABASE_VERSION: u8 = 3; +const DATABASE_VERSION: u8 = 4; const VERSION_KEY: &str = "state-store-version"; -const ALL_GLOBAL_KEYS: &[&str] = &[VERSION_KEY]; /// Sometimes Migrations can't proceed without having to drop existing /// data. This allows you to configure, how these cases should be handled. @@ -66,6 +66,10 @@ impl SledStateStore { if old_version < 3 { self.migrate_to_v3()?; + } + + if old_version < 4 { + self.migrate_to_v4()?; return Ok(()); } @@ -99,13 +103,11 @@ impl SledStateStore { Ok(()) } - pub fn drop_tables(self) -> StoreResult<()> { - for name in ALL_DB_STORES { + pub fn drop_v1_tables(self) -> StoreResult<()> { + for name in V1_DB_STORES { self.inner.drop_tree(name).map_err(StoreError::backend)?; } - for name in ALL_GLOBAL_KEYS { - self.inner.remove(name).map_err(StoreError::backend)?; - } + self.inner.remove(VERSION_KEY).map_err(StoreError::backend)?; Ok(()) } @@ -157,8 +159,67 @@ impl SledStateStore { self.set_db_version(3u8) } + + /// Replace the SYNC_TOKEN and SESSION trees by KV. + fn migrate_to_v4(&self) -> Result<()> { + { + let session = &self.inner.open_tree(old_keys::SESSION)?; + let mut batch = sled::Batch::default(); + + // Sync token + let sync_token = session.get(keys::SYNC_TOKEN.encode())?; + if let Some(sync_token) = sync_token { + batch.insert(keys::SYNC_TOKEN.encode(), sync_token); + } + + // Filters + let key = self.encode_key(keys::SESSION, keys::FILTER); + for res in session.scan_prefix(key) { + let (key, value) = res?; + batch.insert(key, value); + } + self.kv.apply_batch(batch)?; + } + + // This was unused so we can just drop it. + self.inner.drop_tree(old_keys::SYNC_TOKEN)?; + self.inner.drop_tree(old_keys::SESSION)?; + + self.set_db_version(4) + } } +mod old_keys { + /// Old stores. + pub const SYNC_TOKEN: &str = "sync_token"; + pub const SESSION: &str = "session"; +} + +pub const V1_DB_STORES: &[&str] = &[ + keys::ACCOUNT_DATA, + old_keys::SYNC_TOKEN, + keys::DISPLAY_NAME, + keys::INVITED_USER_ID, + keys::JOINED_USER_ID, + keys::MEDIA, + keys::MEMBER, + keys::PRESENCE, + keys::PROFILE, + keys::ROOM_ACCOUNT_DATA, + keys::ROOM_EVENT_RECEIPT, + keys::ROOM_INFO, + keys::ROOM_STATE, + keys::ROOM_USER_RECEIPT, + keys::ROOM, + old_keys::SESSION, + keys::STRIPPED_INVITED_USER_ID, + keys::STRIPPED_JOINED_USER_ID, + keys::STRIPPED_ROOM_INFO, + keys::STRIPPED_ROOM_MEMBER, + keys::STRIPPED_ROOM_STATE, + keys::CUSTOM, +]; + #[cfg(test)] mod test { use matrix_sdk_test::async_test; @@ -169,8 +230,11 @@ mod test { use serde_json::json; use tempfile::TempDir; - use super::MigrationConflictStrategy; - use crate::state_store::{keys, Result, SledStateStore, SledStoreError}; + use super::{old_keys, MigrationConflictStrategy}; + use crate::{ + encode_key::EncodeKey, + state_store::{keys, Result, SledStateStore, SledStoreError}, + }; #[async_test] pub async fn migrating_v1_to_2_plain() -> Result<()> { @@ -309,4 +373,52 @@ mod test { store.get_state_event(room_id, StateEventType::RoomTopic, "").await.unwrap().unwrap(); event.deserialize().unwrap(); } + + #[async_test] + pub async fn migrating_v3_to_v4() { + let sync_token = "a_very_unique_string"; + let filter_1 = "filter_1"; + let filter_1_id = "filter_1_id"; + let filter_2 = "filter_2"; + let filter_2_id = "filter_2_id"; + + let folder = TempDir::new().unwrap(); + let store = SledStateStore::builder() + .path(folder.path().to_path_buf()) + .passphrase("secret".to_owned()) + .build() + .unwrap(); + + let session = store.inner.open_tree(old_keys::SESSION).unwrap(); + let mut batch = sled::Batch::default(); + batch.insert(keys::SYNC_TOKEN.encode(), store.serialize_value(&sync_token).unwrap()); + batch.insert( + store.encode_key(keys::SESSION, (keys::FILTER, filter_1)), + store.serialize_value(&filter_1_id).unwrap(), + ); + batch.insert( + store.encode_key(keys::SESSION, (keys::FILTER, filter_2)), + store.serialize_value(&filter_2_id).unwrap(), + ); + session.apply_batch(batch).unwrap(); + + store.set_db_version(3).unwrap(); + drop(session); + drop(store); + + let store = SledStateStore::builder() + .path(folder.path().to_path_buf()) + .passphrase("secret".to_owned()) + .build() + .unwrap(); + + let stored_sync_token = store.get_sync_token().await.unwrap().unwrap(); + assert_eq!(stored_sync_token, sync_token); + + let stored_filter_1_id = store.get_filter(filter_1).await.unwrap().unwrap(); + assert_eq!(stored_filter_1_id, filter_1_id); + + let stored_filter_2_id = store.get_filter(filter_2).await.unwrap().unwrap(); + assert_eq!(stored_filter_2_id, filter_2_id); + } } diff --git a/crates/matrix-sdk-sled/src/state_store/mod.rs b/crates/matrix-sdk-sled/src/state_store/mod.rs index 83bcb532a..0afb3a878 100644 --- a/crates/matrix-sdk-sled/src/state_store/mod.rs +++ b/crates/matrix-sdk-sled/src/state_store/mod.rs @@ -103,10 +103,14 @@ impl From for StoreError { } mod keys { + // Static keys + pub const SYNC_TOKEN: &str = "sync_token"; + pub const SESSION: &str = "session"; + pub const FILTER: &str = "filter"; + // Stores pub const ACCOUNT_DATA: &str = "account-data"; pub const CUSTOM: &str = "custom"; - pub const SYNC_TOKEN: &str = "sync_token"; pub const DISPLAY_NAME: &str = "display-name"; pub const INVITED_USER_ID: &str = "invited-user-id"; pub const JOINED_USER_ID: &str = "joined-user-id"; @@ -120,41 +124,14 @@ mod keys { pub const ROOM_STATE: &str = "room-state"; pub const ROOM_USER_RECEIPT: &str = "room-user-receipt"; pub const ROOM: &str = "room"; - pub const SESSION: &str = "session"; pub const STRIPPED_INVITED_USER_ID: &str = "stripped-invited-user-id"; pub const STRIPPED_JOINED_USER_ID: &str = "stripped-joined-user-id"; pub const STRIPPED_ROOM_INFO: &str = "stripped-room-info"; pub const STRIPPED_ROOM_MEMBER: &str = "stripped-room-member"; pub const STRIPPED_ROOM_STATE: &str = "stripped-room-state"; - - pub const ALL_DB_STORES: &[&str] = &[ - ACCOUNT_DATA, - SYNC_TOKEN, - DISPLAY_NAME, - INVITED_USER_ID, - JOINED_USER_ID, - MEDIA, - MEMBER, - PRESENCE, - PROFILE, - ROOM_ACCOUNT_DATA, - ROOM_EVENT_RECEIPT, - ROOM_INFO, - ROOM_STATE, - ROOM_USER_RECEIPT, - ROOM, - SESSION, - STRIPPED_INVITED_USER_ID, - STRIPPED_JOINED_USER_ID, - STRIPPED_ROOM_INFO, - STRIPPED_ROOM_MEMBER, - STRIPPED_ROOM_STATE, - CUSTOM, - ]; + pub const KV: &str = "kv"; } -use keys::ALL_DB_STORES; - type Result = std::result::Result; #[derive(Debug, Clone)] @@ -268,11 +245,11 @@ impl SledStateStoreBuilder { )); fs_extra::dir::create_all(&new_path, false)?; fs_extra::dir::copy(path, new_path, &fs_extra::dir::CopyOptions::new())?; - store.drop_tables()?; + store.drop_v1_tables()?; return self.build(); } MigrationConflictStrategy::Drop => { - store.drop_tables()?; + store.drop_v1_tables()?; return self.build(); } MigrationConflictStrategy::Raise => migration_res?, @@ -303,7 +280,7 @@ pub struct SledStateStore { path: Option, pub(crate) inner: Db, store_cipher: Option>, - session: Tree, + kv: Tree, account_data: Tree, members: Tree, profiles: Tree, @@ -341,7 +318,7 @@ impl SledStateStore { path: Option, store_cipher: Option>, ) -> Result { - let session = db.open_tree(keys::SESSION)?; + let kv = db.open_tree(keys::KV)?; let account_data = db.open_tree(keys::ACCOUNT_DATA)?; let members = db.open_tree(keys::MEMBER)?; @@ -372,7 +349,7 @@ impl SledStateStore { path, inner: db, store_cipher, - session, + kv, account_data, members, profiles, @@ -435,22 +412,22 @@ impl SledStateStore { } pub async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<()> { - self.session.insert( - self.encode_key(keys::SESSION, ("filter", filter_name)), + self.kv.insert( + self.encode_key(keys::SESSION, (keys::FILTER, filter_name)), self.serialize_value(&filter_id)?, )?; Ok(()) } pub async fn get_filter(&self, filter_name: &str) -> Result> { - self.session - .get(self.encode_key(keys::SESSION, ("filter", filter_name)))? + self.kv + .get(self.encode_key(keys::SESSION, (keys::FILTER, filter_name)))? .map(|f| self.deserialize_value(&f)) .transpose() } pub async fn get_sync_token(&self) -> Result> { - self.session.get(keys::SYNC_TOKEN.encode())?.map(|t| self.deserialize_value(&t)).transpose() + self.kv.get(keys::SYNC_TOKEN.encode())?.map(|t| self.deserialize_value(&t)).transpose() } pub async fn save_changes(&self, changes: &StateChanges) -> Result<()> { @@ -811,10 +788,10 @@ impl SledStateStore { ret?; // user state - let ret: Result<(), TransactionError> = (&self.session, &self.account_data) - .transaction(|(session, account_data)| { + let ret: Result<(), TransactionError> = (&self.kv, &self.account_data) + .transaction(|(kv, account_data)| { if let Some(s) = &changes.sync_token { - session.insert( + kv.insert( keys::SYNC_TOKEN.encode(), self.serialize_value(s).map_err(ConflictableTransactionError::Abort)?, )?; From d77efd7327e85395fff41b3de12d89cb2d38dea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Fri, 3 Mar 2023 19:25:10 +0100 Subject: [PATCH 136/166] state-store: Replace methods to get and set key-value data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avoid to make the API bigger when we want to add new key-value data in the store. Signed-off-by: Kévin Commaille --- Cargo.lock | 1 + crates/matrix-sdk-base/Cargo.toml | 4 +- crates/matrix-sdk-base/src/client.rs | 18 ++- crates/matrix-sdk-base/src/lib.rs | 2 +- .../src/store/integration_tests.rs | 58 +++++++-- .../matrix-sdk-base/src/store/memory_store.rs | 73 ++++++++--- crates/matrix-sdk-base/src/store/mod.rs | 8 +- crates/matrix-sdk-base/src/store/traits.rs | 118 ++++++++++++++---- .../src/state_store/migrations.rs | 51 ++++++-- .../src/state_store/mod.rs | 93 ++++++++++---- .../src/state_store/migrations.rs | 51 ++++++-- crates/matrix-sdk-sled/src/state_store/mod.rs | 90 +++++++++---- 12 files changed, 433 insertions(+), 134 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9f3b1018f..6b6aaf3b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2677,6 +2677,7 @@ dependencies = [ name = "matrix-sdk-base" version = "0.6.1" dependencies = [ + "assert_matches", "assign", "async-stream", "async-trait", diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index b3cf57ee9..37a4df13b 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -23,9 +23,10 @@ qrcode = ["matrix-sdk-crypto?/qrcode"] experimental-sliding-sync = ["ruma/unstable-msc3575"] # helpers for testing features build upon this -testing = ["dep:http", "dep:matrix-sdk-test"] +testing = ["dep:http", "dep:matrix-sdk-test", "dep:assert_matches"] [dependencies] +assert_matches = { version = "1.5.0", optional = true } async-stream = { workspace = true } async-trait = { workspace = true } dashmap = { workspace = true } @@ -46,6 +47,7 @@ tracing = { workspace = true } zeroize = { workspace = true, features = ["zeroize_derive"] } [dev-dependencies] +assert_matches = "1.5.0" assign = "1.1.1" ctor = { workspace = true } futures = { version = "0.3.21", default-features = false, features = ["executor"] } diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 9813a0192..c46b3ae02 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -64,7 +64,7 @@ use crate::{ rooms::{Room, RoomInfo, RoomType}, store::{ ambiguity_map::AmbiguityCache, DynStateStore, Result as StoreResult, StateChanges, - StateStoreExt, Store, StoreConfig, + StateStoreDataKey, StateStoreDataValue, StateStoreExt, Store, StoreConfig, }, sync::{JoinedRoom, LeftRoom, Rooms, SyncResponse, Timeline}, Session, SessionMeta, SessionTokens, @@ -1025,7 +1025,13 @@ impl BaseClient { filter_name: &str, response: &api::filter::create_filter::v3::Response, ) -> Result<()> { - Ok(self.store.save_filter(filter_name, &response.filter_id).await?) + Ok(self + .store + .set_kv_data( + StateStoreDataKey::Filter(filter_name), + StateStoreDataValue::Filter(response.filter_id.clone()), + ) + .await?) } /// Get the filter id of a previously uploaded filter. @@ -1040,7 +1046,13 @@ impl BaseClient { /// /// [`receive_filter_upload`]: #method.receive_filter_upload pub async fn get_filter(&self, filter_name: &str) -> StoreResult> { - self.store.get_filter(filter_name).await + let filter = self + .store + .get_kv_data(StateStoreDataKey::Filter(filter_name)) + .await? + .map(|d| d.into_filter().expect("State store data not a filter")); + + Ok(filter) } /// Get a to-device request that will share a room key with users in a room. diff --git a/crates/matrix-sdk-base/src/lib.rs b/crates/matrix-sdk-base/src/lib.rs index 8480ad95e..d411e5663 100644 --- a/crates/matrix-sdk-base/src/lib.rs +++ b/crates/matrix-sdk-base/src/lib.rs @@ -43,7 +43,7 @@ pub use http; pub use matrix_sdk_crypto as crypto; pub use once_cell; pub use rooms::{DisplayName, Room, RoomInfo, RoomMember, RoomType}; -pub use store::{StateChanges, StateStore, StoreError}; +pub use store::{StateChanges, StateStore, StateStoreDataKey, StateStoreDataValue, StoreError}; pub use utils::{ MinimalRoomMemberEvent, MinimalStateEvent, OriginalMinimalStateEvent, RedactedMinimalStateEvent, }; diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index 87a6195a5..982ed149a 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -2,6 +2,7 @@ use std::collections::{BTreeMap, BTreeSet}; +use assert_matches::assert_matches; use async_trait::async_trait; use matrix_sdk_test::test_json; use ruma::{ @@ -34,7 +35,7 @@ use crate::{ deserialized_responses::MemberEvent, media::{MediaFormat, MediaRequest, MediaThumbnailSize}, store::{Result, StateStoreExt}, - RoomInfo, RoomType, StateChanges, + RoomInfo, RoomType, StateChanges, StateStoreDataKey, StateStoreDataValue, }; /// `StateStore` integration tests. @@ -265,7 +266,7 @@ impl StateStoreIntegrationTests for DynStateStore { let room_id = room_id(); self.populate().await?; - assert!(self.get_sync_token().await?.is_some()); + assert!(self.get_kv_data(StateStoreDataKey::SyncToken).await?.is_some()); assert_eq!( self.get_state_event_static::(room_id) .await? @@ -312,7 +313,7 @@ impl StateStoreIntegrationTests for DynStateStore { let user_id = user_id(); self.populate().await?; - assert!(self.get_sync_token().await?.is_some()); + assert!(self.get_kv_data(StateStoreDataKey::SyncToken).await?.is_some()); assert!(self.get_presence_event(user_id).await?.is_some()); assert_eq!(self.get_room_infos().await?.len(), 1, "Expected to find 1 room info"); assert_eq!( @@ -401,21 +402,54 @@ impl StateStoreIntegrationTests for DynStateStore { } async fn test_filter_saving(&self) { - let test_name = "filter_name"; + let filter_name = "filter_name"; let filter_id = "filter_id_1234"; - assert_eq!(self.get_filter(test_name).await.unwrap(), None); - self.save_filter(test_name, filter_id).await.unwrap(); - assert_eq!(self.get_filter(test_name).await.unwrap(), Some(filter_id.to_owned())); + + self.set_kv_data( + StateStoreDataKey::Filter(filter_name), + StateStoreDataValue::Filter(filter_id.to_owned()), + ) + .await + .unwrap(); + let stored_filter_id = assert_matches!( + self.get_kv_data(StateStoreDataKey::Filter(filter_name)).await, + Ok(Some(StateStoreDataValue::Filter(s))) => s + ); + assert_eq!(stored_filter_id, filter_id); + + self.remove_kv_data(StateStoreDataKey::Filter(filter_name)).await.unwrap(); + assert_matches!(self.get_kv_data(StateStoreDataKey::Filter(filter_name)).await, Ok(None)); } async fn test_sync_token_saving(&self) { - let mut changes = StateChanges::default(); - let sync_token = "t392-516_47314_0_7_1".to_owned(); + let sync_token_1 = "t392-516_47314_0_7_1"; + let sync_token_2 = "t392-516_47314_0_7_2"; - changes.sync_token = Some(sync_token.clone()); - assert_eq!(self.get_sync_token().await.unwrap(), None); + assert_matches!(self.get_kv_data(StateStoreDataKey::SyncToken).await, Ok(None)); + + let changes = + StateChanges { sync_token: Some(sync_token_1.to_owned()), ..Default::default() }; self.save_changes(&changes).await.unwrap(); - assert_eq!(self.get_sync_token().await.unwrap(), Some(sync_token)); + let stored_sync_token = assert_matches!( + self.get_kv_data(StateStoreDataKey::SyncToken).await, + Ok(Some(StateStoreDataValue::SyncToken(s))) => s + ); + assert_eq!(stored_sync_token, sync_token_1); + + self.set_kv_data( + StateStoreDataKey::SyncToken, + StateStoreDataValue::SyncToken(sync_token_2.to_owned()), + ) + .await + .unwrap(); + let stored_sync_token = assert_matches!( + self.get_kv_data(StateStoreDataKey::SyncToken).await, + Ok(Some(StateStoreDataValue::SyncToken(s))) => s + ); + assert_eq!(stored_sync_token, sync_token_2); + + self.remove_kv_data(StateStoreDataKey::SyncToken).await.unwrap(); + assert_matches!(self.get_kv_data(StateStoreDataKey::SyncToken).await, Ok(None)); } async fn test_stripped_member_saving(&self) { diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index 20a3972bf..14dcc74ef 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -37,7 +37,10 @@ use ruma::{ use tracing::{debug, info, warn}; use super::{Result, RoomInfo, StateChanges, StateStore, StoreError}; -use crate::{deserialized_responses::RawMemberEvent, media::MediaRequest, MinimalRoomMemberEvent}; +use crate::{ + deserialized_responses::RawMemberEvent, media::MediaRequest, MinimalRoomMemberEvent, + StateStoreDataKey, StateStoreDataValue, +}; /// In-Memory, non-persistent implementation of the `StateStore` /// @@ -119,18 +122,48 @@ impl MemoryStore { } } - async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<()> { - self.filters.insert(filter_name.to_owned(), filter_id.to_owned()); + async fn get_kv_data(&self, key: StateStoreDataKey<'_>) -> Result> { + match key { + StateStoreDataKey::SyncToken => { + Ok(self.sync_token.read().unwrap().clone().map(StateStoreDataValue::SyncToken)) + } + StateStoreDataKey::Filter(filter_name) => Ok(self + .filters + .get(filter_name) + .map(|f| StateStoreDataValue::Filter(f.value().clone()))), + } + } + + async fn set_kv_data( + &self, + key: StateStoreDataKey<'_>, + value: StateStoreDataValue, + ) -> Result<()> { + match key { + StateStoreDataKey::SyncToken => { + *self.sync_token.write().unwrap() = + Some(value.into_sync_token().expect("Session data not a sync token")) + } + StateStoreDataKey::Filter(filter_name) => { + self.filters.insert( + filter_name.to_owned(), + value.into_filter().expect("Session data not a filter"), + ); + } + } Ok(()) } - async fn get_filter(&self, filter_name: &str) -> Result> { - Ok(self.filters.get(filter_name).map(|f| f.to_string())) - } + async fn remove_kv_data(&self, key: StateStoreDataKey<'_>) -> Result<()> { + match key { + StateStoreDataKey::SyncToken => *self.sync_token.write().unwrap() = None, + StateStoreDataKey::Filter(filter_name) => { + self.filters.remove(filter_name); + } + } - async fn get_sync_token(&self) -> Result> { - Ok(self.sync_token.read().unwrap().clone()) + Ok(()) } async fn save_changes(&self, changes: &StateChanges) -> Result<()> { @@ -596,22 +629,26 @@ impl MemoryStore { impl StateStore for MemoryStore { type Error = StoreError; - async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<()> { - self.save_filter(filter_name, filter_id).await + async fn get_kv_data(&self, key: StateStoreDataKey<'_>) -> Result> { + self.get_kv_data(key).await + } + + async fn set_kv_data( + &self, + key: StateStoreDataKey<'_>, + value: StateStoreDataValue, + ) -> Result<()> { + self.set_kv_data(key, value).await + } + + async fn remove_kv_data(&self, key: StateStoreDataKey<'_>) -> Result<()> { + self.remove_kv_data(key).await } async fn save_changes(&self, changes: &StateChanges) -> Result<()> { self.save_changes(changes).await } - async fn get_filter(&self, filter_id: &str) -> Result> { - self.get_filter(filter_id).await - } - - async fn get_sync_token(&self) -> Result> { - self.get_sync_token().await - } - async fn get_presence_event(&self, user_id: &UserId) -> Result>> { self.get_presence_event(user_id).await } diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index 2bbb93a41..88104e539 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -74,7 +74,10 @@ mod memory_store; pub use self::integration_tests::StateStoreIntegrationTests; pub use self::{ memory_store::MemoryStore, - traits::{DynStateStore, IntoStateStore, StateStore, StateStoreExt}, + traits::{ + DynStateStore, IntoStateStore, StateStore, StateStoreDataKey, StateStoreDataValue, + StateStoreExt, + }, }; /// State store specific error type. @@ -191,7 +194,8 @@ impl Store { self.stripped_rooms.insert(room.room_id().to_owned(), room); } - let token = self.get_sync_token().await?; + let token = + self.get_kv_data(StateStoreDataKey::SyncToken).await?.and_then(|s| s.into_sync_token()); *self.sync_token.write().await = token; self.session_meta.set(session_meta).expect("Session Meta was already set"); diff --git a/crates/matrix-sdk-base/src/store/traits.rs b/crates/matrix-sdk-base/src/store/traits.rs index d3968db87..6659b75d5 100644 --- a/crates/matrix-sdk-base/src/store/traits.rs +++ b/crates/matrix-sdk-base/src/store/traits.rs @@ -41,30 +41,43 @@ use crate::{ #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait StateStore: AsyncTraitDeps { /// The error type used by this state store. - type Error: fmt::Debug + Into; + type Error: fmt::Debug + Into + From; - /// Save the given filter id under the given name. + /// Get key-value data from the store. /// /// # Arguments /// - /// * `filter_name` - The name that should be used to store the filter id. + /// * `key` - The key to fetch data for. + async fn get_kv_data( + &self, + key: StateStoreDataKey<'_>, + ) -> Result, Self::Error>; + + /// Put key-value data into the store. /// - /// * `filter_id` - The filter id that should be stored in the state store. - async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<(), Self::Error>; + /// # Arguments + /// + /// * `key` - The key to identify the data in the store. + /// + /// * `value` - The data to insert. + /// + /// Panics if the key and value variants do not match. + async fn set_kv_data( + &self, + key: StateStoreDataKey<'_>, + value: StateStoreDataValue, + ) -> Result<(), Self::Error>; + + /// Remove key-value data from the store. + /// + /// # Arguments + /// + /// * `key` - The key to remove the data for. + async fn remove_kv_data(&self, key: StateStoreDataKey<'_>) -> Result<(), Self::Error>; /// Save the set of state changes in the store. async fn save_changes(&self, changes: &StateChanges) -> Result<(), Self::Error>; - /// Get the filter id that was stored under the given filter name. - /// - /// # Arguments - /// - /// * `filter_name` - The name that was used to store the filter id. - async fn get_filter(&self, filter_name: &str) -> Result, Self::Error>; - - /// Get the last stored sync token. - async fn get_sync_token(&self) -> Result, Self::Error>; - /// Get the stored presence event for the given user. /// /// # Arguments @@ -308,22 +321,29 @@ impl fmt::Debug for EraseStateStoreError { impl StateStore for EraseStateStoreError { type Error = StoreError; - async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<(), Self::Error> { - self.0.save_filter(filter_name, filter_id).await.map_err(Into::into) + async fn get_kv_data( + &self, + key: StateStoreDataKey<'_>, + ) -> Result, Self::Error> { + self.0.get_kv_data(key).await.map_err(Into::into) + } + + async fn set_kv_data( + &self, + key: StateStoreDataKey<'_>, + value: StateStoreDataValue, + ) -> Result<(), Self::Error> { + self.0.set_kv_data(key, value).await.map_err(Into::into) + } + + async fn remove_kv_data(&self, key: StateStoreDataKey<'_>) -> Result<(), Self::Error> { + self.0.remove_kv_data(key).await.map_err(Into::into) } async fn save_changes(&self, changes: &StateChanges) -> Result<(), Self::Error> { self.0.save_changes(changes).await.map_err(Into::into) } - async fn get_filter(&self, filter_name: &str) -> Result, Self::Error> { - self.0.get_filter(filter_name).await.map_err(Into::into) - } - - async fn get_sync_token(&self) -> Result, Self::Error> { - self.0.get_sync_token().await.map_err(Into::into) - } - async fn get_presence_event( &self, user_id: &UserId, @@ -605,3 +625,51 @@ where unsafe { Arc::from_raw(ptr_erased) } } } + +/// A value for key-value data that should be persisted into the store. +#[derive(Debug, Clone)] +pub enum StateStoreDataValue { + /// The sync token. + SyncToken(String), + + /// A filter with the given ID. + Filter(String), +} + +impl StateStoreDataValue { + /// Get this value if it is a sync token. + pub fn into_sync_token(self) -> Option { + match self { + Self::SyncToken(token) => Some(token), + Self::Filter(_) => None, + } + } + + /// Get this value if it is a filter. + pub fn into_filter(self) -> Option { + match self { + Self::SyncToken(_) => None, + Self::Filter(id) => Some(id), + } + } +} + +/// A key for key-value data. +#[derive(Debug, Clone, Copy)] +pub enum StateStoreDataKey<'a> { + /// The sync token. + SyncToken, + + /// A filter with the given name. + Filter(&'a str), +} + +impl<'a> StateStoreDataKey<'a> { + /// The string to use to encode this key. + pub const fn encoding_key(&self) -> &str { + match self { + Self::SyncToken => "sync_token", + Self::Filter(_) => "filter", + } + } +} diff --git a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs index fbee5c291..ca8e76f37 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs @@ -20,6 +20,7 @@ use std::{ use gloo_utils::format::JsValueSerdeExt; use indexed_db_futures::{prelude::*, request::OpenDbRequest, IdbDatabase, IdbVersionChangeEvent}; use js_sys::Date as JsDate; +use matrix_sdk_base::StateStoreDataKey; use matrix_sdk_store_encryption::StoreCipher; use serde::{Deserialize, Serialize}; use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue}; @@ -376,12 +377,23 @@ async fn migrate_to_v4( let sync_token = sync_token_store.get(&JsValue::from_str(OLD_KEYS::SYNC_TOKEN))?.await?; if let Some(sync_token) = sync_token { - values.push((encode_key(store_cipher, KEYS::SYNC_TOKEN, KEYS::SYNC_TOKEN), sync_token)); + values.push(( + encode_key( + store_cipher, + StateStoreDataKey::SyncToken.encoding_key(), + StateStoreDataKey::SyncToken.encoding_key(), + ), + sync_token, + )); } // Filters let session_store = tx.object_store(OLD_KEYS::SESSION)?; - let range = encode_to_range(store_cipher, KEYS::FILTER, KEYS::FILTER)?; + let range = encode_to_range( + store_cipher, + StateStoreDataKey::Filter("").encoding_key(), + StateStoreDataKey::Filter("").encoding_key(), + )?; if let Some(cursor) = session_store.open_cursor_with_range(&range)?.await? { while let Some(key) = cursor.key() { let value = cursor.value(); @@ -410,7 +422,7 @@ mod tests { use assert_matches::assert_matches; use indexed_db_futures::prelude::*; - use matrix_sdk_base::{StateStore, StoreError}; + use matrix_sdk_base::{StateStore, StateStoreDataKey, StoreError}; use matrix_sdk_test::async_test; use ruma::{ events::{AnySyncStateEvent, StateEventType}, @@ -701,11 +713,19 @@ mod tests { let session_store = tx.object_store(OLD_KEYS::SESSION)?; session_store.put_key_val( - &encode_key(None, KEYS::FILTER, (KEYS::FILTER, filter_1)), + &encode_key( + None, + StateStoreDataKey::Filter("").encoding_key(), + (StateStoreDataKey::Filter("").encoding_key(), filter_1), + ), &serialize_event(None, &filter_1_id)?, )?; session_store.put_key_val( - &encode_key(None, KEYS::FILTER, (KEYS::FILTER, filter_2)), + &encode_key( + None, + StateStoreDataKey::Filter("").encoding_key(), + (StateStoreDataKey::Filter("").encoding_key(), filter_2), + ), &serialize_event(None, &filter_2_id)?, )?; @@ -716,13 +736,28 @@ mod tests { // this transparently migrates to the latest version let store = IndexeddbStateStore::builder().name(name).build().await?; - let stored_sync_token = store.get_sync_token().await.unwrap().unwrap(); + let stored_sync_token = store + .get_kv_data(StateStoreDataKey::SyncToken) + .await? + .unwrap() + .into_sync_token() + .unwrap(); assert_eq!(stored_sync_token, sync_token); - let stored_filter_1_id = store.get_filter(filter_1).await.unwrap().unwrap(); + let stored_filter_1_id = store + .get_kv_data(StateStoreDataKey::Filter(filter_1)) + .await? + .unwrap() + .into_filter() + .unwrap(); assert_eq!(stored_filter_1_id, filter_1_id); - let stored_filter_2_id = store.get_filter(filter_2).await.unwrap().unwrap(); + let stored_filter_2_id = store + .get_kv_data(StateStoreDataKey::Filter(filter_2)) + .await? + .unwrap() + .into_filter() + .unwrap(); assert_eq!(stored_filter_2_id, filter_2_id); Ok(()) diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index 2f4fe133a..cb615423f 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -25,7 +25,7 @@ use matrix_sdk_base::{ deserialized_responses::RawMemberEvent, media::{MediaRequest, UniqueKey}, store::{StateChanges, StateStore, StoreError}, - MinimalStateEvent, RoomInfo, + MinimalStateEvent, RoomInfo, StateStoreDataKey, StateStoreDataValue, }; use matrix_sdk_store_encryption::{Error as EncryptionError, StoreCipher}; use ruma::{ @@ -147,8 +147,6 @@ mod KEYS { // static keys pub const STORE_KEY: &str = "store_key"; - pub const FILTER: &str = "filter"; - pub const SYNC_TOKEN: &str = "sync_token"; } pub use KEYS::ALL_STORES; @@ -413,6 +411,15 @@ impl IndexeddbStateStore { .map(|f| self.deserialize_event(f)) .transpose() } + + fn encode_kv_data_key(&self, key: StateStoreDataKey<'_>) -> JsValue { + match key { + StateStoreDataKey::SyncToken => self.encode_key(key.encoding_key(), key.encoding_key()), + StateStoreDataKey::Filter(filter_name) => { + self.encode_key(key.encoding_key(), (key.encoding_key(), filter_name)) + } + } + } } // Small hack to have the following macro invocation act as the appropriate @@ -445,41 +452,71 @@ macro_rules! impl_state_store { } impl_state_store! { - async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<()> { + async fn get_kv_data( + &self, + key: StateStoreDataKey<'_>, + ) -> Result> { + let encoded_key = self.encode_kv_data_key(key); + + let value = self + .inner + .transaction_on_one_with_mode(KEYS::KV, IdbTransactionMode::Readonly)? + .object_store(KEYS::KV)? + .get(&encoded_key)? + .await? + .map(|f| self.deserialize_event::(f)) + .transpose()?; + + let value = match key { + StateStoreDataKey::SyncToken => value.map(StateStoreDataValue::SyncToken), + StateStoreDataKey::Filter(_) => value.map(StateStoreDataValue::Filter), + }; + + Ok(value) + } + + async fn set_kv_data( + &self, + key: StateStoreDataKey<'_>, + value: StateStoreDataValue, + ) -> Result<()> { + let encoded_key = self.encode_kv_data_key(key); + + let value = match key { + StateStoreDataKey::SyncToken => { + value.into_sync_token().expect("Session data not a sync token") + } + StateStoreDataKey::Filter(_) => { + value.into_filter().expect("Session data not a filter") + } + }; + let tx = self .inner .transaction_on_one_with_mode(KEYS::KV, IdbTransactionMode::Readwrite)?; let obj = tx.object_store(KEYS::KV)?; - obj.put_key_val( - &self.encode_key(KEYS::FILTER, (KEYS::FILTER, filter_name)), - &self.serialize_event(&filter_id)?, - )?; + obj.put_key_val(&encoded_key, &self.serialize_event(&value)?)?; tx.await.into_result()?; Ok(()) } - async fn get_filter(&self, filter_name: &str) -> Result> { - self.inner - .transaction_on_one_with_mode(KEYS::KV, IdbTransactionMode::Readonly)? - .object_store(KEYS::KV)? - .get(&self.encode_key(KEYS::FILTER, (KEYS::FILTER, filter_name)))? - .await? - .map(|f| self.deserialize_event(f)) - .transpose() - } + async fn remove_kv_data(&self, key: StateStoreDataKey<'_>) -> Result<()> { + let encoded_key = self.encode_kv_data_key(key); - async fn get_sync_token(&self) -> Result> { - self.inner - .transaction_on_one_with_mode(KEYS::KV, IdbTransactionMode::Readonly)? - .object_store(KEYS::KV)? - .get(&self.encode_key(KEYS::SYNC_TOKEN, KEYS::SYNC_TOKEN))? - .await? - .map(|f| self.deserialize_event(f)) - .transpose() + let tx = self + .inner + .transaction_on_one_with_mode(KEYS::KV, IdbTransactionMode::Readwrite)?; + let obj = tx.object_store(KEYS::KV)?; + + obj.delete(&encoded_key)?; + + tx.await.into_result()?; + + Ok(()) } async fn save_changes(&self, changes: &StateChanges) -> Result<()> { @@ -543,8 +580,10 @@ impl_state_store! { self.inner.transaction_on_multi_with_mode(&stores, IdbTransactionMode::Readwrite)?; if let Some(s) = &changes.sync_token { - tx.object_store(KEYS::KV)? - .put_key_val(&self.encode_key(KEYS::SYNC_TOKEN, KEYS::SYNC_TOKEN), &self.serialize_event(s)?)?; + tx.object_store(KEYS::KV)?.put_key_val( + &self.encode_kv_data_key(StateStoreDataKey::SyncToken), + &self.serialize_event(s)?, + )?; } if !changes.ambiguity_maps.is_empty() { diff --git a/crates/matrix-sdk-sled/src/state_store/migrations.rs b/crates/matrix-sdk-sled/src/state_store/migrations.rs index 2c996e8a6..0f3df7541 100644 --- a/crates/matrix-sdk-sled/src/state_store/migrations.rs +++ b/crates/matrix-sdk-sled/src/state_store/migrations.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use matrix_sdk_base::store::{Result as StoreResult, StoreError}; +use matrix_sdk_base::{ + store::{Result as StoreResult, StoreError}, + StateStoreDataKey, +}; use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue}; use sled::{transaction::TransactionError, Batch, Transactional, Tree}; use tracing::debug; @@ -167,13 +170,13 @@ impl SledStateStore { let mut batch = sled::Batch::default(); // Sync token - let sync_token = session.get(keys::SYNC_TOKEN.encode())?; + let sync_token = session.get(StateStoreDataKey::SyncToken.encoding_key().encode())?; if let Some(sync_token) = sync_token { - batch.insert(keys::SYNC_TOKEN.encode(), sync_token); + batch.insert(StateStoreDataKey::SyncToken.encoding_key().encode(), sync_token); } // Filters - let key = self.encode_key(keys::SESSION, keys::FILTER); + let key = self.encode_key(keys::SESSION, StateStoreDataKey::Filter("").encoding_key()); for res in session.scan_prefix(key) { let (key, value) = res?; batch.insert(key, value); @@ -222,6 +225,7 @@ pub const V1_DB_STORES: &[&str] = &[ #[cfg(test)] mod test { + use matrix_sdk_base::StateStoreDataKey; use matrix_sdk_test::async_test; use ruma::{ events::{AnySyncStateEvent, StateEventType}, @@ -391,13 +395,22 @@ mod test { let session = store.inner.open_tree(old_keys::SESSION).unwrap(); let mut batch = sled::Batch::default(); - batch.insert(keys::SYNC_TOKEN.encode(), store.serialize_value(&sync_token).unwrap()); batch.insert( - store.encode_key(keys::SESSION, (keys::FILTER, filter_1)), + StateStoreDataKey::SyncToken.encoding_key().encode(), + store.serialize_value(&sync_token).unwrap(), + ); + batch.insert( + store.encode_key( + keys::SESSION, + (StateStoreDataKey::Filter("").encoding_key(), filter_1), + ), store.serialize_value(&filter_1_id).unwrap(), ); batch.insert( - store.encode_key(keys::SESSION, (keys::FILTER, filter_2)), + store.encode_key( + keys::SESSION, + (StateStoreDataKey::Filter("").encoding_key(), filter_2), + ), store.serialize_value(&filter_2_id).unwrap(), ); session.apply_batch(batch).unwrap(); @@ -412,13 +425,31 @@ mod test { .build() .unwrap(); - let stored_sync_token = store.get_sync_token().await.unwrap().unwrap(); + let stored_sync_token = store + .get_kv_data(StateStoreDataKey::SyncToken) + .await + .unwrap() + .unwrap() + .into_sync_token() + .unwrap(); assert_eq!(stored_sync_token, sync_token); - let stored_filter_1_id = store.get_filter(filter_1).await.unwrap().unwrap(); + let stored_filter_1_id = store + .get_kv_data(StateStoreDataKey::Filter(filter_1)) + .await + .unwrap() + .unwrap() + .into_filter() + .unwrap(); assert_eq!(stored_filter_1_id, filter_1_id); - let stored_filter_2_id = store.get_filter(filter_2).await.unwrap().unwrap(); + let stored_filter_2_id = store + .get_kv_data(StateStoreDataKey::Filter(filter_2)) + .await + .unwrap() + .unwrap() + .into_filter() + .unwrap(); assert_eq!(stored_filter_2_id, filter_2_id); } } diff --git a/crates/matrix-sdk-sled/src/state_store/mod.rs b/crates/matrix-sdk-sled/src/state_store/mod.rs index 0afb3a878..9fbd2b724 100644 --- a/crates/matrix-sdk-sled/src/state_store/mod.rs +++ b/crates/matrix-sdk-sled/src/state_store/mod.rs @@ -26,7 +26,7 @@ use matrix_sdk_base::{ deserialized_responses::RawMemberEvent, media::{MediaRequest, UniqueKey}, store::{Result as StoreResult, StateChanges, StateStore, StoreError}, - MinimalStateEvent, RoomInfo, + MinimalStateEvent, RoomInfo, StateStoreDataKey, StateStoreDataValue, }; use matrix_sdk_store_encryption::{Error as KeyEncryptionError, StoreCipher}; use ruma::{ @@ -104,9 +104,7 @@ impl From for StoreError { mod keys { // Static keys - pub const SYNC_TOKEN: &str = "sync_token"; pub const SESSION: &str = "session"; - pub const FILTER: &str = "filter"; // Stores pub const ACCOUNT_DATA: &str = "account-data"; @@ -411,23 +409,54 @@ impl SledStateStore { } } - pub async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<()> { - self.kv.insert( - self.encode_key(keys::SESSION, (keys::FILTER, filter_name)), - self.serialize_value(&filter_id)?, - )?; + fn encode_kv_data_key(&self, key: StateStoreDataKey<'_>) -> Vec { + match key { + StateStoreDataKey::SyncToken => key.encoding_key().encode(), + StateStoreDataKey::Filter(filter_name) => { + self.encode_key(keys::SESSION, (key.encoding_key(), filter_name)) + } + } + } + + async fn get_kv_data(&self, key: StateStoreDataKey<'_>) -> Result> { + let encoded_key = self.encode_kv_data_key(key); + + let value = + self.kv.get(encoded_key)?.map(|e| self.deserialize_value::(&e)).transpose()?; + + let value = match key { + StateStoreDataKey::SyncToken => value.map(StateStoreDataValue::SyncToken), + StateStoreDataKey::Filter(_) => value.map(StateStoreDataValue::Filter), + }; + + Ok(value) + } + + async fn set_kv_data( + &self, + key: StateStoreDataKey<'_>, + value: StateStoreDataValue, + ) -> Result<()> { + let encoded_key = self.encode_kv_data_key(key); + + let value = match key { + StateStoreDataKey::SyncToken => { + value.into_sync_token().expect("Session data not a sync token") + } + StateStoreDataKey::Filter(_) => value.into_filter().expect("Session data not a filter"), + }; + + self.kv.insert(encoded_key, self.serialize_value(&value)?)?; + Ok(()) } - pub async fn get_filter(&self, filter_name: &str) -> Result> { - self.kv - .get(self.encode_key(keys::SESSION, (keys::FILTER, filter_name)))? - .map(|f| self.deserialize_value(&f)) - .transpose() - } + async fn remove_kv_data(&self, key: StateStoreDataKey<'_>) -> Result<()> { + let encoded_key = self.encode_kv_data_key(key); - pub async fn get_sync_token(&self) -> Result> { - self.kv.get(keys::SYNC_TOKEN.encode())?.map(|t| self.deserialize_value(&t)).transpose() + self.kv.remove(encoded_key)?; + + Ok(()) } pub async fn save_changes(&self, changes: &StateChanges) -> Result<()> { @@ -792,7 +821,7 @@ impl SledStateStore { .transaction(|(kv, account_data)| { if let Some(s) = &changes.sync_token { kv.insert( - keys::SYNC_TOKEN.encode(), + self.encode_kv_data_key(StateStoreDataKey::SyncToken), self.serialize_value(s).map_err(ConflictableTransactionError::Abort)?, )?; } @@ -1323,22 +1352,29 @@ impl SledStateStore { impl StateStore for SledStateStore { type Error = StoreError; - async fn save_filter(&self, filter_name: &str, filter_id: &str) -> StoreResult<()> { - self.save_filter(filter_name, filter_id).await.map_err(Into::into) + async fn get_kv_data( + &self, + key: StateStoreDataKey<'_>, + ) -> StoreResult> { + self.get_kv_data(key).await.map_err(Into::into) + } + + async fn set_kv_data( + &self, + key: StateStoreDataKey<'_>, + value: StateStoreDataValue, + ) -> StoreResult<()> { + self.set_kv_data(key, value).await.map_err(Into::into) + } + + async fn remove_kv_data(&self, key: StateStoreDataKey<'_>) -> StoreResult<()> { + self.remove_kv_data(key).await.map_err(Into::into) } async fn save_changes(&self, changes: &StateChanges) -> StoreResult<()> { self.save_changes(changes).await.map_err(Into::into) } - async fn get_filter(&self, filter_id: &str) -> StoreResult> { - self.get_filter(filter_id).await.map_err(Into::into) - } - - async fn get_sync_token(&self) -> StoreResult> { - self.get_sync_token().await.map_err(Into::into) - } - async fn get_presence_event( &self, user_id: &UserId, From 18e6f1036727ba5833be4b66900efe34bd29efc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= <76261501+zecakeh@users.noreply.github.com> Date: Tue, 7 Mar 2023 15:12:33 +0100 Subject: [PATCH 137/166] examples: Also handle SSO login in the login example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- examples/login/Cargo.toml | 1 + examples/login/src/main.rs | 244 +++++++++++++++++++++++++++++++------ 2 files changed, 207 insertions(+), 38 deletions(-) diff --git a/examples/login/Cargo.toml b/examples/login/Cargo.toml index 0b18afdd9..ef6805564 100644 --- a/examples/login/Cargo.toml +++ b/examples/login/Cargo.toml @@ -17,3 +17,4 @@ url = "2.2.2" [dependencies.matrix-sdk] path = "../../crates/matrix-sdk" version = "0.6.0" +features = ["sso-login"] diff --git a/examples/login/src/main.rs b/examples/login/src/main.rs index e75ab69f9..f954ee2eb 100644 --- a/examples/login/src/main.rs +++ b/examples/login/src/main.rs @@ -1,64 +1,232 @@ -use std::{env, process::exit}; +use std::{ + env, fmt, + io::{self, Write}, + process::exit, +}; +use anyhow::anyhow; use matrix_sdk::{ self, config::SyncSettings, room::Room, - ruma::events::room::message::{ - MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent, TextMessageEventContent, + ruma::{ + api::client::session::get_login_types::v3::{IdentityProvider, LoginType}, + events::room::message::{MessageType, OriginalSyncRoomMessageEvent}, }, Client, }; use url::Url; -async fn on_room_message(event: OriginalSyncRoomMessageEvent, room: Room) { - if let Room::Joined(room) = room { - if let OriginalSyncRoomMessageEvent { - content: - RoomMessageEventContent { - msgtype: MessageType::Text(TextMessageEventContent { body: msg_body, .. }), - .. - }, - sender, - .. - } = event - { - let member = room.get_member(&sender).await.unwrap().unwrap(); - let name = member.display_name().unwrap_or_else(|| member.user_id().as_str()); - println!("{name}: {msg_body}"); - } - } +/// The initial device name when logging in with a device for the first time. +const INITIAL_DEVICE_DISPLAY_NAME: &str = "login client"; + +/// A simple program that adapts to the different login methods offered by a +/// Matrix homeserver. +/// +/// Homeservers usually offer to login either via password, Single Sign-On (SSO) +/// or both. +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + + let Some(homeserver_url) = env::args().nth(1) else { + eprintln!( + "Usage: {} ", + env::args().next().unwrap() + ); + exit(1) + }; + + login_and_sync(homeserver_url).await?; + + Ok(()) } -async fn login(homeserver_url: String, username: &str, password: &str) -> matrix_sdk::Result<()> { - let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL"); - let client = Client::new(homeserver_url).await.unwrap(); +/// Log in to the given homeserver and sync. +async fn login_and_sync(homeserver_url: String) -> anyhow::Result<()> { + let homeserver_url = Url::parse(&homeserver_url)?; + let client = Client::new(homeserver_url).await?; + // First, let's figure out what login types are supported by the homeserver. + let mut choices = Vec::new(); + let login_types = client.get_login_types().await?.flows; + + for login_type in login_types { + match login_type { + LoginType::Password(_) => { + choices.push(LoginChoice::Password) + } + LoginType::Sso(sso) => { + if sso.identity_providers.is_empty() { + choices.push(LoginChoice::Sso) + } else { + choices.extend(sso.identity_providers.into_iter().map(LoginChoice::SsoIdp)) + } + } + // This is used for SSO, so it's not a separate choice. + LoginType::Token(_) | + // This is only for application services, ignore it here. + LoginType::ApplicationService(_) => {}, + // We don't support unknown login types. + _ => {}, + } + } + + match choices.len() { + 0 => return Err(anyhow!("Homeserver login types incompatible with this client")), + 1 => choices[0].login(&client).await?, + _ => offer_choices_and_login(&client, choices).await?, + } + + // Now that we are logged in, we can sync and listen to new messages. client.add_event_handler(on_room_message); - - client.login_username(username, password).initial_device_display_name("rust-sdk").await?; + // This will sync until an error happens or the program is killed. client.sync(SyncSettings::new()).await?; Ok(()) } -#[tokio::main] -async fn main() -> anyhow::Result<()> { - tracing_subscriber::fmt::init(); +#[derive(Debug)] +enum LoginChoice { + /// Login with username and password. + Password, - let (homeserver_url, username, password) = - match (env::args().nth(1), env::args().nth(2), env::args().nth(3)) { - (Some(a), Some(b), Some(c)) => (a, b, c), - _ => { - eprintln!( - "Usage: {} ", - env::args().next().unwrap() - ); - exit(1) + /// Login with SSO. + Sso, + + /// Login with a specific SSO identity provider. + SsoIdp(IdentityProvider), +} + +impl LoginChoice { + /// Login with this login choice. + async fn login(&self, client: &Client) -> anyhow::Result<()> { + match self { + LoginChoice::Password => login_with_password(client).await, + LoginChoice::Sso => login_with_sso(client, None).await, + LoginChoice::SsoIdp(idp) => login_with_sso(client, Some(idp)).await, + } + } +} + +impl fmt::Display for LoginChoice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LoginChoice::Password => write!(f, "Username and password"), + LoginChoice::Sso => write!(f, "SSO"), + LoginChoice::SsoIdp(idp) => write!(f, "SSO via {}", idp.name), + } + } +} + +/// Offer the given choices to the user and login with the selected option. +async fn offer_choices_and_login(client: &Client, choices: Vec) -> anyhow::Result<()> { + println!("Several options are available to login with this homeserver:\n"); + + let choice = loop { + for (idx, login_choice) in choices.iter().enumerate() { + println!("{idx}) {login_choice}"); + } + + print!("\nEnter your choice: "); + io::stdout().flush().expect("Unable to write to stdout"); + let mut choice_str = String::new(); + io::stdin().read_line(&mut choice_str).expect("Unable to read user input"); + + match choice_str.trim().parse::() { + Ok(choice) => { + if choice >= choices.len() { + eprintln!("This is not a valid choice"); + } else { + break choice; + } } + Err(_) => eprintln!("This is not a valid choice. Try again.\n"), }; + }; - login(homeserver_url, &username, &password).await?; + choices[choice].login(client).await?; Ok(()) } + +/// Login with a username and password. +async fn login_with_password(client: &Client) -> anyhow::Result<()> { + println!("Logging in with username and password…"); + + loop { + print!("\nUsername: "); + io::stdout().flush().expect("Unable to write to stdout"); + let mut username = String::new(); + io::stdin().read_line(&mut username).expect("Unable to read user input"); + username = username.trim().to_owned(); + + print!("Password: "); + io::stdout().flush().expect("Unable to write to stdout"); + let mut password = String::new(); + io::stdin().read_line(&mut password).expect("Unable to read user input"); + password = password.trim().to_owned(); + + match client + .login_username(&username, &password) + .initial_device_display_name(INITIAL_DEVICE_DISPLAY_NAME) + .await + { + Ok(_) => { + println!("Logged in as {username}"); + break; + } + Err(error) => { + println!("Error logging in: {error}"); + println!("Please try again\n"); + } + } + } + + Ok(()) +} + +/// Login with SSO. +async fn login_with_sso(client: &Client, idp: Option<&IdentityProvider>) -> anyhow::Result<()> { + println!("Logging in with SSO…"); + + let mut login_builder = client.login_sso(|url| async move { + // Usually we would want to use a library to open the URL in the browser, but + // let's keep it simple. + println!("\nOpen this URL in your browser: {url}\n"); + println!("Waiting for login token…"); + Ok(()) + }); + + if let Some(idp) = idp { + login_builder = login_builder.identity_provider_id(&idp.id); + } + + login_builder.await?; + + println!("Logged in as {}", client.user_id().unwrap()); + + Ok(()) +} + +/// Handle room messages by logging them. +async fn on_room_message(event: OriginalSyncRoomMessageEvent, room: Room) { + // We only want to listen to joined rooms. + let Room::Joined(room) = room else { + return; + }; + + // We only want to log text messages. + let MessageType::Text(msgtype) = &event.content.msgtype else { + return; + }; + + let member = room + .get_member(&event.sender) + .await + .expect("Couldn't get the room member") + .expect("The room member doesn't exist"); + let name = member.name(); + + println!("{name}: {}", msgtype.body); +} From 9f0563eb95d538e13295d83c7d4a8cf7427c1aeb Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 10:11:07 +0100 Subject: [PATCH 138/166] feat(sdk): `SlidingSync::stream` can match expected responses. This patch adds a new feature. Each call to `SlidingSync::stream` generates a unique `stream_id`. This `stream_id` is passed to requests as a `txn_id` field. Returned responses must hold the same `txn_id`, otherwise it means `stream` has received unexpected responses from another `stream` or something else. So far, when the `txn_id` field of the response and the `stream_id` don't match, a log with the `ERROR` level is raised. Should we do more, like ignoring the response entirely? Not sure yet. --- Cargo.lock | 78 ++++++++++++++++++----- Cargo.toml | 3 + crates/matrix-sdk/Cargo.toml | 2 + crates/matrix-sdk/src/sliding_sync/mod.rs | 32 ++++++++-- 4 files changed, 94 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b6aaf3b5..050061321 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2642,6 +2642,7 @@ dependencies = [ "tracing", "tracing-subscriber", "url", + "uuid", "wasm-bindgen-test", "wiremock", "zeroize", @@ -2919,7 +2920,7 @@ dependencies = [ "byteorder", "image 0.23.14", "qrcode", - "ruma-common", + "ruma-common 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", "thiserror", "vodozemac", ] @@ -4291,26 +4292,24 @@ dependencies = [ [[package]] name = "ruma" version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6429e3fae5d6ab07742bcf9a1705f68f97d082801cc5afe9290579bf7abcf053" +source = "git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id#7a78869de4ec920ea770ffd2a27eafcc1b3619b0" dependencies = [ "assign", "js_int", "js_option", "ruma-appservice-api", "ruma-client-api", - "ruma-common", + "ruma-common 0.11.3 (git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id)", "ruma-federation-api", ] [[package]] name = "ruma-appservice-api" version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed16a943a44ba290481f2284f71cac31757fabaee8ca2d059afd20ff8b33c31" +source = "git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id#7a78869de4ec920ea770ffd2a27eafcc1b3619b0" dependencies = [ "js_int", - "ruma-common", + "ruma-common 0.11.3 (git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id)", "serde", "serde_json", ] @@ -4318,8 +4317,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67b35700529224d167697ce575c71ca26c489af5774756843e335af767f6fdf" +source = "git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id#7a78869de4ec920ea770ffd2a27eafcc1b3619b0" dependencies = [ "assign", "bytes", @@ -4327,7 +4325,7 @@ dependencies = [ "js_int", "js_option", "maplit", - "ruma-common", + "ruma-common 0.11.3 (git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id)", "serde", "serde_html_form", "serde_json", @@ -4338,6 +4336,31 @@ name = "ruma-common" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3b4ec3f70ea9afeae96a6c1e5eb86ed02760d5c28a167b5d9a433cefaaf815c" +dependencies = [ + "base64 0.21.0", + "bytes", + "form_urlencoded", + "indexmap", + "js_int", + "js_option", + "konst", + "percent-encoding", + "regex", + "ruma-identifiers-validation 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ruma-macros 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", + "serde", + "serde_html_form", + "serde_json", + "thiserror", + "tracing", + "url", + "wildmatch", +] + +[[package]] +name = "ruma-common" +version = "0.11.3" +source = "git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id#7a78869de4ec920ea770ffd2a27eafcc1b3619b0" dependencies = [ "base64 0.21.0", "bytes", @@ -4355,8 +4378,8 @@ dependencies = [ "pulldown-cmark", "rand 0.8.5", "regex", - "ruma-identifiers-validation", - "ruma-macros", + "ruma-identifiers-validation 0.9.1 (git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id)", + "ruma-macros 0.11.3 (git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id)", "serde", "serde_html_form", "serde_json", @@ -4370,11 +4393,10 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d05ebbed580138816c3d564f9191e576acf96441e1faca9dcefe7092db6979" +source = "git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id#7a78869de4ec920ea770ffd2a27eafcc1b3619b0" dependencies = [ "js_int", - "ruma-common", + "ruma-common 0.11.3 (git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id)", "serde", "serde_json", ] @@ -4389,6 +4411,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ruma-identifiers-validation" +version = "0.9.1" +source = "git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id#7a78869de4ec920ea770ffd2a27eafcc1b3619b0" +dependencies = [ + "js_int", + "thiserror", +] + [[package]] name = "ruma-macros" version = "0.11.3" @@ -4399,7 +4430,22 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "ruma-identifiers-validation", + "ruma-identifiers-validation 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde", + "syn", + "toml 0.7.2", +] + +[[package]] +name = "ruma-macros" +version = "0.11.3" +source = "git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id#7a78869de4ec920ea770ffd2a27eafcc1b3619b0" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "ruma-identifiers-validation 0.9.1 (git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id)", "serde", "syn", "toml 0.7.2", diff --git a/Cargo.toml b/Cargo.toml index f08b467f9..7f026433a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,9 @@ uniffi_bindgen = "0.23.0" vodozemac = { git = "https://github.com/matrix-org/vodozemac", rev = "fb609ca1e4df5a7a818490ae86ac694119e41e71" } zeroize = "1.3.0" +[patch.crates-io] +ruma = { git = "https://github.com/Hywan/ruma", branch = "feat-sliding-sync-response-txn-id" } + # Default release profile, select with `--release` [profile.release] lto = true diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index c83957e8d..0a76b1005 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -49,6 +49,7 @@ experimental-sliding-sync = [ "matrix-sdk-base/experimental-sliding-sync", "experimental-timeline", "reqwest/gzip", + "dep:uuid", ] docsrs = [ @@ -94,6 +95,7 @@ thiserror = { workspace = true } tower = { version = "0.4.13", features = ["make"], optional = true } tracing = { workspace = true, features = ["attributes"] } url = "2.2.2" +uuid = { version = "1.3.0", optional = true } zeroize = { workspace = true } [dependencies.image] diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 779dfcdc3..4e8dac87e 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -626,6 +626,7 @@ use ruma::{ use serde::{Deserialize, Serialize}; use tracing::{debug, error, info_span, instrument, trace, warn, Instrument, Span}; use url::Url; +use uuid::Uuid; use crate::{config::RequestConfig, Client, Result}; @@ -914,8 +915,25 @@ impl SlidingSync { async fn handle_response( &self, sliding_sync_response: v4::Response, + stream_id: &str, list_generators: &mut BTreeMap, ) -> Result { + match &sliding_sync_response.txn_id { + None => { + error!(stream_id, "Sliding Sync has received an unexpected response: `txn_id` must match `stream_id`; it's missing"); + } + + Some(txn_id) if txn_id != stream_id => { + error!( + stream_id, + txn_id, + "Sliding Sync has received an unexpected response: `txn_id` must match `stream_id`; they differ" + ); + } + + _ => {} + } + // Handle and transform a Sliding Sync Response to a `SyncResponse`. // // We may not need the `sync_response` in the future (once `SyncResponse` will @@ -924,7 +942,7 @@ impl SlidingSync { // happens here. let mut sync_response = self.client.process_sliding_sync(&sliding_sync_response).await?; - debug!("sliding sync response has been processed"); + debug!("Sliding sync response has been processed"); Observable::set(&mut self.pos.write().unwrap(), Some(sliding_sync_response.pos)); Observable::set(&mut self.delta_token.write().unwrap(), sliding_sync_response.delta_token); @@ -998,6 +1016,7 @@ impl SlidingSync { async fn sync_once( &self, + stream_id: &str, list_generators: &mut BTreeMap, ) -> Result> { let mut lists = BTreeMap::new(); @@ -1038,10 +1057,11 @@ impl SlidingSync { // Prepare the request. let request = self.client.send_with_homeserver( assign!(v4::Request::new(), { - lists, pos, delta_token, + txn_id: Some(stream_id.to_owned()), timeout: Some(timeout), + lists, room_subscriptions, unsubscribe_rooms, extensions, @@ -1077,7 +1097,7 @@ impl SlidingSync { debug!("Sliding sync response received"); - let updates = self.handle_response(response, list_generators).await?; + let updates = self.handle_response(response, stream_id, list_generators).await?; debug!("Sliding sync response has been handled"); @@ -1102,7 +1122,9 @@ impl SlidingSync { list_generators }; - debug!(?self.extensions, "About to run the sync stream"); + let stream_id = Uuid::new_v4().to_string(); + + debug!(?self.extensions, stream_id, "About to run the sync stream"); let instrument_span = Span::current(); @@ -1114,7 +1136,7 @@ impl SlidingSync { debug!(?self.extensions, "Sync stream loop is running"); }); - match self.sync_once(&mut list_generators).instrument(sync_span.clone()).await { + match self.sync_once(&stream_id, &mut list_generators).instrument(sync_span.clone()).await { Ok(Some(updates)) => { self.reset_counter.store(0, Ordering::SeqCst); From 46d71acb44f586ea57b6bdd6ef25aaea361f12e4 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 11:48:14 +0100 Subject: [PATCH 139/166] chore: Update `ruma` to a specific revision. --- Cargo.lock | 82 +++++++----------------------------- Cargo.toml | 7 +-- crates/matrix-sdk/Cargo.toml | 2 +- 3 files changed, 18 insertions(+), 73 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 050061321..0fca4f785 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2920,7 +2920,7 @@ dependencies = [ "byteorder", "image 0.23.14", "qrcode", - "ruma-common 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", + "ruma-common", "thiserror", "vodozemac", ] @@ -4292,24 +4292,24 @@ dependencies = [ [[package]] name = "ruma" version = "0.8.2" -source = "git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id#7a78869de4ec920ea770ffd2a27eafcc1b3619b0" +source = "git+https://github.com/ruma/ruma?rev=2edfe5bc5f3ee88014e57230c50a5e005119344a#2edfe5bc5f3ee88014e57230c50a5e005119344a" dependencies = [ "assign", "js_int", "js_option", "ruma-appservice-api", "ruma-client-api", - "ruma-common 0.11.3 (git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id)", + "ruma-common", "ruma-federation-api", ] [[package]] name = "ruma-appservice-api" version = "0.8.1" -source = "git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id#7a78869de4ec920ea770ffd2a27eafcc1b3619b0" +source = "git+https://github.com/ruma/ruma?rev=2edfe5bc5f3ee88014e57230c50a5e005119344a#2edfe5bc5f3ee88014e57230c50a5e005119344a" dependencies = [ "js_int", - "ruma-common 0.11.3 (git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id)", + "ruma-common", "serde", "serde_json", ] @@ -4317,7 +4317,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.16.2" -source = "git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id#7a78869de4ec920ea770ffd2a27eafcc1b3619b0" +source = "git+https://github.com/ruma/ruma?rev=2edfe5bc5f3ee88014e57230c50a5e005119344a#2edfe5bc5f3ee88014e57230c50a5e005119344a" dependencies = [ "assign", "bytes", @@ -4325,7 +4325,7 @@ dependencies = [ "js_int", "js_option", "maplit", - "ruma-common 0.11.3 (git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id)", + "ruma-common", "serde", "serde_html_form", "serde_json", @@ -4334,33 +4334,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3b4ec3f70ea9afeae96a6c1e5eb86ed02760d5c28a167b5d9a433cefaaf815c" -dependencies = [ - "base64 0.21.0", - "bytes", - "form_urlencoded", - "indexmap", - "js_int", - "js_option", - "konst", - "percent-encoding", - "regex", - "ruma-identifiers-validation 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", - "ruma-macros 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", - "serde", - "serde_html_form", - "serde_json", - "thiserror", - "tracing", - "url", - "wildmatch", -] - -[[package]] -name = "ruma-common" -version = "0.11.3" -source = "git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id#7a78869de4ec920ea770ffd2a27eafcc1b3619b0" +source = "git+https://github.com/ruma/ruma?rev=2edfe5bc5f3ee88014e57230c50a5e005119344a#2edfe5bc5f3ee88014e57230c50a5e005119344a" dependencies = [ "base64 0.21.0", "bytes", @@ -4378,8 +4352,8 @@ dependencies = [ "pulldown-cmark", "rand 0.8.5", "regex", - "ruma-identifiers-validation 0.9.1 (git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id)", - "ruma-macros 0.11.3 (git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id)", + "ruma-identifiers-validation", + "ruma-macros", "serde", "serde_html_form", "serde_json", @@ -4393,10 +4367,10 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.7.1" -source = "git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id#7a78869de4ec920ea770ffd2a27eafcc1b3619b0" +source = "git+https://github.com/ruma/ruma?rev=2edfe5bc5f3ee88014e57230c50a5e005119344a#2edfe5bc5f3ee88014e57230c50a5e005119344a" dependencies = [ "js_int", - "ruma-common 0.11.3 (git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id)", + "ruma-common", "serde", "serde_json", ] @@ -4404,17 +4378,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebefdab34311af44d07cd2cd91c36cfe6a8c770647c6b00f6ab47f1186b2bb72" -dependencies = [ - "js_int", - "thiserror", -] - -[[package]] -name = "ruma-identifiers-validation" -version = "0.9.1" -source = "git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id#7a78869de4ec920ea770ffd2a27eafcc1b3619b0" +source = "git+https://github.com/ruma/ruma?rev=2edfe5bc5f3ee88014e57230c50a5e005119344a#2edfe5bc5f3ee88014e57230c50a5e005119344a" dependencies = [ "js_int", "thiserror", @@ -4423,29 +4387,13 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e883799456b6213da90fe065d4234f282b89afe161af3e5fcc854e44e8f582" +source = "git+https://github.com/ruma/ruma?rev=2edfe5bc5f3ee88014e57230c50a5e005119344a#2edfe5bc5f3ee88014e57230c50a5e005119344a" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "ruma-identifiers-validation 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", - "serde", - "syn", - "toml 0.7.2", -] - -[[package]] -name = "ruma-macros" -version = "0.11.3" -source = "git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id#7a78869de4ec920ea770ffd2a27eafcc1b3619b0" -dependencies = [ - "once_cell", - "proc-macro-crate", - "proc-macro2", - "quote", - "ruma-identifiers-validation 0.9.1 (git+https://github.com/Hywan/ruma?branch=feat-sliding-sync-response-txn-id)", + "ruma-identifiers-validation", "serde", "syn", "toml 0.7.2", diff --git a/Cargo.toml b/Cargo.toml index 7f026433a..d1a7d1b37 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,8 +30,8 @@ dashmap = "5.2.0" eyeball = "0.1.4" eyeball-im = "0.1.0" http = "0.2.6" -ruma = { version = "0.8.0", features = ["client-api-c"] } -ruma-common = "0.11.2" +ruma = { git = "https://github.com/ruma/ruma", rev = "2edfe5bc5f3ee88014e57230c50a5e005119344a", features = ["client-api-c"] } +ruma-common = { git = "https://github.com/ruma/ruma", rev = "2edfe5bc5f3ee88014e57230c50a5e005119344a" } once_cell = "1.16.0" serde = "1.0.151" serde_html_form = "0.2.0" @@ -43,9 +43,6 @@ uniffi_bindgen = "0.23.0" vodozemac = { git = "https://github.com/matrix-org/vodozemac", rev = "fb609ca1e4df5a7a818490ae86ac694119e41e71" } zeroize = "1.3.0" -[patch.crates-io] -ruma = { git = "https://github.com/Hywan/ruma", branch = "feat-sliding-sync-response-txn-id" } - # Default release profile, select with `--release` [profile.release] lto = true diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 0a76b1005..54bbcc3e3 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -87,7 +87,7 @@ mime = "0.3.16" pin-project-lite = "0.2.9" rand = { version = "0.8.5", optional = true } reqwest = { version = "0.11.10", default_features = false } -ruma = { workspace = true, features = ["compat", "rand", "unstable-msc2448", "unstable-msc2965"] } +ruma = { workspace = true, features = ["rand", "unstable-msc2448", "unstable-msc2965"] } serde = { workspace = true } serde_html_form = { workspace = true } serde_json = { workspace = true } From f8ce49cc49d85f0366f2e0d32466afade04b0505 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 8 Mar 2023 08:51:21 +0100 Subject: [PATCH 140/166] chore: Add documentation. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 4e8dac87e..0f3bee748 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -1059,6 +1059,8 @@ impl SlidingSync { assign!(v4::Request::new(), { pos, delta_token, + // We want to track whether the incoming response maps to this + // request. We use the (optional) `txn_id` field for that. txn_id: Some(stream_id.to_owned()), timeout: Some(timeout), lists, From 8f6f20bbe7848724eb17318a688f3d4c8184d637 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 16:50:36 +0100 Subject: [PATCH 141/166] fix(bindings): `SlidingSync.sync` returns an immediately cancellable `TaskHandle`. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this patch, `SlidingSync::sync` was returning a callback-based `TaskHandle`. It was waiting on the “stream loop” to finish (since it's a long- poll, it means waiting 2*30s, cf. our code), before checking an atomic flag has some value to decide whether it was time to leave the loop or not. So when the user is cancelling this `TaskHandle`, a response (if any) was always handled. But in the meantime, it was possible to start a new `sync`, and it seems like it creates bugs. After this patch, `SlidingSync::sync` now returns a handle-based `TaskHandle`. It means that cancelling it will cancel the “stream loop” immediately. If no response was in-flight from the server, that's perfect, no problem. If a response was in-flight, the inner `pos` of the `SlidingSync` instance won't be updated as the response won't be handled. So the server will re-send the same response with the next sync request. I guess it's better this way. Thoughts? --- bindings/matrix-sdk-ffi/src/sliding_sync.rs | 30 ++++++++------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/sliding_sync.rs b/bindings/matrix-sdk-ffi/src/sliding_sync.rs index a8c8ad0fe..986398b07 100644 --- a/bindings/matrix-sdk-ffi/src/sliding_sync.rs +++ b/bindings/matrix-sdk-ffi/src/sliding_sync.rs @@ -723,43 +723,35 @@ impl SlidingSync { let inner = self.inner.clone(); let client = self.client.clone(); let observer = self.observer.clone(); - let stop_loop = Arc::new(AtomicBool::new(false)); - let remote_stopper = stop_loop.clone(); - let stoppable = Arc::new(TaskHandle::with_callback(Box::new(move || { - remote_stopper.store(true, Ordering::Relaxed); - }))); - - RUNTIME.spawn(async move { + Arc::new(TaskHandle::with_handle(RUNTIME.spawn(async move { let stream = inner.stream(); pin_mut!(stream); + loop { - let update = match stream.next().await { - Some(Ok(u)) => u, - Some(Err(e)) => { - if client.process_sync_error(e) == LoopCtrl::Break { + let update_summary = match stream.next().await { + Some(Ok(update_summary)) => update_summary, + + Some(Err(error)) => { + if client.process_sync_error(error) == LoopCtrl::Break { warn!("loop was stopped by client error processing"); break; } else { continue; } } + None => { warn!("Inner streaming loop ended unexpectedly"); break; } }; + if let Some(ref observer) = *observer.read().unwrap() { - observer.did_receive_sync_update(update.into()); - } - if stop_loop.load(Ordering::Relaxed) { - trace!("stopped sync loop after cancellation"); - break; + observer.did_receive_sync_update(update_summary.into()); } } - }); - - stoppable + }))) } } From 07f6a3b3450c8c52c8ecb56dff8c3197b726444c Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 1 Mar 2023 16:56:32 +0100 Subject: [PATCH 142/166] chore: Remove warnings. --- bindings/matrix-sdk-ffi/src/sliding_sync.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/sliding_sync.rs b/bindings/matrix-sdk-ffi/src/sliding_sync.rs index 986398b07..78e49b1b8 100644 --- a/bindings/matrix-sdk-ffi/src/sliding_sync.rs +++ b/bindings/matrix-sdk-ffi/src/sliding_sync.rs @@ -1,7 +1,4 @@ -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, RwLock, -}; +use std::sync::{Arc, RwLock}; use anyhow::Context; use eyeball::Observable; @@ -20,7 +17,7 @@ pub use matrix_sdk::{ SlidingSyncBuilder as MatrixSlidingSyncBuilder, SlidingSyncMode, SlidingSyncState, }; use tokio::{sync::broadcast::error::RecvError, task::JoinHandle}; -use tracing::{debug, error, trace, warn}; +use tracing::{debug, error, warn}; use url::Url; use super::{Client, Room, RUNTIME}; @@ -41,6 +38,7 @@ impl TaskHandle { Self { handle: Some(handle), callback: Default::default() } } + #[allow(dead_code)] fn with_callback(callback: TaskHandleCallback) -> Self { Self { handle: Default::default(), callback: RwLock::new(Some(callback)) } } From 7369010964127f258fe6537fe1e328d24e15977b Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 8 Mar 2023 09:41:29 +0100 Subject: [PATCH 143/166] feat(sdk): Make `SlidingSync::handle_response` _sync_, no more _async_. The idea is to group all async and await points in `sync_once`. It's easier to think about it. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 62 +++++++++++------------ 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 0f3bee748..d9bc7b8cf 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -613,6 +613,7 @@ pub use error::*; use eyeball::Observable; use futures_core::stream::Stream; pub use list::*; +use matrix_sdk_base::sync::SyncResponse; pub use room::*; use ruma::{ api::client::{ @@ -912,38 +913,12 @@ impl SlidingSync { /// Handle the HTTP response. #[instrument(skip_all, fields(lists = list_generators.len()))] - async fn handle_response( + fn handle_response( &self, sliding_sync_response: v4::Response, - stream_id: &str, + mut sync_response: SyncResponse, list_generators: &mut BTreeMap, ) -> Result { - match &sliding_sync_response.txn_id { - None => { - error!(stream_id, "Sliding Sync has received an unexpected response: `txn_id` must match `stream_id`; it's missing"); - } - - Some(txn_id) if txn_id != stream_id => { - error!( - stream_id, - txn_id, - "Sliding Sync has received an unexpected response: `txn_id` must match `stream_id`; they differ" - ); - } - - _ => {} - } - - // Handle and transform a Sliding Sync Response to a `SyncResponse`. - // - // We may not need the `sync_response` in the future (once `SyncResponse` will - // move to Sliding Sync, i.e. to `v4::Response`), but processing the - // `sliding_sync_response` is vital, so it must be done somewhere; for now it - // happens here. - let mut sync_response = self.client.process_sliding_sync(&sliding_sync_response).await?; - - debug!("Sliding sync response has been processed"); - Observable::set(&mut self.pos.write().unwrap(), Some(sliding_sync_response.pos)); Observable::set(&mut self.delta_token.write().unwrap(), sliding_sync_response.delta_token); @@ -1009,8 +984,6 @@ impl SlidingSync { UpdateSummary { lists: updated_lists, rooms } }; - self.cache_to_storage().await?; - Ok(update_summary) } @@ -1099,7 +1072,34 @@ impl SlidingSync { debug!("Sliding sync response received"); - let updates = self.handle_response(response, stream_id, list_generators).await?; + match &response.txn_id { + None => { + error!(stream_id, "Sliding Sync has received an unexpected response: `txn_id` must match `stream_id`; it's missing"); + } + + Some(txn_id) if txn_id != stream_id => { + error!( + stream_id, + txn_id, + "Sliding Sync has received an unexpected response: `txn_id` must match `stream_id`; they differ" + ); + } + + _ => {} + } + + debug!("Sliding sync response has been processed"); + + // Handle and transform a Sliding Sync Response to a `SyncResponse`. + // + // We may not need the `sync_response` in the future (once `SyncResponse` will + // move to Sliding Sync, i.e. to `v4::Response`), but processing the + // `sliding_sync_response` is vital, so it must be done somewhere; for now it + // happens here. + let sync_response = self.client.process_sliding_sync(&response).await?; + let updates = self.handle_response(response, sync_response, list_generators)?; + + self.cache_to_storage().await?; debug!("Sliding sync response has been handled"); From 7af2bea2fc56bce7123e4ce0bd3a87ce23271605 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 11 Jan 2023 12:23:18 +0100 Subject: [PATCH 144/166] Upgrade UniFFI --- Cargo.lock | 27 +++++++------------ Cargo.toml | 4 +-- .../src/backup_recovery_key.rs | 5 ++-- bindings/matrix-sdk-crypto-ffi/src/error.rs | 3 +-- bindings/matrix-sdk-crypto-ffi/src/lib.rs | 1 + .../matrix-sdk-crypto-ffi/src/responses.rs | 2 +- bindings/matrix-sdk-ffi/src/room.rs | 2 +- bindings/matrix-sdk-ffi/src/sliding_sync.rs | 3 +-- 8 files changed, 18 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0fca4f785..5a86e9ded 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5652,8 +5652,7 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "uniffi" version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f71cc01459bc34cfe43fabf32b39f1228709bc6db1b3a664a92940af3d062376" +source = "git+https://github.com/mozilla/uniffi-rs?rev=58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4#58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4" dependencies = [ "anyhow", "camino", @@ -5674,8 +5673,7 @@ dependencies = [ [[package]] name = "uniffi_bindgen" version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbbba5103051c18f10b22f80a74439ddf7100273f217a547005d2735b2498994" +source = "git+https://github.com/mozilla/uniffi-rs?rev=58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4#58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4" dependencies = [ "anyhow", "askama", @@ -5698,8 +5696,7 @@ dependencies = [ [[package]] name = "uniffi_build" version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee1a28368ff3d83717e3d3e2e15a66269c43488c3f036914131bb68892f29fb" +source = "git+https://github.com/mozilla/uniffi-rs?rev=58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4#58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4" dependencies = [ "anyhow", "camino", @@ -5709,8 +5706,7 @@ dependencies = [ [[package]] name = "uniffi_checksum_derive" version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03de61393a42b4ad4984a3763c0600594ac3e57e5aaa1d05cede933958987c03" +source = "git+https://github.com/mozilla/uniffi-rs?rev=58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4#58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4" dependencies = [ "quote", "syn", @@ -5719,8 +5715,7 @@ dependencies = [ [[package]] name = "uniffi_core" version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2b4852d638d74ca2d70e450475efb6d91fe6d54a7cd8d6bd80ad2ee6cd7daa" +source = "git+https://github.com/mozilla/uniffi-rs?rev=58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4#58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4" dependencies = [ "anyhow", "bytes", @@ -5735,8 +5730,7 @@ dependencies = [ [[package]] name = "uniffi_macros" version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa03394de21e759e0022f1ea8d992d2e39290d735b9ed52b1f74b20a684f794e" +source = "git+https://github.com/mozilla/uniffi-rs?rev=58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4#58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4" dependencies = [ "bincode", "camino", @@ -5754,8 +5748,7 @@ dependencies = [ [[package]] name = "uniffi_meta" version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fdab2c436aed7a6391bec64204ec33948bfed9b11b303235740771f85c4ea6" +source = "git+https://github.com/mozilla/uniffi-rs?rev=58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4#58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4" dependencies = [ "serde", "siphasher", @@ -5765,8 +5758,7 @@ dependencies = [ [[package]] name = "uniffi_testing" version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92b0570953ec41d97ce23e3b92161ac18231670a1f97523258a6d2ab76d7f76c" +source = "git+https://github.com/mozilla/uniffi-rs?rev=58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4#58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4" dependencies = [ "anyhow", "camino", @@ -6050,8 +6042,7 @@ dependencies = [ [[package]] name = "weedle2" version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e79c5206e1f43a2306fd64bdb95025ee4228960f2e6c5a8b173f3caaf807741" +source = "git+https://github.com/mozilla/uniffi-rs?rev=58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4#58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4" dependencies = [ "nom", ] diff --git a/Cargo.toml b/Cargo.toml index d1a7d1b37..596ae59a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,8 +38,8 @@ serde_html_form = "0.2.0" serde_json = "1.0.91" thiserror = "1.0.38" tracing = { version = "0.1.36", default-features = false, features = ["std"] } -uniffi = "0.23.0" -uniffi_bindgen = "0.23.0" +uniffi = { git = "https://github.com/mozilla/uniffi-rs", rev = "58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4" } +uniffi_bindgen = { git = "https://github.com/mozilla/uniffi-rs", rev = "58758341b72e9e8ff51ecd57a3eb22d6cc41a4b4" } vodozemac = { git = "https://github.com/matrix-org/vodozemac", rev = "fb609ca1e4df5a7a818490ae86ac694119e41e71" } zeroize = "1.3.0" diff --git a/bindings/matrix-sdk-crypto-ffi/src/backup_recovery_key.rs b/bindings/matrix-sdk-crypto-ffi/src/backup_recovery_key.rs index 5cf6e3974..110406fb2 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/backup_recovery_key.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/backup_recovery_key.rs @@ -27,8 +27,7 @@ pub enum PkDecryptionError { } /// Error type for the decoding and storing of the backup key. -#[derive(Debug, Error, uniffi::Error)] -#[uniffi(flat_error)] +#[derive(Debug, Error)] pub enum DecodeError { /// An error happened while decoding the recovery key. #[error(transparent)] @@ -41,7 +40,7 @@ pub enum DecodeError { /// Struct containing info about the way the backup key got derived from a /// passphrase. -#[derive(Debug, Clone, uniffi::Record)] +#[derive(Debug, Clone)] pub struct PassphraseInfo { /// The salt that was used during key derivation. pub private_key_salt: String, diff --git a/bindings/matrix-sdk-crypto-ffi/src/error.rs b/bindings/matrix-sdk-crypto-ffi/src/error.rs index b4b0a41cd..6ae377a01 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/error.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/error.rs @@ -39,8 +39,7 @@ pub enum SignatureError { UnknownUserIdentity(String), } -#[derive(Debug, thiserror::Error, uniffi::Error)] -#[uniffi(flat_error)] +#[derive(Debug, thiserror::Error)] pub enum CryptoStoreError { #[error("Failed to open the store")] OpenStore(#[from] OpenStoreError), diff --git a/bindings/matrix-sdk-crypto-ffi/src/lib.rs b/bindings/matrix-sdk-crypto-ffi/src/lib.rs index 3925fdfc2..187e04d68 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/lib.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/lib.rs @@ -47,6 +47,7 @@ use ruma::{ }; use serde::{Deserialize, Serialize}; use tokio::runtime::Runtime; +use uniffi_api::*; pub use users::UserIdentity; pub use verification::{ CancelInfo, ConfirmVerificationResult, QrCode, QrCodeListener, QrCodeState, diff --git a/bindings/matrix-sdk-crypto-ffi/src/responses.rs b/bindings/matrix-sdk-crypto-ffi/src/responses.rs index 24a0c2c14..569def11a 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/responses.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/responses.rs @@ -112,7 +112,7 @@ impl From for OutgoingVerificationRequest { } } -#[derive(Debug, uniffi::Enum)] +#[derive(Debug)] pub enum Request { ToDevice { request_id: String, event_type: String, body: String }, KeysUpload { request_id: String, body: String }, diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index c9d700f10..f011e63aa 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -39,7 +39,7 @@ pub struct Room { timeline: TimelineLock, } -#[derive(Clone, uniffi::Enum)] +#[derive(Clone)] pub enum MembershipState { /// The user is banned. Ban, diff --git a/bindings/matrix-sdk-ffi/src/sliding_sync.rs b/bindings/matrix-sdk-ffi/src/sliding_sync.rs index a8c8ad0fe..d69a62466 100644 --- a/bindings/matrix-sdk-ffi/src/sliding_sync.rs +++ b/bindings/matrix-sdk-ffi/src/sliding_sync.rs @@ -254,7 +254,6 @@ pub struct UpdateSummary { pub rooms: Vec, } -#[derive(uniffi::Record)] pub struct RequiredState { pub key: String, pub value: String, @@ -324,7 +323,7 @@ impl From> for SlidingSyncListRoomsListDiff { } } -#[derive(Clone, Debug, uniffi::Enum)] +#[derive(Clone, Debug)] pub enum RoomListEntry { Empty, Invalidated { room_id: String }, From 9fe880d05dada94ccd4a2a85eb4f9eaefde10597 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 7 Mar 2023 15:19:28 +0100 Subject: [PATCH 145/166] Clean up formatting of olm.udl --- bindings/matrix-sdk-crypto-ffi/src/olm.udl | 35 +++++++++++----------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/olm.udl b/bindings/matrix-sdk-crypto-ffi/src/olm.udl index 7be41032f..5d16670ef 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/olm.udl +++ b/bindings/matrix-sdk-crypto-ffi/src/olm.udl @@ -14,12 +14,11 @@ namespace matrix_sdk_crypto_ffi { string? passphrase, ProgressListener progress_listener ); - }; [Error] interface MigrationError { - Generic(string error_message); + Generic(string error_message); }; callback interface Logger { @@ -168,12 +167,12 @@ interface Sas { [Enum] interface SasState { - Started(); - Accepted(); - KeysExchanged(sequence? emojis, sequence decimals); - Confirmed(); - Done(); - Cancelled(CancelInfo cancel_info); + Started(); + Accepted(); + KeysExchanged(sequence? emojis, sequence decimals); + Confirmed(); + Done(); + Cancelled(CancelInfo cancel_info); }; callback interface SasListener { @@ -208,12 +207,12 @@ interface QrCode { [Enum] interface QrCodeState { - Started(); - Scanned(); - Confirmed(); - Reciprocated(); - Done(); - Cancelled(CancelInfo cancel_info); + Started(); + Scanned(); + Confirmed(); + Reciprocated(); + Done(); + Cancelled(CancelInfo cancel_info); }; callback interface QrCodeListener { @@ -252,10 +251,10 @@ interface VerificationRequest { [Enum] interface VerificationRequestState { - Requested(); - Ready(sequence their_methods, sequence our_methods); - Done(); - Cancelled(CancelInfo cancel_info); + Requested(); + Ready(sequence their_methods, sequence our_methods); + Done(); + Cancelled(CancelInfo cancel_info); }; callback interface VerificationRequestListener { From fb23cbff97ba8bf534fc52d7d14a21f204d8d01e Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 7 Mar 2023 16:53:34 +0100 Subject: [PATCH 146/166] crypto-ffi: Use UniFFI proc-macros for more OlmMachine methods --- bindings/matrix-sdk-crypto-ffi/src/lib.rs | 12 ++- bindings/matrix-sdk-crypto-ffi/src/machine.rs | 99 ++++++++++--------- bindings/matrix-sdk-crypto-ffi/src/olm.udl | 67 ------------- 3 files changed, 61 insertions(+), 117 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/lib.rs b/bindings/matrix-sdk-crypto-ffi/src/lib.rs index 187e04d68..29ff39c60 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/lib.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/lib.rs @@ -717,10 +717,14 @@ mod uniffi_types { backup_recovery_key::{ BackupRecoveryKey, DecodeError, MegolmV1BackupKey, PassphraseInfo, PkDecryptionError, }, - error::CryptoStoreError, - machine::OlmMachine, - responses::Request, - BackupKeys, CrossSigningStatus, RoomKeyCounts, + error::{CryptoStoreError, DecryptionError, SecretImportError}, + machine::{KeyRequestPair, OlmMachine}, + responses::{BootstrapCrossSigningResult, DeviceLists, Request}, + verification::{ + RequestVerificationResult, StartSasResult, Verification, VerificationRequest, + }, + BackupKeys, CrossSigningKeyExport, CrossSigningStatus, DecryptedEvent, EncryptionSettings, + RoomKeyCounts, }; } diff --git a/bindings/matrix-sdk-crypto-ffi/src/machine.rs b/bindings/matrix-sdk-crypto-ffi/src/machine.rs index b80d906ed..814bf021a 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/machine.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/machine.rs @@ -450,7 +450,10 @@ impl OlmMachine { Ok(()) } +} +#[uniffi::export] +impl OlmMachine { /// Let the state machine know about E2EE related sync changes that we /// received from the server. /// @@ -468,12 +471,12 @@ impl OlmMachine { /// * `key_counts` - The map of uploaded one-time key types and counts. pub fn receive_sync_changes( &self, - events: &str, + events: String, device_changes: DeviceLists, key_counts: HashMap, unused_fallback_keys: Option>, ) -> Result { - let to_device: 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() @@ -528,8 +531,8 @@ impl OlmMachine { /// /// A user can be marked for tracking using the /// [`OlmMachine::update_tracked_users()`] method. - pub fn is_user_tracked(&self, user_id: &str) -> Result { - let user_id = parse_user_id(user_id)?; + pub fn is_user_tracked(&self, user_id: String) -> Result { + let user_id = parse_user_id(&user_id)?; Ok(self.runtime.block_on(self.inner.tracked_users())?.contains(&user_id)) } @@ -581,7 +584,7 @@ impl OlmMachine { /// * `settings` - The settings that should be used for the room key. pub fn share_room_key( &self, - room_id: &str, + room_id: String, users: Vec, settings: EncryptionSettings, ) -> Result, CryptoStoreError> { @@ -633,16 +636,16 @@ impl OlmMachine { /// * `content` - The serialized content of the event. pub fn encrypt( &self, - room_id: &str, - event_type: &str, - content: &str, + room_id: String, + event_type: String, + content: String, ) -> Result { let room_id = RoomId::parse(room_id)?; - let content: Value = serde_json::from_str(content)?; + let content: Value = serde_json::from_str(&content)?; let encrypted_content = self .runtime - .block_on(self.inner.encrypt_room_event_raw(&room_id, content, event_type)) + .block_on(self.inner.encrypt_room_event_raw(&room_id, content, &event_type)) .expect("Encrypting an event produced an error"); Ok(serde_json::to_string(&encrypted_content)?) @@ -657,8 +660,8 @@ impl OlmMachine { /// * `room_id` - The unique id of the room where the event was sent to. pub fn decrypt_room_event( &self, - event: &str, - room_id: &str, + event: String, + room_id: String, handle_verification_events: bool, ) -> Result { // Element Android wants only the content and the type and will create a @@ -672,7 +675,7 @@ impl OlmMachine { content: &'a RawValue, } - let event: Raw<_> = serde_json::from_str(event)?; + let event: Raw<_> = serde_json::from_str(&event)?; let room_id = RoomId::parse(room_id)?; let decrypted = self.runtime.block_on(self.inner.decrypt_room_event(&event, &room_id))?; @@ -727,10 +730,10 @@ impl OlmMachine { /// * `room_id` - The id of the room the event was sent to. pub fn request_room_key( &self, - event: &str, - room_id: &str, + event: String, + room_id: String, ) -> Result { - let event: Raw<_> = serde_json::from_str(event)?; + let event: Raw<_> = serde_json::from_str(&event)?; let room_id = RoomId::parse(room_id)?; let (cancel, request) = @@ -753,17 +756,19 @@ impl OlmMachine { /// passphrase into an key. pub fn export_room_keys( &self, - passphrase: &str, + passphrase: String, rounds: i32, ) -> Result { let keys = self.runtime.block_on(self.inner.export_room_keys(|_| true))?; - let encrypted = encrypt_room_key_export(&keys, passphrase, rounds as u32) + let encrypted = encrypt_room_key_export(&keys, &passphrase, rounds as u32) .map_err(CryptoStoreError::Serialization)?; Ok(encrypted) } +} +impl OlmMachine { fn import_room_keys_helper( &self, keys: Vec, @@ -838,10 +843,13 @@ impl OlmMachine { self.import_room_keys_helper(keys, true, progress_listener) } +} +#[uniffi::export] +impl OlmMachine { /// Discard the currently active room key for the given room if there is /// one. - pub fn discard_room_key(&self, room_id: &str) -> Result<(), CryptoStoreError> { + pub fn discard_room_key(&self, room_id: String) -> Result<(), CryptoStoreError> { let room_id = RoomId::parse(room_id)?; self.runtime.block_on(self.inner.invalidate_group_session(&room_id))?; @@ -857,8 +865,8 @@ impl OlmMachine { /// **Note**: This has been deprecated. pub fn receive_unencrypted_verification_event( &self, - event: &str, - room_id: &str, + event: String, + room_id: String, ) -> Result<(), CryptoStoreError> { self.receive_verification_event(event, room_id) } @@ -869,11 +877,11 @@ impl OlmMachine { /// in rooms to the `OlmMachine`. The event should be in the decrypted form. pub fn receive_verification_event( &self, - event: &str, - room_id: &str, + event: String, + room_id: String, ) -> Result<(), CryptoStoreError> { let room_id = RoomId::parse(room_id)?; - let event: AnySyncMessageLikeEvent = serde_json::from_str(event)?; + let event: AnySyncMessageLikeEvent = serde_json::from_str(&event)?; let event = event.into_full_event(room_id); @@ -888,7 +896,7 @@ impl OlmMachine { /// /// * `user_id` - The ID of the user for which we would like to fetch the /// verification requests. - pub fn get_verification_requests(&self, user_id: &str) -> Vec> { + pub fn get_verification_requests(&self, user_id: String) -> Vec> { let Ok(user_id) = UserId::parse(user_id) else { return vec![]; }; @@ -913,8 +921,8 @@ impl OlmMachine { /// * `flow_id` - The ID that uniquely identifies the verification flow. pub fn get_verification_request( &self, - user_id: &str, - flow_id: &str, + user_id: String, + flow_id: String, ) -> Option> { let user_id = UserId::parse(user_id).ok()?; @@ -934,10 +942,10 @@ impl OlmMachine { /// support. pub fn verification_request_content( &self, - user_id: &str, + user_id: String, methods: Vec, ) -> Result, CryptoStoreError> { - let user_id = parse_user_id(user_id)?; + let user_id = parse_user_id(&user_id)?; let identity = self.runtime.block_on(self.inner.get_identity(&user_id, None))?; @@ -974,12 +982,12 @@ impl OlmMachine { /// [verification_request_content()]: #method.verification_request_content pub fn request_verification( &self, - user_id: &str, - room_id: &str, - event_id: &str, + user_id: String, + room_id: String, + event_id: String, methods: Vec, ) -> Result>, CryptoStoreError> { - let user_id = parse_user_id(user_id)?; + let user_id = parse_user_id(&user_id)?; let event_id = EventId::parse(event_id)?; let room_id = RoomId::parse(room_id)?; @@ -1016,17 +1024,18 @@ impl OlmMachine { /// supported in the `m.key.verification.request` event. pub fn request_verification_with_device( &self, - user_id: &str, - device_id: &str, + user_id: String, + device_id: String, methods: Vec, ) -> Result, CryptoStoreError> { - let user_id = parse_user_id(user_id)?; + let user_id = parse_user_id(&user_id)?; + let device_id = device_id.as_str().into(); let methods = methods.into_iter().map(VerificationMethod::from).collect(); Ok( if let Some(device) = - self.runtime.block_on(self.inner.get_device(&user_id, device_id.into(), None))? + self.runtime.block_on(self.inner.get_device(&user_id, device_id, None))? { let (verification, request) = self.runtime.block_on(device.request_verification_with_methods(methods)); @@ -1085,11 +1094,11 @@ impl OlmMachine { /// verification. /// /// * `flow_id` - The ID that uniquely identifies the verification flow. - pub fn get_verification(&self, user_id: &str, flow_id: &str) -> Option> { + pub fn get_verification(&self, user_id: String, flow_id: String) -> Option> { let user_id = UserId::parse(user_id).ok()?; self.inner - .get_verification(&user_id, flow_id) + .get_verification(&user_id, &flow_id) .map(|v| Verification { inner: v, runtime: self.runtime.handle().to_owned() }.into()) } @@ -1109,14 +1118,15 @@ impl OlmMachine { /// [request_verification_with_device()]: #method.request_verification_with_device pub fn start_sas_with_device( &self, - user_id: &str, - device_id: &str, + user_id: String, + device_id: String, ) -> Result, CryptoStoreError> { - let user_id = parse_user_id(user_id)?; + let user_id = parse_user_id(&user_id)?; + let device_id = device_id.as_str().into(); Ok( if let Some(device) = - self.runtime.block_on(self.inner.get_device(&user_id, device_id.into(), None))? + self.runtime.block_on(self.inner.get_device(&user_id, device_id, None))? { let (sas, request) = self.runtime.block_on(device.start_verification())?; @@ -1159,10 +1169,7 @@ impl OlmMachine { Ok(()) } -} -#[uniffi::export] -impl OlmMachine { /// Activate the given backup key to be used with the given backup version. /// /// **Warning**: The caller needs to make sure that the given `BackupKey` is diff --git a/bindings/matrix-sdk-crypto-ffi/src/olm.udl b/bindings/matrix-sdk-crypto-ffi/src/olm.udl index 5d16670ef..48d1d6bf2 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/olm.udl +++ b/bindings/matrix-sdk-crypto-ffi/src/olm.udl @@ -354,11 +354,6 @@ interface OlmMachine { string? passphrase ); - [Throws=CryptoStoreError] - string receive_sync_changes([ByRef] string events, - DeviceLists device_changes, - record key_counts, - sequence? unused_fallback_keys); [Throws=CryptoStoreError] sequence outgoing_requests(); [Throws=CryptoStoreError] @@ -368,11 +363,6 @@ interface OlmMachine { [ByRef] string response ); - [Throws=DecryptionError] - DecryptedEvent decrypt_room_event([ByRef] string event, [ByRef] string room_id, boolean handle_verificaton_events); - [Throws=CryptoStoreError] - string encrypt([ByRef] string room_id, [ByRef] string event_type, [ByRef] string content); - [Throws=CryptoStoreError] UserIdentity? get_identity([ByRef] string user_id, u32 timeout); [Throws=SignatureError] @@ -386,56 +376,6 @@ interface OlmMachine { [Throws=CryptoStoreError] sequence get_user_devices([ByRef] string user_id, u32 timeout); - [Throws=CryptoStoreError] - boolean is_user_tracked([ByRef] string user_id); - [Throws=CryptoStoreError] - void update_tracked_users(sequence users); - [Throws=CryptoStoreError] - Request? get_missing_sessions(sequence users); - [Throws=CryptoStoreError] - 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); - [Throws=CryptoStoreError] - void receive_verification_event([ByRef] string event, [ByRef] string room_id); - sequence get_verification_requests([ByRef] string user_id); - VerificationRequest? get_verification_request([ByRef] string user_id, [ByRef] string flow_id); - Verification? get_verification([ByRef] string user_id, [ByRef] string flow_id); - - [Throws=CryptoStoreError] - VerificationRequest? request_verification( - [ByRef] string user_id, - [ByRef] string room_id, - [ByRef] string event_id, - sequence methods - ); - [Throws=CryptoStoreError] - string? verification_request_content( - [ByRef] string user_id, - sequence methods - ); - [Throws=CryptoStoreError] - RequestVerificationResult? request_self_verification(sequence methods); - [Throws=CryptoStoreError] - RequestVerificationResult? request_verification_with_device( - [ByRef] string user_id, - [ByRef] string device_id, - sequence methods - ); - - [Throws=CryptoStoreError] - StartSasResult? start_sas_with_device([ByRef] string user_id, [ByRef] string device_id); - - [Throws=DecryptionError] - KeyRequestPair request_room_key([ByRef] string event, [ByRef] string room_id); - - [Throws=CryptoStoreError] - string export_room_keys([ByRef] string passphrase, i32 rounds); [Throws=KeyImportError] KeysImportResult import_room_keys( [ByRef] string keys, @@ -447,14 +387,7 @@ interface OlmMachine { [ByRef] string keys, ProgressListener progress_listener ); - [Throws=CryptoStoreError] - void discard_room_key([ByRef] string room_id); - [Throws=CryptoStoreError] - BootstrapCrossSigningResult bootstrap_cross_signing(); - CrossSigningKeyExport? export_cross_signing_keys(); - [Throws=SecretImportError] - void import_cross_signing_keys(CrossSigningKeyExport export); [Throws=CryptoStoreError] boolean is_identity_verified([ByRef] string user_id); From 5c2915bb6a7f93d5fcfeefe17b27bfc9cef03c29 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 7 Mar 2023 16:57:04 +0100 Subject: [PATCH 147/166] crypto-ffi: Use uniffi derives for types no longer referenced in UDL --- bindings/matrix-sdk-crypto-ffi/src/lib.rs | 1 + bindings/matrix-sdk-crypto-ffi/src/olm.udl | 13 ------------- bindings/matrix-sdk-crypto-ffi/src/responses.rs | 1 + 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/lib.rs b/bindings/matrix-sdk-crypto-ffi/src/lib.rs index 29ff39c60..3903289ac 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/lib.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/lib.rs @@ -553,6 +553,7 @@ impl From for RustHistoryVisibility { /// 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. +#[derive(uniffi::Record)] pub struct EncryptionSettings { /// The encryption algorithm that should be used in the room. pub algorithm: EventEncryptionAlgorithm, diff --git a/bindings/matrix-sdk-crypto-ffi/src/olm.udl b/bindings/matrix-sdk-crypto-ffi/src/olm.udl index 48d1d6bf2..62aa0ffbc 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/olm.udl +++ b/bindings/matrix-sdk-crypto-ffi/src/olm.udl @@ -71,11 +71,6 @@ enum DecryptionError { "Store", }; -dictionary DeviceLists { - sequence changed; - sequence left; -}; - dictionary KeysImportResult { i64 imported; i64 total; @@ -337,14 +332,6 @@ enum HistoryVisibility { "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( diff --git a/bindings/matrix-sdk-crypto-ffi/src/responses.rs b/bindings/matrix-sdk-crypto-ffi/src/responses.rs index 569def11a..935c3e2e1 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/responses.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/responses.rs @@ -231,6 +231,7 @@ pub enum RequestType { RoomMessage, } +#[derive(uniffi::Record)] pub struct DeviceLists { pub changed: Vec, pub left: Vec, From 66d4ced90fc4739bbc318135a997d2dbdacc79db Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 8 Mar 2023 12:01:57 +0100 Subject: [PATCH 148/166] chore: Add some inline documentation. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index d9bc7b8cf..67aa4c521 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -1070,6 +1070,15 @@ impl SlidingSync { #[cfg(not(feature = "e2e-encryption"))] let response = request.await?; + // At this point, the request has been sent, and a response has been received. + // + // We must ensure the handling of the response cannot be stopped/ + // cancelled. It must be done entirely, otherwise we can have + // corrupted/incomplete states for Sliding Sync and other parts of + // the code. + // + // That's why we are running the handling of the response in a blocking + // mode since it cannot be cancelled abruptly. debug!("Sliding sync response received"); match &response.txn_id { @@ -1088,8 +1097,6 @@ impl SlidingSync { _ => {} } - debug!("Sliding sync response has been processed"); - // Handle and transform a Sliding Sync Response to a `SyncResponse`. // // We may not need the `sync_response` in the future (once `SyncResponse` will @@ -1097,6 +1104,9 @@ impl SlidingSync { // `sliding_sync_response` is vital, so it must be done somewhere; for now it // happens here. let sync_response = self.client.process_sliding_sync(&response).await?; + + debug!("Sliding sync response has been processed"); + let updates = self.handle_response(response, sync_response, list_generators)?; self.cache_to_storage().await?; From 832146b43dd17fede7ee5e2c53beec33aa294263 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 8 Mar 2023 12:12:58 +0100 Subject: [PATCH 149/166] feat(sdk): Create `SlidingSyncInner`. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move all `SlidingSync`'s fields into a new `SlidingSyncInner` type, and then update `SlidingSync` to contain a single `inner: Arc` field. First off, it's much simpler to understand what's behind an `Arc` for “clonability” of `SlidingSync`, and what's not. We don't need to worry about adding a new field that is not behind an `Arc` for a specific reason or anything. The pattern is clear now. Second, this is required for next commits. --- crates/matrix-sdk/src/sliding_sync/builder.rs | 32 ++-- crates/matrix-sdk/src/sliding_sync/mod.rs | 159 ++++++++++-------- 2 files changed, 103 insertions(+), 88 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/builder.rs b/crates/matrix-sdk/src/sliding_sync/builder.rs index 0eb35a262..6feaa3909 100644 --- a/crates/matrix-sdk/src/sliding_sync/builder.rs +++ b/crates/matrix-sdk/src/sliding_sync/builder.rs @@ -16,8 +16,8 @@ use tracing::trace; use url::Url; use super::{ - Error, FrozenSlidingSync, FrozenSlidingSyncList, SlidingSync, SlidingSyncList, - SlidingSyncListBuilder, SlidingSyncRoom, + Error, FrozenSlidingSync, FrozenSlidingSyncList, SlidingSync, SlidingSyncInner, + SlidingSyncList, SlidingSyncListBuilder, SlidingSyncRoom, }; use crate::{Client, Result}; @@ -286,24 +286,26 @@ impl SlidingSyncBuilder { trace!(len = rooms_found.len(), "rooms unfrozen"); - let rooms = Arc::new(StdRwLock::new(rooms_found)); - let lists = Arc::new(StdRwLock::new(self.lists)); + let rooms = StdRwLock::new(rooms_found); + let lists = StdRwLock::new(self.lists); Ok(SlidingSync { - homeserver: self.homeserver, - client, - storage_key: self.storage_key, + inner: Arc::new(SlidingSyncInner { + homeserver: self.homeserver, + client, + storage_key: self.storage_key, - lists, - rooms, + lists, + rooms, - extensions: Mutex::new(self.extensions).into(), - reset_counter: Default::default(), + extensions: Mutex::new(self.extensions).into(), + reset_counter: Default::default(), - pos: Arc::new(StdRwLock::new(Observable::new(None))), - delta_token: Arc::new(StdRwLock::new(Observable::new(delta_token_inner))), - subscriptions: Arc::new(StdRwLock::new(self.subscriptions)), - unsubscribe: Default::default(), + pos: StdRwLock::new(Observable::new(None)), + delta_token: StdRwLock::new(Observable::new(delta_token_inner)), + subscriptions: StdRwLock::new(self.subscriptions), + unsubscribe: Default::default(), + }), }) } } diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 67aa4c521..725a0aec0 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -643,6 +643,11 @@ const MAXIMUM_SLIDING_SYNC_SESSION_EXPIRATION: u8 = 3; /// The Sliding Sync instance. #[derive(Clone, Debug)] pub struct SlidingSync { + inner: Arc, +} + +#[derive(Debug)] +pub(super) struct SlidingSyncInner { /// Customize the homeserver for sliding sync only homeserver: Option, @@ -653,55 +658,33 @@ pub struct SlidingSync { storage_key: Option, /// The `pos` marker. - pos: Arc>>>, + pos: StdRwLock>>, - delta_token: Arc>>>, + delta_token: StdRwLock>>, /// The lists of this Sliding Sync instance. - lists: Arc>>, + lists: StdRwLock>, /// The rooms details - rooms: Arc>>, + rooms: StdRwLock>, - subscriptions: Arc>>, - unsubscribe: Arc>>, + subscriptions: StdRwLock>, + unsubscribe: StdRwLock>, /// Number of times a Sliding Session session has been reset. - reset_counter: Arc, + reset_counter: AtomicU8, /// the intended state of the extensions being supplied to sliding /sync /// calls. May contain the latest next_batch for to_devices, etc. - extensions: Arc>>, -} - -#[derive(Serialize, Deserialize)] -struct FrozenSlidingSync { - #[serde(skip_serializing_if = "Option::is_none")] - to_device_since: Option, - #[serde(skip_serializing_if = "Option::is_none")] - delta_token: Option, -} - -impl From<&SlidingSync> for FrozenSlidingSync { - fn from(v: &SlidingSync) -> Self { - FrozenSlidingSync { - delta_token: v.delta_token.read().unwrap().clone(), - to_device_since: v - .extensions - .lock() - .unwrap() - .as_ref() - .and_then(|ext| ext.to_device.as_ref()?.since.clone()), - } - } + extensions: Mutex>, } impl SlidingSync { async fn cache_to_storage(&self) -> Result<(), crate::Error> { - let Some(storage_key) = self.storage_key.as_ref() else { return Ok(()) }; + let Some(storage_key) = self.inner.storage_key.as_ref() else { return Ok(()) }; trace!(storage_key, "Saving to storage for later use"); - let store = self.client.store(); + let store = self.inner.client.store(); // Write this `SlidingSync` instance, as a `FrozenSlidingSync` instance, inside // the client store. @@ -714,9 +697,10 @@ impl SlidingSync { // Write every `SlidingSyncList` inside the client the store. let frozen_lists = { - let rooms_lock = self.rooms.read().unwrap(); + let rooms_lock = self.inner.rooms.read().unwrap(); - self.lists + self.inner + .lists .read() .unwrap() .iter() @@ -747,16 +731,16 @@ impl SlidingSync { /// lists but without the current state. pub fn new_builder_copy(&self) -> SlidingSyncBuilder { let mut builder = Self::builder() - .client(self.client.clone()) - .subscriptions(self.subscriptions.read().unwrap().to_owned()); + .client(self.inner.client.clone()) + .subscriptions(self.inner.subscriptions.read().unwrap().to_owned()); - for list in self.lists.read().unwrap().values().map(|list| { + for list in self.inner.lists.read().unwrap().values().map(|list| { list.new_builder().build().expect("builder worked before, builder works now") }) { builder = builder.add_list(list); } - if let Some(homeserver) = &self.homeserver { + if let Some(homeserver) = &self.inner.homeserver { builder.homeserver(homeserver.clone()) } else { builder @@ -769,7 +753,7 @@ impl SlidingSync { /// poll the stream after you've altered this. If you do that during, it /// might take one round trip to take effect. pub fn subscribe(&self, room_id: OwnedRoomId, settings: Option) { - self.subscriptions.write().unwrap().insert(room_id, settings.unwrap_or_default()); + self.inner.subscriptions.write().unwrap().insert(room_id, settings.unwrap_or_default()); } /// Unsubscribe from a given room. @@ -778,14 +762,14 @@ impl SlidingSync { /// poll the stream after you've altered this. If you do that during, it /// might take one round trip to take effect. pub fn unsubscribe(&self, room_id: OwnedRoomId) { - if self.subscriptions.write().unwrap().remove(&room_id).is_some() { - self.unsubscribe.write().unwrap().push(room_id); + if self.inner.subscriptions.write().unwrap().remove(&room_id).is_some() { + self.inner.unsubscribe.write().unwrap().push(room_id); } } /// Add the common extensions if not already configured. pub fn add_common_extensions(&self) { - let mut lock = self.extensions.lock().unwrap(); + let mut lock = self.inner.extensions.lock().unwrap(); let mut cfg = lock.get_or_insert_with(Default::default); if cfg.to_device.is_none() { @@ -803,18 +787,19 @@ impl SlidingSync { /// Lookup a specific room pub fn get_room(&self, room_id: &RoomId) -> Option { - self.rooms.read().unwrap().get(room_id).cloned() + self.inner.rooms.read().unwrap().get(room_id).cloned() } /// Check the number of rooms. pub fn get_number_of_rooms(&self) -> usize { - self.rooms.read().unwrap().len() + self.inner.rooms.read().unwrap().len() } fn update_to_device_since(&self, since: String) { // FIXME: Find a better place where the to-device since token should be // persisted. - self.extensions + self.inner + .extensions .lock() .unwrap() .get_or_insert_with(Default::default) @@ -829,7 +814,7 @@ impl SlidingSync { /// listening to the stream and is therefor not necessarily up to date /// with the lists used for the stream. pub fn list(&self, list_name: &str) -> Option { - self.lists.read().unwrap().get(list_name).cloned() + self.inner.lists.read().unwrap().get(list_name).cloned() } /// Remove the SlidingSyncList named `list_name` from the lists list if @@ -839,7 +824,7 @@ impl SlidingSync { /// stream created after this. The old stream will still continue to use the /// previous set of lists. pub fn pop_list(&self, list_name: &String) -> Option { - self.lists.write().unwrap().remove(list_name) + self.inner.lists.write().unwrap().remove(list_name) } /// Add the list to the list of lists. @@ -852,7 +837,7 @@ impl SlidingSync { /// stream created after this. The old stream will still continue to use the /// previous set of lists. pub fn add_list(&self, list: SlidingSyncList) -> Option { - self.lists.write().unwrap().insert(list.name.clone(), list) + self.inner.lists.write().unwrap().insert(list.name.clone(), list) } /// Lookup a set of rooms @@ -860,14 +845,14 @@ impl SlidingSync { &self, room_ids: I, ) -> Vec> { - let rooms = self.rooms.read().unwrap(); + let rooms = self.inner.rooms.read().unwrap(); room_ids.map(|room_id| rooms.get(&room_id).cloned()).collect() } /// Get all rooms. pub fn get_all_rooms(&self) -> Vec { - self.rooms.read().unwrap().values().cloned().collect() + self.inner.rooms.read().unwrap().values().cloned().collect() } fn prepare_extension_config(&self, pos: Option<&str>) -> ExtensionsConfig { @@ -875,7 +860,7 @@ impl SlidingSync { // The pos is `None`, it's either our initial sync or the proxy forgot about us // and sent us an `UnknownPos` error. We need to send out the config for our // extensions. - let mut extensions = self.extensions.lock().unwrap().clone().unwrap_or_default(); + let mut extensions = self.inner.extensions.lock().unwrap().clone().unwrap_or_default(); // Always enable to-device events and the e2ee-extension on the initial request, // no matter what the caller wants. @@ -898,6 +883,7 @@ impl SlidingSync { // We already enabled all the things, just fetch out the to-device since token // out of self.extensions and set it in a new, and empty, `ExtensionsConfig`. let since = self + .inner .extensions .lock() .unwrap() @@ -919,12 +905,15 @@ impl SlidingSync { mut sync_response: SyncResponse, list_generators: &mut BTreeMap, ) -> Result { - Observable::set(&mut self.pos.write().unwrap(), Some(sliding_sync_response.pos)); - Observable::set(&mut self.delta_token.write().unwrap(), sliding_sync_response.delta_token); + Observable::set(&mut self.inner.pos.write().unwrap(), Some(sliding_sync_response.pos)); + Observable::set( + &mut self.inner.delta_token.write().unwrap(), + sliding_sync_response.delta_token, + ); let update_summary = { let mut rooms = Vec::new(); - let mut rooms_map = self.rooms.write().unwrap(); + let mut rooms_map = self.inner.rooms.write().unwrap(); for (room_id, mut room_data) in sliding_sync_response.rooms.into_iter() { // `sync_response` contains the rooms with decrypted events if any, so look at @@ -948,7 +937,7 @@ impl SlidingSync { rooms_map.insert( room_id.clone(), SlidingSyncRoom::new( - self.client.clone(), + self.inner.client.clone(), room_id.clone(), room_data, timeline, @@ -1014,10 +1003,10 @@ impl SlidingSync { return Ok(None); } - let pos = self.pos.read().unwrap().clone(); - let delta_token = self.delta_token.read().unwrap().clone(); - let room_subscriptions = self.subscriptions.read().unwrap().clone(); - let unsubscribe_rooms = mem::take(&mut *self.unsubscribe.write().unwrap()); + let pos = self.inner.pos.read().unwrap().clone(); + let delta_token = self.inner.delta_token.read().unwrap().clone(); + let room_subscriptions = self.inner.subscriptions.read().unwrap().clone(); + let unsubscribe_rooms = mem::take(&mut *self.inner.unsubscribe.write().unwrap()); let timeout = Duration::from_secs(30); let extensions = self.prepare_extension_config(pos.as_deref()); @@ -1028,7 +1017,7 @@ impl SlidingSync { let request_config = RequestConfig::default().timeout(timeout + Duration::from_secs(30)); // Prepare the request. - let request = self.client.send_with_homeserver( + let request = self.inner.client.send_with_homeserver( assign!(v4::Request::new(), { pos, delta_token, @@ -1042,7 +1031,7 @@ impl SlidingSync { extensions, }), Some(request_config), - self.homeserver.as_ref().map(ToString::to_string), + self.inner.homeserver.as_ref().map(ToString::to_string), ); // Send the request and get a response with end-to-end encryption support. @@ -1057,7 +1046,8 @@ impl SlidingSync { #[cfg(feature = "e2e-encryption")] let response = { let (e2ee_uploads, response) = - futures_util::future::join(self.client.send_outgoing_requests(), request).await; + futures_util::future::join(self.inner.client.send_outgoing_requests(), request) + .await; if let Err(error) = e2ee_uploads { error!(?error, "Error while sending outgoing E2EE requests"); @@ -1103,7 +1093,7 @@ impl SlidingSync { // move to Sliding Sync, i.e. to `v4::Response`), but processing the // `sliding_sync_response` is vital, so it must be done somewhere; for now it // happens here. - let sync_response = self.client.process_sliding_sync(&response).await?; + let sync_response = self.inner.client.process_sliding_sync(&response).await?; debug!("Sliding sync response has been processed"); @@ -1120,12 +1110,12 @@ impl SlidingSync { /// /// This stream will send requests and will handle responses automatically, /// hence updating the lists. - #[instrument(name = "sync_stream", skip_all, parent = &self.client.root_span)] + #[instrument(name = "sync_stream", skip_all, parent = &self.inner.client.root_span)] pub fn stream(&self) -> impl Stream> + '_ { // Collect all the lists that need to be updated. let mut list_generators = { let mut list_generators = BTreeMap::new(); - let lock = self.lists.read().unwrap(); + let lock = self.inner.lists.read().unwrap(); for (name, lists) in lock.iter() { list_generators.insert(name.clone(), lists.request_generator()); @@ -1136,7 +1126,7 @@ impl SlidingSync { let stream_id = Uuid::new_v4().to_string(); - debug!(?self.extensions, stream_id, "About to run the sync stream"); + debug!(?self.inner.extensions, stream_id, "About to run the sync stream"); let instrument_span = Span::current(); @@ -1145,12 +1135,12 @@ impl SlidingSync { let sync_span = info_span!(parent: &instrument_span, "sync_once"); sync_span.in_scope(|| { - debug!(?self.extensions, "Sync stream loop is running"); + debug!(?self.inner.extensions, "Sync stream loop is running"); }); match self.sync_once(&stream_id, &mut list_generators).instrument(sync_span.clone()).await { Ok(Some(updates)) => { - self.reset_counter.store(0, Ordering::SeqCst); + self.inner.reset_counter.store(0, Ordering::SeqCst); yield Ok(updates); } @@ -1164,7 +1154,7 @@ impl SlidingSync { // The session has expired. // Has it expired too many times? - if self.reset_counter.fetch_add(1, Ordering::SeqCst) >= MAXIMUM_SLIDING_SYNC_SESSION_EXPIRATION { + if self.inner.reset_counter.fetch_add(1, Ordering::SeqCst) >= MAXIMUM_SLIDING_SYNC_SESSION_EXPIRATION { sync_span.in_scope(|| error!("Session expired {MAXIMUM_SLIDING_SYNC_SESSION_EXPIRATION} times in a row")); // The session has expired too many times, let's raise an error! @@ -1178,9 +1168,9 @@ impl SlidingSync { warn!("Session expired. Restarting Sliding Sync."); // To “restart” a Sliding Sync session, we set `pos` to its initial value. - Observable::set(&mut self.pos.write().unwrap(), None); + Observable::set(&mut self.inner.pos.write().unwrap(), None); - debug!(?self.extensions, "Sliding Sync has been reset"); + debug!(?self.inner.extensions, "Sliding Sync has been reset"); }); } @@ -1198,12 +1188,35 @@ impl SlidingSync { impl SlidingSync { /// Get a copy of the `pos` value. pub fn pos(&self) -> Option { - self.pos.read().unwrap().clone() + self.inner.pos.read().unwrap().clone() } /// Set a new value for `pos`. pub fn set_pos(&self, new_pos: String) { - Observable::set(&mut self.pos.write().unwrap(), Some(new_pos)); + Observable::set(&mut self.inner.pos.write().unwrap(), Some(new_pos)); + } +} + +#[derive(Serialize, Deserialize)] +struct FrozenSlidingSync { + #[serde(skip_serializing_if = "Option::is_none")] + to_device_since: Option, + #[serde(skip_serializing_if = "Option::is_none")] + delta_token: Option, +} + +impl From<&SlidingSync> for FrozenSlidingSync { + fn from(sliding_sync: &SlidingSync) -> Self { + FrozenSlidingSync { + delta_token: sliding_sync.inner.delta_token.read().unwrap().clone(), + to_device_since: sliding_sync + .inner + .extensions + .lock() + .unwrap() + .as_ref() + .and_then(|ext| ext.to_device.as_ref()?.since.clone()), + } } } From be6c7c8b4ce3515b7071280203b0bb07b3cb7b7e Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Wed, 8 Mar 2023 13:30:46 +0100 Subject: [PATCH 150/166] Add user avatar URL caching --- Cargo.lock | 8 ++--- bindings/matrix-sdk-ffi/src/api.udl | 5 ++- bindings/matrix-sdk-ffi/src/client.rs | 14 ++++++-- .../matrix-sdk-base/src/store/memory_store.rs | 23 +++++++++++++ crates/matrix-sdk-base/src/store/traits.rs | 32 +++++++++++++++++-- .../src/state_store/mod.rs | 21 ++++++++++++ crates/matrix-sdk-sled/src/state_store/mod.rs | 20 ++++++++++++ crates/matrix-sdk/src/account.rs | 24 ++++++++++++++ 8 files changed, 136 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5a86e9ded..f11811c82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1484,9 +1484,9 @@ dependencies = [ [[package]] name = "eyeball" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7293923c1e4c768a56358441174001e3805a808fc453d3a93b20a5d3ebde347" +checksum = "3609348664c9c1b07d9ff3933a466beaa4d197fb393b5d41ffda349458867626" dependencies = [ "futures-core", "readlock", @@ -4880,9 +4880,9 @@ checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb" [[package]] name = "string_cache" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213494b7a2b503146286049378ce02b482200519accc31872ee8be91fa820a08" +checksum = "7d69e88b23f23030bf4d0e9ca7b07434f70e1c1f4d3ca7e93ce958b373654d9f" dependencies = [ "new_debug_unreachable", "once_cell", diff --git a/bindings/matrix-sdk-ffi/src/api.udl b/bindings/matrix-sdk-ffi/src/api.udl index c9f34913b..a85047e57 100644 --- a/bindings/matrix-sdk-ffi/src/api.udl +++ b/bindings/matrix-sdk-ffi/src/api.udl @@ -171,7 +171,10 @@ interface Client { void set_display_name(string name); [Throws=ClientError] - string avatar_url(); + string? avatar_url(); + + [Throws=ClientError] + string? cached_avatar_url(); [Throws=ClientError] string device_id(); diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 5bc2bf738..7176bd88a 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -224,11 +224,19 @@ impl Client { }) } - pub fn avatar_url(&self) -> anyhow::Result { + 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?.context("No User ID found")?; - Ok(avatar_url.to_string()) + let avatar_url = l.account().get_avatar_url().await?; + Ok(avatar_url.map(|u| u.to_string())) + }) + } + + pub fn cached_avatar_url(&self) -> anyhow::Result> { + let l = self.client.clone(); + RUNTIME.block_on(async move { + let url = l.account().get_cached_avatar_url().await?; + Ok(url) }) } diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index 14dcc74ef..81b35e886 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -48,6 +48,7 @@ use crate::{ #[allow(clippy::type_complexity)] #[derive(Debug, Clone)] pub struct MemoryStore { + user_avatar_url: Arc>, sync_token: Arc>>, filters: Arc>, account_data: Arc>>, @@ -95,6 +96,7 @@ impl MemoryStore { /// Create a new empty MemoryStore pub fn new() -> Self { Self { + user_avatar_url: Default::default(), sync_token: Default::default(), filters: Default::default(), account_data: Default::default(), @@ -131,6 +133,10 @@ impl MemoryStore { .filters .get(filter_name) .map(|f| StateStoreDataValue::Filter(f.value().clone()))), + StateStoreDataKey::UserAvatarUrl(user_id) => Ok(self + .user_avatar_url + .get(user_id.as_str()) + .map(|u| StateStoreDataValue::UserAvatarUrl(u.value().clone()))), } } @@ -150,6 +156,12 @@ impl MemoryStore { value.into_filter().expect("Session data not a filter"), ); } + StateStoreDataKey::UserAvatarUrl(user_id) => { + self.filters.insert( + user_id.to_string(), + value.into_user_avatar_url().expect("Session data not a user avatar url"), + ); + } } Ok(()) @@ -161,6 +173,9 @@ impl MemoryStore { StateStoreDataKey::Filter(filter_name) => { self.filters.remove(filter_name); } + StateStoreDataKey::UserAvatarUrl(user_id) => { + self.filters.remove(user_id.as_str()); + } } Ok(()) @@ -591,6 +606,10 @@ impl MemoryStore { Ok(self.custom.insert(key.to_vec(), value)) } + async fn remove_custom_value(&self, key: &[u8]) -> Result>> { + Ok(self.custom.remove(key).map(|entry| entry.1)) + } + // The in-memory store doesn't cache media async fn add_media_content(&self, _request: &MediaRequest, _data: Vec) -> Result<()> { Ok(()) @@ -769,6 +788,10 @@ impl StateStore for MemoryStore { self.set_custom_value(key, value).await } + async fn remove_custom_value(&self, key: &[u8]) -> Result>> { + self.remove_custom_value(key).await + } + async fn add_media_content(&self, request: &MediaRequest, data: Vec) -> Result<()> { self.add_media_content(request, data).await } diff --git a/crates/matrix-sdk-base/src/store/traits.rs b/crates/matrix-sdk-base/src/store/traits.rs index 6659b75d5..00ed28d39 100644 --- a/crates/matrix-sdk-base/src/store/traits.rs +++ b/crates/matrix-sdk-base/src/store/traits.rs @@ -261,6 +261,13 @@ pub trait StateStore: AsyncTraitDeps { value: Vec, ) -> Result>, Self::Error>; + /// Remove arbitrary data from the custom store and return it if existed + /// + /// # Arguments + /// + /// * `key` - The key to remove data from + async fn remove_custom_value(&self, key: &[u8]) -> Result>, Self::Error>; + /// Add a media file's content in the media store. /// /// # Arguments @@ -468,6 +475,10 @@ impl StateStore for EraseStateStoreError { self.0.set_custom_value(key, value).await.map_err(Into::into) } + async fn remove_custom_value(&self, key: &[u8]) -> Result>, Self::Error> { + self.0.remove_custom_value(key).await.map_err(Into::into) + } + async fn add_media_content( &self, request: &MediaRequest, @@ -634,6 +645,9 @@ pub enum StateStoreDataValue { /// A filter with the given ID. Filter(String), + + /// The user avatar url + UserAvatarUrl(String), } impl StateStoreDataValue { @@ -641,15 +655,23 @@ impl StateStoreDataValue { pub fn into_sync_token(self) -> Option { match self { Self::SyncToken(token) => Some(token), - Self::Filter(_) => None, + _ => None, } } /// Get this value if it is a filter. pub fn into_filter(self) -> Option { match self { - Self::SyncToken(_) => None, - Self::Filter(id) => Some(id), + Self::Filter(filter) => Some(filter), + _ => None, + } + } + + /// Get this value if it is a user avatar url. + pub fn into_user_avatar_url(self) -> Option { + match self { + Self::UserAvatarUrl(user_avatar_url) => Some(user_avatar_url), + _ => None, } } } @@ -662,6 +684,9 @@ pub enum StateStoreDataKey<'a> { /// A filter with the given name. Filter(&'a str), + + /// Avatar URL + UserAvatarUrl(&'a UserId), } impl<'a> StateStoreDataKey<'a> { @@ -670,6 +695,7 @@ impl<'a> StateStoreDataKey<'a> { match self { Self::SyncToken => "sync_token", Self::Filter(_) => "filter", + Self::UserAvatarUrl(_) => "user_avatar_url", } } } diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index cb615423f..94dc622d8 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -418,6 +418,9 @@ impl IndexeddbStateStore { StateStoreDataKey::Filter(filter_name) => { self.encode_key(key.encoding_key(), (key.encoding_key(), filter_name)) } + StateStoreDataKey::UserAvatarUrl(user_id) => { + self.encode_key(KEYS::KV, (key.encoding_key(), user_id)) + } } } } @@ -470,6 +473,7 @@ impl_state_store! { let value = match key { StateStoreDataKey::SyncToken => value.map(StateStoreDataValue::SyncToken), StateStoreDataKey::Filter(_) => value.map(StateStoreDataValue::Filter), + StateStoreDataKey::UserAvatarUrl(_) => value.map(StateStoreDataValue::UserAvatarUrl), }; Ok(value) @@ -489,6 +493,9 @@ impl_state_store! { StateStoreDataKey::Filter(_) => { value.into_filter().expect("Session data not a filter") } + StateStoreDataKey::UserAvatarUrl(_) => { + value.into_user_avatar_url().expect("Session data not an user avatar url") + } }; let tx = self @@ -1150,6 +1157,20 @@ impl_state_store! { Ok(prev) } + async fn remove_custom_value(&self, key: &[u8]) -> Result>> { + let jskey = JsValue::from_str(core::str::from_utf8(key).map_err(StoreError::Codec)?); + + let prev = self.get_custom_value_for_js(&jskey).await?; + + let tx = + self.inner.transaction_on_one_with_mode(KEYS::CUSTOM, IdbTransactionMode::Readwrite)?; + + tx.object_store(KEYS::CUSTOM)?.delete(&jskey)?; + + tx.await.into_result().map_err(IndexeddbStateStoreError::from)?; + Ok(prev) + } + async fn remove_media_content(&self, request: &MediaRequest) -> Result<()> { let key = self .encode_key(KEYS::MEDIA, (request.source.unique_key(), request.format.unique_key())); diff --git a/crates/matrix-sdk-sled/src/state_store/mod.rs b/crates/matrix-sdk-sled/src/state_store/mod.rs index 9fbd2b724..ab3dc1501 100644 --- a/crates/matrix-sdk-sled/src/state_store/mod.rs +++ b/crates/matrix-sdk-sled/src/state_store/mod.rs @@ -415,6 +415,9 @@ impl SledStateStore { StateStoreDataKey::Filter(filter_name) => { self.encode_key(keys::SESSION, (key.encoding_key(), filter_name)) } + StateStoreDataKey::UserAvatarUrl(user_id) => { + self.encode_key(keys::SESSION, (key.encoding_key(), user_id)) + } } } @@ -427,6 +430,7 @@ impl SledStateStore { let value = match key { StateStoreDataKey::SyncToken => value.map(StateStoreDataValue::SyncToken), StateStoreDataKey::Filter(_) => value.map(StateStoreDataValue::Filter), + StateStoreDataKey::UserAvatarUrl(_) => value.map(StateStoreDataValue::UserAvatarUrl), }; Ok(value) @@ -444,6 +448,9 @@ impl SledStateStore { value.into_sync_token().expect("Session data not a sync token") } StateStoreDataKey::Filter(_) => value.into_filter().expect("Session data not a filter"), + StateStoreDataKey::UserAvatarUrl(_) => { + value.into_user_avatar_url().expect("Session data not an user avatar url") + } }; self.kv.insert(encoded_key, self.serialize_value(&value)?)?; @@ -1157,6 +1164,15 @@ impl SledStateStore { ret } + async fn remove_custom_value(&self, key: &[u8]) -> Result>> { + let key = self.encode_key(keys::CUSTOM, EncodeUnchecked::from(key)); + let me = self.clone(); + let ret = self.custom.remove(key)?.map(|v| me.deserialize_value(&v)).transpose(); + self.inner.flush_async().await?; + + ret + } + async fn remove_media_content(&self, request: &MediaRequest) -> Result<()> { self.media.remove( self.encode_key( @@ -1515,6 +1531,10 @@ impl StateStore for SledStateStore { self.set_custom_value(key, value).await.map_err(Into::into) } + async fn remove_custom_value(&self, key: &[u8]) -> StoreResult>> { + self.remove_custom_value(key).await.map_err(Into::into) + } + async fn add_media_content(&self, request: &MediaRequest, data: Vec) -> StoreResult<()> { self.add_media_content(request, data).await.map_err(Into::into) } diff --git a/crates/matrix-sdk/src/account.rs b/crates/matrix-sdk/src/account.rs index 656292a44..9579106f1 100644 --- a/crates/matrix-sdk/src/account.rs +++ b/crates/matrix-sdk/src/account.rs @@ -17,6 +17,7 @@ use matrix_sdk_base::{ media::{MediaFormat, MediaRequest}, store::StateStoreExt, + StateStoreDataKey, StateStoreDataValue, }; use mime::Mime; use ruma::{ @@ -133,9 +134,32 @@ impl Account { let config = Some(RequestConfig::new().force_auth()); let response = self.client.send(request, config).await?; + if let Some(url) = response.avatar_url.clone() { + // If an avatar is found cache it. + let _ = self + .client + .store() + .set_kv_data( + StateStoreDataKey::UserAvatarUrl(user_id), + StateStoreDataValue::UserAvatarUrl(url.to_string()), + ) + .await; + } else { + // If there is no avatar the user has removed it and we uncache it. + let _ = + self.client.store().remove_kv_data(StateStoreDataKey::UserAvatarUrl(user_id)).await; + } Ok(response.avatar_url) } + /// Get the URL of the account's avatar, if is stored in cache. + pub async fn get_cached_avatar_url(&self) -> Result> { + let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?; + let data = + self.client.store().get_kv_data(StateStoreDataKey::UserAvatarUrl(user_id)).await?; + Ok(data.map(|v| v.into_user_avatar_url().expect("Session data is not a user avatar url"))) + } + /// Set the MXC URI of the account's avatar. /// /// The avatar is unset if `url` is `None`. From 48bf20fcca17c120f765c449b73a13c54ba48874 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 8 Mar 2023 12:53:23 +0000 Subject: [PATCH 151/166] Add integration test for receipts in sliding sync --- Cargo.lock | 14 ++-- Cargo.toml | 4 +- crates/matrix-sdk-base/src/sliding_sync.rs | 9 +-- crates/matrix-sdk/src/sliding_sync/builder.rs | 12 +-- .../assets/docker-compose.yml | 2 +- .../sliding-sync-integration-test/src/lib.rs | 76 ++++++++++++++++++- 6 files changed, 94 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0fca4f785..a43c0e425 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4292,7 +4292,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.8.2" -source = "git+https://github.com/ruma/ruma?rev=2edfe5bc5f3ee88014e57230c50a5e005119344a#2edfe5bc5f3ee88014e57230c50a5e005119344a" +source = "git+https://github.com/ruma/ruma?rev=543c03f8f2d0f5a357b6be41cc0e166aa58cce63#543c03f8f2d0f5a357b6be41cc0e166aa58cce63" dependencies = [ "assign", "js_int", @@ -4306,7 +4306,7 @@ dependencies = [ [[package]] name = "ruma-appservice-api" version = "0.8.1" -source = "git+https://github.com/ruma/ruma?rev=2edfe5bc5f3ee88014e57230c50a5e005119344a#2edfe5bc5f3ee88014e57230c50a5e005119344a" +source = "git+https://github.com/ruma/ruma?rev=543c03f8f2d0f5a357b6be41cc0e166aa58cce63#543c03f8f2d0f5a357b6be41cc0e166aa58cce63" dependencies = [ "js_int", "ruma-common", @@ -4317,7 +4317,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.16.2" -source = "git+https://github.com/ruma/ruma?rev=2edfe5bc5f3ee88014e57230c50a5e005119344a#2edfe5bc5f3ee88014e57230c50a5e005119344a" +source = "git+https://github.com/ruma/ruma?rev=543c03f8f2d0f5a357b6be41cc0e166aa58cce63#543c03f8f2d0f5a357b6be41cc0e166aa58cce63" dependencies = [ "assign", "bytes", @@ -4334,7 +4334,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.11.3" -source = "git+https://github.com/ruma/ruma?rev=2edfe5bc5f3ee88014e57230c50a5e005119344a#2edfe5bc5f3ee88014e57230c50a5e005119344a" +source = "git+https://github.com/ruma/ruma?rev=543c03f8f2d0f5a357b6be41cc0e166aa58cce63#543c03f8f2d0f5a357b6be41cc0e166aa58cce63" dependencies = [ "base64 0.21.0", "bytes", @@ -4367,7 +4367,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.7.1" -source = "git+https://github.com/ruma/ruma?rev=2edfe5bc5f3ee88014e57230c50a5e005119344a#2edfe5bc5f3ee88014e57230c50a5e005119344a" +source = "git+https://github.com/ruma/ruma?rev=543c03f8f2d0f5a357b6be41cc0e166aa58cce63#543c03f8f2d0f5a357b6be41cc0e166aa58cce63" dependencies = [ "js_int", "ruma-common", @@ -4378,7 +4378,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.9.1" -source = "git+https://github.com/ruma/ruma?rev=2edfe5bc5f3ee88014e57230c50a5e005119344a#2edfe5bc5f3ee88014e57230c50a5e005119344a" +source = "git+https://github.com/ruma/ruma?rev=543c03f8f2d0f5a357b6be41cc0e166aa58cce63#543c03f8f2d0f5a357b6be41cc0e166aa58cce63" dependencies = [ "js_int", "thiserror", @@ -4387,7 +4387,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.11.3" -source = "git+https://github.com/ruma/ruma?rev=2edfe5bc5f3ee88014e57230c50a5e005119344a#2edfe5bc5f3ee88014e57230c50a5e005119344a" +source = "git+https://github.com/ruma/ruma?rev=543c03f8f2d0f5a357b6be41cc0e166aa58cce63#543c03f8f2d0f5a357b6be41cc0e166aa58cce63" dependencies = [ "once_cell", "proc-macro-crate", diff --git a/Cargo.toml b/Cargo.toml index d1a7d1b37..a9589d57f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,8 +30,8 @@ dashmap = "5.2.0" eyeball = "0.1.4" eyeball-im = "0.1.0" http = "0.2.6" -ruma = { git = "https://github.com/ruma/ruma", rev = "2edfe5bc5f3ee88014e57230c50a5e005119344a", features = ["client-api-c"] } -ruma-common = { git = "https://github.com/ruma/ruma", rev = "2edfe5bc5f3ee88014e57230c50a5e005119344a" } +ruma = { git = "https://github.com/ruma/ruma", rev = "543c03f8f2d0f5a357b6be41cc0e166aa58cce63", features = ["client-api-c"] } +ruma-common = { git = "https://github.com/ruma/ruma", rev = "543c03f8f2d0f5a357b6be41cc0e166aa58cce63" } once_cell = "1.16.0" serde = "1.0.151" serde_html_form = "0.2.0" diff --git a/crates/matrix-sdk-base/src/sliding_sync.rs b/crates/matrix-sdk-base/src/sliding_sync.rs index f649dbac6..23ede4ef4 100644 --- a/crates/matrix-sdk-base/src/sliding_sync.rs +++ b/crates/matrix-sdk-base/src/sliding_sync.rs @@ -9,7 +9,6 @@ use ruma::{ v3::{self, Ephemeral}, v4, DeviceLists, }, - events::AnyEphemeralRoomEvent, DeviceKeyAlgorithm, UInt, }; use tracing::{debug, info, instrument}; @@ -53,7 +52,7 @@ impl BaseClient { return Ok(SyncResponse::default()); }; - let v4::Extensions { to_device, e2ee, account_data, receipt, .. } = extensions; + let v4::Extensions { to_device, e2ee, account_data, receipts, .. } = extensions; let to_device_events = to_device.as_ref().map(|v4| v4.events.clone()).unwrap_or_default(); @@ -229,10 +228,10 @@ impl BaseClient { } // Process receipts now we have rooms - if let Some(receipts) = &receipt { + if let Some(receipts) = &receipts { for (room_id, receipt_edu) in &receipts.rooms { - if let Ok(AnyEphemeralRoomEvent::Receipt(event)) = receipt_edu.deserialize() { - changes.add_receipts(room_id, event.content); + if let Ok(receipt_edu) = receipt_edu.deserialize() { + changes.add_receipts(room_id, receipt_edu.content); } } } diff --git a/crates/matrix-sdk/src/sliding_sync/builder.rs b/crates/matrix-sdk/src/sliding_sync/builder.rs index 0eb35a262..1144b5e4c 100644 --- a/crates/matrix-sdk/src/sliding_sync/builder.rs +++ b/crates/matrix-sdk/src/sliding_sync/builder.rs @@ -7,7 +7,7 @@ use std::{ use eyeball::Observable; use ruma::{ api::client::sync::sync_events::v4::{ - self, AccountDataConfig, E2EEConfig, ExtensionsConfig, ReceiptConfig, ToDeviceConfig, + self, AccountDataConfig, E2EEConfig, ExtensionsConfig, ReceiptsConfig, ToDeviceConfig, TypingConfig, }, assign, OwnedRoomId, @@ -154,8 +154,8 @@ impl SlidingSyncBuilder { Some(assign!(AccountDataConfig::default(), { enabled: Some(true) })); } - if cfg.receipt.is_none() { - cfg.receipt = Some(assign!(ReceiptConfig::default(), { enabled: Some(true) })); + if cfg.receipts.is_none() { + cfg.receipts = Some(assign!(ReceiptsConfig::default(), { enabled: Some(true) })); } if cfg.typing.is_none() { @@ -214,14 +214,14 @@ impl SlidingSyncBuilder { } /// Set the Receipt extension configuration. - pub fn with_receipt_extension(mut self, receipt: ReceiptConfig) -> Self { - self.extensions.get_or_insert_with(Default::default).receipt = Some(receipt); + pub fn with_receipt_extension(mut self, receipt: ReceiptsConfig) -> Self { + self.extensions.get_or_insert_with(Default::default).receipts = Some(receipt); self } /// Unset the Receipt extension configuration. pub fn without_receipt_extension(mut self) -> Self { - self.extensions.get_or_insert_with(Default::default).receipt = None; + self.extensions.get_or_insert_with(Default::default).receipts = None; self } diff --git a/testing/sliding-sync-integration-test/assets/docker-compose.yml b/testing/sliding-sync-integration-test/assets/docker-compose.yml index 0e5cd9c90..f906b9edd 100644 --- a/testing/sliding-sync-integration-test/assets/docker-compose.yml +++ b/testing/sliding-sync-integration-test/assets/docker-compose.yml @@ -28,7 +28,7 @@ services: - ./data/db:/var/lib/postgresql/data sliding-sync-proxy: - image: ghcr.io/matrix-org/sliding-sync:v0.99.0 + image: ghcr.io/matrix-org/sliding-sync:v0.99.1 depends_on: postgres: condition: service_healthy diff --git a/testing/sliding-sync-integration-test/src/lib.rs b/testing/sliding-sync-integration-test/src/lib.rs index bf858ca5b..aa670c8f6 100644 --- a/testing/sliding-sync-integration-test/src/lib.rs +++ b/testing/sliding-sync-integration-test/src/lib.rs @@ -79,8 +79,16 @@ mod tests { use matrix_sdk::{ room::timeline::EventTimelineItem, ruma::{ - api::client::error::ErrorKind as RumaError, - events::room::message::RoomMessageEventContent, uint, + api::client::{ + error::ErrorKind as RumaError, + receipt::create_receipt::v3::ReceiptType as CreateReceiptType, + sync::sync_events::v4::ReceiptsConfig, + }, + events::{ + receipt::{ReceiptThread, ReceiptType}, + room::message::RoomMessageEventContent, + }, + uint, }, SlidingSyncList, SlidingSyncMode, SlidingSyncState, }; @@ -1323,4 +1331,68 @@ mod tests { Ok(()) } + + #[tokio::test(flavor = "multi_thread", worker_threads = 4)] + async fn receipts_extension_works() -> anyhow::Result<()> { + let (client, sync_proxy_builder) = random_setup_with_rooms(1).await?; + let list = SlidingSyncList::builder() + .sync_mode(SlidingSyncMode::Selective) + .ranges(vec![(0u32, 1u32)]) + .sort(vec!["by_recency".to_owned()]) + .name("a") + .build()?; + + let mut config = ReceiptsConfig::default(); + config.enabled = Some(true); + + let sync_proxy = sync_proxy_builder + .clone() + .add_list(list) + .with_receipt_extension(config) + .build() + .await?; + let list = sync_proxy.list("a").context("but we just added that list!")?; + + let stream = sync_proxy.stream(); + pin_mut!(stream); + + stream.next().await.context("sync has closed unexpectedly")??; + + // find the room and send an event which we will send a receipt for + let room_id = list.get_room_id(0).unwrap(); + let room = client.get_joined_room(&room_id).context("No joined room {room_id}")?; + let event_id = + room.send(RoomMessageEventContent::text_plain("Hello world"), None).await?.event_id; + + // now send a receipt + room.send_single_receipt( + CreateReceiptType::Read, + ReceiptThread::Unthreaded, + event_id.clone(), + ) + .await?; + + // we expect to see it because we have enabled the receipt extension. We don't know when we'll + // see it though + let mut found_receipt = false; + 'sync_loop: for _n in 0..3 { + stream.next().await.context("sync has closed unexpectedly")??; + + // try to find it + let room = client.get_room(&room_id).context("No joined room {room_id}")?; + let receipts = room + .event_receipts(ReceiptType::Read, ReceiptThread::Unthreaded, &event_id) + .await + .unwrap(); + + for (user_id, _receipt_data) in receipts { + if user_id == client.user_id().unwrap() { + found_receipt = true; + break 'sync_loop; + } + } + } + assert_eq!(found_receipt, true); + Ok(()) + } } From 9a53602d736531f9b9830bac802de159e846bc64 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 8 Mar 2023 12:58:48 +0000 Subject: [PATCH 152/166] Formatting --- testing/sliding-sync-integration-test/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/sliding-sync-integration-test/src/lib.rs b/testing/sliding-sync-integration-test/src/lib.rs index aa670c8f6..6df4f1856 100644 --- a/testing/sliding-sync-integration-test/src/lib.rs +++ b/testing/sliding-sync-integration-test/src/lib.rs @@ -1372,8 +1372,8 @@ mod tests { ) .await?; - // we expect to see it because we have enabled the receipt extension. We don't know when we'll - // see it though + // we expect to see it because we have enabled the receipt extension. We don't + // know when we'll see it though let mut found_receipt = false; 'sync_loop: for _n in 0..3 { stream.next().await.context("sync has closed unexpectedly")??; From 4670142a837c4d45dc3026b15f8457bec1f839ad Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 8 Mar 2023 13:21:56 +0000 Subject: [PATCH 153/166] Clippy --- testing/sliding-sync-integration-test/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/sliding-sync-integration-test/src/lib.rs b/testing/sliding-sync-integration-test/src/lib.rs index 6df4f1856..b55958e52 100644 --- a/testing/sliding-sync-integration-test/src/lib.rs +++ b/testing/sliding-sync-integration-test/src/lib.rs @@ -1392,7 +1392,7 @@ mod tests { } } } - assert_eq!(found_receipt, true); + assert!(found_receipt); Ok(()) } } From 68f8ed5a92f25249da0b22142f838cdf8515f351 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 8 Mar 2023 14:53:13 +0100 Subject: [PATCH 154/166] Add futures-util as a workspace dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … and always activate its `alloc` feature. Fixes building matrix-sdk without the e2e-encryption feature. --- Cargo.toml | 1 + crates/matrix-sdk-base/Cargo.toml | 2 +- crates/matrix-sdk-common/Cargo.toml | 2 +- crates/matrix-sdk-crypto/Cargo.toml | 2 +- crates/matrix-sdk-sled/Cargo.toml | 2 +- crates/matrix-sdk-sqlite/Cargo.toml | 2 +- crates/matrix-sdk/Cargo.toml | 2 +- 7 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 596ae59a2..0927c82a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ ctor = "0.1.26" dashmap = "5.2.0" eyeball = "0.1.4" eyeball-im = "0.1.0" +futures-util = { version = "0.3.26", default-features = false, features = ["alloc"] } http = "0.2.6" ruma = { git = "https://github.com/ruma/ruma", rev = "2edfe5bc5f3ee88014e57230c50a5e005119344a", features = ["client-api-c"] } ruma-common = { git = "https://github.com/ruma/ruma", rev = "2edfe5bc5f3ee88014e57230c50a5e005119344a" } diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index 37a4df13b..4bcfd5dc0 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -32,7 +32,7 @@ async-trait = { workspace = true } dashmap = { workspace = true } eyeball = { workspace = true } futures-core = "0.3.21" -futures-util = { version = "0.3.21", default-features = false } +futures-util = { workspace = true } http = { workspace = true, optional = true } matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" } matrix-sdk-crypto = { version = "0.6.0", path = "../matrix-sdk-crypto", optional = true } diff --git a/crates/matrix-sdk-common/Cargo.toml b/crates/matrix-sdk-common/Cargo.toml index 234ebe262..9682fe8c2 100644 --- a/crates/matrix-sdk-common/Cargo.toml +++ b/crates/matrix-sdk-common/Cargo.toml @@ -27,7 +27,7 @@ serde_json = { workspace = true } [target.'cfg(target_arch = "wasm32")'.dependencies] async-lock = "2.5.0" -futures-util = { version = "0.3.21", default-features = false, features = ["channel"] } +futures-util = { workspace = true, features = ["channel"] } wasm-bindgen-futures = { version = "0.4.33", optional = true } gloo-timers = { version = "0.2.6", features = ["futures"] } diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index 3f3421193..c3a5e3283 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -36,7 +36,7 @@ dashmap = { workspace = true } event-listener = "2.5.2" eyeball = { workspace = true } futures-core = "0.3.24" -futures-util = { version = "0.3.21", default-features = false, features = ["alloc"] } +futures-util = { workspace = true } hmac = "0.12.1" http = { workspace = true, optional = true } # feature = testing only matrix-sdk-qrcode = { version = "0.4.0", path = "../matrix-sdk-qrcode", optional = true } diff --git a/crates/matrix-sdk-sled/Cargo.toml b/crates/matrix-sdk-sled/Cargo.toml index 8de085944..67f0d2b40 100644 --- a/crates/matrix-sdk-sled/Cargo.toml +++ b/crates/matrix-sdk-sled/Cargo.toml @@ -29,7 +29,7 @@ async-trait = { workspace = true } dashmap = { workspace = true } fs_extra = "1.2.0" futures-core = "0.3.21" -futures-util = { version = "0.3.21", default-features = false } +futures-util = { workspace = true } matrix-sdk-base = { version = "0.6.0", path = "../matrix-sdk-base", optional = true } matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" } matrix-sdk-crypto = { version = "0.6.0", path = "../matrix-sdk-crypto", optional = true } diff --git a/crates/matrix-sdk-sqlite/Cargo.toml b/crates/matrix-sdk-sqlite/Cargo.toml index 4d04603d2..576f5a9e3 100644 --- a/crates/matrix-sdk-sqlite/Cargo.toml +++ b/crates/matrix-sdk-sqlite/Cargo.toml @@ -23,7 +23,7 @@ dashmap = { workspace = true } deadpool-sqlite = "0.5.0" fs_extra = "1.2.0" futures-core = "0.3.21" -futures-util = { version = "0.3.21", default-features = false } +futures-util = { workspace = true } matrix-sdk-base = { version = "0.6.0", path = "../matrix-sdk-base", optional = true } matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" } matrix-sdk-crypto = { version = "0.6.0", path = "../matrix-sdk-crypto", optional = true } diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 54bbcc3e3..fff94ed01 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -74,7 +74,7 @@ eyeball = { workspace = true } eyeball-im = { workspace = true } eyre = { version = "0.6.8", optional = true } futures-core = "0.3.21" -futures-util = { version = "0.3.21", default-features = false } +futures-util = { workspace = true } http = { workspace = true } im = { version = "15.1.0", features = ["serde"] } indexmap = "1.9.1" From 932cf2ad9969bdff4765e76bcee60f0316fa76e2 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 8 Mar 2023 14:59:37 +0100 Subject: [PATCH 155/166] Fix experimental features not compiling without encryption --- .../matrix-sdk/src/room/timeline/builder.rs | 8 +++---- .../src/room/timeline/event_handler.rs | 3 +++ crates/matrix-sdk/src/room/timeline/inner.rs | 22 +++++++++---------- .../matrix-sdk/src/room/timeline/tests/mod.rs | 1 + xtask/src/ci.rs | 5 ++++- 5 files changed, 21 insertions(+), 18 deletions(-) diff --git a/crates/matrix-sdk/src/room/timeline/builder.rs b/crates/matrix-sdk/src/room/timeline/builder.rs index f22a23fb4..13073766e 100644 --- a/crates/matrix-sdk/src/room/timeline/builder.rs +++ b/crates/matrix-sdk/src/room/timeline/builder.rs @@ -25,11 +25,9 @@ use ruma::events::{ }; use tracing::error; -use super::{ - inner::TimelineInner, - to_device::{handle_forwarded_room_key_event, handle_room_key_event}, - Timeline, TimelineEventHandlerHandles, -}; +#[cfg(feature = "e2e-encryption")] +use super::to_device::{handle_forwarded_room_key_event, handle_room_key_event}; +use super::{inner::TimelineInner, Timeline, TimelineEventHandlerHandles}; use crate::room; /// Builder that allows creating and configuring various parts of a diff --git a/crates/matrix-sdk/src/room/timeline/event_handler.rs b/crates/matrix-sdk/src/room/timeline/event_handler.rs index 76722ab48..411e8e865 100644 --- a/crates/matrix-sdk/src/room/timeline/event_handler.rs +++ b/crates/matrix-sdk/src/room/timeline/event_handler.rs @@ -185,6 +185,7 @@ pub(super) enum TimelineItemPosition { #[derive(Default)] pub(super) struct HandleEventResult { pub(super) item_added: bool, + #[cfg(feature = "e2e-encryption")] pub(super) item_removed: bool, pub(super) items_updated: u16, } @@ -325,6 +326,8 @@ impl<'a> TimelineEventHandler<'a> { if !self.result.item_added { trace!("No new item added"); + + #[cfg(feature = "e2e-encryption")] if let Flow::Remote { position: TimelineItemPosition::Update(idx), .. } = self.flow { // If add was not called, that means the UTD event is one that // wouldn't normally be visible. Remove it. diff --git a/crates/matrix-sdk/src/room/timeline/inner.rs b/crates/matrix-sdk/src/room/timeline/inner.rs index c73f968c4..b5e43af2b 100644 --- a/crates/matrix-sdk/src/room/timeline/inner.rs +++ b/crates/matrix-sdk/src/room/timeline/inner.rs @@ -12,20 +12,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{ - collections::{BTreeSet, HashMap}, - sync::Arc, -}; +#[cfg(feature = "e2e-encryption")] +use std::collections::BTreeSet; +use std::{collections::HashMap, sync::Arc}; use async_trait::async_trait; use eyeball_im::{ObservableVector, VectorSubscriber}; use im::Vector; use indexmap::{IndexMap, IndexSet}; +#[cfg(feature = "e2e-encryption")] +use matrix_sdk_base::crypto::OlmMachine; use matrix_sdk_base::{ - crypto::OlmMachine, deserialized_responses::{EncryptionInfo, SyncTimelineEvent, TimelineEvent}, locks::{Mutex, MutexGuard}, }; +#[cfg(feature = "e2e-encryption")] +use ruma::RoomId; use ruma::{ events::{ fully_read::FullyReadEvent, @@ -34,16 +36,12 @@ use ruma::{ AnyMessageLikeEventContent, AnySyncTimelineEvent, }, serde::Raw, - EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId, RoomId, + EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId, TransactionId, UserId, }; -use tracing::{ - debug, error, - field::{self, debug}, - info, info_span, warn, Instrument as _, -}; +use tracing::{debug, error, field::debug, instrument, trace, warn}; #[cfg(feature = "e2e-encryption")] -use tracing::{instrument, trace}; +use tracing::{field, info, info_span, Instrument as _}; use super::{ event_handler::{ diff --git a/crates/matrix-sdk/src/room/timeline/tests/mod.rs b/crates/matrix-sdk/src/room/timeline/tests/mod.rs index 45f125da8..15e9bb717 100644 --- a/crates/matrix-sdk/src/room/timeline/tests/mod.rs +++ b/crates/matrix-sdk/src/room/timeline/tests/mod.rs @@ -45,6 +45,7 @@ use super::{inner::RoomDataProvider, Profile, TimelineInner, TimelineItem}; mod basic; mod echo; +#[cfg(feature = "e2e-encryption")] mod encryption; mod invalid; mod read_receipts; diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs index 29e228469..65f264b16 100644 --- a/xtask/src/ci.rs +++ b/xtask/src/ci.rs @@ -191,7 +191,10 @@ fn check_docs() -> Result<()> { fn run_feature_tests(cmd: Option) -> Result<()> { let args = BTreeMap::from([ - (FeatureSet::NoEncryption, "--no-default-features --features sled,native-tls"), + ( + FeatureSet::NoEncryption, + "--no-default-features --features sled,native-tls,experimental-sliding-sync", + ), (FeatureSet::NoSled, "--no-default-features --features e2e-encryption,native-tls"), (FeatureSet::NoEncryptionAndSled, "--no-default-features --features native-tls"), ( From 64ae77ec70f30c8a1e26db2c6d336536520ccf80 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 8 Mar 2023 15:05:04 +0100 Subject: [PATCH 156/166] feat(sdk): Run SS response handling in a single non-cancellable block. --- crates/matrix-sdk/src/sliding_sync/builder.rs | 30 +++--- crates/matrix-sdk/src/sliding_sync/mod.rs | 98 ++++++++++++------- 2 files changed, 78 insertions(+), 50 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/builder.rs b/crates/matrix-sdk/src/sliding_sync/builder.rs index 6feaa3909..6f680399a 100644 --- a/crates/matrix-sdk/src/sliding_sync/builder.rs +++ b/crates/matrix-sdk/src/sliding_sync/builder.rs @@ -1,7 +1,7 @@ use std::{ collections::BTreeMap, fmt::Debug, - sync::{Arc, Mutex, RwLock as StdRwLock}, + sync::{Mutex, RwLock as StdRwLock}, }; use eyeball::Observable; @@ -289,23 +289,21 @@ impl SlidingSyncBuilder { let rooms = StdRwLock::new(rooms_found); let lists = StdRwLock::new(self.lists); - Ok(SlidingSync { - inner: Arc::new(SlidingSyncInner { - homeserver: self.homeserver, - client, - storage_key: self.storage_key, + Ok(SlidingSync::new(SlidingSyncInner { + homeserver: self.homeserver, + client, + storage_key: self.storage_key, - lists, - rooms, + lists, + rooms, - extensions: Mutex::new(self.extensions).into(), - reset_counter: Default::default(), + extensions: Mutex::new(self.extensions), + reset_counter: Default::default(), - pos: StdRwLock::new(Observable::new(None)), - delta_token: StdRwLock::new(Observable::new(delta_token_inner)), - subscriptions: StdRwLock::new(self.subscriptions), - unsubscribe: Default::default(), - }), - }) + pos: StdRwLock::new(Observable::new(None)), + delta_token: StdRwLock::new(Observable::new(delta_token_inner)), + subscriptions: StdRwLock::new(self.subscriptions), + unsubscribe: Default::default(), + })) } } diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 725a0aec0..567c52ac4 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -597,6 +597,7 @@ mod list; mod room; use std::{ + borrow::BorrowMut, collections::BTreeMap, fmt::Debug, mem, @@ -614,6 +615,7 @@ use eyeball::Observable; use futures_core::stream::Stream; pub use list::*; use matrix_sdk_base::sync::SyncResponse; +use matrix_sdk_common::locks::Mutex as AsyncMutex; pub use room::*; use ruma::{ api::client::{ @@ -625,6 +627,7 @@ use ruma::{ assign, OwnedRoomId, RoomId, }; use serde::{Deserialize, Serialize}; +use tokio::spawn; use tracing::{debug, error, info_span, instrument, trace, warn, Instrument, Span}; use url::Url; use uuid::Uuid; @@ -641,9 +644,15 @@ use crate::{config::RequestConfig, Client, Result}; const MAXIMUM_SLIDING_SYNC_SESSION_EXPIRATION: u8 = 3; /// The Sliding Sync instance. +/// +/// It is OK to clone this type as much as you need: cloning it is cheap. #[derive(Clone, Debug)] pub struct SlidingSync { + /// The Sliding Sync data. inner: Arc, + + /// A lock to ensure that responses are handled one at a time. + response_handling_lock: Arc>, } #[derive(Debug)] @@ -680,6 +689,10 @@ pub(super) struct SlidingSyncInner { } impl SlidingSync { + pub(super) fn new(inner: SlidingSyncInner) -> Self { + Self { inner: Arc::new(inner), response_handling_lock: Arc::new(AsyncMutex::new(())) } + } + async fn cache_to_storage(&self) -> Result<(), crate::Error> { let Some(storage_key) = self.inner.storage_key.as_ref() else { return Ok(()) }; trace!(storage_key, "Saving to storage for later use"); @@ -979,11 +992,13 @@ impl SlidingSync { async fn sync_once( &self, stream_id: &str, - list_generators: &mut BTreeMap, + list_generators: Arc>>, ) -> Result> { let mut lists = BTreeMap::new(); { + let mut list_generators_lock = list_generators.lock().unwrap(); + let list_generators = list_generators_lock.borrow_mut(); let mut lists_to_remove = Vec::new(); for (name, generator) in list_generators.iter_mut() { @@ -997,10 +1012,10 @@ impl SlidingSync { for list_name in lists_to_remove { list_generators.remove(&list_name); } - } - if list_generators.is_empty() { - return Ok(None); + if list_generators.is_empty() { + return Ok(None); + } } let pos = self.inner.pos.read().unwrap().clone(); @@ -1067,43 +1082,57 @@ impl SlidingSync { // corrupted/incomplete states for Sliding Sync and other parts of // the code. // - // That's why we are running the handling of the response in a blocking - // mode since it cannot be cancelled abruptly. - debug!("Sliding sync response received"); + // That's why we are running the handling of the response in a spawned + // future that cannot be cancelled by anything. + let this = self.clone(); + let stream_id = stream_id.to_owned(); - match &response.txn_id { - None => { - error!(stream_id, "Sliding Sync has received an unexpected response: `txn_id` must match `stream_id`; it's missing"); + // Spawn a new future to ensure that the code inside this future cannot be + // cancelled if this method is cancelled. + spawn(async move { + debug!("Sliding sync response received"); + + // In case the task running this future is detached, we must be + // ensure responses are handled one at a time, hence we lock the + // `response_handling_lock`. + let global_lock = this.response_handling_lock.lock().await; + + match &response.txn_id { + None => { + error!(stream_id, "Sliding Sync has received an unexpected response: `txn_id` must match `stream_id`; it's missing"); + } + + Some(txn_id) if txn_id != &stream_id => { + error!( + stream_id, + txn_id, + "Sliding Sync has received an unexpected response: `txn_id` must match `stream_id`; they differ" + ); + } + + _ => {} } - Some(txn_id) if txn_id != stream_id => { - error!( - stream_id, - txn_id, - "Sliding Sync has received an unexpected response: `txn_id` must match `stream_id`; they differ" - ); - } + // Handle and transform a Sliding Sync Response to a `SyncResponse`. + // + // We may not need the `sync_response` in the future (once `SyncResponse` will + // move to Sliding Sync, i.e. to `v4::Response`), but processing the + // `sliding_sync_response` is vital, so it must be done somewhere; for now it + // happens here. + let sync_response = this.inner.client.process_sliding_sync(&response).await?; - _ => {} - } + debug!("Sliding sync response has been processed"); - // Handle and transform a Sliding Sync Response to a `SyncResponse`. - // - // We may not need the `sync_response` in the future (once `SyncResponse` will - // move to Sliding Sync, i.e. to `v4::Response`), but processing the - // `sliding_sync_response` is vital, so it must be done somewhere; for now it - // happens here. - let sync_response = self.inner.client.process_sliding_sync(&response).await?; + let updates = this.handle_response(response, sync_response, list_generators.lock().unwrap().borrow_mut())?; - debug!("Sliding sync response has been processed"); + this.cache_to_storage().await?; - let updates = self.handle_response(response, sync_response, list_generators)?; + drop(global_lock); - self.cache_to_storage().await?; + debug!("Sliding sync response has been handled"); - debug!("Sliding sync response has been handled"); - - Ok(Some(updates)) + Ok(Some(updates)) + }).await.unwrap() } /// Create a _new_ Sliding Sync stream. @@ -1113,7 +1142,7 @@ impl SlidingSync { #[instrument(name = "sync_stream", skip_all, parent = &self.inner.client.root_span)] pub fn stream(&self) -> impl Stream> + '_ { // Collect all the lists that need to be updated. - let mut list_generators = { + let list_generators = { let mut list_generators = BTreeMap::new(); let lock = self.inner.lists.read().unwrap(); @@ -1129,6 +1158,7 @@ impl SlidingSync { debug!(?self.inner.extensions, stream_id, "About to run the sync stream"); let instrument_span = Span::current(); + let list_generators = Arc::new(Mutex::new(list_generators)); async_stream::stream! { loop { @@ -1138,7 +1168,7 @@ impl SlidingSync { debug!(?self.inner.extensions, "Sync stream loop is running"); }); - match self.sync_once(&stream_id, &mut list_generators).instrument(sync_span.clone()).await { + match self.sync_once(&stream_id, list_generators.clone()).instrument(sync_span.clone()).await { Ok(Some(updates)) => { self.inner.reset_counter.store(0, Ordering::SeqCst); From 2f637cda6f33536990cb074d73e438fa8310da87 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 8 Mar 2023 15:33:40 +0100 Subject: [PATCH 157/166] doc(sdk): Add note about fairness of the SS lock. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 567c52ac4..f502949e0 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -652,6 +652,8 @@ pub struct SlidingSync { inner: Arc, /// A lock to ensure that responses are handled one at a time. + /// [The lock][AsyncMutex] is fair, and this fairness property is important + /// to ensure responses are handled in the correct order. response_handling_lock: Arc>, } From 26e259455a05815a60c52ece2738a759fe178bae Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 8 Mar 2023 11:30:45 +0100 Subject: [PATCH 158/166] Adhere to module naming conventions --- .../matrix-sdk-indexeddb/src/crypto_store.rs | 227 ++++++----- .../src/state_store/migrations.rs | 116 +++--- .../src/state_store/mod.rs | 361 +++++++++--------- 3 files changed, 349 insertions(+), 355 deletions(-) diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store.rs b/crates/matrix-sdk-indexeddb/src/crypto_store.rs index ca26cd7b4..cbb60e18e 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store.rs @@ -40,9 +40,8 @@ use web_sys::IdbKeyRange; use crate::safe_encode::SafeEncode; -#[allow(non_snake_case)] -mod KEYS { - // STORES +mod keys { + // stores pub const CORE: &str = "core"; pub const SESSION: &str = "session"; @@ -61,12 +60,12 @@ mod KEYS { pub const SECRET_REQUESTS_BY_INFO: &str = "secret_requests_by_info"; pub const KEY_REQUEST: &str = "key_request"; - // KEYS + // keys pub const STORE_CIPHER: &str = "store_cipher"; pub const ACCOUNT: &str = "account"; pub const PRIVATE_IDENTITY: &str = "private_identity"; - // BACKUP v1 + // backup v1 pub const BACKUP_KEYS: &str = "backup_keys"; pub const BACKUP_KEY_V1: &str = "backup_key_v1"; pub const RECOVERY_KEY_V1: &str = "recovery_key_v1"; @@ -151,21 +150,21 @@ impl IndexeddbCryptoStore { // migrating to version 1 let db = evt.db(); - db.create_object_store(KEYS::CORE)?; - db.create_object_store(KEYS::SESSION)?; + db.create_object_store(keys::CORE)?; + db.create_object_store(keys::SESSION)?; - db.create_object_store(KEYS::INBOUND_GROUP_SESSIONS)?; - db.create_object_store(KEYS::OUTBOUND_GROUP_SESSIONS)?; - db.create_object_store(KEYS::TRACKED_USERS)?; - db.create_object_store(KEYS::OLM_HASHES)?; - db.create_object_store(KEYS::DEVICES)?; + db.create_object_store(keys::INBOUND_GROUP_SESSIONS)?; + db.create_object_store(keys::OUTBOUND_GROUP_SESSIONS)?; + db.create_object_store(keys::TRACKED_USERS)?; + db.create_object_store(keys::OLM_HASHES)?; + db.create_object_store(keys::DEVICES)?; - db.create_object_store(KEYS::IDENTITIES)?; - db.create_object_store(KEYS::OUTGOING_SECRET_REQUESTS)?; - db.create_object_store(KEYS::UNSENT_SECRET_REQUESTS)?; - db.create_object_store(KEYS::SECRET_REQUESTS_BY_INFO)?; + db.create_object_store(keys::IDENTITIES)?; + db.create_object_store(keys::OUTGOING_SECRET_REQUESTS)?; + db.create_object_store(keys::UNSENT_SECRET_REQUESTS)?; + db.create_object_store(keys::SECRET_REQUESTS_BY_INFO)?; - db.create_object_store(KEYS::BACKUP_KEYS)?; + db.create_object_store(keys::BACKUP_KEYS)?; } else if old_version < 1.1 { // We changed how we store inbound group sessions, the key used to // be a trippled of `(room_id, sender_key, session_id)` now it's a @@ -175,8 +174,8 @@ impl IndexeddbCryptoStore { let db = evt.db(); - db.delete_object_store(KEYS::INBOUND_GROUP_SESSIONS)?; - db.create_object_store(KEYS::INBOUND_GROUP_SESSIONS)?; + db.delete_object_store(keys::INBOUND_GROUP_SESSIONS)?; + db.create_object_store(keys::INBOUND_GROUP_SESSIONS)?; } Ok(()) @@ -250,7 +249,7 @@ impl IndexeddbCryptoStore { let ob = tx.object_store("matrix-sdk-crypto")?; let store_cipher: Option> = ob - .get(&JsValue::from_str(KEYS::STORE_CIPHER))? + .get(&JsValue::from_str(keys::STORE_CIPHER))? .await? .map(|k| k.into_serde()) .transpose()?; @@ -272,7 +271,7 @@ impl IndexeddbCryptoStore { let ob = tx.object_store("matrix-sdk-crypto")?; ob.put_key_val( - &JsValue::from_str(KEYS::STORE_CIPHER), + &JsValue::from_str(keys::STORE_CIPHER), &JsValue::from_serde(&export.map_err(CryptoStoreError::backend)?)?, )?; tx.await.into_result()?; @@ -351,22 +350,22 @@ macro_rules! impl_crypto_store { impl_crypto_store! { async fn save_changes(&self, changes: Changes) -> Result<()> { let mut stores: Vec<&str> = [ - (changes.account.is_some() || changes.private_identity.is_some(), KEYS::CORE), - (changes.recovery_key.is_some() || changes.backup_version.is_some(), KEYS::BACKUP_KEYS), - (!changes.sessions.is_empty(), KEYS::SESSION), + (changes.account.is_some() || changes.private_identity.is_some(), keys::CORE), + (changes.recovery_key.is_some() || changes.backup_version.is_some(), keys::BACKUP_KEYS), + (!changes.sessions.is_empty(), keys::SESSION), ( !changes.devices.new.is_empty() || !changes.devices.changed.is_empty() || !changes.devices.deleted.is_empty(), - KEYS::DEVICES, + keys::DEVICES, ), ( !changes.identities.new.is_empty() || !changes.identities.changed.is_empty(), - KEYS::IDENTITIES, + keys::IDENTITIES, ), - (!changes.inbound_group_sessions.is_empty(), KEYS::INBOUND_GROUP_SESSIONS), - (!changes.outbound_group_sessions.is_empty(), KEYS::OUTBOUND_GROUP_SESSIONS), - (!changes.message_hashes.is_empty(), KEYS::OLM_HASHES), + (!changes.inbound_group_sessions.is_empty(), keys::INBOUND_GROUP_SESSIONS), + (!changes.outbound_group_sessions.is_empty(), keys::OUTBOUND_GROUP_SESSIONS), + (!changes.message_hashes.is_empty(), keys::OLM_HASHES), ] .iter() .filter_map(|(id, key)| if *id { Some(*key) } else { None }) @@ -374,9 +373,9 @@ impl_crypto_store! { if !changes.key_requests.is_empty() { stores.extend([ - KEYS::SECRET_REQUESTS_BY_INFO, - KEYS::UNSENT_SECRET_REQUESTS, - KEYS::OUTGOING_SECRET_REQUESTS, + keys::SECRET_REQUESTS_BY_INFO, + keys::UNSENT_SECRET_REQUESTS, + keys::OUTGOING_SECRET_REQUESTS, ]) } @@ -408,50 +407,50 @@ impl_crypto_store! { let backup_version = changes.backup_version; if let Some(a) = &account_pickle { - tx.object_store(KEYS::CORE)? - .put_key_val(&JsValue::from_str(KEYS::ACCOUNT), &self.serialize_value(&a)?)?; + tx.object_store(keys::CORE)? + .put_key_val(&JsValue::from_str(keys::ACCOUNT), &self.serialize_value(&a)?)?; } if let Some(i) = &private_identity_pickle { - tx.object_store(KEYS::CORE)?.put_key_val( - &JsValue::from_str(KEYS::PRIVATE_IDENTITY), + tx.object_store(keys::CORE)?.put_key_val( + &JsValue::from_str(keys::PRIVATE_IDENTITY), &self.serialize_value(i)?, )?; } if let Some(a) = &recovery_key_pickle { - tx.object_store(KEYS::BACKUP_KEYS)?.put_key_val( - &JsValue::from_str(KEYS::RECOVERY_KEY_V1), + tx.object_store(keys::BACKUP_KEYS)?.put_key_val( + &JsValue::from_str(keys::RECOVERY_KEY_V1), &self.serialize_value(&a)?, )?; } if let Some(a) = &backup_version { - tx.object_store(KEYS::BACKUP_KEYS)? - .put_key_val(&JsValue::from_str(KEYS::BACKUP_KEY_V1), &self.serialize_value(&a)?)?; + tx.object_store(keys::BACKUP_KEYS)? + .put_key_val(&JsValue::from_str(keys::BACKUP_KEY_V1), &self.serialize_value(&a)?)?; } if !changes.sessions.is_empty() { - let sessions = tx.object_store(KEYS::SESSION)?; + let sessions = tx.object_store(keys::SESSION)?; for session in &changes.sessions { let sender_key = session.sender_key().to_base64(); let session_id = session.session_id(); let pickle = session.pickle().await; - let key = self.encode_key(KEYS::SESSION, (&sender_key, session_id)); + let key = self.encode_key(keys::SESSION, (&sender_key, session_id)); sessions.put_key_val(&key, &self.serialize_value(&pickle)?)?; } } if !changes.inbound_group_sessions.is_empty() { - let sessions = tx.object_store(KEYS::INBOUND_GROUP_SESSIONS)?; + let sessions = tx.object_store(keys::INBOUND_GROUP_SESSIONS)?; for session in changes.inbound_group_sessions { let room_id = session.room_id(); let session_id = session.session_id(); - let key = self.encode_key(KEYS::INBOUND_GROUP_SESSIONS, (room_id, session_id)); + let key = self.encode_key(keys::INBOUND_GROUP_SESSIONS, (room_id, session_id)); let pickle = session.pickle().await; sessions.put_key_val(&key, &self.serialize_value(&pickle)?)?; @@ -459,13 +458,13 @@ impl_crypto_store! { } if !changes.outbound_group_sessions.is_empty() { - let sessions = tx.object_store(KEYS::OUTBOUND_GROUP_SESSIONS)?; + let sessions = tx.object_store(keys::OUTBOUND_GROUP_SESSIONS)?; for session in changes.outbound_group_sessions { let room_id = session.room_id(); let pickle = session.pickle().await; sessions.put_key_val( - &self.encode_key(KEYS::OUTBOUND_GROUP_SESSIONS, room_id), + &self.encode_key(keys::OUTBOUND_GROUP_SESSIONS, room_id), &self.serialize_value(&pickle)?, )?; } @@ -477,9 +476,9 @@ impl_crypto_store! { let key_requests = changes.key_requests; if !device_changes.new.is_empty() || !device_changes.changed.is_empty() { - let device_store = tx.object_store(KEYS::DEVICES)?; + let device_store = tx.object_store(keys::DEVICES)?; for device in device_changes.new.iter().chain(&device_changes.changed) { - let key = self.encode_key(KEYS::DEVICES, (device.user_id(), device.device_id())); + let key = self.encode_key(keys::DEVICES, (device.user_id(), device.device_id())); let device = self.serialize_value(&device)?; device_store.put_key_val(&key, &device)?; @@ -487,43 +486,43 @@ impl_crypto_store! { } if !device_changes.deleted.is_empty() { - let device_store = tx.object_store(KEYS::DEVICES)?; + let device_store = tx.object_store(keys::DEVICES)?; for device in &device_changes.deleted { - let key = self.encode_key(KEYS::DEVICES, (device.user_id(), device.device_id())); + let key = self.encode_key(keys::DEVICES, (device.user_id(), device.device_id())); device_store.delete(&key)?; } } if !identity_changes.changed.is_empty() || !identity_changes.new.is_empty() { - let identities = tx.object_store(KEYS::IDENTITIES)?; + let identities = tx.object_store(keys::IDENTITIES)?; for identity in identity_changes.changed.iter().chain(&identity_changes.new) { identities.put_key_val( - &self.encode_key(KEYS::IDENTITIES, identity.user_id()), + &self.encode_key(keys::IDENTITIES, identity.user_id()), &self.serialize_value(&identity)?, )?; } } if !olm_hashes.is_empty() { - let hashes = tx.object_store(KEYS::OLM_HASHES)?; + let hashes = tx.object_store(keys::OLM_HASHES)?; for hash in &olm_hashes { hashes.put_key_val( - &self.encode_key(KEYS::OLM_HASHES, (&hash.sender_key, &hash.hash)), + &self.encode_key(keys::OLM_HASHES, (&hash.sender_key, &hash.hash)), &JsValue::TRUE, )?; } } if !key_requests.is_empty() { - let secret_requests_by_info = tx.object_store(KEYS::SECRET_REQUESTS_BY_INFO)?; - let unsent_secret_requests = tx.object_store(KEYS::UNSENT_SECRET_REQUESTS)?; - let outgoing_secret_requests = tx.object_store(KEYS::OUTGOING_SECRET_REQUESTS)?; + let secret_requests_by_info = tx.object_store(keys::SECRET_REQUESTS_BY_INFO)?; + let unsent_secret_requests = tx.object_store(keys::UNSENT_SECRET_REQUESTS)?; + let outgoing_secret_requests = tx.object_store(keys::OUTGOING_SECRET_REQUESTS)?; for key_request in &key_requests { let key_request_id = - self.encode_key(KEYS::KEY_REQUEST, key_request.request_id.as_str()); + self.encode_key(keys::KEY_REQUEST, key_request.request_id.as_str()); secret_requests_by_info.put_key_val( - &self.encode_key(KEYS::KEY_REQUEST, key_request.info.as_key()), + &self.encode_key(keys::KEY_REQUEST, key_request.info.as_key()), &key_request_id, )?; @@ -552,8 +551,8 @@ impl_crypto_store! { async fn load_tracked_users(&self) -> Result> { let tx = self .inner - .transaction_on_one_with_mode(KEYS::TRACKED_USERS, IdbTransactionMode::Readonly)?; - let os = tx.object_store(KEYS::TRACKED_USERS)?; + .transaction_on_one_with_mode(keys::TRACKED_USERS, IdbTransactionMode::Readonly)?; + let os = tx.object_store(keys::TRACKED_USERS)?; let user_ids = os.get_all_keys()?.await?; let mut users = Vec::new(); @@ -577,11 +576,11 @@ impl_crypto_store! { if let Some(value) = self .inner .transaction_on_one_with_mode( - KEYS::OUTBOUND_GROUP_SESSIONS, + keys::OUTBOUND_GROUP_SESSIONS, IdbTransactionMode::Readonly, )? - .object_store(KEYS::OUTBOUND_GROUP_SESSIONS)? - .get(&self.encode_key(KEYS::OUTBOUND_GROUP_SESSIONS, room_id))? + .object_store(keys::OUTBOUND_GROUP_SESSIONS)? + .get(&self.encode_key(keys::OUTBOUND_GROUP_SESSIONS, room_id))? .await? { Ok(Some( @@ -603,11 +602,11 @@ impl_crypto_store! { ) -> Result> { // in this internal we expect key to already be escaped or encrypted let jskey = JsValue::from_str(request_id.as_str()); - let dbs = [KEYS::OUTGOING_SECRET_REQUESTS, KEYS::UNSENT_SECRET_REQUESTS]; + let dbs = [keys::OUTGOING_SECRET_REQUESTS, keys::UNSENT_SECRET_REQUESTS]; let tx = self.inner.transaction_on_multi_with_mode(&dbs, IdbTransactionMode::Readonly)?; let request = tx - .object_store(KEYS::OUTGOING_SECRET_REQUESTS)? + .object_store(keys::OUTGOING_SECRET_REQUESTS)? .get(&jskey)? .await? .map(|i| self.deserialize_value(i)) @@ -615,7 +614,7 @@ impl_crypto_store! { Ok(match request { None => tx - .object_store(KEYS::UNSENT_SECRET_REQUESTS)? + .object_store(keys::UNSENT_SECRET_REQUESTS)? .get(&jskey)? .await? .map(|i| self.deserialize_value(i)) @@ -627,9 +626,9 @@ impl_crypto_store! { async fn load_account(&self) -> Result> { if let Some(pickle) = self .inner - .transaction_on_one_with_mode(KEYS::CORE, IdbTransactionMode::Readonly)? - .object_store(KEYS::CORE)? - .get(&JsValue::from_str(KEYS::ACCOUNT))? + .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readonly)? + .object_store(keys::CORE)? + .get(&JsValue::from_str(keys::ACCOUNT))? .await? { let pickle = self.deserialize_value(pickle)?; @@ -658,9 +657,9 @@ impl_crypto_store! { async fn load_identity(&self) -> Result> { if let Some(pickle) = self .inner - .transaction_on_one_with_mode(KEYS::CORE, IdbTransactionMode::Readonly)? - .object_store(KEYS::CORE)? - .get(&JsValue::from_str(KEYS::PRIVATE_IDENTITY))? + .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readonly)? + .object_store(keys::CORE)? + .get(&JsValue::from_str(keys::PRIVATE_IDENTITY))? .await? { let pickle = self.deserialize_value(pickle)?; @@ -679,11 +678,11 @@ impl_crypto_store! { let account_info = self.get_account_info().ok_or(CryptoStoreError::AccountUnset)?; if self.session_cache.get(sender_key).is_none() { - let range = self.encode_to_range(KEYS::SESSION, sender_key)?; + let range = self.encode_to_range(keys::SESSION, sender_key)?; let sessions: Vec = self .inner - .transaction_on_one_with_mode(KEYS::SESSION, IdbTransactionMode::Readonly)? - .object_store(KEYS::SESSION)? + .transaction_on_one_with_mode(keys::SESSION, IdbTransactionMode::Readonly)? + .object_store(keys::SESSION)? .get_all_with_key(&range)? .await? .iter() @@ -709,14 +708,14 @@ impl_crypto_store! { room_id: &RoomId, session_id: &str, ) -> Result> { - let key = self.encode_key(KEYS::INBOUND_GROUP_SESSIONS, (room_id, session_id)); + let key = self.encode_key(keys::INBOUND_GROUP_SESSIONS, (room_id, session_id)); if let Some(pickle) = self .inner .transaction_on_one_with_mode( - KEYS::INBOUND_GROUP_SESSIONS, + keys::INBOUND_GROUP_SESSIONS, IdbTransactionMode::Readonly, )? - .object_store(KEYS::INBOUND_GROUP_SESSIONS)? + .object_store(keys::INBOUND_GROUP_SESSIONS)? .get(&key)? .await? { @@ -731,10 +730,10 @@ impl_crypto_store! { Ok(self .inner .transaction_on_one_with_mode( - KEYS::INBOUND_GROUP_SESSIONS, + keys::INBOUND_GROUP_SESSIONS, IdbTransactionMode::Readonly, )? - .object_store(KEYS::INBOUND_GROUP_SESSIONS)? + .object_store(keys::INBOUND_GROUP_SESSIONS)? .get_all()? .await? .iter() @@ -787,8 +786,8 @@ impl_crypto_store! { async fn save_tracked_users(&self, users: &[(&UserId, bool)]) -> Result<()> { let tx = self .inner - .transaction_on_one_with_mode(KEYS::TRACKED_USERS, IdbTransactionMode::Readwrite)?; - let os = tx.object_store(KEYS::TRACKED_USERS)?; + .transaction_on_one_with_mode(keys::TRACKED_USERS, IdbTransactionMode::Readwrite)?; + let os = tx.object_store(keys::TRACKED_USERS)?; for (user, dirty) in users { os.put_key_val(&JsValue::from_str(user.as_str()), &JsValue::from(*dirty))?; @@ -803,11 +802,11 @@ impl_crypto_store! { user_id: &UserId, device_id: &DeviceId, ) -> Result> { - let key = self.encode_key(KEYS::DEVICES, (user_id, device_id)); + let key = self.encode_key(keys::DEVICES, (user_id, device_id)); Ok(self .inner - .transaction_on_one_with_mode(KEYS::DEVICES, IdbTransactionMode::Readonly)? - .object_store(KEYS::DEVICES)? + .transaction_on_one_with_mode(keys::DEVICES, IdbTransactionMode::Readonly)? + .object_store(keys::DEVICES)? .get(&key)? .await? .map(|i| self.deserialize_value(i)) @@ -818,11 +817,11 @@ impl_crypto_store! { &self, user_id: &UserId, ) -> Result> { - let range = self.encode_to_range(KEYS::DEVICES, user_id)?; + let range = self.encode_to_range(keys::DEVICES, user_id)?; Ok(self .inner - .transaction_on_one_with_mode(KEYS::DEVICES, IdbTransactionMode::Readonly)? - .object_store(KEYS::DEVICES)? + .transaction_on_one_with_mode(keys::DEVICES, IdbTransactionMode::Readonly)? + .object_store(keys::DEVICES)? .get_all_with_key(&range)? .await? .iter() @@ -836,9 +835,9 @@ impl_crypto_store! { async fn get_user_identity(&self, user_id: &UserId) -> Result> { Ok(self .inner - .transaction_on_one_with_mode(KEYS::IDENTITIES, IdbTransactionMode::Readonly)? - .object_store(KEYS::IDENTITIES)? - .get(&self.encode_key(KEYS::IDENTITIES, user_id))? + .transaction_on_one_with_mode(keys::IDENTITIES, IdbTransactionMode::Readonly)? + .object_store(keys::IDENTITIES)? + .get(&self.encode_key(keys::IDENTITIES, user_id))? .await? .map(|i| self.deserialize_value(i)) .transpose()?) @@ -847,9 +846,9 @@ impl_crypto_store! { async fn is_message_known(&self, hash: &OlmMessageHash) -> Result { Ok(self .inner - .transaction_on_one_with_mode(KEYS::OLM_HASHES, IdbTransactionMode::Readonly)? - .object_store(KEYS::OLM_HASHES)? - .get(&self.encode_key(KEYS::OLM_HASHES, (&hash.sender_key, &hash.hash)))? + .transaction_on_one_with_mode(keys::OLM_HASHES, IdbTransactionMode::Readonly)? + .object_store(keys::OLM_HASHES)? + .get(&self.encode_key(keys::OLM_HASHES, (&hash.sender_key, &hash.hash)))? .await? .is_some()) } @@ -861,11 +860,11 @@ impl_crypto_store! { let id = self .inner .transaction_on_one_with_mode( - KEYS::SECRET_REQUESTS_BY_INFO, + keys::SECRET_REQUESTS_BY_INFO, IdbTransactionMode::Readonly, )? - .object_store(KEYS::SECRET_REQUESTS_BY_INFO)? - .get(&self.encode_key(KEYS::KEY_REQUEST, key_info.as_key()))? + .object_store(keys::SECRET_REQUESTS_BY_INFO)? + .get(&self.encode_key(keys::KEY_REQUEST, key_info.as_key()))? .await? .and_then(|i| i.as_string()); if let Some(id) = id { @@ -879,10 +878,10 @@ impl_crypto_store! { Ok(self .inner .transaction_on_one_with_mode( - KEYS::UNSENT_SECRET_REQUESTS, + keys::UNSENT_SECRET_REQUESTS, IdbTransactionMode::Readonly, )? - .object_store(KEYS::UNSENT_SECRET_REQUESTS)? + .object_store(keys::UNSENT_SECRET_REQUESTS)? .get_all()? .await? .iter() @@ -891,16 +890,16 @@ impl_crypto_store! { } async fn delete_outgoing_secret_requests(&self, request_id: &TransactionId) -> Result<()> { - let jskey = self.encode_key(KEYS::KEY_REQUEST, request_id); //.as_str()); + let jskey = self.encode_key(keys::KEY_REQUEST, request_id); //.as_str()); let dbs = [ - KEYS::OUTGOING_SECRET_REQUESTS, - KEYS::UNSENT_SECRET_REQUESTS, - KEYS::SECRET_REQUESTS_BY_INFO, + keys::OUTGOING_SECRET_REQUESTS, + keys::UNSENT_SECRET_REQUESTS, + keys::SECRET_REQUESTS_BY_INFO, ]; let tx = self.inner.transaction_on_multi_with_mode(&dbs, IdbTransactionMode::Readwrite)?; let request: Option = tx - .object_store(KEYS::OUTGOING_SECRET_REQUESTS)? + .object_store(keys::OUTGOING_SECRET_REQUESTS)? .get(&jskey)? .await? .map(|i| self.deserialize_value(i)) @@ -908,7 +907,7 @@ impl_crypto_store! { let request = match request { None => tx - .object_store(KEYS::UNSENT_SECRET_REQUESTS)? + .object_store(keys::UNSENT_SECRET_REQUESTS)? .get(&jskey)? .await? .map(|i| self.deserialize_value(i)) @@ -917,12 +916,12 @@ impl_crypto_store! { }; if let Some(inner) = request { - tx.object_store(KEYS::SECRET_REQUESTS_BY_INFO)? - .delete(&self.encode_key(KEYS::KEY_REQUEST, inner.info.as_key()))?; + tx.object_store(keys::SECRET_REQUESTS_BY_INFO)? + .delete(&self.encode_key(keys::KEY_REQUEST, inner.info.as_key()))?; } - tx.object_store(KEYS::UNSENT_SECRET_REQUESTS)?.delete(&jskey)?; - tx.object_store(KEYS::OUTGOING_SECRET_REQUESTS)?.delete(&jskey)?; + tx.object_store(keys::UNSENT_SECRET_REQUESTS)?.delete(&jskey)?; + tx.object_store(keys::OUTGOING_SECRET_REQUESTS)?.delete(&jskey)?; tx.await.into_result().map_err(|e| e.into()) } @@ -931,17 +930,17 @@ impl_crypto_store! { let key = { let tx = self .inner - .transaction_on_one_with_mode(KEYS::BACKUP_KEYS, IdbTransactionMode::Readonly)?; - let store = tx.object_store(KEYS::BACKUP_KEYS)?; + .transaction_on_one_with_mode(keys::BACKUP_KEYS, IdbTransactionMode::Readonly)?; + let store = tx.object_store(keys::BACKUP_KEYS)?; let backup_version = store - .get(&JsValue::from_str(KEYS::BACKUP_KEY_V1))? + .get(&JsValue::from_str(keys::BACKUP_KEY_V1))? .await? .map(|i| self.deserialize_value(i)) .transpose()?; let recovery_key = store - .get(&JsValue::from_str(KEYS::RECOVERY_KEY_V1))? + .get(&JsValue::from_str(keys::RECOVERY_KEY_V1))? .await? .map(|i| self.deserialize_value(i)) .transpose()?; diff --git a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs index ca8e76f37..146546716 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs @@ -28,7 +28,7 @@ use wasm_bindgen::JsValue; use web_sys::IdbTransactionMode; use super::{ - deserialize_event, encode_key, encode_to_range, serialize_event, Result, ALL_STORES, KEYS, + deserialize_event, encode_key, encode_to_range, keys, serialize_event, Result, ALL_STORES, }; use crate::IndexeddbStateStoreError; @@ -53,9 +53,7 @@ pub enum MigrationConflictStrategy { #[derive(Clone, Serialize, Deserialize)] struct StoreKeyWrapper(Vec); -#[allow(non_snake_case)] -mod OLD_KEYS { - // Old stores +mod old_keys { pub const SESSION: &str = "session"; pub const SYNC_TOKEN: &str = "sync_token"; } @@ -71,11 +69,11 @@ pub async fn upgrade_meta_db( let old_version = evt.old_version() as u32; if old_version < 1 { - db.create_object_store(KEYS::INTERNAL_STATE)?; + db.create_object_store(keys::INTERNAL_STATE)?; } if old_version < 2 { - db.create_object_store(KEYS::BACKUPS_META)?; + db.create_object_store(keys::BACKUPS_META)?; } Ok(()) @@ -85,11 +83,11 @@ pub async fn upgrade_meta_db( let store_cipher = if let Some(passphrase) = passphrase { let tx: IdbTransaction<'_> = meta_db - .transaction_on_one_with_mode(KEYS::INTERNAL_STATE, IdbTransactionMode::Readwrite)?; - let ob = tx.object_store(KEYS::INTERNAL_STATE)?; + .transaction_on_one_with_mode(keys::INTERNAL_STATE, IdbTransactionMode::Readwrite)?; + let ob = tx.object_store(keys::INTERNAL_STATE)?; let cipher = if let Some(StoreKeyWrapper(inner)) = ob - .get(&JsValue::from_str(KEYS::STORE_KEY))? + .get(&JsValue::from_str(keys::STORE_KEY))? .await? .map(|v| v.into_serde()) .transpose()? @@ -102,7 +100,7 @@ pub async fn upgrade_meta_db( #[cfg(test)] let export = cipher._insecure_export_fast_for_testing(passphrase)?; ob.put_key_val( - &JsValue::from_str(KEYS::STORE_KEY), + &JsValue::from_str(keys::STORE_KEY), &JsValue::from_serde(&StoreKeyWrapper(export))?, )?; cipher @@ -240,27 +238,27 @@ pub async fn upgrade_inner_db( } pub const V1_STORES: &[&str] = &[ - OLD_KEYS::SESSION, - KEYS::ACCOUNT_DATA, - KEYS::MEMBERS, - KEYS::PROFILES, - KEYS::DISPLAY_NAMES, - KEYS::JOINED_USER_IDS, - KEYS::INVITED_USER_IDS, - KEYS::ROOM_STATE, - KEYS::ROOM_INFOS, - KEYS::PRESENCE, - KEYS::ROOM_ACCOUNT_DATA, - KEYS::STRIPPED_ROOM_INFOS, - KEYS::STRIPPED_MEMBERS, - KEYS::STRIPPED_ROOM_STATE, - KEYS::STRIPPED_JOINED_USER_IDS, - KEYS::STRIPPED_INVITED_USER_IDS, - KEYS::ROOM_USER_RECEIPTS, - KEYS::ROOM_EVENT_RECEIPTS, - KEYS::MEDIA, - KEYS::CUSTOM, - OLD_KEYS::SYNC_TOKEN, + old_keys::SESSION, + keys::ACCOUNT_DATA, + keys::MEMBERS, + keys::PROFILES, + keys::DISPLAY_NAMES, + keys::JOINED_USER_IDS, + keys::INVITED_USER_IDS, + keys::ROOM_STATE, + keys::ROOM_INFOS, + keys::PRESENCE, + keys::ROOM_ACCOUNT_DATA, + keys::STRIPPED_ROOM_INFOS, + keys::STRIPPED_MEMBERS, + keys::STRIPPED_ROOM_STATE, + keys::STRIPPED_JOINED_USER_IDS, + keys::STRIPPED_INVITED_USER_IDS, + keys::ROOM_USER_RECEIPTS, + keys::ROOM_EVENT_RECEIPTS, + keys::MEDIA, + keys::CUSTOM, + old_keys::SYNC_TOKEN, ]; async fn backup_v1(source: &IdbDatabase, meta: &IdbDatabase) -> Result<()> { @@ -300,8 +298,8 @@ async fn backup_v1(source: &IdbDatabase, meta: &IdbDatabase) -> Result<()> { } let tx = - meta.transaction_on_one_with_mode(KEYS::BACKUPS_META, IdbTransactionMode::Readwrite)?; - let backup_store = tx.object_store(KEYS::BACKUPS_META)?; + meta.transaction_on_one_with_mode(keys::BACKUPS_META, IdbTransactionMode::Readwrite)?; + let backup_store = tx.object_store(keys::BACKUPS_META)?; backup_store.put_key_val(&JsValue::from_f64(now), &JsValue::from_str(&backup_name))?; tx.await; @@ -351,12 +349,12 @@ async fn v3_fix_store( /// Fix serialized redacted state events. async fn migrate_to_v3(db: &IdbDatabase, store_cipher: Option<&StoreCipher>) -> Result<()> { let tx = db.transaction_on_multi_with_mode( - &[KEYS::ROOM_STATE, KEYS::ROOM_INFOS], + &[keys::ROOM_STATE, keys::ROOM_INFOS], IdbTransactionMode::Readwrite, )?; - v3_fix_store(&tx.object_store(KEYS::ROOM_STATE)?, store_cipher).await?; - v3_fix_store(&tx.object_store(KEYS::ROOM_INFOS)?, store_cipher).await?; + v3_fix_store(&tx.object_store(keys::ROOM_STATE)?, store_cipher).await?; + v3_fix_store(&tx.object_store(keys::ROOM_INFOS)?, store_cipher).await?; tx.await.into_result().map_err(|e| e.into()) } @@ -367,14 +365,14 @@ async fn migrate_to_v4( store_cipher: Option<&StoreCipher>, ) -> Result { let tx = db.transaction_on_multi_with_mode( - &[OLD_KEYS::SYNC_TOKEN, OLD_KEYS::SESSION], + &[old_keys::SYNC_TOKEN, old_keys::SESSION], IdbTransactionMode::Readonly, )?; let mut values = Vec::new(); // Sync token - let sync_token_store = tx.object_store(OLD_KEYS::SYNC_TOKEN)?; - let sync_token = sync_token_store.get(&JsValue::from_str(OLD_KEYS::SYNC_TOKEN))?.await?; + let sync_token_store = tx.object_store(old_keys::SYNC_TOKEN)?; + let sync_token = sync_token_store.get(&JsValue::from_str(old_keys::SYNC_TOKEN))?.await?; if let Some(sync_token) = sync_token { values.push(( @@ -388,7 +386,7 @@ async fn migrate_to_v4( } // Filters - let session_store = tx.object_store(OLD_KEYS::SESSION)?; + let session_store = tx.object_store(old_keys::SESSION)?; let range = encode_to_range( store_cipher, StateStoreDataKey::Filter("").encoding_key(), @@ -406,12 +404,12 @@ async fn migrate_to_v4( let mut data = HashMap::new(); if !values.is_empty() { - data.insert(KEYS::KV, values); + data.insert(keys::KV, values); } Ok(OngoingMigration { - drop_stores: [OLD_KEYS::SYNC_TOKEN, OLD_KEYS::SESSION].into_iter().collect(), - create_stores: [KEYS::KV].into_iter().collect(), + drop_stores: [old_keys::SYNC_TOKEN, old_keys::SESSION].into_iter().collect(), + create_stores: [keys::KV].into_iter().collect(), data, }) } @@ -433,11 +431,11 @@ mod tests { use wasm_bindgen::JsValue; use super::{ - MigrationConflictStrategy, CURRENT_DB_VERSION, CURRENT_META_DB_VERSION, OLD_KEYS, V1_STORES, + old_keys, MigrationConflictStrategy, CURRENT_DB_VERSION, CURRENT_META_DB_VERSION, V1_STORES, }; use crate::{ safe_encode::SafeEncode, - state_store::{encode_key, serialize_event, Result, ALL_STORES, KEYS}, + state_store::{encode_key, keys, serialize_event, Result, ALL_STORES}, IndexeddbStateStore, IndexeddbStateStoreError, }; @@ -493,8 +491,8 @@ mod tests { { let db = create_fake_db(&name, 1).await?; let tx = - db.transaction_on_one_with_mode(KEYS::CUSTOM, IdbTransactionMode::Readwrite)?; - let custom = tx.object_store(KEYS::CUSTOM)?; + db.transaction_on_one_with_mode(keys::CUSTOM, IdbTransactionMode::Readwrite)?; + let custom = tx.object_store(keys::CUSTOM)?; let jskey = JsValue::from_str( core::str::from_utf8(CUSTOM_DATA_KEY).map_err(StoreError::Codec)?, ); @@ -531,8 +529,8 @@ mod tests { { let db = create_fake_db(&name, 1).await?; let tx = - db.transaction_on_one_with_mode(KEYS::CUSTOM, IdbTransactionMode::Readwrite)?; - let custom = tx.object_store(KEYS::CUSTOM)?; + db.transaction_on_one_with_mode(keys::CUSTOM, IdbTransactionMode::Readwrite)?; + let custom = tx.object_store(keys::CUSTOM)?; let jskey = JsValue::from_str( core::str::from_utf8(CUSTOM_DATA_KEY).map_err(StoreError::Codec)?, ); @@ -569,8 +567,8 @@ mod tests { { let db = create_fake_db(&name, 1).await?; let tx = - db.transaction_on_one_with_mode(KEYS::CUSTOM, IdbTransactionMode::Readwrite)?; - let custom = tx.object_store(KEYS::CUSTOM)?; + db.transaction_on_one_with_mode(keys::CUSTOM, IdbTransactionMode::Readwrite)?; + let custom = tx.object_store(keys::CUSTOM)?; let jskey = JsValue::from_str( core::str::from_utf8(CUSTOM_DATA_KEY).map_err(StoreError::Codec)?, ); @@ -610,8 +608,8 @@ mod tests { { let db = create_fake_db(&name, 1).await?; let tx = - db.transaction_on_one_with_mode(KEYS::CUSTOM, IdbTransactionMode::Readwrite)?; - let custom = tx.object_store(KEYS::CUSTOM)?; + db.transaction_on_one_with_mode(keys::CUSTOM, IdbTransactionMode::Readwrite)?; + let custom = tx.object_store(keys::CUSTOM)?; let jskey = JsValue::from_str( core::str::from_utf8(CUSTOM_DATA_KEY).map_err(StoreError::Codec)?, ); @@ -666,8 +664,8 @@ mod tests { { let db = create_fake_db(&name, 2).await?; let tx = - db.transaction_on_one_with_mode(KEYS::ROOM_STATE, IdbTransactionMode::Readwrite)?; - let state = tx.object_store(KEYS::ROOM_STATE)?; + db.transaction_on_one_with_mode(keys::ROOM_STATE, IdbTransactionMode::Readwrite)?; + let state = tx.object_store(keys::ROOM_STATE)?; let key = (room_id, StateEventType::RoomTopic, "").encode(); state.put_key_val(&key, &serialize_event(None, &wrong_redacted_state_event)?)?; tx.await.into_result()?; @@ -701,17 +699,17 @@ mod tests { { let db = create_fake_db(&name, 3).await?; let tx = db.transaction_on_multi_with_mode( - &[OLD_KEYS::SYNC_TOKEN, OLD_KEYS::SESSION], + &[old_keys::SYNC_TOKEN, old_keys::SESSION], IdbTransactionMode::Readwrite, )?; - let sync_token_store = tx.object_store(OLD_KEYS::SYNC_TOKEN)?; + let sync_token_store = tx.object_store(old_keys::SYNC_TOKEN)?; sync_token_store.put_key_val( - &JsValue::from_str(OLD_KEYS::SYNC_TOKEN), + &JsValue::from_str(old_keys::SYNC_TOKEN), &serialize_event(None, &sync_token)?, )?; - let session_store = tx.object_store(OLD_KEYS::SESSION)?; + let session_store = tx.object_store(old_keys::SESSION)?; session_store.put_key_val( &encode_key( None, diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index 94dc622d8..3dfd66c43 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -86,10 +86,7 @@ impl From for StoreError { } } -#[allow(non_snake_case)] -mod KEYS { - // STORES - +mod keys { pub const INTERNAL_STATE: &str = "matrix-sdk-state"; pub const BACKUPS_META: &str = "backups"; @@ -149,7 +146,7 @@ mod KEYS { pub const STORE_KEY: &str = "store_key"; } -pub use KEYS::ALL_STORES; +pub use keys::ALL_STORES; fn serialize_event(store_cipher: Option<&StoreCipher>, event: &impl Serialize) -> Result { Ok(match store_cipher { @@ -236,7 +233,7 @@ impl IndexeddbStateStoreBuilder { let migration_strategy = self.migration_conflict_strategy.clone(); let name = self.name.unwrap_or_else(|| "state".to_owned()); - let meta_name = format!("{name}::{}", KEYS::INTERNAL_STATE); + let meta_name = format!("{name}::{}", keys::INTERNAL_STATE); let (meta, store_cipher) = upgrade_meta_db(&meta_name, self.passphrase.as_deref()).await?; let inner = @@ -281,8 +278,8 @@ impl IndexeddbStateStore { pub async fn has_backups(&self) -> Result { Ok(self .meta - .transaction_on_one_with_mode(KEYS::BACKUPS_META, IdbTransactionMode::Readonly)? - .object_store(KEYS::BACKUPS_META)? + .transaction_on_one_with_mode(keys::BACKUPS_META, IdbTransactionMode::Readonly)? + .object_store(keys::BACKUPS_META)? .count()? .await? > 0) @@ -292,8 +289,8 @@ impl IndexeddbStateStore { pub async fn latest_backup(&self) -> Result> { Ok(self .meta - .transaction_on_one_with_mode(KEYS::BACKUPS_META, IdbTransactionMode::Readonly)? - .object_store(KEYS::BACKUPS_META)? + .transaction_on_one_with_mode(keys::BACKUPS_META, IdbTransactionMode::Readonly)? + .object_store(keys::BACKUPS_META)? .open_cursor_with_direction(indexed_db_futures::prelude::IdbCursorDirection::Prev)? .await? .and_then(|c| c.value().as_string())) @@ -330,11 +327,11 @@ impl IndexeddbStateStore { } pub async fn get_invited_user_ids_inner(&self, room_id: &RoomId) -> Result> { - let range = self.encode_to_range(KEYS::INVITED_USER_IDS, room_id)?; + let range = self.encode_to_range(keys::INVITED_USER_IDS, room_id)?; let entries = self .inner - .transaction_on_one_with_mode(KEYS::INVITED_USER_IDS, IdbTransactionMode::Readonly)? - .object_store(KEYS::INVITED_USER_IDS)? + .transaction_on_one_with_mode(keys::INVITED_USER_IDS, IdbTransactionMode::Readonly)? + .object_store(keys::INVITED_USER_IDS)? .get_all_with_key(&range)? .await? .iter() @@ -345,11 +342,11 @@ impl IndexeddbStateStore { } pub async fn get_joined_user_ids_inner(&self, room_id: &RoomId) -> Result> { - let range = self.encode_to_range(KEYS::JOINED_USER_IDS, room_id)?; + let range = self.encode_to_range(keys::JOINED_USER_IDS, room_id)?; Ok(self .inner - .transaction_on_one_with_mode(KEYS::JOINED_USER_IDS, IdbTransactionMode::Readonly)? - .object_store(KEYS::JOINED_USER_IDS)? + .transaction_on_one_with_mode(keys::JOINED_USER_IDS, IdbTransactionMode::Readonly)? + .object_store(keys::JOINED_USER_IDS)? .get_all_with_key(&range)? .await? .iter() @@ -369,14 +366,14 @@ impl IndexeddbStateStore { &self, room_id: &RoomId, ) -> Result> { - let range = self.encode_to_range(KEYS::STRIPPED_INVITED_USER_IDS, room_id)?; + let range = self.encode_to_range(keys::STRIPPED_INVITED_USER_IDS, room_id)?; let entries = self .inner .transaction_on_one_with_mode( - KEYS::STRIPPED_INVITED_USER_IDS, + keys::STRIPPED_INVITED_USER_IDS, IdbTransactionMode::Readonly, )? - .object_store(KEYS::STRIPPED_INVITED_USER_IDS)? + .object_store(keys::STRIPPED_INVITED_USER_IDS)? .get_all_with_key(&range)? .await? .iter() @@ -387,14 +384,14 @@ impl IndexeddbStateStore { } pub async fn get_stripped_joined_user_ids(&self, room_id: &RoomId) -> Result> { - let range = self.encode_to_range(KEYS::STRIPPED_JOINED_USER_IDS, room_id)?; + let range = self.encode_to_range(keys::STRIPPED_JOINED_USER_IDS, room_id)?; Ok(self .inner .transaction_on_one_with_mode( - KEYS::STRIPPED_JOINED_USER_IDS, + keys::STRIPPED_JOINED_USER_IDS, IdbTransactionMode::Readonly, )? - .object_store(KEYS::STRIPPED_JOINED_USER_IDS)? + .object_store(keys::STRIPPED_JOINED_USER_IDS)? .get_all_with_key(&range)? .await? .iter() @@ -404,8 +401,8 @@ impl IndexeddbStateStore { async fn get_custom_value_for_js(&self, jskey: &JsValue) -> Result>> { self.inner - .transaction_on_one_with_mode(KEYS::CUSTOM, IdbTransactionMode::Readonly)? - .object_store(KEYS::CUSTOM)? + .transaction_on_one_with_mode(keys::CUSTOM, IdbTransactionMode::Readonly)? + .object_store(keys::CUSTOM)? .get(jskey)? .await? .map(|f| self.deserialize_event(f)) @@ -419,7 +416,7 @@ impl IndexeddbStateStore { self.encode_key(key.encoding_key(), (key.encoding_key(), filter_name)) } StateStoreDataKey::UserAvatarUrl(user_id) => { - self.encode_key(KEYS::KV, (key.encoding_key(), user_id)) + self.encode_key(keys::KV, (key.encoding_key(), user_id)) } } } @@ -463,8 +460,8 @@ impl_state_store! { let value = self .inner - .transaction_on_one_with_mode(KEYS::KV, IdbTransactionMode::Readonly)? - .object_store(KEYS::KV)? + .transaction_on_one_with_mode(keys::KV, IdbTransactionMode::Readonly)? + .object_store(keys::KV)? .get(&encoded_key)? .await? .map(|f| self.deserialize_event::(f)) @@ -500,9 +497,9 @@ impl_state_store! { let tx = self .inner - .transaction_on_one_with_mode(KEYS::KV, IdbTransactionMode::Readwrite)?; + .transaction_on_one_with_mode(keys::KV, IdbTransactionMode::Readwrite)?; - let obj = tx.object_store(KEYS::KV)?; + let obj = tx.object_store(keys::KV)?; obj.put_key_val(&encoded_key, &self.serialize_event(&value)?)?; @@ -516,8 +513,8 @@ impl_state_store! { let tx = self .inner - .transaction_on_one_with_mode(KEYS::KV, IdbTransactionMode::Readwrite)?; - let obj = tx.object_store(KEYS::KV)?; + .transaction_on_one_with_mode(keys::KV, IdbTransactionMode::Readwrite)?; + let obj = tx.object_store(keys::KV)?; obj.delete(&encoded_key)?; @@ -528,53 +525,53 @@ impl_state_store! { async fn save_changes(&self, changes: &StateChanges) -> Result<()> { let mut stores: HashSet<&'static str> = [ - (changes.sync_token.is_some(), KEYS::KV), - (!changes.ambiguity_maps.is_empty(), KEYS::DISPLAY_NAMES), - (!changes.account_data.is_empty(), KEYS::ACCOUNT_DATA), - (!changes.presence.is_empty(), KEYS::PRESENCE), - (!changes.profiles.is_empty(), KEYS::PROFILES), - (!changes.room_account_data.is_empty(), KEYS::ROOM_ACCOUNT_DATA), - (!changes.receipts.is_empty(), KEYS::ROOM_EVENT_RECEIPTS), - (!changes.stripped_state.is_empty(), KEYS::STRIPPED_ROOM_STATE), + (changes.sync_token.is_some(), keys::KV), + (!changes.ambiguity_maps.is_empty(), keys::DISPLAY_NAMES), + (!changes.account_data.is_empty(), keys::ACCOUNT_DATA), + (!changes.presence.is_empty(), keys::PRESENCE), + (!changes.profiles.is_empty(), keys::PROFILES), + (!changes.room_account_data.is_empty(), keys::ROOM_ACCOUNT_DATA), + (!changes.receipts.is_empty(), keys::ROOM_EVENT_RECEIPTS), + (!changes.stripped_state.is_empty(), keys::STRIPPED_ROOM_STATE), ] .iter() .filter_map(|(id, key)| if *id { Some(*key) } else { None }) .collect(); if !changes.state.is_empty() { - stores.extend([KEYS::ROOM_STATE, KEYS::STRIPPED_ROOM_STATE]); + stores.extend([keys::ROOM_STATE, keys::STRIPPED_ROOM_STATE]); } if !changes.redactions.is_empty() { - stores.extend([KEYS::ROOM_STATE, KEYS::ROOM_INFOS]); + stores.extend([keys::ROOM_STATE, keys::ROOM_INFOS]); } if !changes.room_infos.is_empty() || !changes.stripped_room_infos.is_empty() { - stores.extend([KEYS::ROOM_INFOS, KEYS::STRIPPED_ROOM_INFOS]); + stores.extend([keys::ROOM_INFOS, keys::STRIPPED_ROOM_INFOS]); } if !changes.members.is_empty() { stores.extend([ - KEYS::PROFILES, - KEYS::MEMBERS, - KEYS::INVITED_USER_IDS, - KEYS::JOINED_USER_IDS, - KEYS::STRIPPED_MEMBERS, - KEYS::STRIPPED_INVITED_USER_IDS, - KEYS::STRIPPED_JOINED_USER_IDS, + keys::PROFILES, + keys::MEMBERS, + keys::INVITED_USER_IDS, + keys::JOINED_USER_IDS, + keys::STRIPPED_MEMBERS, + keys::STRIPPED_INVITED_USER_IDS, + keys::STRIPPED_JOINED_USER_IDS, ]) } if !changes.stripped_members.is_empty() { stores.extend([ - KEYS::STRIPPED_MEMBERS, - KEYS::STRIPPED_INVITED_USER_IDS, - KEYS::STRIPPED_JOINED_USER_IDS, + keys::STRIPPED_MEMBERS, + keys::STRIPPED_INVITED_USER_IDS, + keys::STRIPPED_JOINED_USER_IDS, ]) } if !changes.receipts.is_empty() { - stores.extend([KEYS::ROOM_EVENT_RECEIPTS, KEYS::ROOM_USER_RECEIPTS]) + stores.extend([keys::ROOM_EVENT_RECEIPTS, keys::ROOM_USER_RECEIPTS]) } if stores.is_empty() { @@ -587,17 +584,17 @@ impl_state_store! { self.inner.transaction_on_multi_with_mode(&stores, IdbTransactionMode::Readwrite)?; if let Some(s) = &changes.sync_token { - tx.object_store(KEYS::KV)?.put_key_val( + tx.object_store(keys::KV)?.put_key_val( &self.encode_kv_data_key(StateStoreDataKey::SyncToken), &self.serialize_event(s)?, )?; } if !changes.ambiguity_maps.is_empty() { - let store = tx.object_store(KEYS::DISPLAY_NAMES)?; + let store = tx.object_store(keys::DISPLAY_NAMES)?; for (room_id, ambiguity_maps) in &changes.ambiguity_maps { for (display_name, map) in ambiguity_maps { - let key = self.encode_key(KEYS::DISPLAY_NAMES, (room_id, display_name)); + let key = self.encode_key(keys::DISPLAY_NAMES, (room_id, display_name)); store.put_key_val(&key, &self.serialize_event(&map)?)?; } @@ -605,32 +602,32 @@ impl_state_store! { } if !changes.account_data.is_empty() { - let store = tx.object_store(KEYS::ACCOUNT_DATA)?; + let store = tx.object_store(keys::ACCOUNT_DATA)?; for (event_type, event) in &changes.account_data { store.put_key_val( - &self.encode_key(KEYS::ACCOUNT_DATA, event_type), + &self.encode_key(keys::ACCOUNT_DATA, event_type), &self.serialize_event(&event)?, )?; } } if !changes.room_account_data.is_empty() { - let store = tx.object_store(KEYS::ROOM_ACCOUNT_DATA)?; + let store = tx.object_store(keys::ROOM_ACCOUNT_DATA)?; for (room, events) in &changes.room_account_data { for (event_type, event) in events { - let key = self.encode_key(KEYS::ROOM_ACCOUNT_DATA, (room, event_type)); + let key = self.encode_key(keys::ROOM_ACCOUNT_DATA, (room, event_type)); store.put_key_val(&key, &self.serialize_event(&event)?)?; } } } if !changes.state.is_empty() { - let state = tx.object_store(KEYS::ROOM_STATE)?; - let stripped_state = tx.object_store(KEYS::STRIPPED_ROOM_STATE)?; + let state = tx.object_store(keys::ROOM_STATE)?; + let stripped_state = tx.object_store(keys::STRIPPED_ROOM_STATE)?; for (room, event_types) in &changes.state { for (event_type, events) in event_types { for (state_key, event) in events { - let key = self.encode_key(KEYS::ROOM_STATE, (room, event_type, state_key)); + let key = self.encode_key(keys::ROOM_STATE, (room, event_type, state_key)); state.put_key_val(&key, &self.serialize_event(&event)?)?; stripped_state.delete(&key)?; } @@ -639,43 +636,43 @@ impl_state_store! { } if !changes.room_infos.is_empty() { - let room_infos = tx.object_store(KEYS::ROOM_INFOS)?; - let stripped_room_infos = tx.object_store(KEYS::STRIPPED_ROOM_INFOS)?; + let room_infos = tx.object_store(keys::ROOM_INFOS)?; + let stripped_room_infos = tx.object_store(keys::STRIPPED_ROOM_INFOS)?; for (room_id, room_info) in &changes.room_infos { room_infos.put_key_val( - &self.encode_key(KEYS::ROOM_INFOS, room_id), + &self.encode_key(keys::ROOM_INFOS, room_id), &self.serialize_event(&room_info)?, )?; - stripped_room_infos.delete(&self.encode_key(KEYS::STRIPPED_ROOM_INFOS, room_id))?; + stripped_room_infos.delete(&self.encode_key(keys::STRIPPED_ROOM_INFOS, room_id))?; } } if !changes.presence.is_empty() { - let store = tx.object_store(KEYS::PRESENCE)?; + let store = tx.object_store(keys::PRESENCE)?; for (sender, event) in &changes.presence { store.put_key_val( - &self.encode_key(KEYS::PRESENCE, sender), + &self.encode_key(keys::PRESENCE, sender), &self.serialize_event(&event)?, )?; } } if !changes.stripped_room_infos.is_empty() { - let stripped_room_infos = tx.object_store(KEYS::STRIPPED_ROOM_INFOS)?; - let room_infos = tx.object_store(KEYS::ROOM_INFOS)?; + let stripped_room_infos = tx.object_store(keys::STRIPPED_ROOM_INFOS)?; + let room_infos = tx.object_store(keys::ROOM_INFOS)?; for (room_id, info) in &changes.stripped_room_infos { stripped_room_infos.put_key_val( - &self.encode_key(KEYS::STRIPPED_ROOM_INFOS, room_id), + &self.encode_key(keys::STRIPPED_ROOM_INFOS, room_id), &self.serialize_event(&info)?, )?; - room_infos.delete(&self.encode_key(KEYS::ROOM_INFOS, room_id))?; + room_infos.delete(&self.encode_key(keys::ROOM_INFOS, room_id))?; } } if !changes.stripped_members.is_empty() { - let store = tx.object_store(KEYS::STRIPPED_MEMBERS)?; - let joined = tx.object_store(KEYS::STRIPPED_JOINED_USER_IDS)?; - let invited = tx.object_store(KEYS::STRIPPED_INVITED_USER_IDS)?; + let store = tx.object_store(keys::STRIPPED_MEMBERS)?; + let joined = tx.object_store(keys::STRIPPED_JOINED_USER_IDS)?; + let invited = tx.object_store(keys::STRIPPED_INVITED_USER_IDS)?; for (room, raw_events) in &changes.stripped_members { for raw_event in raw_events.values() { let event = match raw_event.deserialize() { @@ -693,27 +690,27 @@ impl_state_store! { match event.content.membership { MembershipState::Join => { joined.put_key_val_owned( - &self.encode_key(KEYS::STRIPPED_JOINED_USER_IDS, key), + &self.encode_key(keys::STRIPPED_JOINED_USER_IDS, key), &self.serialize_event(&event.state_key)?, )?; invited - .delete(&self.encode_key(KEYS::STRIPPED_INVITED_USER_IDS, key))?; + .delete(&self.encode_key(keys::STRIPPED_INVITED_USER_IDS, key))?; } MembershipState::Invite => { invited.put_key_val_owned( - &self.encode_key(KEYS::STRIPPED_INVITED_USER_IDS, key), + &self.encode_key(keys::STRIPPED_INVITED_USER_IDS, key), &self.serialize_event(&event.state_key)?, )?; - joined.delete(&self.encode_key(KEYS::STRIPPED_JOINED_USER_IDS, key))?; + joined.delete(&self.encode_key(keys::STRIPPED_JOINED_USER_IDS, key))?; } _ => { - joined.delete(&self.encode_key(KEYS::STRIPPED_JOINED_USER_IDS, key))?; + joined.delete(&self.encode_key(keys::STRIPPED_JOINED_USER_IDS, key))?; invited - .delete(&self.encode_key(KEYS::STRIPPED_INVITED_USER_IDS, key))?; + .delete(&self.encode_key(keys::STRIPPED_INVITED_USER_IDS, key))?; } } store.put_key_val( - &self.encode_key(KEYS::STRIPPED_MEMBERS, key), + &self.encode_key(keys::STRIPPED_MEMBERS, key), &self.serialize_event(&raw_event)?, )?; } @@ -721,12 +718,12 @@ impl_state_store! { } if !changes.stripped_state.is_empty() { - let store = tx.object_store(KEYS::STRIPPED_ROOM_STATE)?; + let store = tx.object_store(keys::STRIPPED_ROOM_STATE)?; for (room, event_types) in &changes.stripped_state { for (event_type, events) in event_types { for (state_key, event) in events { let key = self - .encode_key(KEYS::STRIPPED_ROOM_STATE, (room, event_type, state_key)); + .encode_key(keys::STRIPPED_ROOM_STATE, (room, event_type, state_key)); store.put_key_val(&key, &self.serialize_event(&event)?)?; } } @@ -734,13 +731,13 @@ impl_state_store! { } if !changes.members.is_empty() { - let profiles = tx.object_store(KEYS::PROFILES)?; - let joined = tx.object_store(KEYS::JOINED_USER_IDS)?; - let invited = tx.object_store(KEYS::INVITED_USER_IDS)?; - let members = tx.object_store(KEYS::MEMBERS)?; - let stripped_members = tx.object_store(KEYS::STRIPPED_MEMBERS)?; - let stripped_joined = tx.object_store(KEYS::STRIPPED_JOINED_USER_IDS)?; - let stripped_invited = tx.object_store(KEYS::STRIPPED_INVITED_USER_IDS)?; + let profiles = tx.object_store(keys::PROFILES)?; + let joined = tx.object_store(keys::JOINED_USER_IDS)?; + let invited = tx.object_store(keys::INVITED_USER_IDS)?; + let members = tx.object_store(keys::MEMBERS)?; + let stripped_members = tx.object_store(keys::STRIPPED_MEMBERS)?; + let stripped_joined = tx.object_store(keys::STRIPPED_JOINED_USER_IDS)?; + let stripped_invited = tx.object_store(keys::STRIPPED_INVITED_USER_IDS)?; for (room, raw_events) in &changes.members { let profile_changes = changes.profiles.get(room); @@ -759,40 +756,40 @@ impl_state_store! { let key = (room, event.state_key()); stripped_joined - .delete(&self.encode_key(KEYS::STRIPPED_JOINED_USER_IDS, key))?; + .delete(&self.encode_key(keys::STRIPPED_JOINED_USER_IDS, key))?; stripped_invited - .delete(&self.encode_key(KEYS::STRIPPED_INVITED_USER_IDS, key))?; + .delete(&self.encode_key(keys::STRIPPED_INVITED_USER_IDS, key))?; match event.membership() { MembershipState::Join => { joined.put_key_val_owned( - &self.encode_key(KEYS::JOINED_USER_IDS, key), + &self.encode_key(keys::JOINED_USER_IDS, key), &self.serialize_event(event.state_key())?, )?; - invited.delete(&self.encode_key(KEYS::INVITED_USER_IDS, key))?; + invited.delete(&self.encode_key(keys::INVITED_USER_IDS, key))?; } MembershipState::Invite => { invited.put_key_val_owned( - &self.encode_key(KEYS::INVITED_USER_IDS, key), + &self.encode_key(keys::INVITED_USER_IDS, key), &self.serialize_event(event.state_key())?, )?; - joined.delete(&self.encode_key(KEYS::JOINED_USER_IDS, key))?; + joined.delete(&self.encode_key(keys::JOINED_USER_IDS, key))?; } _ => { - joined.delete(&self.encode_key(KEYS::JOINED_USER_IDS, key))?; - invited.delete(&self.encode_key(KEYS::INVITED_USER_IDS, key))?; + joined.delete(&self.encode_key(keys::JOINED_USER_IDS, key))?; + invited.delete(&self.encode_key(keys::INVITED_USER_IDS, key))?; } } members.put_key_val_owned( - &self.encode_key(KEYS::MEMBERS, key), + &self.encode_key(keys::MEMBERS, key), &self.serialize_event(&raw_event)?, )?; - stripped_members.delete(&self.encode_key(KEYS::STRIPPED_MEMBERS, key))?; + stripped_members.delete(&self.encode_key(keys::STRIPPED_MEMBERS, key))?; if let Some(profile) = profile_changes.and_then(|p| p.get(event.state_key())) { profiles.put_key_val_owned( - &self.encode_key(KEYS::PROFILES, key), + &self.encode_key(keys::PROFILES, key), &self.serialize_event(&profile)?, )?; } @@ -801,8 +798,8 @@ impl_state_store! { } if !changes.receipts.is_empty() { - let room_user_receipts = tx.object_store(KEYS::ROOM_USER_RECEIPTS)?; - let room_event_receipts = tx.object_store(KEYS::ROOM_EVENT_RECEIPTS)?; + let room_user_receipts = tx.object_store(keys::ROOM_USER_RECEIPTS)?; + let room_event_receipts = tx.object_store(keys::ROOM_EVENT_RECEIPTS)?; for (room, content) in &changes.receipts { for (event_id, receipts) in &content.0 { @@ -810,11 +807,11 @@ impl_state_store! { for (user_id, receipt) in receipts { let key = match receipt.thread.as_str() { Some(thread_id) => self.encode_key( - KEYS::ROOM_USER_RECEIPTS, + keys::ROOM_USER_RECEIPTS, (room, receipt_type, thread_id, user_id), ), None => self.encode_key( - KEYS::ROOM_USER_RECEIPTS, + keys::ROOM_USER_RECEIPTS, (room, receipt_type, user_id), ), }; @@ -826,11 +823,11 @@ impl_state_store! { { let key = match receipt.thread.as_str() { Some(thread_id) => self.encode_key( - KEYS::ROOM_EVENT_RECEIPTS, + keys::ROOM_EVENT_RECEIPTS, (room, receipt_type, thread_id, old_event, user_id), ), None => self.encode_key( - KEYS::ROOM_EVENT_RECEIPTS, + keys::ROOM_EVENT_RECEIPTS, (room, receipt_type, old_event, user_id), ), }; @@ -843,11 +840,11 @@ impl_state_store! { // Add the receipt to the room event receipts let key = match receipt.thread.as_str() { Some(thread_id) => self.encode_key( - KEYS::ROOM_EVENT_RECEIPTS, + keys::ROOM_EVENT_RECEIPTS, (room, receipt_type, thread_id, event_id, user_id), ), None => self.encode_key( - KEYS::ROOM_EVENT_RECEIPTS, + keys::ROOM_EVENT_RECEIPTS, (room, receipt_type, event_id, user_id), ), }; @@ -860,11 +857,11 @@ impl_state_store! { } if !changes.redactions.is_empty() { - let state = tx.object_store(KEYS::ROOM_STATE)?; - let room_info = tx.object_store(KEYS::ROOM_INFOS)?; + let state = tx.object_store(keys::ROOM_STATE)?; + let room_info = tx.object_store(keys::ROOM_INFOS)?; for (room_id, redactions) in &changes.redactions { - let range = self.encode_to_range(KEYS::ROOM_STATE, room_id)?; + let range = self.encode_to_range(keys::ROOM_STATE, room_id)?; let Some(cursor) = state.open_cursor_with_range(&range)?.await? else { continue }; let mut room_version = None; @@ -877,7 +874,7 @@ impl_state_store! { let version = { if room_version.is_none() { room_version.replace(room_info - .get(&self.encode_key(KEYS::ROOM_INFOS, room_id))? + .get(&self.encode_key(keys::ROOM_INFOS, room_id))? .await? .and_then(|f| self.deserialize_event::(f).ok()) .and_then(|info| info.room_version().cloned()) @@ -911,9 +908,9 @@ impl_state_store! { async fn get_presence_event(&self, user_id: &UserId) -> Result>> { self.inner - .transaction_on_one_with_mode(KEYS::PRESENCE, IdbTransactionMode::Readonly)? - .object_store(KEYS::PRESENCE)? - .get(&self.encode_key(KEYS::PRESENCE, user_id))? + .transaction_on_one_with_mode(keys::PRESENCE, IdbTransactionMode::Readonly)? + .object_store(keys::PRESENCE)? + .get(&self.encode_key(keys::PRESENCE, user_id))? .await? .map(|f| self.deserialize_event(f)) .transpose() @@ -926,9 +923,9 @@ impl_state_store! { state_key: &str, ) -> Result>> { self.inner - .transaction_on_one_with_mode(KEYS::ROOM_STATE, IdbTransactionMode::Readonly)? - .object_store(KEYS::ROOM_STATE)? - .get(&self.encode_key(KEYS::ROOM_STATE, (room_id, event_type, state_key)))? + .transaction_on_one_with_mode(keys::ROOM_STATE, IdbTransactionMode::Readonly)? + .object_store(keys::ROOM_STATE)? + .get(&self.encode_key(keys::ROOM_STATE, (room_id, event_type, state_key)))? .await? .map(|f| self.deserialize_event(f)) .transpose() @@ -939,11 +936,11 @@ impl_state_store! { room_id: &RoomId, event_type: StateEventType, ) -> Result>> { - let range = self.encode_to_range(KEYS::ROOM_STATE, (room_id, event_type))?; + let range = self.encode_to_range(keys::ROOM_STATE, (room_id, event_type))?; Ok(self .inner - .transaction_on_one_with_mode(KEYS::ROOM_STATE, IdbTransactionMode::Readonly)? - .object_store(KEYS::ROOM_STATE)? + .transaction_on_one_with_mode(keys::ROOM_STATE, IdbTransactionMode::Readonly)? + .object_store(keys::ROOM_STATE)? .get_all_with_key(&range)? .await? .iter() @@ -957,9 +954,9 @@ impl_state_store! { user_id: &UserId, ) -> Result>> { self.inner - .transaction_on_one_with_mode(KEYS::PROFILES, IdbTransactionMode::Readonly)? - .object_store(KEYS::PROFILES)? - .get(&self.encode_key(KEYS::PROFILES, (room_id, user_id)))? + .transaction_on_one_with_mode(keys::PROFILES, IdbTransactionMode::Readonly)? + .object_store(keys::PROFILES)? + .get(&self.encode_key(keys::PROFILES, (room_id, user_id)))? .await? .map(|f| self.deserialize_event(f)) .transpose() @@ -972,9 +969,9 @@ impl_state_store! { ) -> Result> { if let Some(e) = self .inner - .transaction_on_one_with_mode(KEYS::STRIPPED_MEMBERS, IdbTransactionMode::Readonly)? - .object_store(KEYS::STRIPPED_MEMBERS)? - .get(&self.encode_key(KEYS::STRIPPED_MEMBERS, (room_id, state_key)))? + .transaction_on_one_with_mode(keys::STRIPPED_MEMBERS, IdbTransactionMode::Readonly)? + .object_store(keys::STRIPPED_MEMBERS)? + .get(&self.encode_key(keys::STRIPPED_MEMBERS, (room_id, state_key)))? .await? .map(|f| self.deserialize_event(f)) .transpose()? @@ -982,9 +979,9 @@ impl_state_store! { Ok(Some(RawMemberEvent::Stripped(e))) } else if let Some(e) = self .inner - .transaction_on_one_with_mode(KEYS::MEMBERS, IdbTransactionMode::Readonly)? - .object_store(KEYS::MEMBERS)? - .get(&self.encode_key(KEYS::MEMBERS, (room_id, state_key)))? + .transaction_on_one_with_mode(keys::MEMBERS, IdbTransactionMode::Readonly)? + .object_store(keys::MEMBERS)? + .get(&self.encode_key(keys::MEMBERS, (room_id, state_key)))? .await? .map(|f| self.deserialize_event(f)) .transpose()? @@ -998,8 +995,8 @@ impl_state_store! { async fn get_room_infos(&self) -> Result> { let entries: Vec<_> = self .inner - .transaction_on_one_with_mode(KEYS::ROOM_INFOS, IdbTransactionMode::Readonly)? - .object_store(KEYS::ROOM_INFOS)? + .transaction_on_one_with_mode(keys::ROOM_INFOS, IdbTransactionMode::Readonly)? + .object_store(keys::ROOM_INFOS)? .get_all()? .await? .iter() @@ -1012,8 +1009,8 @@ impl_state_store! { async fn get_stripped_room_infos(&self) -> Result> { let entries = self .inner - .transaction_on_one_with_mode(KEYS::STRIPPED_ROOM_INFOS, IdbTransactionMode::Readonly)? - .object_store(KEYS::STRIPPED_ROOM_INFOS)? + .transaction_on_one_with_mode(keys::STRIPPED_ROOM_INFOS, IdbTransactionMode::Readonly)? + .object_store(keys::STRIPPED_ROOM_INFOS)? .get_all()? .await? .iter() @@ -1029,9 +1026,9 @@ impl_state_store! { display_name: &str, ) -> Result> { self.inner - .transaction_on_one_with_mode(KEYS::DISPLAY_NAMES, IdbTransactionMode::Readonly)? - .object_store(KEYS::DISPLAY_NAMES)? - .get(&self.encode_key(KEYS::DISPLAY_NAMES, (room_id, display_name)))? + .transaction_on_one_with_mode(keys::DISPLAY_NAMES, IdbTransactionMode::Readonly)? + .object_store(keys::DISPLAY_NAMES)? + .get(&self.encode_key(keys::DISPLAY_NAMES, (room_id, display_name)))? .await? .map(|f| self.deserialize_event::>(f)) .unwrap_or_else(|| Ok(Default::default())) @@ -1042,9 +1039,9 @@ impl_state_store! { event_type: GlobalAccountDataEventType, ) -> Result>> { self.inner - .transaction_on_one_with_mode(KEYS::ACCOUNT_DATA, IdbTransactionMode::Readonly)? - .object_store(KEYS::ACCOUNT_DATA)? - .get(&self.encode_key(KEYS::ACCOUNT_DATA, event_type))? + .transaction_on_one_with_mode(keys::ACCOUNT_DATA, IdbTransactionMode::Readonly)? + .object_store(keys::ACCOUNT_DATA)? + .get(&self.encode_key(keys::ACCOUNT_DATA, event_type))? .await? .map(|f| self.deserialize_event(f)) .transpose() @@ -1056,9 +1053,9 @@ impl_state_store! { event_type: RoomAccountDataEventType, ) -> Result>> { self.inner - .transaction_on_one_with_mode(KEYS::ROOM_ACCOUNT_DATA, IdbTransactionMode::Readonly)? - .object_store(KEYS::ROOM_ACCOUNT_DATA)? - .get(&self.encode_key(KEYS::ROOM_ACCOUNT_DATA, (room_id, event_type)))? + .transaction_on_one_with_mode(keys::ROOM_ACCOUNT_DATA, IdbTransactionMode::Readonly)? + .object_store(keys::ROOM_ACCOUNT_DATA)? + .get(&self.encode_key(keys::ROOM_ACCOUNT_DATA, (room_id, event_type)))? .await? .map(|f| self.deserialize_event(f)) .transpose() @@ -1073,12 +1070,12 @@ impl_state_store! { ) -> Result> { let key = match thread.as_str() { Some(thread_id) => self - .encode_key(KEYS::ROOM_USER_RECEIPTS, (room_id, receipt_type, thread_id, user_id)), - None => self.encode_key(KEYS::ROOM_USER_RECEIPTS, (room_id, receipt_type, user_id)), + .encode_key(keys::ROOM_USER_RECEIPTS, (room_id, receipt_type, thread_id, user_id)), + None => self.encode_key(keys::ROOM_USER_RECEIPTS, (room_id, receipt_type, user_id)), }; self.inner - .transaction_on_one_with_mode(KEYS::ROOM_USER_RECEIPTS, IdbTransactionMode::Readonly)? - .object_store(KEYS::ROOM_USER_RECEIPTS)? + .transaction_on_one_with_mode(keys::ROOM_USER_RECEIPTS, IdbTransactionMode::Readonly)? + .object_store(keys::ROOM_USER_RECEIPTS)? .get(&key)? .await? .map(|f| self.deserialize_event(f)) @@ -1094,18 +1091,18 @@ impl_state_store! { ) -> Result> { let range = match thread.as_str() { Some(thread_id) => self.encode_to_range( - KEYS::ROOM_EVENT_RECEIPTS, + keys::ROOM_EVENT_RECEIPTS, (room_id, receipt_type, thread_id, event_id), ), None => { - self.encode_to_range(KEYS::ROOM_EVENT_RECEIPTS, (room_id, receipt_type, event_id)) + self.encode_to_range(keys::ROOM_EVENT_RECEIPTS, (room_id, receipt_type, event_id)) } }?; let tx = self.inner.transaction_on_one_with_mode( - KEYS::ROOM_EVENT_RECEIPTS, + keys::ROOM_EVENT_RECEIPTS, IdbTransactionMode::Readonly, )?; - let store = tx.object_store(KEYS::ROOM_EVENT_RECEIPTS)?; + let store = tx.object_store(keys::ROOM_EVENT_RECEIPTS)?; Ok(store .get_all_with_key(&range)? @@ -1117,21 +1114,21 @@ impl_state_store! { async fn add_media_content(&self, request: &MediaRequest, data: Vec) -> Result<()> { let key = self - .encode_key(KEYS::MEDIA, (request.source.unique_key(), request.format.unique_key())); + .encode_key(keys::MEDIA, (request.source.unique_key(), request.format.unique_key())); let tx = - self.inner.transaction_on_one_with_mode(KEYS::MEDIA, IdbTransactionMode::Readwrite)?; + self.inner.transaction_on_one_with_mode(keys::MEDIA, IdbTransactionMode::Readwrite)?; - tx.object_store(KEYS::MEDIA)?.put_key_val(&key, &self.serialize_event(&data)?)?; + tx.object_store(keys::MEDIA)?.put_key_val(&key, &self.serialize_event(&data)?)?; tx.await.into_result().map_err(|e| e.into()) } async fn get_media_content(&self, request: &MediaRequest) -> Result>> { let key = self - .encode_key(KEYS::MEDIA, (request.source.unique_key(), request.format.unique_key())); + .encode_key(keys::MEDIA, (request.source.unique_key(), request.format.unique_key())); self.inner - .transaction_on_one_with_mode(KEYS::MEDIA, IdbTransactionMode::Readonly)? - .object_store(KEYS::MEDIA)? + .transaction_on_one_with_mode(keys::MEDIA, IdbTransactionMode::Readonly)? + .object_store(keys::MEDIA)? .get(&key)? .await? .map(|f| self.deserialize_event(f)) @@ -1149,9 +1146,9 @@ impl_state_store! { let prev = self.get_custom_value_for_js(&jskey).await?; let tx = - self.inner.transaction_on_one_with_mode(KEYS::CUSTOM, IdbTransactionMode::Readwrite)?; + self.inner.transaction_on_one_with_mode(keys::CUSTOM, IdbTransactionMode::Readwrite)?; - tx.object_store(KEYS::CUSTOM)?.put_key_val(&jskey, &self.serialize_event(&value)?)?; + tx.object_store(keys::CUSTOM)?.put_key_val(&jskey, &self.serialize_event(&value)?)?; tx.await.into_result().map_err(IndexeddbStateStoreError::from)?; Ok(prev) @@ -1163,9 +1160,9 @@ impl_state_store! { let prev = self.get_custom_value_for_js(&jskey).await?; let tx = - self.inner.transaction_on_one_with_mode(KEYS::CUSTOM, IdbTransactionMode::Readwrite)?; + self.inner.transaction_on_one_with_mode(keys::CUSTOM, IdbTransactionMode::Readwrite)?; - tx.object_store(KEYS::CUSTOM)?.delete(&jskey)?; + tx.object_store(keys::CUSTOM)?.delete(&jskey)?; tx.await.into_result().map_err(IndexeddbStateStoreError::from)?; Ok(prev) @@ -1173,20 +1170,20 @@ impl_state_store! { async fn remove_media_content(&self, request: &MediaRequest) -> Result<()> { let key = self - .encode_key(KEYS::MEDIA, (request.source.unique_key(), request.format.unique_key())); + .encode_key(keys::MEDIA, (request.source.unique_key(), request.format.unique_key())); let tx = - self.inner.transaction_on_one_with_mode(KEYS::MEDIA, IdbTransactionMode::Readwrite)?; + self.inner.transaction_on_one_with_mode(keys::MEDIA, IdbTransactionMode::Readwrite)?; - tx.object_store(KEYS::MEDIA)?.delete(&key)?; + tx.object_store(keys::MEDIA)?.delete(&key)?; tx.await.into_result().map_err(|e| e.into()) } async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> { - let range = self.encode_to_range(KEYS::MEDIA, uri)?; + let range = self.encode_to_range(keys::MEDIA, uri)?; let tx = - self.inner.transaction_on_one_with_mode(KEYS::MEDIA, IdbTransactionMode::Readwrite)?; - let store = tx.object_store(KEYS::MEDIA)?; + self.inner.transaction_on_one_with_mode(keys::MEDIA, IdbTransactionMode::Readwrite)?; + let store = tx.object_store(keys::MEDIA)?; for k in store.get_all_keys_with_key(&range)?.await?.iter() { store.delete(&k)?; @@ -1196,20 +1193,20 @@ impl_state_store! { } async fn remove_room(&self, room_id: &RoomId) -> Result<()> { - let direct_stores = [KEYS::ROOM_INFOS, KEYS::STRIPPED_ROOM_INFOS]; + let direct_stores = [keys::ROOM_INFOS, keys::STRIPPED_ROOM_INFOS]; let prefixed_stores = [ - KEYS::MEMBERS, - KEYS::PROFILES, - KEYS::DISPLAY_NAMES, - KEYS::INVITED_USER_IDS, - KEYS::JOINED_USER_IDS, - KEYS::ROOM_STATE, - KEYS::ROOM_ACCOUNT_DATA, - KEYS::ROOM_EVENT_RECEIPTS, - KEYS::ROOM_USER_RECEIPTS, - KEYS::STRIPPED_ROOM_STATE, - KEYS::STRIPPED_MEMBERS, + keys::MEMBERS, + keys::PROFILES, + keys::DISPLAY_NAMES, + keys::INVITED_USER_IDS, + keys::JOINED_USER_IDS, + keys::ROOM_STATE, + keys::ROOM_ACCOUNT_DATA, + keys::ROOM_EVENT_RECEIPTS, + keys::ROOM_USER_RECEIPTS, + keys::STRIPPED_ROOM_STATE, + keys::STRIPPED_MEMBERS, ]; let all_stores = { From 0d9d130833a3d4a9f602fecff4cb494674ebf39f Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 8 Mar 2023 13:41:46 +0100 Subject: [PATCH 159/166] Replace StateStoreDataKey::encoding_key by associated constants --- crates/matrix-sdk-base/src/store/traits.rs | 17 ++++++------- .../src/state_store/migrations.rs | 25 ++++--------------- .../src/state_store/mod.rs | 11 +++++--- .../src/state_store/migrations.rs | 18 +++++-------- crates/matrix-sdk-sled/src/state_store/mod.rs | 6 ++--- 5 files changed, 30 insertions(+), 47 deletions(-) diff --git a/crates/matrix-sdk-base/src/store/traits.rs b/crates/matrix-sdk-base/src/store/traits.rs index 00ed28d39..972dc0e2b 100644 --- a/crates/matrix-sdk-base/src/store/traits.rs +++ b/crates/matrix-sdk-base/src/store/traits.rs @@ -689,13 +689,12 @@ pub enum StateStoreDataKey<'a> { UserAvatarUrl(&'a UserId), } -impl<'a> StateStoreDataKey<'a> { - /// The string to use to encode this key. - pub const fn encoding_key(&self) -> &str { - match self { - Self::SyncToken => "sync_token", - Self::Filter(_) => "filter", - Self::UserAvatarUrl(_) => "user_avatar_url", - } - } +impl StateStoreDataKey<'_> { + /// Key to use for the [`SyncToken`][Self::SyncToken] variant. + pub const SYNC_TOKEN: &str = "sync_token"; + /// Key prefix to use for the [`Filter`][Self::Filter] variant. + pub const FILTER: &str = "filter"; + /// Key prefix to use for the [`UserAvatarUrl`][Self::UserAvatarUrl] + /// variant. + pub const USER_AVATAR_URL: &str = "user_avatar_url"; } diff --git a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs index 146546716..f9db21cbb 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs @@ -376,22 +376,15 @@ async fn migrate_to_v4( if let Some(sync_token) = sync_token { values.push(( - encode_key( - store_cipher, - StateStoreDataKey::SyncToken.encoding_key(), - StateStoreDataKey::SyncToken.encoding_key(), - ), + encode_key(store_cipher, StateStoreDataKey::SYNC_TOKEN, StateStoreDataKey::SYNC_TOKEN), sync_token, )); } // Filters let session_store = tx.object_store(old_keys::SESSION)?; - let range = encode_to_range( - store_cipher, - StateStoreDataKey::Filter("").encoding_key(), - StateStoreDataKey::Filter("").encoding_key(), - )?; + let range = + encode_to_range(store_cipher, StateStoreDataKey::FILTER, StateStoreDataKey::FILTER)?; if let Some(cursor) = session_store.open_cursor_with_range(&range)?.await? { while let Some(key) = cursor.key() { let value = cursor.value(); @@ -711,19 +704,11 @@ mod tests { let session_store = tx.object_store(old_keys::SESSION)?; session_store.put_key_val( - &encode_key( - None, - StateStoreDataKey::Filter("").encoding_key(), - (StateStoreDataKey::Filter("").encoding_key(), filter_1), - ), + &encode_key(None, StateStoreDataKey::FILTER, (StateStoreDataKey::FILTER, filter_1)), &serialize_event(None, &filter_1_id)?, )?; session_store.put_key_val( - &encode_key( - None, - StateStoreDataKey::Filter("").encoding_key(), - (StateStoreDataKey::Filter("").encoding_key(), filter_2), - ), + &encode_key(None, StateStoreDataKey::FILTER, (StateStoreDataKey::FILTER, filter_2)), &serialize_event(None, &filter_2_id)?, )?; diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index 3dfd66c43..fd8fbe01d 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -410,13 +410,18 @@ impl IndexeddbStateStore { } fn encode_kv_data_key(&self, key: StateStoreDataKey<'_>) -> JsValue { + // Use the key (prefix) for the table name as well, to keep encoded + // keys compatible for the sync token and filters, which were in + // separate tables initially. match key { - StateStoreDataKey::SyncToken => self.encode_key(key.encoding_key(), key.encoding_key()), + StateStoreDataKey::SyncToken => { + self.encode_key(StateStoreDataKey::SYNC_TOKEN, StateStoreDataKey::SYNC_TOKEN) + } StateStoreDataKey::Filter(filter_name) => { - self.encode_key(key.encoding_key(), (key.encoding_key(), filter_name)) + self.encode_key(StateStoreDataKey::FILTER, (StateStoreDataKey::FILTER, filter_name)) } StateStoreDataKey::UserAvatarUrl(user_id) => { - self.encode_key(keys::KV, (key.encoding_key(), user_id)) + self.encode_key(keys::KV, (StateStoreDataKey::USER_AVATAR_URL, user_id)) } } } diff --git a/crates/matrix-sdk-sled/src/state_store/migrations.rs b/crates/matrix-sdk-sled/src/state_store/migrations.rs index 0f3df7541..e399e5922 100644 --- a/crates/matrix-sdk-sled/src/state_store/migrations.rs +++ b/crates/matrix-sdk-sled/src/state_store/migrations.rs @@ -170,13 +170,13 @@ impl SledStateStore { let mut batch = sled::Batch::default(); // Sync token - let sync_token = session.get(StateStoreDataKey::SyncToken.encoding_key().encode())?; + let sync_token = session.get(StateStoreDataKey::SYNC_TOKEN.encode())?; if let Some(sync_token) = sync_token { - batch.insert(StateStoreDataKey::SyncToken.encoding_key().encode(), sync_token); + batch.insert(StateStoreDataKey::SYNC_TOKEN.encode(), sync_token); } // Filters - let key = self.encode_key(keys::SESSION, StateStoreDataKey::Filter("").encoding_key()); + let key = self.encode_key(keys::SESSION, StateStoreDataKey::FILTER); for res in session.scan_prefix(key) { let (key, value) = res?; batch.insert(key, value); @@ -396,21 +396,15 @@ mod test { let session = store.inner.open_tree(old_keys::SESSION).unwrap(); let mut batch = sled::Batch::default(); batch.insert( - StateStoreDataKey::SyncToken.encoding_key().encode(), + StateStoreDataKey::SYNC_TOKEN.encode(), store.serialize_value(&sync_token).unwrap(), ); batch.insert( - store.encode_key( - keys::SESSION, - (StateStoreDataKey::Filter("").encoding_key(), filter_1), - ), + store.encode_key(keys::SESSION, (StateStoreDataKey::FILTER, filter_1)), store.serialize_value(&filter_1_id).unwrap(), ); batch.insert( - store.encode_key( - keys::SESSION, - (StateStoreDataKey::Filter("").encoding_key(), filter_2), - ), + store.encode_key(keys::SESSION, (StateStoreDataKey::FILTER, filter_2)), store.serialize_value(&filter_2_id).unwrap(), ); session.apply_batch(batch).unwrap(); diff --git a/crates/matrix-sdk-sled/src/state_store/mod.rs b/crates/matrix-sdk-sled/src/state_store/mod.rs index ab3dc1501..e5d8f68cc 100644 --- a/crates/matrix-sdk-sled/src/state_store/mod.rs +++ b/crates/matrix-sdk-sled/src/state_store/mod.rs @@ -411,12 +411,12 @@ impl SledStateStore { fn encode_kv_data_key(&self, key: StateStoreDataKey<'_>) -> Vec { match key { - StateStoreDataKey::SyncToken => key.encoding_key().encode(), + StateStoreDataKey::SyncToken => StateStoreDataKey::SYNC_TOKEN.encode(), StateStoreDataKey::Filter(filter_name) => { - self.encode_key(keys::SESSION, (key.encoding_key(), filter_name)) + self.encode_key(keys::SESSION, (StateStoreDataKey::FILTER, filter_name)) } StateStoreDataKey::UserAvatarUrl(user_id) => { - self.encode_key(keys::SESSION, (key.encoding_key(), user_id)) + self.encode_key(keys::SESSION, (StateStoreDataKey::USER_AVATAR_URL, user_id)) } } } From aec65c818b19034b53ceeaab0e11676dffb2a6b2 Mon Sep 17 00:00:00 2001 From: kegsay Date: Wed, 8 Mar 2023 14:51:26 +0000 Subject: [PATCH 160/166] Update testing/sliding-sync-integration-test/src/lib.rs Co-authored-by: Ivan Enderlin --- testing/sliding-sync-integration-test/src/lib.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/testing/sliding-sync-integration-test/src/lib.rs b/testing/sliding-sync-integration-test/src/lib.rs index b55958e52..5fc3e0153 100644 --- a/testing/sliding-sync-integration-test/src/lib.rs +++ b/testing/sliding-sync-integration-test/src/lib.rs @@ -1385,11 +1385,11 @@ mod tests { .await .unwrap(); - for (user_id, _receipt_data) in receipts { - if user_id == client.user_id().unwrap() { - found_receipt = true; - break 'sync_loop; - } + let expected_user_id = client.user_id().unwrap(); + let found_receipt = receipts.iter().any(|(user_id, _)| user_id == expected_user_id); + + if found_receipt { + break; } } assert!(found_receipt); From 2c0466a8cc763a63291663c9fee9d97022e0d701 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 8 Mar 2023 15:03:41 +0000 Subject: [PATCH 161/166] Unbreak tests; don't shadow --- testing/sliding-sync-integration-test/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/testing/sliding-sync-integration-test/src/lib.rs b/testing/sliding-sync-integration-test/src/lib.rs index 5fc3e0153..c9bb0872a 100644 --- a/testing/sliding-sync-integration-test/src/lib.rs +++ b/testing/sliding-sync-integration-test/src/lib.rs @@ -1386,8 +1386,7 @@ mod tests { .unwrap(); let expected_user_id = client.user_id().unwrap(); - let found_receipt = receipts.iter().any(|(user_id, _)| user_id == expected_user_id); - + found_receipt = receipts.iter().any(|(user_id, _)| user_id == expected_user_id); if found_receipt { break; } From 5dd404d2f1238ffddb6d17cc3e27e95b51171062 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 8 Mar 2023 16:09:05 +0100 Subject: [PATCH 162/166] doc(sdk): Precise in which order SS responses will be handled. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index f502949e0..ec1a5d0bd 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -653,7 +653,7 @@ pub struct SlidingSync { /// A lock to ensure that responses are handled one at a time. /// [The lock][AsyncMutex] is fair, and this fairness property is important - /// to ensure responses are handled in the correct order. + /// to ensure responses are handled by their arrival order. response_handling_lock: Arc>, } From 417008cc87529440f2ddb007e383ebbeb76808cc Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 8 Mar 2023 15:14:59 +0000 Subject: [PATCH 163/166] Clippy --- testing/sliding-sync-integration-test/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/sliding-sync-integration-test/src/lib.rs b/testing/sliding-sync-integration-test/src/lib.rs index c9bb0872a..8aa4a883f 100644 --- a/testing/sliding-sync-integration-test/src/lib.rs +++ b/testing/sliding-sync-integration-test/src/lib.rs @@ -1375,7 +1375,7 @@ mod tests { // we expect to see it because we have enabled the receipt extension. We don't // know when we'll see it though let mut found_receipt = false; - 'sync_loop: for _n in 0..3 { + for _n in 0..3 { stream.next().await.context("sync has closed unexpectedly")??; // try to find it From ac863409bb27db8c4726778cb304508916b6b7b8 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 8 Mar 2023 16:20:50 +0100 Subject: [PATCH 164/166] doc(sdk): Remove notion of fairness. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index ec1a5d0bd..567c52ac4 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -652,8 +652,6 @@ pub struct SlidingSync { inner: Arc, /// A lock to ensure that responses are handled one at a time. - /// [The lock][AsyncMutex] is fair, and this fairness property is important - /// to ensure responses are handled by their arrival order. response_handling_lock: Arc>, } From 4996a19b31ad66cad10a89998c7d05abeccd799b Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 8 Mar 2023 16:53:27 +0100 Subject: [PATCH 165/166] feat(ffi): Rethink `TaskHandle`. With https://github.com/matrix-org/matrix-rust-sdk/pull/1601, `TaskHandle::with_callback` is no longer necessary. This patch updates `TaskHandle` as follows: 1. Before, the `handle` and `callback` fields were not mutually exclusive! Indeed, the `callback` field was used as an abort mechanism, but _also_ as a finalizer, i.e. a code that runs _after_ the cancellation of `handle`. Now that the callback-based solution is no longer used, we can keep one usage for this field and rename it `finalizer`. 2. The `handle` field is no longer optional, as it will always be set. 3. The `with_callback` method is renamed `new` as it's now the only constructor. 4. The `set_callback` method is renamed `set_finalizer`. Tadaaa. --- bindings/matrix-sdk-ffi/src/sliding_sync.rs | 48 +++++++++------------ 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/sliding_sync.rs b/bindings/matrix-sdk-ffi/src/sliding_sync.rs index bb6e2b3b2..072bee6fc 100644 --- a/bindings/matrix-sdk-ffi/src/sliding_sync.rs +++ b/bindings/matrix-sdk-ffi/src/sliding_sync.rs @@ -26,25 +26,22 @@ use crate::{ TimelineItem, TimelineListener, }; -type TaskHandleCallback = Box; +type TaskHandleFinalizer = Box; pub struct TaskHandle { - handle: Option>, - callback: RwLock>, + handle: JoinHandle<()>, + finalizer: RwLock>, } impl TaskHandle { - fn with_handle(handle: JoinHandle<()>) -> Self { - Self { handle: Some(handle), callback: Default::default() } + // Create a new task handle. + fn new(handle: JoinHandle<()>) -> Self { + Self { handle, finalizer: RwLock::new(None) } } - #[allow(dead_code)] - fn with_callback(callback: TaskHandleCallback) -> Self { - Self { handle: Default::default(), callback: RwLock::new(Some(callback)) } - } - - fn set_callback(&mut self, f: TaskHandleCallback) { - *self.callback.write().unwrap() = Some(f) + /// Define a function that will run after the handle has been aborted. + fn set_finalizer(&mut self, finalizer: TaskHandleFinalizer) { + *self.finalizer.write().unwrap() = Some(finalizer); } } @@ -53,19 +50,16 @@ impl TaskHandle { pub fn cancel(&self) { debug!("stoppable.cancel() called"); - if let Some(handle) = &self.handle { - handle.abort(); - } + self.handle.abort(); - if let Some(callback) = self.callback.write().unwrap().take() { - callback(); + if let Some(finalizer) = self.finalizer.write().unwrap().take() { + finalizer(); } } - /// Check whether a handle-based `TaskHandle` is finished; will return - /// `false` for callback-based `TaskHandle`. + /// Check whether the handle is finished. pub fn is_finished(&self) -> bool { - self.handle.as_ref().map(|handle| handle.is_finished()).unwrap_or_default() + self.handle.is_finished() } } @@ -180,7 +174,7 @@ impl SlidingSyncRoom { self.runner.subscribe(room_id.clone(), settings.map(Into::into)); let runner = self.runner.clone(); - stoppable_spawn.set_callback(Box::new(move || runner.unsubscribe(room_id))); + stoppable_spawn.set_finalizer(Box::new(move || runner.unsubscribe(room_id))); Ok(SlidingSyncSubscribeResult { items, task_handle: Arc::new(stoppable_spawn) }) } @@ -233,7 +227,7 @@ impl SlidingSyncRoom { }; let items = timeline_items.into_iter().map(TimelineItem::from_arc).collect(); - let task_handle = TaskHandle::with_handle(RUNTIME.spawn(async move { + let task_handle = TaskHandle::new(RUNTIME.spawn(async move { join(handle_events, handle_sliding_sync_reset).await; })); @@ -519,7 +513,7 @@ impl SlidingSyncList { ) -> Arc { let mut state_stream = self.inner.state_stream(); - Arc::new(TaskHandle::with_handle(RUNTIME.spawn(async move { + Arc::new(TaskHandle::new(RUNTIME.spawn(async move { loop { if let Some(new_state) = state_stream.next().await { observer.did_receive_update(new_state); @@ -534,7 +528,7 @@ impl SlidingSyncList { ) -> Arc { let mut rooms_list_stream = self.inner.rooms_list_stream(); - Arc::new(TaskHandle::with_handle(RUNTIME.spawn(async move { + Arc::new(TaskHandle::new(RUNTIME.spawn(async move { loop { if let Some(diff) = rooms_list_stream.next().await { observer.did_receive_update(diff.into()); @@ -549,7 +543,7 @@ impl SlidingSyncList { ) -> Arc { let mut rooms_updated = Observable::subscribe(&self.inner.rooms_updated_broadcast.read().unwrap()); - Arc::new(TaskHandle::with_handle(RUNTIME.spawn(async move { + Arc::new(TaskHandle::new(RUNTIME.spawn(async move { loop { if rooms_updated.next().await.is_some() { observer.did_receive_update(); @@ -564,7 +558,7 @@ impl SlidingSyncList { ) -> Arc { let mut rooms_count_stream = self.inner.rooms_count_stream(); - Arc::new(TaskHandle::with_handle(RUNTIME.spawn(async move { + Arc::new(TaskHandle::new(RUNTIME.spawn(async move { loop { if let Some(Some(new)) = rooms_count_stream.next().await { observer.did_receive_update(new); @@ -721,7 +715,7 @@ impl SlidingSync { let client = self.client.clone(); let observer = self.observer.clone(); - Arc::new(TaskHandle::with_handle(RUNTIME.spawn(async move { + Arc::new(TaskHandle::new(RUNTIME.spawn(async move { let stream = inner.stream(); pin_mut!(stream); From 508f1bc976eedf4f38247fbd0f0adfaf5c6c5f00 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 9 Mar 2023 10:57:20 +0000 Subject: [PATCH 166/166] Fix races between `/sync` and `/keys/queries` (#1619) The comments in the code should explain the general approach, but to give an overview: We assign a unique sequence number to each device-list-invalidation notice, and keep track of the sequence number at which each user was most recently invalidated. Then, when we build a `/keys/query` request, we take note of the current sequence number. Finally, when we get the response from that `/keys/query` request, we compare that sequence number with the most-recent-invalidation of each user that is now supposedly up-to-date. All of that means that we can see if the user has been invalidated *since* we built the `/keys/query` request, in which case our request may have raced against the new device, so we do not mark the user as up-to-date. To make that work, we need to keep track of the sequence number for in-flight `/keys/query` requests; we do that in the `IdentityManager`. Since there should only ever be at most one batch of requests in flight, that is relatively easy; however, we also record the request ids just to check that the application isn't doing something weird. There is no need to persist the sequence numbers to permanent storage: provided the `IdentityManager` and `Store` objects have a lifetime at least as long as an in-flight `/keys/query` request, it is sufficient just to retain the `dirty` bit in permanent storage, and then, when we reload the ephemeral `Store` cache, to treat everything with a `dirty` bit as a "new" invalidation. Fixes: #1386. --- .../src/identities/manager.rs | 210 +++++++++++------- crates/matrix-sdk-crypto/src/machine.rs | 23 +- crates/matrix-sdk-crypto/src/store/mod.rs | 189 +++++++++++++--- 3 files changed, 302 insertions(+), 120 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/identities/manager.rs b/crates/matrix-sdk-crypto/src/identities/manager.rs index 9c0f5f326..1943e4d23 100644 --- a/crates/matrix-sdk-crypto/src/identities/manager.rs +++ b/crates/matrix-sdk-crypto/src/identities/manager.rs @@ -22,11 +22,13 @@ use std::{ use futures_util::future::join_all; use matrix_sdk_common::{ executor::spawn, + locks::Mutex, timeout::{timeout, ElapsedError}, }; use ruma::{ api::client::keys::get_keys::v3::Response as KeysQueryResponse, serde::Raw, DeviceId, - OwnedDeviceId, OwnedServerName, OwnedUserId, ServerName, UserId, + OwnedDeviceId, OwnedServerName, OwnedTransactionId, OwnedUserId, ServerName, TransactionId, + UserId, }; use tracing::{debug, info, instrument, trace, warn}; @@ -89,7 +91,7 @@ impl KeysQueryListener { timeout: Duration, user: &UserId, ) -> Result { - let users_for_key_query = self.store.users_for_key_query().await.unwrap_or_default(); + let (users_for_key_query, _) = self.store.users_for_key_query().await.unwrap_or_default(); if users_for_key_query.contains(user) { if let Err(e) = self.wait(timeout).await { @@ -124,6 +126,22 @@ pub(crate) struct IdentityManager { keys_query_listener: KeysQueryListener, failures: FailuresCache, store: Store, + + /// Details of the current "in-flight" key query request, if any + keys_query_request_details: Arc>, +} + +/// Details of an in-flight key query request +#[derive(Debug, Clone, Default)] +struct KeysQueryRequestDetails { + /// The sequence number, to be passed to + /// `Store.mark_tracked_users_as_up_to_date`. + sequence_number: i64, + + /// A single batch of queries returned by the Store is broken up into one or + /// more actual KeysQueryRequests, each with their own request id. We + /// record the outstanding request ids here. + request_ids: HashSet, } impl IdentityManager { @@ -131,6 +149,7 @@ impl IdentityManager { pub fn new(user_id: Arc, device_id: Arc, store: Store) -> Self { let keys_query_listener = KeysQueryListener::new(store.clone()); + let keys_query_request_details = Mutex::new(KeysQueryRequestDetails::default()); IdentityManager { user_id, @@ -138,6 +157,7 @@ impl IdentityManager { store, keys_query_listener, failures: Default::default(), + keys_query_request_details: keys_query_request_details.into(), } } @@ -156,13 +176,16 @@ impl IdentityManager { /// /// # Arguments /// + /// * `request_id` - The request_id returned by users_for_key_query /// * `response` - The keys query response of the request that the client /// performed. pub async fn receive_keys_query_response( &self, + request_id: &TransactionId, response: &KeysQueryResponse, ) -> OlmResult<(DeviceChanges, IdentityChanges)> { debug!( + ?request_id, users = ?response.device_keys.keys().collect::>(), failures = ?response.failures, "Handling a keys query response" @@ -196,8 +219,22 @@ impl IdentityManager { }; self.store.save_changes(changes).await?; - self.mark_tracked_users_as_up_to_date(response.device_keys.keys().map(Deref::deref)) - .await?; + + // if this request is one of those we expected to be in flight, pass the + // sequence number back to the store so that it can mark devices up to + // date + let (sequence_number, removed) = { + let mut request_details = self.keys_query_request_details.lock().await; + (request_details.sequence_number, request_details.request_ids.remove(request_id)) + }; + if removed { + self.store + .mark_tracked_users_as_up_to_date( + response.device_keys.keys().map(Deref::deref), + sequence_number, + ) + .await?; + } let changed_devices = devices.changed.iter().fold(BTreeMap::new(), |mut acc, d| { acc.entry(d.user_id()).or_insert_with(BTreeSet::new).insert(d.device_id()); @@ -219,6 +256,7 @@ impl IdentityManager { identities.changed.iter().map(|i| i.user_id()).collect::>(); debug!( + ?request_id, ?new_devices, ?changed_devices, ?deleted_devices, @@ -644,44 +682,58 @@ impl IdentityManager { Ok((changes, changed_identity)) } - /// Get a key query request if one is needed. + /// Get a list of key query requests needed. /// - /// Returns a key query request if the client should query E2E keys, - /// otherwise None. + /// # Returns + /// + /// A list of pairs of `(request_id, request)` /// /// The response of a successful key query requests needs to be passed to /// the [`OlmMachine`] with the [`receive_keys_query_response`]. /// /// [`OlmMachine`]: struct.OlmMachine.html /// [`receive_keys_query_response`]: #method.receive_keys_query_response - pub async fn users_for_key_query(&self) -> StoreResult> { - let users = self.store.users_for_key_query().await?; + pub async fn users_for_key_query( + &self, + ) -> StoreResult> { + // forget about any previous key queries in flight + *(self.keys_query_request_details.lock().await) = KeysQueryRequestDetails::default(); + + let (users, sequence_number) = self.store.users_for_key_query().await?; // We always want to track our own user, but in case we aren't in an encrypted // room yet, we won't be tracking ourselves yet. This ensures we are always // tracking ourselves. // // The check for emptiness is done first for performance. - let users = + let (users, sequence_number) = if users.is_empty() && !self.store.tracked_users().await?.contains(self.user_id()) { self.store.mark_user_as_changed(self.user_id()).await?; self.store.users_for_key_query().await? } else { - users + (users, sequence_number) }; if users.is_empty() { - Ok(Vec::new()) - } else { - let users: Vec = - users.into_iter().filter(|u| !self.failures.contains(u.server_name())).collect(); - - Ok(users - .chunks(Self::MAX_KEY_QUERY_USERS) - .map(|u| u.iter().map(|u| (u.clone(), Vec::new())).collect()) - .map(KeysQueryRequest::new) - .collect()) + return Ok(Vec::new()); } + + let mut result: Vec<(OwnedTransactionId, KeysQueryRequest)> = Vec::new(); + let mut request_details = + KeysQueryRequestDetails { sequence_number, request_ids: HashSet::new() }; + + let filtered_users: Vec = + users.into_iter().filter(|u| !self.failures.contains(u.server_name())).collect(); + + for c in filtered_users.chunks(Self::MAX_KEY_QUERY_USERS) { + let request_id = TransactionId::new(); + let req = KeysQueryRequest::new(c.iter().map(|u| (u.clone(), Vec::new())).collect()); + debug!(?request_id, users = ?c, "Created keys query request"); + result.push((request_id.clone(), req)); + request_details.request_ids.insert(request_id); + } + *(self.keys_query_request_details.lock().await) = request_details; + Ok(result) } /// Receive the list of users that contained changed devices from the @@ -695,15 +747,7 @@ impl IdentityManager { &self, users: impl Iterator, ) -> StoreResult<()> { - let mut changed_user: Vec<(&UserId, bool)> = Vec::new(); - - for user_id in users { - if self.store.is_user_tracked(user_id).await? { - changed_user.push((user_id, true)) - } - } - - self.store.save_tracked_users(&changed_user).await + self.store.mark_tracked_users_as_changed(users).await } /// See the docs for [`OlmMachine::update_tracked_users()`]. @@ -711,23 +755,7 @@ impl IdentityManager { &self, users: impl IntoIterator, ) -> StoreResult<()> { - let mut tracked_users = Vec::new(); - - for user_id in users { - if !self.store.is_user_tracked(user_id).await? { - tracked_users.push((user_id, true)); - } - } - - self.store.save_tracked_users(&tracked_users).await - } - - pub async fn mark_tracked_users_as_up_to_date( - &self, - users: impl Iterator, - ) -> StoreResult<()> { - let updated_users: Vec<(&UserId, bool)> = users.map(|u| (u, false)).collect(); - self.store.save_tracked_users(&updated_users).await + self.store.update_tracked_users(users.into_iter()).await } } @@ -980,24 +1008,12 @@ pub(crate) mod tests { use matrix_sdk_test::{async_test, response_from_file}; use ruma::{ api::{client::keys::get_keys::v3::Response as KeysQueryResponse, IncomingResponse}, - device_id, user_id, + device_id, user_id, TransactionId, }; use serde_json::json; use super::testing::{device_id, key_query, manager, other_key_query, other_user_id, user_id}; - fn key_query_without_failures() -> KeysQueryResponse { - let response = json!({ - "device_keys": { - "@alice:example.org": { - }, - } - }); - - let response = response_from_file(&response); - - KeysQueryResponse::try_from_http_response(response).unwrap() - } fn key_query_with_failures() -> KeysQueryResponse { let response = json!({ "device_keys": { @@ -1026,11 +1042,11 @@ pub(crate) mod tests { ); manager.receive_device_changes([alice].iter().map(Deref::deref)).await.unwrap(); assert!( - !manager.store.is_user_tracked(alice).await.unwrap(), + !manager.store.tracked_users().await.unwrap().contains(alice), "Receiving a device changes update for a user we don't track does nothing" ); assert!( - !manager.store.users_for_key_query().await.unwrap().contains(alice), + !manager.store.users_for_key_query().await.unwrap().0.contains(alice), "The user we don't track doesn't end up in the `/keys/query` request" ); } @@ -1052,7 +1068,10 @@ pub(crate) mod tests { let task = tokio::task::spawn(async move { listener.wait(Duration::from_secs(10)).await }); - manager.receive_keys_query_response(&other_key_query()).await.unwrap(); + manager + .receive_keys_query_response(&TransactionId::new(), &other_key_query()) + .await + .unwrap(); task.await.unwrap().unwrap(); @@ -1085,7 +1104,10 @@ pub(crate) mod tests { let device_keys = manager.store.account().device_keys().await; manager - .receive_keys_query_response(&key_query(identity_request, device_keys)) + .receive_keys_query_response( + &TransactionId::new(), + &key_query(identity_request, device_keys), + ) .await .unwrap(); @@ -1120,6 +1142,37 @@ pub(crate) mod tests { ); } + /// If a user is invalidated while a /keys/query request is in flight, that + /// user is not removed from the list of outdated users when the + /// response is received + #[async_test] + async fn invalidation_race_handling() { + let manager = manager().await; + let alice = other_user_id(); + manager.update_tracked_users([alice]).await.unwrap(); + + // alice should be in the list of key queries + let (reqid, req) = manager.users_for_key_query().await.unwrap().pop().unwrap(); + assert!(req.device_keys.contains_key(alice)); + + // another invalidation turns up + manager.receive_device_changes([alice].into_iter()).await.unwrap(); + + // the response from the query arrives + manager.receive_keys_query_response(&reqid, &other_key_query()).await.unwrap(); + + // alice should *still* be in the list of key queries + let (reqid, req) = manager.users_for_key_query().await.unwrap().pop().unwrap(); + assert!(req.device_keys.contains_key(alice)); + + // another key query response + manager.receive_keys_query_response(&reqid, &other_key_query()).await.unwrap(); + + // finally alice should not be in the list + let queries = manager.users_for_key_query().await.unwrap(); + assert!(!queries.iter().any(|(_, r)| r.device_keys.contains_key(alice))); + } + #[async_test] async fn failure_handling() { let manager = manager().await; @@ -1135,40 +1188,27 @@ pub(crate) mod tests { manager.store.tracked_users().await.unwrap().contains(alice), "Alice is tracked after being marked as tracked" ); - assert!(manager - .users_for_key_query() - .await - .unwrap() - .iter() - .any(|r| r.device_keys.contains_key(alice))); + let (reqid, req) = manager.users_for_key_query().await.unwrap().pop().unwrap(); + assert!(req.device_keys.contains_key(alice)); + // a failure should stop us querying for the user's keys. let response = key_query_with_failures(); - - manager.receive_keys_query_response(&response).await.unwrap(); + manager.receive_keys_query_response(&reqid, &response).await.unwrap(); assert!(manager.failures.contains(alice.server_name())); assert!(!manager .users_for_key_query() .await .unwrap() .iter() - .any(|r| r.device_keys.contains_key(alice))); + .any(|(_, r)| r.device_keys.contains_key(alice))); - let response = key_query_without_failures(); - manager.receive_keys_query_response(&response).await.unwrap(); - assert!(!manager.failures.contains(alice.server_name())); - assert!(!manager - .users_for_key_query() - .await - .unwrap() - .iter() - .any(|r| r.device_keys.contains_key(alice))); - - manager.store.mark_user_as_changed(alice).await.unwrap(); + // clearing the failure flag should make the user reappear in the query list. + manager.failures.remove([alice.server_name().to_owned()].iter()); assert!(manager .users_for_key_query() .await .unwrap() .iter() - .any(|r| r.device_keys.contains_key(alice))); + .any(|(_, r)| r.device_keys.contains_key(alice))); } } diff --git a/crates/matrix-sdk-crypto/src/machine.rs b/crates/matrix-sdk-crypto/src/machine.rs index 1b2b84d99..fb8afe13a 100644 --- a/crates/matrix-sdk-crypto/src/machine.rs +++ b/crates/matrix-sdk-crypto/src/machine.rs @@ -342,9 +342,13 @@ impl OlmMachine { requests.push(r); } - for request in self.identity_manager.users_for_key_query().await?.into_iter().map(|r| { - OutgoingRequest { request_id: TransactionId::new(), request: Arc::new(r.into()) } - }) { + for request in self + .identity_manager + .users_for_key_query() + .await? + .into_iter() + .map(|(request_id, r)| OutgoingRequest { request_id, request: Arc::new(r.into()) }) + { requests.push(request); } @@ -373,7 +377,7 @@ impl OlmMachine { self.receive_keys_upload_response(response).await?; } IncomingResponse::KeysQuery(response) => { - self.receive_keys_query_response(response).await?; + self.receive_keys_query_response(request_id, response).await?; } IncomingResponse::KeysClaim(response) => { self.receive_keys_claim_response(response).await?; @@ -527,9 +531,10 @@ impl OlmMachine { /// performed. async fn receive_keys_query_response( &self, + request_id: &TransactionId, response: &KeysQueryResponse, ) -> OlmResult<(DeviceChanges, IdentityChanges)> { - self.identity_manager.receive_keys_query_response(response).await + self.identity_manager.receive_keys_query_response(request_id, response).await } /// Get a request to upload E2EE keys to the server. @@ -1633,7 +1638,7 @@ pub(crate) mod tests { room_id, serde::Raw, uint, user_id, DeviceId, DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch, - OwnedDeviceKeyId, UserId, + OwnedDeviceKeyId, TransactionId, UserId, }; use serde_json::json; use vodozemac::{ @@ -1715,8 +1720,9 @@ pub(crate) mod tests { async fn get_machine_after_query() -> (OlmMachine, OneTimeKeys) { let (machine, otk) = get_prepared_machine().await; let response = keys_query_response(); + let req_id = TransactionId::new(); - machine.receive_keys_query_response(&response).await.unwrap(); + machine.receive_keys_query_response(&req_id, &response).await.unwrap(); (machine, otk) } @@ -1936,7 +1942,8 @@ pub(crate) mod tests { let alice_devices = machine.store.get_user_devices(alice_id).await.unwrap(); assert!(alice_devices.devices().peekable().peek().is_none()); - machine.receive_keys_query_response(&response).await.unwrap(); + let req_id = TransactionId::new(); + machine.receive_keys_query_response(&req_id, &response).await.unwrap(); let device = machine.store.get_device(alice_id, alice_device_id).await.unwrap().unwrap(); assert_eq!(device.user_id(), alice_id); diff --git a/crates/matrix-sdk-crypto/src/store/mod.rs b/crates/matrix-sdk-crypto/src/store/mod.rs index 7a35c1284..467cba2d1 100644 --- a/crates/matrix-sdk-crypto/src/store/mod.rs +++ b/crates/matrix-sdk-crypto/src/store/mod.rs @@ -51,7 +51,7 @@ use matrix_sdk_common::locks::Mutex; use ruma::{events::secret::request::SecretName, DeviceId, OwnedDeviceId, OwnedUserId, UserId}; use serde::{Deserialize, Serialize}; use thiserror::Error; -use tracing::{info, warn}; +use tracing::{info, instrument, trace, warn, Span}; use vodozemac::{megolm::SessionOrdering, Curve25519PublicKey}; use zeroize::Zeroize; @@ -98,11 +98,92 @@ pub(crate) struct Store { inner: Arc, verification_machine: VerificationMachine, tracked_users_cache: Arc>, - users_for_key_query_cache: Arc>, + users_for_key_query: Arc>, tracked_user_loading_lock: Arc>, tracked_users_loaded: Arc, } +/// Record of the users that are waiting for a /keys/query. +/// +/// To avoid races, we maintain a sequence number which is updated each time we +/// receive an invalidation notification. We also record the sequence number at +/// which each user was last invalidated. Then, we attach the current sequence +/// number to each `/keys/query` request, and when we get the response we can +/// tell if any users have been invalidated more recently than that request. +#[derive(Debug)] +struct UsersForKeyQuery { + /// The sequence number we will assign to the next addition to user_map + next_sequence_number: InvalidationSequenceNumber, + + /// The users pending a lookup, together with the sequence number at which + /// they were added to the list + user_map: HashMap, +} + +// We use wrapping arithmetic for the sequence numbers, to make sure we never +// run out of numbers. (2**64 should be enough for anyone, but it's easy enough +// just to make it wrap.) +// +// We use a *signed* counter so that we can compare values via a subtraction. +// For example, suppose we've just overflowed from i64::MAX to i64::MIN. +// (i64::MAX.wrapping_sub(i64::MIN)) is -1, which tells us that i64::MAX comes +// before i64::MIN in the sequence. +type InvalidationSequenceNumber = i64; + +impl UsersForKeyQuery { + /// Create a new, empty, `UsersForKeyQueryCache` + fn new() -> Self { + UsersForKeyQuery { next_sequence_number: 0, user_map: HashMap::new() } + } + + /// Record a new user that requires a key query + fn insert_user(&mut self, user: &UserId) { + let seq = self.next_sequence_number; + trace!(?user, sequence_number = seq, "Flagging user for key query"); + self.user_map.insert(user.to_owned(), seq); + self.next_sequence_number = self.next_sequence_number.wrapping_add(1); + } + + /// Record that a user has received an update with the given sequence + /// number. + /// + /// If the sequence number is newer than the oldest invalidation for this + /// user, it is removed from the list of those needing an update. + /// + /// Returns true if the user is now up-to-date, else false + #[instrument(level = "trace", skip(self), fields(invalidation_sequence))] + fn maybe_remove_user( + &mut self, + user: &UserId, + query_sequence: InvalidationSequenceNumber, + ) -> bool { + let last_invalidation = self.user_map.get(user); + + if let Some(invalidation_sequence) = last_invalidation { + Span::current().record("invalidation_sequence", invalidation_sequence); + if invalidation_sequence.wrapping_sub(query_sequence) > 0 { + trace!("User invalidated since this query started: still not up-to-date"); + false + } else { + trace!("User now up-to-date"); + self.user_map.remove(user); + true + } + } else { + trace!("User already up-to-date, nothing to do"); + true + } + } + + /// Fetch the list of users waiting for a key query, and the current + /// sequence number + fn users_for_key_query(&self) -> (HashSet, InvalidationSequenceNumber) { + // we return the sequence number of the last invalidation + let sequence_number = self.next_sequence_number.wrapping_sub(1); + (self.user_map.keys().cloned().collect(), sequence_number) + } +} + #[derive(Default, Debug)] #[allow(missing_docs)] pub struct Changes { @@ -290,7 +371,7 @@ impl Store { inner: store, verification_machine, tracked_users_cache: DashSet::new().into(), - users_for_key_query_cache: DashSet::new().into(), + users_for_key_query: Mutex::new(UsersForKeyQuery::new()).into(), tracked_users_loaded: AtomicBool::new(false).into(), tracked_user_loading_lock: Mutex::new(()).into(), } @@ -613,30 +694,82 @@ impl Store { Ok(()) } - /// Mark that the given user has an outdated device list. + /// Mark the given user as being tracked for device lists, and mark that it + /// has an outdated device list. /// /// This means that the user will be considered for a `/keys/query` request /// next time [`Store::users_for_key_query()`] is called. pub async fn mark_user_as_changed(&self, user: &UserId) -> Result<()> { - self.save_tracked_users(&[(user, true)]).await + self.users_for_key_query.lock().await.insert_user(user); + self.tracked_users_cache.insert(user.to_owned()); + + self.inner.save_tracked_users(&[(user, true)]).await } - /// Save the list of users and their outdated/dirty flags to the store. + /// Add entries to the list of users being tracked for device changes /// - /// This method will fill up the store-internal caches, unlike the method on - /// the various [`CryptoStore`] implementations. - pub async fn save_tracked_users(&self, users: &[(&UserId, bool)]) -> Result<()> { - for &(user, dirty) in users { - if dirty { - self.users_for_key_query_cache.insert(user.to_owned()); - } else { - self.users_for_key_query_cache.remove(user); - } + /// Any users not already on the list are flagged as awaiting a key query. + /// Users that were already in the list are unaffected. + pub async fn update_tracked_users(&self, users: impl Iterator) -> Result<()> { + self.load_tracked_users().await?; - self.tracked_users_cache.insert(user.to_owned()); + let mut store_updates = Vec::new(); + let mut key_query_lock = self.users_for_key_query.lock().await; + + for user_id in users { + if !self.tracked_users_cache.contains(user_id) { + self.tracked_users_cache.insert(user_id.to_owned()); + key_query_lock.insert_user(user_id); + store_updates.push((user_id, true)) + } } - self.inner.save_tracked_users(users).await?; + self.inner.save_tracked_users(&store_updates).await + } + + /// Process notifications that users have changed devices. + /// + /// This is used to handle the list of device-list updates that is received + /// from the `/sync` response. Any users *whose device lists we are + /// tracking* are flagged as needing a key query. Users whose devices we + /// are not tracking are ignored. + pub async fn mark_tracked_users_as_changed( + &self, + users: impl Iterator, + ) -> Result<()> { + self.load_tracked_users().await?; + let mut store_updates: Vec<(&UserId, bool)> = Vec::new(); + + let mut key_query_lock = self.users_for_key_query.lock().await; + for user_id in users { + if self.tracked_users_cache.contains(user_id) { + key_query_lock.insert_user(user_id); + store_updates.push((user_id, true)); + } + } + self.inner.save_tracked_users(&store_updates).await?; + Ok(()) + } + + /// Flag that the given users devices are now up-to-date. + /// + /// This is called after processing the response to a /keys/query request. + /// Any users whose device lists we are tracking are removed from the + /// list of those pending a /keys/query. + pub async fn mark_tracked_users_as_up_to_date( + &self, + users: impl Iterator, + sequence_number: InvalidationSequenceNumber, + ) -> Result<()> { + let mut store_updates: Vec<(&UserId, bool)> = Vec::new(); + let mut key_query_lock = self.users_for_key_query.lock().await; + for user_id in users { + if self.tracked_users_cache.contains(user_id) { + let clean = key_query_lock.maybe_remove_user(user_id, sequence_number); + store_updates.push((user_id, !clean)); + } + } + self.inner.save_tracked_users(&store_updates).await?; Ok(()) } @@ -658,11 +791,12 @@ impl Store { if !self.tracked_users_loaded.load(Ordering::SeqCst) { let tracked_users = self.inner.load_tracked_users().await?; + let mut query_users_lock = self.users_for_key_query.lock().await; for user in tracked_users { self.tracked_users_cache.insert(user.user_id.to_owned()); if user.dirty { - self.users_for_key_query_cache.insert(user.user_id); + query_users_lock.insert_user(&user.user_id); } } @@ -673,22 +807,23 @@ impl Store { Ok(()) } - /// Are we tracking the list of devices this user has? - pub async fn is_user_tracked(&self, user_id: &UserId) -> Result { - self.load_tracked_users().await?; - - Ok(self.tracked_users_cache.contains(user_id)) - } - /// Get the set of users that has the outdate/dirty flag set for their list /// of devices. /// /// This set should be included in a `/keys/query` request which will update /// the device list. - pub async fn users_for_key_query(&self) -> Result> { + /// + /// # Returns + /// + /// A pair `(users, sequence_number)`, where `users` is the list of users to + /// be queried, and `sequence_number` is the current sequence number, + /// which should be returned in `mark_tracked_users_as_up_to_date`. + pub async fn users_for_key_query( + &self, + ) -> Result<(HashSet, InvalidationSequenceNumber)> { self.load_tracked_users().await?; - Ok(self.users_for_key_query_cache.iter().map(|u| u.clone()).collect()) + Ok(self.users_for_key_query.lock().await.users_for_key_query()) } /// See the docs for [`crate::OlmMachine::tracked_users()`].

    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�so?UZRaosNAh@3LaJk~NeP9iSGvy)ccYiU40v zS=Qgps62Fy48hPD7{ChOhOhDR~1FHpu4)hK4>ca$OaiHh}p3^GL1yKe@AzIY25 zwCsG(7_=3;kqOaU>n-W1xP4jh;|9B<0Fr!268)V#&(l_XIX!EOad>bX?rF=-lQN2K z_R>+#VS8?vg-k8ii+J3MOFKWG02J6oKZSMDpUkm|EMcU1Lur$0i&`Qf(+DlgTEh3P z)9)h@k~vR8{A3De(-(tX8M#-KO*u(nAG_VRn<>k+Y245TpSB*|HFl4RO>MJVW3fdu zZ_F4>;I0XyH=^Nk5dlK1n0eg((+5+aOdkbxl4+dffk?`!6rtpC*ZtASc6S;S}PUI)MM2x%o_hRA&wH9_x+Azuy=`$-RhfZ^2m+s+~;~5X%cg^7=5u8 z=H?n2xLp);LAY>R0}SNuS~b9^&an1HGxR0BPRiSnUg=akXd-VI9s>)PJii1M;3^}j zOO)``zL+)*p?h@k#%fufjC7L_Sf#{Qt1b6}0a@2;wPBeo>x|6oiCvNz_H_%ojjotl zt6lV%j1p%wZ5pFGDGElk@lUvX$2XiUj~t8GWftOPiX{VE1lufiwGXFm>$WlL8Wr zXAjhGiw3G*4qQ%}xMlvT*K`9WyU6_E0MD-m^9&3i<7x63ok$(E2b_(KIMQNMcx}2? zU0%Q*Ev%r&=ei}8eqORa-I%#IMKaL*9SQTNO6p%xnADYg^<(R0X6tWlVe8G2BCIV> z8azKb07pz!F~y%hIxqJO9qS7jLfSpXyKBa~fl+ph^7|=<2U%_z<*)!O^rsv`+e^9> z5;K}gHe4z7w33MrO<4lIo1RGPlW7ZI7(qEn6-#X9xB4v;Y;#F>MLbq6JN1w?To1tM z_#mxy{8iRGHLASfI~@>% z+~`8w?ZVUe-D^1XHE_IM$eJGJlz<=H4&}Xr4mpE?+ank^7z}F2I&N=u&}LK*ba!po zhtA@^L8DC%fIn*0Wt3G1K)>n+FhZd_AbDMI5-h_i(WG>S?qJxXdmRru`Tw)`CV+7q z*s5T*657s3DnNsxlgYXMNwYGz3F%rt-&XQoHfGbA7u z*NM+f>>O(+cH-F1X(vwX+=+8L&h0qP;Uvy||J?WafBygZ`R~0tx~itn0YFmPfy9}r zn(nGsRj*#Xt6pIUL^@L6^WjDELJZhjUJn~-IDz}w&%loDw_K5NyIN)oo@346j(7qs ze}sKga3*1!_C#;2iQYJqWMbRN#P-CtZQHgnv2EM7ZQIG_`~TXit=faFy6Ud(lRoV3 z=f0#S&&6@xBxJU9Qa^B&f?7bA&JF3deETYTf@!p#nKfrAN2wTu!8#M%2DXPJ`%~a{ zhZQnVY%rTwd@k}x4~Q3Gt493_2x=g5N3-CZ^87IF8(+%yjABmv*vQfaPBpXbM8S5s8M%&GfEuX%=-i#>6c#;KcYt?Q~GNPINb#>|6F2>4hV|_ z*_e428ZJ3rI$6z)R(JW=Kd_vIrte15%Q@t94x7qJGIIu=p1#IT2nG9227cv|&P^p!Q2N)}&0Yl&4>5|Jp4 z3l5$@W!)3iy$b-*wL}N`7OSI|yGb68PY=ch?GtG*q&QM#0=w$2W;r=WON_2>Mg~iQ zF1jULnL-+S-H9Zq0+}RQYa)-5-~RE>2x5IlL~()mIFnpWm?U#d=2iYtIDs!1nPrU| zdZ4je9Nju3rA>)!}Pg9TI z)aMmE@0KtHZAp$9cJdh+2;4KBdpFL|)wk=Uar3 zoqfsW0?O> z4x8{$b+O=+``1btMENWK%lL)LKMkANCGhAG)zP=(DSGj3iKf<^`5{XILC?kj)=BPjZqp8E$er6L$C#P(!fAb z;$h){rPrC^+@!>v3$VIoSAc?vNb72=cF{(?9|ZkX@^$y<&-UbNNmm#zoQB;2Rbyc; zi~htky+m9r?zaNk$!SRHkhUtuTU$p*xo0gX`Ss6_mjtz1wYQd~Y9t}yZ@nU3-`r{YcmovzuGsH zlAk8EG_&#Q+z(HrTB-Ofc&MwQv936+W>>}6q9PJC)X0)wze~dd*fSR;$1Ylqt(Vse zsEn&-Km6qdLM&$cILfb5V z)L~Uu@)B8zr<=ON2YppnvG_7S|KYpnTmmAjV`8D}i$+E8sf%^INID-PN*b?S{ipBf zHKdhO8j`lsh3Lm2gOg4JlqYSi+yLx{7(FEM_w&ByPJ=_jvuQxWSa5f{B*OFTdUGQ} z`un!+mhs1025gF)a&dMB(MpGecUQO`a^8Hf#BeLRcp1($&<0Nj$iw2@?Mo5B)H~RV z0wp7av>Y!8oE>^)8NSOQCg2HW%+b)`8TK05-uZ3MgRuDX#2ks8b*0m3)L$ME(i$y9 zHh7UM0kYEFvel;(?oZ|mYMDb5t&R-Ru$l|MA4vZ)(|yqTb#oQT>Tn1r!TmOfF03WT zuxjMYe}uL{W3DOHDqFnF$`}aa<7=ECOFHX-zsATH;1(_zWK*X52~l3fqv?~IJ3Mu# z;^GVcXk)Dd%+6g8m49f!iH{Ldor) zl3dM%Y{8EXuHQLBmwfmX2k+E>Sv|WzJ&Q4DuRnfU6pPZ_>OMex4h}Y|-?wT$XHFvX zW&eQ)N?!D3Zcq-mp^Y2znsI!+*!agtCOy$(Q-2*=$-6D{hn$yxg<|$)ns2?dT4mb? zm-NuI6QGu_Z*NEpc?U$fS+*4g!!7HPl%mF^t?k#s4YtA!_OhkV$QD3*$c`%cs@M8D zNqChJ_$WFXaD=%wU(_x`uRxD^<`{NJfqh>wPCXZK9}x(C7i^ z)cuFC6dMHoCLC=;ot`C^k^7)@UjL*aJXJzQ1Xrj)1`ES4#xh#mAV-(vBs3hEQey1J)#MF9EXkOXAqH3A z!XqLNAh3rjw{D<6HuakMCKkx?Ojn> zbic&yz>5I(f+KrO0wi)3OEYOe&^RY#KBoMjcbKrqqV#ues;}A5JEH=tf&;7cbAPpaivY@>yD`b7-8f|W`ZZ(B9s7Xg-t z{aw+(vnUIrI{tboHAn5m!dQiPks0A9HNTplU!64n?w~1Bn?#DN4H}w*E=NnwKI0eh zIuD!{NDy?~|1cbg`Rzv9$XZj>UHx43G!L(?+~Ezv8-n8=FlPwyq=C;yiE%?b7NXNs z55aD%GJsH--I>K<|0VSc9#pPV5C@aafG?xa%zg#zgY;E*h!b}^d&i*uq~$`XxAFzA z1BJITRL8*pRQ4qTYdt-~n|g*`5I7KjV61E7yj*73o;{ShqLJ02f*9Y_Ex0}q&+)t} zw!anmgmsS79^5pWDdjqAyfiA|P=(|b!9#@yZ`cpmzzD!^>VE)L!?QNdix(UeP}ENa zS@LvxJRiNkt6~1#X*mjMY}^+%c>vZ|PKN&PJP|4+&Mm0`W}&Xmc44~gXtZP(rHaJL zAr7Qkg!CtRAW`*lUAH`0@UpWWc@m}`M6qgp|mQgc7N*`eioYCiRLta)r4BIIIq~=wW9WZ%B7%dtKz7{i;Ru;vXpY@Z*5;F`+^ZMolmfRV7|{ zW*HQmkVZo-V?7K3wo*uFk(O_6JXDVkyhFPho`c(tUI~_#tkrqlYG#c}2|Zj(HlSeH z@d{n;mv#;s+n;w4up7DfaXI{r)w~sP54!o!q*6lKct320O*?4bjI4(WPnv;6x=Z$=TS+{`iEL#I)xhI}JE2}W&$%}}M9jv7TCKJg2T z5Hww9Wo37{8-Qs4u70$F9{%Y?EajH8lBP*mWzJX0O9L3hz@A-QWUP||Ggf?>`JL4nUvVKli zGP(0sq^B|`6YUs>LuTarS6?>Z1g%7JlQQVNMX}|YXP05Ritz1YK@##?qd5+Uw!ct_ zh=}+>hsT?(5{343)ZTN^_p5g54j+?PvCSmbvCW7@|30eRgFmlZb>b^&yn51m{VV>! z9U5Xi^ArqhuoNdt*y)kL5e$Py8N;1|GXnKr)u;U=i5Elk#jCdC1^^TUh z>m+*W$^B5sq8dPAnt|k4-IP${J*By6sl_@gaj@V>HHB($LE+cd$$-j1$j-Mv*GQq?cLrhI=g8*zpA6BxszkY zG%Z|QK7Y$pBSO2{Wr}@nRnn2tKo11|@Py0zZC+fLEduk1e*2v4JB_AGC`=kQRo_ zZq)0Z-zX@LJmwOuYu5cK@Dd><%-h9Eu9JCgJaqD~j;^aUNaHPkW(SRU7kW09=k8^I z?}mr%CO`wUp0m$ezXerv2E~fwB!m3Y=lA-D{A8hs@XKF*=b!kgl;UByff=6$K1t19`83lq zgYf~?V8TEF&eNCklKl>-Q+O|zadk0ZE!hrG}@AOeH3LNca##gH1JGdvwx z|18t^5)fuc9YYd?89>OuKDl`W<4I4bq_Bg zDUG32LjtOIhU!M&ae!zRAQy=1z7!M-g@S@WtHLaB{WNfWH)BYBw~*KrGYJVUdUoPF z6P&o6Bf`0cTzQQcWk||Ql9?r_#N(lIjF6gzMz6M9$yme(44dTVa51mrSPY{=sKqMM zmUJ$&CIb8@NINB=(2wWiXvSJtxZ*7Wt7HA07umkI12yyV;q=^ZJ0dxxkda73QUNiQAshg< zKT+;!>h8O-h*yCkohi~8Mv=$Uu@y!rg-MSE)YMRpMi;#kwtACB^GDH3;YH)f3esx! z4eY{JLT_9FSED?d_fK`feGSr0+c3Ez;!G$?XR$gB&vH9r&!JR1q{5A<>+i?E!vDUj z1In6jRmS6xH)F*y=u*pZ;vHSqM^^nYPPADLANq3xTp>alrn%mt^}Gju90U%*Az1jS zK=MJ@ZI5U-coI76MInpO_uXLRCe&3gLfS*USsSB!;a3yfl5M`E!5q1;1a0P(jy73e ze-!gn<%dK2(-twJG&SJ>@}l#JnhUo`S5%>p#)7)3Q_yA!qXyI z{ls`C!~^=e0RM?XguHPp5!!cH1{^KA%>fV#|LAsMkhn2y1(Uey=8?v?Gkn4-c*m_f zGXUNO=)t^7LJ_Hb(5SehTx_isdKcr+bfW+Py-KLSoPmr6m`u2(l>&3Q!aDu zJ%T>GbTBqBDeO?MULfS^wN}5atT;vaHa_8Ic2%qWM1FV^yG5Lx=yS!xb#AmTBEP=z zG(?Y2`7O%{toke6xKC{vbDBJY=W(WNo;_u?O$E7qGCqkhDwyvUwB*kTE$CPVq0n;t zYJNI3AT6qkwA?!J8y;?ZUPhq@a~iLdSYUvCBLH@!EfN^I$l-PG(yl5smONv;4Aj@b z7rAo!&{%|HMMc5h?d|ox_h;#{v>$rSbh_PsZkK@LNg5~r46&!TI974Y%KUkx9{AYi zcJ&Esd}?7u1MsunfP~Tl2L8bGSYc>06{H)yQAtjNbyb&VuwgNL3rOojfRN1F$hgyH z^f+YGHvwJ z6rwasYLJOHPTC0%t<76F|Apb~+bZh6 z(VGXy0|sLRbg0}dfpUzUV)>ZL8;Laia~;PYlO#_9{Dw%~4xj}z&PTQBr|N!V0W5n} zB639y*H7}ZLeLC~vaS?x*os;d$%7%-_gfYhA628JOCQ=I{5qt+nn;khztMS0*xi9w zXZbOEGue=VzBMyMkXb?9#GDtuLi93nvlFq;MkBro6x8l~bvxxr{C8Nh7O|Y?)nX|0 zW~YR#$uJord3Ta=Hiv$M6=c{gOZ~A?-*Kl8P>#Xz?Tw-7k}|%=9~nEdY!dF%s@RW* zsXZ_0!}0EEMgFz1I{Rg8boY?Qo2I=&YJS#wT2h$|{a3(vly?4-C=zfXYZ97hHTi_` zX9aqnp&iKY(y>m7&$Qhk+O59>5hz_6bi(JJ$f&ffMmiL&~qP#4eqKF4i#BeX|3_XI?y!HGe$uHe}ApHW^jU$&M6QZM!Q` z{XxE$QD3CV;#Iux&Gwj}2&E*Dm50{s;kKN|$OZZc{VG#%=DZRx|L+2*KT2T19dsh^ zEf;K*+G4lA{(AjGv+%CTP=I1c0KpGWqsB;h{mhIN_#-1Y8{UE4czh*s`mN5PKUF>Y z2T7t)HR9%Zg#h(-eX$20n$yFesLVA9>aE<|-uF0icqe2}2H)60aPr>Dp-02AJaVuV zzo!)s8@xr6>>XTxev^_(>gzWYh!6ZVq{A^+MP1+0pR()}ryyGsu`2CsE@}(!L(&5# zqR+OP@_LSU*iv*eotw>hA4Rluhv@joYavsmr!$!P*1tNK^&)zxeq+F3%jL&D*V||2 z(|^bH{M3WIXh3%!=@P|@HF!Nuqa*6tUJF(G{rcAUezv1y zB+*1sAcRx9G08>;%@c|I=D#ucRxZN)5mD*tl3n5Y3^0fOP@??O7fWU-?h`a-Q-%cg7`7B~}wmc9$gj#Se4)SDfAzn zAFZ&yimv|3WOF1SKz?{Sdld@bSd0H9`x_u{O7=GpE;_RtC2niQ+Frx#>a%FEZPmdt zZDKrO#Mq)!o&)Ul^h7@=x>Jm1A%v^=g{&xO$bS4W44Q-h{C|SGFWvV-T<|DHQh|@<0soYyw)m z`4c|WlNny*9Bvs8YH8nYlu5yWAke)1|brf9dyo7gdSTr3n}U4N4twE38f; zZ-+{!Nb$4d(hiPSnqEgf920Di;25Na4E@_H$>l+U?p7w6Z#2(DFSZJu_bgr zP|Dn4m_QWtGz@nyB$sNKaIkNCV{AA%;g)>+8GIx1f3^}zN8mU3?ESny%9%H2b!uJN zHWLE>jxrOP8Xxc5K}R!e(Yh@qBb%LrJ|w-P?n<9?N}qT16d#ZvYuHMiCI?6M-A2V}R{L2;f4q+mGxqg& z)dTC~F9qSZtlJQ%P>Sa>&XeuI8c*u|+hcHL0wc_%`um1aw4d~rj%mXXXcQwocQA}e zfcP{0YY>En>!=|mvIOsHE5@X{?oSjxG;wsf9 zQKGLN?D{m!+jN|@Mc9??HIxH;t2MsE-3}K(p-l+lfBJHO&WvvOJEVC zR!k;n_oAy`r0ZHv*Avh&ZcQYI5N8E5uT3)43wyY3Od|6buPwzGuU$ri8oQoa#&)n< zNK|^W&S;)gBwu$`QVjluL=r3nx)bn@|1}3>;Yh0zVB&b#PaHHEYe_-kZ(MRR^vz-# z^XTjkoRB#bn<~iCWkh?z0+J0HYFb|p#?>3(kRfZ?ME4|y4rim8aV7A)Fpcd{OSSd* zeq)G0SK0R72t>zYjO_=`JFaSs@=bK(3~%qazOn&+F)Fg~WQX(`LkD6y^jmShvT4m7 z5vn-gKIXj3>&s@v``gv2mBXJlS4!C1L-!GJNhHmBK93=HZ*DvScGFzY@K7ZqR)rRV zZy|niGhCMA;Cd_J@%pH~ROUn6H!R}aZ=i2z&zyW>znF^6VKOyg!o|%26ljmoAU`(R z2`}d8-FX#-s8f3Tn+}8PKn{=Bj&<0sTLqFWU%>F6{<);fYKa4z)IFIBjV-sXgb1u= zeSK_xymKS+jTO=BRF|%{Sl*I*&KjA)S^0(&y+OlWPkgV!-#^EzDm7Oo0>uHkk=|`YhkT>~S90i{E z+Y(Da7Ny~5B&`9hEL%Q0^ni$LwB}=%H1^ly7tTj3)jax^D!h6JT{ELH>Mu*>9s{GC zN}ulK?`er&eu>)i%B75x4zSkFG^V^ZSH+Aie=0jt!hrY^2Ij)_yUB(Kw~gh#>biF~ zTJkeme|2T6MVrY_w;3U)wU|3Qv}0SYvRQO@9M@<>o*uUjOxDu}&$B*9jqRQeXB=~0 z#f_jn#U1BQHL0#zt~@7?ZSHL_HlMh*1D`sqgE14fg!gUXH5Vcf{E9Bb4sm|G?rdJT zgRz>HZUt?8xHvc`{-rRt_O5#FgdPoN_ho@42()PRY+7?joim-NU&kXGBWROQ_1Y|t zNbUuDVp}fsHv&0(W#ku4`G3TltXCh$i)E$z-B^w+&_0|!0$bm_%j@s#?6Z#+U6qYt zkp^D@V%CK@%Lajt^b}<>?6*sD)`S51k_Ca6RdGQ?-iDMv^i6pBHl1ZMQ~cL;2Bl-; z^O{qcd@7^a#{NcH?qQ^|W2a1nA0v*%o9B%C{Z!kF9<_=}Mp{|?*#Xx+`OtK#Nu^d6 zMYsF@4tYBx#wcpYF^MFr$s%1xNLGefb(Rb9w-aTDhy^klP(=;w?Pp(7>;4(j=$VTGA3z4fc62qcW|%9JB!kxoOcBSI$B z*WUpj%`0PbzKrs3L{fw`+MGf#g0r-H_XMy%GUZbeZa4WF9)u{&PRjMU=^S^%XLD*q z!kMi$V&fHDZdVX{F5Wo#1Y)uCM{5oDTf(jH&6z! z&65SbjtfQ~b*5pH4t;+k{dN)4p+l?WxqYRSCu>F-u-53>W?jIVKV?^nDRX&=x@ri+ zoj3CLkhAcaMzpL1RG+W;YWHsxu<{857CB$Bwh%^lH6g-=>zX;PI*uJjifA7B5M$d6 zIqZpi;rf|C49K!5}l z@bUONloO7=NeEIh15RiETf!Rp`;IK?5D}R7QaPk&Dzs+L*NSI#qig1OfNR}ock98R;^NA)bVU0_PIwn*yJ|X2m+)dtsCDz#1(lm6Nsds2=AE zF8xz&_nlZ8I6j^(V1}DQO;7GYUZ=Ya;CR@T1dfh8T@q2s($*3lLBwpz@Y)GdJ$Zeq z##0qzvmNPP;k6$82A+~c)Q??u2SNR~X4Wpv;_+ys-8}Pz zC6JRB7G;x&X6`eX2+a&Ihr|9MAUhv2&T{<(l>)aCm`z4Y_Y=eiVCkm*C|tDR@lTP&}lHV$OBYC;0|LKcH9taW^eE=c54D{ z#><|Bm77W!%TAtEefdlGfFs=Z7FdN#_)-itM6MZtOV4`9coZ7tC03wp7$={Ww6uR2VqL6yU6mQaxC-QCaUyPVtH1R z)$OKA8vR-aF}pueT-e+Qfza8A+d!;;Wj)n%wy$kMk)cSS$srFYN@5bR1v~>r{J|j zlkIMkcexf*(Xho`NR~K}B!fxOK)StYBM_ta5*M!gZ!FVK^nWCL?_{1enyszut`Eau z`QpVz?Iie;t|jCsvN-bOB`E&Nl`u^I>)rpHfbPkxUbaquV@2apMxP@MYGP;zkxNHM zmpEXoCMhD_y)>YfUfdO2&m{Q#9wBZ2%?gTq-)YcfKiItt;k{9?9QZorPM#|)$HSg5 zYKr1xTDfo06i?~bdaKa_NnJRFS$UfI47EoQk=cJ51&Rg?%B-TBE1U#<)g!1PV0N9P za=SSmoD*F*Pa|HOzwg|*>2*Ddo!K6|ah`$uq-oi#5v05b6f~@}Y`aLmyOCIF>do7+ zLN#OmR%%}o_0(d$BgQF96b4-*1vz$ng30J|)#-jocaOXw@oZrPGsQ0y#p6_Mfj$(r zqWkl=T4c9y+rLy;^uc;j%>I)T?fTeW`dZom$Wy+E(@Yyt!)W8IuqSV8D`)l)$at4i zR=UKi31QR$IofyNNI~Pv^+uU*7Q&- zBlAW?NE}f)hXc~E{J1M8rGF*p+$jIhz4|;{Iv1|j8@#c2$0ugj+zwT|<5Kb(9(V^P zTC2R!s=YKOYB$~{mwRk{r$Z&;8+G99S(_a(46r1~usq+%Yw$>{7mCC+ZH;n-g-Q210GyClnSmISg!MQ|kou`%aWpj)VzxRHX){OI^IsXss2F1hVWDr{B7-W#D9F^b-B{I_Q z2Pz4)Qe)%2(p=@KL{;>Pqm`BB88JXNUZ$uDYSa%rW8VTp|iP_&_{*g*k3_8zZEnK9cU$}oI?eqym9Q0$I15g=jmSfO{@{pVnac3 zPb~~fdejB!qDkGda?)LPzc1{5m=k+3nG$EQCA7~hHvu(kZnyLfvge>%b~6g?OdS}s z%$^eS>^5~M#G#)gx8bO%V;jh9ei|o2LNyGEe4aBv%g{@pfUXKq zWh#I@960NO2te%!oiN;<0S-O_N76`WqP&`XeshB+={8aAxHTi$%@eL0>M;f~#VrI% zmhPe!*KFuk7JFJ}oGJ+6T9&YGQ9+3Vz(4!P4s95|V?1hfgPvN)zkoaaR5dmCM2}l9!n^$fP4m`hy2JL(;slBjNg*T;`1$E+@h^iCTZ6z z&ggI?f;(%qmmXgZ4V+Pe%8fIi)+Zzh+4O^76r~|3)c1)JZPtGFa`E=y zC{?o~?0(;X-j$g|mi!;!B0PdZRmQ>i`nlHy*lOp%zKwOv#OeG&3p$?=bZ9v}L zm6f-8mfFv=n($H-COo`Fa14!zU@(su)vJ^fgC~872_`rkY#+{=8qPXOJxd6xFMA=x zIol1Iatar3-lWTe$>I6lKFFK9q%+Ngf+y$q=r_1SW1Vg#syg2i>63TLFBIw&93kUI z>cj^u%%yvF38c~U-?$>>)w-=3FGk|#&(2q-KQZo@C=Qavb+Oj{-3i9q&&ZW$_!U8o zI2o2rNXAm6SYT7R`GJr^sov#D6)7!?WG^+8Ic0sTNiVn%Bn`*jesh^pL-?0ei~e}> zv7oCa(79`?yA9uk*w*|c<(R@`2~Cfi{!e*(h~d-+{&2etS>hx2gJ_2M+Z=LvpNZ@P zvdZT+n>5G$)7wzLDyA5gA=Rx+MANv*O~=k(Mp;4`iyoFBV;$e!Ne5xPF&8dp?<}CR6p+;W@^_o=0d{^!9|GSD@n--lg+a&(?jkWh zO@Z1lV>BO^{j1A3Xc!}S#aZ8z)q?T4%NaFm{f6~Cw4o#!>gR`9!fO-+WZYEqrxfc5EjopTTrHZhFk`XtLEOb z()zE5feWjV!n;n%%?i@S3Xj&dx(z_33nl8d-WClsAq`Jzxl)HJs>+a_s6GsM zPKHcPeLnt<93X8_VXs^c#dSJg@}#MlYOU`g4P?1r;nZSGjF&F0hbKcCSts! zqO32}nuQ(SxR$;xE|Rxon%bQ3=;kRxRQ-K;G((RcRRp^Bhq`6Kxf#Q@3)1@&w57_( zj5i{T@pL1ci!>W~u*p|rW#zgQg)OcthY9U8np9k84QrQ4MEiuDp!^)y&81DKwt0uY z%KR~}q}FMBbr6^E$G9|VDMzSA+u{Dj?l8=d618qRVOhrsb*M@jDEWstZQy76M2meybBV)ei!|lQNkNRc-PHd)SbVer&USx3{L!N z1SeSo#%3!D+LLP66Xm$s_9Whik4mLJVj=Sxu(qy0^;a^8XBgk)*%Tbs_cuHm|LA4x(@lAH@GW zGo)&=cfa@$)4%YX+Cy}|_|ucbspDH#SE#_igR3nSKezggtK+#<#G7IkVI3-&$MTUZ z=EE$s65#B+_M_q4>&f_PY$q~JKEnzJiTaSi8C!RS|N5hNL!eu`^J4aJyGtLZ>d}*S z8>Uv-*wqSJa>J-_RJK<~R?o=&pDYF3mi$$`72ZTw18pUubXOHQ^iA3a7d0e_GHq`T z`>xxe=5vI#iwt_2gZ2olshV0r&U?9z?X!U=74(EN)FLJOV_Q^-p;(gGQ#}mY`-EiWmm$saAMN0I(wQcPa0w3 zJG^V3uC_L-hR5Fsj}bn(af1)48QpNBjut6}Zm6RonmVFb3G_LWa19Ray22Sx#jYv087yr~K+lot>)r|mw(w&BrOVY(*gb2NW%lnO35%vkaWY@+ zr&P-3>m<*!qoG}Yp15%3{y>4D%l%%E>HKMyOTNQu?96K7J_BSpaznN2CTJ~!%A`k9Q3GL3A_vn!!6n47`gX2@IM`R!AH!&WBzqFc;DZygp7pN1St zu*Z*Ap*w^PaTF4jj-R4pZ zdjEqSizW5yxb_7l48C~2^k%n5n*r;6^}3)GDi-7GbPBWTbF8nhop;5a#C?*}c`8&J z&3i%xEn5~A#(KYajCp{)j!C;Uo3+$U?ynz)(V zeg(SwkB7ZH>xufAMp#fJ*;Xi4f4D5@Utr$*+=V)+Keq`Rnns?S6N^ZYr%5&Xi9uh# z0e(Y@JT0Q0Dae;C$hQUiDM^?kLzq+aT?r2I;d^(IDv+rS1#jwZIK}96TT{d7Y9NT; ziy>GFy}f4(=*kJU-6vFJ_1=hC9cLY5ieGq zKq6EjgLXLOpJWux?Z1e7kzE4t((fMvNh^Dcty|vM{7Am57U}n`1&1(Tq6>sVqi;@q zyIv&cY42)^J=yL`#X85}vQk_2>VxHi=e>F!X-Cg#QLVG8F1uWLsNJ#+vmmi9&V}zE zug;Gi9GuDvts8HB6Q3&HkK!uJ>U0>X3at;caVfAuLO}oR`W-L9?Ezv=dbl^>@SP4I zd8|&O29KlA*82nOzA4`uBOR9PDL-{A3->>I%li>gZ?muc&~&1N;sEGAJ^v8-wDnuo zSQiqG%FrnDZ}BGXlHd^YjIES|#`h)Bzdg{?-fw-W(+5bX?*DZx1g!yN0K|-yrC-og zmEkJ4t1P{eQPxc#5I&i_jY>dMif-%KZD>+j(I2CmvQ_z-$xJL3eez2Kp~Ja#YqmbA zoI<7&2kt|6DkQ0sFiFJ!uY=@3fxt@3%cQix z>oTs!4(D*m+R_dV^RQZ8<9~fyJix4~6+L~+Ulsz@Dp=g|C3+uyUg1HiYtCcA{nl56 z;Bod0xu-#*>D%Bk*BtAlt;7fn(<9}()Vo?}6HWt@+Wg1gdEZ}2jknyl`6*K?LJk8- zRwhfOj~t1K#7WaQm}~JXi^q`8+xFLp{7N_2cMSMCWGZ|7h4^;i)(P8+h^UcPb*f;8 zla3OnR&urz2$S7RJexPlkb?3OV(5|i0TN<{cg%zH(R*4+ z$+DGY!pLa(gW;k-arPZkhq^F&VU9)LsqO^)o0e6CaepaMC#9TDfNGG^=MDwM)x-?$uUKzZBG} zI82VKk1>{83CBamR;=sp~A5eUx+pa%J($)9}$!6v7 zWJ&gf5&z}-lLUJS$5Ym7x8Za{+3#U7@K2Y!G0H0|^nZJTsbDz{Td>(+WbVDBmAQv)0#+qszM7tgrEpT3j>Ph7vV}84=-C_a?I{ z)xg+F<6?=dN34>wkW%x-CWKW^lAP;tkk@4H!?z?5$+q? zoHOf2=b8{n)Q=EOMOHg6WPAi)Zt_lF6~7gD;FX&BE8M&O^MpaEMjK+Oqq_puPWmQJ zZ-&jJvwO$x$W(^{;j#9Bk zH7c2;tcwQP7U*i9Pjuw~jc@h#d4Xw+jW}@oWTDQ-{@PL52H~@AYb;DreE~M~6U(Z3 zEyoarc9n=VvA{47kdE{T>yK1wJ}cg@OEWPScunVt3=7^*&zvVBzbG1n*$}JCGFGPP z4IqQH(6oZnBK{VNQ+*cDqig3?x2n-BSebQ1>X#|R(76eO=r^ zhBJgz+&{&}D1*_@ApJBygDegFU>)@7VugwFxA@#L(gh`)u)?3^tC*})VAxJA^yf8b zdaSyDeuYnh-lkNQ`Vsb9l_rx-DXV}FTmzfItwJx@0Q-JD6~1u;pu!xFu!t|wqa&xO zBzMKmF3DOpu$mKPbTYe+6Zj%Duo`Yy748I9G!ZO=TQ#LaMkB)K=Dda-vL^I*$oJT7 z-q=0Ulh7qu zlNnu&$2#{wmGX<6@+%{;O7-P8<&=C>C~x|XN|VQ<#A4g`2Y`00HQzwec6&fU#fAm^ zo2vp%qXVoQon{1jKf<-&aOLxk!6q%=Wut_i8V4le%+`&oRsUicq@PM%K*@$RRS+&v zq61IEbbdG~76IqZf|}Y}WLl^&eW9WBUK{NykXd6b{oJ>Szu^F`Xba_*&n?-~A?!5_ zH*T}kI(9$80A_w*pj467G~v&~QnZM4?cX;AiL(0Dazkc^Wr96KL9aZK#fwx{pN#=O zeP@nZc1VV4EL^vp+l{&>UDeAre4k&>ji3Ev+ICD;w;&nT3^6i%?`n@W2Li@u8a<7# z-|g>3eUJ8GZ{d*Ih=~Bf!Q+_1ZB>5pndmqtSC^^V=>@5@Exi5-GOVG#1K0f&wJH^d7k=^ z|CWTnv`bLm-@ksxQNx>!jB#C3od!-Mh2yHP^s4t(kIrIOByrY@cywH&ymA`?BNovn z6+^?Zl0jA*l17(XW|!i$%^IPjGyKsXiY=tPR#$l*CX_RdJ`J?TtzSSIF5xh)ICAFU z2C4_mxSD#Uj;bjuAw#%3qi~A1F%`u{^VNsgLjV2-$J46LD34c1YRSCL=&j-?TmKoo z1AIOSZ9C6C$d{ zjzD?MO3wi$APW_@EoB1C{Y3}x1XkPmqKQ+U-~KJWxm`F~J}^(7#So!v^*)PhPFd^O! z{I~9nWv|LVQSEIo*b1s_Pp3BN!zZAkLfu5au18%p6(UM6k8nM8=hb#`585N0D=PUby6R!1~G7?U@9xBR30)*7NlG!-Q)aVHo}VJIg~jw|}Ob>`%Vb{o77% z|C}uuGC#Vs6)$hW*WiP>GUF7*jObwE?)Q*02q0TIgq=<41Y%L9D%$;2c^ zc{UzPzSyEH29^b*qa1ddWB6>7 z)yOYHwo#O;GLgM%OuF2D)*h-#4Q=fXj|+;n+V|6nhl6m+CZ#H?at$@rJiq@U>mGtc z3Aii)mu=g&%~!5hwr$(C%~!T<+qP}nn)-XXXJTSDnaj*o;>J1OO--fWQPWpdsnOsU z1pGNWA1^wJUP`?yiL6jnW$zxX*45Nym0bKsRif+k;CJ2rA^grXw>*-X!?rA@vaVRk z`l?|5CXIB9i6@9xTkSZqK_r3sZ~gHD7yPI#cg=N+t^@iQ_lIt3imA67`g2Y>PT9#n z`|$p#4}j!Df8wDi3BP#dOTXvrex>%2XV%-*w3iuuQ_t_4iGgJgIkijAPr=jQbO68l zKmx-h#;f18hC_Q+^>K|8J=ew7Ns!2*N-lNUT`$2(8>COw&$If9Jq-?)yM@~>*vrSq z8}|oLjaH$#8g|3RKNUtkNdy0~S(|3C{_p5{pC%K$f%MKsUed-rj=C2vf#O;B%H6EV zGZWbNAkrJOkD)H3bd?r_a}}sEMNaYLd-KKp31>CPy{a-NsauuIOANBsd0;k{Sg@%o zH^4s(5FLlCj#YGFm#M2}EgTF+*%@uUla{{Kz2=%$fr>Z3h=eDd4Q!ZxrfOATYuH19 zUKicFj~4xi>6WkFBS*(<%X-YQc0gE;FMuh>V}zcY*DLON6@(3qFhZN&>A)1wnMr>u zY%<`Wh2IJ#%fikTtk>O>^fetfKS+~XD7)9*I_kWHoI%Oi^DBkWHL0zo=M*d(3NxH> zMV3xRW{Vk9t*~CCumx`mmmN1T=qGM(37J#J z(8w)PZta4OEs&sJ&iXW6^fc&8%!-pdb1+9UwFlHcfK5%??$X=!E}4Pa`p zsLbS(8F4N`Wc0pXJrt|gfn%4n0qhVUMN48IU``LGTMmZX$k(q75)d(x7F=ggE(K6V@!Igv3(oXNk{JI@(7 zYp+U*Z+~;W%sRgOx*F||?wQ=P!|teW^Sdub0gHGPB)WDt|HV7%VV=A9G+7=(GIa$l zfD}k4d1_Zn9H|o^Z~1@EgW`x30E7%o$MOI(pV+38x<-Yd{xJ~swF6yjr0v?xKWWy? zC1xG}tWLghopwYdscjG9$E58sirW9{2k(L>?kCT3+W7Q&F2d7qob)}gt|=ZEh1SFH2tKTMG1a}o zjHT(ip<*kr0*~{%e@%Fd;*4{+ac29zGVOQJl#bw^7M3?vGwO25aML;-kk3htp?WFow<6zj?ZS{q7oFtn;j4BR8YRvk&kLi&)cPC9tC3OY5N~**>r5qN7rD`sMa-=bc zY}%g~NM^5KMVSK8#(47t;GgJMc1fT`)m&CF7z@GVYR3zUG;lZ@jMDSEpxu*MjbMQ1 z;uhA+NSW}RK}E|`EPo!rBfl%hEE&NWVJMd88_}Fn;DA&U4_~8WuG-hj2!0qP_Rvh? zu4MrEOE@56C=RDfDl2B%%B;&&$wo!WQ+>zP8)VBlTmSAgSloI!Gc!i#=QL0wgp&}B zILJjCX7T%{=i2*`X29z}(0ee{OsBUgBW{c2t7z<9g#Z14@%< zhAgkKxkNhnV44RO|8;;y+qDx?xcgUGXY$9|Gz6{D8N&ND{U-&#USCHebJK!ML>r zv@Qz?e`z=BLJ==SjYr$IkBy`K3XEHwKufPJ(2vB5V`7Na0BU^F z<*e-Fwwnez-Jfx)|e%2S}ux zl>->czy z)^!?3;6}zbj!~A6FtBRlSzagdCR=?W`CyXRqiAUQKiK$rVbKbmysCx2VVes1T^&am z{7tSKCgA%F7-iuLF{f$5XRMB&(osyLSY4dx0f~QOI)vI9I%f%nes;D(}T?D|=`l?j|hJJ9EL;6}6QN2$(%`KgiYlj7P_Nb03B2 zb?vcJ=^kc|$ja|j=f(o>z3iZPh}o33iINdoR(78es0g}&*!0J5v($dvjckh*GTGli zWCxIJJVr_E0;tzV5QVejP4#x)buj{xjgFRJ|DcRKwS)@U^qSo84AJ^nIMa?nCr2 zzI@yl*>YUSd3gKzW|J87mJbu(2&GPHe0yH`_g~`k}ae_1z8M zmfy$UqL(aHMdq5QRaL5OZYP5+Pi*C`_t)=>!VH*O?uITpb(qR4h5oA2Z)xayTOTct zuVZPpbs}{GL)H#roi!y#Qd?51&Op!VXlScz>Tv%cecuLSQ(s^!jh!z+Q`eMp2@csm z4Zn|9)vA7CGJw|Tv^swm9W@<2U{hNvc|5Jto?qS{O|0msw-HQKYjxCm*gt(T!TP;n zKfbR=SD?EIrD^xrf4w&~Mc?`#*{xRBXmj_}cgOJRbU|tNx!*Pj|3eF=PqboZfH%3IjQZ=2``l zNaAGXZm^6&(c$mI(mYtB*X{?eFnU|#$cI7yn~c!;Ba0>s1~})dC4}(=RQ_B68te608C!3H}I~DUm3-%WF<8@^V+4wtCzQ zJ=g%c^M@X&#yYS+kag}Ybrb+?k9`W}jq;D7_2H_W>bavJ&|i;7dpK0jLTamoq{-R2 zG`=_h#ehneAcjxH#|g1CZxGCKU|bl0zdL?Ze%#V$rBU$qo2c7b!u=WDz8j81E0XBu zxNKXteX?D9?w0_T4r~Gx1!~a#R(Y*q-|?T5h(5Ml0v@2p=>0tS6!Axr9<;Jl{OJc! z`ssqE!Dt>;10@Ny>XBBm6Hs~X*#-83hY<1ZNVP(R90s|bW`EV>;&aU00dE{5nXxO6 z*tvTr_t@QuCD38Uz}r8$_1yX_p`A+U@L7KM34z-W;)u_F6fzB`v_gk=EC`+9cL%$E zldTi74@nCCD8ev8hx;%P6k%_W=vN#xxl*>lW$5}V)Z7;R-K6tD$k!**%kY*wIN|KR zW^*n;GX`qD0zuDnO9U~`npR5*tqV`HD%1}^T3okH+|?TDRh}!HRNO`-fVDJ>R~EEkCTO@= z&hrBV$tj+;&*dWJ4+0LIs=zai?CZA}9=GoNv9!BHzpRdZ2f`16uBu1j4=F(4p27FJ zs7K{DqbD#Jx=d8Ax508LiAjwN3$An5p-HhOv~M$8LLXP44qjTfazs)~;E*q!P55{; z##)6TpI*X2<(i30_fro?RlV21J!ryxt+!!Y2UjT*#H5F-CXM$TRpK$~v0$_hZYRMj zIjaPwj}*)86yG!y>s&J@T6rD88;-;RmbQD*e9;p$I``xc2}}|Ua@Q-B-F4)dJ}LP3 zn_3Ush)0X$ft|gL82MzejxmS2@{10Pb-M_ISDI;>L62H~^WL-1d9U*ddZ+*&V9kKf z%FbH?nPMOmCQ7vySA4@MHqo08nMo{=e%|X*1T3@%TuoJEG9HrPtrhoCT%NX@jyM2S zIOGs18%4_*epLy}J5B@Wk!gVQ!kybDW8q=@%Ea!O2H1clw`hyFABjPM1gND0vK{iZ z{lLFWkd*HBjnMQNtyJ`gebm0$ms^}J?eY<99~vwHhfW_}pQrkfDwf#03YsN!IQy?A z%4|M-?2A6>bMPPUz~$&kz(}|=?n+r=u^fdz=O3KBcq1FQJ0M83>9`$)F z6LWqc>Ez~;Zb;<*H=qLUgnI?uP55CXMAE%a4VLw+qwpUD4CNpc8l7Y&Loy`D_x5FH zA&X?ta;-*y_*=$Ck5EjmF>W~hPSp{nrNw)TVA?a9(b}IO640+?BRzJ_?h!#}ejMon z5<9mTyZm~_x3~<~SrcF$%WYe&eWY0KD`-upWe>+*o$*ZtIxsoR=O1g;hFw19HbpqQ&a^Yve82s8z4@_R^MfK^ z#0>w1yTUXt0r{GA7AW*vrlJs2z4z%m+9B#W-b@xr>wLt5ph~rI+6kclh6E)B=itXFLlDI4<4Iq|zcDh1t#Eq1 zk?=r4w7@IUL-D!EodiDS`8;>1zj3(%K)z;6y;GISeTP8W1~{?_VFF-?J+0dSHc`16 zu6=5Q!&Lw>!K8GDhR1-@=rw}yD1$)$fxFd#X%~cj1aUL4#ukY?)=uC6;aH@p0Mm3v zSW79_9B2>yK(bowjDo8IyxDv*x@S|H^Pjr_2=xKZf)A6}xD`<3xb$NMhG4?g*|2YCJI^ctRgiC#JtnJ>Rpdr&fW~Qsn3#4}MEqv*eVWGn^QxoA?Ns{D z_q$o#c8X5B+rnucE_6N@f;f`^#C92(u($&;6d3^}wkAgm}Uu&t=-vO~g%kK>{IGab~Sq%|JoZQN^js@ec<+i`N{l z2(8QpXNV^tJMpX>qNrmA(Qn4vjg~+c=@E13+ph4I2EJa?5cuUdOy)z=R-Ew&MA-k{ zy}Ki9**+Y=*WQ%D=_!|{K+nqJ;#)DMqjIs0C)OuTMxe{(itc#4@8%jr1!=H zVp40SWNB^(Ee}~H9V<3Oaid;pI&m6Mu0G3suRq`Phd>CSgKYk2qH$W&$_~sFG2T6u zzw)(sX0ZrBRMQB)=|I`)qU>PvU6xQB`{%zm{{>nOq32m4Tyc;H0K)vOd$*P-B+sq@ z90I+9+eXkML3lF6(0RkY7DvRRrySSxqMiH8ow^FjH_>$qR>`#Ar3&O9 zwLz9TvH`V$Rf$ZzC6LLq#RXZqvfw`{N!w_=# z9w40H1uNcy>TGrV3`o*2Hk1$H%RuFO0VT5lsS%-R{8iJW`SW=lMF3Q)Y!ElL|c$aXXUzrM=2s&VJ zn=Tq^B7CGu$TRhs6VVu+^`sJF>8o}fdtMv70|-}9t0B4i!>g&^Vlp@p;$u_b>B;hveyS*;NP`cA5E|l=UadSuEd5?; z%Z+&1;A-N|UXGUGNSK}1+J6-nr#{gqI!DiFXBm^e$Y#ASL$h_%mDa$q&*9*9xMmCW zg6C@S!%s8|c?U_mUv*sBNr@y5y}RRugNoTjtOJPz^kFEr43$yr6}^0m}DXG>5Rg@8Y7)ZuK*?g#v^2RR_~#&B_YV1e7Qxn`L+uNOTve@unwi z$}DC1c{qtNdUcg&}mG`%>6eTLb*=>@;iUlC1QYwY$ zK$2J@NkvaF)rN;K0(==t?SYo8aZf=#3Yzl~KoVE0;})tQo@kARf7=|gH?*zC%7x|a zY+gDY#2F&Q{;~y`>lcFE0pCDog@xF1jS(^LEeBzu_|l1ULP4E&VkDSs666jPwt{~! ze?y0A&Il-aq`)I2Wx{LI&&I({0KGUHFC6>pnVHDh4kIT2otmg(_mhB-epkzTgH8at z&`)4e6ep;KWEGWpf9VzOYD5 zM4(q2i7!dDuN1V>BjGYLu`}rK`gyK(Rc<*jutYAC39Q~%GLxgO6jeq;4wNrhRAx0fL?weSKr@+ zcq_7w9*_1&iU7hk3t3qjK>?)dXNHox07(Og=9OHobAo0TFIOce(&nq58p#k`Hqv)A9>0l9m=RGT7MP1sRhN-T=cUdhfmtU&8%Fd zfUUKiLGeV2k!@$5P5N0Az~EutE1K@NK(>*ayM80*Xg%w^W;JeLA;ltSHd%kf1Eut( zQ!pM;bnl1?u!}Q?1Uup zlpwjovGJfdDTuM4IIktdgp~z)o`iTicj15PC|CI zh6mpE7=n9R(+niM?zKbbwF1|EY+ml)PiH@|;OvaQ3a6 zeq);ATFzIR@%?QJTu-cj0ls$nlbD`R1}%p!y31+Qm*_xG`X^X5)m1JPXV3=Z$JRu7 zNc8*2f%@WI_Udv334A9XK)0@Rt*a76pWGMS-m3%n$Brmw9yYWbHda~XgZyQ8N_}aLQf35X6YckS#SJOH4wlq zKa!Bs+Y*G!%J=V@b|AjgCg&yF&Ds;-n>hdXoub|i%G6t)UIozPjUlS7zpD8xNh%Z0 z^STe2ejGFm+-y}{;r!D%>g9_QQq+`&N6AC=6jTo&J^|C*4o$>|v03Bo!KD`(dC`}i zqb2#z{G?PIdIVuuGsGqNrOw8T$$rr*n{$PN;++`JUxZ{T{q_>AsL!{Q741AN{aENr z^b^I2tbfg2|IrlU4wO1s$08q zxTS32r+4ZC}Q3_R2U6m-lA9u(r^+Ti8T2F1 z6h^0o9{2V2jIR?GR9ij{NjBiNaDsDqf~`9l?gwt9FBFa)4%2tz_4p4F4Y6C6ZGx)j zqnWdRor5Dd@*zUX84<071Rt)TF2}z&G_LiMnAsa5SaksJox#Aq#sh=WMUxNb&CyeA zs^q6(zYXoTUIk3L)@FD}J6aPD#l@pc_JrlBDk21Ix@g;Rz?}k$B4&S*^>#L|*xYL* z22>Mlb6>SJ(i9{`u0qX~Q+7W`okRcIU>(p-O5u7SpBvIg5h?o*zfzyqDUEk2++Fc4X&AUD~ImA z6sllAAmN()-*5?OxVz04;Qwf<@C@B~nmDf_JdjH`D08WWZY$VU#ODLVfTKjngT99d zKLEAMi`aceE0UOKDwHc>h3@v_M@Z#gb{LXJ>u2kC>lc`vbT7x?5sZ)%EEvNAyBw1@ z6QYg2$gcULLi)3fdq#v0Dm<(F68wdJ%Da;W{w_)}0(p*b*BKc&op1XP*@qt z>vFVGYL}3aU(cp>LSEH0#j-HA#ujyx4DwVt{;Zt5h_2+8_VsALP08QX^8WSzElKhb z?b~$5#;`2#R$(i#p5PcC;e+6}!2W#eVMl$B!*M_q))+n_P9P3kCWi1zs^Ht&MqCZ< zeO(5@9)%`WnuH`j(3>{=TZ$okU}wydc1a1RVT>mw3*-Mi)DQk%tA0>8#Z7Y*33*{{ z(1}gIj-2NC_pBcIE4cpNQyBm6;s4x|H+V$HoxHq*T9yKgq03YqA+Ye3jTub@gPQjs zgBowiq(zvEkVIik^L6B>((g{PM)@nTk%cMikBygDEbkwuatZq#u6HLVF~)V$t2xgV~Z8T9KiP>A|Sj5sGy#njhk4qv9kn zy^3J7H?jVgepo-RlokJCPdWMxKbm+uGhqv!E(hUCxgNZHL1u+iphfPokQ7Z%YWW&^ z_MAiynt(`n9L!vpxgOMyH{!DdIpPm~9xK3lh5 zYqtZOSGR(knL(Fp8uW@r9l)YuF92Yc{+0V#l(@tu+w7Mgm!zd{_NpP@SRb&5S?`mw zS)<>0T`}k1@0T-&l%${W(fMRx#HNbaucZgF-*LFv=D1+ATg4<@&QYtt`!0|FCivKo z@c<1|A_$es7O$C!>;!btlRUlDUoZO6G(bcwrfm@^~G+S1;uO{;2h(M+15h#4bAsq9CYrSIN>t%xo3_-+YW zs65S8t3FjLKYtam=H#D1mG6?@0HN9&w90pj=G%md&(Qct)iZ@b)LV`?#;fZEsBBP$ zBK$%+hZ}v#>=^swaE~$@gU_MxG{A6|?)-IGgf;M!$eCaqNdYdtzx{|wBR`sdvkyWeCxF@X=)!8`hJ1yaPWzrq?i5(q_YwjFT^Gi z@AIpFtbvllylLDYQ*eGKfdzaK^-|){=rWa}nI7rLRx*AbFH8CEV!OIxe?QF++sSA4 z#p5>u)XAq~=s6;-g$$XrleRt&v>nuN_Pj+mDlSvPx{$w7NcfALOivYt9@3E#ANH;> z5%S|S^kH5meILh>Qsn6t)}s0l;EjZ?1`V3ex1M+)i-YVGU4)B>>ZORN-H&O8IPsf*@L`J|sen5=$r(m=Aqsj9xVqjVO2+t*RT6bH44s)Ar z!{JA~Vy~_7BhrOx_+9ub+irKnHm@C}uVkjT*ko0XSP|@0PK?NwTj+i3f-pME?DaNw z?TZ~M{AWdhs*RP_WC1up>)?)pa+k_)P?~e1YQdQMU%7E-J>YjgBIKe>8-R+M>8Jv$rL!HjhXf7+-}Rr;)9BD}Uvx z|Hj*OV#}W|@DECt^6m6HQIv08;K~5HcFE2*Emz?FA`Xlgbk6BJ@Ha{lkg=V|Yz=$p zcQ0(K?eu*9tv#R2_s9e5>=sT&0XN@?C960-eZk9-60C`Z#hF`XN8IU>{pqs(DLT4G zSyoEvkCW6_$*@g`&Tiz`qvCmm2?Q)CVT!PFT0GI&eH{!9OqUzUN3p4xweBP+ zH_{KqD1PkPR3}~MNs~#}Z#4`kTDumSsumad6p_j66=3u>Z?1PKUeU@PDRwNLW+}U* zOKbGcaEj0CtQsulOtwrmn0-rC+sk@i4`(7&bB?;rX80m2Rzda{*Ifi(3@2SKWw9M< z`>^!M&$mK?X0ZNmy#E;C9b2f!3ixi!EU1_AVi)U540dD&bHJWh3yyRFzsd!RFW+n4 zV&n~eL>ixSK*Xk9MK+@QEA{GPgsB6nlRoGLrXtUO_Et=yIrp>wEGjIn0(FDpNg=?X zpH1NmCo2w;a=Sau0vQ$AS2cr6Mnp%?>>!zJO(W3sObtXfaY(ROo&eEYzY~=cuPBoo z@;9k9!t9oU9cnD95hsFoIZ8CNghy1c$`*gZW*slMtw91O~Sb@Cz-wPUm?I%3UU z(WVvO=VZS6+_>)05mDq9<1Pq=C|by7?^D1po4O16)3>1?$g}(|JB_AaZy!Mh_hgK%xWEPht=I4tuC4Q zI#hH5+WA6JeWtyncu*u=VXcxhx`R!&*6Oos+Nqz;AEmq{=s{2nd#Q7>csrQBysT*J zZ2#eWt*zB2IId-|Q-=qWXo3PI6mA!GReLzHRY$D1F-;J;WoJcWN56(#&4mT{9{^N_ z(lDTmL+Km5Avf>90K4DP-#zG|vgsBFs!3E9*5&b=(shq9WzaN?F-Kn%{lrh07q4VCqL~qtj4cmg+rMQGUc%WJ~iGM+l^cRCcPR0xxt{1O=dL^ zWSv$E`_a!<2#)_2WaB&U1r-H_2tiq|;g1hG zmorysHvZb01pOQ(lNJ&v$r%|K@d#~q&9&Xj=R+X~lyRGT(_*^Oy%TvMOSzQxUdZ>4 zheBWn-$hei2d5{lFg^NlswVjb#E_JvsZxM?!2(%^P0nRMP4`sbHnUuOn8#qd5?=mwEuA5s{%8r;kjK8M zy+5>eDedyKfNy!OHb3ZqbNQqNN^pvNt$?69^I&!WU`G4Gt=XNx#P7Ieq-8qeCddM zruowCI#a-Cx?)m&NN)M5yRSq*U5PQ(}u1Y_(9Lg-cU19wkt zc0n;)D09Agc~e*zgA?|EJp{C$?93j!--6zEljaNY+ZZC`oIdS%J@ntep1PBlcN8X@yJxznWO7aT_UNcxwfdP4-Y3>|-ZzStZkG=H^(OFKWo=LzJ|!-@v6vIP*tgKD5`$Cu^|eh7EG%pTIurxb))#59069Z3;oeK{k{j`YhwXxW)FP)>|kr|}QBo}nD(13%9~gcp6rp^{I*_vs%^@1ICckl%() zicN~+{*2s?^zI6sFa}&>{!tIVGoocfNPHH^U=B8e1rVW*lnah*T$Ab5<9)7#b_+h# z9+@UQ3=dH5>aFNB$5c#--qFp9+Ww-CLpSz6fctXnhJl8nLSaNW_WqR7U-DVQ(WwE$ zTa&U8wvKa<>3>x}4pC<}Nz7T^kT z`8Euf@l0VO1lI2abFLyF732ouw*BJ5h~>Ig`U)BXjDhxsNX*9+hjX7oy>SR4+o*v= zoZu!>hg?i>4!F4ZZFn;=TC#^?)vj0WF#7NcMhA1N;3)Y#e0ikHP8YF;e}CreG~@Ix z9$)Kt>DfA_ev;Od4WzP1*@C_kF#Jh(e&^jg+F{UYKanp_&;Il#PwTV8Why|}hiU$d z-Sn}AlpThd(<=x7_ZvRck+zFIoY-p2e&cGOzB5{iQBv5tu>QhG(H?76Xqc6B#RFqE;=fP&ACk~D=bHpNk~sq7$?P(~ z%?uy}jTsNiSD`uT?^4zX%isT^`Va#T*H$V%@{}Xl=cONocu{8ffd{CPa!(;mQC2WDR8ws z-g9Fx9mq|Wjs4~#_5?i0T1}JlKp$?T5&Q0s{I`bD)j|#YVx^iRV_}SfV zo*eL8W8yx8V7Cn6kG5jN_HAn8k3NE;;PS~3YjBPG?*3+c8wJt>oadZo6VKLj@IuBQ z@u_!s1bBtW8t(yL9ejff6yx)xFmW*xq6)@)tc%0lZrP%oS!;C7X_j+ft`JW@bK9N~ z9gf}JIdHz=%5qNSfiT^yT8K3}h&{yBEc1&Ym-s0bQ_=aNjivIyneIyQ-54mlA^;mE zbEuC#y(n9-b@AceI_)%Nr+2QHvfmdu(RZv~Y9Ea>*_gfmP{hSD*14J>6-b<*k8Ut| zCx4mHmFiX_+IzfOzJgEND(1HJ2>((J6*%@=TfgV(vs3Lk-WBkm%u@p6bzpD)c=qgx zcsCif&=i>TaQlJYo(F-w&mMlhFYRB2`Me1~?7k~@4@@T$0_Fgn#Ci1Y@k^prf9Ygx z!6hlS(cOH8!CHmbjTMfy=k<;XdsA zI%NB`?f9}LH{*NEC!y3trXwU8>LVYlnz1Kb8H3f_rAp4tnPqMDA%kv&xjm%T({1RY zU7n)8iY#co+z3WF5PAQI?>o1=AU)JGTJ!$Tnqz9?Q&K@^V9s9O$4e_MXO(#}r0DtO zvO;%!CR0Xl%xRl4qZ|5?;^7kItgYXZ^m@y8PbzC_h;#k(`wL(%SRP^aoF;>;qr<@} z7g0TxsXMAR6_%?=vikz|-v%>7AaE+(OGJHy*o4zZWpGi?%*EG?$@o4m1$403xy`%1 zrBIUibj4bQ&H-`>ap?K3P4)ZXS=x7a_!69#@YE$Rvbz;HJ0RYTDvD7^o^FpZpTgg@ z@mO|ga=Y|a;c%ACqbNSiGHe^Jh_!}SN?>cKdS(T0e(OihNa_)Pm*v&WZvhzESPMoMPV<(4nUd1Gt0Pq2`GeBr zXVYsqa@ZLAc^I(9l@R?VnSUbdl#6!^lz*>9Y@f9pR7teJ1SbV*7MoJE#uc_EIPPjj zq&{=7YSKq93Up<0Gm;L1T?*t6?nh{rPGn7tcSz1eCE0RhxcT+xrf+*Wz~WACie=L5 znT^-)+y*e7q2I|KlpXdmu1d@?WEQJ6`@AWf&+E5+Y_EW4jG7C)SY>8qhbWMQ9^nil z$&rejvxF29L@2g0DSPBPVqIhr>}D5Gr7|O(jh}jYAb_|rqz5=^NF0>>*eUT@4!guh z4FPq^3!x>z*Q;LrWak_f4XD0k=62KkeTc_iWTP!HpgC`X5$T36hZQW?>t|guxTMxX z_H)MD??yF_UqGvcaWJ+V!?trR>vATEP3qO35;J*A+gjBu4_cDBIt|N=`7q(z7UM{q z1faoOD(gqLw1ZQMJ;2bJJUo?j)G}*m%zWR7yMf@Q}IL z+vKwxGZy7=#VKPdZy3%E;?FrYGJQ@o{D|Oht8F$w5)Xixfkb&qYgS=4iR{k@R&o(} zI>)%lM`vn9Y~&rhV4T)L|AYCNE=}C&AVUA(D}OqFxASRmK-{D}iEz+3_g~tvtk>D; z01hDc`fxK0+AwHky4!}hqWJacUuyxAjUV3Nc#^<2bm9(CsxwjpZ7ahYW+=B`bqp<#TkXSv}lPw<)G zyauxkMUqAa{0`rTbAH5||EM^UX$FA+C%Gxa_~K&`ZUySsT$v=oE%1^r05h3v8N$#* zVLXcCw$!y`rWWSNuUubPGn%&z+tY#l`OxNaY-?5hdYBGJ zm?wJ2+z!y8qOy?N*n8b#{1H?Bqj_ulW_zi?btDcw9C?fq*rc~eP2v`^qCvv-ME`q5{Vh`E z`w1iK#HaD=cL1D_?CQ{HUCRDTq?i5|kxq*FsEI42q%st8)1nQIfJ@b6a&fd7XU%m* zKyOB_AXuQ0$VfR~a;J02^wAZ6SgG<$q`w&Fi8-A6$}h9Fr~5|#VQkT~^M^G2o}w0s zr6Sf8$MgjWV4gZIcU0v3bG1fZ?-nQ#`kjv=AZBn#X-K|%5=|U-dGou>cSLdd`g^;R zG}M@+vmS_VrAo7I)sB*%P3=KTyA^zLTC2%K%5@Vj&Gyf?z%lL{VCKnZV7mWVRKuX2 zKyX-qzRtSSJ8Y=?&NLqxl}uIlw~>j95LMxcg(54ckwX5Z-jFtgAwd(!0M~(03*Hs~+9RhahGIA`o$o3J zx-;zKltmp|X+cEAU`=@xCZT?|mgI~4zX-qQ>t=R@zLz76KDhPy0~dTOXHbRMeLx{R zL$eSuk}!u6(Uim{CRq+ZEMq+S z3%wM6D4q&9+rr09!^W_+>FD_-LixVmNM~bvL}K{O^H${%yw~Apu5jvscllVoiESpQ z<=$s}iBh-z{x_%4rSdJS@y4y1&VZN0wjj@JG=$lJl1*55tBvUdrs}E_mUZBa4bLbN$%v zmCn3sF^t(#w({O=#pB$$=@Y4 zoh`u7Ovzy1+x{K3J7c=86S&8plwB+16DYs!soR?bbu&d9)2n13qlg+pgD0QgjoA2E9Bome zP|9uElS%vSvgzDd(2TTQg=w+vhDxLOOu;eG*Sv$?C8ipeLBAXjJXF@k)uRG&pJdon zc0SeS;SL2eu~{eaIzy9mmU|+dW-Di|?}3P$wy4K@Ck~U6wET++;lb)C!Ys~Ze$ZoR z*mMBbA{xqB_-rsN8CFyp?60=}2PrSV`VUfWk&|TeKS=q)=Kmn&Q&)m*;>a`PSgBS2 zPS5IZmoTlk1Bu`;kfKF|lZezp&xR1ug9xgq=piZ;DDGc3Fh%%KTV%x%*^vmD*j$W~ z0)yHdU2z(PbIItOvAI_n8^`W)SV!wh2(=Lbdtg7Q#Bgk?3>`q8Yt zeOdi~O)uQ_fQ2E4BjAerm&9TOM7S(`^IxMc&_9bc0yTI?pd3)?cka4W>H@Kh5a=Rc z)llSiSB8tr#p@%x?eySic zGIW zKF!`~Q<1uwfmc~DfXLkR9LiYTcmVhGJwfyAHQxosu4YNBQ&N}a#GvkI*qAK}INMeG z9~rTAjpc-68p$J8^!|PM)Exk0&1FG|)C4ybXi)F+jNDIJ{-QW7GRsX}hxse;U?Ql5 zp53cY9n3@wu85S@uh=T`9i9#;2jXf&Sa#1p(E@#ij7Ep9$Rom_xo{Iq z5;pF#QPhYCL(5{*_;hS%@;nqa(vDPuh?W(ObX=HM0@t#BI_tQ1W&!YR4es!y$>P)z*+>X;F0f}NLck2qPJ({I@$<>Y?er6%}+ z$suW+0+A>XEcbsBv>?r7WTExPZ7|yd<$E|$!kq4U-bbMqqpL?ffs3CP;&G8CdVcvY zlfqDMzVlveiL`ZtiEM==x~<`@A_4>T{*hT)D2uno$mS=GQ9y=jBci2fBNgVi7Vhn# z{VZ&tRcneXG7-W8mRhxir91vpwv*R&FhejW2EaH~$l;EnU9!)Sec&~_5ISU;1~mQ` zr9AI{R?6GEV)+&I3`jwM;MN~nFJiyl+{*u?l;4^Dhf?nFzfsC{MIkXewPmvNtF@t_ zh<}0!8-yXm*t(B6UprkB3n}lB6=n=41XOz0tf_6-1ClrJv+xG5P#hmyI{~fd-gx9a zr5v2syED2OPWB2tvz5ULP@3h*QUir^MyJWc3c&f4>=OP`9XynAQd0VJc0%pZ++$%4 zi9EHnaI>x3wKI;D=Ny0CHcr9m$$0p1OKJ%O)wibPR#Sit9RP0%fW=WL49Ky=mrGql zZiAbdj*V%&_}>KX_8#l2hiqz+wy$EIoA|caOV)mPGb_(?-5W{X@-1kKY|}tbzW{@C z2W2-{|K2Effh<^`@3a^IVuD6Od*NGfm*^QwXy zPpDt--uY|lB8>Mp9n0QoQ5II;;(*_-$Y){(M-HLYtN=dS3G_>OqCdCY;j0nHWnf(# zU)lqUCzd)T|2S`4(Q1Et(`_?8ob%Uoh)MVEuHDjZZok-}XaCgtgcVR_u#+PP!T zJHin@OBaPd+r)lS$_?B^rlrAA516zkYe}sEf~&tQ1LpAyk+?ILp!zby5oF|3Z#7}$ zLas9#ua{2tfXD