Compare commits
615 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b41efb063e | |||
| 23db199262 | |||
| 76d1f8bd18 | |||
| 550f4c5fde | |||
| b3f07f4587 | |||
| 56980745b4 | |||
| 13c1d20482 | |||
| 7f3e144cb3 | |||
| fe8bd2fdf3 | |||
| 7cdfb0d1c0 | |||
| d8652d27f8 | |||
| aa67148247 | |||
| 769fcdb1fb | |||
| 0f4afb32f7 | |||
| 398787253d | |||
| 7a6e29c347 | |||
| 6e628781c0 | |||
| a75a2b4113 | |||
| 216e878231 | |||
| e54b20fa68 | |||
| a120057ec3 | |||
| 737bda44a2 | |||
| df65c94974 | |||
| 39ee5112b4 | |||
| 96afdeffad | |||
| 669d91e7e9 | |||
| dcec53ba00 | |||
| 9d9ce4a68b | |||
| dc24365ddf | |||
| ba53390547 | |||
| c860e7fca7 | |||
| ebcb74a86d | |||
| 65bb20c965 | |||
| bdda4abf56 | |||
| 47e81818bc | |||
| fe015b7eda | |||
| baf6824bc4 | |||
| c1ce92bf48 | |||
| 4eb7e0c845 | |||
| 766ff3f8e9 | |||
| a855f1df2c | |||
| 0478037b57 | |||
| caa07a8007 | |||
| 93f2c61447 | |||
| c8a5c43232 | |||
| 2aeb1a0353 | |||
| a1ad772642 | |||
| b8f850b6f2 | |||
| c6ed2d1963 | |||
| c48a2d68d1 | |||
| 5a1909aab9 | |||
| fc81178504 | |||
| d3f63e91d5 | |||
| 70705f4e9d | |||
| 8c66e0ba2f | |||
| f5e0c6f004 | |||
| 2a140770a0 | |||
| becbb63ad7 | |||
| 34d3cd496b | |||
| 1f9c3394c5 | |||
| 005f002747 | |||
| 80b8a6d8cc | |||
| f7265c39e0 | |||
| 4468c36b14 | |||
| 25841c787e | |||
| 9461ef3a5a | |||
| b8ae210e4a | |||
| a3225e5cd7 | |||
| 0777e6e08a | |||
| 8b2088fd61 | |||
| d38f409351 | |||
| 58b8a2560c | |||
| edb7a0f433 | |||
| 0f73ffde68 | |||
| d3be744244 | |||
| ca63d60068 | |||
| 8cc3b0fa33 | |||
| bf201e317e | |||
| fe11fda832 | |||
| 281faa7a0b | |||
| 7962253ebd | |||
| dd14df086e | |||
| c426138971 | |||
| ff4b7a8acc | |||
| a152f9c074 | |||
| 50be8a158c | |||
| aa291079d0 | |||
| 672bb9f460 | |||
| 9a75007535 | |||
| ee06965d2e | |||
| 8e3ad22d92 | |||
| c7f6190cff | |||
| 5ccb1c6fa8 | |||
| b77a02662d | |||
| 13306be4ed | |||
| 491f81c376 | |||
| 703c01004c | |||
| b1d34763d4 | |||
| c4aa200f19 | |||
| a099879563 | |||
| b6569762db | |||
| edabb2362b | |||
| 8a5d4f0d82 | |||
| 12aed8dc67 | |||
| 7d03d4ce0d | |||
| 2680dc65fd | |||
| a1e2eed467 | |||
| 5600ce7a77 | |||
| f28a2b1cc3 | |||
| 1a07ec22b8 | |||
| 7b8671d82c | |||
| 0f877a3e80 | |||
| c6e55c1a36 | |||
| 40648f6998 | |||
| 58b46813d6 | |||
| f2a90cb921 | |||
| efda26ef6f | |||
| 433209ca9b | |||
| 1680891982 | |||
| 3c965ed56e | |||
| 0bb2c44254 | |||
| 23cdcaf5c8 | |||
| ebc7396334 | |||
| c7234b6f13 | |||
| 56be9dec59 | |||
| 2fa6a98052 | |||
| f03407fdb9 | |||
| fd4fd3c4f9 | |||
| a9513e6f73 | |||
| 5f0bcef5ce | |||
| 96049f41a5 | |||
| ba8998623e | |||
| d6bf3019f9 | |||
| ef037631a1 | |||
| 42ade32bea | |||
| 37d7e26929 | |||
| e51dceb399 | |||
| cee0129225 | |||
| ef36665d7d | |||
| e45e357841 | |||
| 6657501ef4 | |||
| 69e8f1e86c | |||
| e613fc269f | |||
| 5c7566c6c9 | |||
| aba0adf18d | |||
| 2c8e71e560 | |||
| 6f683d3cde | |||
| 9242d1869a | |||
| 59d632fd45 | |||
| c55652d327 | |||
| 0220689964 | |||
| d8969db30a | |||
| 8eec683793 | |||
| 4705389ab7 | |||
| db931c5d5c | |||
| 9a0b56ad1a | |||
| 3c20ee41d6 | |||
| ed245a0cf0 | |||
| 4626c4caaf | |||
| 7cda6d2ea6 | |||
| 2ec15984a8 | |||
| 01f035d574 | |||
| 53b01fb8a5 | |||
| 79c47b4470 | |||
| 089abec866 | |||
| 064fd6cb0b | |||
| 60d3b3d56b | |||
| 995838d9d3 | |||
| 1d4d4bc741 | |||
| 41a5fd90f4 | |||
| 3d9d619f8b | |||
| af38f0d1ee | |||
| 091a5fb354 | |||
| 8a1d6ce0eb | |||
| 0f5f24527b | |||
| da60c1488e | |||
| f65893d65e | |||
| 3e89b6b8f9 | |||
| 7531167824 | |||
| d24e269ecc | |||
| 88c18c5499 | |||
| d0f1e6ce6d | |||
| 10668f20b0 | |||
| 2537f0a508 | |||
| 1387772589 | |||
| c4f9ef2e2e | |||
| acce75a6dc | |||
| 5e0704a7c7 | |||
| 206857bc9e | |||
| 9e1ea5d7d3 | |||
| e90b105b36 | |||
| 40ffd404e8 | |||
| b6b9dc8c1a | |||
| 4a397f5ab4 | |||
| be927730fa | |||
| 81036da44c | |||
| b016f60e46 | |||
| cd76cec089 | |||
| 820c73dd2f | |||
| 42a4a6c1a8 | |||
| eec9a067be | |||
| e53eaf4213 | |||
| a3725b6b24 | |||
| f0c7370637 | |||
| f108840e28 | |||
| f4f63a7e41 | |||
| 26afd890ce | |||
| bfac815a5e | |||
| 95e8d0589b | |||
| 16a923edda | |||
| 9c227c2321 | |||
| 5d617da74c | |||
| 3aa356dcd6 | |||
| 491f7cd529 | |||
| 6d5ad4eddc | |||
| ec638e017b | |||
| 8f693f4615 | |||
| fe8f77ed93 | |||
| 5c6238f132 | |||
| a5b932d086 | |||
| b0e8b8a532 | |||
| 2984030f90 | |||
| a3c82e9087 | |||
| 56082f93d0 | |||
| c8474511a7 | |||
| 1348525447 | |||
| 56f9b2d9f6 | |||
| b18680b853 | |||
| 72b2763dad | |||
| 7278a36704 | |||
| bd9a895089 | |||
| a71e7923e0 | |||
| 2433e91a6c | |||
| 48d2a1543c | |||
| bcd75362f7 | |||
| f4488e42a2 | |||
| 3d13b60b68 | |||
| f1c54d1e27 | |||
| 27edb163e9 | |||
| 6d9d202701 | |||
| 61d99ef709 | |||
| 6f031261d5 | |||
| 4a57044680 | |||
| d5c0e209fc | |||
| 737654549b | |||
| cc34603864 | |||
| bad82ccd42 | |||
| d71cd68a90 | |||
| 726111b073 | |||
| 298fa7d5d2 | |||
| 4a627baae8 | |||
| 560e33c27b | |||
| d84cf0614d | |||
| 3f1bc2591e | |||
| 8e83b724da | |||
| 767b10f5e2 | |||
| 4e2b5562f1 | |||
| 8ef471b492 | |||
| de6998dbe0 | |||
| e48b1f6056 | |||
| 7074110780 | |||
| c90d272374 | |||
| e6dc203c4d | |||
| 195ee35eea | |||
| 70122a4407 | |||
| f2ca0697af | |||
| 9f196be2f6 | |||
| 254d86bc45 | |||
| a8dcea6931 | |||
| eed04ecdb3 | |||
| c131e0cfe5 | |||
| 5f01c72a48 | |||
| bf7d5e7841 | |||
| dff1886015 | |||
| 14a73dc932 | |||
| 51be581d48 | |||
| 0a41febe15 | |||
| 63eb429843 | |||
| 91815ab678 | |||
| a98f71ed0c | |||
| 6f8b744c24 | |||
| 5e9c76f476 | |||
| 4fd0f2a32c | |||
| 74d5d6e265 | |||
| caa53be00e | |||
| b3086accd5 | |||
| f010587201 | |||
| 2af751d8c6 | |||
| f36f9915d1 | |||
| e8f8e7bfd6 | |||
| dbe23777a0 | |||
| b7191e3dc2 | |||
| 56ce93ce72 | |||
| 9cbc674cf2 | |||
| 16fe53c40d | |||
| 1b581d52e8 | |||
| dd6604e9f3 | |||
| f173510482 | |||
| 905d9b9aba | |||
| 810056cd4d | |||
| 4d2c261ff8 | |||
| d4626835bb | |||
| f684883902 | |||
| cf6316e290 | |||
| 12caf12d8a | |||
| 9809e1b53c | |||
| 80b7eed14b | |||
| 6980dc5628 | |||
| 9366bc85e9 | |||
| 0c193500d2 | |||
| 6c68cef6e0 | |||
| 5a7a42cde3 | |||
| 0ad88842cc | |||
| 7c0f7f4715 | |||
| 52c38ec44d | |||
| 21de891ea5 | |||
| 154f29e5a0 | |||
| 7f07731471 | |||
| 1a0a4d7905 | |||
| e3bcd4d5b2 | |||
| ea4c9a41f8 | |||
| ac2c7f431c | |||
| c0d6e87c99 | |||
| 6162600bda | |||
| 1187539ea4 | |||
| 2649587d2f | |||
| 68651aac1f | |||
| 2c8f48fabb | |||
| c426c03624 | |||
| eeaa091024 | |||
| 7ef962f931 | |||
| 8480e0fc55 | |||
| bf5e0124ab | |||
| d56ad64cc2 | |||
| ab3f22c212 | |||
| e41dbd6300 | |||
| 29a8556f10 | |||
| 8d0d920808 | |||
| 1cdc9ff6e4 | |||
| 9541123fcf | |||
| ed4b789f87 | |||
| 18a3c37554 | |||
| 92b4b03a8d | |||
| 192c50dcad | |||
| 7b34eaabe5 | |||
| 2eb4278835 | |||
| 293d4ee08c | |||
| 87066a127e | |||
| 4847a3135b | |||
| af02e0c472 | |||
| da2dda0e45 | |||
| 69b8878890 | |||
| 7893e55a8d | |||
| c0e45a2e0f | |||
| 13a65c8dfe | |||
| 36667c1298 | |||
| e4f2299785 | |||
| 81a2679bb8 | |||
| 9087263da4 | |||
| d58111fa04 | |||
| ab87ea5770 | |||
| 6b62b41a60 | |||
| b1f088277d | |||
| 277eae75d9 | |||
| 20559e3f2c | |||
| 341f9c267d | |||
| af3039abde | |||
| 4d027ec405 | |||
| 3cd64ac03b | |||
| 44e103c0e3 | |||
| 7d992d1af8 | |||
| ac42953524 | |||
| 06759359af | |||
| a3a30885c0 | |||
| b448c4ac39 | |||
| 6a874fec2d | |||
| 998019b8b8 | |||
| 5a0b33fcd1 | |||
| ec55d7cb58 | |||
| 706f78d5b3 | |||
| 1218cb4c28 | |||
| 31bd8a3587 | |||
| 9e70cc5dde | |||
| d78a4927fd | |||
| bf246b6c09 | |||
| 5da2235973 | |||
| a9de4709f9 | |||
| 3b8d7ffacd | |||
| 3eee5a4a92 | |||
| 6fed5747bb | |||
| 442094a725 | |||
| de66047eee | |||
| 48da03a148 | |||
| 13a2a8757e | |||
| 1d901ec12a | |||
| 7115203a90 | |||
| 68fb60f223 | |||
| 1f064fe474 | |||
| 008c6f6d6c | |||
| 1afad3ab78 | |||
| d1802086ad | |||
| cabe9632af | |||
| 08aa9c8614 | |||
| 831bba5cf0 | |||
| d727111a51 | |||
| 8d785b762e | |||
| ad4ae230d5 | |||
| 54f7963152 | |||
| b59c0b671e | |||
| c6453a4cb3 | |||
| 74bf699615 | |||
| e1f94bf9c4 | |||
| 44a0745110 | |||
| 94b76168e8 | |||
| 55ea80b485 | |||
| 4e501e88ee | |||
| afc02781e9 | |||
| c2072e1cc2 | |||
| eef99b2679 | |||
| 581d54f65f | |||
| 5f5ea69a32 | |||
| 08800f7d60 | |||
| eb51a7f145 | |||
| 8cf09217d6 | |||
| f81945ad7e | |||
| c21f97274c | |||
| da67bacfbf | |||
| 175d854a9b | |||
| afabfb97b6 | |||
| 8e4554d3c0 | |||
| f1ea47f0b6 | |||
| 26f1282c6a | |||
| 4053321cd0 | |||
| 561158c7bb | |||
| c023745dcf | |||
| 6ba68fe87e | |||
| 7afb46cc0c | |||
| 93dcd07073 | |||
| cf2f507951 | |||
| 35a2ce97d8 | |||
| f042084bd2 | |||
| be6d5f9bd9 | |||
| 506060f23d | |||
| 55d475df04 | |||
| f349a66292 | |||
| 3742bdc7cf | |||
| b5b2450eac | |||
| fc071bafb2 | |||
| e4ce1790cd | |||
| 3461b13ec7 | |||
| 83e4314645 | |||
| c726bc5904 | |||
| 970af0de7c | |||
| 8be0a7df95 | |||
| 8fd122c431 | |||
| f661b82f18 | |||
| 77ee7f1d19 | |||
| af90b7ac4e | |||
| 3f3daef01c | |||
| c2e859273d | |||
| 3b84b2c5e7 | |||
| 284db61540 | |||
| 8e19a5eb33 | |||
| ef4cb79cde | |||
| 9fbb9cbe9b | |||
| 4e64f28318 | |||
| 5e2f775b2b | |||
| 0856f4e6b0 | |||
| ae4cdda939 | |||
| 0db273bf38 | |||
| a912a7584f | |||
| fa1aa57581 | |||
| b22bb3fa86 | |||
| c3ed8b9e7b | |||
| 6e442d9046 | |||
| 79c5edd319 | |||
| bd6361e23a | |||
| 02fdf8c0d3 | |||
| 1e835b24fb | |||
| 1a12ba3ad4 | |||
| d9f2588561 | |||
| 5268bc35db | |||
| ff32840387 | |||
| b4afb91de5 | |||
| d800d3c324 | |||
| 7c84ab2701 | |||
| 6e119c737c | |||
| 237c0256a2 | |||
| f763d3690d | |||
| 91d085c41b | |||
| afb6627bef | |||
| 8c3f55456f | |||
| 03d9e9b368 | |||
| 75c4af5f4e | |||
| c9f6938cb7 | |||
| 939af521f3 | |||
| bb9d481d88 | |||
| 3df336ab1c | |||
| 12e358a54f | |||
| 468e7c35f6 | |||
| a3cb1cd6b5 | |||
| 1554e05d8a | |||
| 85e0626d5f | |||
| e89c45ba42 | |||
| 6173aef064 | |||
| 00364d95af | |||
| 3aa0983a5c | |||
| 4be4d39851 | |||
| 884775086a | |||
| a60e336f85 | |||
| 426a4ff1bf | |||
| 9492614ea6 | |||
| 234e0be337 | |||
| b6d71a3875 | |||
| 4c8e2fd4ae | |||
| 55342a84fa | |||
| 9950268164 | |||
| f17c9fb2d5 | |||
| 93c961d673 | |||
| 6e786e0ede | |||
| 6c4a4382d7 | |||
| 7272a347fa | |||
| 6d1c24f6fb | |||
| 4300148663 | |||
| 75cde02283 | |||
| 59ecb1edbd | |||
| 6e963917d6 | |||
| 7adf60d2c6 | |||
| bd576c22c0 | |||
| 35023ceb0b | |||
| f1e7894c01 | |||
| ef44631fc6 | |||
| 541586f6cc | |||
| f89150d3ee | |||
| b27770801c | |||
| a49bffac4c | |||
| d4a0c2882d | |||
| 031f4ec329 | |||
| 4bf103db38 | |||
| 4363105976 | |||
| 3b133865f0 | |||
| 82a0708b4e | |||
| 3eafefcf37 | |||
| d2874afb75 | |||
| a848506669 | |||
| f0e49c2adf | |||
| 74e2e767dd | |||
| 3ec831f5da | |||
| ab34330e47 | |||
| 35e5cca3fb | |||
| 80a7aadf9f | |||
| 4837add55e | |||
| abf0bbb1a6 | |||
| c1d885f913 | |||
| 568e60b434 | |||
| d05796d8cb | |||
| adb7cd33d1 | |||
| 18f20a7e29 | |||
| 96bdd91bad | |||
| bc50cae35f | |||
| d36b2a6869 | |||
| ed232df0b6 | |||
| 1a4f6effda | |||
| dc6fe93d1e | |||
| a5537a8f24 | |||
| a27d6e2655 | |||
| bce6c19bba | |||
| 22d092b83c | |||
| ee5671bef5 | |||
| 0b58b9112d | |||
| 0fed5147b9 | |||
| b06234149f | |||
| 739f306bf0 | |||
| 5f3c96607f | |||
| 235facb793 | |||
| 9adb0deaa5 | |||
| 61b711ce76 | |||
| 9e6dc71609 | |||
| 33b1c02873 | |||
| f4ad575090 | |||
| f55730716a | |||
| bad1c683f8 | |||
| 2479339c46 | |||
| 45d5d1a802 | |||
| a218105aca | |||
| 6bbb7fb498 | |||
| 80db096be8 | |||
| 9ef1a040dd | |||
| f11158ab6c | |||
| 84a030aed0 | |||
| 7b25a50a51 | |||
| f32d0099fc | |||
| 6d9cf861f6 | |||
| 13e565a4dc | |||
| aeca1f1495 | |||
| c16bc6b435 | |||
| 846fcfb408 | |||
| 1141b7db1a | |||
| b85a1a0998 | |||
| e62313d7ba | |||
| 71510de602 | |||
| 28888a414b | |||
| 3538bb91e3 | |||
| fdcc6dbeda | |||
| f1cd8120a8 | |||
| 473852c7d5 | |||
| f19f8b25db | |||
| 4de202fc64 | |||
| 7b206d33f0 | |||
| 085de8bdae | |||
| fe0b954019 | |||
| e21ae2ae53 | |||
| 0306683cbf | |||
| e020ba1023 | |||
| 0697e0705b |
+1
-1
@@ -52,7 +52,7 @@ allow-git = [
|
||||
# A patch override for the bindings fixing a bug for Android before upstream
|
||||
# releases a new version.
|
||||
"https://github.com/element-hq/tracing.git",
|
||||
# Sam as for the tracing dependency.
|
||||
# Same as for the tracing dependency.
|
||||
"https://github.com/element-hq/paranoid-android.git",
|
||||
# Well, it's Ruma.
|
||||
"https://github.com/ruma/ruma",
|
||||
|
||||
@@ -6,11 +6,6 @@ on:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -295,7 +290,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check the spelling of the files in our repo
|
||||
uses: crate-ci/typos@v1.31.1
|
||||
uses: crate-ci/typos@v1.33.1
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
@@ -380,7 +375,6 @@ jobs:
|
||||
RUST_LOG: "info,matrix_sdk=trace"
|
||||
HOMESERVER_URL: "http://localhost:8008"
|
||||
HOMESERVER_DOMAIN: "synapse"
|
||||
SLIDING_SYNC_PROXY_URL: "http://localhost:8118"
|
||||
run: |
|
||||
cargo nextest run -p matrix-sdk-integration-testing
|
||||
|
||||
|
||||
@@ -5,11 +5,6 @@ on:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -83,7 +78,6 @@ jobs:
|
||||
CARGO_PROFILE_COV_DEBUG: 1
|
||||
HOMESERVER_URL: "http://localhost:8008"
|
||||
HOMESERVER_DOMAIN: "synapse"
|
||||
SLIDING_SYNC_PROXY_URL: "http://localhost:8118"
|
||||
|
||||
# Copied with minimal adjustments, source:
|
||||
# https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/test.yml
|
||||
|
||||
@@ -7,11 +7,6 @@ on:
|
||||
workflow_dispatch:
|
||||
pull_request: # focus on the changed files in current PR
|
||||
branches: [main]
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -25,7 +20,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@6f67ee9ac810f0192ea7b3d2086406f97847bcf9 # v45
|
||||
uses: tj-actions/changed-files@115870536a85eaf050e369291c7895748ff12aea
|
||||
- name: Detect long path
|
||||
env:
|
||||
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} # ignore the deleted files
|
||||
|
||||
@@ -4,11 +4,6 @@ on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
@@ -6,11 +6,6 @@ on:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
jobs:
|
||||
msrv:
|
||||
|
||||
@@ -57,6 +57,23 @@ cargo insta test
|
||||
cargo insta review
|
||||
```
|
||||
|
||||
### Intermittent failure policy
|
||||
|
||||
While we strive to add test coverage for as many features as we can, it sometimes happens that the
|
||||
tests will be intermittently failing in CI (such tests are sometimes called "flaky"). This can be
|
||||
caused by race conditions of all sorts, either in the test code itself, but sometimes in the
|
||||
underlying feature being tested too, and as such, it requires some investigation, usually from the
|
||||
original author of the test.
|
||||
|
||||
Whenever such an intermittent failure happens, we try to open an issue to track the failures,
|
||||
adding the
|
||||
[`intermittent-failure`](https://github.com/matrix-org/matrix-rust-sdk/issues?q=is%3Aissue%20state%3Aopen%20label%3Aintermittent-failure)
|
||||
label to it, and commenting with links to CI runs where the failure happened.
|
||||
|
||||
If a test has been intermittently failing for **two weeks** or more, and no one is actively working
|
||||
on fixing it, then we might decide to mark the test as `ignored` until it is fixed, to not cause
|
||||
unrelated failures in other contributors' pull requests and pushes.
|
||||
|
||||
## Pull requests
|
||||
|
||||
Ideally, a PR should have a *proper title*, with *atomic logical commits*, and
|
||||
|
||||
Generated
+392
-151
File diff suppressed because it is too large
Load Diff
+52
-48
@@ -4,15 +4,13 @@ members = [
|
||||
"bindings/matrix-sdk-crypto-ffi",
|
||||
"bindings/matrix-sdk-ffi",
|
||||
"crates/*",
|
||||
"testing/*",
|
||||
"examples/*",
|
||||
"labs/*",
|
||||
"testing/*",
|
||||
"uniffi-bindgen",
|
||||
"xtask",
|
||||
]
|
||||
exclude = [
|
||||
"testing/data",
|
||||
]
|
||||
exclude = ["testing/data"]
|
||||
# xtask, testing and the bindings should only be built when invoked explicitly.
|
||||
default-members = ["benchmarks", "crates/*", "labs/*"]
|
||||
resolver = "2"
|
||||
@@ -23,14 +21,16 @@ rust-version = "1.85"
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.95"
|
||||
aquamarine = "0.6.0"
|
||||
as_variant = "1.3.0"
|
||||
assert-json-diff = "2.0.2"
|
||||
assert_matches = "1.5.0"
|
||||
assert_matches2 = "0.1.2"
|
||||
async-compat = "0.2.4"
|
||||
async-rx = "0.1.3"
|
||||
async-stream = "0.3.5"
|
||||
async-trait = "0.1.85"
|
||||
as_variant = "1.3.0"
|
||||
base64 = "0.22.1"
|
||||
bitflags = "2.8.0"
|
||||
byteorder = "1.5.0"
|
||||
chrono = "0.4.39"
|
||||
eyeball = { version = "0.8.8", features = ["tracing"] }
|
||||
@@ -60,7 +60,7 @@ reqwest = { version = "0.12.12", default-features = false }
|
||||
rmp-serde = "1.3.0"
|
||||
# Be careful to use commits from the https://github.com/ruma/ruma/tree/ruma-0.12
|
||||
# branch until a proper release with breaking changes happens.
|
||||
ruma = { version = "0.12.2", features = [
|
||||
ruma = { version = "0.12.3", features = [
|
||||
"client-api-c",
|
||||
"compat-upload-signatures",
|
||||
"compat-user-id",
|
||||
@@ -74,9 +74,13 @@ ruma = { version = "0.12.2", features = [
|
||||
"unstable-msc4075",
|
||||
"unstable-msc4140",
|
||||
"unstable-msc4171",
|
||||
] }
|
||||
"unstable-msc4278",
|
||||
"unstable-msc4286",
|
||||
] }
|
||||
ruma-common = "0.15.2"
|
||||
serde = "1.0.217"
|
||||
sentry = "0.36.0"
|
||||
sentry-tracing = "0.36.0"
|
||||
serde = { version = "1.0.217", features = ["rc"] }
|
||||
serde_html_form = "0.2.7"
|
||||
serde_json = "1.0.138"
|
||||
sha2 = "0.10.8"
|
||||
@@ -84,7 +88,7 @@ similar-asserts = "1.6.1"
|
||||
stream_assert = "0.1.1"
|
||||
tempfile = "3.16.0"
|
||||
thiserror = "2.0.11"
|
||||
tokio = { version = "1.43.0", default-features = false, features = ["sync"] }
|
||||
tokio = { version = "1.43.1", default-features = false, features = ["sync"] }
|
||||
tokio-stream = "0.1.17"
|
||||
tracing = { version = "0.1.40", default-features = false, features = ["std"] }
|
||||
tracing-core = "0.1.32"
|
||||
@@ -96,22 +100,50 @@ url = "2.5.4"
|
||||
uuid = "1.12.1"
|
||||
vodozemac = { version = "0.9.0", features = ["insecure-pk-encryption"] }
|
||||
wasm-bindgen = "0.2.84"
|
||||
wasm-bindgen-test = "0.3.33"
|
||||
wasm-bindgen-test = "0.3.50"
|
||||
web-sys = "0.3.69"
|
||||
wiremock = "0.6.2"
|
||||
zeroize = "1.8.1"
|
||||
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.11.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.11.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.11.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.11.0" }
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.12.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.12.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.12.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.12.0" }
|
||||
matrix-sdk-ffi-macros = { path = "bindings/matrix-sdk-ffi-macros", version = "0.7.0" }
|
||||
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.11.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.11.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.11.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.11.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.11.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.11.0", default-features = false }
|
||||
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.12.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.12.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.12.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.12.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.12.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.12.0", default-features = false }
|
||||
|
||||
[workspace.lints.rust]
|
||||
rust_2018_idioms = "warn"
|
||||
semicolon_in_expressions_from_macros = "warn"
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(tarpaulin_include)', # Used by tarpaulin (code coverage)
|
||||
'cfg(ruma_unstable_exhaustive_types)', # Used by Ruma's EventContent derive macro
|
||||
] }
|
||||
unused_extern_crates = "warn"
|
||||
unused_import_braces = "warn"
|
||||
unused_qualifications = "warn"
|
||||
trivial_casts = "warn"
|
||||
trivial_numeric_casts = "warn"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
assigning_clones = "allow"
|
||||
box_default = "allow"
|
||||
cloned_instead_of_copied = "warn"
|
||||
dbg_macro = "warn"
|
||||
inefficient_to_string = "warn"
|
||||
macro_use_imports = "warn"
|
||||
mut_mut = "warn"
|
||||
needless_borrow = "warn"
|
||||
nonstandard_macro_braces = "warn"
|
||||
str_to_string = "warn"
|
||||
todo = "warn"
|
||||
unused_async = "warn"
|
||||
redundant_clone = "warn"
|
||||
|
||||
# Default development profile; default for most Cargo commands, otherwise
|
||||
# selected with `--debug`
|
||||
@@ -155,31 +187,3 @@ tracing-core = { git = "https://github.com/element-hq/tracing.git", rev = "ca943
|
||||
tracing-subscriber = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
tracing-appender = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
paranoid-android = { git = "https://github.com/element-hq/paranoid-android.git", rev = "69388ac5b4afeed7be4401c70ce17f6d9a2cf19b" }
|
||||
|
||||
[workspace.lints.rust]
|
||||
rust_2018_idioms = "warn"
|
||||
semicolon_in_expressions_from_macros = "warn"
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(tarpaulin_include)', # Used by tarpaulin (code coverage)
|
||||
'cfg(ruma_unstable_exhaustive_types)', # Used by Ruma's EventContent derive macro
|
||||
] }
|
||||
unused_extern_crates = "warn"
|
||||
unused_import_braces = "warn"
|
||||
unused_qualifications = "warn"
|
||||
trivial_casts = "warn"
|
||||
trivial_numeric_casts = "warn"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
assigning_clones = "allow"
|
||||
box_default = "allow"
|
||||
cloned_instead_of_copied = "warn"
|
||||
dbg_macro = "warn"
|
||||
inefficient_to_string = "warn"
|
||||
macro_use_imports = "warn"
|
||||
mut_mut = "warn"
|
||||
needless_borrow = "warn"
|
||||
nonstandard_macro_braces = "warn"
|
||||
str_to_string = "warn"
|
||||
todo = "warn"
|
||||
unused_async = "warn"
|
||||
redundant_clone = "warn"
|
||||
|
||||
+13
-13
@@ -3,24 +3,27 @@ name = "benchmarks"
|
||||
description = "Matrix SDK benchmarks"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
rust-version = { workspace = true }
|
||||
rust-version.workspace = true
|
||||
version = "1.0.0"
|
||||
publish = false
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
[dependencies]
|
||||
criterion = { version = "0.5.1", features = ["async", "async_tokio", "html_reports"] }
|
||||
matrix-sdk-base = { workspace = true }
|
||||
matrix-sdk-crypto = { workspace = true }
|
||||
matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
|
||||
matrix-sdk-test = { workspace = true }
|
||||
matrix-sdk-ui = { workspace = true }
|
||||
matrix-sdk = { workspace = true, features = ["native-tls", "e2e-encryption", "sqlite", "testing"] }
|
||||
ruma = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
matrix-sdk-base.workspace = true
|
||||
matrix-sdk-crypto.workspace = true
|
||||
matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
|
||||
matrix-sdk-test.workspace = true
|
||||
matrix-sdk-ui.workspace = true
|
||||
ruma.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tempfile = "3.3.0"
|
||||
tokio = { workspace = true, default-features = false, features = ["rt-multi-thread"] }
|
||||
wiremock = { workspace = true }
|
||||
wiremock.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
pprof = { version = "0.14.0", features = ["flamegraph", "criterion"] }
|
||||
@@ -44,6 +47,3 @@ harness = false
|
||||
[[bench]]
|
||||
name = "timeline"
|
||||
harness = false
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::{sync::Arc, time::Duration};
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::{
|
||||
linked_chunk::{lazy_loader, LinkedChunk, Update},
|
||||
linked_chunk::{lazy_loader, LinkedChunk, LinkedChunkId, Update},
|
||||
SqliteEventCacheStore,
|
||||
};
|
||||
use matrix_sdk_base::event_cache::{
|
||||
@@ -29,6 +29,7 @@ fn writing(c: &mut Criterion) {
|
||||
.expect("Failed to create an asynchronous runtime");
|
||||
|
||||
let room_id = room_id!("!foo:bar.baz");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let event_factory = EventFactory::new().room(room_id).sender(&ALICE);
|
||||
|
||||
let mut group = c.benchmark_group("writing");
|
||||
@@ -115,9 +116,9 @@ fn writing(c: &mut Criterion) {
|
||||
|
||||
if let Some(store) = &store {
|
||||
let updates = linked_chunk.updates().unwrap().take();
|
||||
store.handle_linked_chunk_updates(room_id, updates).await.unwrap();
|
||||
store.handle_linked_chunk_updates(linked_chunk_id, updates).await.unwrap();
|
||||
// Empty the store.
|
||||
store.handle_linked_chunk_updates(room_id, vec![Update::Clear]).await.unwrap();
|
||||
store.handle_linked_chunk_updates(linked_chunk_id, vec![Update::Clear]).await.unwrap();
|
||||
}
|
||||
|
||||
},
|
||||
@@ -145,6 +146,7 @@ fn reading(c: &mut Criterion) {
|
||||
.expect("Failed to create an asynchronous runtime");
|
||||
|
||||
let room_id = room_id!("!foo:bar.baz");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let event_factory = EventFactory::new().room(room_id).sender(&ALICE);
|
||||
|
||||
let mut group = c.benchmark_group("reading");
|
||||
@@ -195,7 +197,9 @@ fn reading(c: &mut Criterion) {
|
||||
|
||||
// Now persist the updates to recreate this full linked chunk.
|
||||
let updates = lc.updates().unwrap().take();
|
||||
runtime.block_on(store.handle_linked_chunk_updates(room_id, updates)).unwrap();
|
||||
runtime
|
||||
.block_on(store.handle_linked_chunk_updates(linked_chunk_id, updates))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Define the throughput.
|
||||
@@ -206,7 +210,8 @@ fn reading(c: &mut Criterion) {
|
||||
// Bench the routine.
|
||||
bencher.to_async(&runtime).iter(|| async {
|
||||
// Load the last chunk first,
|
||||
let (last_chunk, chunk_id_gen) = store.load_last_chunk(room_id).await.unwrap();
|
||||
let (last_chunk, chunk_id_gen) =
|
||||
store.load_last_chunk(linked_chunk_id).await.unwrap();
|
||||
|
||||
let mut lc =
|
||||
lazy_loader::from_last_chunk::<128, _, _>(last_chunk, chunk_id_gen)
|
||||
@@ -216,7 +221,7 @@ fn reading(c: &mut Criterion) {
|
||||
// Then load until the start of the linked chunk.
|
||||
let mut cur_chunk_id = lc.chunks().next().unwrap().identifier();
|
||||
while let Some(prev) =
|
||||
store.load_previous_chunk(room_id, cur_chunk_id).await.unwrap()
|
||||
store.load_previous_chunk(linked_chunk_id, cur_chunk_id).await.unwrap()
|
||||
{
|
||||
cur_chunk_id = prev.identifier;
|
||||
lazy_loader::insert_new_first_chunk(&mut lc, prev)
|
||||
|
||||
@@ -7,7 +7,7 @@ use matrix_sdk_base::{
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteStateStore;
|
||||
use matrix_sdk_test::{event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent};
|
||||
use matrix_sdk_ui::{timeline::TimelineFocus, Timeline};
|
||||
use matrix_sdk_ui::timeline::{TimelineBuilder, TimelineFocus};
|
||||
use ruma::{
|
||||
api::client::membership::get_member_events,
|
||||
device_id,
|
||||
@@ -29,7 +29,7 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
|
||||
let f = EventFactory::new().room(&room_id);
|
||||
let mut member_events: Vec<Raw<RoomMemberEvent>> = Vec::with_capacity(MEMBERS_IN_ROOM);
|
||||
for i in 0..MEMBERS_IN_ROOM {
|
||||
let user_id = OwnedUserId::try_from(format!("@user_{}:matrix.org", i)).unwrap();
|
||||
let user_id = OwnedUserId::try_from(format!("@user_{i}:matrix.org")).unwrap();
|
||||
let event = f
|
||||
.member(&user_id)
|
||||
.membership(MembershipState::Join)
|
||||
@@ -178,11 +178,11 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
.lock()
|
||||
.await
|
||||
.unwrap()
|
||||
.clear_all_rooms_chunks()
|
||||
.clear_all_linked_chunks()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let timeline = Timeline::builder(&room)
|
||||
let timeline = TimelineBuilder::new(&room)
|
||||
.with_focus(TimelineFocus::PinnedEvents {
|
||||
max_events_to_load: 100,
|
||||
max_concurrent_requests: 10,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::test_utils::mocks::MatrixMockServer;
|
||||
use matrix_sdk_test::{event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent};
|
||||
use matrix_sdk_ui::Timeline;
|
||||
use matrix_sdk_ui::timeline::TimelineBuilder;
|
||||
use ruma::{
|
||||
events::room::message::RoomMessageEventContentWithoutRelation, owned_room_id, owned_user_id,
|
||||
EventId,
|
||||
@@ -102,7 +102,7 @@ pub fn create_timeline_with_initial_events(c: &mut Criterion) {
|
||||
BenchmarkId::new("create_timeline_with_initial_events", format!("{NUM_EVENTS} events")),
|
||||
|b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let timeline = Timeline::builder(&room)
|
||||
let timeline = TimelineBuilder::new(&room)
|
||||
.track_read_marker_and_receipts()
|
||||
.build()
|
||||
.await
|
||||
|
||||
@@ -3,12 +3,15 @@ name = "matrix-sdk-crypto-ffi"
|
||||
version = "0.1.0"
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk>"]
|
||||
edition = "2021"
|
||||
rust-version = { workspace = true }
|
||||
rust-version.workspace = true
|
||||
description = "Uniffi based bindings for the Rust SDK crypto crate"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
license = "Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "staticlib"]
|
||||
|
||||
@@ -21,23 +24,23 @@ default = ["bundled-sqlite"]
|
||||
bundled-sqlite = ["matrix-sdk-sqlite/bundled"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
anyhow.workspace = true
|
||||
futures-util.workspace = true
|
||||
hmac = "0.12.1"
|
||||
http = { workspace = true }
|
||||
http.workspace = true
|
||||
matrix-sdk-common = { workspace = true, features = ["uniffi"] }
|
||||
matrix-sdk-ffi-macros = { workspace = true }
|
||||
matrix-sdk-ffi-macros.workspace = true
|
||||
pbkdf2 = "0.12.2"
|
||||
rand = { workspace = true }
|
||||
ruma = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
rand.workspace = true
|
||||
ruma.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
# keep in sync with uniffi dependency in matrix-sdk-ffi, and uniffi_bindgen in ffi CI job
|
||||
uniffi = { workspace = true, features = ["cli"] }
|
||||
vodozemac = { workspace = true }
|
||||
vodozemac.workspace = true
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
[dependencies.js_int]
|
||||
@@ -53,20 +56,17 @@ workspace = true
|
||||
features = ["crypto-store"]
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.33.0"
|
||||
version = "1.43.1"
|
||||
default-features = false
|
||||
features = ["rt-multi-thread"]
|
||||
|
||||
[build-dependencies]
|
||||
vergen = { version = "8.2.5", features = ["build", "git", "gitcl"] }
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
vergen = { version = "8.2.5", features = ["build", "git", "gitcl"] }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches2.workspace = true
|
||||
tempfile = "3.8.0"
|
||||
assert_matches2 = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
@@ -78,10 +78,10 @@ pub enum DecryptionError {
|
||||
|
||||
impl From<MegolmError> for DecryptionError {
|
||||
fn from(value: MegolmError) -> Self {
|
||||
match value {
|
||||
match &value {
|
||||
MegolmError::MissingRoomKey(withheld_code) => Self::MissingRoomKey {
|
||||
error: "Withheld Inbound group session".to_owned(),
|
||||
withheld_code: withheld_code.map(|w| w.as_str().to_owned()),
|
||||
error: value.to_string(),
|
||||
withheld_code: withheld_code.as_ref().map(|w| w.as_str().to_owned()),
|
||||
},
|
||||
_ => Self::Megolm { error: value.to_string() },
|
||||
}
|
||||
|
||||
@@ -354,7 +354,7 @@ impl OlmMachine {
|
||||
.map(|d| d.into()))
|
||||
}
|
||||
|
||||
/// Manually the device of the given user with the given device ID.
|
||||
/// Manually verify the device of the given user with the given device ID.
|
||||
///
|
||||
/// This method will attempt to sign the device using our private cross
|
||||
/// signing key.
|
||||
@@ -554,8 +554,10 @@ impl OlmMachine {
|
||||
}),
|
||||
)?;
|
||||
|
||||
let to_device_events =
|
||||
to_device_events.into_iter().map(|event| event.json().get().to_owned()).collect();
|
||||
let to_device_events = to_device_events
|
||||
.into_iter()
|
||||
.map(|event| event.to_raw().json().get().to_owned())
|
||||
.collect();
|
||||
let room_key_infos = room_key_infos.into_iter().map(|info| info.into()).collect();
|
||||
|
||||
Ok(SyncChangesResult { to_device_events, room_key_infos })
|
||||
@@ -915,20 +917,25 @@ impl OlmMachine {
|
||||
let event_json: Event<'_> = serde_json::from_str(decrypted.event.json().get())?;
|
||||
|
||||
Ok(match &encryption_info.algorithm_info {
|
||||
AlgorithmInfo::MegolmV1AesSha2 { curve25519_key, sender_claimed_keys } => {
|
||||
DecryptedEvent {
|
||||
clear_event: serde_json::to_string(&event_json)?,
|
||||
sender_curve25519_key: curve25519_key.to_owned(),
|
||||
claimed_ed25519_key: sender_claimed_keys
|
||||
.get(&DeviceKeyAlgorithm::Ed25519)
|
||||
.cloned(),
|
||||
forwarding_curve25519_chain: vec![],
|
||||
shield_state: if strict_shields {
|
||||
encryption_info.verification_state.to_shield_state_strict().into()
|
||||
} else {
|
||||
encryption_info.verification_state.to_shield_state_lax().into()
|
||||
},
|
||||
}
|
||||
AlgorithmInfo::MegolmV1AesSha2 {
|
||||
curve25519_key,
|
||||
sender_claimed_keys,
|
||||
session_id: _,
|
||||
} => DecryptedEvent {
|
||||
clear_event: serde_json::to_string(&event_json)?,
|
||||
sender_curve25519_key: curve25519_key.to_owned(),
|
||||
claimed_ed25519_key: sender_claimed_keys.get(&DeviceKeyAlgorithm::Ed25519).cloned(),
|
||||
forwarding_curve25519_chain: vec![],
|
||||
shield_state: if strict_shields {
|
||||
encryption_info.verification_state.to_shield_state_strict().into()
|
||||
} else {
|
||||
encryption_info.verification_state.to_shield_state_lax().into()
|
||||
},
|
||||
},
|
||||
AlgorithmInfo::OlmV1Curve25519AesSha2 { .. } => {
|
||||
// cannot happen because `decrypt_room_event` would have fail to decrypt olm for
|
||||
// a room (EventError::UnsupportedAlgorithm)
|
||||
panic!("Unsupported olm algorithm in room")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1335,7 +1342,8 @@ impl OlmMachine {
|
||||
let (sas, request) = self.runtime.block_on(device.start_verification())?;
|
||||
|
||||
Some(StartSasResult {
|
||||
sas: Sas { inner: sas, runtime: self.runtime.handle().to_owned() }.into(),
|
||||
sas: Sas { inner: Box::new(sas), runtime: self.runtime.handle().to_owned() }
|
||||
.into(),
|
||||
request: request.into(),
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -224,8 +224,8 @@ impl From<&ToDeviceRequest> for Request {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RoomMessageRequest> for Request {
|
||||
fn from(r: &RoomMessageRequest) -> Self {
|
||||
impl From<&Box<RoomMessageRequest>> for Request {
|
||||
fn from(r: &Box<RoomMessageRequest>) -> Self {
|
||||
Self::RoomMessage {
|
||||
request_id: r.txn_id.to_string(),
|
||||
room_id: r.room_id.to_string(),
|
||||
|
||||
@@ -88,7 +88,7 @@ impl Verification {
|
||||
/// returns `None` if the verification is not a `Sas` verification.
|
||||
pub fn as_sas(&self) -> Option<Arc<Sas>> {
|
||||
if let InnerVerification::SasV1(sas) = &self.inner {
|
||||
Some(Sas { inner: sas.to_owned(), runtime: self.runtime.to_owned() }.into())
|
||||
Some(Sas { inner: sas.clone(), runtime: self.runtime.to_owned() }.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -98,7 +98,7 @@ impl Verification {
|
||||
/// returns `None` if the verification is not a `QrCode` verification.
|
||||
pub fn as_qr(&self) -> Option<Arc<QrCode>> {
|
||||
if let InnerVerification::QrV1(qr) = &self.inner {
|
||||
Some(QrCode { inner: qr.to_owned(), runtime: self.runtime.to_owned() }.into())
|
||||
Some(QrCode { inner: qr.clone(), runtime: self.runtime.to_owned() }.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -108,7 +108,7 @@ impl Verification {
|
||||
/// The `m.sas.v1` verification flow.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Sas {
|
||||
pub(crate) inner: InnerSas,
|
||||
pub(crate) inner: Box<InnerSas>,
|
||||
pub(crate) runtime: Handle,
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@ impl From<QrVerificationState> for QrCodeState {
|
||||
/// verification flow.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct QrCode {
|
||||
pub(crate) inner: InnerQr,
|
||||
pub(crate) inner: Box<InnerQr>,
|
||||
pub(crate) runtime: Handle,
|
||||
}
|
||||
|
||||
@@ -669,7 +669,7 @@ impl VerificationRequest {
|
||||
/// verification flow.
|
||||
pub fn start_sas_verification(&self) -> Result<Option<StartSasResult>, CryptoStoreError> {
|
||||
Ok(self.runtime.block_on(self.inner.start_sas())?.map(|(sas, r)| StartSasResult {
|
||||
sas: Arc::new(Sas { inner: sas, runtime: self.runtime.clone() }),
|
||||
sas: Arc::new(Sas { inner: Box::new(sas), runtime: self.runtime.clone() }),
|
||||
request: r.into(),
|
||||
}))
|
||||
}
|
||||
@@ -690,7 +690,7 @@ impl VerificationRequest {
|
||||
Ok(self
|
||||
.runtime
|
||||
.block_on(self.inner.generate_qr_code())?
|
||||
.map(|qr| QrCode { inner: qr, runtime: self.runtime.clone() }.into()))
|
||||
.map(|qr| QrCode { inner: Box::new(qr), runtime: self.runtime.clone() }.into()))
|
||||
}
|
||||
|
||||
/// Pass data from a scanned QR code to an active verification request and
|
||||
@@ -717,7 +717,7 @@ impl VerificationRequest {
|
||||
let request = qr.reciprocate()?;
|
||||
|
||||
Some(ScanResult {
|
||||
qr: QrCode { inner: qr, runtime: self.runtime.clone() }.into(),
|
||||
qr: QrCode { inner: Box::new(qr), runtime: self.runtime.clone() }.into(),
|
||||
request: request.into(),
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -7,7 +7,7 @@ license = "Apache-2.0"
|
||||
name = "matrix-sdk-ffi-macros"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = { workspace = true }
|
||||
rust-version.workspace = true
|
||||
version = "0.7.0"
|
||||
publish = false
|
||||
|
||||
|
||||
@@ -51,7 +51,12 @@ pub fn export(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
|
||||
let res = match syn::parse(item) {
|
||||
Ok(item) => match has_async_fn(item) {
|
||||
true => quote! { #[uniffi::export(async_runtime = "tokio", #attr2)] },
|
||||
true => {
|
||||
quote! {
|
||||
#[cfg_attr(target_family = "wasm", uniffi::export(#attr2))]
|
||||
#[cfg_attr(not(target_family = "wasm"), uniffi::export(async_runtime = "tokio", #attr2))]
|
||||
}
|
||||
}
|
||||
false => quote! { #[uniffi::export(#attr2)] },
|
||||
},
|
||||
Err(e) => e.into_compile_error(),
|
||||
|
||||
@@ -6,6 +6,48 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
## [0.12.0] - 2025-06-10
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- `Client::send_call_notification_if_needed` now returns `Result<bool>` instead of `Result<()>` so we can check if
|
||||
the event was sent.
|
||||
- `Client::upload_avatar` and `Timeline::send_attachment` now may fail if a file too large for the homeserver media
|
||||
config is uploaded.
|
||||
- `UploadParameters` replaces field `filename: String` with `source: UploadSource`.
|
||||
`UploadSource` is an enum which may take a filename or a filename and bytes, which
|
||||
allows a foreign language to read file contents natively and then pass those contents to
|
||||
the foreign function when uploading a file through the `Timeline`.
|
||||
([#4948](https://github.com/matrix-org/matrix-rust-sdk/pull/4948))
|
||||
- `RoomInfo` replaces its field `is_tombstoned: bool` with `tombstone: Option<RoomTombstoneInfo>`,
|
||||
containing the data needed to implement the room migration UI, a message and the replacement room id.
|
||||
([#5027](https://github.com/matrix-org/matrix-rust-sdk/pull/5027))
|
||||
|
||||
Additions:
|
||||
|
||||
- `Client::subscribe_to_room_info` allows clients to subscribe to room info updates in rooms which may not be known yet.
|
||||
This is useful when displaying a room preview for an unknown room, so when we receive any membership change for it,
|
||||
we can automatically update the UI.
|
||||
- `Client::get_max_media_upload_size` to get the max size of a request sent to the homeserver so we can tweak our media
|
||||
uploads by compressing/transcoding the media.
|
||||
- Add `ClientBuilder::enable_share_history_on_invite` to enable experimental support for sharing encrypted room history on invite, per [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268).
|
||||
([#5141](https://github.com/matrix-org/matrix-rust-sdk/pull/5141))
|
||||
- Support for adding a Sentry layer to the FFI bindings has been added. Only `tracing` statements with
|
||||
the field `sentry=true` will be forwarded to Sentry, in addition to default Sentry filters.
|
||||
- Add room topic string to `StateEventContent`
|
||||
- Add `UploadSource` for representing upload data - this is analogous to `matrix_sdk_ui::timeline::AttachmentSource`
|
||||
- Add `Client::observe_account_data_event` and `Client::observe_room_account_data_event` to
|
||||
subscribe to global and room account data changes.
|
||||
([#4994](https://github.com/matrix-org/matrix-rust-sdk/pull/4994))
|
||||
- Add `Timeline::send_gallery` to send MSC4274-style galleries.
|
||||
([#5163](https://github.com/matrix-org/matrix-rust-sdk/pull/5163))
|
||||
- Add `reply_params` to `GalleryUploadParameters` to allow sending galleries as (threaded) replies.
|
||||
([#5173](https://github.com/matrix-org/matrix-rust-sdk/pull/5173))
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- `contacts` has been removed from `OidcConfiguration` (it was unused since the switch to OAuth).
|
||||
|
||||
## [0.11.0] - 2025-04-11
|
||||
|
||||
Breaking changes:
|
||||
@@ -20,7 +62,7 @@ Breaking changes:
|
||||
programs can set it to `true`.
|
||||
|
||||
- Matrix client API errors coming from API responses will now be mapped to `ClientError::MatrixApi`, containing both the
|
||||
original message and the associated error code and kind.
|
||||
original message and the associated error code and kind.
|
||||
|
||||
- `EventSendState` now has two additional variants: `CrossSigningNotSetup` and
|
||||
`SendingFromUnverifiedDevice`. These indicate that your own device is not
|
||||
|
||||
@@ -1,52 +1,81 @@
|
||||
[package]
|
||||
name = "matrix-sdk-ffi"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ffi"]
|
||||
license = "Apache-2.0"
|
||||
readme = "README.md"
|
||||
rust-version = { workspace = true }
|
||||
rust-version.workspace = true
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
publish = false
|
||||
|
||||
[package.metadata.release]
|
||||
release = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "staticlib"]
|
||||
|
||||
[features]
|
||||
default = ["bundled-sqlite"]
|
||||
default = ["bundled-sqlite", "unstable-msc4274"]
|
||||
bundled-sqlite = ["matrix-sdk/bundled-sqlite"]
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
vergen = { version = "8.1.3", features = ["build", "git", "gitcl"] }
|
||||
unstable-msc4274 = ["matrix-sdk-ui/unstable-msc4274"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
as_variant = { workspace = true }
|
||||
anyhow.workspace = true
|
||||
as_variant.workspace = true
|
||||
async-compat = "0.2.4"
|
||||
eyeball-im = { workspace = true }
|
||||
extension-trait = "1.0.1"
|
||||
futures-util = { workspace = true }
|
||||
eyeball-im.workspace = true
|
||||
futures-util.workspace = true
|
||||
language-tags = "0.3.2"
|
||||
log-panics = { version = "2", features = ["with-backtrace"] }
|
||||
matrix-sdk-ffi-macros = { workspace = true }
|
||||
matrix-sdk-common.workspace = true
|
||||
matrix-sdk-ffi-macros.workspace = true
|
||||
matrix-sdk-ui = { workspace = true, features = ["uniffi"] }
|
||||
mime = "0.3.16"
|
||||
once_cell = { workspace = true }
|
||||
ruma = { workspace = true, features = ["html", "unstable-unspecified", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat"] }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-core = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
tracing-appender = { version = "0.2.2" }
|
||||
once_cell.workspace = true
|
||||
ruma = { workspace = true, features = ["html", "unstable-unspecified", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat", "unstable-msc4278"] }
|
||||
sentry-tracing = "0.36.0"
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
tracing.workspace = true
|
||||
tracing-appender = { version = "0.2.2" }
|
||||
tracing-core.workspace = true
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
uniffi = { workspace = true, features = ["tokio"] }
|
||||
url = { workspace = true }
|
||||
zeroize = { workspace = true }
|
||||
url.workspace = true
|
||||
uuid = { version = "1.4.1", features = ["v4"] }
|
||||
language-tags = "0.3.2"
|
||||
zeroize.workspace = true
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies.matrix-sdk]
|
||||
workspace = true
|
||||
features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
"experimental-widgets",
|
||||
"markdown",
|
||||
# note: differ from block below
|
||||
"native-tls",
|
||||
"socks",
|
||||
"sqlite",
|
||||
"uniffi",
|
||||
]
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies.sentry]
|
||||
version = "0.36.0"
|
||||
default-features = false
|
||||
features = [
|
||||
# TLS lib used on non-Android platforms.
|
||||
"native-tls",
|
||||
# Most default features enabled otherwise.
|
||||
"backtrace",
|
||||
"contexts",
|
||||
"panic",
|
||||
"reqwest",
|
||||
]
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
paranoid-android = "0.2.1"
|
||||
@@ -58,27 +87,29 @@ features = [
|
||||
"e2e-encryption",
|
||||
"experimental-widgets",
|
||||
"markdown",
|
||||
"rustls-tls", # note: differ from block below
|
||||
# note: differ from block above
|
||||
"rustls-tls",
|
||||
"socks",
|
||||
"sqlite",
|
||||
"uniffi",
|
||||
]
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies.matrix-sdk]
|
||||
workspace = true
|
||||
[target.'cfg(target_os = "android")'.dependencies.sentry]
|
||||
version = "0.36.0"
|
||||
default-features = false
|
||||
features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
"experimental-widgets",
|
||||
"markdown",
|
||||
"native-tls", # note: differ from block above
|
||||
"socks",
|
||||
"sqlite",
|
||||
"uniffi",
|
||||
# TLS lib specific for Android.
|
||||
"rustls",
|
||||
# Most default features enabled otherwise.
|
||||
"backtrace",
|
||||
"contexts",
|
||||
"panic",
|
||||
"reqwest",
|
||||
]
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
vergen = { version = "8.1.3", features = ["build", "git", "gitcl"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.release]
|
||||
release = true
|
||||
|
||||
@@ -119,8 +119,6 @@ pub struct OidcConfiguration {
|
||||
pub tos_uri: Option<String>,
|
||||
/// A URI that contains the client's privacy policy.
|
||||
pub policy_uri: Option<String>,
|
||||
/// An array of e-mail addresses of people responsible for this client.
|
||||
pub contacts: Option<Vec<String>>,
|
||||
|
||||
/// Pre-configured registrations for use with homeservers that don't support
|
||||
/// dynamic client registration.
|
||||
@@ -173,7 +171,7 @@ impl OidcConfiguration {
|
||||
.iter()
|
||||
.filter_map(|(issuer, client_id)| {
|
||||
let Ok(issuer) = Url::parse(issuer) else {
|
||||
tracing::error!("Failed to parse {:?}", issuer);
|
||||
tracing::error!("Failed to parse {issuer:?}");
|
||||
return None;
|
||||
};
|
||||
Some((issuer, ClientId::new(client_id.clone())))
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::Debug,
|
||||
sync::{Arc, RwLock},
|
||||
path::PathBuf,
|
||||
sync::{Arc, OnceLock, RwLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use async_compat::get_runtime_handle;
|
||||
use futures_util::pin_mut;
|
||||
use matrix_sdk::{
|
||||
authentication::oauth::{
|
||||
AccountManagementActionFull, ClientId, OAuthAuthorizationData, OAuthSession,
|
||||
@@ -36,17 +38,27 @@ use matrix_sdk::{
|
||||
sliding_sync::Version as SdkSlidingSyncVersion,
|
||||
store::RoomLoadSettings as SdkRoomLoadSettings,
|
||||
AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens,
|
||||
STATE_STORE_DATABASE_NAME,
|
||||
};
|
||||
use matrix_sdk_ui::notification_client::{
|
||||
NotificationClient as MatrixNotificationClient,
|
||||
NotificationProcessSetup as MatrixNotificationProcessSetup,
|
||||
use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::{
|
||||
notification_client::{
|
||||
NotificationClient as MatrixNotificationClient,
|
||||
NotificationProcessSetup as MatrixNotificationProcessSetup,
|
||||
},
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
api::client::{alias::get_alias, error::ErrorKind, uiaa::UserIdentifier},
|
||||
events::{
|
||||
direct::DirectEventContent,
|
||||
fully_read::FullyReadEventContent,
|
||||
identity_server::IdentityServerEventContent,
|
||||
ignored_user_list::IgnoredUserListEventContent,
|
||||
key::verification::request::ToDeviceKeyVerificationRequestEvent,
|
||||
marked_unread::{MarkedUnreadEventContent, UnstableMarkedUnreadEventContent},
|
||||
push_rules::PushRulesEventContent,
|
||||
room::{
|
||||
history_visibility::RoomHistoryVisibilityEventContent,
|
||||
join_rules::{
|
||||
@@ -55,7 +67,13 @@ use ruma::{
|
||||
message::OriginalSyncRoomMessageEvent,
|
||||
power_levels::RoomPowerLevelsEventContent,
|
||||
},
|
||||
GlobalAccountDataEventType,
|
||||
secret_storage::{
|
||||
default_key::SecretStorageDefaultKeyEventContent, key::SecretStorageKeyEventContent,
|
||||
},
|
||||
tag::TagEventContent,
|
||||
GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
|
||||
GlobalAccountDataEventType as RumaGlobalAccountDataEventType,
|
||||
RoomAccountDataEvent as RumaRoomAccountDataEvent,
|
||||
},
|
||||
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
|
||||
OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
|
||||
@@ -73,12 +91,18 @@ use crate::{
|
||||
encryption::Encryption,
|
||||
notification::NotificationClient,
|
||||
notification_settings::NotificationSettings,
|
||||
room::RoomHistoryVisibility,
|
||||
room::{RoomHistoryVisibility, RoomInfoListener},
|
||||
room_directory_search::RoomDirectorySearch,
|
||||
room_info::RoomInfo,
|
||||
room_preview::RoomPreview,
|
||||
ruma::{AuthData, MediaSource},
|
||||
ruma::{
|
||||
AccountDataEvent, AccountDataEventType, AuthData, InviteAvatars, MediaPreviewConfig,
|
||||
MediaPreviews, MediaSource, RoomAccountDataEvent, RoomAccountDataEventType,
|
||||
},
|
||||
runtime::get_runtime_handle,
|
||||
sync_service::{SyncService, SyncServiceBuilder},
|
||||
task_handle::TaskHandle,
|
||||
utd::{UnableToDecryptDelegate, UtdHook},
|
||||
utils::AsyncRuntimeDropped,
|
||||
ClientError,
|
||||
};
|
||||
@@ -144,30 +168,43 @@ impl From<PushFormat> for RumaPushFormat {
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait ClientDelegate: Sync + Send {
|
||||
pub trait ClientDelegate: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn did_receive_auth_error(&self, is_soft_logout: bool);
|
||||
fn did_refresh_tokens(&self);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait ClientSessionDelegate: Sync + Send {
|
||||
pub trait ClientSessionDelegate: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn retrieve_session_from_keychain(&self, user_id: String) -> Result<Session, ClientError>;
|
||||
fn save_session_in_keychain(&self, session: Session);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait ProgressWatcher: Send + Sync {
|
||||
pub trait ProgressWatcher: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn transmission_progress(&self, progress: TransmissionProgress);
|
||||
}
|
||||
|
||||
/// A listener to the global (client-wide) error reporter of the send queue.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SendQueueRoomErrorListener: Sync + Send {
|
||||
pub trait SendQueueRoomErrorListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
/// Called every time the send queue has ran into an error for a given room,
|
||||
/// which will disable the send queue for that particular room.
|
||||
fn on_error(&self, room_id: String, error: ClientError);
|
||||
}
|
||||
|
||||
/// A listener for changes of global account data events.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait AccountDataListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
/// Called when a global account data event has changed.
|
||||
fn on_change(&self, event: AccountDataEvent);
|
||||
}
|
||||
|
||||
/// A listener for changes of room account data events.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomAccountDataListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
/// Called when a room account data event was changed.
|
||||
fn on_change(&self, event: RoomAccountDataEvent, room_id: String);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, uniffi::Record)]
|
||||
pub struct TransmissionProgress {
|
||||
pub current: u64,
|
||||
@@ -186,9 +223,14 @@ impl From<matrix_sdk::TransmissionProgress> for TransmissionProgress {
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Client {
|
||||
pub(crate) inner: AsyncRuntimeDropped<MatrixClient>,
|
||||
delegate: RwLock<Option<Arc<dyn ClientDelegate>>>,
|
||||
delegate: OnceLock<Arc<dyn ClientDelegate>>,
|
||||
pub(crate) utd_hook_manager: OnceLock<Arc<UtdHookManager>>,
|
||||
session_verification_controller:
|
||||
Arc<tokio::sync::RwLock<Option<SessionVerificationController>>>,
|
||||
/// The path to the directory where the state store and the crypto store are
|
||||
/// located, if the `Client` instance has been built with a SQLite store
|
||||
/// backend.
|
||||
store_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
@@ -196,6 +238,7 @@ impl Client {
|
||||
sdk_client: MatrixClient,
|
||||
enable_oidc_refresh_lock: bool,
|
||||
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
|
||||
store_path: Option<PathBuf>,
|
||||
) -> Result<Self, ClientError> {
|
||||
let session_verification_controller: Arc<
|
||||
tokio::sync::RwLock<Option<SessionVerificationController>>,
|
||||
@@ -229,9 +272,11 @@ impl Client {
|
||||
sdk_client.cross_process_store_locks_holder_name().to_owned();
|
||||
|
||||
let client = Client {
|
||||
inner: AsyncRuntimeDropped::new(sdk_client),
|
||||
delegate: RwLock::new(None),
|
||||
inner: AsyncRuntimeDropped::new(sdk_client.clone()),
|
||||
delegate: OnceLock::new(),
|
||||
utd_hook_manager: OnceLock::new(),
|
||||
session_verification_controller,
|
||||
store_path,
|
||||
};
|
||||
|
||||
if enable_oidc_refresh_lock {
|
||||
@@ -400,10 +445,18 @@ impl Client {
|
||||
/// * `prompt` - The desired user experience in the web UI. No value means
|
||||
/// that the user wishes to login into an existing account, and a value of
|
||||
/// `Create` means that the user wishes to register a new account.
|
||||
///
|
||||
/// * `login_hint` - A generic login hint that an identity provider can use
|
||||
/// to pre-fill the login form. The format of this hint is not restricted
|
||||
/// by the spec as external providers all have their own way to handle the hint.
|
||||
/// However, it should be noted that when providing a user ID as a hint
|
||||
/// for MAS (with no upstream provider), then the format to use is defined
|
||||
/// by [MSC4198]: https://github.com/matrix-org/matrix-spec-proposals/pull/4198
|
||||
pub async fn url_for_oidc(
|
||||
&self,
|
||||
oidc_configuration: &OidcConfiguration,
|
||||
prompt: Option<OidcPrompt>,
|
||||
login_hint: Option<String>,
|
||||
) -> Result<Arc<OAuthAuthorizationData>, OidcError> {
|
||||
let registration_data = oidc_configuration.registration_data()?;
|
||||
let redirect_uri = oidc_configuration.redirect_uri()?;
|
||||
@@ -413,6 +466,9 @@ impl Client {
|
||||
if let Some(prompt) = prompt {
|
||||
url_builder = url_builder.prompt(vec![prompt.into()]);
|
||||
}
|
||||
if let Some(login_hint) = login_hint {
|
||||
url_builder = url_builder.login_hint(login_hint);
|
||||
}
|
||||
|
||||
let data = url_builder.build().await?;
|
||||
|
||||
@@ -486,7 +542,7 @@ impl Client {
|
||||
auth_session,
|
||||
room_load_settings
|
||||
.try_into()
|
||||
.map_err(|error| ClientError::Generic { msg: error })?,
|
||||
.map_err(|error| ClientError::from_str(error, None))?,
|
||||
)
|
||||
.await?;
|
||||
self.inner.set_sliding_sync_version(sliding_sync_version.try_into()?);
|
||||
@@ -525,7 +581,7 @@ impl Client {
|
||||
loop {
|
||||
match subscriber.recv().await {
|
||||
Ok(report) => listener
|
||||
.on_error(report.room_id.to_string(), ClientError::new(report.error)),
|
||||
.on_error(report.room_id.to_string(), ClientError::from_err(report.error)),
|
||||
Err(err) => {
|
||||
error!("error when listening to the send queue error reporter: {err}");
|
||||
}
|
||||
@@ -534,6 +590,132 @@ impl Client {
|
||||
})))
|
||||
}
|
||||
|
||||
/// Subscribe to updates of global account data events.
|
||||
///
|
||||
/// Be careful that only the most recent value can be observed. Subscribers
|
||||
/// are notified when a new value is sent, but there is no guarantee that
|
||||
/// they will see all values.
|
||||
pub fn observe_account_data_event(
|
||||
&self,
|
||||
event_type: AccountDataEventType,
|
||||
listener: Box<dyn AccountDataListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
macro_rules! observe {
|
||||
($t:ty, $cb: expr) => {{
|
||||
// Using an Arc here is mandatory or else the subscriber will never trigger
|
||||
let observer =
|
||||
Arc::new(self.inner.observe_events::<RumaGlobalAccountDataEvent<$t>, ()>());
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
let mut subscriber = observer.subscribe();
|
||||
loop {
|
||||
if let Some(next) = subscriber.next().await {
|
||||
$cb(next.0);
|
||||
}
|
||||
}
|
||||
})))
|
||||
}};
|
||||
|
||||
($t:ty) => {{
|
||||
observe!($t, |event: RumaGlobalAccountDataEvent<$t>| {
|
||||
listener.on_change(event.into());
|
||||
})
|
||||
}};
|
||||
}
|
||||
|
||||
match event_type {
|
||||
AccountDataEventType::Direct => {
|
||||
observe!(DirectEventContent)
|
||||
}
|
||||
AccountDataEventType::IdentityServer => {
|
||||
observe!(IdentityServerEventContent)
|
||||
}
|
||||
AccountDataEventType::IgnoredUserList => {
|
||||
observe!(IgnoredUserListEventContent)
|
||||
}
|
||||
AccountDataEventType::PushRules => {
|
||||
observe!(PushRulesEventContent, |event: RumaGlobalAccountDataEvent<
|
||||
PushRulesEventContent,
|
||||
>| {
|
||||
if let Ok(event) = event.try_into() {
|
||||
listener.on_change(event);
|
||||
}
|
||||
})
|
||||
}
|
||||
AccountDataEventType::SecretStorageDefaultKey => {
|
||||
observe!(SecretStorageDefaultKeyEventContent)
|
||||
}
|
||||
AccountDataEventType::SecretStorageKey { key_id } => {
|
||||
observe!(SecretStorageKeyEventContent, |event: RumaGlobalAccountDataEvent<
|
||||
SecretStorageKeyEventContent,
|
||||
>| {
|
||||
if event.content.key_id != key_id {
|
||||
return;
|
||||
}
|
||||
if let Ok(event) = event.try_into() {
|
||||
listener.on_change(event);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribe to updates of room account data events.
|
||||
///
|
||||
/// Be careful that only the most recent value can be observed. Subscribers
|
||||
/// are notified when a new value is sent, but there is no guarantee that
|
||||
/// they will see all values.
|
||||
pub fn observe_room_account_data_event(
|
||||
&self,
|
||||
room_id: String,
|
||||
event_type: RoomAccountDataEventType,
|
||||
listener: Box<dyn RoomAccountDataListener>,
|
||||
) -> Result<Arc<TaskHandle>, ClientError> {
|
||||
macro_rules! observe {
|
||||
($t:ty, $cb: expr) => {{
|
||||
// Using an Arc here is mandatory or else the subscriber will never trigger
|
||||
let observer =
|
||||
Arc::new(self.inner.observe_room_events::<RumaRoomAccountDataEvent<$t>, ()>(
|
||||
&RoomId::parse(&room_id)?,
|
||||
));
|
||||
|
||||
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
let mut subscriber = observer.subscribe();
|
||||
loop {
|
||||
if let Some(next) = subscriber.next().await {
|
||||
$cb(next.0);
|
||||
}
|
||||
}
|
||||
}))))
|
||||
}};
|
||||
|
||||
($t:ty) => {{
|
||||
observe!($t, |event: RumaRoomAccountDataEvent<$t>| {
|
||||
listener.on_change(event.into(), room_id.clone());
|
||||
})
|
||||
}};
|
||||
}
|
||||
|
||||
match event_type {
|
||||
RoomAccountDataEventType::FullyRead => {
|
||||
observe!(FullyReadEventContent)
|
||||
}
|
||||
RoomAccountDataEventType::MarkedUnread => {
|
||||
observe!(MarkedUnreadEventContent)
|
||||
}
|
||||
RoomAccountDataEventType::Tag => {
|
||||
observe!(TagEventContent, |event: RumaRoomAccountDataEvent<TagEventContent>| {
|
||||
if let Ok(event) = event.try_into() {
|
||||
listener.on_change(event, room_id.clone());
|
||||
}
|
||||
})
|
||||
}
|
||||
RoomAccountDataEventType::UnstableMarkedUnread => {
|
||||
observe!(UnstableMarkedUnreadEventContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows generic GET requests to be made through the SDKs internal HTTP
|
||||
/// client
|
||||
pub async fn get_url(&self, url: String) -> Result<String, ClientError> {
|
||||
@@ -581,11 +763,20 @@ impl Client {
|
||||
self.inner.available_sliding_sync_versions().await.into_iter().map(Into::into).collect()
|
||||
}
|
||||
|
||||
/// Sets the [ClientDelegate] which will inform about authentication errors.
|
||||
/// Returns an error if the delegate was already set.
|
||||
pub fn set_delegate(
|
||||
self: Arc<Self>,
|
||||
delegate: Option<Box<dyn ClientDelegate>>,
|
||||
) -> Option<Arc<TaskHandle>> {
|
||||
delegate.map(|delegate| {
|
||||
) -> Result<Option<Arc<TaskHandle>>, ClientError> {
|
||||
if self.delegate.get().is_some() {
|
||||
return Err(ClientError::Generic {
|
||||
msg: "Delegate already initialized".to_owned(),
|
||||
details: None,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(delegate.map(|delegate| {
|
||||
let mut session_change_receiver = self.inner.subscribe_to_session_changes();
|
||||
let client_clone = self.clone();
|
||||
let session_change_task = get_runtime_handle().spawn(async move {
|
||||
@@ -601,9 +792,44 @@ impl Client {
|
||||
}
|
||||
});
|
||||
|
||||
*self.delegate.write().unwrap() = Some(Arc::from(delegate));
|
||||
self.delegate.get_or_init(|| Arc::from(delegate));
|
||||
|
||||
Arc::new(TaskHandle::new(session_change_task))
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
/// Sets the [UnableToDecryptDelegate] which will inform about UTDs.
|
||||
/// Returns an error if the delegate was already set.
|
||||
pub async fn set_utd_delegate(
|
||||
self: Arc<Self>,
|
||||
utd_delegate: Box<dyn UnableToDecryptDelegate>,
|
||||
) -> Result<(), ClientError> {
|
||||
if self.utd_hook_manager.get().is_some() {
|
||||
return Err(ClientError::Generic {
|
||||
msg: "UTD delegate already initialized".to_owned(),
|
||||
details: None,
|
||||
});
|
||||
}
|
||||
|
||||
// UTDs detected before this duration may be reclassified as "late decryption"
|
||||
// events (or discarded, if they get decrypted fast enough).
|
||||
const UTD_HOOK_GRACE_PERIOD: Duration = Duration::from_secs(60);
|
||||
|
||||
let mut utd_hook_manager = UtdHookManager::new(
|
||||
Arc::new(UtdHook { delegate: utd_delegate.into() }),
|
||||
(*self.inner).clone(),
|
||||
)
|
||||
.with_max_delay(UTD_HOOK_GRACE_PERIOD);
|
||||
|
||||
if let Err(e) = utd_hook_manager.reload_from_store().await {
|
||||
error!("Unable to reload UTD hook data from data store: {e}");
|
||||
// Carry on with the setup anyway; we shouldn't fail setup just
|
||||
// because the UTD hook failed to load its data.
|
||||
}
|
||||
|
||||
self.utd_hook_manager.get_or_init(|| Arc::new(utd_hook_manager));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn session(&self) -> Result<Session, ClientError> {
|
||||
@@ -862,7 +1088,11 @@ impl Client {
|
||||
}
|
||||
|
||||
pub fn rooms(&self) -> Vec<Arc<Room>> {
|
||||
self.inner.rooms().into_iter().map(|room| Arc::new(Room::new(room))).collect()
|
||||
self.inner
|
||||
.rooms()
|
||||
.into_iter()
|
||||
.map(|room| Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get a room by its ID.
|
||||
@@ -879,14 +1109,17 @@ impl Client {
|
||||
pub fn get_room(&self, room_id: String) -> Result<Option<Arc<Room>>, ClientError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
let sdk_room = self.inner.get_room(&room_id);
|
||||
let room = sdk_room.map(|room| Arc::new(Room::new(room)));
|
||||
|
||||
let room =
|
||||
sdk_room.map(|room| Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())));
|
||||
Ok(room)
|
||||
}
|
||||
|
||||
pub fn get_dm_room(&self, user_id: String) -> Result<Option<Arc<Room>>, ClientError> {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
let sdk_room = self.inner.get_dm_room(&user_id);
|
||||
let dm = sdk_room.map(|room| Arc::new(Room::new(room)));
|
||||
let dm =
|
||||
sdk_room.map(|room| Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())));
|
||||
Ok(dm)
|
||||
}
|
||||
|
||||
@@ -918,12 +1151,12 @@ impl Client {
|
||||
Ok(Arc::new(NotificationClient {
|
||||
inner: MatrixNotificationClient::new((*self.inner).clone(), process_setup.into())
|
||||
.await?,
|
||||
_client: self.clone(),
|
||||
client: self.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn sync_service(&self) -> Arc<SyncServiceBuilder> {
|
||||
SyncServiceBuilder::new((*self.inner).clone())
|
||||
SyncServiceBuilder::new((*self.inner).clone(), self.utd_hook_manager.get().cloned())
|
||||
}
|
||||
|
||||
pub async fn get_notification_settings(&self) -> Arc<NotificationSettings> {
|
||||
@@ -942,7 +1175,7 @@ impl Client {
|
||||
if let Some(raw_content) = self
|
||||
.inner
|
||||
.account()
|
||||
.fetch_account_data(GlobalAccountDataEventType::IgnoredUserList)
|
||||
.fetch_account_data(RumaGlobalAccountDataEventType::IgnoredUserList)
|
||||
.await?
|
||||
{
|
||||
let content = raw_content.deserialize_as::<IgnoredUserListEventContent>()?;
|
||||
@@ -993,7 +1226,7 @@ impl Client {
|
||||
pub async fn join_room_by_id(&self, room_id: String) -> Result<Arc<Room>, ClientError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
let room = self.inner.join_room_by_id(room_id.as_ref()).await?;
|
||||
Ok(Arc::new(Room::new(room)))
|
||||
Ok(Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())))
|
||||
}
|
||||
|
||||
/// Join a room by its ID or alias.
|
||||
@@ -1014,7 +1247,7 @@ impl Client {
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let room =
|
||||
self.inner.join_room_by_id_or_alias(room_id.as_ref(), server_names.as_ref()).await?;
|
||||
Ok(Arc::new(Room::new(room)))
|
||||
Ok(Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())))
|
||||
}
|
||||
|
||||
/// Knock on a room to join it using its ID or alias.
|
||||
@@ -1028,7 +1261,7 @@ impl Client {
|
||||
let server_names =
|
||||
server_names.iter().map(ServerName::parse).collect::<Result<Vec<_>, _>>()?;
|
||||
let room = self.inner.knock(room_id, reason, server_names).await?;
|
||||
Ok(Arc::new(Room::new(room)))
|
||||
Ok(Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())))
|
||||
}
|
||||
|
||||
pub async fn get_recently_visited_rooms(&self) -> Result<Vec<String>, ClientError> {
|
||||
@@ -1122,7 +1355,10 @@ impl Client {
|
||||
/// or an externally set timeout happens.**
|
||||
pub async fn await_room_remote_echo(&self, room_id: String) -> Result<Arc<Room>, ClientError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
Ok(Arc::new(Room::new(self.inner.await_room_remote_echo(&room_id).await)))
|
||||
Ok(Arc::new(Room::new(
|
||||
self.inner.await_room_remote_echo(&room_id).await,
|
||||
self.utd_hook_manager.get().cloned(),
|
||||
)))
|
||||
}
|
||||
|
||||
/// Lets the user know whether this is an `m.login.password` based
|
||||
@@ -1182,30 +1418,201 @@ impl Client {
|
||||
|
||||
/// Clear all the non-critical caches for this Client instance.
|
||||
///
|
||||
/// WARNING: This will clear all the caches, including the base store (state
|
||||
/// store), so callers must make sure that any sync is inactive before
|
||||
/// calling this method. In particular, the `SyncService` must not be
|
||||
/// running. After the method returns, the Client will be in an unstable
|
||||
/// state, and it is required that the caller reinstantiates a new
|
||||
/// Client instance, be it via dropping the previous and re-creating it,
|
||||
/// restarting their application, or any other similar means.
|
||||
///
|
||||
/// - This will get rid of the backing state store file, if provided.
|
||||
/// - This will empty all the room's persisted event caches, so all rooms
|
||||
/// will start as if they were empty.
|
||||
/// - This will empty the media cache according to the current media
|
||||
/// retention policy.
|
||||
pub async fn clear_caches(&self) -> Result<(), ClientError> {
|
||||
let closure = async || -> Result<_, EventCacheError> {
|
||||
let closure = async || -> Result<_, ClientError> {
|
||||
// Clean up the media cache according to the current media retention policy.
|
||||
self.inner.event_cache_store().lock().await?.clean_up_media_cache().await?;
|
||||
self.inner
|
||||
.event_cache_store()
|
||||
.lock()
|
||||
.await
|
||||
.map_err(EventCacheError::from)?
|
||||
.clean_up_media_cache()
|
||||
.await
|
||||
.map_err(EventCacheError::from)?;
|
||||
|
||||
// Clear all the room chunks. It's important to *not* call
|
||||
// `EventCacheStore::clear_all_rooms_chunks` here, because there might be live
|
||||
// `EventCacheStore::clear_all_linked_chunks` here, because there might be live
|
||||
// observers of the linked chunks, and that would cause some very bad state
|
||||
// mismatch.
|
||||
self.inner.event_cache().clear_all_rooms().await?;
|
||||
|
||||
// Delete the state store file, if it exists.
|
||||
if let Some(store_path) = &self.store_path {
|
||||
debug!("Removing the state store: {}", store_path.display());
|
||||
|
||||
// The state store and the crypto store both live in the same store path, so we
|
||||
// can't blindly delete the directory.
|
||||
//
|
||||
// Delete the state store SQLite file, as well as the write-ahead log (WAL) and
|
||||
// shared-memory (SHM) files, if they exist.
|
||||
|
||||
for file_name in [
|
||||
PathBuf::from(STATE_STORE_DATABASE_NAME),
|
||||
PathBuf::from(format!("{STATE_STORE_DATABASE_NAME}.wal")),
|
||||
PathBuf::from(format!("{STATE_STORE_DATABASE_NAME}.shm")),
|
||||
] {
|
||||
let file_path = store_path.join(file_name);
|
||||
if file_path.exists() {
|
||||
debug!("Removing file: {}", file_path.display());
|
||||
std::fs::remove_file(&file_path).map_err(|err| ClientError::Generic {
|
||||
msg: format!(
|
||||
"couldn't delete the state store file {}: {err}",
|
||||
file_path.display()
|
||||
),
|
||||
details: None,
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
Ok(closure().await?)
|
||||
closure().await
|
||||
}
|
||||
|
||||
/// Checks if the server supports the report room API.
|
||||
pub async fn is_report_room_api_supported(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.server_versions().await?.contains(&ruma::api::MatrixVersion::V1_13))
|
||||
}
|
||||
|
||||
/// Subscribe to changes in the media preview configuration.
|
||||
pub async fn subscribe_to_media_preview_config(
|
||||
&self,
|
||||
listener: Box<dyn MediaPreviewConfigListener>,
|
||||
) -> Result<Arc<TaskHandle>, ClientError> {
|
||||
let (initial_value, stream) = self.inner.account().observe_media_preview_config().await?;
|
||||
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
// Send the initial value to the listener.
|
||||
listener.on_change(initial_value.map(|config| config.into()));
|
||||
// Listen for changes and notify the listener.
|
||||
pin_mut!(stream);
|
||||
while let Some(media_preview_config) = stream.next().await {
|
||||
listener.on_change(Some(media_preview_config.into()));
|
||||
}
|
||||
}))))
|
||||
}
|
||||
|
||||
/// Set the media previews timeline display policy
|
||||
pub async fn set_media_preview_display_policy(
|
||||
&self,
|
||||
policy: MediaPreviews,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner.account().set_media_previews_display_policy(policy.into()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the media previews timeline display policy
|
||||
/// currently stored in the cache.
|
||||
pub async fn get_media_preview_display_policy(
|
||||
&self,
|
||||
) -> Result<Option<MediaPreviews>, ClientError> {
|
||||
let configuration = self.inner.account().get_media_preview_config_event_content().await?;
|
||||
match configuration {
|
||||
Some(configuration) => Ok(Some(configuration.media_previews.into())),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the invite request avatars display policy
|
||||
pub async fn set_invite_avatars_display_policy(
|
||||
&self,
|
||||
policy: InviteAvatars,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner.account().set_invite_avatars_display_policy(policy.into()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the invite request avatars display policy
|
||||
/// currently stored in the cache.
|
||||
pub async fn get_invite_avatars_display_policy(
|
||||
&self,
|
||||
) -> Result<Option<InviteAvatars>, ClientError> {
|
||||
let configuration = self.inner.account().get_media_preview_config_event_content().await?;
|
||||
match configuration {
|
||||
Some(configuration) => Ok(Some(configuration.invite_avatars.into())),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch the media preview configuration from the server.
|
||||
pub async fn fetch_media_preview_config(
|
||||
&self,
|
||||
) -> Result<Option<MediaPreviewConfig>, ClientError> {
|
||||
Ok(self.inner.account().fetch_media_preview_config_event_content().await?.map(Into::into))
|
||||
}
|
||||
|
||||
/// Gets the `max_upload_size` value from the homeserver, which controls the
|
||||
/// max size a media upload request can have.
|
||||
pub async fn get_max_media_upload_size(&self) -> Result<u64, ClientError> {
|
||||
let max_upload_size = self.inner.load_or_fetch_max_upload_size().await?;
|
||||
Ok(max_upload_size.into())
|
||||
}
|
||||
|
||||
/// Subscribe to [`RoomInfo`] updates given a provided [`RoomId`].
|
||||
///
|
||||
/// This works even for rooms we haven't received yet, so we can subscribe
|
||||
/// to this and wait until we receive updates from them when sync responses
|
||||
/// are processed.
|
||||
///
|
||||
/// Note this method should be used sparingly since using callback
|
||||
/// interfaces is expensive, as well as keeping them alive for a long
|
||||
/// time. Usages of this method should be short-lived and dropped as
|
||||
/// soon as possible.
|
||||
pub async fn subscribe_to_room_info(
|
||||
&self,
|
||||
room_id: String,
|
||||
listener: Box<dyn RoomInfoListener>,
|
||||
) -> Result<Arc<TaskHandle>, ClientError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
|
||||
// Emit the initial event, if present
|
||||
if let Some(room) = self.inner.get_room(&room_id) {
|
||||
if let Ok(room_info) = RoomInfo::new(&room).await {
|
||||
listener.call(room_info);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn({
|
||||
let client = self.inner.clone();
|
||||
let mut receiver = client.room_info_notable_update_receiver();
|
||||
async move {
|
||||
while let Ok(room_update) = receiver.recv().await {
|
||||
if room_update.room_id != room_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(room) = client.get_room(&room_id) {
|
||||
if let Ok(room_info) = RoomInfo::new(&room).await {
|
||||
listener.call(room_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))))
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait IgnoredUsersListener: Sync + Send {
|
||||
pub trait MediaPreviewConfigListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_change(&self, media_preview_config: Option<MediaPreviewConfig>);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait IgnoredUsersListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn call(&self, ignored_user_ids: Vec<String>);
|
||||
}
|
||||
|
||||
@@ -1280,15 +1687,13 @@ impl From<&search_users::v3::User> for UserProfile {
|
||||
|
||||
impl Client {
|
||||
fn process_session_change(&self, session_change: SessionChange) {
|
||||
if let Some(delegate) = self.delegate.read().unwrap().clone() {
|
||||
if let Some(delegate) = self.delegate.get().cloned() {
|
||||
debug!("Applying session change: {session_change:?}");
|
||||
get_runtime_handle().spawn_blocking(move || match session_change {
|
||||
SessionChange::UnknownToken { soft_logout } => {
|
||||
delegate.did_receive_auth_error(soft_logout);
|
||||
}
|
||||
SessionChange::TokensRefreshed => {
|
||||
delegate.did_refresh_tokens();
|
||||
}
|
||||
SessionChange::TokensRefreshed => {}
|
||||
});
|
||||
} else {
|
||||
debug!(
|
||||
@@ -1519,7 +1924,8 @@ impl TryFrom<CreateRoomParameters> for create_room::v3::Request {
|
||||
Err(e) => {
|
||||
return Err(ClientError::Generic {
|
||||
msg: format!("Failed to serialize power levels, error: {e}"),
|
||||
})
|
||||
details: Some(format!("{e:?}")),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1984,7 +2390,7 @@ impl TryFrom<RumaJoinRule> for JoinRule {
|
||||
}
|
||||
RumaJoinRule::Invite => Ok(JoinRule::Invite),
|
||||
RumaJoinRule::_Custom(_) => Ok(JoinRule::Custom { repr: value.as_str().to_owned() }),
|
||||
_ => Err(format!("Unknown JoinRule: {:?}", value)),
|
||||
_ => Err(format!("Unknown JoinRule: {value:?}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2001,7 +2407,7 @@ impl TryFrom<RumaAllowRule> for AllowRule {
|
||||
.map_err(|e| format!("Couldn't serialize custom AllowRule: {e:?}"))?;
|
||||
Ok(Self::Custom { json })
|
||||
}
|
||||
_ => Err(format!("Invalid AllowRule: {:?}", value)),
|
||||
_ => Err(format!("Invalid AllowRule: {value:?}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::{fs, num::NonZeroUsize, path::Path, sync::Arc, time::Duration};
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
authentication::oauth::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason},
|
||||
@@ -19,6 +18,7 @@ use matrix_sdk::{
|
||||
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
|
||||
RumaApiError, SqliteStoreConfig,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::api::error::{DeserializationError, FromHttpResponseError};
|
||||
use tracing::{debug, error};
|
||||
use zeroize::Zeroizing;
|
||||
@@ -26,7 +26,7 @@ use zeroize::Zeroizing;
|
||||
use super::client::Client;
|
||||
use crate::{
|
||||
authentication::OidcConfiguration, client::ClientSessionDelegate, error::ClientError,
|
||||
helpers::unwrap_or_clone_arc, task_handle::TaskHandle,
|
||||
helpers::unwrap_or_clone_arc, runtime::get_runtime_handle, task_handle::TaskHandle,
|
||||
};
|
||||
|
||||
/// A list of bytes containing a certificate in DER or PEM form.
|
||||
@@ -57,6 +57,18 @@ impl QrCodeData {
|
||||
pub fn from_bytes(bytes: Vec<u8>) -> Result<Arc<Self>, QrCodeDecodeError> {
|
||||
Ok(Self { inner: qrcode::QrCodeData::from_bytes(&bytes)? }.into())
|
||||
}
|
||||
|
||||
/// The server name contained within the scanned QR code data.
|
||||
///
|
||||
/// Note: This value is only present when scanning a QR code the belongs to
|
||||
/// a logged in client. The mode where the new client shows the QR code
|
||||
/// will return `None`.
|
||||
pub fn server_name(&self) -> Option<String> {
|
||||
match &self.inner.mode_data {
|
||||
QrCodeModeData::Reciprocate { server_name } => Some(server_name.to_owned()),
|
||||
QrCodeModeData::Login => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for the decoding of the [`QrCodeData`].
|
||||
@@ -161,7 +173,7 @@ pub enum QrLoginProgress {
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait QrLoginProgressListener: Sync + Send {
|
||||
pub trait QrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, state: QrLoginProgress);
|
||||
}
|
||||
|
||||
@@ -275,11 +287,8 @@ pub struct ClientBuilder {
|
||||
encryption_settings: EncryptionSettings,
|
||||
room_key_recipient_strategy: CollectStrategy,
|
||||
decryption_trust_requirement: TrustRequirement,
|
||||
enable_share_history_on_invite: bool,
|
||||
request_config: Option<RequestConfig>,
|
||||
|
||||
/// Whether to enable use of the event cache store, for reloading events
|
||||
/// when building timelines et al.
|
||||
use_event_cache_persistent_storage: bool,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
@@ -313,28 +322,11 @@ impl ClientBuilder {
|
||||
},
|
||||
room_key_recipient_strategy: Default::default(),
|
||||
decryption_trust_requirement: TrustRequirement::Untrusted,
|
||||
enable_share_history_on_invite: false,
|
||||
request_config: Default::default(),
|
||||
use_event_cache_persistent_storage: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether to use the event cache persistent storage or not.
|
||||
///
|
||||
/// This is a temporary feature flag, for testing the event cache's
|
||||
/// persistent storage. Follow new developments in https://github.com/matrix-org/matrix-rust-sdk/issues/3280.
|
||||
///
|
||||
/// This is disabled by default. When disabled, a one-time cleanup is
|
||||
/// performed when creating the client, and it will clear all the events
|
||||
/// previously stored in the event cache.
|
||||
///
|
||||
/// When enabled, it will attempt to store events in the event cache as
|
||||
/// they're received, and reuse them when reconstructing timelines.
|
||||
pub fn use_event_cache_persistent_storage(self: Arc<Self>, value: bool) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.use_event_cache_persistent_storage = value;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn cross_process_store_locks_holder_name(
|
||||
self: Arc<Self>,
|
||||
holder_name: String,
|
||||
@@ -562,6 +554,19 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set whether to enable the experimental support for sending and receiving
|
||||
/// encrypted room history on invite, per [MSC4268].
|
||||
///
|
||||
/// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
|
||||
pub fn enable_share_history_on_invite(
|
||||
self: Arc<Self>,
|
||||
enable_share_history_on_invite: bool,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.enable_share_history_on_invite = enable_share_history_on_invite;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Add a default request config to this client.
|
||||
pub fn request_config(self: Arc<Self>, config: RequestConfig) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
@@ -578,14 +583,16 @@ impl ClientBuilder {
|
||||
inner_builder.cross_process_store_locks_holder_name(holder_name.clone());
|
||||
}
|
||||
|
||||
if let Some(session_paths) = &builder.session_paths {
|
||||
let store_path = if let Some(session_paths) = &builder.session_paths {
|
||||
// This is the path where both the state store and the crypto store will live.
|
||||
let data_path = Path::new(&session_paths.data_path);
|
||||
// This is the path where the event cache store will live.
|
||||
let cache_path = Path::new(&session_paths.cache_path);
|
||||
|
||||
debug!(
|
||||
data_path = %data_path.to_string_lossy(),
|
||||
cache_path = %cache_path.to_string_lossy(),
|
||||
"Creating directories for data and cache stores.",
|
||||
event_cache_path = %cache_path.to_string_lossy(),
|
||||
"Creating directories for data (state and crypto) and cache stores.",
|
||||
);
|
||||
|
||||
fs::create_dir_all(data_path)?;
|
||||
@@ -614,9 +621,12 @@ impl ClientBuilder {
|
||||
|
||||
inner_builder = inner_builder
|
||||
.sqlite_store_with_config_and_cache_path(sqlite_store_config, Some(cache_path));
|
||||
|
||||
Some(data_path.to_owned())
|
||||
} else {
|
||||
debug!("Not using a store path.");
|
||||
}
|
||||
None
|
||||
};
|
||||
|
||||
// Determine server either from URL, server name or user ID.
|
||||
inner_builder = match builder.homeserver_cfg {
|
||||
@@ -685,7 +695,8 @@ impl ClientBuilder {
|
||||
inner_builder = inner_builder
|
||||
.with_encryption_settings(builder.encryption_settings)
|
||||
.with_room_key_recipient_strategy(builder.room_key_recipient_strategy)
|
||||
.with_decryption_trust_requirement(builder.decryption_trust_requirement);
|
||||
.with_decryption_trust_requirement(builder.decryption_trust_requirement)
|
||||
.with_enable_share_history_on_invite(builder.enable_share_history_on_invite);
|
||||
|
||||
match builder.sliding_sync_version_builder {
|
||||
SlidingSyncVersionBuilder::None => {
|
||||
@@ -727,22 +738,14 @@ impl ClientBuilder {
|
||||
|
||||
let sdk_client = inner_builder.build().await?;
|
||||
|
||||
if builder.use_event_cache_persistent_storage {
|
||||
// Enable the persistent storage \o/
|
||||
sdk_client.event_cache().enable_storage()?;
|
||||
} else {
|
||||
// Get rid of all the previous events, if any.
|
||||
let store = sdk_client
|
||||
.event_cache_store()
|
||||
.lock()
|
||||
.await
|
||||
.map_err(EventCacheError::LockingStorage)?;
|
||||
store.clear_all_rooms_chunks().await.map_err(EventCacheError::Storage)?;
|
||||
}
|
||||
|
||||
Ok(Arc::new(
|
||||
Client::new(sdk_client, builder.enable_oidc_refresh_lock, builder.session_delegate)
|
||||
.await?,
|
||||
Client::new(
|
||||
sdk_client,
|
||||
builder.enable_oidc_refresh_lock,
|
||||
builder.session_delegate,
|
||||
store_path,
|
||||
)
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -18,5 +18,5 @@ pub struct ElementWellKnown {
|
||||
/// Helper function to parse a string into a ElementWellKnown struct
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn make_element_well_known(string: String) -> Result<ElementWellKnown, ClientError> {
|
||||
serde_json::from_str(&string).map_err(ClientError::new)
|
||||
serde_json::from_str(&string).map_err(ClientError::from_err)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
encryption,
|
||||
encryption::{backups, recovery},
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use thiserror::Error;
|
||||
use tracing::{error, info};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{client::Client, error::ClientError, ruma::AuthData, task_handle::TaskHandle};
|
||||
use crate::{
|
||||
client::Client, error::ClientError, ruma::AuthData, runtime::get_runtime_handle,
|
||||
task_handle::TaskHandle,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Encryption {
|
||||
@@ -25,22 +28,22 @@ pub struct Encryption {
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait BackupStateListener: Sync + Send {
|
||||
pub trait BackupStateListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, status: BackupState);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait BackupSteadyStateListener: Sync + Send {
|
||||
pub trait BackupSteadyStateListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, status: BackupUploadState);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RecoveryStateListener: Sync + Send {
|
||||
pub trait RecoveryStateListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, status: RecoveryState);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait VerificationStateListener: Sync + Send {
|
||||
pub trait VerificationStateListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, status: VerificationState);
|
||||
}
|
||||
|
||||
@@ -164,7 +167,7 @@ impl From<recovery::RecoveryState> for RecoveryState {
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait EnableRecoveryProgressListener: Sync + Send {
|
||||
pub trait EnableRecoveryProgressListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, status: EnableRecoveryProgress);
|
||||
}
|
||||
|
||||
@@ -369,12 +372,8 @@ impl Encryption {
|
||||
/// Completely reset the current user's crypto identity: reset the cross
|
||||
/// signing keys, delete the existing backup and recovery key.
|
||||
pub async fn reset_identity(&self) -> Result<Option<Arc<IdentityResetHandle>>, ClientError> {
|
||||
if let Some(reset_handle) = self
|
||||
.inner
|
||||
.recovery()
|
||||
.reset_identity()
|
||||
.await
|
||||
.map_err(|e| ClientError::Generic { msg: e.to_string() })?
|
||||
if let Some(reset_handle) =
|
||||
self.inner.recovery().reset_identity().await.map_err(ClientError::from_err)?
|
||||
{
|
||||
return Ok(Some(Arc::new(IdentityResetHandle { inner: reset_handle })));
|
||||
}
|
||||
@@ -441,7 +440,7 @@ impl Encryption {
|
||||
info!("No identity found in the store.");
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Failed fetching identity from the store: {}", error);
|
||||
error!("Failed fetching identity from the store: {error}");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -541,12 +540,9 @@ impl IdentityResetHandle {
|
||||
/// 4. Finally, re-enable key backups only if they were enabled before
|
||||
pub async fn reset(&self, auth: Option<AuthData>) -> Result<(), ClientError> {
|
||||
if let Some(auth) = auth {
|
||||
self.inner
|
||||
.reset(Some(auth.into()))
|
||||
.await
|
||||
.map_err(|e| ClientError::Generic { msg: e.to_string() })
|
||||
self.inner.reset(Some(auth.into())).await.map_err(ClientError::from_err)
|
||||
} else {
|
||||
self.inner.reset(None).await.map_err(|e| ClientError::Generic { msg: e.to_string() })
|
||||
self.inner.reset(None).await.map_err(ClientError::from_err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{collections::HashMap, fmt, fmt::Display, time::SystemTime};
|
||||
use std::{collections::HashMap, error::Error, fmt, fmt::Display, time::SystemTime};
|
||||
|
||||
use matrix_sdk::{
|
||||
authentication::oauth::OAuthError, encryption::CryptoStoreError, event_cache::EventCacheError,
|
||||
@@ -8,6 +8,7 @@ use matrix_sdk::{
|
||||
};
|
||||
use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline};
|
||||
use ruma::api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter};
|
||||
use tracing::warn;
|
||||
use uniffi::UnexpectedUniFFICallbackError;
|
||||
|
||||
use crate::{room_list::RoomListError, timeline::FocusEventError};
|
||||
@@ -15,32 +16,39 @@ use crate::{room_list::RoomListError, timeline::FocusEventError};
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
pub enum ClientError {
|
||||
#[error("client error: {msg}")]
|
||||
Generic { msg: String },
|
||||
Generic { msg: String, details: Option<String> },
|
||||
#[error("api error {code}: {msg}")]
|
||||
MatrixApi { kind: ErrorKind, code: String, msg: String },
|
||||
MatrixApi { kind: ErrorKind, code: String, msg: String, details: Option<String> },
|
||||
}
|
||||
|
||||
impl ClientError {
|
||||
pub(crate) fn new<E: Display>(error: E) -> Self {
|
||||
Self::Generic { msg: error.to_string() }
|
||||
pub(crate) fn from_str<E: Display>(error: E, details: Option<String>) -> Self {
|
||||
warn!("Error: {error}");
|
||||
Self::Generic { msg: error.to_string(), details }
|
||||
}
|
||||
|
||||
pub(crate) fn from_err<E: Error>(e: E) -> Self {
|
||||
let details = Some(format!("{e:?}"));
|
||||
Self::from_str(e, details)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for ClientError {
|
||||
fn from(e: anyhow::Error) -> ClientError {
|
||||
ClientError::Generic { msg: format!("{e:#}") }
|
||||
let details = format!("{e:?}");
|
||||
ClientError::Generic { msg: format!("{e:#}"), details: Some(details) }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for ClientError {
|
||||
fn from(e: reqwest::Error) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UnexpectedUniFFICallbackError> for ClientError {
|
||||
fn from(e: UnexpectedUniFFICallbackError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,135 +61,140 @@ impl From<matrix_sdk::Error> for ClientError {
|
||||
let code = kind.errcode().to_string();
|
||||
let Ok(kind) = kind.to_owned().try_into() else {
|
||||
// We couldn't parse the API error, so we return a generic one instead
|
||||
return Self::Generic { msg: message.to_string() };
|
||||
return (*http_error).into();
|
||||
};
|
||||
return Self::MatrixApi {
|
||||
kind,
|
||||
code,
|
||||
msg: message.to_owned(),
|
||||
details: Some(format!("{api_error:?}")),
|
||||
};
|
||||
return Self::MatrixApi { kind, code, msg: message.to_owned() };
|
||||
}
|
||||
}
|
||||
Self::Generic { msg: http_error.to_string() }
|
||||
(*http_error).into()
|
||||
}
|
||||
_ => Self::Generic { msg: e.to_string() },
|
||||
_ => Self::from_err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StoreError> for ClientError {
|
||||
fn from(e: StoreError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CryptoStoreError> for ClientError {
|
||||
fn from(e: CryptoStoreError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HttpError> for ClientError {
|
||||
fn from(e: HttpError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IdParseError> for ClientError {
|
||||
fn from(e: IdParseError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for ClientError {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<url::ParseError> for ClientError {
|
||||
fn from(e: url::ParseError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<mime::FromStrError> for ClientError {
|
||||
fn from(e: mime::FromStrError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<encryption_sync_service::Error> for ClientError {
|
||||
fn from(e: encryption_sync_service::Error) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<timeline::Error> for ClientError {
|
||||
fn from(e: timeline::Error) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<timeline::UnsupportedEditItem> for ClientError {
|
||||
fn from(e: timeline::UnsupportedEditItem) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<notification_client::Error> for ClientError {
|
||||
fn from(e: notification_client::Error) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sync_service::Error> for ClientError {
|
||||
fn from(e: sync_service::Error) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OAuthError> for ClientError {
|
||||
fn from(e: OAuthError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomError> for ClientError {
|
||||
fn from(e: RoomError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomListError> for ClientError {
|
||||
fn from(e: RoomListError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EventCacheError> for ClientError {
|
||||
fn from(e: EventCacheError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EditError> for ClientError {
|
||||
fn from(e: EditError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomSendQueueError> for ClientError {
|
||||
fn from(e: RoomSendQueueError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NotYetImplemented> for ClientError {
|
||||
fn from(_: NotYetImplemented) -> Self {
|
||||
Self::new("This functionality is not implemented yet.")
|
||||
Self::from_str("This functionality is not implemented yet.", None)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FocusEventError> for ClientError {
|
||||
fn from(e: FocusEventError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use anyhow::{bail, Context};
|
||||
use matrix_sdk::IdParseError;
|
||||
use matrix_sdk_ui::timeline::TimelineEventItemId;
|
||||
@@ -22,7 +24,7 @@ use crate::{
|
||||
};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct TimelineEvent(pub(crate) AnySyncTimelineEvent);
|
||||
pub struct TimelineEvent(pub(crate) Box<AnySyncTimelineEvent>);
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl TimelineEvent {
|
||||
@@ -39,7 +41,7 @@ impl TimelineEvent {
|
||||
}
|
||||
|
||||
pub fn event_type(&self) -> Result<TimelineEventType, ClientError> {
|
||||
let event_type = match &self.0 {
|
||||
let event_type = match self.0.deref() {
|
||||
AnySyncTimelineEvent::MessageLike(event) => {
|
||||
TimelineEventType::MessageLike { content: event.clone().try_into()? }
|
||||
}
|
||||
@@ -53,11 +55,19 @@ impl TimelineEvent {
|
||||
|
||||
impl From<AnyTimelineEvent> for TimelineEvent {
|
||||
fn from(event: AnyTimelineEvent) -> Self {
|
||||
Self(event.into())
|
||||
Self(Box::new(event.into()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
// A note about this `allow(clippy::large_enum_variant)`.
|
||||
// In order to reduce the size of `TimelineEventType`, we would need to
|
||||
// put some parts in a `Box`, or an `Arc`. Sadly, it doesn't play well with
|
||||
// UniFFI. We would need to change the `uniffi::Record` of the subtypes into
|
||||
// `uniffi::Object`, which is a radical change. It would simplify the memory
|
||||
// usage, but it would slow down the performance around the FFI border. Thus,
|
||||
// let's consider this is a false-positive lint in this particular case.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum TimelineEventType {
|
||||
MessageLike { content: MessageLikeEventContent },
|
||||
State { content: StateEventContent },
|
||||
@@ -83,7 +93,7 @@ pub enum StateEventContent {
|
||||
RoomServerAcl,
|
||||
RoomThirdPartyInvite,
|
||||
RoomTombstone,
|
||||
RoomTopic,
|
||||
RoomTopic { topic: String },
|
||||
SpaceChild,
|
||||
SpaceParent,
|
||||
}
|
||||
@@ -118,16 +128,28 @@ impl TryFrom<AnySyncStateEvent> for StateEventContent {
|
||||
AnySyncStateEvent::RoomServerAcl(_) => StateEventContent::RoomServerAcl,
|
||||
AnySyncStateEvent::RoomThirdPartyInvite(_) => StateEventContent::RoomThirdPartyInvite,
|
||||
AnySyncStateEvent::RoomTombstone(_) => StateEventContent::RoomTombstone,
|
||||
AnySyncStateEvent::RoomTopic(_) => StateEventContent::RoomTopic,
|
||||
AnySyncStateEvent::RoomTopic(content) => {
|
||||
let content = get_state_event_original_content(content)?;
|
||||
|
||||
StateEventContent::RoomTopic { topic: content.topic }
|
||||
}
|
||||
AnySyncStateEvent::SpaceChild(_) => StateEventContent::SpaceChild,
|
||||
AnySyncStateEvent::SpaceParent(_) => StateEventContent::SpaceParent,
|
||||
_ => bail!("Unsupported state event"),
|
||||
_ => bail!("Unsupported state event: {:?}", value.event_type()),
|
||||
};
|
||||
Ok(event)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
// A note about this `allow(clippy::large_enum_variant)`.
|
||||
// In order to reduce the size of `MessageLineEventContent`, we would need to
|
||||
// put some parts in a `Box`, or an `Arc`. Sadly, it doesn't play well with
|
||||
// UniFFI. We would need to change the `uniffi::Record` of the subtypes into
|
||||
// `uniffi::Object`, which is a radical change. It would simplify the memory
|
||||
// usage, but it would slow down the performance around the FFI border. Thus,
|
||||
// let's consider this is a false-positive lint in this particular case.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum MessageLikeEventContent {
|
||||
CallAnswer,
|
||||
CallInvite,
|
||||
@@ -222,7 +244,7 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
|
||||
MessageLikeEventContent::RoomRedaction { redacted_event_id, reason }
|
||||
}
|
||||
AnySyncMessageLikeEvent::Sticker(_) => MessageLikeEventContent::Sticker,
|
||||
_ => bail!("Unsupported Event Type"),
|
||||
_ => bail!("Unsupported Event Type: {:?}", value.event_type()),
|
||||
};
|
||||
Ok(content)
|
||||
}
|
||||
@@ -365,6 +387,8 @@ pub enum RoomMessageEventMessageType {
|
||||
Audio,
|
||||
Emote,
|
||||
File,
|
||||
#[cfg(feature = "unstable-msc4274")]
|
||||
Gallery,
|
||||
Image,
|
||||
Location,
|
||||
Notice,
|
||||
@@ -381,6 +405,8 @@ impl From<RumaMessageType> for RoomMessageEventMessageType {
|
||||
RumaMessageType::Audio { .. } => Self::Audio,
|
||||
RumaMessageType::Emote { .. } => Self::Emote,
|
||||
RumaMessageType::File { .. } => Self::File,
|
||||
#[cfg(feature = "unstable-msc4274")]
|
||||
RumaMessageType::Gallery { .. } => Self::Gallery,
|
||||
RumaMessageType::Image { .. } => Self::Image,
|
||||
RumaMessageType::Location { .. } => Self::Location,
|
||||
RumaMessageType::Notice { .. } => Self::Notice,
|
||||
|
||||
@@ -26,11 +26,13 @@ mod room_list;
|
||||
mod room_member;
|
||||
mod room_preview;
|
||||
mod ruma;
|
||||
mod runtime;
|
||||
mod session_verification;
|
||||
mod sync_service;
|
||||
mod task_handle;
|
||||
mod timeline;
|
||||
mod tracing;
|
||||
mod utd;
|
||||
mod utils;
|
||||
mod widget;
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
use std::sync::Arc;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use matrix_sdk_ui::notification_client::{
|
||||
NotificationClient as MatrixNotificationClient, NotificationItem as MatrixNotificationItem,
|
||||
};
|
||||
use ruma::{EventId, RoomId};
|
||||
use ruma::{EventId, OwnedEventId, OwnedRoomId, RoomId};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
client::{Client, JoinRule},
|
||||
error::ClientError,
|
||||
event::TimelineEvent,
|
||||
room::Room,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
@@ -94,11 +96,23 @@ pub struct NotificationClient {
|
||||
/// Note: we do this to make it so that the FFI `NotificationClient` keeps
|
||||
/// the FFI `Client` and thus the SDK `Client` alive. Otherwise, we
|
||||
/// would need to repeat the hack done in the FFI `Client::drop` method.
|
||||
pub(crate) _client: Arc<Client>,
|
||||
pub(crate) client: Arc<Client>,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl NotificationClient {
|
||||
/// Fetches a room by its ID using the in-memory state store backed client.
|
||||
///
|
||||
/// Useful to retrieve room information after running the limited
|
||||
/// notification client sliding sync loop.
|
||||
pub fn get_room(&self, room_id: String) -> Result<Option<Arc<Room>>, ClientError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
let sdk_room = self.inner.get_room(&room_id);
|
||||
let room = sdk_room
|
||||
.map(|room| Arc::new(Room::new(room, self.client.utd_hook_manager.get().cloned())));
|
||||
Ok(room)
|
||||
}
|
||||
|
||||
/// See also documentation of
|
||||
/// `MatrixNotificationClient::get_notification`.
|
||||
pub async fn get_notification(
|
||||
@@ -118,4 +132,66 @@ impl NotificationClient {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get several notification items in a single batch.
|
||||
///
|
||||
/// Returns an error if the flow failed when preparing to fetch the
|
||||
/// notifications, and a [`HashMap`] containing either a
|
||||
/// [`NotificationItem`] or no entry for it if it failed to fetch a
|
||||
/// notification for the provided [`EventId`].
|
||||
pub async fn get_notifications(
|
||||
&self,
|
||||
requests: Vec<NotificationItemsRequest>,
|
||||
) -> Result<HashMap<String, NotificationItem>, ClientError> {
|
||||
let requests =
|
||||
requests.into_iter().map(TryInto::try_into).collect::<Result<Vec<_>, _>>()?;
|
||||
let items = self.inner.get_notifications(&requests).await?;
|
||||
let mut result = HashMap::new();
|
||||
for (key, value) in items.into_iter() {
|
||||
match value {
|
||||
Ok(item) => {
|
||||
result.insert(key.to_string(), NotificationItem::from_inner(item));
|
||||
}
|
||||
Err(error) => {
|
||||
// TODO This error should actually be returned so the clients can handle the
|
||||
// error as they see fit, but it's failing when creating
|
||||
// bindings for Go, i.e.
|
||||
// (https://github.com/NordSecurity/uniffi-bindgen-go/issues/62)
|
||||
error!("Could not fetch notification {key}, an error happened: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// A request for notification items grouped by their room.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct NotificationItemsRequest {
|
||||
room_id: String,
|
||||
event_ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl NotificationItemsRequest {
|
||||
/// The parsed [`OwnedRoomId`] to use with the SDK crates.
|
||||
pub fn room_id(&self) -> Result<OwnedRoomId, ClientError> {
|
||||
RoomId::parse(&self.room_id).map_err(ClientError::from)
|
||||
}
|
||||
|
||||
/// The parsed [`OwnedEventId`] list to use with the SDK crates.
|
||||
pub fn event_ids(&self) -> Result<Vec<OwnedEventId>, ClientError> {
|
||||
self.event_ids
|
||||
.iter()
|
||||
.map(|id| EventId::parse(id).map_err(ClientError::from))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<NotificationItemsRequest>
|
||||
for matrix_sdk_ui::notification_client::NotificationItemsRequest
|
||||
{
|
||||
type Error = ClientError;
|
||||
fn try_from(value: NotificationItemsRequest) -> Result<Self, Self::Error> {
|
||||
Ok(Self { room_id: value.room_id()?, event_ids: value.event_ids()? })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use matrix_sdk::{
|
||||
ruma::events::push_rules::PushRulesEvent,
|
||||
Client as MatrixClient,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::{
|
||||
push::{
|
||||
Action as SdkAction, ComparisonOperator as SdkComparisonOperator, PredefinedOverrideRuleId,
|
||||
@@ -161,7 +162,7 @@ pub enum PushCondition {
|
||||
}
|
||||
|
||||
impl TryFrom<SdkPushCondition> for PushCondition {
|
||||
type Error = ();
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: SdkPushCondition) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
@@ -179,7 +180,7 @@ impl TryFrom<SdkPushCondition> for PushCondition {
|
||||
SdkPushCondition::EventPropertyContains { key, value } => {
|
||||
Self::EventPropertyContains { key, value: value.into() }
|
||||
}
|
||||
_ => return Err(()),
|
||||
_ => return Err("Unsupported condition type".to_owned()),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -290,7 +291,7 @@ impl TryFrom<SdkTweak> for Tweak {
|
||||
SdkTweak::Highlight(highlight) => Self::Highlight { value: highlight },
|
||||
SdkTweak::Custom { name, value } => {
|
||||
let json_string = serde_json::to_string(&value)
|
||||
.map_err(|e| format!("Failed to serialize custom tweak value: {}", e))?;
|
||||
.map_err(|e| format!("Failed to serialize custom tweak value: {e}"))?;
|
||||
|
||||
Self::Custom { name, value: json_string }
|
||||
}
|
||||
@@ -308,9 +309,9 @@ impl TryFrom<Tweak> for SdkTweak {
|
||||
Tweak::Highlight { value } => Self::Highlight(value),
|
||||
Tweak::Custom { name, value } => {
|
||||
let json_value: serde_json::Value = serde_json::from_str(&value)
|
||||
.map_err(|e| format!("Failed to deserialize custom tweak value: {}", e))?;
|
||||
.map_err(|e| format!("Failed to deserialize custom tweak value: {e}"))?;
|
||||
let value = serde_json::from_value(json_value)
|
||||
.map_err(|e| format!("Failed to convert JSON value: {}", e))?;
|
||||
.map_err(|e| format!("Failed to convert JSON value: {e}"))?;
|
||||
|
||||
Self::Custom { name, value }
|
||||
}
|
||||
@@ -334,7 +335,7 @@ impl TryFrom<SdkAction> for Action {
|
||||
Ok(match value {
|
||||
SdkAction::Notify => Self::Notify,
|
||||
SdkAction::SetTweak(tweak) => Self::SetTweak {
|
||||
value: tweak.try_into().map_err(|e| format!("Failed to convert tweak: {}", e))?,
|
||||
value: tweak.try_into().map_err(|e| format!("Failed to convert tweak: {e}"))?,
|
||||
},
|
||||
_ => return Err("Unsupported action type".to_owned()),
|
||||
})
|
||||
@@ -348,7 +349,7 @@ impl TryFrom<Action> for SdkAction {
|
||||
Ok(match value {
|
||||
Action::Notify => Self::Notify,
|
||||
Action::SetTweak { value } => Self::SetTweak(
|
||||
value.try_into().map_err(|e| format!("Failed to convert tweak: {}", e))?,
|
||||
value.try_into().map_err(|e| format!("Failed to convert tweak: {e}"))?,
|
||||
),
|
||||
})
|
||||
}
|
||||
@@ -387,7 +388,7 @@ impl From<RoomNotificationMode> for SdkRoomNotificationMode {
|
||||
|
||||
/// Delegate to notify of changes in push rules
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait NotificationSettingsDelegate: Sync + Send {
|
||||
pub trait NotificationSettingsDelegate: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn settings_did_change(&self);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
use std::sync::{atomic::AtomicBool, Arc, OnceLock};
|
||||
|
||||
use tracing::warn;
|
||||
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
||||
use tracing_core::Subscriber;
|
||||
use tracing_subscriber::{
|
||||
@@ -8,19 +11,13 @@ use tracing_subscriber::{
|
||||
time::FormatTime,
|
||||
FormatEvent, FormatFields, FormattedFields,
|
||||
},
|
||||
layer::SubscriberExt,
|
||||
layer::SubscriberExt as _,
|
||||
registry::LookupSpan,
|
||||
util::SubscriberInitExt,
|
||||
EnvFilter, Layer,
|
||||
util::SubscriberInitExt as _,
|
||||
Layer,
|
||||
};
|
||||
|
||||
use crate::tracing::LogLevel;
|
||||
|
||||
pub fn log_panics() {
|
||||
std::env::set_var("RUST_BACKTRACE", "1");
|
||||
|
||||
log_panics::init();
|
||||
}
|
||||
use crate::{error::ClientError, tracing::LogLevel};
|
||||
|
||||
fn text_layers<S>(config: TracingConfiguration) -> impl Layer<S>
|
||||
where
|
||||
@@ -343,6 +340,20 @@ impl TraceLogPacks {
|
||||
}
|
||||
}
|
||||
|
||||
struct SentryLoggingCtx {
|
||||
/// The Sentry client guard, which keeps the Sentry context alive.
|
||||
_guard: sentry::ClientInitGuard,
|
||||
|
||||
/// Whether the Sentry layer is enabled or not, at a global level.
|
||||
enabled: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
struct LoggingCtx {
|
||||
sentry: Option<SentryLoggingCtx>,
|
||||
}
|
||||
|
||||
static LOGGING: OnceLock<LoggingCtx> = OnceLock::new();
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct TracingConfiguration {
|
||||
/// The desired log level.
|
||||
@@ -363,6 +374,89 @@ pub struct TracingConfiguration {
|
||||
|
||||
/// If set, configures rotated log files where to write additional logs.
|
||||
write_to_files: Option<TracingFileConfiguration>,
|
||||
|
||||
/// If set, the Sentry DSN to use for error reporting.
|
||||
sentry_dsn: Option<String>,
|
||||
}
|
||||
|
||||
impl TracingConfiguration {
|
||||
/// Sets up the tracing configuration and return a [`Logger`] instance
|
||||
/// holding onto it.
|
||||
fn build(mut self) -> LoggingCtx {
|
||||
// Show full backtraces, if we run into panics.
|
||||
std::env::set_var("RUST_BACKTRACE", "1");
|
||||
|
||||
// Log panics.
|
||||
log_panics::init();
|
||||
|
||||
// Prepare the Sentry layer, if a DSN is provided.
|
||||
let (sentry_layer, sentry_logging_ctx) = if let Some(sentry_dsn) = self.sentry_dsn.take() {
|
||||
// Initialize the Sentry client with the given options.
|
||||
let sentry_guard = sentry::init((
|
||||
sentry_dsn,
|
||||
sentry::ClientOptions {
|
||||
traces_sample_rate: 0.0,
|
||||
attach_stacktrace: true,
|
||||
..sentry::ClientOptions::default()
|
||||
},
|
||||
));
|
||||
|
||||
let sentry_enabled = Arc::new(AtomicBool::new(true));
|
||||
|
||||
// Add a Sentry layer to the tracing subscriber.
|
||||
//
|
||||
// Pass custom event and span filters, which will ignore anything, if the Sentry
|
||||
// support has been globally disabled, or if the statement doesn't include a
|
||||
// `sentry` field set to `true`.
|
||||
let sentry_layer = sentry_tracing::layer()
|
||||
.event_filter({
|
||||
let enabled = sentry_enabled.clone();
|
||||
|
||||
move |metadata| {
|
||||
if enabled.load(std::sync::atomic::Ordering::SeqCst)
|
||||
&& metadata.fields().field("sentry").is_some()
|
||||
{
|
||||
sentry_tracing::default_event_filter(metadata)
|
||||
} else {
|
||||
// Ignore the event.
|
||||
sentry_tracing::EventFilter::Ignore
|
||||
}
|
||||
}
|
||||
})
|
||||
.span_filter({
|
||||
let enabled = sentry_enabled.clone();
|
||||
|
||||
move |metadata| {
|
||||
if enabled.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
sentry_tracing::default_span_filter(metadata)
|
||||
} else {
|
||||
// Ignore, if sentry is globally disabled.
|
||||
false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
(
|
||||
Some(sentry_layer),
|
||||
Some(SentryLoggingCtx { _guard: sentry_guard, enabled: sentry_enabled }),
|
||||
)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let env_filter = build_tracing_filter(&self);
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::EnvFilter::new(&env_filter))
|
||||
.with(crate::platform::text_layers(self))
|
||||
.with(sentry_layer)
|
||||
.init();
|
||||
|
||||
// Log the log levels 🧠.
|
||||
tracing::info!(env_filter, "Logging has been set up");
|
||||
|
||||
LoggingCtx { sentry: sentry_logging_ctx }
|
||||
}
|
||||
}
|
||||
|
||||
fn build_tracing_filter(config: &TracingConfiguration) -> String {
|
||||
@@ -407,24 +501,38 @@ fn build_tracing_filter(config: &TracingConfiguration) -> String {
|
||||
/// the NSE process on iOS). Otherwise, this can remain false, in which case a
|
||||
/// multithreaded tokio runtime will be set up.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn init_platform(config: TracingConfiguration, use_lightweight_tokio_runtime: bool) {
|
||||
log_panics();
|
||||
|
||||
let env_filter = build_tracing_filter(&config);
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(EnvFilter::new(&env_filter))
|
||||
.with(text_layers(config))
|
||||
.init();
|
||||
|
||||
// Log the log levels 🧠.
|
||||
tracing::info!(env_filter, "Logging has been set up");
|
||||
pub fn init_platform(
|
||||
config: TracingConfiguration,
|
||||
use_lightweight_tokio_runtime: bool,
|
||||
) -> Result<(), ClientError> {
|
||||
LOGGING.set(config.build()).map_err(|_| ClientError::Generic {
|
||||
msg: "logger already initialized".to_owned(),
|
||||
details: None,
|
||||
})?;
|
||||
|
||||
if use_lightweight_tokio_runtime {
|
||||
setup_lightweight_tokio_runtime();
|
||||
} else {
|
||||
setup_multithreaded_tokio_runtime();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the global enablement level for the Sentry layer (after the logs have
|
||||
/// been set up).
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn enable_sentry_logging(enabled: bool) {
|
||||
if let Some(ctx) = LOGGING.get() {
|
||||
if let Some(sentry_ctx) = &ctx.sentry {
|
||||
sentry_ctx.enabled.store(enabled, std::sync::atomic::Ordering::SeqCst);
|
||||
} else {
|
||||
warn!("Sentry logging is not enabled");
|
||||
}
|
||||
} else {
|
||||
// Can't use log statements here, since logging hasn't been enabled yet 🧠
|
||||
eprintln!("Logging hasn't been enabled yet");
|
||||
};
|
||||
}
|
||||
|
||||
fn setup_multithreaded_tokio_runtime() {
|
||||
@@ -479,6 +587,7 @@ mod tests {
|
||||
extra_targets: vec!["super_duper_app".to_owned()],
|
||||
write_to_stdout_or_system: true,
|
||||
write_to_files: None,
|
||||
sentry_dsn: None,
|
||||
};
|
||||
|
||||
let filter = build_tracing_filter(&config);
|
||||
@@ -515,6 +624,7 @@ mod tests {
|
||||
extra_targets: vec!["super_duper_app".to_owned(), "some_other_span".to_owned()],
|
||||
write_to_stdout_or_system: true,
|
||||
write_to_files: None,
|
||||
sentry_dsn: None,
|
||||
};
|
||||
|
||||
let filter = build_tracing_filter(&config);
|
||||
@@ -552,6 +662,7 @@ mod tests {
|
||||
extra_targets: vec!["super_duper_app".to_owned()],
|
||||
write_to_stdout_or_system: true,
|
||||
write_to_files: None,
|
||||
sentry_dsn: None,
|
||||
};
|
||||
|
||||
let filter = build_tracing_filter(&config);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::{collections::HashMap, pin::pin, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use async_compat::get_runtime_handle;
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use matrix_sdk::{
|
||||
crypto::LocalTrust,
|
||||
@@ -10,9 +9,14 @@ use matrix_sdk::{
|
||||
TryFromReportedContentScoreError,
|
||||
},
|
||||
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType, EncryptionState,
|
||||
RoomHero as SdkRoomHero, RoomMemberships, RoomState,
|
||||
PredecessorRoom as SdkPredecessorRoom, RoomHero as SdkRoomHero, RoomMemberships, RoomState,
|
||||
SuccessorRoom as SdkSuccessorRoom,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::{
|
||||
timeline::{default_event_filter, RoomExt, TimelineBuilder},
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{default_event_filter, RoomExt};
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
assign,
|
||||
@@ -26,9 +30,9 @@ use ruma::{
|
||||
},
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent, TimelineEventType,
|
||||
},
|
||||
EventId, Int, OwnedDeviceId, OwnedUserId, RoomAliasId, UserId,
|
||||
EventId, Int, OwnedDeviceId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomAliasId,
|
||||
ServerName, UserId,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{error, warn};
|
||||
|
||||
use crate::{
|
||||
@@ -40,12 +44,14 @@ use crate::{
|
||||
live_location_share::{LastLocation, LiveLocationShare},
|
||||
room_info::RoomInfo,
|
||||
room_member::{RoomMember, RoomMemberWithSenderInfo},
|
||||
room_preview::RoomPreview,
|
||||
ruma::{ImageInfo, LocationContent, Mentions, NotifyType},
|
||||
runtime::get_runtime_handle,
|
||||
timeline::{
|
||||
configuration::{TimelineConfiguration, TimelineFilter},
|
||||
ReceiptType, SendHandle, Timeline,
|
||||
EventTimelineItem, ReceiptType, SendHandle, Timeline,
|
||||
},
|
||||
utils::u64_to_uint,
|
||||
utils::{u64_to_uint, AsyncRuntimeDropped},
|
||||
TaskHandle,
|
||||
};
|
||||
|
||||
@@ -70,21 +76,15 @@ impl From<RoomState> for Membership {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type TimelineLock = Arc<RwLock<Option<Arc<Timeline>>>>;
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Room {
|
||||
pub(super) inner: SdkRoom,
|
||||
timeline: TimelineLock,
|
||||
utd_hook_manager: Option<Arc<UtdHookManager>>,
|
||||
}
|
||||
|
||||
impl Room {
|
||||
pub(crate) fn new(inner: SdkRoom) -> Self {
|
||||
Room { inner, timeline: Default::default() }
|
||||
}
|
||||
|
||||
pub(crate) fn with_timeline(inner: SdkRoom, timeline: TimelineLock) -> Self {
|
||||
Room { inner, timeline }
|
||||
pub(crate) fn new(inner: SdkRoom, utd_hook_manager: Option<Arc<UtdHookManager>>) -> Self {
|
||||
Room { inner, utd_hook_manager }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,8 +122,31 @@ impl Room {
|
||||
self.inner.is_space()
|
||||
}
|
||||
|
||||
pub fn is_tombstoned(&self) -> bool {
|
||||
self.inner.is_tombstoned()
|
||||
/// If this room is tombstoned, return the “reference” to the successor room
|
||||
/// —i.e. the room replacing this one.
|
||||
///
|
||||
/// A room is tombstoned if it has received a [`m.room.tombstone`] state
|
||||
/// event.
|
||||
///
|
||||
/// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
|
||||
pub fn successor_room(&self) -> Option<SuccessorRoom> {
|
||||
self.inner.successor_room().map(Into::into)
|
||||
}
|
||||
|
||||
/// If this room is the successor of a tombstoned room, return the
|
||||
/// “reference” to the predecessor room.
|
||||
///
|
||||
/// A room is tombstoned if it has received a [`m.room.tombstone`] state
|
||||
/// event.
|
||||
///
|
||||
/// To determine if a room is the successor of a tombstoned room, the
|
||||
/// [`m.room.create`] must have been received, **with** a `predecessor`
|
||||
/// field.
|
||||
///
|
||||
/// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
|
||||
/// [`m.room.create`]: https://spec.matrix.org/v1.14/client-server-api/#mroomcreate
|
||||
pub fn predecessor_room(&self) -> Option<PredecessorRoom> {
|
||||
self.inner.predecessor_room().map(Into::into)
|
||||
}
|
||||
|
||||
pub fn canonical_alias(&self) -> Option<String> {
|
||||
@@ -134,6 +157,17 @@ impl Room {
|
||||
self.inner.alt_aliases().iter().map(|a| a.to_string()).collect()
|
||||
}
|
||||
|
||||
/// Get the user who created the invite, if any.
|
||||
pub async fn inviter(&self) -> Result<Option<RoomMember>, ClientError> {
|
||||
let invite_details = self.inner.invite_details().await?;
|
||||
|
||||
match invite_details.inviter {
|
||||
Some(inviter) => Ok(Some(inviter.try_into()?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// The room's current membership state.
|
||||
pub fn membership(&self) -> Membership {
|
||||
self.inner.state().into()
|
||||
}
|
||||
@@ -173,15 +207,10 @@ impl Room {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a timeline with a default configuration, i.e. a live timeline
|
||||
/// with read receipts and read marker tracking.
|
||||
pub async fn timeline(&self) -> Result<Arc<Timeline>, ClientError> {
|
||||
let mut write_guard = self.timeline.write().await;
|
||||
if let Some(timeline) = &*write_guard {
|
||||
Ok(timeline.clone())
|
||||
} else {
|
||||
let timeline = Timeline::new(self.inner.timeline().await?);
|
||||
*write_guard = Some(timeline.clone());
|
||||
Ok(timeline)
|
||||
}
|
||||
Ok(Timeline::new(self.inner.timeline().await?))
|
||||
}
|
||||
|
||||
/// Build a new timeline instance with the given configuration.
|
||||
@@ -189,7 +218,7 @@ impl Room {
|
||||
&self,
|
||||
configuration: TimelineConfiguration,
|
||||
) -> Result<Arc<Timeline>, ClientError> {
|
||||
let mut builder = matrix_sdk_ui::timeline::Timeline::builder(&self.inner);
|
||||
let mut builder = matrix_sdk_ui::timeline::TimelineBuilder::new(&self.inner);
|
||||
|
||||
builder = builder
|
||||
.with_focus(configuration.focus.try_into()?)
|
||||
@@ -233,6 +262,14 @@ impl Room {
|
||||
builder = builder.with_internal_id_prefix(internal_id_prefix);
|
||||
}
|
||||
|
||||
if configuration.report_utds {
|
||||
if let Some(utd_hook_manager) = self.utd_hook_manager.clone() {
|
||||
builder = builder.with_unable_to_decrypt_hook(utd_hook_manager);
|
||||
} else {
|
||||
return Err(ClientError::Generic { msg: "Failed creating timeline because the configuration is set to report UTDs but no hook manager is set".to_owned(), details: None });
|
||||
}
|
||||
}
|
||||
|
||||
let timeline = builder.build().await?;
|
||||
|
||||
Ok(Timeline::new(timeline))
|
||||
@@ -246,6 +283,22 @@ impl Room {
|
||||
self.inner.encryption_state()
|
||||
}
|
||||
|
||||
/// Checks whether the room is encrypted or not.
|
||||
///
|
||||
/// **Note**: this info may not be reliable if you don't set up
|
||||
/// `m.room.encryption` as required state.
|
||||
async fn is_encrypted(&self) -> bool {
|
||||
self.inner
|
||||
.latest_encryption_state()
|
||||
.await
|
||||
.map(|state| state.is_encrypted())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn latest_event(&self) -> Option<EventTimelineItem> {
|
||||
self.inner.latest_event_item().await.map(Into::into)
|
||||
}
|
||||
|
||||
pub async fn latest_encryption_state(&self) -> Result<EncryptionState, ClientError> {
|
||||
Ok(self.inner.latest_encryption_state().await?)
|
||||
}
|
||||
@@ -346,8 +399,11 @@ impl Room {
|
||||
///
|
||||
/// * `content` - The content of the event to send encoded as JSON string.
|
||||
pub async fn send_raw(&self, event_type: String, content: String) -> Result<(), ClientError> {
|
||||
let content_json: serde_json::Value = serde_json::from_str(&content)
|
||||
.map_err(|e| ClientError::Generic { msg: format!("Failed to parse JSON: {e}") })?;
|
||||
let content_json: serde_json::Value =
|
||||
serde_json::from_str(&content).map_err(|e| ClientError::Generic {
|
||||
msg: format!("Failed to parse JSON: {e}"),
|
||||
details: Some(format!("{e:?}")),
|
||||
})?;
|
||||
|
||||
self.inner.send_raw(&event_type, content_json).await?;
|
||||
|
||||
@@ -404,9 +460,7 @@ impl Room {
|
||||
.report_content(
|
||||
EventId::parse(event_id)?,
|
||||
score.map(TryFrom::try_from).transpose().map_err(
|
||||
|error: TryFromReportedContentScoreError| ClientError::Generic {
|
||||
msg: error.to_string(),
|
||||
},
|
||||
|error: TryFromReportedContentScoreError| ClientError::from_err(error),
|
||||
)?,
|
||||
reason,
|
||||
)
|
||||
@@ -655,11 +709,11 @@ impl Room {
|
||||
/// Mark a room as read, by attaching a read receipt on the latest event.
|
||||
///
|
||||
/// Note: this does NOT unset the unread flag; it's the caller's
|
||||
/// responsibility to do so, if needs be.
|
||||
/// responsibility to do so, if need be.
|
||||
pub async fn mark_as_read(&self, receipt_type: ReceiptType) -> Result<(), ClientError> {
|
||||
let timeline = self.timeline().await?;
|
||||
let timeline = TimelineBuilder::new(&self.inner).build().await?;
|
||||
|
||||
timeline.mark_as_read(receipt_type).await?;
|
||||
timeline.mark_as_read(receipt_type.into()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -689,10 +743,7 @@ impl Room {
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
self.inner
|
||||
.update_power_levels(updates)
|
||||
.await
|
||||
.map_err(|e| ClientError::Generic { msg: e.to_string() })?;
|
||||
self.inner.update_power_levels(updates).await.map_err(ClientError::from_err)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -726,9 +777,13 @@ impl Room {
|
||||
/// It will configure the notify type: ring or notify based on:
|
||||
/// - is this a DM room -> ring
|
||||
/// - is this a group with more than one other member -> notify
|
||||
pub async fn send_call_notification_if_needed(&self) -> Result<(), ClientError> {
|
||||
self.inner.send_call_notification_if_needed().await?;
|
||||
Ok(())
|
||||
///
|
||||
/// Returns:
|
||||
/// - `Ok(true)` if the event was successfully sent.
|
||||
/// - `Ok(false)` if we didn't send it because it was unnecessary.
|
||||
/// - `Err(_)` if sending the event failed.
|
||||
pub async fn send_call_notification_if_needed(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.send_call_notification_if_needed().await?)
|
||||
}
|
||||
|
||||
/// Send a call notification event in the current room.
|
||||
@@ -1084,11 +1139,46 @@ impl Room {
|
||||
self.inner.forget().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Builds a `RoomPreview` from a room list item. This is intended for
|
||||
/// invited, knocked or banned rooms.
|
||||
async fn preview_room(&self, via: Vec<String>) -> Result<Arc<RoomPreview>, ClientError> {
|
||||
// Validate parameters first.
|
||||
let server_names: Vec<OwnedServerName> = via
|
||||
.into_iter()
|
||||
.map(|server| ServerName::parse(server).map_err(ClientError::from))
|
||||
.collect::<Result<_, ClientError>>()?;
|
||||
|
||||
// Do the thing.
|
||||
let client = self.inner.client();
|
||||
let (room_or_alias_id, mut server_names) = if let Some(alias) = self.inner.canonical_alias()
|
||||
{
|
||||
let room_or_alias_id: OwnedRoomOrAliasId = alias.into();
|
||||
(room_or_alias_id, Vec::new())
|
||||
} else {
|
||||
let room_or_alias_id: OwnedRoomOrAliasId = self.inner.room_id().to_owned().into();
|
||||
(room_or_alias_id, server_names)
|
||||
};
|
||||
|
||||
// If no server names are provided and the room's membership is invited,
|
||||
// add the server name from the sender's user id as a fallback value
|
||||
if server_names.is_empty() {
|
||||
if let Ok(invite_details) = self.inner.invite_details().await {
|
||||
if let Some(inviter) = invite_details.inviter {
|
||||
server_names.push(inviter.user_id().server_name().to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let room_preview = client.get_room_preview(&room_or_alias_id, server_names).await?;
|
||||
|
||||
Ok(Arc::new(RoomPreview::new(AsyncRuntimeDropped::new(client), room_preview)))
|
||||
}
|
||||
}
|
||||
|
||||
/// A listener for receiving new live location shares in a room.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait LiveLocationShareListener: Sync + Send {
|
||||
pub trait LiveLocationShareListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn call(&self, live_location_shares: Vec<LiveLocationShare>);
|
||||
}
|
||||
|
||||
@@ -1110,7 +1200,7 @@ impl From<matrix_sdk::room::knock_requests::KnockRequest> for KnockRequest {
|
||||
|
||||
/// A listener for receiving new requests to a join a room.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait KnockRequestsListener: Send + Sync {
|
||||
pub trait KnockRequestsListener: SendOutsideWasm + SyncOutsideWasm {
|
||||
fn call(&self, join_requests: Vec<KnockRequest>);
|
||||
}
|
||||
|
||||
@@ -1230,17 +1320,17 @@ impl From<RumaPowerLevels> for RoomPowerLevels {
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomInfoListener: Sync + Send {
|
||||
pub trait RoomInfoListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn call(&self, room_info: RoomInfo);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait TypingNotificationsListener: Sync + Send {
|
||||
pub trait TypingNotificationsListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn call(&self, typing_user_ids: Vec<String>);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait IdentityStatusChangeListener: Sync + Send {
|
||||
pub trait IdentityStatusChangeListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn call(&self, identity_status_change: Vec<IdentityStatusChange>);
|
||||
}
|
||||
|
||||
@@ -1462,3 +1552,48 @@ impl TryFrom<RoomHistoryVisibility> for RumaHistoryVisibility {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// When a room A is tombstoned, it is replaced by a room B. The room A is the
|
||||
/// predecessor of B, and B is the successor of A. This type holds information
|
||||
/// about the successor room. See [`Room::successor_room`].
|
||||
///
|
||||
/// A room is tombstoned if it has received a [`m.room.tombstone`] state event.
|
||||
///
|
||||
/// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct SuccessorRoom {
|
||||
/// The ID of the replacement room.
|
||||
pub room_id: String,
|
||||
|
||||
/// The message explaining why the room has been tombstoned.
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
impl From<SdkSuccessorRoom> for SuccessorRoom {
|
||||
fn from(value: SdkSuccessorRoom) -> Self {
|
||||
Self { room_id: value.room_id.to_string(), reason: value.reason }
|
||||
}
|
||||
}
|
||||
|
||||
/// When a room A is tombstoned, it is replaced by a room B. The room A is the
|
||||
/// predecessor of B, and B is the successor of A. This type holds information
|
||||
/// about the predecessor room. See [`Room::predecessor_room`].
|
||||
///
|
||||
/// To know the predecessor of a room, the [`m.room.create`] state event must
|
||||
/// have been received.
|
||||
///
|
||||
/// [`m.room.create`]: https://spec.matrix.org/v1.14/client-server-api/#mroomcreate
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct PredecessorRoom {
|
||||
/// The ID of the replacement room.
|
||||
pub room_id: String,
|
||||
|
||||
/// The event ID of the last known event in the predecesssor room.
|
||||
pub last_event_id: String,
|
||||
}
|
||||
|
||||
impl From<SdkPredecessorRoom> for PredecessorRoom {
|
||||
fn from(value: SdkPredecessorRoom) -> Self {
|
||||
Self { room_id: value.room_id.to_string(), last_event_id: value.last_event_id.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,14 +15,14 @@
|
||||
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::room_directory_search::RoomDirectorySearch as SdkRoomDirectorySearch;
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::ServerName;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{error::ClientError, task_handle::TaskHandle};
|
||||
use crate::{error::ClientError, runtime::get_runtime_handle, task_handle::TaskHandle};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum PublicRoomJoinRule {
|
||||
@@ -198,6 +198,6 @@ impl From<VectorDiff<matrix_sdk::room_directory_search::RoomDescription>>
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomDirectorySearchEntriesListener: Send + Sync + Debug {
|
||||
pub trait RoomDirectorySearchEntriesListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
fn on_update(&self, room_entries_update: Vec<RoomDirectorySearchEntryUpdate>);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::{
|
||||
client::JoinRule,
|
||||
error::ClientError,
|
||||
notification_settings::RoomNotificationMode,
|
||||
room::{Membership, RoomHero, RoomHistoryVisibility},
|
||||
room::{Membership, RoomHero, RoomHistoryVisibility, SuccessorRoom},
|
||||
room_member::RoomMember,
|
||||
};
|
||||
|
||||
@@ -26,7 +26,8 @@ pub struct RoomInfo {
|
||||
is_direct: bool,
|
||||
is_public: bool,
|
||||
is_space: bool,
|
||||
is_tombstoned: bool,
|
||||
/// If present, it means the room has been archived/upgraded.
|
||||
successor_room: Option<SuccessorRoom>,
|
||||
is_favourite: bool,
|
||||
canonical_alias: Option<String>,
|
||||
alternative_aliases: Vec<String>,
|
||||
@@ -80,7 +81,7 @@ impl RoomInfo {
|
||||
|
||||
let join_rule = room.join_rule().try_into();
|
||||
if let Err(e) = &join_rule {
|
||||
warn!("Failed to parse join rule: {:?}", e);
|
||||
warn!("Failed to parse join rule: {e:?}");
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
@@ -94,7 +95,7 @@ impl RoomInfo {
|
||||
is_direct: room.is_direct().await?,
|
||||
is_public: room.is_public(),
|
||||
is_space: room.is_space(),
|
||||
is_tombstoned: room.is_tombstoned(),
|
||||
successor_room: room.successor_room().map(Into::into),
|
||||
is_favourite: room.is_favourite(),
|
||||
canonical_alias: room.canonical_alias().map(Into::into),
|
||||
alternative_aliases: room.alt_aliases().into_iter().map(Into::into).collect(),
|
||||
|
||||
@@ -2,33 +2,29 @@
|
||||
|
||||
use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Duration};
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{pin_mut, StreamExt, TryFutureExt};
|
||||
use matrix_sdk::ruma::{
|
||||
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
RoomId,
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use matrix_sdk::{
|
||||
ruma::{
|
||||
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
RoomId,
|
||||
},
|
||||
Room as SdkRoom,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::{
|
||||
room_list_service::filters::{
|
||||
new_filter_all, new_filter_any, new_filter_category, new_filter_favourite,
|
||||
new_filter_fuzzy_match_room_name, new_filter_invite, new_filter_joined,
|
||||
new_filter_non_left, new_filter_none, new_filter_normalized_match_room_name,
|
||||
new_filter_unread, BoxedFilterFn, RoomCategory,
|
||||
new_filter_all, new_filter_any, new_filter_category, new_filter_deduplicate_versions,
|
||||
new_filter_favourite, new_filter_fuzzy_match_room_name, new_filter_invite,
|
||||
new_filter_joined, new_filter_non_left, new_filter_none,
|
||||
new_filter_normalized_match_room_name, new_filter_unread, BoxedFilterFn, RoomCategory,
|
||||
},
|
||||
timeline::default_event_filter,
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
use ruma::{OwnedRoomOrAliasId, OwnedServerName, ServerName};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
error::ClientError,
|
||||
room::{Membership, Room},
|
||||
room_info::RoomInfo,
|
||||
room_preview::RoomPreview,
|
||||
timeline::{configuration::TimelineEventTypeFilter, EventTimelineItem, Timeline},
|
||||
utils::AsyncRuntimeDropped,
|
||||
runtime::get_runtime_handle,
|
||||
TaskHandle,
|
||||
};
|
||||
|
||||
@@ -44,12 +40,6 @@ pub enum RoomListError {
|
||||
RoomNotFound { room_name: String },
|
||||
#[error("invalid room ID: {error}")]
|
||||
InvalidRoomId { error: String },
|
||||
#[error("A timeline instance already exists for room {room_name}")]
|
||||
TimelineAlreadyExists { room_name: String },
|
||||
#[error("A timeline instance hasn't been initialized for room {room_name}")]
|
||||
TimelineNotInitialized { room_name: String },
|
||||
#[error("Timeline couldn't be initialized: {error}")]
|
||||
InitializingTimeline { error: String },
|
||||
#[error("Event cache ran into an error: {error}")]
|
||||
EventCache { error: String },
|
||||
#[error("The requested room doesn't match the membership requirements {expected:?}, observed {actual:?}")]
|
||||
@@ -64,12 +54,6 @@ impl From<matrix_sdk_ui::room_list_service::Error> for RoomListError {
|
||||
SlidingSync(error) => Self::SlidingSync { error: error.to_string() },
|
||||
UnknownList(list_name) => Self::UnknownList { list_name },
|
||||
RoomNotFound(room_id) => Self::RoomNotFound { room_name: room_id.to_string() },
|
||||
TimelineAlreadyExists(room_id) => {
|
||||
Self::TimelineAlreadyExists { room_name: room_id.to_string() }
|
||||
}
|
||||
InitializingTimeline(source) => {
|
||||
Self::InitializingTimeline { error: source.to_string() }
|
||||
}
|
||||
EventCache(error) => Self::EventCache { error: error.to_string() },
|
||||
}
|
||||
}
|
||||
@@ -101,13 +85,10 @@ impl RoomListService {
|
||||
})))
|
||||
}
|
||||
|
||||
fn room(&self, room_id: String) -> Result<Arc<RoomListItem>, RoomListError> {
|
||||
fn room(&self, room_id: String) -> Result<Arc<Room>, RoomListError> {
|
||||
let room_id = <&RoomId>::try_from(room_id.as_str()).map_err(RoomListError::from)?;
|
||||
|
||||
Ok(Arc::new(RoomListItem {
|
||||
inner: Arc::new(self.inner.room(room_id)?),
|
||||
utd_hook: self.utd_hook.clone(),
|
||||
}))
|
||||
Ok(Arc::new(Room::new(self.inner.room(room_id)?, self.utd_hook.clone())))
|
||||
}
|
||||
|
||||
async fn all_rooms(self: Arc<Self>) -> Result<Arc<RoomList>, RoomListError> {
|
||||
@@ -182,8 +163,7 @@ impl RoomList {
|
||||
page_size: u32,
|
||||
listener: Box<dyn RoomListEntriesListener>,
|
||||
) -> Arc<RoomListEntriesWithDynamicAdaptersResult> {
|
||||
let this = self.clone();
|
||||
let utd_hook = self.room_list_service.utd_hook.clone();
|
||||
let this = self;
|
||||
|
||||
// The following code deserves a bit of explanation.
|
||||
// `matrix_sdk_ui::room_list_service::RoomList::entries_with_dynamic_adapters`
|
||||
@@ -237,6 +217,7 @@ impl RoomList {
|
||||
let dynamic_entries_controller =
|
||||
Arc::new(RoomListDynamicEntriesController::new(dynamic_entries_controller));
|
||||
|
||||
let utd_hook = this.room_list_service.utd_hook.clone();
|
||||
let entries_stream = Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(entries_stream);
|
||||
|
||||
@@ -244,7 +225,7 @@ impl RoomList {
|
||||
listener.on_update(
|
||||
diffs
|
||||
.into_iter()
|
||||
.map(|diff| RoomListEntriesUpdate::from(diff, utd_hook.clone()))
|
||||
.map(|room| RoomListEntriesUpdate::from(utd_hook.clone(), room))
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
@@ -271,7 +252,7 @@ impl RoomList {
|
||||
Arc::new(unsafe { result.assume_init() })
|
||||
}
|
||||
|
||||
fn room(&self, room_id: String) -> Result<Arc<RoomListItem>, RoomListError> {
|
||||
fn room(&self, room_id: String) -> Result<Arc<Room>, RoomListError> {
|
||||
self.room_list_service.room(room_id)
|
||||
}
|
||||
}
|
||||
@@ -362,63 +343,60 @@ impl From<matrix_sdk_ui::room_list_service::RoomListLoadingState> for RoomListLo
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomListServiceStateListener: Send + Sync + Debug {
|
||||
pub trait RoomListServiceStateListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
fn on_update(&self, state: RoomListServiceState);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomListLoadingStateListener: Send + Sync + Debug {
|
||||
pub trait RoomListLoadingStateListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
fn on_update(&self, state: RoomListLoadingState);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomListServiceSyncIndicatorListener: Send + Sync + Debug {
|
||||
pub trait RoomListServiceSyncIndicatorListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
fn on_update(&self, sync_indicator: RoomListServiceSyncIndicator);
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RoomListEntriesUpdate {
|
||||
Append { values: Vec<Arc<RoomListItem>> },
|
||||
Append { values: Vec<Arc<Room>> },
|
||||
Clear,
|
||||
PushFront { value: Arc<RoomListItem> },
|
||||
PushBack { value: Arc<RoomListItem> },
|
||||
PushFront { value: Arc<Room> },
|
||||
PushBack { value: Arc<Room> },
|
||||
PopFront,
|
||||
PopBack,
|
||||
Insert { index: u32, value: Arc<RoomListItem> },
|
||||
Set { index: u32, value: Arc<RoomListItem> },
|
||||
Insert { index: u32, value: Arc<Room> },
|
||||
Set { index: u32, value: Arc<Room> },
|
||||
Remove { index: u32 },
|
||||
Truncate { length: u32 },
|
||||
Reset { values: Vec<Arc<RoomListItem>> },
|
||||
Reset { values: Vec<Arc<Room>> },
|
||||
}
|
||||
|
||||
impl RoomListEntriesUpdate {
|
||||
fn from(
|
||||
vector_diff: VectorDiff<matrix_sdk_ui::room_list_service::Room>,
|
||||
utd_hook: Option<Arc<UtdHookManager>>,
|
||||
) -> Self {
|
||||
fn from(utd_hook: Option<Arc<UtdHookManager>>, vector_diff: VectorDiff<SdkRoom>) -> Self {
|
||||
match vector_diff {
|
||||
VectorDiff::Append { values } => Self::Append {
|
||||
values: values
|
||||
.into_iter()
|
||||
.map(|value| Arc::new(RoomListItem::from(value, utd_hook.clone())))
|
||||
.map(|value| Arc::new(Room::new(value, utd_hook.clone())))
|
||||
.collect(),
|
||||
},
|
||||
VectorDiff::Clear => Self::Clear,
|
||||
VectorDiff::PushFront { value } => {
|
||||
Self::PushFront { value: Arc::new(RoomListItem::from(value, utd_hook)) }
|
||||
Self::PushFront { value: Arc::new(Room::new(value, utd_hook)) }
|
||||
}
|
||||
VectorDiff::PushBack { value } => {
|
||||
Self::PushBack { value: Arc::new(RoomListItem::from(value, utd_hook)) }
|
||||
Self::PushBack { value: Arc::new(Room::new(value, utd_hook)) }
|
||||
}
|
||||
VectorDiff::PopFront => Self::PopFront,
|
||||
VectorDiff::PopBack => Self::PopBack,
|
||||
VectorDiff::Insert { index, value } => Self::Insert {
|
||||
index: u32::try_from(index).unwrap(),
|
||||
value: Arc::new(RoomListItem::from(value, utd_hook)),
|
||||
value: Arc::new(Room::new(value, utd_hook)),
|
||||
},
|
||||
VectorDiff::Set { index, value } => Self::Set {
|
||||
index: u32::try_from(index).unwrap(),
|
||||
value: Arc::new(RoomListItem::from(value, utd_hook)),
|
||||
value: Arc::new(Room::new(value, utd_hook)),
|
||||
},
|
||||
VectorDiff::Remove { index } => Self::Remove { index: u32::try_from(index).unwrap() },
|
||||
VectorDiff::Truncate { length } => {
|
||||
@@ -427,7 +405,7 @@ impl RoomListEntriesUpdate {
|
||||
VectorDiff::Reset { values } => Self::Reset {
|
||||
values: values
|
||||
.into_iter()
|
||||
.map(|value| Arc::new(RoomListItem::from(value, utd_hook.clone())))
|
||||
.map(|value| Arc::new(Room::new(value, utd_hook.clone())))
|
||||
.collect(),
|
||||
},
|
||||
}
|
||||
@@ -435,7 +413,7 @@ impl RoomListEntriesUpdate {
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomListEntriesListener: Send + Sync + Debug {
|
||||
pub trait RoomListEntriesListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
fn on_update(&self, room_entries_update: Vec<RoomListEntriesUpdate>);
|
||||
}
|
||||
|
||||
@@ -480,6 +458,7 @@ pub enum RoomListEntriesDynamicFilterKind {
|
||||
None,
|
||||
NormalizedMatchRoomName { pattern: String },
|
||||
FuzzyMatchRoomName { pattern: String },
|
||||
DeduplicateVersions,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
@@ -521,177 +500,11 @@ impl From<RoomListEntriesDynamicFilterKind> for BoxedFilterFn {
|
||||
Kind::FuzzyMatchRoomName { pattern } => {
|
||||
Box::new(new_filter_fuzzy_match_room_name(&pattern))
|
||||
}
|
||||
Kind::DeduplicateVersions => Box::new(new_filter_deduplicate_versions()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomListItem {
|
||||
inner: Arc<matrix_sdk_ui::room_list_service::Room>,
|
||||
utd_hook: Option<Arc<UtdHookManager>>,
|
||||
}
|
||||
|
||||
impl RoomListItem {
|
||||
fn from(
|
||||
value: matrix_sdk_ui::room_list_service::Room,
|
||||
utd_hook: Option<Arc<UtdHookManager>>,
|
||||
) -> Self {
|
||||
Self { inner: Arc::new(value), utd_hook }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomListItem {
|
||||
fn id(&self) -> String {
|
||||
self.inner.id().to_string()
|
||||
}
|
||||
|
||||
/// Returns the room's name from the state event if available, otherwise
|
||||
/// compute a room name based on the room's nature (DM or not) and number of
|
||||
/// members.
|
||||
fn display_name(&self) -> Option<String> {
|
||||
self.inner.cached_display_name()
|
||||
}
|
||||
|
||||
fn avatar_url(&self) -> Option<String> {
|
||||
self.inner.avatar_url().map(|uri| uri.to_string())
|
||||
}
|
||||
|
||||
async fn is_direct(&self) -> bool {
|
||||
self.inner.inner_room().is_direct().await.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn canonical_alias(&self) -> Option<String> {
|
||||
self.inner.inner_room().canonical_alias().map(|alias| alias.to_string())
|
||||
}
|
||||
|
||||
async fn room_info(&self) -> Result<RoomInfo, ClientError> {
|
||||
RoomInfo::new(self.inner.inner_room()).await
|
||||
}
|
||||
|
||||
/// The room's current membership state.
|
||||
fn membership(&self) -> Membership {
|
||||
self.inner.inner_room().state().into()
|
||||
}
|
||||
|
||||
/// Builds a `RoomPreview` from a room list item. This is intended for
|
||||
/// invited, knocked or banned rooms.
|
||||
async fn preview_room(&self, via: Vec<String>) -> Result<Arc<RoomPreview>, ClientError> {
|
||||
// Validate parameters first.
|
||||
let server_names: Vec<OwnedServerName> = via
|
||||
.into_iter()
|
||||
.map(|server| ServerName::parse(server).map_err(ClientError::from))
|
||||
.collect::<Result<_, ClientError>>()?;
|
||||
|
||||
// Do the thing.
|
||||
let client = self.inner.client();
|
||||
let (room_or_alias_id, mut server_names) = if let Some(alias) = self.inner.canonical_alias()
|
||||
{
|
||||
let room_or_alias_id: OwnedRoomOrAliasId = alias.into();
|
||||
(room_or_alias_id, Vec::new())
|
||||
} else {
|
||||
let room_or_alias_id: OwnedRoomOrAliasId = self.inner.id().to_owned().into();
|
||||
(room_or_alias_id, server_names)
|
||||
};
|
||||
|
||||
// If no server names are provided and the room's membership is invited,
|
||||
// add the server name from the sender's user id as a fallback value
|
||||
if server_names.is_empty() {
|
||||
if let Ok(invite_details) = self.inner.invite_details().await {
|
||||
if let Some(inviter) = invite_details.inviter {
|
||||
server_names.push(inviter.user_id().server_name().to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let room_preview = client.get_room_preview(&room_or_alias_id, server_names).await?;
|
||||
|
||||
Ok(Arc::new(RoomPreview::new(AsyncRuntimeDropped::new(client), room_preview)))
|
||||
}
|
||||
|
||||
/// Build a full `Room` FFI object, filling its associated timeline.
|
||||
///
|
||||
/// An error will be returned if the room is a state different than joined
|
||||
/// or if its internal timeline hasn't been initialized.
|
||||
fn full_room(&self) -> Result<Arc<Room>, RoomListError> {
|
||||
if !matches!(self.membership(), Membership::Joined) {
|
||||
return Err(RoomListError::IncorrectRoomMembership {
|
||||
expected: vec![Membership::Joined],
|
||||
actual: self.membership(),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(timeline) = self.inner.timeline() {
|
||||
Ok(Arc::new(Room::with_timeline(
|
||||
self.inner.inner_room().clone(),
|
||||
Arc::new(RwLock::new(Some(Timeline::from_arc(timeline)))),
|
||||
)))
|
||||
} else {
|
||||
Err(RoomListError::TimelineNotInitialized {
|
||||
room_name: self.inner.inner_room().room_id().to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks whether the Room's timeline has been initialized before.
|
||||
fn is_timeline_initialized(&self) -> bool {
|
||||
self.inner.is_timeline_initialized()
|
||||
}
|
||||
|
||||
/// Initializes the timeline for this room using the provided parameters.
|
||||
///
|
||||
/// * `event_type_filter` - An optional [`TimelineEventTypeFilter`] to be
|
||||
/// used to filter timeline events besides the default timeline filter. If
|
||||
/// `None` is passed, only the default timeline filter will be used.
|
||||
/// * `internal_id_prefix` - An optional String that will be prepended to
|
||||
/// all the timeline item's internal IDs, making it possible to
|
||||
/// distinguish different timeline instances from each other.
|
||||
async fn init_timeline(
|
||||
&self,
|
||||
event_type_filter: Option<Arc<TimelineEventTypeFilter>>,
|
||||
internal_id_prefix: Option<String>,
|
||||
) -> Result<(), RoomListError> {
|
||||
let mut timeline_builder = self
|
||||
.inner
|
||||
.default_room_timeline_builder()
|
||||
.await
|
||||
.map_err(|err| RoomListError::InitializingTimeline { error: err.to_string() })?;
|
||||
|
||||
if let Some(event_type_filter) = event_type_filter {
|
||||
timeline_builder = timeline_builder.event_filter(move |event, room_version_id| {
|
||||
// Always perform the default filter first
|
||||
default_event_filter(event, room_version_id) && event_type_filter.filter(event)
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(internal_id_prefix) = internal_id_prefix {
|
||||
timeline_builder = timeline_builder.with_internal_id_prefix(internal_id_prefix);
|
||||
}
|
||||
|
||||
if let Some(utd_hook) = self.utd_hook.clone() {
|
||||
timeline_builder = timeline_builder.with_unable_to_decrypt_hook(utd_hook);
|
||||
}
|
||||
|
||||
self.inner.init_timeline_with_builder(timeline_builder).map_err(RoomListError::from).await
|
||||
}
|
||||
|
||||
/// Checks whether the room is encrypted or not.
|
||||
///
|
||||
/// **Note**: this info may not be reliable if you don't set up
|
||||
/// `m.room.encryption` as required state.
|
||||
async fn is_encrypted(&self) -> bool {
|
||||
self.inner
|
||||
.latest_encryption_state()
|
||||
.await
|
||||
.map(|state| state.is_encrypted())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn latest_event(&self) -> Option<EventTimelineItem> {
|
||||
self.inner.latest_event().await.map(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct UnreadNotificationsCount {
|
||||
highlight_count: u32,
|
||||
|
||||
@@ -59,15 +59,7 @@ impl RoomPreview {
|
||||
let room =
|
||||
self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?;
|
||||
|
||||
let should_forget = matches!(room.state(), matrix_sdk::RoomState::Invited);
|
||||
|
||||
room.leave().await.map_err(ClientError::from)?;
|
||||
|
||||
if should_forget {
|
||||
_ = self.forget().await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(room.leave().await?)
|
||||
}
|
||||
|
||||
/// Get the user who created the invite, if any.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,110 @@
|
||||
// Copyright 2025 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.
|
||||
|
||||
//! Runtime abstractions for cross-platform async execution. This provides
|
||||
//! a stand-in for tokio's `get_runtime_handle` method that will work on
|
||||
//! both Wasm and non-Wasm platforms. It also provides corresponding types
|
||||
//! that can be used in place of tokio's `Handle` and `Runtime` types.
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
mod sys {
|
||||
pub use tokio::runtime::Handle;
|
||||
|
||||
/// Get a runtime handle appropriate for the current target platform.
|
||||
///
|
||||
/// This function returns a unified `Handle` type that works across both
|
||||
/// Wasm and non-Wasm platforms, allowing code to be written that is
|
||||
/// agnostic to the platform-specific runtime implementation.
|
||||
///
|
||||
/// Returns:
|
||||
/// - A `tokio::runtime::Handle` on non-Wasm platforms
|
||||
/// - A `WasmRuntimeHandle` on Wasm platforms
|
||||
pub fn get_runtime_handle() -> Handle {
|
||||
async_compat::get_runtime_handle()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
mod sys {
|
||||
use std::future::Future;
|
||||
|
||||
use crate::executor::{spawn, JoinHandle};
|
||||
|
||||
/// A dummy guard that does nothing when dropped.
|
||||
/// This is used for the Wasm implementation to match
|
||||
/// tokio::runtime::EnterGuard.
|
||||
#[derive(Debug)]
|
||||
pub struct RuntimeGuard;
|
||||
|
||||
/// A runtime handle implementation for WebAssembly targets.
|
||||
///
|
||||
/// This implements a minimal subset of the tokio::runtime::Handle API
|
||||
/// that is needed for the matrix-rust-sdk to function on Wasm.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Handle;
|
||||
pub type Runtime = Handle;
|
||||
|
||||
impl Handle {
|
||||
/// Spawns a future in the wasm32 bindgen runtime.
|
||||
#[track_caller]
|
||||
pub fn spawn<F>(&self, future: F) -> JoinHandle<F::Output>
|
||||
where
|
||||
F: Future + 'static,
|
||||
F::Output: 'static,
|
||||
{
|
||||
spawn(future)
|
||||
}
|
||||
|
||||
/// Runs the provided function on an executor dedicated to blocking
|
||||
/// operations.
|
||||
#[track_caller]
|
||||
pub fn spawn_blocking<F, R>(&self, func: F) -> JoinHandle<R>
|
||||
where
|
||||
F: FnOnce() -> R + 'static,
|
||||
R: 'static,
|
||||
{
|
||||
spawn(async move { func() })
|
||||
}
|
||||
|
||||
/// Runs a future to completion on the current thread.
|
||||
pub fn block_on<F, T>(&self, future: F) -> T
|
||||
where
|
||||
F: Future<Output = T>,
|
||||
{
|
||||
futures_executor::block_on(future)
|
||||
}
|
||||
|
||||
/// Enters the runtime context.
|
||||
///
|
||||
/// For WebAssembly, this is a no-op that returns a dummy guard.
|
||||
pub fn enter(&self) -> RuntimeGuard {
|
||||
RuntimeGuard
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a runtime handle appropriate for the current target platform.
|
||||
///
|
||||
/// This function returns a unified `Handle` type that works across both
|
||||
/// Wasm and non-Wasm platforms, allowing code to be written that is
|
||||
/// agnostic to the platform-specific runtime implementation.
|
||||
///
|
||||
/// Returns:
|
||||
/// - A `tokio::runtime::Handle` on non-Wasm platforms
|
||||
/// - A `WasmRuntimeHandle` on Wasm platforms
|
||||
pub fn get_runtime_handle() -> Handle {
|
||||
Handle
|
||||
}
|
||||
}
|
||||
|
||||
pub use sys::*;
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
encryption::{
|
||||
@@ -11,10 +10,13 @@ use matrix_sdk::{
|
||||
ruma::events::key::verification::VerificationMethod,
|
||||
Account,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::UserId;
|
||||
use tracing::{error, warn};
|
||||
|
||||
use crate::{client::UserProfile, error::ClientError, utils::Timestamp};
|
||||
use crate::{
|
||||
client::UserProfile, error::ClientError, runtime::get_runtime_handle, utils::Timestamp,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SessionVerificationEmoji {
|
||||
@@ -51,7 +53,7 @@ pub struct SessionVerificationRequestDetails {
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SessionVerificationControllerDelegate: Sync + Send {
|
||||
pub trait SessionVerificationControllerDelegate: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn did_receive_verification_request(&self, details: SessionVerificationRequestDetails);
|
||||
fn did_accept_verification_request(&self);
|
||||
fn did_start_sas_verification(&self);
|
||||
@@ -94,7 +96,7 @@ impl SessionVerificationController {
|
||||
.encryption
|
||||
.get_verification_request(&sender_id, flow_id)
|
||||
.await
|
||||
.ok_or(ClientError::new("Unknown session verification request"))?;
|
||||
.ok_or(ClientError::from_str("Unknown session verification request", None))?;
|
||||
|
||||
self.set_ongoing_verification_request(verification_request)
|
||||
}
|
||||
@@ -131,10 +133,10 @@ impl SessionVerificationController {
|
||||
.encryption
|
||||
.get_user_identity(&user_id)
|
||||
.await?
|
||||
.ok_or(ClientError::new("Unknown user identity"))?;
|
||||
.ok_or(ClientError::from_str("Unknown user identity", None))?;
|
||||
|
||||
if user_identity.is_verified() {
|
||||
return Err(ClientError::new("User is already verified"));
|
||||
return Err(ClientError::from_str("User is already verified", None));
|
||||
}
|
||||
|
||||
let methods = vec![VerificationMethod::SasV1];
|
||||
@@ -153,7 +155,7 @@ impl SessionVerificationController {
|
||||
let verification_request = self.verification_request.read().unwrap().clone();
|
||||
|
||||
let Some(verification_request) = verification_request else {
|
||||
return Err(ClientError::new("Verification request missing."));
|
||||
return Err(ClientError::from_str("Verification request missing.", None));
|
||||
};
|
||||
|
||||
match verification_request.start_sas().await {
|
||||
@@ -183,7 +185,7 @@ impl SessionVerificationController {
|
||||
let sas_verification = self.sas_verification.read().unwrap().clone();
|
||||
|
||||
let Some(sas_verification) = sas_verification else {
|
||||
return Err(ClientError::new("SAS verification missing"));
|
||||
return Err(ClientError::from_str("SAS verification missing", None));
|
||||
};
|
||||
|
||||
Ok(sas_verification.confirm().await?)
|
||||
@@ -194,7 +196,7 @@ impl SessionVerificationController {
|
||||
let sas_verification = self.sas_verification.read().unwrap().clone();
|
||||
|
||||
let Some(sas_verification) = sas_verification else {
|
||||
return Err(ClientError::new("SAS verification missing"));
|
||||
return Err(ClientError::from_str("SAS verification missing", None));
|
||||
};
|
||||
|
||||
Ok(sas_verification.mismatch().await?)
|
||||
@@ -205,7 +207,7 @@ impl SessionVerificationController {
|
||||
let verification_request = self.verification_request.read().unwrap().clone();
|
||||
|
||||
let Some(verification_request) = verification_request else {
|
||||
return Err(ClientError::new("Verification request missing."));
|
||||
return Err(ClientError::from_str("Verification request missing.", None));
|
||||
};
|
||||
|
||||
Ok(verification_request.cancel().await?)
|
||||
@@ -239,7 +241,7 @@ impl SessionVerificationController {
|
||||
if sender != self.user_identity.user_id() {
|
||||
if let Some(status) = self.encryption.cross_signing_status().await {
|
||||
if !status.is_complete() {
|
||||
warn!("Cannot verify other users until our own device's cross-signing status is complete: {:?}", status);
|
||||
warn!("Cannot verify other users until our own device's cross-signing status is complete: {status:?}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -285,7 +287,10 @@ impl SessionVerificationController {
|
||||
if !ongoing_verification_request.is_done()
|
||||
&& !ongoing_verification_request.is_cancelled()
|
||||
{
|
||||
return Err(ClientError::new("There is another verification flow ongoing."));
|
||||
return Err(ClientError::from_str(
|
||||
"There is another verification flow ongoing.",
|
||||
None,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,24 +12,22 @@
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{fmt::Debug, sync::Arc, time::Duration};
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use futures_util::pin_mut;
|
||||
use matrix_sdk::{crypto::types::events::UtdCause, Client};
|
||||
use matrix_sdk::Client;
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::{
|
||||
sync_service::{
|
||||
State as MatrixSyncServiceState, SyncService as MatrixSyncService,
|
||||
SyncServiceBuilder as MatrixSyncServiceBuilder,
|
||||
},
|
||||
unable_to_decrypt_hook::{
|
||||
UnableToDecryptHook, UnableToDecryptInfo as SdkUnableToDecryptInfo, UtdHookManager,
|
||||
},
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
error::ClientError, helpers::unwrap_or_clone_arc, room_list::RoomListService, TaskHandle,
|
||||
error::ClientError, helpers::unwrap_or_clone_arc, room_list::RoomListService,
|
||||
runtime::get_runtime_handle, TaskHandle,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
@@ -54,7 +52,7 @@ impl From<MatrixSyncServiceState> for SyncServiceState {
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SyncServiceStateObserver: Send + Sync + Debug {
|
||||
pub trait SyncServiceStateObserver: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
fn on_update(&self, state: SyncServiceState);
|
||||
}
|
||||
|
||||
@@ -96,19 +94,13 @@ impl SyncService {
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct SyncServiceBuilder {
|
||||
client: Client,
|
||||
builder: MatrixSyncServiceBuilder,
|
||||
|
||||
utd_hook: Option<Arc<UtdHookManager>>,
|
||||
}
|
||||
|
||||
impl SyncServiceBuilder {
|
||||
pub(crate) fn new(client: Client) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
client: client.clone(),
|
||||
builder: MatrixSyncService::builder(client),
|
||||
utd_hook: None,
|
||||
})
|
||||
pub(crate) fn new(client: Client, utd_hook: Option<Arc<UtdHookManager>>) -> Arc<Self> {
|
||||
Arc::new(Self { builder: MatrixSyncService::builder(client), utd_hook })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,40 +109,14 @@ impl SyncServiceBuilder {
|
||||
pub fn with_cross_process_lock(self: Arc<Self>) -> Arc<Self> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
let builder = this.builder.with_cross_process_lock();
|
||||
Arc::new(Self { client: this.client, builder, utd_hook: this.utd_hook })
|
||||
Arc::new(Self { builder, ..this })
|
||||
}
|
||||
|
||||
/// Enable the "offline" mode for the [`SyncService`].
|
||||
pub fn with_offline_mode(self: Arc<Self>) -> Arc<Self> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
let builder = this.builder.with_offline_mode();
|
||||
Arc::new(Self { client: this.client, builder, utd_hook: this.utd_hook })
|
||||
}
|
||||
|
||||
pub async fn with_utd_hook(
|
||||
self: Arc<Self>,
|
||||
delegate: Box<dyn UnableToDecryptDelegate>,
|
||||
) -> Arc<Self> {
|
||||
// UTDs detected before this duration may be reclassified as "late decryption"
|
||||
// events (or discarded, if they get decrypted fast enough).
|
||||
const UTD_HOOK_GRACE_PERIOD: Duration = Duration::from_secs(60);
|
||||
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
|
||||
let mut utd_hook = UtdHookManager::new(Arc::new(UtdHook { delegate }), this.client.clone())
|
||||
.with_max_delay(UTD_HOOK_GRACE_PERIOD);
|
||||
|
||||
if let Err(e) = utd_hook.reload_from_store().await {
|
||||
error!("Unable to reload UTD hook data from data store: {}", e);
|
||||
// Carry on with the setup anyway; we shouldn't fail setup just
|
||||
// because the UTD hook failed to load its data.
|
||||
}
|
||||
|
||||
Arc::new(Self {
|
||||
client: this.client,
|
||||
builder: this.builder,
|
||||
utd_hook: Some(Arc::new(utd_hook)),
|
||||
})
|
||||
Arc::new(Self { builder, ..this })
|
||||
}
|
||||
|
||||
pub async fn finish(self: Arc<Self>) -> Result<Arc<SyncService>, ClientError> {
|
||||
@@ -161,83 +127,3 @@ impl SyncServiceBuilder {
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait UnableToDecryptDelegate: Sync + Send {
|
||||
fn on_utd(&self, info: UnableToDecryptInfo);
|
||||
}
|
||||
|
||||
struct UtdHook {
|
||||
delegate: Box<dyn UnableToDecryptDelegate>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for UtdHook {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("UtdHook").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl UnableToDecryptHook for UtdHook {
|
||||
fn on_utd(&self, info: SdkUnableToDecryptInfo) {
|
||||
const IGNORE_UTD_PERIOD: Duration = Duration::from_secs(4);
|
||||
|
||||
// UTDs that have been decrypted in the `IGNORE_UTD_PERIOD` are just ignored and
|
||||
// not considered UTDs.
|
||||
if let Some(duration) = &info.time_to_decrypt {
|
||||
if *duration < IGNORE_UTD_PERIOD {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Report the UTD to the client.
|
||||
self.delegate.on_utd(info.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct UnableToDecryptInfo {
|
||||
/// The identifier of the event that couldn't get decrypted.
|
||||
event_id: String,
|
||||
|
||||
/// If the event could be decrypted late (that is, the event was encrypted
|
||||
/// at first, but could be decrypted later on), then this indicates the
|
||||
/// time it took to decrypt the event. If it is not set, this is
|
||||
/// considered a definite UTD.
|
||||
///
|
||||
/// If set, this is in milliseconds.
|
||||
pub time_to_decrypt_ms: Option<u64>,
|
||||
|
||||
/// What we know about what caused this UTD. E.g. was this event sent when
|
||||
/// we were not a member of this room?
|
||||
pub cause: UtdCause,
|
||||
|
||||
/// The difference between the event creation time (`origin_server_ts`) and
|
||||
/// the time our device was created. If negative, this event was sent
|
||||
/// *before* our device was created.
|
||||
pub event_local_age_millis: i64,
|
||||
|
||||
/// Whether the user had verified their own identity at the point they
|
||||
/// received the UTD event.
|
||||
pub user_trusts_own_identity: bool,
|
||||
|
||||
/// The homeserver of the user that sent the undecryptable event.
|
||||
pub sender_homeserver: String,
|
||||
|
||||
/// Our local user's own homeserver, or `None` if the client is not logged
|
||||
/// in.
|
||||
pub own_homeserver: Option<String>,
|
||||
}
|
||||
|
||||
impl From<SdkUnableToDecryptInfo> for UnableToDecryptInfo {
|
||||
fn from(value: SdkUnableToDecryptInfo) -> Self {
|
||||
Self {
|
||||
event_id: value.event_id.to_string(),
|
||||
time_to_decrypt_ms: value.time_to_decrypt.map(|ttd| ttd.as_millis() as u64),
|
||||
cause: value.cause,
|
||||
event_local_age_millis: value.event_local_age_millis,
|
||||
user_trusts_own_identity: value.user_trusts_own_identity,
|
||||
sender_homeserver: value.sender_homeserver.to_string(),
|
||||
own_homeserver: value.own_homeserver.map(String::from),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,9 +63,28 @@ impl From<FilterTimelineEventType> for TimelineEventType {
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum TimelineFocus {
|
||||
Live,
|
||||
Event { event_id: String, num_context_events: u16 },
|
||||
PinnedEvents { max_events_to_load: u16, max_concurrent_requests: u16 },
|
||||
Live {
|
||||
/// Whether to hide in-thread replies from the live timeline.
|
||||
hide_threaded_events: bool,
|
||||
},
|
||||
Event {
|
||||
/// The initial event to focus on. This is usually the target of a
|
||||
/// permalink.
|
||||
event_id: String,
|
||||
/// The number of context events to load around the focused event.
|
||||
num_context_events: u16,
|
||||
/// Whether to hide in-thread replies from the live timeline.
|
||||
hide_threaded_events: bool,
|
||||
},
|
||||
Thread {
|
||||
/// The thread root event ID to focus on.
|
||||
root_event_id: String,
|
||||
num_events: u16,
|
||||
},
|
||||
PinnedEvents {
|
||||
max_events_to_load: u16,
|
||||
max_concurrent_requests: u16,
|
||||
},
|
||||
}
|
||||
|
||||
impl TryFrom<TimelineFocus> for matrix_sdk_ui::timeline::TimelineFocus {
|
||||
@@ -75,15 +94,29 @@ impl TryFrom<TimelineFocus> for matrix_sdk_ui::timeline::TimelineFocus {
|
||||
value: TimelineFocus,
|
||||
) -> Result<matrix_sdk_ui::timeline::TimelineFocus, Self::Error> {
|
||||
match value {
|
||||
TimelineFocus::Live => Ok(Self::Live),
|
||||
TimelineFocus::Event { event_id, num_context_events } => {
|
||||
TimelineFocus::Live { hide_threaded_events } => Ok(Self::Live { hide_threaded_events }),
|
||||
TimelineFocus::Event { event_id, num_context_events, hide_threaded_events } => {
|
||||
let parsed_event_id =
|
||||
EventId::parse(&event_id).map_err(|err| FocusEventError::InvalidEventId {
|
||||
event_id: event_id.clone(),
|
||||
err: err.to_string(),
|
||||
})?;
|
||||
|
||||
Ok(Self::Event { target: parsed_event_id, num_context_events })
|
||||
Ok(Self::Event {
|
||||
target: parsed_event_id,
|
||||
num_context_events,
|
||||
hide_threaded_events,
|
||||
})
|
||||
}
|
||||
TimelineFocus::Thread { root_event_id, num_events } => {
|
||||
let parsed_root_event_id = EventId::parse(&root_event_id).map_err(|err| {
|
||||
FocusEventError::InvalidEventId {
|
||||
event_id: root_event_id.clone(),
|
||||
err: err.to_string(),
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(Self::Thread { root_event_id: parsed_root_event_id, num_events })
|
||||
}
|
||||
TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests } => {
|
||||
Ok(Self::PinnedEvents { max_events_to_load, max_concurrent_requests })
|
||||
@@ -146,4 +179,8 @@ pub struct TimelineConfiguration {
|
||||
/// As this has a non negligible performance impact, make sure to enable it
|
||||
/// only when you need it.
|
||||
pub track_read_receipts: bool,
|
||||
|
||||
/// Whether this timeline instance should report UTDs through the client's
|
||||
/// delegate.
|
||||
pub report_utds: bool,
|
||||
}
|
||||
|
||||
@@ -96,6 +96,14 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
// A note about this `allow(clippy::large_enum_variant)`.
|
||||
// In order to reduce the size of `TimelineItemContent`, we would need to
|
||||
// put some parts in a `Box`, or an `Arc`. Sadly, it doesn't play well with
|
||||
// UniFFI. We would need to change the `uniffi::Record` of the subtypes into
|
||||
// `uniffi::Object`, which is a radical change. It would simplify the memory
|
||||
// usage, but it would slow down the performance around the FFI border. Thus,
|
||||
// let's consider this is a false-positive lint in this particular case.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum TimelineItemContent {
|
||||
MsgLike {
|
||||
content: MsgLikeContent,
|
||||
|
||||
@@ -16,7 +16,6 @@ use std::{collections::HashMap, fmt::Write as _, fs, panic, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use as_variant::as_variant;
|
||||
use async_compat::get_runtime_handle;
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{pin_mut, StreamExt as _};
|
||||
use matrix_sdk::{
|
||||
@@ -32,11 +31,11 @@ use matrix_sdk::{
|
||||
},
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{
|
||||
self, EventItemOrigin, Profile, RepliedToEvent, TimelineDetails,
|
||||
self, AttachmentSource, EventItemOrigin, Profile, TimelineDetails,
|
||||
TimelineUniqueId as SdkTimelineUniqueId,
|
||||
};
|
||||
use mime::Mime;
|
||||
use reply::{InReplyToDetails, RepliedToEventDetails};
|
||||
use reply::{EmbeddedEventDetails, InReplyToDetails};
|
||||
use ruma::{
|
||||
events::{
|
||||
location::{AssetType as RumaAssetType, LocationContent, ZoomLevel},
|
||||
@@ -75,6 +74,7 @@ use crate::{
|
||||
AssetType, AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, PollKind,
|
||||
ThumbnailInfo, VideoInfo,
|
||||
},
|
||||
runtime::get_runtime_handle,
|
||||
task_handle::TaskHandle,
|
||||
utils::Timestamp,
|
||||
};
|
||||
@@ -85,6 +85,7 @@ mod msg_like;
|
||||
mod reply;
|
||||
|
||||
use matrix_sdk::utils::formatted_body_from;
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
|
||||
use crate::error::QueueWedgeError;
|
||||
|
||||
@@ -99,11 +100,6 @@ impl Timeline {
|
||||
Arc::new(Self { inner })
|
||||
}
|
||||
|
||||
pub(crate) fn from_arc(inner: Arc<matrix_sdk_ui::timeline::Timeline>) -> Arc<Self> {
|
||||
// SAFETY: repr(transparent) means transmuting the arc this way is allowed
|
||||
unsafe { Arc::from_raw(Arc::into_raw(inner) as _) }
|
||||
}
|
||||
|
||||
fn send_attachment(
|
||||
self: Arc<Self>,
|
||||
params: UploadParameters,
|
||||
@@ -131,7 +127,7 @@ impl Timeline {
|
||||
|
||||
let handle = SendAttachmentJoinHandle::new(get_runtime_handle().spawn(async move {
|
||||
let mut request =
|
||||
self.inner.send_attachment(params.filename, mime_type, attachment_config);
|
||||
self.inner.send_attachment(params.source, mime_type, attachment_config);
|
||||
|
||||
if params.use_send_queue {
|
||||
request = request.use_send_queue();
|
||||
@@ -201,8 +197,8 @@ fn build_thumbnail_info(
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct UploadParameters {
|
||||
/// Filename (previously called "url") for the media to be sent.
|
||||
filename: String,
|
||||
/// Source from which to upload data
|
||||
source: UploadSource,
|
||||
/// Optional non-formatted caption, for clients that support it.
|
||||
caption: Option<String>,
|
||||
/// Optional HTML-formatted caption, for clients that support it.
|
||||
@@ -217,6 +213,32 @@ pub struct UploadParameters {
|
||||
use_send_queue: bool,
|
||||
}
|
||||
|
||||
/// A source for uploading a file
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum UploadSource {
|
||||
/// Upload source is a file on disk
|
||||
File {
|
||||
/// Path to file
|
||||
filename: String,
|
||||
},
|
||||
/// Upload source is data in memory
|
||||
Data {
|
||||
/// Bytes being uploaded
|
||||
bytes: Vec<u8>,
|
||||
/// Filename to associate with bytes
|
||||
filename: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<UploadSource> for AttachmentSource {
|
||||
fn from(value: UploadSource) -> Self {
|
||||
match value {
|
||||
UploadSource::File { filename } => Self::File(filename.into()),
|
||||
UploadSource::Data { bytes, filename } => Self::Data { bytes, filename },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct ReplyParameters {
|
||||
/// The ID of the event to reply to.
|
||||
@@ -661,25 +683,28 @@ impl Timeline {
|
||||
let event_id = EventId::parse(&event_id_str)?;
|
||||
|
||||
let replied_to = match self.inner.room().load_or_fetch_event(&event_id, None).await {
|
||||
Ok(event) => RepliedToEvent::try_from_timeline_event_for_room(event, self.inner.room())
|
||||
.await
|
||||
.map_err(ClientError::from),
|
||||
Ok(event) => self.inner.make_replied_to(event).await.map_err(ClientError::from),
|
||||
Err(e) => Err(ClientError::from(e)),
|
||||
};
|
||||
|
||||
match replied_to {
|
||||
Ok(replied_to) => Ok(Arc::new(InReplyToDetails::new(
|
||||
Ok(Some(replied_to)) => Ok(Arc::new(InReplyToDetails::new(
|
||||
event_id_str,
|
||||
RepliedToEventDetails::Ready {
|
||||
content: replied_to.content().clone().into(),
|
||||
sender: replied_to.sender().to_string(),
|
||||
sender_profile: replied_to.sender_profile().into(),
|
||||
EmbeddedEventDetails::Ready {
|
||||
content: replied_to.content.clone().into(),
|
||||
sender: replied_to.sender.to_string(),
|
||||
sender_profile: replied_to.sender_profile.into(),
|
||||
},
|
||||
))),
|
||||
|
||||
Ok(None) => Ok(Arc::new(InReplyToDetails::new(
|
||||
event_id_str,
|
||||
EmbeddedEventDetails::Error { message: "unsupported event".to_owned() },
|
||||
))),
|
||||
|
||||
Err(e) => Ok(Arc::new(InReplyToDetails::new(
|
||||
event_id_str,
|
||||
RepliedToEventDetails::Error { message: e.to_string() },
|
||||
EmbeddedEventDetails::Error { message: e.to_string() },
|
||||
))),
|
||||
}
|
||||
}
|
||||
@@ -783,12 +808,12 @@ pub enum FocusEventError {
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait TimelineListener: Sync + Send {
|
||||
pub trait TimelineListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, diff: Vec<Arc<TimelineDiff>>);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait PaginationStatusListener: Sync + Send {
|
||||
pub trait PaginationStatusListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, status: RoomPaginationStatus);
|
||||
}
|
||||
|
||||
@@ -1079,7 +1104,7 @@ impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
|
||||
is_remote: !item.is_local_echo(),
|
||||
event_or_transaction_id: item.identifier().into(),
|
||||
sender: item.sender().to_string(),
|
||||
sender_profile: item.sender_profile().into(),
|
||||
sender_profile: item.sender_profile().clone().into(),
|
||||
is_own: item.is_own(),
|
||||
is_editable: item.is_editable(),
|
||||
content: item.content().clone().into(),
|
||||
@@ -1120,13 +1145,13 @@ pub enum ProfileDetails {
|
||||
Error { message: String },
|
||||
}
|
||||
|
||||
impl From<&TimelineDetails<Profile>> for ProfileDetails {
|
||||
fn from(details: &TimelineDetails<Profile>) -> Self {
|
||||
impl From<TimelineDetails<Profile>> for ProfileDetails {
|
||||
fn from(details: TimelineDetails<Profile>) -> Self {
|
||||
match details {
|
||||
TimelineDetails::Unavailable => Self::Unavailable,
|
||||
TimelineDetails::Pending => Self::Pending,
|
||||
TimelineDetails::Ready(profile) => Self::Ready {
|
||||
display_name: profile.display_name.clone(),
|
||||
display_name: profile.display_name,
|
||||
display_name_ambiguous: profile.display_name_ambiguous,
|
||||
avatar_url: profile.avatar_url.as_ref().map(ToString::to_string),
|
||||
},
|
||||
@@ -1176,15 +1201,15 @@ impl TryFrom<PollData> for UnstablePollStartContentBlock {
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SendAttachmentJoinHandle {
|
||||
join_hdl: Arc<Mutex<JoinHandle<Result<(), RoomError>>>>,
|
||||
abort_hdl: AbortHandle,
|
||||
join_handle: Arc<Mutex<JoinHandle<Result<(), RoomError>>>>,
|
||||
abort_handle: AbortHandle,
|
||||
}
|
||||
|
||||
impl SendAttachmentJoinHandle {
|
||||
fn new(join_hdl: JoinHandle<Result<(), RoomError>>) -> Arc<Self> {
|
||||
let abort_hdl = join_hdl.abort_handle();
|
||||
let join_hdl = Arc::new(Mutex::new(join_hdl));
|
||||
Arc::new(Self { join_hdl, abort_hdl })
|
||||
fn new(join_handle: JoinHandle<Result<(), RoomError>>) -> Arc<Self> {
|
||||
let abort_handle = join_handle.abort_handle();
|
||||
let join_handle = Arc::new(Mutex::new(join_handle));
|
||||
Arc::new(Self { join_handle, abort_handle })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1194,7 +1219,7 @@ impl SendAttachmentJoinHandle {
|
||||
///
|
||||
/// If the sending had been cancelled, will return immediately.
|
||||
pub async fn join(&self) -> Result<(), RoomError> {
|
||||
let handle = self.join_hdl.clone();
|
||||
let handle = self.join_handle.clone();
|
||||
let mut locked_handle = handle.lock().await;
|
||||
let join_result = (&mut *locked_handle).await;
|
||||
match join_result {
|
||||
@@ -1213,7 +1238,7 @@ impl SendAttachmentJoinHandle {
|
||||
///
|
||||
/// A subsequent call to [`Self::join`] will return immediately.
|
||||
pub fn cancel(&self) {
|
||||
self.abort_hdl.abort();
|
||||
self.abort_handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1343,3 +1368,240 @@ impl LazyTimelineItemProvider {
|
||||
self.0.contains_only_emojis()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "unstable-msc4274")]
|
||||
mod galleries {
|
||||
use std::{panic, sync::Arc};
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use matrix_sdk::{
|
||||
attachment::{
|
||||
AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo, Thumbnail,
|
||||
},
|
||||
utils::formatted_body_from,
|
||||
};
|
||||
use matrix_sdk_ui::timeline::GalleryConfig;
|
||||
use mime::Mime;
|
||||
use tokio::{
|
||||
sync::Mutex,
|
||||
task::{AbortHandle, JoinHandle},
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
error::RoomError,
|
||||
ruma::{AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, VideoInfo},
|
||||
timeline::{build_thumbnail_info, ReplyParameters, Timeline},
|
||||
};
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct GalleryUploadParameters {
|
||||
/// Optional non-formatted caption, for clients that support it.
|
||||
caption: Option<String>,
|
||||
/// Optional HTML-formatted caption, for clients that support it.
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
/// Optional intentional mentions to be sent with the gallery.
|
||||
mentions: Option<Mentions>,
|
||||
/// Optional parameters for sending the media as (threaded) reply.
|
||||
reply_params: Option<ReplyParameters>,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum GalleryItemInfo {
|
||||
Audio {
|
||||
audio_info: AudioInfo,
|
||||
filename: String,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
},
|
||||
File {
|
||||
file_info: FileInfo,
|
||||
filename: String,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
},
|
||||
Image {
|
||||
image_info: ImageInfo,
|
||||
filename: String,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
thumbnail_path: Option<String>,
|
||||
},
|
||||
Video {
|
||||
video_info: VideoInfo,
|
||||
filename: String,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
thumbnail_path: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl GalleryItemInfo {
|
||||
fn mimetype(&self) -> &Option<String> {
|
||||
match self {
|
||||
GalleryItemInfo::Audio { audio_info, .. } => &audio_info.mimetype,
|
||||
GalleryItemInfo::File { file_info, .. } => &file_info.mimetype,
|
||||
GalleryItemInfo::Image { image_info, .. } => &image_info.mimetype,
|
||||
GalleryItemInfo::Video { video_info, .. } => &video_info.mimetype,
|
||||
}
|
||||
}
|
||||
|
||||
fn filename(&self) -> &String {
|
||||
match self {
|
||||
GalleryItemInfo::Audio { filename, .. } => filename,
|
||||
GalleryItemInfo::File { filename, .. } => filename,
|
||||
GalleryItemInfo::Image { filename, .. } => filename,
|
||||
GalleryItemInfo::Video { filename, .. } => filename,
|
||||
}
|
||||
}
|
||||
|
||||
fn caption(&self) -> &Option<String> {
|
||||
match self {
|
||||
GalleryItemInfo::Audio { caption, .. } => caption,
|
||||
GalleryItemInfo::File { caption, .. } => caption,
|
||||
GalleryItemInfo::Image { caption, .. } => caption,
|
||||
GalleryItemInfo::Video { caption, .. } => caption,
|
||||
}
|
||||
}
|
||||
|
||||
fn formatted_caption(&self) -> &Option<FormattedBody> {
|
||||
match self {
|
||||
GalleryItemInfo::Audio { formatted_caption, .. } => formatted_caption,
|
||||
GalleryItemInfo::File { formatted_caption, .. } => formatted_caption,
|
||||
GalleryItemInfo::Image { formatted_caption, .. } => formatted_caption,
|
||||
GalleryItemInfo::Video { formatted_caption, .. } => formatted_caption,
|
||||
}
|
||||
}
|
||||
|
||||
fn attachment_info(&self) -> Result<AttachmentInfo, RoomError> {
|
||||
match self {
|
||||
GalleryItemInfo::Audio { audio_info, .. } => Ok(AttachmentInfo::Audio(
|
||||
BaseAudioInfo::try_from(audio_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
)),
|
||||
GalleryItemInfo::File { file_info, .. } => Ok(AttachmentInfo::File(
|
||||
BaseFileInfo::try_from(file_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
)),
|
||||
GalleryItemInfo::Image { image_info, .. } => Ok(AttachmentInfo::Image(
|
||||
BaseImageInfo::try_from(image_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
)),
|
||||
GalleryItemInfo::Video { video_info, .. } => Ok(AttachmentInfo::Video(
|
||||
BaseVideoInfo::try_from(video_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn thumbnail(&self) -> Result<Option<Thumbnail>, RoomError> {
|
||||
match self {
|
||||
GalleryItemInfo::Audio { .. } | GalleryItemInfo::File { .. } => Ok(None),
|
||||
GalleryItemInfo::Image { image_info, thumbnail_path, .. } => {
|
||||
build_thumbnail_info(thumbnail_path.clone(), image_info.thumbnail_info.clone())
|
||||
}
|
||||
GalleryItemInfo::Video { video_info, thumbnail_path, .. } => {
|
||||
build_thumbnail_info(thumbnail_path.clone(), video_info.thumbnail_info.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<matrix_sdk_ui::timeline::GalleryItemInfo> for GalleryItemInfo {
|
||||
type Error = RoomError;
|
||||
|
||||
fn try_into(
|
||||
self,
|
||||
) -> std::result::Result<matrix_sdk_ui::timeline::GalleryItemInfo, Self::Error> {
|
||||
let mime_str = self.mimetype().as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
Ok(matrix_sdk_ui::timeline::GalleryItemInfo {
|
||||
source: self.filename().into(),
|
||||
content_type: mime_type,
|
||||
attachment_info: self.attachment_info()?,
|
||||
caption: self.caption().clone(),
|
||||
formatted_caption: self
|
||||
.formatted_caption()
|
||||
.clone()
|
||||
.map(ruma::events::room::message::FormattedBody::from),
|
||||
thumbnail: self.thumbnail()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SendGalleryJoinHandle {
|
||||
join_handle: Arc<Mutex<JoinHandle<Result<(), RoomError>>>>,
|
||||
abort_handle: AbortHandle,
|
||||
}
|
||||
|
||||
impl SendGalleryJoinHandle {
|
||||
fn new(join_handle: JoinHandle<Result<(), RoomError>>) -> Arc<Self> {
|
||||
let abort_handle = join_handle.abort_handle();
|
||||
let join_handle = Arc::new(Mutex::new(join_handle));
|
||||
Arc::new(Self { join_handle, abort_handle })
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SendGalleryJoinHandle {
|
||||
/// Wait until the gallery has been sent.
|
||||
///
|
||||
/// If the sending had been cancelled, will return immediately.
|
||||
pub async fn join(&self) -> Result<(), RoomError> {
|
||||
let handle = self.join_handle.clone();
|
||||
let mut locked_handle = handle.lock().await;
|
||||
let join_result = (&mut *locked_handle).await;
|
||||
match join_result {
|
||||
Ok(res) => res,
|
||||
Err(err) => {
|
||||
if err.is_cancelled() {
|
||||
return Ok(());
|
||||
}
|
||||
error!("task panicked! resuming panic from here.");
|
||||
panic::resume_unwind(err.into_panic());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel the current sending task.
|
||||
///
|
||||
/// A subsequent call to [`Self::join`] will return immediately.
|
||||
pub fn cancel(&self) {
|
||||
self.abort_handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Timeline {
|
||||
pub fn send_gallery(
|
||||
self: Arc<Self>,
|
||||
params: GalleryUploadParameters,
|
||||
item_infos: Vec<GalleryItemInfo>,
|
||||
) -> Result<Arc<SendGalleryJoinHandle>, RoomError> {
|
||||
let formatted_caption = formatted_body_from(
|
||||
params.caption.as_deref(),
|
||||
params.formatted_caption.map(Into::into),
|
||||
);
|
||||
|
||||
let mut gallery_config = GalleryConfig::new()
|
||||
.caption(params.caption)
|
||||
.formatted_caption(formatted_caption)
|
||||
.mentions(params.mentions.map(Into::into))
|
||||
.reply(params.reply_params.map(|p| p.try_into()).transpose()?);
|
||||
|
||||
for item_info in item_infos {
|
||||
gallery_config = gallery_config.add_item(item_info.try_into()?);
|
||||
}
|
||||
|
||||
let handle = SendGalleryJoinHandle::new(get_runtime_handle().spawn(async move {
|
||||
let request = self.inner.send_gallery(gallery_config);
|
||||
request.await.map_err(|_| RoomError::FailedSendingAttachment)?;
|
||||
Ok(())
|
||||
}));
|
||||
|
||||
Ok(handle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,10 @@ use std::{collections::HashMap, sync::Arc};
|
||||
use matrix_sdk::crypto::types::events::UtdCause;
|
||||
use ruma::events::{room::MediaSource as RumaMediaSource, EventContent};
|
||||
|
||||
use super::{content::Reaction, reply::InReplyToDetails};
|
||||
use super::{
|
||||
content::Reaction,
|
||||
reply::{EmbeddedEventDetails, InReplyToDetails},
|
||||
};
|
||||
use crate::{
|
||||
error::ClientError,
|
||||
ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind},
|
||||
@@ -56,10 +59,12 @@ pub enum MsgLikeKind {
|
||||
pub struct MsgLikeContent {
|
||||
pub kind: MsgLikeKind,
|
||||
pub reactions: Vec<Reaction>,
|
||||
/// Event ID of the thread root, if this is a threaded message.
|
||||
pub thread_root: Option<String>,
|
||||
/// The event this message is replying to, if any.
|
||||
pub in_reply_to: Option<Arc<InReplyToDetails>>,
|
||||
/// Event ID of the thread root, if this is a message in a thread.
|
||||
pub thread_root: Option<String>,
|
||||
/// Details about the thread this message is the root of.
|
||||
pub thread_summary: Option<Arc<ThreadSummary>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
@@ -95,6 +100,8 @@ impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
|
||||
|
||||
let thread_root = value.thread_root.map(|id| id.to_string());
|
||||
|
||||
let thread_summary = value.thread_summary.map(|t| Arc::new(t.into()));
|
||||
|
||||
Ok(match value.kind {
|
||||
Kind::Message(message) => {
|
||||
let msg_type = TryInto::<MessageType>::try_into(message.msgtype().clone())
|
||||
@@ -112,6 +119,7 @@ impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
|
||||
reactions,
|
||||
in_reply_to,
|
||||
thread_root,
|
||||
thread_summary,
|
||||
}
|
||||
}
|
||||
Kind::Sticker(sticker) => {
|
||||
@@ -134,6 +142,7 @@ impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
|
||||
reactions,
|
||||
in_reply_to,
|
||||
thread_root,
|
||||
thread_summary,
|
||||
}
|
||||
}
|
||||
Kind::Poll(poll_state) => {
|
||||
@@ -156,16 +165,22 @@ impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
|
||||
reactions,
|
||||
in_reply_to,
|
||||
thread_root,
|
||||
thread_summary,
|
||||
}
|
||||
}
|
||||
Kind::Redacted => {
|
||||
Self { kind: MsgLikeKind::Redacted, reactions, in_reply_to, thread_root }
|
||||
}
|
||||
Kind::Redacted => Self {
|
||||
kind: MsgLikeKind::Redacted,
|
||||
reactions,
|
||||
in_reply_to,
|
||||
thread_root,
|
||||
thread_summary,
|
||||
},
|
||||
Kind::UnableToDecrypt(msg) => Self {
|
||||
kind: MsgLikeKind::UnableToDecrypt { msg: EncryptedMessage::new(&msg) },
|
||||
reactions,
|
||||
in_reply_to,
|
||||
thread_root,
|
||||
thread_summary,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -222,3 +237,25 @@ pub struct PollAnswer {
|
||||
pub id: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct ThreadSummary {
|
||||
pub latest_event: EmbeddedEventDetails,
|
||||
pub num_replies: usize,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl ThreadSummary {
|
||||
pub fn latest_event(&self) -> EmbeddedEventDetails {
|
||||
self.latest_event.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::ThreadSummary> for ThreadSummary {
|
||||
fn from(value: matrix_sdk_ui::timeline::ThreadSummary) -> Self {
|
||||
Self {
|
||||
latest_event: EmbeddedEventDetails::from(value.latest_event),
|
||||
num_replies: value.num_replies,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,18 +12,18 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk_ui::timeline::TimelineDetails;
|
||||
use matrix_sdk_ui::timeline::{EmbeddedEvent, TimelineDetails};
|
||||
|
||||
use super::{content::TimelineItemContent, ProfileDetails};
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct InReplyToDetails {
|
||||
event_id: String,
|
||||
event: RepliedToEventDetails,
|
||||
event: EmbeddedEventDetails,
|
||||
}
|
||||
|
||||
impl InReplyToDetails {
|
||||
pub(crate) fn new(event_id: String, event: RepliedToEventDetails) -> Self {
|
||||
pub(crate) fn new(event_id: String, event: EmbeddedEventDetails) -> Self {
|
||||
Self { event_id, event }
|
||||
}
|
||||
}
|
||||
@@ -34,35 +34,37 @@ impl InReplyToDetails {
|
||||
self.event_id.clone()
|
||||
}
|
||||
|
||||
pub fn event(&self) -> RepliedToEventDetails {
|
||||
pub fn event(&self) -> EmbeddedEventDetails {
|
||||
self.event.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
|
||||
fn from(inner: matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
|
||||
let event_id = inner.event_id.to_string();
|
||||
let event = match &inner.event {
|
||||
TimelineDetails::Unavailable => RepliedToEventDetails::Unavailable,
|
||||
TimelineDetails::Pending => RepliedToEventDetails::Pending,
|
||||
TimelineDetails::Ready(event) => RepliedToEventDetails::Ready {
|
||||
content: event.content().clone().into(),
|
||||
sender: event.sender().to_string(),
|
||||
sender_profile: event.sender_profile().into(),
|
||||
},
|
||||
TimelineDetails::Error(err) => {
|
||||
RepliedToEventDetails::Error { message: err.to_string() }
|
||||
}
|
||||
};
|
||||
|
||||
Self { event_id, event }
|
||||
Self { event_id: inner.event_id.to_string(), event: inner.event.into() }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum RepliedToEventDetails {
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum EmbeddedEventDetails {
|
||||
Unavailable,
|
||||
Pending,
|
||||
Ready { content: TimelineItemContent, sender: String, sender_profile: ProfileDetails },
|
||||
Error { message: String },
|
||||
}
|
||||
|
||||
impl From<TimelineDetails<Box<EmbeddedEvent>>> for EmbeddedEventDetails {
|
||||
fn from(event: TimelineDetails<Box<EmbeddedEvent>>) -> Self {
|
||||
match event {
|
||||
TimelineDetails::Unavailable => EmbeddedEventDetails::Unavailable,
|
||||
TimelineDetails::Pending => EmbeddedEventDetails::Pending,
|
||||
TimelineDetails::Ready(event) => EmbeddedEventDetails::Ready {
|
||||
content: event.content.into(),
|
||||
sender: event.sender.to_string(),
|
||||
sender_profile: event.sender_profile.into(),
|
||||
},
|
||||
TimelineDetails::Error(err) => EmbeddedEventDetails::Error { message: err.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{fmt::Debug, sync::Arc, time::Duration};
|
||||
|
||||
use matrix_sdk::crypto::types::events::UtdCause;
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::unable_to_decrypt_hook::{
|
||||
UnableToDecryptHook, UnableToDecryptInfo as SdkUnableToDecryptInfo,
|
||||
};
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait UnableToDecryptDelegate: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_utd(&self, info: UnableToDecryptInfo);
|
||||
}
|
||||
|
||||
pub struct UtdHook {
|
||||
pub delegate: Arc<dyn UnableToDecryptDelegate>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for UtdHook {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("UtdHook").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl UnableToDecryptHook for UtdHook {
|
||||
fn on_utd(&self, info: SdkUnableToDecryptInfo) {
|
||||
const IGNORE_UTD_PERIOD: Duration = Duration::from_secs(4);
|
||||
|
||||
// UTDs that have been decrypted in the `IGNORE_UTD_PERIOD` are just ignored and
|
||||
// not considered UTDs.
|
||||
if let Some(duration) = &info.time_to_decrypt {
|
||||
if *duration < IGNORE_UTD_PERIOD {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Report the UTD to the client.
|
||||
self.delegate.on_utd(info.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct UnableToDecryptInfo {
|
||||
/// The identifier of the event that couldn't get decrypted.
|
||||
event_id: String,
|
||||
|
||||
/// If the event could be decrypted late (that is, the event was encrypted
|
||||
/// at first, but could be decrypted later on), then this indicates the
|
||||
/// time it took to decrypt the event. If it is not set, this is
|
||||
/// considered a definite UTD.
|
||||
///
|
||||
/// If set, this is in milliseconds.
|
||||
pub time_to_decrypt_ms: Option<u64>,
|
||||
|
||||
/// What we know about what caused this UTD. E.g. was this event sent when
|
||||
/// we were not a member of this room?
|
||||
pub cause: UtdCause,
|
||||
|
||||
/// The difference between the event creation time (`origin_server_ts`) and
|
||||
/// the time our device was created. If negative, this event was sent
|
||||
/// *before* our device was created.
|
||||
pub event_local_age_millis: i64,
|
||||
|
||||
/// Whether the user had verified their own identity at the point they
|
||||
/// received the UTD event.
|
||||
pub user_trusts_own_identity: bool,
|
||||
|
||||
/// The homeserver of the user that sent the undecryptable event.
|
||||
pub sender_homeserver: String,
|
||||
|
||||
/// Our local user's own homeserver, or `None` if the client is not logged
|
||||
/// in.
|
||||
pub own_homeserver: Option<String>,
|
||||
}
|
||||
|
||||
impl From<SdkUnableToDecryptInfo> for UnableToDecryptInfo {
|
||||
fn from(value: SdkUnableToDecryptInfo) -> Self {
|
||||
Self {
|
||||
event_id: value.event_id.to_string(),
|
||||
time_to_decrypt_ms: value.time_to_decrypt.map(|ttd| ttd.as_millis() as u64),
|
||||
cause: value.cause,
|
||||
event_local_age_millis: value.event_local_age_millis,
|
||||
user_trusts_own_identity: value.user_trusts_own_identity,
|
||||
sender_homeserver: value.sender_homeserver.to_string(),
|
||||
own_homeserver: value.own_homeserver.map(String::from),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,11 @@
|
||||
|
||||
use std::{mem::ManuallyDrop, ops::Deref};
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use ruma::{MilliSecondsSinceUnixEpoch, UInt};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::runtime::get_runtime_handle;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Timestamp(u64);
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use language_tags::LanguageTag;
|
||||
use matrix_sdk::{
|
||||
async_trait,
|
||||
widget::{MessageLikeEventFilter, StateEventFilter},
|
||||
widget::{MessageLikeEventFilter, StateEventFilter, ToDeviceEventFilter},
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::events::MessageLikeEventType;
|
||||
use tracing::error;
|
||||
|
||||
use crate::room::Room;
|
||||
use crate::{room::Room, runtime::get_runtime_handle};
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct WidgetDriverAndHandle {
|
||||
@@ -95,7 +95,7 @@ impl From<matrix_sdk::widget::WidgetSettings> for WidgetSettings {
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `widget_settings` - The widget settings to generate the url for.
|
||||
/// * `room` - A matrix room which is used to query the logged in username
|
||||
/// * `room` - A Matrix room which is used to query the logged in username
|
||||
/// * `props` - Properties from the client that can be used by a widget to adapt
|
||||
/// to the client. e.g. language, font-scale...
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
@@ -248,6 +248,11 @@ pub struct VirtualElementCallWidgetOptions {
|
||||
/// Sentry [environment](https://docs.sentry.io/concepts/key-terms/key-terms/)
|
||||
/// Supported since Element Call v0.9.0. Only used by the embedded package.
|
||||
pub sentry_environment: Option<String>,
|
||||
//// - `true`: The webview should show the list of media devices it detects using
|
||||
//// `enumerateDevices`.
|
||||
/// - `false`: the webview shows a a list of devices injected by the
|
||||
/// client. (used on ios & android)
|
||||
pub controlled_media_devices: bool,
|
||||
}
|
||||
|
||||
impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElementCallWidgetOptions {
|
||||
@@ -271,6 +276,7 @@ impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElemen
|
||||
rageshake_submit_url: value.rageshake_submit_url,
|
||||
sentry_dsn: value.sentry_dsn,
|
||||
sentry_environment: value.sentry_environment,
|
||||
controlled_media_devices: value.controlled_media_devices,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -321,7 +327,9 @@ pub fn get_element_call_required_permissions(
|
||||
event_type: "org.matrix.rageshake_request".to_owned(),
|
||||
},
|
||||
// To read and send encryption keys
|
||||
WidgetEventFilter::ToDevice { event_type: "io.element.call.encryption_keys".to_owned() },
|
||||
// TODO change this to the appropriate to-device version once ready
|
||||
// remove this once all matrixRTC call apps supports to-device encryption.
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "io.element.call.encryption_keys".to_owned(),
|
||||
},
|
||||
@@ -373,7 +381,7 @@ pub fn get_element_call_required_permissions(
|
||||
state_key: format!("{own_user_id}_{own_device_id}"),
|
||||
},
|
||||
// The same as above but with an underscore.
|
||||
// To work around the issue that state events starting with `@` have to be matrix id's
|
||||
// To work around the issue that state events starting with `@` have to be Matrix id's
|
||||
// but we use mxId+deviceId.
|
||||
WidgetEventFilter::StateWithTypeAndStateKey {
|
||||
event_type: StateEventType::CallMember.to_string(),
|
||||
@@ -442,7 +450,7 @@ pub struct WidgetCapabilities {
|
||||
/// Types of the messages that a widget wants to be able to send.
|
||||
pub send: Vec<WidgetEventFilter>,
|
||||
/// If this capability is requested by the widget, it can not operate
|
||||
/// separately from the matrix client.
|
||||
/// separately from the Matrix client.
|
||||
///
|
||||
/// This means clients should not offer to open the widget in a separate
|
||||
/// browser/tab/webview that is not connected to the postmessage widget-api.
|
||||
@@ -488,9 +496,11 @@ pub enum WidgetEventFilter {
|
||||
StateWithType { event_type: String },
|
||||
/// Matches state events with the given `type` and `state_key`.
|
||||
StateWithTypeAndStateKey { event_type: String, state_key: String },
|
||||
/// Matches to-device events with the given `event_type`.
|
||||
ToDevice { event_type: String },
|
||||
}
|
||||
|
||||
impl From<WidgetEventFilter> for matrix_sdk::widget::EventFilter {
|
||||
impl From<WidgetEventFilter> for matrix_sdk::widget::Filter {
|
||||
fn from(value: WidgetEventFilter) -> Self {
|
||||
match value {
|
||||
WidgetEventFilter::MessageLikeWithType { event_type } => {
|
||||
@@ -505,13 +515,16 @@ impl From<WidgetEventFilter> for matrix_sdk::widget::EventFilter {
|
||||
WidgetEventFilter::StateWithTypeAndStateKey { event_type, state_key } => {
|
||||
Self::State(StateEventFilter::WithTypeAndStateKey(event_type.into(), state_key))
|
||||
}
|
||||
WidgetEventFilter::ToDevice { event_type } => {
|
||||
Self::ToDevice(ToDeviceEventFilter { event_type: event_type.into() })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::widget::EventFilter> for WidgetEventFilter {
|
||||
fn from(value: matrix_sdk::widget::EventFilter) -> Self {
|
||||
use matrix_sdk::widget::EventFilter as F;
|
||||
impl From<matrix_sdk::widget::Filter> for WidgetEventFilter {
|
||||
fn from(value: matrix_sdk::widget::Filter) -> Self {
|
||||
use matrix_sdk::widget::Filter as F;
|
||||
|
||||
match value {
|
||||
F::MessageLike(MessageLikeEventFilter::WithType(event_type)) => {
|
||||
@@ -526,18 +539,22 @@ impl From<matrix_sdk::widget::EventFilter> for WidgetEventFilter {
|
||||
F::State(StateEventFilter::WithTypeAndStateKey(event_type, state_key)) => {
|
||||
Self::StateWithTypeAndStateKey { event_type: event_type.to_string(), state_key }
|
||||
}
|
||||
F::ToDevice(ToDeviceEventFilter { event_type }) => {
|
||||
Self::ToDevice { event_type: event_type.to_string() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait WidgetCapabilitiesProvider: Send + Sync {
|
||||
pub trait WidgetCapabilitiesProvider: SendOutsideWasm + SyncOutsideWasm {
|
||||
fn acquire_capabilities(&self, capabilities: WidgetCapabilities) -> WidgetCapabilities;
|
||||
}
|
||||
|
||||
struct CapabilitiesProviderWrap(Arc<dyn WidgetCapabilitiesProvider>);
|
||||
|
||||
#[async_trait]
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
impl matrix_sdk::widget::CapabilitiesProvider for CapabilitiesProviderWrap {
|
||||
async fn acquire_capabilities(
|
||||
&self,
|
||||
@@ -631,8 +648,7 @@ mod tests {
|
||||
let cap_assert = |capability: &str| {
|
||||
assert!(
|
||||
permission_array.contains(&capability.to_owned()),
|
||||
"The \"{}\" capability was missing from the element call capability list.",
|
||||
capability
|
||||
"The \"{capability}\" capability was missing from the element call capability list."
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
## [0.12.0] - 2025-06-10
|
||||
|
||||
No notable changes in this release.
|
||||
|
||||
## [0.11.0] - 2025-04-11
|
||||
|
||||
### Features
|
||||
@@ -40,6 +44,14 @@ All notable changes to this project will be documented in this file.
|
||||
- [**breaking**] `BaseClient::set_session_metadata` is renamed
|
||||
`activate`, and `BaseClient::logged_in` is renamed `is_activated`
|
||||
([#4850](https://github.com/matrix-org/matrix-rust-sdk/pull/4850))
|
||||
- [**breaking] `DependentQueuedRequestKind::UploadFileWithThumbnail`
|
||||
was renamed to `DependentQueuedRequestKind::UploadFileOrThumbnail`.
|
||||
Under the `unstable-msc4274` feature, `DependentQueuedRequestKind::UploadFileOrThumbnail`
|
||||
and `SentMediaInfo` were generalized to allow chaining multiple dependent
|
||||
file / thumbnail uploads.
|
||||
([#4897](https://github.com/matrix-org/matrix-rust-sdk/pull/4897))
|
||||
- [**breaking**] `RoomInfo::prev_state` has been removed due to being useless.
|
||||
([#5054](https://github.com/matrix-org/matrix-rust-sdk/pull/5054))
|
||||
|
||||
## [0.10.0] - 2025-02-04
|
||||
|
||||
|
||||
@@ -8,19 +8,25 @@ license = "Apache-2.0"
|
||||
name = "matrix-sdk-base"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = { workspace = true }
|
||||
version = "0.11.0"
|
||||
rust-version.workspace = true
|
||||
version = "0.12.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
rustdoc-args = ["--cfg", "docsrs", "--generate-link-to-definition"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
e2e-encryption = ["dep:matrix-sdk-crypto"]
|
||||
js = ["matrix-sdk-common/js", "matrix-sdk-crypto?/js", "ruma/js", "matrix-sdk-store-encryption/js"]
|
||||
js = [
|
||||
"matrix-sdk-common/js",
|
||||
"matrix-sdk-crypto?/js",
|
||||
"ruma/js",
|
||||
"matrix-sdk-store-encryption/js",
|
||||
]
|
||||
qrcode = ["matrix-sdk-crypto?/qrcode"]
|
||||
automatic-room-key-forwarding = ["matrix-sdk-crypto?/automatic-room-key-forwarding"]
|
||||
experimental-send-custom-to-device = ["matrix-sdk-crypto?/experimental-send-custom-to-device"]
|
||||
uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"]
|
||||
|
||||
# Private feature, see
|
||||
@@ -43,23 +49,26 @@ testing = [
|
||||
"matrix-sdk-crypto?/testing",
|
||||
]
|
||||
|
||||
# Add support for inline media galleries via msgtypes
|
||||
unstable-msc4274 = []
|
||||
|
||||
[dependencies]
|
||||
as_variant = { workspace = true }
|
||||
as_variant.workspace = true
|
||||
assert_matches = { workspace = true, optional = true }
|
||||
assert_matches2 = { workspace = true, optional = true }
|
||||
async-trait = { workspace = true }
|
||||
bitflags = { version = "2.8.0", features = ["serde"] }
|
||||
decancer = "3.2.8"
|
||||
async-trait.workspace = true
|
||||
bitflags = { workspace = true, features = ["serde"] }
|
||||
decancer = "3.3.0"
|
||||
eyeball = { workspace = true, features = ["async-lock"] }
|
||||
eyeball-im = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
growable-bloom-filter = { workspace = true }
|
||||
eyeball-im.workspace = true
|
||||
futures-util.workspace = true
|
||||
growable-bloom-filter.workspace = true
|
||||
http = { workspace = true, optional = true }
|
||||
matrix-sdk-common = { workspace = true }
|
||||
matrix-sdk-common.workspace = true
|
||||
matrix-sdk-crypto = { workspace = true, optional = true }
|
||||
matrix-sdk-store-encryption = { workspace = true }
|
||||
matrix-sdk-store-encryption.workspace = true
|
||||
matrix-sdk-test = { workspace = true, optional = true }
|
||||
once_cell = { workspace = true }
|
||||
once_cell.workspace = true
|
||||
regex = "1.11.1"
|
||||
ruma = { workspace = true, features = [
|
||||
"canonical-json",
|
||||
@@ -68,29 +77,29 @@ ruma = { workspace = true, features = [
|
||||
"unstable-msc4186",
|
||||
"rand",
|
||||
] }
|
||||
unicode-normalization = { workspace = true }
|
||||
serde = { workspace = true, features = ["rc"] }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
unicode-normalization.workspace = true
|
||||
uniffi = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = { workspace = true }
|
||||
assert_matches2 = { workspace = true }
|
||||
assert_matches.workspace = true
|
||||
assert_matches2.workspace = true
|
||||
assign = "1.1.1"
|
||||
futures-executor = { workspace = true }
|
||||
http = { workspace = true }
|
||||
matrix-sdk-test = { workspace = true }
|
||||
stream_assert = { workspace = true }
|
||||
similar-asserts = { workspace = true }
|
||||
futures-executor.workspace = true
|
||||
http.workspace = true
|
||||
matrix-sdk-test.workspace = true
|
||||
similar-asserts.workspace = true
|
||||
stream_assert.workspace = true
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
[target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
wasm-bindgen-test = { workspace = true }
|
||||
[target.'cfg(target_family = "wasm")'.dev-dependencies]
|
||||
wasm-bindgen-test.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
fmt, iter,
|
||||
fmt,
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
@@ -36,37 +36,36 @@ use ruma::DeviceId;
|
||||
use ruma::{
|
||||
api::client::{self as api, sync::sync_events::v5},
|
||||
events::{
|
||||
ignored_user_list::IgnoredUserListEventContent,
|
||||
push_rules::{PushRulesEvent, PushRulesEventContent},
|
||||
room::member::SyncRoomMemberEvent,
|
||||
AnyStrippedStateEvent, AnySyncEphemeralRoomEvent, StateEvent, StateEventType,
|
||||
StateEvent, StateEventType,
|
||||
},
|
||||
push::{Action, Ruleset},
|
||||
serde::Raw,
|
||||
push::Ruleset,
|
||||
time::Instant,
|
||||
OwnedRoomId, OwnedUserId, RoomId,
|
||||
OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
use tokio::sync::{broadcast, Mutex};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
use tracing::{debug, info, instrument};
|
||||
use tracing::{debug, enabled, info, instrument, warn, Level};
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use crate::RoomMemberships;
|
||||
use crate::{
|
||||
deserialized_responses::{DisplayName, RawAnySyncOrStrippedTimelineEvent},
|
||||
deserialized_responses::DisplayName,
|
||||
error::{Error, Result},
|
||||
event_cache::store::EventCacheStoreLock,
|
||||
response_processors::{self as processors, Context},
|
||||
rooms::{
|
||||
normal::{RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMembersUpdate},
|
||||
Room, RoomInfo, RoomState,
|
||||
room::{
|
||||
Room, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMembersUpdate, RoomState,
|
||||
},
|
||||
store::{
|
||||
ambiguity_map::AmbiguityCache, BaseStateStore, DynStateStore, MemoryStore,
|
||||
Result as StoreResult, RoomLoadSettings, StateChanges, StateStoreDataKey,
|
||||
StateStoreDataValue, StateStoreExt, StoreConfig,
|
||||
},
|
||||
sync::{JoinedRoomUpdate, LeftRoomUpdate, Notification, RoomUpdates, SyncResponse},
|
||||
sync::{RoomUpdates, SyncResponse},
|
||||
RoomStateFilter, SessionMeta,
|
||||
};
|
||||
|
||||
@@ -367,65 +366,6 @@ impl BaseClient {
|
||||
self.state_store.sync_token.read().await.clone()
|
||||
}
|
||||
|
||||
/// Handles the stripped state events in `invite_state`, modifying the
|
||||
/// room's info and posting notifications as needed.
|
||||
///
|
||||
/// * `room` - The [`Room`] to modify.
|
||||
/// * `events` - The contents of `invite_state` in the form of list of pairs
|
||||
/// of raw stripped state events with their deserialized counterpart.
|
||||
/// * `push_rules` - The push rules for this room.
|
||||
/// * `room_info` - The current room's info.
|
||||
/// * `changes` - The accumulated list of changes to apply once the
|
||||
/// processing is finished.
|
||||
/// * `notifications` - Notifications to post for the current room.
|
||||
#[instrument(skip_all, fields(room_id = ?room_info.room_id))]
|
||||
pub(crate) async fn handle_invited_state(
|
||||
&self,
|
||||
context: &mut Context,
|
||||
room: &Room,
|
||||
events: (Vec<Raw<AnyStrippedStateEvent>>, Vec<AnyStrippedStateEvent>),
|
||||
push_rules: &Ruleset,
|
||||
room_info: &mut RoomInfo,
|
||||
notifications: &mut BTreeMap<OwnedRoomId, Vec<Notification>>,
|
||||
) -> Result<()> {
|
||||
let mut state_events = BTreeMap::new();
|
||||
|
||||
for (raw_event, event) in iter::zip(events.0, events.1) {
|
||||
room_info.handle_stripped_state_event(&event);
|
||||
state_events
|
||||
.entry(event.event_type())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(event.state_key().to_owned(), raw_event);
|
||||
}
|
||||
|
||||
context
|
||||
.state_changes
|
||||
.stripped_state
|
||||
.insert(room_info.room_id().to_owned(), state_events.clone());
|
||||
|
||||
// We need to check for notifications after we have handled all state
|
||||
// events, to make sure we have the full push context.
|
||||
if let Some(push_context) =
|
||||
processors::timeline::get_push_room_context(context, room, room_info, &self.state_store)
|
||||
.await?
|
||||
{
|
||||
// Check every event again for notification.
|
||||
for event in state_events.values().flat_map(|map| map.values()) {
|
||||
let actions = push_rules.get_actions(event, &push_context);
|
||||
if actions.iter().any(Action::should_notify) {
|
||||
notifications.entry(room.room_id().to_owned()).or_default().push(
|
||||
Notification {
|
||||
actions: actions.to_owned(),
|
||||
event: RawAnySyncOrStrippedTimelineEvent::Stripped(event.clone()),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// User has knocked on a room.
|
||||
///
|
||||
/// Update the internal and cached state accordingly. Return the final Room.
|
||||
@@ -546,25 +486,19 @@ impl BaseClient {
|
||||
return Ok(SyncResponse::default());
|
||||
}
|
||||
|
||||
let now = Instant::now();
|
||||
let now = if enabled!(Level::INFO) { Some(Instant::now()) } else { None };
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
let olm_machine = self.olm_machine().await;
|
||||
|
||||
let mut context =
|
||||
Context::new(StateChanges::new(response.next_batch.clone()), Default::default());
|
||||
let mut context = Context::new(StateChanges::new(response.next_batch.clone()));
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
let to_device = {
|
||||
let processors::e2ee::to_device::Output {
|
||||
decrypted_to_device_events: to_device,
|
||||
room_key_updates,
|
||||
} = processors::e2ee::to_device::from_sync_v2(
|
||||
&mut context,
|
||||
&response,
|
||||
olm_machine.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
} = processors::e2ee::to_device::from_sync_v2(&response, olm_machine.as_ref()).await?;
|
||||
|
||||
processors::latest_event::decrypt_from_rooms(
|
||||
&mut context,
|
||||
@@ -573,9 +507,11 @@ impl BaseClient {
|
||||
.flatten()
|
||||
.filter_map(|room_key_info| self.get_room(&room_key_info.room_id))
|
||||
.collect(),
|
||||
olm_machine.as_ref(),
|
||||
self.decryption_trust_requirement,
|
||||
self.handle_verification_events,
|
||||
processors::e2ee::E2EE::new(
|
||||
olm_machine.as_ref(),
|
||||
self.decryption_trust_requirement,
|
||||
self.handle_verification_events,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -592,87 +528,30 @@ impl BaseClient {
|
||||
|
||||
let push_rules = self.get_push_rules(&global_account_data_processor).await?;
|
||||
|
||||
let mut new_rooms = RoomUpdates::default();
|
||||
let mut room_updates = RoomUpdates::default();
|
||||
let mut notifications = Default::default();
|
||||
|
||||
let mut updated_members_in_room: BTreeMap<OwnedRoomId, BTreeSet<OwnedUserId>> =
|
||||
BTreeMap::new();
|
||||
|
||||
for (room_id, new_info) in response.rooms.join {
|
||||
let room = self.state_store.get_or_create_room(
|
||||
&room_id,
|
||||
RoomState::Joined,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
);
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
|
||||
room_info.mark_as_joined();
|
||||
room_info.update_from_ruma_summary(&new_info.summary);
|
||||
room_info.set_prev_batch(new_info.timeline.prev_batch.as_deref());
|
||||
room_info.mark_state_fully_synced();
|
||||
room_info.handle_encryption_state(requested_required_states.for_room(&room_id));
|
||||
|
||||
let (raw_state_events, state_events) =
|
||||
processors::state_events::sync::collect(&mut context, &new_info.state.events);
|
||||
|
||||
let mut new_user_ids = processors::state_events::dispatch_and_get_new_users(
|
||||
for (room_id, joined_room) in response.rooms.join {
|
||||
let joined_room_update = processors::room::sync_v2::update_joined_room(
|
||||
&mut context,
|
||||
(&raw_state_events, &state_events),
|
||||
&mut room_info,
|
||||
&mut ambiguity_cache,
|
||||
)
|
||||
.await?;
|
||||
|
||||
for raw in &new_info.ephemeral.events {
|
||||
match raw.deserialize() {
|
||||
Ok(AnySyncEphemeralRoomEvent::Receipt(event)) => {
|
||||
context.state_changes.add_receipts(&room_id, event.content);
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let event_id: Option<String> = raw.get_field("event_id").ok().flatten();
|
||||
#[rustfmt::skip]
|
||||
info!(
|
||||
?room_id, event_id,
|
||||
"Failed to deserialize ephemeral room event: {e}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if new_info.timeline.limited {
|
||||
room_info.mark_members_missing();
|
||||
}
|
||||
|
||||
let (raw_state_events_from_timeline, state_events_from_timeline) =
|
||||
processors::state_events::sync::collect_from_timeline(
|
||||
&mut context,
|
||||
&new_info.timeline.events,
|
||||
);
|
||||
|
||||
let mut other_new_user_ids = processors::state_events::dispatch_and_get_new_users(
|
||||
&mut context,
|
||||
(&raw_state_events_from_timeline, &state_events_from_timeline),
|
||||
&mut room_info,
|
||||
&mut ambiguity_cache,
|
||||
)
|
||||
.await?;
|
||||
new_user_ids.append(&mut other_new_user_ids);
|
||||
updated_members_in_room.insert(room_id.to_owned(), new_user_ids.clone());
|
||||
|
||||
let timeline = processors::timeline::build(
|
||||
&mut context,
|
||||
&room,
|
||||
&mut room_info,
|
||||
processors::timeline::builder::Timeline::from(new_info.timeline),
|
||||
processors::timeline::builder::Notification::new(
|
||||
processors::room::RoomCreationData::new(
|
||||
&room_id,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
requested_required_states,
|
||||
&mut ambiguity_cache,
|
||||
),
|
||||
joined_room,
|
||||
&mut updated_members_in_room,
|
||||
processors::notification::Notification::new(
|
||||
&push_rules,
|
||||
&mut notifications,
|
||||
&self.state_store,
|
||||
),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
processors::timeline::builder::E2EE::new(
|
||||
processors::e2ee::E2EE::new(
|
||||
olm_machine.as_ref(),
|
||||
self.decryption_trust_requirement,
|
||||
self.handle_verification_events,
|
||||
@@ -680,106 +559,26 @@ impl BaseClient {
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Save the new `RoomInfo`.
|
||||
context.state_changes.add_room(room_info);
|
||||
|
||||
processors::account_data::for_room(
|
||||
&mut context,
|
||||
&room_id,
|
||||
&new_info.account_data.events,
|
||||
&self.state_store,
|
||||
)
|
||||
.await;
|
||||
|
||||
// `Self::handle_room_account_data` might have updated the `RoomInfo`. Let's
|
||||
// fetch it again.
|
||||
//
|
||||
// SAFETY: `unwrap` is safe because the `RoomInfo` has been inserted 2 lines
|
||||
// above.
|
||||
let mut room_info = context.state_changes.room_infos.get(&room_id).unwrap().clone();
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
processors::e2ee::tracked_users::update_or_set_if_room_is_newly_encrypted(
|
||||
&mut context,
|
||||
olm_machine.as_ref(),
|
||||
&new_user_ids,
|
||||
room_info.encryption_state(),
|
||||
room.encryption_state(),
|
||||
&room_id,
|
||||
&self.state_store,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let notification_count = new_info.unread_notifications.into();
|
||||
room_info.update_notification_count(notification_count);
|
||||
|
||||
let ambiguity_changes = ambiguity_cache.changes.remove(&room_id).unwrap_or_default();
|
||||
|
||||
new_rooms.join.insert(
|
||||
room_id,
|
||||
JoinedRoomUpdate::new(
|
||||
timeline,
|
||||
new_info.state.events,
|
||||
new_info.account_data.events,
|
||||
new_info.ephemeral.events,
|
||||
notification_count,
|
||||
ambiguity_changes,
|
||||
),
|
||||
);
|
||||
|
||||
context.state_changes.add_room(room_info);
|
||||
room_updates.joined.insert(room_id, joined_room_update);
|
||||
}
|
||||
|
||||
for (room_id, new_info) in response.rooms.leave {
|
||||
let room = self.state_store.get_or_create_room(
|
||||
&room_id,
|
||||
RoomState::Left,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
);
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_as_left();
|
||||
room_info.mark_state_partially_synced();
|
||||
room_info.handle_encryption_state(requested_required_states.for_room(&room_id));
|
||||
|
||||
let (raw_state_events, state_events) =
|
||||
processors::state_events::sync::collect(&mut context, &new_info.state.events);
|
||||
|
||||
let mut new_user_ids = processors::state_events::dispatch_and_get_new_users(
|
||||
for (room_id, left_room) in response.rooms.leave {
|
||||
let left_room_update = processors::room::sync_v2::update_left_room(
|
||||
&mut context,
|
||||
(&raw_state_events, &state_events),
|
||||
&mut room_info,
|
||||
&mut ambiguity_cache,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (raw_state_events_from_timeline, state_events_from_timeline) =
|
||||
processors::state_events::sync::collect_from_timeline(
|
||||
&mut context,
|
||||
&new_info.timeline.events,
|
||||
);
|
||||
|
||||
let mut other_new_user_ids = processors::state_events::dispatch_and_get_new_users(
|
||||
&mut context,
|
||||
(&raw_state_events_from_timeline, &state_events_from_timeline),
|
||||
&mut room_info,
|
||||
&mut ambiguity_cache,
|
||||
)
|
||||
.await?;
|
||||
new_user_ids.append(&mut other_new_user_ids);
|
||||
|
||||
let timeline = processors::timeline::build(
|
||||
&mut context,
|
||||
&room,
|
||||
&mut room_info,
|
||||
processors::timeline::builder::Timeline::from(new_info.timeline),
|
||||
processors::timeline::builder::Notification::new(
|
||||
processors::room::RoomCreationData::new(
|
||||
&room_id,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
requested_required_states,
|
||||
&mut ambiguity_cache,
|
||||
),
|
||||
left_room,
|
||||
processors::notification::Notification::new(
|
||||
&push_rules,
|
||||
&mut notifications,
|
||||
&self.state_store,
|
||||
),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
processors::timeline::builder::E2EE::new(
|
||||
processors::e2ee::E2EE::new(
|
||||
olm_machine.as_ref(),
|
||||
self.decryption_trust_requirement,
|
||||
self.handle_verification_events,
|
||||
@@ -787,90 +586,41 @@ impl BaseClient {
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Save the new `RoomInfo`.
|
||||
context.state_changes.add_room(room_info);
|
||||
room_updates.left.insert(room_id, left_room_update);
|
||||
}
|
||||
|
||||
processors::account_data::for_room(
|
||||
for (room_id, invited_room) in response.rooms.invite {
|
||||
let invited_room_update = processors::room::sync_v2::update_invited_room(
|
||||
&mut context,
|
||||
&room_id,
|
||||
&new_info.account_data.events,
|
||||
&self.state_store,
|
||||
)
|
||||
.await;
|
||||
|
||||
let ambiguity_changes = ambiguity_cache.changes.remove(&room_id).unwrap_or_default();
|
||||
|
||||
new_rooms.leave.insert(
|
||||
room_id,
|
||||
LeftRoomUpdate::new(
|
||||
timeline,
|
||||
new_info.state.events,
|
||||
new_info.account_data.events,
|
||||
ambiguity_changes,
|
||||
invited_room,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
processors::notification::Notification::new(
|
||||
&push_rules,
|
||||
&mut notifications,
|
||||
&self.state_store,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
for (room_id, new_info) in response.rooms.invite {
|
||||
let room = self.state_store.get_or_create_room(
|
||||
&room_id,
|
||||
RoomState::Invited,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
);
|
||||
|
||||
let invite_state = processors::state_events::stripped::collect(
|
||||
&mut context,
|
||||
&new_info.invite_state.events,
|
||||
);
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_as_invited();
|
||||
room_info.mark_state_fully_synced();
|
||||
|
||||
self.handle_invited_state(
|
||||
&mut context,
|
||||
&room,
|
||||
invite_state,
|
||||
&push_rules,
|
||||
&mut room_info,
|
||||
&mut notifications,
|
||||
)
|
||||
.await?;
|
||||
|
||||
context.state_changes.add_room(room_info);
|
||||
|
||||
new_rooms.invite.insert(room_id, new_info);
|
||||
room_updates.invited.insert(room_id, invited_room_update);
|
||||
}
|
||||
|
||||
for (room_id, new_info) in response.rooms.knock {
|
||||
let room = self.state_store.get_or_create_room(
|
||||
for (room_id, knocked_room) in response.rooms.knock {
|
||||
let knocked_room_update = processors::room::sync_v2::update_knocked_room(
|
||||
&mut context,
|
||||
&room_id,
|
||||
RoomState::Knocked,
|
||||
knocked_room,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
);
|
||||
|
||||
let knock_state = processors::state_events::stripped::collect(
|
||||
&mut context,
|
||||
&new_info.knock_state.events,
|
||||
);
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_as_knocked();
|
||||
room_info.mark_state_fully_synced();
|
||||
|
||||
self.handle_invited_state(
|
||||
&mut context,
|
||||
&room,
|
||||
knock_state,
|
||||
&push_rules,
|
||||
&mut room_info,
|
||||
&mut notifications,
|
||||
processors::notification::Notification::new(
|
||||
&push_rules,
|
||||
&mut notifications,
|
||||
&self.state_store,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
context.state_changes.add_room(room_info);
|
||||
|
||||
new_rooms.knocked.insert(room_id, new_info);
|
||||
room_updates.knocked.insert(room_id, knocked_room_update);
|
||||
}
|
||||
|
||||
global_account_data_processor.apply(&mut context, &self.state_store).await;
|
||||
@@ -899,12 +649,19 @@ impl BaseClient {
|
||||
.await?;
|
||||
}
|
||||
|
||||
let mut context = Context::default();
|
||||
|
||||
// Now that all the rooms information have been saved, update the display name
|
||||
// cache (which relies on information stored in the database). This will
|
||||
// live in memory, until the next sync which will saves the room info to
|
||||
// disk; we do this to avoid saving that would be redundant with the
|
||||
// above. Oh well.
|
||||
new_rooms.update_in_memory_caches(&self.state_store).await;
|
||||
// of the updated rooms (which relies on information stored in the database).
|
||||
processors::room::display_name::update_for_rooms(
|
||||
&mut context,
|
||||
&room_updates,
|
||||
&self.state_store,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Save the new display name updates if any.
|
||||
processors::changes::save_only(context, &self.state_store).await?;
|
||||
|
||||
for (room_id, member_ids) in updated_members_in_room {
|
||||
if let Some(room) = self.get_room(&room_id) {
|
||||
@@ -913,10 +670,12 @@ impl BaseClient {
|
||||
}
|
||||
}
|
||||
|
||||
info!("Processed a sync response in {:?}", now.elapsed());
|
||||
if enabled!(Level::INFO) {
|
||||
info!("Processed a sync response in {:?}", now.map(|now| now.elapsed()));
|
||||
}
|
||||
|
||||
let response = SyncResponse {
|
||||
rooms: new_rooms,
|
||||
rooms: room_updates,
|
||||
presence: response.presence.events,
|
||||
account_data: response.account_data.events,
|
||||
to_device,
|
||||
@@ -958,7 +717,7 @@ impl BaseClient {
|
||||
};
|
||||
|
||||
let mut chunk = Vec::with_capacity(response.chunk.len());
|
||||
let mut context = Context::new(StateChanges::default(), Default::default());
|
||||
let mut context = Context::default();
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
let mut user_ids = BTreeSet::new();
|
||||
@@ -1018,7 +777,6 @@ impl BaseClient {
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
processors::e2ee::tracked_users::update(
|
||||
&mut context,
|
||||
self.olm_machine().await.as_ref(),
|
||||
room.encryption_state(),
|
||||
&user_ids,
|
||||
@@ -1203,6 +961,27 @@ impl BaseClient {
|
||||
pub fn room_info_notable_update_receiver(&self) -> broadcast::Receiver<RoomInfoNotableUpdate> {
|
||||
self.room_info_notable_update_sender.subscribe()
|
||||
}
|
||||
|
||||
/// Checks whether the provided `user_id` belongs to an ignored user.
|
||||
pub async fn is_user_ignored(&self, user_id: &UserId) -> bool {
|
||||
match self.state_store.get_account_data_event_static::<IgnoredUserListEventContent>().await
|
||||
{
|
||||
Ok(Some(raw_ignored_user_list)) => match raw_ignored_user_list.deserialize() {
|
||||
Ok(current_ignored_user_list) => {
|
||||
current_ignored_user_list.content.ignored_users.contains_key(user_id)
|
||||
}
|
||||
Err(error) => {
|
||||
warn!(?error, "Failed to deserialize the ignored user list event");
|
||||
false
|
||||
}
|
||||
},
|
||||
Ok(None) => false,
|
||||
Err(error) => {
|
||||
warn!(?error, "Could not get the ignored user list from the state store");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represent the `required_state` values sent by a sync request.
|
||||
@@ -1568,7 +1347,7 @@ mod tests {
|
||||
let room = client.get_room(room_id).expect("Room not found");
|
||||
assert_eq!(room.state(), RoomState::Invited);
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.expect("fetching display name failed"),
|
||||
room.compute_display_name().await.expect("fetching display name failed").into_inner(),
|
||||
RoomDisplayName::Calculated("Kyra".to_owned())
|
||||
);
|
||||
}
|
||||
@@ -1840,4 +1619,27 @@ mod tests {
|
||||
assert_let!(Some(ignored) = subscriber.next().await);
|
||||
assert!(ignored.is_empty());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_is_user_ignored() {
|
||||
let ignored_user_id = user_id!("@alice:example.org");
|
||||
let client = logged_in_base_client(None).await;
|
||||
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
let response = sync_builder
|
||||
.add_global_account_data_event(matrix_sdk_test::GlobalAccountDataTestEvent::Custom(
|
||||
json!({
|
||||
"content": {
|
||||
"ignored_users": {
|
||||
ignored_user_id: {}
|
||||
}
|
||||
},
|
||||
"type": "m.ignored_user_list",
|
||||
}),
|
||||
))
|
||||
.build_sync_response();
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
assert!(client.is_user_ignored(ignored_user_id).await);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,6 +263,18 @@ pub enum RawAnySyncOrStrippedTimelineEvent {
|
||||
Stripped(Raw<AnyStrippedStateEvent>),
|
||||
}
|
||||
|
||||
impl From<Raw<AnySyncTimelineEvent>> for RawAnySyncOrStrippedTimelineEvent {
|
||||
fn from(event: Raw<AnySyncTimelineEvent>) -> Self {
|
||||
Self::Sync(event)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Raw<AnyStrippedStateEvent>> for RawAnySyncOrStrippedTimelineEvent {
|
||||
fn from(event: Raw<AnyStrippedStateEvent>) -> Self {
|
||||
Self::Stripped(event)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper around both versions of any raw state event.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
|
||||
@@ -14,14 +14,17 @@
|
||||
|
||||
//! Trait and macro of integration tests for `EventCacheStore` implementations.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
deserialized_responses::{
|
||||
AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, TimelineEvent, TimelineEventKind,
|
||||
VerificationState,
|
||||
},
|
||||
linked_chunk::{lazy_loader, ChunkContent, ChunkIdentifier as CId, Position, Update},
|
||||
linked_chunk::{
|
||||
lazy_loader, ChunkContent, ChunkIdentifier as CId, LinkedChunkId, Position, Update,
|
||||
},
|
||||
};
|
||||
use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID};
|
||||
use ruma::{
|
||||
@@ -56,16 +59,16 @@ pub fn make_test_event_with_event_id(
|
||||
content: &str,
|
||||
event_id: Option<&EventId>,
|
||||
) -> TimelineEvent {
|
||||
let encryption_info = EncryptionInfo {
|
||||
let encryption_info = Arc::new(EncryptionInfo {
|
||||
sender: (*ALICE).into(),
|
||||
sender_device: None,
|
||||
algorithm_info: AlgorithmInfo::MegolmV1AesSha2 {
|
||||
curve25519_key: "1337".to_owned(),
|
||||
sender_claimed_keys: Default::default(),
|
||||
session_id: Some("mysessionid9".to_owned()),
|
||||
},
|
||||
verification_state: VerificationState::Verified,
|
||||
session_id: Some("mysessionid9".to_owned()),
|
||||
};
|
||||
});
|
||||
|
||||
let mut builder = EventFactory::new().text_msg(content).room(room_id).sender(*ALICE);
|
||||
if let Some(event_id) = event_id {
|
||||
@@ -73,14 +76,10 @@ pub fn make_test_event_with_event_id(
|
||||
}
|
||||
let event = builder.into_raw_timeline().cast();
|
||||
|
||||
TimelineEvent {
|
||||
kind: TimelineEventKind::Decrypted(DecryptedRoomEvent {
|
||||
event,
|
||||
encryption_info,
|
||||
unsigned_encryption_info: None,
|
||||
}),
|
||||
push_actions: Some(vec![Action::Notify]),
|
||||
}
|
||||
TimelineEvent::from_decrypted(
|
||||
DecryptedRoomEvent { event, encryption_info, unsigned_encryption_info: None },
|
||||
Some(vec![Action::Notify]),
|
||||
)
|
||||
}
|
||||
|
||||
/// Check that an event created with [`make_test_event`] contains the expected
|
||||
@@ -90,7 +89,7 @@ pub fn make_test_event_with_event_id(
|
||||
#[track_caller]
|
||||
pub fn check_test_event(event: &TimelineEvent, text: &str) {
|
||||
// Check push actions.
|
||||
let actions = event.push_actions.as_ref().unwrap();
|
||||
let actions = event.push_actions().unwrap();
|
||||
assert_eq!(actions.len(), 1);
|
||||
assert_matches!(&actions[0], Action::Notify);
|
||||
|
||||
@@ -114,8 +113,7 @@ pub fn check_test_event(event: &TimelineEvent, text: &str) {
|
||||
///
|
||||
/// This trait is not meant to be used directly, but will be used with the
|
||||
/// `event_cache_store_integration_tests!` macro.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait EventCacheStoreIntegrationTests {
|
||||
/// Test media content storage.
|
||||
async fn test_media_content(&self);
|
||||
@@ -136,7 +134,7 @@ pub trait EventCacheStoreIntegrationTests {
|
||||
async fn test_rebuild_empty_linked_chunk(&self);
|
||||
|
||||
/// Test that clear all the rooms' linked chunks works.
|
||||
async fn test_clear_all_rooms_chunks(&self);
|
||||
async fn test_clear_all_linked_chunks(&self);
|
||||
|
||||
/// Test that removing a room from storage empties all associated data.
|
||||
async fn test_remove_room(&self);
|
||||
@@ -154,8 +152,6 @@ pub trait EventCacheStoreIntegrationTests {
|
||||
async fn test_save_event(&self);
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
async fn test_media_content(&self) {
|
||||
let uri = mxc_uri!("mxc://localhost/media");
|
||||
@@ -343,9 +339,10 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
|
||||
async fn test_handle_updates_and_rebuild_linked_chunk(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
room_id,
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
// new chunk
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
@@ -377,10 +374,11 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
.unwrap();
|
||||
|
||||
// The linked chunk is correctly reloaded.
|
||||
let lc =
|
||||
lazy_loader::from_all_chunks::<3, _, _>(self.load_all_chunks(room_id).await.unwrap())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let lc = lazy_loader::from_all_chunks::<3, _, _>(
|
||||
self.load_all_chunks(linked_chunk_id).await.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let mut chunks = lc.chunks();
|
||||
|
||||
@@ -421,19 +419,20 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
|
||||
async fn test_linked_chunk_incremental_loading(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let event = |msg: &str| make_test_event(room_id, msg);
|
||||
|
||||
// Load the last chunk, but none exists yet.
|
||||
{
|
||||
let (last_chunk, chunk_identifier_generator) =
|
||||
self.load_last_chunk(room_id).await.unwrap();
|
||||
self.load_last_chunk(linked_chunk_id).await.unwrap();
|
||||
|
||||
assert!(last_chunk.is_none());
|
||||
assert_eq!(chunk_identifier_generator.current(), 0);
|
||||
}
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
room_id,
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
// new chunk for items
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
@@ -464,7 +463,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
// Load the last chunk.
|
||||
let mut linked_chunk = {
|
||||
let (last_chunk, chunk_identifier_generator) =
|
||||
self.load_last_chunk(room_id).await.unwrap();
|
||||
self.load_last_chunk(linked_chunk_id).await.unwrap();
|
||||
|
||||
assert_eq!(chunk_identifier_generator.current(), 2);
|
||||
|
||||
@@ -499,9 +498,9 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
{
|
||||
let first_chunk = linked_chunk.chunks().next().unwrap().identifier();
|
||||
let previous_chunk =
|
||||
self.load_previous_chunk(room_id, first_chunk).await.unwrap().unwrap();
|
||||
self.load_previous_chunk(linked_chunk_id, first_chunk).await.unwrap().unwrap();
|
||||
|
||||
let _ = lazy_loader::insert_new_first_chunk(&mut linked_chunk, previous_chunk).unwrap();
|
||||
lazy_loader::insert_new_first_chunk(&mut linked_chunk, previous_chunk).unwrap();
|
||||
|
||||
let mut rchunks = linked_chunk.rchunks();
|
||||
|
||||
@@ -536,9 +535,9 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
{
|
||||
let first_chunk = linked_chunk.chunks().next().unwrap().identifier();
|
||||
let previous_chunk =
|
||||
self.load_previous_chunk(room_id, first_chunk).await.unwrap().unwrap();
|
||||
self.load_previous_chunk(linked_chunk_id, first_chunk).await.unwrap().unwrap();
|
||||
|
||||
let _ = lazy_loader::insert_new_first_chunk(&mut linked_chunk, previous_chunk).unwrap();
|
||||
lazy_loader::insert_new_first_chunk(&mut linked_chunk, previous_chunk).unwrap();
|
||||
|
||||
let mut rchunks = linked_chunk.rchunks();
|
||||
|
||||
@@ -585,7 +584,8 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
// Load the previous chunk: there is none.
|
||||
{
|
||||
let first_chunk = linked_chunk.chunks().next().unwrap().identifier();
|
||||
let previous_chunk = self.load_previous_chunk(room_id, first_chunk).await.unwrap();
|
||||
let previous_chunk =
|
||||
self.load_previous_chunk(linked_chunk_id, first_chunk).await.unwrap();
|
||||
|
||||
assert!(previous_chunk.is_none());
|
||||
}
|
||||
@@ -637,19 +637,21 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
async fn test_rebuild_empty_linked_chunk(&self) {
|
||||
// When I rebuild a linked chunk from an empty store, it's empty.
|
||||
let linked_chunk = lazy_loader::from_all_chunks::<3, _, _>(
|
||||
self.load_all_chunks(&DEFAULT_TEST_ROOM_ID).await.unwrap(),
|
||||
self.load_all_chunks(LinkedChunkId::Room(&DEFAULT_TEST_ROOM_ID)).await.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(linked_chunk.is_none());
|
||||
}
|
||||
|
||||
async fn test_clear_all_rooms_chunks(&self) {
|
||||
async fn test_clear_all_linked_chunks(&self) {
|
||||
let r0 = room_id!("!r0:matrix.org");
|
||||
let linked_chunk_id0 = LinkedChunkId::Room(r0);
|
||||
let r1 = room_id!("!r1:matrix.org");
|
||||
let linked_chunk_id1 = LinkedChunkId::Room(r1);
|
||||
|
||||
// Add updates for the first room.
|
||||
self.handle_linked_chunk_updates(
|
||||
r0,
|
||||
linked_chunk_id0,
|
||||
vec![
|
||||
// new chunk
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
@@ -665,7 +667,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
|
||||
// Add updates for the second room.
|
||||
self.handle_linked_chunk_updates(
|
||||
r1,
|
||||
linked_chunk_id1,
|
||||
vec![
|
||||
// Empty items chunk.
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
@@ -689,32 +691,42 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
.unwrap();
|
||||
|
||||
// Sanity check: both linked chunks can be reloaded.
|
||||
assert!(lazy_loader::from_all_chunks::<3, _, _>(self.load_all_chunks(r0).await.unwrap())
|
||||
.unwrap()
|
||||
.is_some());
|
||||
assert!(lazy_loader::from_all_chunks::<3, _, _>(self.load_all_chunks(r1).await.unwrap())
|
||||
.unwrap()
|
||||
.is_some());
|
||||
assert!(lazy_loader::from_all_chunks::<3, _, _>(
|
||||
self.load_all_chunks(linked_chunk_id0).await.unwrap()
|
||||
)
|
||||
.unwrap()
|
||||
.is_some());
|
||||
assert!(lazy_loader::from_all_chunks::<3, _, _>(
|
||||
self.load_all_chunks(linked_chunk_id1).await.unwrap()
|
||||
)
|
||||
.unwrap()
|
||||
.is_some());
|
||||
|
||||
// Clear the chunks.
|
||||
self.clear_all_rooms_chunks().await.unwrap();
|
||||
self.clear_all_linked_chunks().await.unwrap();
|
||||
|
||||
// Both rooms now have no linked chunk.
|
||||
assert!(lazy_loader::from_all_chunks::<3, _, _>(self.load_all_chunks(r0).await.unwrap())
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert!(lazy_loader::from_all_chunks::<3, _, _>(self.load_all_chunks(r1).await.unwrap())
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert!(lazy_loader::from_all_chunks::<3, _, _>(
|
||||
self.load_all_chunks(linked_chunk_id0).await.unwrap()
|
||||
)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert!(lazy_loader::from_all_chunks::<3, _, _>(
|
||||
self.load_all_chunks(linked_chunk_id1).await.unwrap()
|
||||
)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
}
|
||||
|
||||
async fn test_remove_room(&self) {
|
||||
let r0 = room_id!("!r0:matrix.org");
|
||||
let linked_chunk_id0 = LinkedChunkId::Room(r0);
|
||||
let r1 = room_id!("!r1:matrix.org");
|
||||
let linked_chunk_id1 = LinkedChunkId::Room(r1);
|
||||
|
||||
// Add updates to the first room.
|
||||
self.handle_linked_chunk_updates(
|
||||
r0,
|
||||
linked_chunk_id0,
|
||||
vec![
|
||||
// new chunk
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
@@ -730,7 +742,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
|
||||
// Add updates to the second room.
|
||||
self.handle_linked_chunk_updates(
|
||||
r1,
|
||||
linked_chunk_id1,
|
||||
vec![
|
||||
// new chunk
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
@@ -748,17 +760,19 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
self.remove_room(r0).await.unwrap();
|
||||
|
||||
// Check that r0 doesn't have a linked chunk anymore.
|
||||
let r0_linked_chunk = self.load_all_chunks(r0).await.unwrap();
|
||||
let r0_linked_chunk = self.load_all_chunks(linked_chunk_id0).await.unwrap();
|
||||
assert!(r0_linked_chunk.is_empty());
|
||||
|
||||
// Check that r1 is unaffected.
|
||||
let r1_linked_chunk = self.load_all_chunks(r1).await.unwrap();
|
||||
let r1_linked_chunk = self.load_all_chunks(linked_chunk_id1).await.unwrap();
|
||||
assert!(!r1_linked_chunk.is_empty());
|
||||
}
|
||||
|
||||
async fn test_filter_duplicated_events(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let another_room_id = room_id!("!r1:matrix.org");
|
||||
let another_linked_chunk_id = LinkedChunkId::Room(another_room_id);
|
||||
let event = |msg: &str| make_test_event(room_id, msg);
|
||||
|
||||
let event_comte = event("comté");
|
||||
@@ -770,7 +784,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
let event_mont_dor = event("mont d'or");
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
room_id,
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
@@ -796,7 +810,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
// Add other events in another room, to ensure filtering take the `room_id` into
|
||||
// account.
|
||||
self.handle_linked_chunk_updates(
|
||||
another_room_id,
|
||||
another_linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
@@ -810,7 +824,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
|
||||
let duplicated_events = self
|
||||
.filter_duplicated_events(
|
||||
room_id,
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
event_comte.event_id().unwrap().to_owned(),
|
||||
event_raclette.event_id().unwrap().to_owned(),
|
||||
@@ -841,6 +855,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
async fn test_find_event(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let another_room_id = room_id!("!r1:matrix.org");
|
||||
let another_linked_chunk_id = LinkedChunkId::Room(another_room_id);
|
||||
let event = |msg: &str| make_test_event(room_id, msg);
|
||||
|
||||
let event_comte = event("comté");
|
||||
@@ -848,7 +863,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
|
||||
// Add one event in one room.
|
||||
self.handle_linked_chunk_updates(
|
||||
room_id,
|
||||
LinkedChunkId::Room(room_id),
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
@@ -862,7 +877,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
|
||||
// Add another event in another room.
|
||||
self.handle_linked_chunk_updates(
|
||||
another_room_id,
|
||||
another_linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
@@ -891,7 +906,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
.is_none());
|
||||
|
||||
// Clearing the rooms also clears the event's storage.
|
||||
self.clear_all_rooms_chunks().await.expect("failed to clear all rooms chunks");
|
||||
self.clear_all_linked_chunks().await.expect("failed to clear all rooms chunks");
|
||||
assert!(self
|
||||
.find_event(room_id, event_comte.event_id().unwrap().as_ref())
|
||||
.await
|
||||
@@ -1091,10 +1106,10 @@ macro_rules! event_cache_store_integration_tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_clear_all_rooms_chunks() {
|
||||
async fn test_clear_all_linked_chunks() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_clear_all_rooms_chunks().await;
|
||||
event_cache_store.test_clear_all_linked_chunks().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
@@ -1141,7 +1156,7 @@ macro_rules! event_cache_store_integration_tests {
|
||||
#[macro_export]
|
||||
macro_rules! event_cache_store_integration_tests_time {
|
||||
() => {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
mod event_cache_store_integration_tests_time {
|
||||
use std::time::Duration;
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
//! Trait and macro of integration tests for `EventCacheStoreMedia`
|
||||
//! implementations.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use ruma::{
|
||||
events::room::MediaSource,
|
||||
mxc_uri, owned_mxc_uri,
|
||||
@@ -31,8 +30,7 @@ use crate::media::{MediaFormat, MediaRequestParameters};
|
||||
///
|
||||
/// This trait is not meant to be used directly, but will be used with the
|
||||
/// `event_cache_store_media_integration_tests!` macro.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait EventCacheStoreMediaIntegrationTests {
|
||||
/// Test media retention policy storage.
|
||||
async fn test_store_media_retention_policy(&self);
|
||||
@@ -58,8 +56,6 @@ pub trait EventCacheStoreMediaIntegrationTests {
|
||||
async fn test_store_last_media_cleanup_time(&self);
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<Store> EventCacheStoreMediaIntegrationTests for Store
|
||||
where
|
||||
Store: EventCacheStoreMedia + std::fmt::Debug,
|
||||
|
||||
@@ -356,8 +356,8 @@ where
|
||||
/// [`MediaRetentionPolicy`] by wrapping this in a [`MediaService`], and to
|
||||
/// simplify the implementation of tests by being able to have complete control
|
||||
/// over the `SystemTime`s provided to the store.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
pub trait EventCacheStoreMedia: AsyncTraitDeps + Clone {
|
||||
/// The error type used by this media cache store.
|
||||
type Error: fmt::Debug + fmt::Display + Into<EventCacheStoreError>;
|
||||
@@ -630,8 +630,8 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
impl EventCacheStoreMedia for MockEventCacheStoreMedia {
|
||||
type Error = MockEventCacheStoreMediaError;
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ use std::{
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
linked_chunk::{
|
||||
relational::RelationalLinkedChunk, ChunkIdentifier, ChunkIdentifierGenerator, Position,
|
||||
RawChunk, Update,
|
||||
relational::RelationalLinkedChunk, ChunkIdentifier, ChunkIdentifierGenerator,
|
||||
LinkedChunkId, Position, RawChunk, Update,
|
||||
},
|
||||
ring_buffer::RingBuffer,
|
||||
store_locks::memory_store_helper::try_take_leased_lock,
|
||||
@@ -110,8 +110,8 @@ impl MemoryStore {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
impl EventCacheStore for MemoryStore {
|
||||
type Error = EventCacheStoreError;
|
||||
|
||||
@@ -128,57 +128,57 @@ impl EventCacheStore for MemoryStore {
|
||||
|
||||
async fn handle_linked_chunk_updates(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
updates: Vec<Update<Event, Gap>>,
|
||||
) -> Result<(), Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
inner.events.apply_updates(room_id, updates);
|
||||
inner.events.apply_updates(linked_chunk_id, updates);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_all_chunks(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
inner
|
||||
.events
|
||||
.load_all_chunks(room_id)
|
||||
.load_all_chunks(linked_chunk_id)
|
||||
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
|
||||
}
|
||||
|
||||
async fn load_last_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<(Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator), Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
inner
|
||||
.events
|
||||
.load_last_chunk(room_id)
|
||||
.load_last_chunk(linked_chunk_id)
|
||||
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
|
||||
}
|
||||
|
||||
async fn load_previous_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
before_chunk_identifier: ChunkIdentifier,
|
||||
) -> Result<Option<RawChunk<Event, Gap>>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
inner
|
||||
.events
|
||||
.load_previous_chunk(room_id, before_chunk_identifier)
|
||||
.load_previous_chunk(linked_chunk_id, before_chunk_identifier)
|
||||
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
|
||||
}
|
||||
|
||||
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error> {
|
||||
async fn clear_all_linked_chunks(&self) -> Result<(), Self::Error> {
|
||||
self.inner.write().unwrap().events.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn filter_duplicated_events(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
mut events: Vec<OwnedEventId>,
|
||||
) -> Result<Vec<(OwnedEventId, Position)>, Self::Error> {
|
||||
// Collect all duplicated events.
|
||||
@@ -186,7 +186,7 @@ impl EventCacheStore for MemoryStore {
|
||||
|
||||
let mut duplicated_events = Vec::new();
|
||||
|
||||
for (event, position) in inner.events.unordered_room_items(room_id) {
|
||||
for (event, position) in inner.events.unordered_linked_chunk_items(linked_chunk_id) {
|
||||
// If `events` is empty, we can short-circuit.
|
||||
if events.is_empty() {
|
||||
break;
|
||||
@@ -212,8 +212,9 @@ impl EventCacheStore for MemoryStore {
|
||||
) -> Result<Option<Event>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
let event = inner.events.items().find_map(|(event, this_room_id)| {
|
||||
(room_id == this_room_id && event.event_id()? == event_id).then_some(event.clone())
|
||||
let event = inner.events.items().find_map(|(event, this_linked_chunk_id)| {
|
||||
(room_id == this_linked_chunk_id.room_id() && event.event_id()? == event_id)
|
||||
.then_some(event.clone())
|
||||
});
|
||||
|
||||
Ok(event)
|
||||
@@ -232,9 +233,9 @@ impl EventCacheStore for MemoryStore {
|
||||
let related_events = inner
|
||||
.events
|
||||
.items()
|
||||
.filter_map(|(event, this_room_id)| {
|
||||
.filter_map(|(event, this_linked_chunk_id)| {
|
||||
// Must be in the same room.
|
||||
if room_id != this_room_id {
|
||||
if room_id != this_linked_chunk_id.room_id() {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -364,8 +365,8 @@ impl EventCacheStore for MemoryStore {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
impl EventCacheStoreMedia for MemoryStore {
|
||||
type Error = EventCacheStoreError;
|
||||
|
||||
|
||||
@@ -126,8 +126,14 @@ impl Deref for EventCacheStoreLockGuard<'_> {
|
||||
pub enum EventCacheStoreError {
|
||||
/// An error happened in the underlying database backend.
|
||||
#[error(transparent)]
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
Backend(Box<dyn std::error::Error + Send + Sync>),
|
||||
|
||||
/// An error happened in the underlying database backend.
|
||||
#[error(transparent)]
|
||||
#[cfg(target_family = "wasm")]
|
||||
Backend(Box<dyn std::error::Error>),
|
||||
|
||||
/// The store is locked with a passphrase and an incorrect passphrase
|
||||
/// was given.
|
||||
#[error("The event cache store failed to be unlocked")]
|
||||
@@ -169,12 +175,25 @@ impl EventCacheStoreError {
|
||||
///
|
||||
/// Shorthand for `EventCacheStoreError::Backend(Box::new(error))`.
|
||||
#[inline]
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub fn backend<E>(error: E) -> Self
|
||||
where
|
||||
E: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
Self::Backend(Box::new(error))
|
||||
}
|
||||
|
||||
/// Create a new [`Backend`][Self::Backend] error.
|
||||
///
|
||||
/// Shorthand for `EventCacheStoreError::Backend(Box::new(error))`.
|
||||
#[inline]
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub fn backend<E>(error: E) -> Self
|
||||
where
|
||||
E: std::error::Error + 'static,
|
||||
{
|
||||
Self::Backend(Box::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
/// An `EventCacheStore` specific result type.
|
||||
@@ -185,8 +204,6 @@ pub type Result<T, E = EventCacheStoreError> = std::result::Result<T, E>;
|
||||
#[derive(Clone, Debug)]
|
||||
struct LockableEventCacheStore(Arc<DynEventCacheStore>);
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
|
||||
impl BackingStore for LockableEventCacheStore {
|
||||
type LockError = EventCacheStoreError;
|
||||
|
||||
|
||||
@@ -16,7 +16,9 @@ use std::{fmt, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
linked_chunk::{ChunkIdentifier, ChunkIdentifierGenerator, Position, RawChunk, Update},
|
||||
linked_chunk::{
|
||||
ChunkIdentifier, ChunkIdentifierGenerator, LinkedChunkId, Position, RawChunk, Update,
|
||||
},
|
||||
AsyncTraitDeps,
|
||||
};
|
||||
use ruma::{events::relation::RelationType, EventId, MxcUri, OwnedEventId, RoomId};
|
||||
@@ -37,8 +39,8 @@ pub const DEFAULT_CHUNK_CAPACITY: usize = 128;
|
||||
|
||||
/// An abstract trait that can be used to implement different store backends
|
||||
/// for the event cache of the SDK.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
pub trait EventCacheStore: AsyncTraitDeps {
|
||||
/// The error type used by this event cache store.
|
||||
type Error: fmt::Debug + Into<EventCacheStoreError>;
|
||||
@@ -56,7 +58,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
/// in-memory. This method aims at forwarding this update inside this store.
|
||||
async fn handle_linked_chunk_updates(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
updates: Vec<Update<Event, Gap>>,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
@@ -64,7 +66,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
async fn remove_room(&self, room_id: &RoomId) -> Result<(), Self::Error> {
|
||||
// Right now, this means removing all the linked chunk. If implementations
|
||||
// override this behavior, they should *also* include this code.
|
||||
self.handle_linked_chunk_updates(room_id, vec![Update::Clear]).await
|
||||
self.handle_linked_chunk_updates(LinkedChunkId::Room(room_id), vec![Update::Clear]).await
|
||||
}
|
||||
|
||||
/// Return all the raw components of a linked chunk, so the caller may
|
||||
@@ -72,7 +74,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
#[doc(hidden)]
|
||||
async fn load_all_chunks(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error>;
|
||||
|
||||
/// Load the last chunk of the `LinkedChunk` holding all events of the room
|
||||
@@ -81,7 +83,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
/// This is used to iteratively load events for the `EventCache`.
|
||||
async fn load_last_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<(Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator), Self::Error>;
|
||||
|
||||
/// Load the chunk before the chunk identified by `before_chunk_identifier`
|
||||
@@ -91,7 +93,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
/// This is used to iteratively load events for the `EventCache`.
|
||||
async fn load_previous_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
before_chunk_identifier: ChunkIdentifier,
|
||||
) -> Result<Option<RawChunk<Event, Gap>>, Self::Error>;
|
||||
|
||||
@@ -105,17 +107,17 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
/// ⚠ This is meant only for super specific use cases, where there shouldn't
|
||||
/// be any live in-memory linked chunks. In general, prefer using
|
||||
/// `EventCache::clear_all_rooms()` from the common SDK crate.
|
||||
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error>;
|
||||
async fn clear_all_linked_chunks(&self) -> Result<(), Self::Error>;
|
||||
|
||||
/// Given a set of event IDs, return the duplicated events along with their
|
||||
/// position if there are any.
|
||||
async fn filter_duplicated_events(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
events: Vec<OwnedEventId>,
|
||||
) -> Result<Vec<(OwnedEventId, Position)>, Self::Error>;
|
||||
|
||||
/// Find an event by its ID.
|
||||
/// Find an event by its ID in a room.
|
||||
async fn find_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
@@ -124,6 +126,11 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
|
||||
/// Find all the events that relate to a given event.
|
||||
///
|
||||
/// Note: it doesn't process relations recursively: for instance, if
|
||||
/// requesting only thread events, it will NOT return the aggregated
|
||||
/// events affecting the returned events. It is the responsibility of
|
||||
/// the caller to do so, if needed.
|
||||
///
|
||||
/// An additional filter can be provided to only retrieve related events for
|
||||
/// a certain relationship.
|
||||
async fn find_event_relations(
|
||||
@@ -277,8 +284,8 @@ impl<T: fmt::Debug> fmt::Debug for EraseEventCacheStoreError<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
|
||||
type Error = EventCacheStoreError;
|
||||
|
||||
@@ -293,44 +300,47 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
|
||||
|
||||
async fn handle_linked_chunk_updates(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
updates: Vec<Update<Event, Gap>>,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.handle_linked_chunk_updates(room_id, updates).await.map_err(Into::into)
|
||||
self.0.handle_linked_chunk_updates(linked_chunk_id, updates).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn load_all_chunks(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
|
||||
self.0.load_all_chunks(room_id).await.map_err(Into::into)
|
||||
self.0.load_all_chunks(linked_chunk_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn load_last_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<(Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator), Self::Error> {
|
||||
self.0.load_last_chunk(room_id).await.map_err(Into::into)
|
||||
self.0.load_last_chunk(linked_chunk_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn load_previous_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
before_chunk_identifier: ChunkIdentifier,
|
||||
) -> Result<Option<RawChunk<Event, Gap>>, Self::Error> {
|
||||
self.0.load_previous_chunk(room_id, before_chunk_identifier).await.map_err(Into::into)
|
||||
self.0
|
||||
.load_previous_chunk(linked_chunk_id, before_chunk_identifier)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error> {
|
||||
self.0.clear_all_rooms_chunks().await.map_err(Into::into)
|
||||
async fn clear_all_linked_chunks(&self) -> Result<(), Self::Error> {
|
||||
self.0.clear_all_linked_chunks().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn filter_duplicated_events(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
events: Vec<OwnedEventId>,
|
||||
) -> Result<Vec<(OwnedEventId, Position)>, Self::Error> {
|
||||
self.0.filter_duplicated_events(room_id, events).await.map_err(Into::into)
|
||||
self.0.filter_duplicated_events(linked_chunk_id, events).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn find_event(
|
||||
|
||||
@@ -629,7 +629,7 @@ mod tests {
|
||||
latest_event: LatestEvent,
|
||||
}
|
||||
|
||||
let event = TimelineEvent::new(
|
||||
let event = TimelineEvent::from_plaintext(
|
||||
Raw::from_json_string(json!({ "event_id": "$1" }).to_string()).unwrap(),
|
||||
);
|
||||
|
||||
@@ -654,8 +654,9 @@ mod tests {
|
||||
"event_id": "$1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"thread_summary": "None",
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))]
|
||||
#![cfg_attr(target_family = "wasm", allow(clippy::arc_with_non_send_sync))]
|
||||
#![warn(missing_docs, missing_debug_implementations)]
|
||||
|
||||
pub use matrix_sdk_common::*;
|
||||
@@ -34,10 +34,9 @@ pub mod latest_event;
|
||||
pub mod media;
|
||||
pub mod notification_settings;
|
||||
mod response_processors;
|
||||
mod rooms;
|
||||
mod room;
|
||||
|
||||
pub mod read_receipts;
|
||||
pub use read_receipts::PreviousEventsProvider;
|
||||
pub mod sliding_sync;
|
||||
|
||||
pub mod store;
|
||||
@@ -55,10 +54,10 @@ pub use http;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub use matrix_sdk_crypto as crypto;
|
||||
pub use once_cell;
|
||||
pub use rooms::{
|
||||
apply_redaction, EncryptionState, Room, RoomCreateWithCreatorEventContent, RoomDisplayName,
|
||||
RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember,
|
||||
RoomMembersUpdate, RoomMemberships, RoomState, RoomStateFilter,
|
||||
pub use room::{
|
||||
apply_redaction, EncryptionState, PredecessorRoom, Room, RoomCreateWithCreatorEventContent,
|
||||
RoomDisplayName, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons,
|
||||
RoomMember, RoomMembersUpdate, RoomMemberships, RoomState, RoomStateFilter, SuccessorRoom,
|
||||
};
|
||||
pub use store::{
|
||||
ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey,
|
||||
|
||||
@@ -122,7 +122,6 @@ use std::{
|
||||
num::NonZeroUsize,
|
||||
};
|
||||
|
||||
use eyeball_im::Vector;
|
||||
use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer};
|
||||
use ruma::{
|
||||
events::{
|
||||
@@ -210,7 +209,7 @@ impl RoomReadReceipts {
|
||||
let mut has_notify = false;
|
||||
let mut has_mention = false;
|
||||
|
||||
let Some(actions) = event.push_actions.as_ref() else {
|
||||
let Some(actions) = event.push_actions() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -268,19 +267,6 @@ impl RoomReadReceipts {
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider for timeline events prior to the current sync.
|
||||
pub trait PreviousEventsProvider: Send + Sync {
|
||||
/// Returns the list of known timeline events, in sync order, for the given
|
||||
/// room.
|
||||
fn for_room(&self, room_id: &RoomId) -> Vector<TimelineEvent>;
|
||||
}
|
||||
|
||||
impl PreviousEventsProvider for () {
|
||||
fn for_room(&self, _: &RoomId) -> Vector<TimelineEvent> {
|
||||
Vector::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Small helper to select the "best" receipt (that with the biggest sync
|
||||
/// order).
|
||||
struct ReceiptSelector {
|
||||
@@ -294,10 +280,7 @@ struct ReceiptSelector {
|
||||
}
|
||||
|
||||
impl ReceiptSelector {
|
||||
fn new(
|
||||
all_events: &Vector<TimelineEvent>,
|
||||
latest_active_receipt_event: Option<&EventId>,
|
||||
) -> Self {
|
||||
fn new(all_events: &[TimelineEvent], latest_active_receipt_event: Option<&EventId>) -> Self {
|
||||
let event_id_to_pos = Self::create_sync_index(all_events.iter());
|
||||
|
||||
let best_pos =
|
||||
@@ -457,23 +440,22 @@ pub(crate) fn compute_unread_counts(
|
||||
user_id: &UserId,
|
||||
room_id: &RoomId,
|
||||
receipt_event: Option<&ReceiptEventContent>,
|
||||
previous_events: Vector<TimelineEvent>,
|
||||
mut previous_events: Vec<TimelineEvent>,
|
||||
new_events: &[TimelineEvent],
|
||||
read_receipts: &mut RoomReadReceipts,
|
||||
) {
|
||||
debug!(?read_receipts, "Starting.");
|
||||
debug!(?read_receipts, "Starting");
|
||||
|
||||
let all_events = if events_intersects(previous_events.iter(), new_events) {
|
||||
// The previous and new events sets can intersect, for instance if we restored
|
||||
// previous events from the disk cache, or a timeline was limited. This
|
||||
// means the old events will be cleared, because we don't reconcile
|
||||
// timelines in sliding sync (yet). As a result, forget
|
||||
// timelines in the event cache (yet). As a result, forget
|
||||
// about the previous events.
|
||||
Vector::from_iter(new_events.iter().cloned())
|
||||
new_events.to_owned()
|
||||
} else {
|
||||
let mut all_events = previous_events;
|
||||
all_events.extend(new_events.iter().cloned());
|
||||
all_events
|
||||
previous_events.extend(new_events.iter().cloned());
|
||||
previous_events
|
||||
};
|
||||
|
||||
let new_receipt = {
|
||||
@@ -481,6 +463,7 @@ pub(crate) fn compute_unread_counts(
|
||||
&all_events,
|
||||
read_receipts.latest_active.as_ref().map(|receipt| &*receipt.event_id),
|
||||
);
|
||||
|
||||
selector.try_match_implicit(user_id, new_events);
|
||||
selector.handle_pending_receipts(&mut read_receipts.pending);
|
||||
if let Some(receipt_event) = receipt_event {
|
||||
@@ -622,7 +605,6 @@ fn marks_as_unread(event: &Raw<AnySyncTimelineEvent>, user_id: &UserId) -> bool
|
||||
mod tests {
|
||||
use std::{num::NonZeroUsize, ops::Not as _};
|
||||
|
||||
use eyeball_im::Vector;
|
||||
use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer};
|
||||
use matrix_sdk_test::event_factory::EventFactory;
|
||||
use ruma::{
|
||||
@@ -729,7 +711,7 @@ mod tests {
|
||||
.sender(user_id)
|
||||
.event_id(event_id!("$ida"))
|
||||
.into_event();
|
||||
ev.push_actions = Some(push_actions);
|
||||
ev.set_push_actions(push_actions);
|
||||
ev
|
||||
}
|
||||
|
||||
@@ -915,7 +897,7 @@ mod tests {
|
||||
let room_id = room_id!("!room:example.org");
|
||||
let receipt_event_id = event_id!("$1");
|
||||
|
||||
let mut previous_events = Vector::new();
|
||||
let mut previous_events = Vec::new();
|
||||
|
||||
let f = EventFactory::new();
|
||||
let ev1 = f.text_msg("A").sender(other_user_id).event_id(receipt_event_id).into_event();
|
||||
@@ -940,8 +922,8 @@ mod tests {
|
||||
assert_eq!(read_receipts.num_unread, 1);
|
||||
|
||||
// Receive the same receipt event, with a new sync event.
|
||||
previous_events.push_back(ev1);
|
||||
previous_events.push_back(ev2);
|
||||
previous_events.push(ev1);
|
||||
previous_events.push(ev2);
|
||||
|
||||
let new_event =
|
||||
f.text_msg("A").sender(other_user_id).event_id(event_id!("$3")).into_event();
|
||||
@@ -958,7 +940,7 @@ mod tests {
|
||||
assert_eq!(read_receipts.num_unread, 2);
|
||||
}
|
||||
|
||||
fn make_test_events(user_id: &UserId) -> Vector<TimelineEvent> {
|
||||
fn make_test_events(user_id: &UserId) -> Vec<TimelineEvent> {
|
||||
let f = EventFactory::new().sender(user_id);
|
||||
let ev1 = f.text_msg("With the lights out, it's less dangerous").event_id(event_id!("$1"));
|
||||
let ev2 = f.text_msg("Here we are now, entertain us").event_id(event_id!("$2"));
|
||||
@@ -976,7 +958,7 @@ mod tests {
|
||||
let room_id = room_id!("!room:example.org");
|
||||
|
||||
let all_events = make_test_events(user_id!("@bob:example.org"));
|
||||
let head_events: Vector<_> = all_events.iter().take(2).cloned().collect();
|
||||
let head_events: Vec<_> = all_events.iter().take(2).cloned().collect();
|
||||
let tail_events: Vec<_> = all_events.iter().skip(2).cloned().collect();
|
||||
|
||||
// Given a receipt event marking events 1-3 as read using a combination of
|
||||
@@ -1165,7 +1147,7 @@ mod tests {
|
||||
|
||||
{
|
||||
// No initial active receipt, so the first receipt we get *will* win.
|
||||
let mut selector = ReceiptSelector::new(&vec![].into(), None);
|
||||
let mut selector = ReceiptSelector::new(&[], None);
|
||||
selector.try_select_later(event_id!("$1"), 0);
|
||||
let best_receipt = selector.select();
|
||||
assert_eq!(best_receipt.unwrap().event_id, event_id!("$1"));
|
||||
@@ -1203,11 +1185,11 @@ mod tests {
|
||||
let f = EventFactory::new().sender(sender);
|
||||
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
|
||||
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
|
||||
let events: Vector<_> = vec![ev1, ev2].into();
|
||||
let events = &[ev1, ev2][..];
|
||||
|
||||
{
|
||||
// No pending receipt => no better receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, None);
|
||||
let mut selector = ReceiptSelector::new(events, None);
|
||||
|
||||
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
|
||||
selector.handle_pending_receipts(&mut pending);
|
||||
@@ -1221,7 +1203,7 @@ mod tests {
|
||||
{
|
||||
// No pending receipt, and there was an active last receipt => no better
|
||||
// receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$1")));
|
||||
let mut selector = ReceiptSelector::new(events, Some(event_id!("$1")));
|
||||
|
||||
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
|
||||
selector.handle_pending_receipts(&mut pending);
|
||||
@@ -1239,11 +1221,11 @@ mod tests {
|
||||
let f = EventFactory::new().sender(sender);
|
||||
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
|
||||
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
|
||||
let events: Vector<_> = vec![ev1, ev2].into();
|
||||
let events = &[ev1, ev2][..];
|
||||
|
||||
{
|
||||
// A pending receipt for an event that is still missing => no better receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, None);
|
||||
let mut selector = ReceiptSelector::new(events, None);
|
||||
|
||||
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
|
||||
pending.push(owned_event_id!("$3"));
|
||||
@@ -1257,7 +1239,7 @@ mod tests {
|
||||
|
||||
{
|
||||
// Ditto but there was an active receipt => no better receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$1")));
|
||||
let mut selector = ReceiptSelector::new(events, Some(event_id!("$1")));
|
||||
|
||||
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
|
||||
pending.push(owned_event_id!("$3"));
|
||||
@@ -1276,11 +1258,11 @@ mod tests {
|
||||
let f = EventFactory::new().sender(sender);
|
||||
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
|
||||
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
|
||||
let events: Vector<_> = vec![ev1, ev2].into();
|
||||
let events = &[ev1, ev2][..];
|
||||
|
||||
{
|
||||
// A pending receipt for an event that is present => better receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, None);
|
||||
let mut selector = ReceiptSelector::new(events, None);
|
||||
|
||||
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
|
||||
pending.push(owned_event_id!("$2"));
|
||||
@@ -1296,7 +1278,7 @@ mod tests {
|
||||
|
||||
{
|
||||
// Mixed found and not found receipt => better receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, None);
|
||||
let mut selector = ReceiptSelector::new(events, None);
|
||||
|
||||
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
|
||||
pending.push(owned_event_id!("$1"));
|
||||
@@ -1318,12 +1300,12 @@ mod tests {
|
||||
let f = EventFactory::new().sender(sender);
|
||||
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
|
||||
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
|
||||
let events: Vector<_> = vec![ev1, ev2].into();
|
||||
let events = &[ev1, ev2][..];
|
||||
|
||||
{
|
||||
// Same, and there was an initial receipt that was less good than the one we
|
||||
// selected => better receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$1")));
|
||||
let mut selector = ReceiptSelector::new(events, Some(event_id!("$1")));
|
||||
|
||||
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
|
||||
pending.push(owned_event_id!("$2"));
|
||||
@@ -1339,7 +1321,7 @@ mod tests {
|
||||
|
||||
{
|
||||
// Same, but the previous receipt was better => no better receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$2")));
|
||||
let mut selector = ReceiptSelector::new(events, Some(event_id!("$2")));
|
||||
|
||||
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
|
||||
pending.push(owned_event_id!("$1"));
|
||||
@@ -1484,26 +1466,26 @@ mod tests {
|
||||
// When the selector sees only other users' events,
|
||||
let mut selector = ReceiptSelector::new(&events, None);
|
||||
// And I search for my implicit read receipt,
|
||||
selector.try_match_implicit(&myself, &events.iter().cloned().collect::<Vec<_>>());
|
||||
selector.try_match_implicit(&myself, &events);
|
||||
// Then I don't find any.
|
||||
let best_receipt = selector.select();
|
||||
assert!(best_receipt.is_none());
|
||||
|
||||
// Now, if there are events I've written too...
|
||||
let f = EventFactory::new();
|
||||
events.push_back(
|
||||
events.push(
|
||||
f.text_msg("A mulatto, an albino")
|
||||
.sender(&myself)
|
||||
.event_id(event_id!("$6"))
|
||||
.into_event(),
|
||||
);
|
||||
events.push_back(
|
||||
events.push(
|
||||
f.text_msg("A mosquito, my libido").sender(bob).event_id(event_id!("$7")).into_event(),
|
||||
);
|
||||
|
||||
let mut selector = ReceiptSelector::new(&events, None);
|
||||
// And I search for my implicit read receipt,
|
||||
selector.try_match_implicit(&myself, &events.iter().cloned().collect::<Vec<_>>());
|
||||
selector.try_match_implicit(&myself, &events);
|
||||
// Then my last sent event counts as a read receipt.
|
||||
let best_receipt = selector.select();
|
||||
assert_eq!(best_receipt.unwrap().event_id, event_id!("$6"));
|
||||
@@ -1520,7 +1502,7 @@ mod tests {
|
||||
|
||||
// One by me,
|
||||
let f = EventFactory::new();
|
||||
events.push_back(
|
||||
events.push(
|
||||
f.text_msg("A mulatto, an albino")
|
||||
.sender(user_id)
|
||||
.event_id(event_id!("$6"))
|
||||
@@ -1528,10 +1510,10 @@ mod tests {
|
||||
);
|
||||
|
||||
// And others by Bob,
|
||||
events.push_back(
|
||||
events.push(
|
||||
f.text_msg("A mosquito, my libido").sender(bob).event_id(event_id!("$7")).into_event(),
|
||||
);
|
||||
events.push_back(
|
||||
events.push(
|
||||
f.text_msg("A denial, a denial").sender(bob).event_id(event_id!("$8")).into_event(),
|
||||
);
|
||||
|
||||
@@ -1551,7 +1533,7 @@ mod tests {
|
||||
user_id,
|
||||
room_id,
|
||||
Some(&receipt_event),
|
||||
Vector::new(),
|
||||
Vec::new(),
|
||||
&events,
|
||||
&mut read_receipts,
|
||||
);
|
||||
|
||||
@@ -20,7 +20,10 @@ use ruma::{
|
||||
use tracing::{instrument, warn};
|
||||
|
||||
use super::super::{Context, RoomInfoNotableUpdates};
|
||||
use crate::{store::BaseStateStore, RoomInfo, RoomInfoNotableUpdateReasons, StateChanges};
|
||||
use crate::{
|
||||
room::AccountDataSource, store::BaseStateStore, RoomInfo, RoomInfoNotableUpdateReasons,
|
||||
StateChanges,
|
||||
};
|
||||
|
||||
#[instrument(skip_all, fields(?room_id))]
|
||||
pub async fn for_room(
|
||||
@@ -49,6 +52,7 @@ pub async fn for_room(
|
||||
on_unread_marker(
|
||||
room_id,
|
||||
&event.content,
|
||||
AccountDataSource::Stable,
|
||||
room_info,
|
||||
&mut context.room_info_notable_updates,
|
||||
);
|
||||
@@ -64,6 +68,7 @@ pub async fn for_room(
|
||||
on_unread_marker(
|
||||
room_id,
|
||||
&event.content.0,
|
||||
AccountDataSource::Unstable,
|
||||
room_info,
|
||||
&mut context.room_info_notable_updates,
|
||||
);
|
||||
@@ -127,9 +132,17 @@ fn on_room_info<F>(
|
||||
fn on_unread_marker(
|
||||
room_id: &RoomId,
|
||||
content: &MarkedUnreadEventContent,
|
||||
source: AccountDataSource,
|
||||
room_info: &mut RoomInfo,
|
||||
room_info_notable_updates: &mut RoomInfoNotableUpdates,
|
||||
) {
|
||||
if room_info.base_info.is_marked_unread_source == AccountDataSource::Stable
|
||||
&& source != AccountDataSource::Stable
|
||||
{
|
||||
// Ignore the unstable source if a stable source was used previously.
|
||||
return;
|
||||
}
|
||||
|
||||
if room_info.base_info.is_marked_unread != content.unread {
|
||||
// Notify the room list about a manual read marker change if the
|
||||
// value's changed.
|
||||
@@ -140,4 +153,5 @@ fn on_unread_marker(
|
||||
}
|
||||
|
||||
room_info.base_info.is_marked_unread = content.unread;
|
||||
room_info.base_info.is_marked_unread_source = source;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,16 @@ use crate::{
|
||||
Result,
|
||||
};
|
||||
|
||||
/// Save the [`StateChanges`] from the [`Context`] inside the [`BaseStateStore`]
|
||||
/// only! The changes aren't applied on the in-memory rooms.
|
||||
#[instrument(skip_all)]
|
||||
pub async fn save_only(context: Context, state_store: &BaseStateStore) -> Result<()> {
|
||||
save_changes(&context, state_store, None).await?;
|
||||
broadcast_room_info_notable_updates(&context, state_store);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save the [`StateChanges`] from the [`Context`] inside the
|
||||
/// [`BaseStateStore`], and apply them on the in-memory rooms.
|
||||
#[instrument(skip_all)]
|
||||
@@ -39,28 +49,36 @@ pub async fn save_and_apply(
|
||||
let previous_ignored_user_list =
|
||||
state_store.get_account_data_event_static().await.ok().flatten();
|
||||
|
||||
state_store.save_changes(&context.state_changes).await?;
|
||||
|
||||
if let Some(sync_token) = sync_token {
|
||||
*state_store.sync_token.write().await = Some(sync_token);
|
||||
}
|
||||
apply_changes(context, state_store, ignore_user_list_changes, previous_ignored_user_list);
|
||||
save_changes(&context, state_store, sync_token).await?;
|
||||
apply_changes(&context, ignore_user_list_changes, previous_ignored_user_list);
|
||||
broadcast_room_info_notable_updates(&context, state_store);
|
||||
|
||||
trace!("applied changes");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_changes(
|
||||
context: Context,
|
||||
async fn save_changes(
|
||||
context: &Context,
|
||||
state_store: &BaseStateStore,
|
||||
sync_token: Option<String>,
|
||||
) -> Result<()> {
|
||||
state_store.save_changes(&context.state_changes).await?;
|
||||
|
||||
if let Some(sync_token) = sync_token {
|
||||
*state_store.sync_token.write().await = Some(sync_token);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_changes(
|
||||
context: &Context,
|
||||
ignore_user_list_changes: &SharedObservable<Vec<String>>,
|
||||
previous_ignored_user_list: Option<Raw<IgnoredUserListEvent>>,
|
||||
) {
|
||||
let (state_changes, room_info_notable_updates) = context.into_parts();
|
||||
|
||||
if let Some(event) =
|
||||
state_changes.account_data.get(&GlobalAccountDataEventType::IgnoredUserList)
|
||||
context.state_changes.account_data.get(&GlobalAccountDataEventType::IgnoredUserList)
|
||||
{
|
||||
match event.deserialize_as::<IgnoredUserListEvent>() {
|
||||
Ok(event) => {
|
||||
@@ -93,11 +111,13 @@ fn apply_changes(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (room_id, room_info) in &state_changes.room_infos {
|
||||
fn broadcast_room_info_notable_updates(context: &Context, state_store: &BaseStateStore) {
|
||||
for (room_id, room_info) in &context.state_changes.room_infos {
|
||||
if let Some(room) = state_store.room(room_id) {
|
||||
let room_info_notable_update_reasons =
|
||||
room_info_notable_updates.get(room_id).copied().unwrap_or_default();
|
||||
context.room_info_notable_updates.get(room_id).copied().unwrap_or_default();
|
||||
|
||||
room.set_room_info(room_info.clone(), room_info_notable_update_reasons)
|
||||
}
|
||||
|
||||
@@ -13,12 +13,10 @@
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use matrix_sdk_crypto::{
|
||||
DecryptionSettings, OlmMachine, RoomEventDecryptionResult, TrustRequirement,
|
||||
};
|
||||
use matrix_sdk_crypto::{DecryptionSettings, RoomEventDecryptionResult};
|
||||
use ruma::{events::AnySyncTimelineEvent, serde::Raw, RoomId};
|
||||
|
||||
use super::super::{verification, Context};
|
||||
use super::{super::verification, E2EE};
|
||||
use crate::Result;
|
||||
|
||||
/// Attempt to decrypt the given raw event into a [`TimelineEvent`].
|
||||
@@ -29,38 +27,29 @@ use crate::Result;
|
||||
///
|
||||
/// Returns `Ok(None)` if encryption is not configured.
|
||||
pub async fn sync_timeline_event(
|
||||
context: &mut Context,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
e2ee: E2EE<'_>,
|
||||
event: &Raw<AnySyncTimelineEvent>,
|
||||
room_id: &RoomId,
|
||||
decryption_trust_requirement: TrustRequirement,
|
||||
verification_is_allowed: bool,
|
||||
) -> Result<Option<TimelineEvent>> {
|
||||
let Some(olm) = olm_machine else { return Ok(None) };
|
||||
let Some(olm) = e2ee.olm_machine else { return Ok(None) };
|
||||
|
||||
let decryption_settings =
|
||||
DecryptionSettings { sender_device_trust_requirement: decryption_trust_requirement };
|
||||
DecryptionSettings { sender_device_trust_requirement: e2ee.decryption_trust_requirement };
|
||||
|
||||
Ok(Some(
|
||||
match olm.try_decrypt_room_event(event.cast_ref(), room_id, &decryption_settings).await? {
|
||||
RoomEventDecryptionResult::Decrypted(decrypted) => {
|
||||
let timeline_event = TimelineEvent::from(decrypted);
|
||||
// Note: the push actions are set by the caller.
|
||||
let timeline_event = TimelineEvent::from_decrypted(decrypted, None);
|
||||
|
||||
if let Ok(sync_timeline_event) = timeline_event.raw().deserialize() {
|
||||
verification::process_if_relevant(
|
||||
context,
|
||||
&sync_timeline_event,
|
||||
verification_is_allowed,
|
||||
olm_machine,
|
||||
room_id,
|
||||
)
|
||||
.await?;
|
||||
verification::process_if_relevant(&sync_timeline_event, e2ee, room_id).await?;
|
||||
}
|
||||
|
||||
timeline_event
|
||||
}
|
||||
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
|
||||
TimelineEvent::new_utd_event(event.clone(), utd_info)
|
||||
TimelineEvent::from_utd(event.clone(), utd_info)
|
||||
}
|
||||
},
|
||||
))
|
||||
|
||||
@@ -12,6 +12,26 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk_crypto::{OlmMachine, TrustRequirement};
|
||||
|
||||
pub mod decrypt;
|
||||
pub mod to_device;
|
||||
pub mod tracked_users;
|
||||
|
||||
/// A classical set of data used by some processors in this module.
|
||||
#[derive(Clone)]
|
||||
pub struct E2EE<'a> {
|
||||
pub olm_machine: Option<&'a OlmMachine>,
|
||||
pub decryption_trust_requirement: TrustRequirement,
|
||||
pub verification_is_allowed: bool,
|
||||
}
|
||||
|
||||
impl<'a> E2EE<'a> {
|
||||
pub fn new(
|
||||
olm_machine: Option<&'a OlmMachine>,
|
||||
decryption_trust_requirement: TrustRequirement,
|
||||
verification_is_allowed: bool,
|
||||
) -> Self {
|
||||
Self { olm_machine, decryption_trust_requirement, verification_is_allowed }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ use ruma::{
|
||||
OneTimeKeyAlgorithm, UInt,
|
||||
};
|
||||
|
||||
use super::super::Context;
|
||||
use crate::Result;
|
||||
|
||||
/// Process the to-device events and other related e2ee data based on a response
|
||||
@@ -31,13 +30,11 @@ use crate::Result;
|
||||
/// This returns a list of all the to-device events that were passed in but
|
||||
/// encrypted ones were replaced with their decrypted version.
|
||||
pub async fn from_msc4186(
|
||||
context: &mut Context,
|
||||
to_device: Option<&v5::response::ToDevice>,
|
||||
e2ee: &v5::response::E2EE,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
) -> Result<Output> {
|
||||
process(
|
||||
context,
|
||||
olm_machine,
|
||||
to_device.as_ref().map(|to_device| to_device.events.clone()).unwrap_or_default(),
|
||||
&e2ee.device_lists,
|
||||
@@ -54,12 +51,10 @@ pub async fn from_msc4186(
|
||||
/// This returns a list of all the to-device events that were passed in but
|
||||
/// encrypted ones were replaced with their decrypted version.
|
||||
pub async fn from_sync_v2(
|
||||
context: &mut Context,
|
||||
response: &v3::Response,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
) -> Result<Output> {
|
||||
process(
|
||||
context,
|
||||
olm_machine,
|
||||
response.to_device.events.clone(),
|
||||
&response.device_lists,
|
||||
@@ -75,7 +70,6 @@ pub async fn from_sync_v2(
|
||||
/// This returns a list of all the to-device events that were passed in but
|
||||
/// encrypted ones were replaced with their decrypted version.
|
||||
async fn process(
|
||||
_context: &mut Context,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
to_device_events: Vec<Raw<AnyToDeviceEvent>>,
|
||||
device_lists: &DeviceLists,
|
||||
@@ -99,6 +93,15 @@ async fn process(
|
||||
let (events, room_key_updates) =
|
||||
olm_machine.receive_sync_changes(encryption_sync_changes).await?;
|
||||
|
||||
let events = events
|
||||
.iter()
|
||||
// TODO: There is loss of information here, after calling `to_raw` it is not
|
||||
// possible to make the difference between a successfully decrypted event and a plain
|
||||
// text event. This information needs to be propagated to top layer at some point if
|
||||
// clients relies on custom encrypted to device events.
|
||||
.map(|p| p.to_raw())
|
||||
.collect();
|
||||
|
||||
Output { decrypted_to_device_events: events, room_key_updates: Some(room_key_updates) }
|
||||
} else {
|
||||
// If we have no `OlmMachine`, just return the events that were passed in.
|
||||
|
||||
@@ -17,12 +17,10 @@ use std::collections::BTreeSet;
|
||||
use matrix_sdk_crypto::OlmMachine;
|
||||
use ruma::{OwnedUserId, RoomId};
|
||||
|
||||
use super::super::Context;
|
||||
use crate::{store::BaseStateStore, EncryptionState, Result, RoomMemberships};
|
||||
|
||||
/// Update tracked users, if the room is encrypted.
|
||||
pub async fn update(
|
||||
_context: &mut Context,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
room_encryption_state: EncryptionState,
|
||||
user_ids_to_track: &BTreeSet<OwnedUserId>,
|
||||
@@ -41,7 +39,6 @@ pub async fn update(
|
||||
/// Update tracked users, if the room is encrypted, or if the room has become
|
||||
/// encrypted.
|
||||
pub async fn update_or_set_if_room_is_newly_encrypted(
|
||||
_context: &mut Context,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
user_ids_to_track: &BTreeSet<OwnedUserId>,
|
||||
new_room_encryption_state: EncryptionState,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright 2025 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 ruma::{events::AnySyncEphemeralRoomEvent, serde::Raw, RoomId};
|
||||
use tracing::info;
|
||||
|
||||
use super::Context;
|
||||
|
||||
/// Dispatch [`AnySyncEphemeralRoomEvent`]s on the [`Context`].
|
||||
pub fn dispatch(
|
||||
context: &mut Context,
|
||||
raw_events: &[Raw<AnySyncEphemeralRoomEvent>],
|
||||
room_id: &RoomId,
|
||||
) {
|
||||
for raw_event in raw_events {
|
||||
dispatch_receipt(context, raw_event, room_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispatch the [`AnySyncEphemeralRoomEvent::Receipt`] on the [`Context`].
|
||||
pub(super) fn dispatch_receipt(
|
||||
context: &mut Context,
|
||||
raw_event: &Raw<AnySyncEphemeralRoomEvent>,
|
||||
room_id: &RoomId,
|
||||
) {
|
||||
match raw_event.deserialize() {
|
||||
Ok(AnySyncEphemeralRoomEvent::Receipt(event)) => {
|
||||
context.state_changes.add_receipts(room_id, event.content);
|
||||
}
|
||||
|
||||
Ok(_) => {}
|
||||
|
||||
Err(e) => {
|
||||
let event_id = raw_event.get_field::<String>("event_id").ok().flatten();
|
||||
|
||||
info!(?room_id, event_id, "Failed to deserialize ephemeral room event: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,10 @@
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use matrix_sdk_crypto::{
|
||||
DecryptionSettings, OlmMachine, RoomEventDecryptionResult, TrustRequirement,
|
||||
};
|
||||
use matrix_sdk_crypto::{DecryptionSettings, RoomEventDecryptionResult};
|
||||
use ruma::{events::AnySyncTimelineEvent, serde::Raw, RoomId};
|
||||
|
||||
use super::{verification, Context};
|
||||
use super::{e2ee::E2EE, verification, Context};
|
||||
use crate::{
|
||||
latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent},
|
||||
Result, Room,
|
||||
@@ -33,27 +31,19 @@ use crate::{
|
||||
pub async fn decrypt_from_rooms(
|
||||
context: &mut Context,
|
||||
rooms: Vec<Room>,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
decryption_trust_requirement: TrustRequirement,
|
||||
verification_is_allowed: bool,
|
||||
e2ee: E2EE<'_>,
|
||||
) -> Result<()> {
|
||||
let Some(olm_machine) = olm_machine else {
|
||||
// All functions used by this one expect an `OlmMachine`. Return if there is
|
||||
// none.
|
||||
if e2ee.olm_machine.is_none() {
|
||||
return Ok(());
|
||||
};
|
||||
}
|
||||
|
||||
for room in rooms {
|
||||
// Try to find a message we can decrypt and is suitable for using as the latest
|
||||
// event. If we found one, set it as the latest and delete any older
|
||||
// encrypted events
|
||||
if let Some((found, found_index)) = find_suitable_and_decrypt(
|
||||
context,
|
||||
olm_machine,
|
||||
&room,
|
||||
&decryption_trust_requirement,
|
||||
verification_is_allowed,
|
||||
)
|
||||
.await
|
||||
{
|
||||
if let Some((found, found_index)) = find_suitable_and_decrypt(&room, &e2ee).await {
|
||||
room.on_latest_event_decrypted(
|
||||
found,
|
||||
found_index,
|
||||
@@ -67,11 +57,8 @@ pub async fn decrypt_from_rooms(
|
||||
}
|
||||
|
||||
async fn find_suitable_and_decrypt(
|
||||
context: &mut Context,
|
||||
olm_machine: &OlmMachine,
|
||||
room: &Room,
|
||||
decryption_trust_requirement: &TrustRequirement,
|
||||
verification_is_allowed: bool,
|
||||
e2ee: &E2EE<'_>,
|
||||
) -> Option<(Box<LatestEvent>, usize)> {
|
||||
let enc_events = room.latest_encrypted_events();
|
||||
let power_levels = room.power_levels().await.ok();
|
||||
@@ -82,14 +69,8 @@ async fn find_suitable_and_decrypt(
|
||||
// Size of the `decrypt_sync_room_event` future should not impact this
|
||||
// async fn since it is likely that there aren't even any encrypted
|
||||
// events when calling it.
|
||||
let decrypt_sync_room_event = Box::pin(decrypt_sync_room_event(
|
||||
context,
|
||||
olm_machine,
|
||||
event,
|
||||
room.room_id(),
|
||||
decryption_trust_requirement,
|
||||
verification_is_allowed,
|
||||
));
|
||||
let decrypt_sync_room_event =
|
||||
Box::pin(decrypt_sync_room_event(event, e2ee, room.room_id()));
|
||||
|
||||
if let Ok(decrypted) = decrypt_sync_room_event.await {
|
||||
// We found an event we can decrypt
|
||||
@@ -119,41 +100,37 @@ async fn find_suitable_and_decrypt(
|
||||
/// representing the decryption error; in the case of problems with our
|
||||
/// application, returns `Err`.
|
||||
///
|
||||
/// Returns `Ok(None)` if encryption is not configured.
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if there is no [`OlmMachine`] in [`E2EE`].
|
||||
async fn decrypt_sync_room_event(
|
||||
context: &mut Context,
|
||||
olm_machine: &OlmMachine,
|
||||
event: &Raw<AnySyncTimelineEvent>,
|
||||
e2ee: &E2EE<'_>,
|
||||
room_id: &RoomId,
|
||||
decryption_trust_requirement: &TrustRequirement,
|
||||
verification_is_allowed: bool,
|
||||
) -> Result<TimelineEvent> {
|
||||
let decryption_settings =
|
||||
DecryptionSettings { sender_device_trust_requirement: *decryption_trust_requirement };
|
||||
DecryptionSettings { sender_device_trust_requirement: e2ee.decryption_trust_requirement };
|
||||
|
||||
let event = match olm_machine
|
||||
let event = match e2ee
|
||||
.olm_machine
|
||||
.expect("An `OlmMachine` is expected")
|
||||
.try_decrypt_room_event(event.cast_ref(), room_id, &decryption_settings)
|
||||
.await?
|
||||
{
|
||||
RoomEventDecryptionResult::Decrypted(decrypted) => {
|
||||
let event: TimelineEvent = decrypted.into();
|
||||
// We're fine not setting the push actions for the latest event.
|
||||
let event = TimelineEvent::from_decrypted(decrypted, None);
|
||||
|
||||
if let Ok(sync_timeline_event) = event.raw().deserialize() {
|
||||
verification::process_if_relevant(
|
||||
context,
|
||||
&sync_timeline_event,
|
||||
verification_is_allowed,
|
||||
Some(olm_machine),
|
||||
room_id,
|
||||
)
|
||||
.await?;
|
||||
verification::process_if_relevant(&sync_timeline_event, e2ee.clone(), room_id)
|
||||
.await?;
|
||||
}
|
||||
|
||||
event
|
||||
}
|
||||
|
||||
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
|
||||
TimelineEvent::new_utd_event(event.clone(), utd_info)
|
||||
TimelineEvent::from_utd(event.clone(), utd_info)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -167,11 +144,8 @@ mod tests {
|
||||
};
|
||||
use ruma::{event_id, events::room::member::MembershipState, room_id, user_id};
|
||||
|
||||
use super::{decrypt_from_rooms, Context};
|
||||
use crate::{
|
||||
rooms::normal::RoomInfoNotableUpdateReasons, test_utils::logged_in_base_client,
|
||||
StateChanges,
|
||||
};
|
||||
use super::{decrypt_from_rooms, Context, E2EE};
|
||||
use crate::{room::RoomInfoNotableUpdateReasons, test_utils::logged_in_base_client};
|
||||
|
||||
#[async_test]
|
||||
async fn test_when_there_are_no_latest_encrypted_events_decrypting_them_does_nothing() {
|
||||
@@ -203,14 +177,16 @@ mod tests {
|
||||
assert!(room.latest_event().is_none());
|
||||
|
||||
// When I tell it to do some decryption
|
||||
let mut context = Context::new(StateChanges::default(), Default::default());
|
||||
let mut context = Context::default();
|
||||
|
||||
decrypt_from_rooms(
|
||||
&mut context,
|
||||
vec![room.clone()],
|
||||
client.olm_machine().await.as_ref(),
|
||||
client.decryption_trust_requirement,
|
||||
client.handle_verification_events,
|
||||
E2EE::new(
|
||||
client.olm_machine().await.as_ref(),
|
||||
client.decryption_trust_requirement,
|
||||
client.handle_verification_events,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -16,9 +16,12 @@ pub mod account_data;
|
||||
pub mod changes;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub mod e2ee;
|
||||
pub mod ephemeral_events;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub mod latest_event;
|
||||
pub mod notification;
|
||||
pub mod profiles;
|
||||
pub mod room;
|
||||
pub mod state_events;
|
||||
pub mod timeline;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
@@ -33,22 +36,14 @@ use crate::{RoomInfoNotableUpdateReasons, StateChanges};
|
||||
type RoomInfoNotableUpdates = BTreeMap<OwnedRoomId, RoomInfoNotableUpdateReasons>;
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Context {
|
||||
pub(super) state_changes: StateChanges,
|
||||
pub(super) room_info_notable_updates: RoomInfoNotableUpdates,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn new(
|
||||
state_changes: StateChanges,
|
||||
room_info_notable_updates: RoomInfoNotableUpdates,
|
||||
) -> Self {
|
||||
Self { state_changes, room_info_notable_updates }
|
||||
}
|
||||
|
||||
pub fn into_parts(self) -> (StateChanges, RoomInfoNotableUpdates) {
|
||||
let Self { state_changes, room_info_notable_updates } = self;
|
||||
|
||||
(state_changes, room_info_notable_updates)
|
||||
pub fn new(state_changes: StateChanges) -> Self {
|
||||
Self { state_changes, room_info_notable_updates: Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use ruma::{
|
||||
push::{Action, PushConditionRoomCtx, Ruleset},
|
||||
serde::Raw,
|
||||
OwnedRoomId, RoomId,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
deserialized_responses::RawAnySyncOrStrippedTimelineEvent, store::BaseStateStore, sync,
|
||||
};
|
||||
|
||||
/// A classical set of data used by some processors dealing with notifications
|
||||
/// and push rules.
|
||||
pub struct Notification<'a> {
|
||||
pub push_rules: &'a Ruleset,
|
||||
pub notifications: &'a mut BTreeMap<OwnedRoomId, Vec<sync::Notification>>,
|
||||
pub state_store: &'a BaseStateStore,
|
||||
}
|
||||
|
||||
impl<'a> Notification<'a> {
|
||||
pub fn new(
|
||||
push_rules: &'a Ruleset,
|
||||
notifications: &'a mut BTreeMap<OwnedRoomId, Vec<sync::Notification>>,
|
||||
state_store: &'a BaseStateStore,
|
||||
) -> Self {
|
||||
Self { push_rules, notifications, state_store }
|
||||
}
|
||||
|
||||
fn push_notification(
|
||||
&mut self,
|
||||
room_id: &RoomId,
|
||||
actions: Vec<Action>,
|
||||
event: RawAnySyncOrStrippedTimelineEvent,
|
||||
) {
|
||||
self.notifications
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.push(sync::Notification { actions, event });
|
||||
}
|
||||
|
||||
/// Push a new [`sync::Notification`] in [`Self::notifications`] from
|
||||
/// `event` if and only if `predicate` returns `true` for at least one of
|
||||
/// the [`Action`]s associated to this event and this
|
||||
/// `push_condition_room_ctx`. (based on `Self::push_rules`).
|
||||
///
|
||||
/// This method returns the fetched [`Action`]s.
|
||||
pub fn push_notification_from_event_if<E, P>(
|
||||
&mut self,
|
||||
room_id: &RoomId,
|
||||
push_condition_room_ctx: &PushConditionRoomCtx,
|
||||
event: &Raw<E>,
|
||||
predicate: P,
|
||||
) -> &[Action]
|
||||
where
|
||||
Raw<E>: Into<RawAnySyncOrStrippedTimelineEvent>,
|
||||
P: Fn(&Action) -> bool,
|
||||
{
|
||||
let actions = self.push_rules.get_actions(event, push_condition_room_ctx);
|
||||
|
||||
if actions.iter().any(predicate) {
|
||||
self.push_notification(room_id, actions.to_owned(), event.clone().into());
|
||||
}
|
||||
|
||||
actions
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2025 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 super::super::Context;
|
||||
use crate::{
|
||||
room::UpdatedRoomDisplayName, store::BaseStateStore, sync::RoomUpdates,
|
||||
RoomInfoNotableUpdateReasons,
|
||||
};
|
||||
|
||||
pub async fn update_for_rooms(
|
||||
context: &mut Context,
|
||||
room_updates: &RoomUpdates,
|
||||
state_store: &BaseStateStore,
|
||||
) {
|
||||
for room in room_updates.iter_all_room_ids().filter_map(|room_id| state_store.room(room_id)) {
|
||||
// Compute the display name. If it's different, let's register the `RoomInfo` in
|
||||
// the `StateChanges`.
|
||||
if let Ok(UpdatedRoomDisplayName::New(_)) = room.compute_display_name().await {
|
||||
let room_id = room.room_id().to_owned();
|
||||
|
||||
context.state_changes.room_infos.insert(room_id.clone(), room.clone_info());
|
||||
context
|
||||
.room_info_notable_updates
|
||||
.entry(room_id)
|
||||
.or_default()
|
||||
.insert(RoomInfoNotableUpdateReasons::DISPLAY_NAME);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2025 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 ruma::RoomId;
|
||||
use tokio::sync::broadcast::Sender;
|
||||
|
||||
use crate::{store::ambiguity_map::AmbiguityCache, RequestedRequiredStates, RoomInfoNotableUpdate};
|
||||
|
||||
pub mod display_name;
|
||||
pub mod msc4186;
|
||||
pub mod sync_v2;
|
||||
|
||||
/// A classical set of data used by some processors in this module.
|
||||
pub struct RoomCreationData<'a> {
|
||||
room_id: &'a RoomId,
|
||||
room_info_notable_update_sender: Sender<RoomInfoNotableUpdate>,
|
||||
requested_required_states: &'a RequestedRequiredStates,
|
||||
ambiguity_cache: &'a mut AmbiguityCache,
|
||||
}
|
||||
|
||||
impl<'a> RoomCreationData<'a> {
|
||||
pub fn new(
|
||||
room_id: &'a RoomId,
|
||||
room_info_notable_update_sender: Sender<RoomInfoNotableUpdate>,
|
||||
requested_required_states: &'a RequestedRequiredStates,
|
||||
ambiguity_cache: &'a mut AmbiguityCache,
|
||||
) -> Self {
|
||||
Self {
|
||||
room_id,
|
||||
room_info_notable_update_sender,
|
||||
requested_required_states,
|
||||
ambiguity_cache,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::v5 as http,
|
||||
events::{receipt::ReceiptEventContent, AnySyncEphemeralRoomEvent, SyncEphemeralRoomEvent},
|
||||
serde::Raw,
|
||||
OwnedRoomId, RoomId,
|
||||
};
|
||||
|
||||
use super::super::super::{
|
||||
account_data::for_room as account_data_for_room, ephemeral_events::dispatch_receipt, Context,
|
||||
};
|
||||
use crate::{
|
||||
store::BaseStateStore,
|
||||
sync::{JoinedRoomUpdate, RoomUpdates},
|
||||
RoomState,
|
||||
};
|
||||
|
||||
/// Dispatch the ephemeral events in the `extensions.typing` part of the
|
||||
/// response.
|
||||
pub fn dispatch_typing_ephemeral_events(
|
||||
typing: &http::response::Typing,
|
||||
joined_room_updates: &mut BTreeMap<OwnedRoomId, JoinedRoomUpdate>,
|
||||
) {
|
||||
for (room_id, raw) in &typing.rooms {
|
||||
joined_room_updates
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.ephemeral
|
||||
.push(raw.clone().cast());
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispatch the ephemeral event in the `extensions.receipts` part of the
|
||||
/// response for a particular room.
|
||||
pub fn dispatch_receipt_ephemeral_event_for_room(
|
||||
context: &mut Context,
|
||||
room_id: &RoomId,
|
||||
receipt: &Raw<SyncEphemeralRoomEvent<ReceiptEventContent>>,
|
||||
joined_room_update: &mut JoinedRoomUpdate,
|
||||
) {
|
||||
let receipt: Raw<AnySyncEphemeralRoomEvent> = receipt.cast_ref().clone();
|
||||
|
||||
dispatch_receipt(context, &receipt, room_id);
|
||||
joined_room_update.ephemeral.push(receipt);
|
||||
}
|
||||
|
||||
pub async fn room_account_data(
|
||||
context: &mut Context,
|
||||
account_data: &http::response::AccountData,
|
||||
room_updates: &mut RoomUpdates,
|
||||
state_store: &BaseStateStore,
|
||||
) {
|
||||
for (room_id, raw) in &account_data.rooms {
|
||||
account_data_for_room(context, room_id, raw, state_store).await;
|
||||
|
||||
if let Some(room) = state_store.room(room_id) {
|
||||
match room.state() {
|
||||
RoomState::Joined => room_updates
|
||||
.joined
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.account_data
|
||||
.append(&mut raw.to_vec()),
|
||||
RoomState::Left | RoomState::Banned => room_updates
|
||||
.left
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.account_data
|
||||
.append(&mut raw.to_vec()),
|
||||
RoomState::Invited | RoomState::Knocked => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,564 @@
|
||||
// Copyright 2025 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.
|
||||
|
||||
pub mod extensions;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::events::StateEventType;
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::{
|
||||
v3::{InviteState, InvitedRoom, KnockState, KnockedRoom},
|
||||
v5 as http,
|
||||
},
|
||||
assign,
|
||||
events::{
|
||||
room::member::{MembershipState, RoomMemberEventContent},
|
||||
AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
JsOption, OwnedRoomId, RoomId, UserId,
|
||||
};
|
||||
use tokio::sync::broadcast::Sender;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use super::super::e2ee;
|
||||
use super::{
|
||||
super::{notification, state_events, timeline, Context},
|
||||
RoomCreationData,
|
||||
};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use crate::StateChanges;
|
||||
use crate::{
|
||||
store::BaseStateStore,
|
||||
sync::{InvitedRoomUpdate, JoinedRoomUpdate, KnockedRoomUpdate, LeftRoomUpdate},
|
||||
Result, Room, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons,
|
||||
RoomState,
|
||||
};
|
||||
|
||||
/// Represent any kind of room updates.
|
||||
pub enum RoomUpdateKind {
|
||||
Joined(JoinedRoomUpdate),
|
||||
Left(LeftRoomUpdate),
|
||||
Invited(InvitedRoomUpdate),
|
||||
Knocked(KnockedRoomUpdate),
|
||||
}
|
||||
|
||||
pub async fn update_any_room(
|
||||
context: &mut Context,
|
||||
user_id: &UserId,
|
||||
room_creation_data: RoomCreationData<'_>,
|
||||
room_response: &http::response::Room,
|
||||
rooms_account_data: &BTreeMap<OwnedRoomId, Vec<Raw<AnyRoomAccountDataEvent>>>,
|
||||
#[cfg(feature = "e2e-encryption")] e2ee: e2ee::E2EE<'_>,
|
||||
notification: notification::Notification<'_>,
|
||||
) -> Result<Option<(RoomInfo, RoomUpdateKind)>> {
|
||||
let RoomCreationData {
|
||||
room_id,
|
||||
room_info_notable_update_sender,
|
||||
requested_required_states,
|
||||
ambiguity_cache,
|
||||
} = room_creation_data;
|
||||
|
||||
// Read state events from the `required_state` field.
|
||||
//
|
||||
// Don't read state events from the `timeline` field, because they might be
|
||||
// incomplete or staled already. We must only read state events from
|
||||
// `required_state`.
|
||||
let (raw_state_events, state_events) =
|
||||
state_events::sync::collect(&room_response.required_state);
|
||||
|
||||
let state_store = notification.state_store;
|
||||
|
||||
// Find or create the room in the store
|
||||
let is_new_room = !state_store.room_exists(room_id);
|
||||
|
||||
let invite_state_events =
|
||||
room_response.invite_state.as_ref().map(|events| state_events::stripped::collect(events));
|
||||
|
||||
#[allow(unused_mut)] // Required for some feature flag combinations
|
||||
let (mut room, mut room_info, maybe_room_update_kind) = membership(
|
||||
context,
|
||||
&state_events,
|
||||
&invite_state_events,
|
||||
state_store,
|
||||
user_id,
|
||||
room_id,
|
||||
room_info_notable_update_sender,
|
||||
);
|
||||
|
||||
room_info.mark_state_partially_synced();
|
||||
room_info.handle_encryption_state(requested_required_states.for_room(room_id));
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
let mut new_user_ids = BTreeSet::new();
|
||||
|
||||
#[cfg(not(feature = "e2e-encryption"))]
|
||||
let mut new_user_ids = ();
|
||||
|
||||
state_events::sync::dispatch(
|
||||
context,
|
||||
(&raw_state_events, &state_events),
|
||||
&mut room_info,
|
||||
ambiguity_cache,
|
||||
&mut new_user_ids,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// This will be used for both invited and knocked rooms.
|
||||
if let Some((raw_events, events)) = invite_state_events {
|
||||
state_events::stripped::dispatch_invite_or_knock(
|
||||
context,
|
||||
(&raw_events, &events),
|
||||
&room,
|
||||
&mut room_info,
|
||||
notification::Notification::new(
|
||||
notification.push_rules,
|
||||
notification.notifications,
|
||||
notification.state_store,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
properties(context, room_id, room_response, &mut room_info, is_new_room);
|
||||
|
||||
let timeline = timeline::build(
|
||||
context,
|
||||
&room,
|
||||
&mut room_info,
|
||||
timeline::builder::Timeline::from(room_response),
|
||||
notification,
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
e2ee.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Cache the latest decrypted event in room_info, and also keep any later
|
||||
// encrypted events, so we can slot them in when we get the keys.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
cache_latest_events(
|
||||
&room,
|
||||
&mut room_info,
|
||||
&timeline.events,
|
||||
Some(&context.state_changes),
|
||||
Some(state_store),
|
||||
)
|
||||
.await;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
e2ee::tracked_users::update_or_set_if_room_is_newly_encrypted(
|
||||
e2ee.olm_machine,
|
||||
&new_user_ids,
|
||||
room_info.encryption_state(),
|
||||
room.encryption_state(),
|
||||
room_id,
|
||||
state_store,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let notification_count = room_response.unread_notifications.clone().into();
|
||||
room_info.update_notification_count(notification_count);
|
||||
|
||||
let ambiguity_changes = ambiguity_cache.changes.remove(room_id).unwrap_or_default();
|
||||
let room_account_data = rooms_account_data.get(room_id);
|
||||
|
||||
match (room_info.state(), maybe_room_update_kind) {
|
||||
(RoomState::Joined, None) => {
|
||||
// Ephemeral events are added separately, because we might not
|
||||
// have a room subsection in the response, yet we may have receipts for
|
||||
// that room.
|
||||
let ephemeral = Vec::new();
|
||||
|
||||
Ok(Some((
|
||||
room_info,
|
||||
RoomUpdateKind::Joined(JoinedRoomUpdate::new(
|
||||
timeline,
|
||||
raw_state_events,
|
||||
room_account_data.cloned().unwrap_or_default(),
|
||||
ephemeral,
|
||||
notification_count,
|
||||
ambiguity_changes,
|
||||
)),
|
||||
)))
|
||||
}
|
||||
|
||||
(RoomState::Left, None) | (RoomState::Banned, None) => Ok(Some((
|
||||
room_info,
|
||||
RoomUpdateKind::Left(LeftRoomUpdate::new(
|
||||
timeline,
|
||||
raw_state_events,
|
||||
room_account_data.cloned().unwrap_or_default(),
|
||||
ambiguity_changes,
|
||||
)),
|
||||
))),
|
||||
|
||||
(RoomState::Invited, Some(update @ RoomUpdateKind::Invited(_)))
|
||||
| (RoomState::Knocked, Some(update @ RoomUpdateKind::Knocked(_))) => {
|
||||
Ok(Some((room_info, update)))
|
||||
}
|
||||
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Look through the sliding sync data for this room, find/create it in the
|
||||
/// store, and process any invite information.
|
||||
///
|
||||
/// If there is any invite state events, the room can be considered an invited
|
||||
/// or knocked room, depending of the membership event (if any).
|
||||
fn membership(
|
||||
context: &mut Context,
|
||||
state_events: &[AnySyncStateEvent],
|
||||
invite_state_events: &Option<(Vec<Raw<AnyStrippedStateEvent>>, Vec<AnyStrippedStateEvent>)>,
|
||||
store: &BaseStateStore,
|
||||
user_id: &UserId,
|
||||
room_id: &RoomId,
|
||||
room_info_notable_update_sender: Sender<RoomInfoNotableUpdate>,
|
||||
) -> (Room, RoomInfo, Option<RoomUpdateKind>) {
|
||||
// There are invite state events. It means the room can be:
|
||||
//
|
||||
// 1. either an invited room,
|
||||
// 2. or a knocked room.
|
||||
//
|
||||
// Let's find out.
|
||||
if let Some(state_events) = invite_state_events {
|
||||
// We need to find the membership event since it could be for either an invited
|
||||
// or knocked room.
|
||||
let membership_event = state_events.1.iter().find_map(|event| {
|
||||
if let AnyStrippedStateEvent::RoomMember(membership_event) = event {
|
||||
if membership_event.state_key == user_id {
|
||||
return Some(membership_event.content.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
});
|
||||
|
||||
match membership_event {
|
||||
// There is a membership event indicating it's a knocked room.
|
||||
Some(RoomMemberEventContent { membership: MembershipState::Knock, .. }) => {
|
||||
let room = store.get_or_create_room(
|
||||
room_id,
|
||||
RoomState::Knocked,
|
||||
room_info_notable_update_sender,
|
||||
);
|
||||
let mut room_info = room.clone_info();
|
||||
// Override the room state if the room already exists.
|
||||
room_info.mark_as_knocked();
|
||||
|
||||
let raw_events = state_events.0.clone();
|
||||
let knock_state = assign!(KnockState::default(), { events: raw_events });
|
||||
let knocked_room = assign!(KnockedRoom::default(), { knock_state: knock_state });
|
||||
|
||||
(room, room_info, Some(RoomUpdateKind::Knocked(knocked_room)))
|
||||
}
|
||||
|
||||
// Otherwise, assume it's an invited room because there are invite state events.
|
||||
_ => {
|
||||
let room = store.get_or_create_room(
|
||||
room_id,
|
||||
RoomState::Invited,
|
||||
room_info_notable_update_sender,
|
||||
);
|
||||
let mut room_info = room.clone_info();
|
||||
// Override the room state if the room already exists.
|
||||
room_info.mark_as_invited();
|
||||
|
||||
let raw_events = state_events.0.clone();
|
||||
let invited_room = InvitedRoom::from(InviteState::from(raw_events));
|
||||
|
||||
(room, room_info, Some(RoomUpdateKind::Invited(invited_room)))
|
||||
}
|
||||
}
|
||||
}
|
||||
// No invite state events. We assume this is a joined room for the moment. See this block to
|
||||
// learn more.
|
||||
else {
|
||||
let room =
|
||||
store.get_or_create_room(room_id, RoomState::Joined, room_info_notable_update_sender);
|
||||
let mut room_info = room.clone_info();
|
||||
|
||||
// We default to considering this room joined if it's not an invite. If it's
|
||||
// actually left (and we remembered to request membership events in
|
||||
// our sync request), then we can find this out from the events in
|
||||
// required_state by calling handle_own_room_membership.
|
||||
room_info.mark_as_joined();
|
||||
|
||||
// We don't need to do this in a v2 sync, because the membership of a room can
|
||||
// be figured out by whether the room is in the "join", "leave" etc.
|
||||
// property. In sliding sync we only have invite_state,
|
||||
// required_state and timeline, so we must process required_state and timeline
|
||||
// looking for relevant membership events.
|
||||
own_membership(context, user_id, state_events, &mut room_info);
|
||||
|
||||
(room, room_info, None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Find any `m.room.member` events that refer to the current user, and update
|
||||
/// the state in room_info to reflect the "membership" property.
|
||||
fn own_membership(
|
||||
context: &mut Context,
|
||||
user_id: &UserId,
|
||||
state_events: &[AnySyncStateEvent],
|
||||
room_info: &mut RoomInfo,
|
||||
) {
|
||||
// Start from the last event; the first membership event we see in that order is
|
||||
// the last in the regular order, so that's the only one we need to
|
||||
// consider.
|
||||
for event in state_events.iter().rev() {
|
||||
if let AnySyncStateEvent::RoomMember(member) = &event {
|
||||
// If this event updates the current user's membership, record that in the
|
||||
// room_info.
|
||||
if member.state_key() == user_id.as_str() {
|
||||
let new_state: RoomState = member.membership().into();
|
||||
|
||||
if new_state != room_info.state() {
|
||||
room_info.set_state(new_state);
|
||||
// Update an existing notable update entry or create a new one
|
||||
context
|
||||
.room_info_notable_updates
|
||||
.entry(room_info.room_id.to_owned())
|
||||
.or_default()
|
||||
.insert(RoomInfoNotableUpdateReasons::MEMBERSHIP);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn properties(
|
||||
context: &mut Context,
|
||||
room_id: &RoomId,
|
||||
room_response: &http::response::Room,
|
||||
room_info: &mut RoomInfo,
|
||||
is_new_room: bool,
|
||||
) {
|
||||
// Handle the room's avatar.
|
||||
//
|
||||
// It can be updated via the state events, or via the
|
||||
// [`http::ResponseRoom::avatar`] field. This part of the code handles the
|
||||
// latter case. The former case is handled by [`BaseClient::handle_state`].
|
||||
match &room_response.avatar {
|
||||
// A new avatar!
|
||||
JsOption::Some(avatar_uri) => room_info.update_avatar(Some(avatar_uri.to_owned())),
|
||||
// Avatar must be removed.
|
||||
JsOption::Null => room_info.update_avatar(None),
|
||||
// Nothing to do.
|
||||
JsOption::Undefined => {}
|
||||
}
|
||||
|
||||
// Sliding sync doesn't have a room summary, nevertheless it contains the joined
|
||||
// and invited member counts, in addition to the heroes.
|
||||
if let Some(count) = room_response.joined_count {
|
||||
room_info.update_joined_member_count(count.into());
|
||||
}
|
||||
if let Some(count) = room_response.invited_count {
|
||||
room_info.update_invited_member_count(count.into());
|
||||
}
|
||||
|
||||
if let Some(heroes) = &room_response.heroes {
|
||||
room_info.update_heroes(
|
||||
heroes
|
||||
.iter()
|
||||
.map(|hero| RoomHero {
|
||||
user_id: hero.user_id.clone(),
|
||||
display_name: hero.name.clone(),
|
||||
avatar_url: hero.avatar.clone(),
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
room_info.set_prev_batch(room_response.prev_batch.as_deref());
|
||||
|
||||
if room_response.limited {
|
||||
room_info.mark_members_missing();
|
||||
}
|
||||
|
||||
if let Some(recency_stamp) = &room_response.bump_stamp {
|
||||
let recency_stamp: u64 = (*recency_stamp).into();
|
||||
|
||||
if room_info.recency_stamp.as_ref() != Some(&recency_stamp) {
|
||||
room_info.update_recency_stamp(recency_stamp);
|
||||
|
||||
// If it's not a new room, let's emit a `RECENCY_STAMP` update.
|
||||
// For a new room, the room will appear as new, so we don't care about this
|
||||
// update.
|
||||
if !is_new_room {
|
||||
context
|
||||
.room_info_notable_updates
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.insert(RoomInfoNotableUpdateReasons::RECENCY_STAMP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the most recent decrypted event and cache it in the supplied RoomInfo.
|
||||
///
|
||||
/// If any encrypted events are found after that one, store them in the RoomInfo
|
||||
/// too so we can use them when we get the relevant keys.
|
||||
///
|
||||
/// It is the responsibility of the caller to update the `RoomInfo` instance
|
||||
/// stored in the `Room`.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub(crate) async fn cache_latest_events(
|
||||
room: &Room,
|
||||
room_info: &mut RoomInfo,
|
||||
events: &[TimelineEvent],
|
||||
changes: Option<&StateChanges>,
|
||||
store: Option<&BaseStateStore>,
|
||||
) {
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
deserialized_responses::DisplayName,
|
||||
latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent},
|
||||
store::ambiguity_map::is_display_name_ambiguous,
|
||||
};
|
||||
|
||||
let mut encrypted_events =
|
||||
Vec::with_capacity(room.latest_encrypted_events.read().unwrap().capacity());
|
||||
|
||||
// Try to get room power levels from the current changes
|
||||
let power_levels_from_changes = || {
|
||||
let state_changes = changes?.state.get(room_info.room_id())?;
|
||||
let room_power_levels_state =
|
||||
state_changes.get(&StateEventType::RoomPowerLevels)?.values().next()?;
|
||||
match room_power_levels_state.deserialize().ok()? {
|
||||
AnySyncStateEvent::RoomPowerLevels(ev) => Some(ev.power_levels()),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
|
||||
// If we didn't get any info, try getting it from local data
|
||||
let power_levels = match power_levels_from_changes() {
|
||||
Some(power_levels) => Some(power_levels),
|
||||
None => room.power_levels().await.ok(),
|
||||
};
|
||||
|
||||
let power_levels_info = Some(room.own_user_id()).zip(power_levels.as_ref());
|
||||
|
||||
for event in events.iter().rev() {
|
||||
if let Ok(timeline_event) = event.raw().deserialize() {
|
||||
match is_suitable_for_latest_event(&timeline_event, power_levels_info) {
|
||||
PossibleLatestEvent::YesRoomMessage(_)
|
||||
| PossibleLatestEvent::YesPoll(_)
|
||||
| PossibleLatestEvent::YesCallInvite(_)
|
||||
| PossibleLatestEvent::YesCallNotify(_)
|
||||
| PossibleLatestEvent::YesSticker(_)
|
||||
| PossibleLatestEvent::YesKnockedStateEvent(_) => {
|
||||
// We found a suitable latest event. Store it.
|
||||
|
||||
// In order to make the latest event fast to read, we want to keep the
|
||||
// associated sender in cache. This is a best-effort to gather enough
|
||||
// information for creating a user profile as fast as possible. If information
|
||||
// are missing, let's go back on the “slow” path.
|
||||
|
||||
let mut sender_profile = None;
|
||||
let mut sender_name_is_ambiguous = None;
|
||||
|
||||
// First off, look up the sender's profile from the `StateChanges`, they are
|
||||
// likely to be the most recent information.
|
||||
if let Some(changes) = changes {
|
||||
sender_profile = changes
|
||||
.profiles
|
||||
.get(room.room_id())
|
||||
.and_then(|profiles_by_user| {
|
||||
profiles_by_user.get(timeline_event.sender())
|
||||
})
|
||||
.cloned();
|
||||
|
||||
if let Some(sender_profile) = sender_profile.as_ref() {
|
||||
sender_name_is_ambiguous = sender_profile
|
||||
.as_original()
|
||||
.and_then(|profile| profile.content.displayname.as_ref())
|
||||
.and_then(|display_name| {
|
||||
let display_name = DisplayName::new(display_name);
|
||||
|
||||
changes.ambiguity_maps.get(room.room_id()).and_then(
|
||||
|map_for_room| {
|
||||
map_for_room.get(&display_name).map(|users| {
|
||||
is_display_name_ambiguous(&display_name, users)
|
||||
})
|
||||
},
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, look up the sender's profile from the `Store`.
|
||||
if sender_profile.is_none() {
|
||||
if let Some(store) = store {
|
||||
sender_profile = store
|
||||
.get_profile(room.room_id(), timeline_event.sender())
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
// TODO: need to update `sender_name_is_ambiguous`,
|
||||
// but how?
|
||||
}
|
||||
}
|
||||
|
||||
let latest_event = Box::new(LatestEvent::new_with_sender_details(
|
||||
event.clone(),
|
||||
sender_profile,
|
||||
sender_name_is_ambiguous,
|
||||
));
|
||||
|
||||
// Store it in the return RoomInfo (it will be saved for us in the room later).
|
||||
room_info.latest_event = Some(latest_event);
|
||||
// We don't need any of the older encrypted events because we have a new
|
||||
// decrypted one.
|
||||
room.latest_encrypted_events.write().unwrap().clear();
|
||||
// We can stop looking through the timeline now because everything else is
|
||||
// older.
|
||||
break;
|
||||
}
|
||||
PossibleLatestEvent::NoEncrypted => {
|
||||
// m.room.encrypted - this might be the latest event later - we can't tell until
|
||||
// we are able to decrypt it, so store it for now
|
||||
//
|
||||
// Check how many encrypted events we have seen. Only store another if we
|
||||
// haven't already stored the maximum number.
|
||||
if encrypted_events.len() < encrypted_events.capacity() {
|
||||
encrypted_events.push(event.raw().clone());
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Ignore unsuitable events
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
"Failed to deserialize event as AnySyncTimelineEvent. ID={}",
|
||||
event.event_id().expect("Event has no ID!")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Push the encrypted events we found into the Room, in reverse order, so
|
||||
// the latest is last
|
||||
room.latest_encrypted_events.write().unwrap().extend(encrypted_events.into_iter().rev());
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::v3::{InvitedRoom, JoinedRoom, KnockedRoom, LeftRoom},
|
||||
OwnedRoomId, OwnedUserId, RoomId,
|
||||
};
|
||||
use tokio::sync::broadcast::Sender;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use super::super::e2ee;
|
||||
use super::{
|
||||
super::{account_data, ephemeral_events, notification, state_events, timeline, Context},
|
||||
RoomCreationData,
|
||||
};
|
||||
use crate::{
|
||||
sync::{InvitedRoomUpdate, JoinedRoomUpdate, KnockedRoomUpdate, LeftRoomUpdate},
|
||||
Result, RoomInfoNotableUpdate, RoomState,
|
||||
};
|
||||
|
||||
/// Process updates of a joined room.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn update_joined_room(
|
||||
context: &mut Context,
|
||||
room_creation_data: RoomCreationData<'_>,
|
||||
joined_room: JoinedRoom,
|
||||
updated_members_in_room: &mut BTreeMap<OwnedRoomId, BTreeSet<OwnedUserId>>,
|
||||
notification: notification::Notification<'_>,
|
||||
#[cfg(feature = "e2e-encryption")] e2ee: e2ee::E2EE<'_>,
|
||||
) -> Result<JoinedRoomUpdate> {
|
||||
let RoomCreationData {
|
||||
room_id,
|
||||
room_info_notable_update_sender,
|
||||
requested_required_states,
|
||||
ambiguity_cache,
|
||||
} = room_creation_data;
|
||||
|
||||
let state_store = notification.state_store;
|
||||
|
||||
let room =
|
||||
state_store.get_or_create_room(room_id, RoomState::Joined, room_info_notable_update_sender);
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
|
||||
room_info.mark_as_joined();
|
||||
room_info.update_from_ruma_summary(&joined_room.summary);
|
||||
room_info.set_prev_batch(joined_room.timeline.prev_batch.as_deref());
|
||||
room_info.mark_state_fully_synced();
|
||||
room_info.handle_encryption_state(requested_required_states.for_room(room_id));
|
||||
|
||||
let (raw_state_events, state_events) = state_events::sync::collect(&joined_room.state.events);
|
||||
|
||||
let mut new_user_ids = BTreeSet::new();
|
||||
|
||||
state_events::sync::dispatch(
|
||||
context,
|
||||
(&raw_state_events, &state_events),
|
||||
&mut room_info,
|
||||
ambiguity_cache,
|
||||
&mut new_user_ids,
|
||||
)
|
||||
.await?;
|
||||
|
||||
ephemeral_events::dispatch(context, &joined_room.ephemeral.events, room_id);
|
||||
|
||||
if joined_room.timeline.limited {
|
||||
room_info.mark_members_missing();
|
||||
}
|
||||
|
||||
let (raw_state_events_from_timeline, state_events_from_timeline) =
|
||||
state_events::sync::collect_from_timeline(&joined_room.timeline.events);
|
||||
|
||||
state_events::sync::dispatch(
|
||||
context,
|
||||
(&raw_state_events_from_timeline, &state_events_from_timeline),
|
||||
&mut room_info,
|
||||
ambiguity_cache,
|
||||
&mut new_user_ids,
|
||||
)
|
||||
.await?;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
let olm_machine = e2ee.olm_machine;
|
||||
|
||||
let timeline = timeline::build(
|
||||
context,
|
||||
&room,
|
||||
&mut room_info,
|
||||
timeline::builder::Timeline::from(joined_room.timeline),
|
||||
notification,
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
e2ee,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Save the new `RoomInfo`.
|
||||
context.state_changes.add_room(room_info);
|
||||
|
||||
account_data::for_room(context, room_id, &joined_room.account_data.events, state_store).await;
|
||||
|
||||
// `processors::account_data::from_room` might have updated the `RoomInfo`.
|
||||
// Let's fetch it again.
|
||||
//
|
||||
// SAFETY: `expect` is safe because the `RoomInfo` has been inserted 2 lines
|
||||
// above.
|
||||
let mut room_info = context
|
||||
.state_changes
|
||||
.room_infos
|
||||
.get(room_id)
|
||||
.expect("`RoomInfo` must exist in `StateChanges` at this point")
|
||||
.clone();
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
e2ee::tracked_users::update_or_set_if_room_is_newly_encrypted(
|
||||
olm_machine,
|
||||
&new_user_ids,
|
||||
room_info.encryption_state(),
|
||||
room.encryption_state(),
|
||||
room_id,
|
||||
state_store,
|
||||
)
|
||||
.await?;
|
||||
|
||||
updated_members_in_room.insert(room_id.to_owned(), new_user_ids);
|
||||
|
||||
let notification_count = joined_room.unread_notifications.into();
|
||||
room_info.update_notification_count(notification_count);
|
||||
|
||||
context.state_changes.add_room(room_info);
|
||||
|
||||
Ok(JoinedRoomUpdate::new(
|
||||
timeline,
|
||||
joined_room.state.events,
|
||||
joined_room.account_data.events,
|
||||
joined_room.ephemeral.events,
|
||||
notification_count,
|
||||
ambiguity_cache.changes.remove(room_id).unwrap_or_default(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Process historical updates of a left room.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn update_left_room(
|
||||
context: &mut Context,
|
||||
room_creation_data: RoomCreationData<'_>,
|
||||
left_room: LeftRoom,
|
||||
notification: notification::Notification<'_>,
|
||||
#[cfg(feature = "e2e-encryption")] e2ee: e2ee::E2EE<'_>,
|
||||
) -> Result<LeftRoomUpdate> {
|
||||
let RoomCreationData {
|
||||
room_id,
|
||||
room_info_notable_update_sender,
|
||||
requested_required_states,
|
||||
ambiguity_cache,
|
||||
} = room_creation_data;
|
||||
|
||||
let state_store = notification.state_store;
|
||||
|
||||
let room =
|
||||
state_store.get_or_create_room(room_id, RoomState::Left, room_info_notable_update_sender);
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_as_left();
|
||||
room_info.mark_state_partially_synced();
|
||||
room_info.handle_encryption_state(requested_required_states.for_room(room_id));
|
||||
|
||||
let (raw_state_events, state_events) = state_events::sync::collect(&left_room.state.events);
|
||||
|
||||
state_events::sync::dispatch(
|
||||
context,
|
||||
(&raw_state_events, &state_events),
|
||||
&mut room_info,
|
||||
ambiguity_cache,
|
||||
&mut (),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (raw_state_events_from_timeline, state_events_from_timeline) =
|
||||
state_events::sync::collect_from_timeline(&left_room.timeline.events);
|
||||
|
||||
state_events::sync::dispatch(
|
||||
context,
|
||||
(&raw_state_events_from_timeline, &state_events_from_timeline),
|
||||
&mut room_info,
|
||||
ambiguity_cache,
|
||||
&mut (),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let timeline = timeline::build(
|
||||
context,
|
||||
&room,
|
||||
&mut room_info,
|
||||
timeline::builder::Timeline::from(left_room.timeline),
|
||||
notification,
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
e2ee,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Save the new `RoomInfo`.
|
||||
context.state_changes.add_room(room_info);
|
||||
|
||||
account_data::for_room(context, room_id, &left_room.account_data.events, state_store).await;
|
||||
|
||||
let ambiguity_changes = ambiguity_cache.changes.remove(room_id).unwrap_or_default();
|
||||
|
||||
Ok(LeftRoomUpdate::new(
|
||||
timeline,
|
||||
left_room.state.events,
|
||||
left_room.account_data.events,
|
||||
ambiguity_changes,
|
||||
))
|
||||
}
|
||||
|
||||
/// Process updates of an invited room.
|
||||
pub async fn update_invited_room(
|
||||
context: &mut Context,
|
||||
room_id: &RoomId,
|
||||
invited_room: InvitedRoom,
|
||||
room_info_notable_update_sender: Sender<RoomInfoNotableUpdate>,
|
||||
notification: notification::Notification<'_>,
|
||||
) -> Result<InvitedRoomUpdate> {
|
||||
let state_store = notification.state_store;
|
||||
|
||||
let room = state_store.get_or_create_room(
|
||||
room_id,
|
||||
RoomState::Invited,
|
||||
room_info_notable_update_sender,
|
||||
);
|
||||
|
||||
let (raw_events, events) = state_events::stripped::collect(&invited_room.invite_state.events);
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_as_invited();
|
||||
room_info.mark_state_fully_synced();
|
||||
|
||||
state_events::stripped::dispatch_invite_or_knock(
|
||||
context,
|
||||
(&raw_events, &events),
|
||||
&room,
|
||||
&mut room_info,
|
||||
notification,
|
||||
)
|
||||
.await?;
|
||||
|
||||
context.state_changes.add_room(room_info);
|
||||
|
||||
Ok(invited_room)
|
||||
}
|
||||
|
||||
/// Process updates of a knocked room.
|
||||
pub async fn update_knocked_room(
|
||||
context: &mut Context,
|
||||
room_id: &RoomId,
|
||||
knocked_room: KnockedRoom,
|
||||
room_info_notable_update_sender: Sender<RoomInfoNotableUpdate>,
|
||||
notification: notification::Notification<'_>,
|
||||
) -> Result<KnockedRoomUpdate> {
|
||||
let state_store = notification.state_store;
|
||||
|
||||
let room = state_store.get_or_create_room(
|
||||
room_id,
|
||||
RoomState::Knocked,
|
||||
room_info_notable_update_sender,
|
||||
);
|
||||
|
||||
let (raw_events, events) = state_events::stripped::collect(&knocked_room.knock_state.events);
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_as_knocked();
|
||||
room_info.mark_state_fully_synced();
|
||||
|
||||
state_events::stripped::dispatch_invite_or_knock(
|
||||
context,
|
||||
(&raw_events, &events),
|
||||
&room,
|
||||
&mut room_info,
|
||||
notification,
|
||||
)
|
||||
.await?;
|
||||
|
||||
context.state_changes.add_room(room_info);
|
||||
|
||||
Ok(knocked_room)
|
||||
}
|
||||
@@ -12,34 +12,33 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
iter,
|
||||
};
|
||||
|
||||
use ruma::{
|
||||
events::{room::member::MembershipState, AnySyncStateEvent},
|
||||
serde::Raw,
|
||||
OwnedUserId,
|
||||
};
|
||||
use ruma::{events::AnySyncStateEvent, serde::Raw};
|
||||
use serde::Deserialize;
|
||||
use tracing::{instrument, warn};
|
||||
use tracing::warn;
|
||||
|
||||
use super::{profiles, Context};
|
||||
use crate::{
|
||||
store::{ambiguity_map::AmbiguityCache, Result as StoreResult},
|
||||
RoomInfo,
|
||||
};
|
||||
use super::Context;
|
||||
|
||||
/// Collect [`AnySyncStateEvent`].
|
||||
pub mod sync {
|
||||
use ruma::events::AnySyncTimelineEvent;
|
||||
use std::{collections::BTreeSet, iter};
|
||||
|
||||
use super::{AnySyncStateEvent, Context, Raw};
|
||||
use ruma::{
|
||||
events::{
|
||||
room::member::{MembershipState, RoomMemberEventContent},
|
||||
AnySyncTimelineEvent, SyncStateEvent,
|
||||
},
|
||||
OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
use tracing::instrument;
|
||||
|
||||
use super::{super::profiles, AnySyncStateEvent, Context, Raw};
|
||||
use crate::{
|
||||
store::{ambiguity_map::AmbiguityCache, Result as StoreResult},
|
||||
RoomInfo,
|
||||
};
|
||||
|
||||
/// Collect [`AnySyncStateEvent`] to [`AnySyncStateEvent`].
|
||||
pub fn collect(
|
||||
_context: &mut Context,
|
||||
raw_events: &[Raw<AnySyncStateEvent>],
|
||||
) -> (Vec<Raw<AnySyncStateEvent>>, Vec<AnySyncStateEvent>) {
|
||||
super::collect(raw_events)
|
||||
@@ -50,7 +49,6 @@ pub mod sync {
|
||||
/// A [`AnySyncTimelineEvent`] can represent either message-like events or
|
||||
/// state events. The message-like events are filtered out.
|
||||
pub fn collect_from_timeline(
|
||||
_context: &mut Context,
|
||||
raw_events: &[Raw<AnySyncTimelineEvent>],
|
||||
) -> (Vec<Raw<AnySyncStateEvent>>, Vec<AnySyncStateEvent>) {
|
||||
super::collect(raw_events.iter().filter_map(|raw_event| {
|
||||
@@ -61,21 +59,176 @@ pub mod sync {
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/// Dispatch the sync state events.
|
||||
///
|
||||
/// `raw_events` and `events` must be generated from [`collect`].
|
||||
/// Events must be exactly the same list of events that are in
|
||||
/// `raw_events`, but deserialised. We demand them here to avoid
|
||||
/// deserialising multiple times.
|
||||
///
|
||||
/// The `new_users` mutable reference allows to collect the new users for
|
||||
/// this room.
|
||||
#[instrument(skip_all, fields(room_id = ?room_info.room_id))]
|
||||
pub async fn dispatch<U>(
|
||||
context: &mut Context,
|
||||
(raw_events, events): (&[Raw<AnySyncStateEvent>], &[AnySyncStateEvent]),
|
||||
room_info: &mut RoomInfo,
|
||||
ambiguity_cache: &mut AmbiguityCache,
|
||||
new_users: &mut U,
|
||||
) -> StoreResult<()>
|
||||
where
|
||||
U: NewUsers,
|
||||
{
|
||||
for (raw_event, event) in iter::zip(raw_events, events) {
|
||||
room_info.handle_state_event(event);
|
||||
|
||||
if let AnySyncStateEvent::RoomMember(member) = event {
|
||||
dispatch_room_member(
|
||||
context,
|
||||
&room_info.room_id,
|
||||
member,
|
||||
ambiguity_cache,
|
||||
new_users,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
context
|
||||
.state_changes
|
||||
.state
|
||||
.entry(room_info.room_id.to_owned())
|
||||
.or_default()
|
||||
.entry(event.event_type())
|
||||
.or_default()
|
||||
.insert(event.state_key().to_owned(), raw_event.clone());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Dispatch a [`RoomMemberEventContent>`] state event.
|
||||
async fn dispatch_room_member<U>(
|
||||
context: &mut Context,
|
||||
room_id: &RoomId,
|
||||
event: &SyncStateEvent<RoomMemberEventContent>,
|
||||
ambiguity_cache: &mut AmbiguityCache,
|
||||
new_users: &mut U,
|
||||
) -> StoreResult<()>
|
||||
where
|
||||
U: NewUsers,
|
||||
{
|
||||
ambiguity_cache.handle_event(&context.state_changes, room_id, event).await?;
|
||||
|
||||
match event.membership() {
|
||||
MembershipState::Join | MembershipState::Invite => {
|
||||
new_users.insert(event.state_key());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
profiles::upsert_or_delete(context, room_id, event);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A trait to collect new users in [`dispatch`].
|
||||
trait NewUsers {
|
||||
/// Insert a new user in the collection of new users.
|
||||
fn insert(&mut self, user_id: &UserId);
|
||||
}
|
||||
|
||||
impl NewUsers for BTreeSet<OwnedUserId> {
|
||||
fn insert(&mut self, user_id: &UserId) {
|
||||
self.insert(user_id.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
impl NewUsers for () {
|
||||
fn insert(&mut self, _user_id: &UserId) {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect [`AnyStrippedStateEvent`].
|
||||
pub mod stripped {
|
||||
use ruma::events::AnyStrippedStateEvent;
|
||||
use std::{collections::BTreeMap, iter};
|
||||
|
||||
use super::{Context, Raw};
|
||||
use ruma::{events::AnyStrippedStateEvent, push::Action};
|
||||
use tracing::instrument;
|
||||
|
||||
use super::{
|
||||
super::{notification, timeline},
|
||||
Context, Raw,
|
||||
};
|
||||
use crate::{Result, Room, RoomInfo};
|
||||
|
||||
/// Collect [`AnyStrippedStateEvent`] to [`AnyStrippedStateEvent`].
|
||||
pub fn collect(
|
||||
_context: &mut Context,
|
||||
raw_events: &[Raw<AnyStrippedStateEvent>],
|
||||
) -> (Vec<Raw<AnyStrippedStateEvent>>, Vec<AnyStrippedStateEvent>) {
|
||||
super::collect(raw_events)
|
||||
}
|
||||
|
||||
/// Dispatch the stripped state events.
|
||||
///
|
||||
/// `raw_events` and `events` must be generated from [`collect`].
|
||||
/// Events must be exactly the same list of events that are in
|
||||
/// `raw_events`, but deserialised. We demand them here to avoid
|
||||
/// deserialising multiple times.
|
||||
///
|
||||
/// Dispatch the stripped state events in `invite_state` or `knock_state`,
|
||||
/// modifying the room's info and posting notifications as needed.
|
||||
///
|
||||
/// * `raw_events` and `events` - The contents of `invite_state` in the form
|
||||
/// of list of pairs of raw stripped state events with their deserialized
|
||||
/// counterpart.
|
||||
/// * `room` - The [`Room`] to modify.
|
||||
/// * `room_info` - The current room's info.
|
||||
/// * `notifications` - Notifications to post for the current room.
|
||||
#[instrument(skip_all, fields(room_id = ?room_info.room_id))]
|
||||
pub(crate) async fn dispatch_invite_or_knock(
|
||||
context: &mut Context,
|
||||
(raw_events, events): (&[Raw<AnyStrippedStateEvent>], &[AnyStrippedStateEvent]),
|
||||
room: &Room,
|
||||
room_info: &mut RoomInfo,
|
||||
mut notification: notification::Notification<'_>,
|
||||
) -> Result<()> {
|
||||
let mut state_events = BTreeMap::new();
|
||||
|
||||
for (raw_event, event) in iter::zip(raw_events, events) {
|
||||
room_info.handle_stripped_state_event(event);
|
||||
state_events
|
||||
.entry(event.event_type())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(event.state_key().to_owned(), raw_event.clone());
|
||||
}
|
||||
|
||||
context
|
||||
.state_changes
|
||||
.stripped_state
|
||||
.insert(room_info.room_id().to_owned(), state_events.clone());
|
||||
|
||||
// We need to check for notifications after we have handled all state
|
||||
// events, to make sure we have the full push context.
|
||||
if let Some(push_condition_room_ctx) =
|
||||
timeline::get_push_room_context(context, room, room_info, notification.state_store)
|
||||
.await?
|
||||
{
|
||||
let room_id = room.room_id();
|
||||
|
||||
// Check every event again for notification.
|
||||
for event in state_events.values().flat_map(|map| map.values()) {
|
||||
notification.push_notification_from_event_if(
|
||||
room_id,
|
||||
&push_condition_room_ctx,
|
||||
event,
|
||||
Action::should_notify,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn collect<'a, I, T>(raw_events: I) -> (Vec<Raw<T>>, Vec<T>)
|
||||
@@ -95,51 +248,45 @@ where
|
||||
.unzip()
|
||||
}
|
||||
|
||||
/// Dispatch the state events and return the new users for this room.
|
||||
///
|
||||
/// `raw_events` and `events` must be generated from [`collect_sync`]. Events
|
||||
/// must be exactly the same list of events that are in raw_events, but
|
||||
/// deserialised. We demand them here to avoid deserialising multiple times.
|
||||
#[instrument(skip_all, fields(room_id = ?room_info.room_id))]
|
||||
pub async fn dispatch_and_get_new_users(
|
||||
context: &mut Context,
|
||||
(raw_events, events): (&[Raw<AnySyncStateEvent>], &[AnySyncStateEvent]),
|
||||
room_info: &mut RoomInfo,
|
||||
ambiguity_cache: &mut AmbiguityCache,
|
||||
) -> StoreResult<BTreeSet<OwnedUserId>> {
|
||||
let mut user_ids = BTreeSet::new();
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use matrix_sdk_test::{
|
||||
async_test, event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent,
|
||||
SyncResponseBuilder, DEFAULT_TEST_ROOM_ID,
|
||||
};
|
||||
use ruma::{event_id, user_id};
|
||||
|
||||
if raw_events.is_empty() {
|
||||
return Ok(user_ids);
|
||||
use crate::test_utils::logged_in_base_client;
|
||||
|
||||
#[async_test]
|
||||
async fn test_state_events_after_sync() {
|
||||
// Given a room
|
||||
let user_id = user_id!("@u:u.to");
|
||||
|
||||
let client = logged_in_base_client(Some(user_id)).await;
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
|
||||
let room_name = EventFactory::new()
|
||||
.sender(user_id)
|
||||
.room_topic("this is the test topic in the timeline")
|
||||
.event_id(event_id!("$2"))
|
||||
.into_raw_sync();
|
||||
|
||||
let response = sync_builder
|
||||
.add_joined_room(
|
||||
JoinedRoomBuilder::new(&DEFAULT_TEST_ROOM_ID)
|
||||
.add_timeline_event(room_name)
|
||||
.add_state_event(StateTestEvent::PowerLevels),
|
||||
)
|
||||
.build_sync_response();
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
let room = client.get_room(&DEFAULT_TEST_ROOM_ID).expect("Just-created room not found!");
|
||||
|
||||
// ensure that we have the power levels
|
||||
assert!(room.power_levels().await.is_ok());
|
||||
|
||||
// ensure that we have the topic
|
||||
assert_eq!(room.topic().unwrap(), "this is the test topic in the timeline");
|
||||
}
|
||||
|
||||
let mut state_events = BTreeMap::new();
|
||||
|
||||
for (raw_event, event) in iter::zip(raw_events, events) {
|
||||
room_info.handle_state_event(event);
|
||||
|
||||
if let AnySyncStateEvent::RoomMember(member) = event {
|
||||
ambiguity_cache
|
||||
.handle_event(&context.state_changes, &room_info.room_id, member)
|
||||
.await?;
|
||||
|
||||
match member.membership() {
|
||||
MembershipState::Join | MembershipState::Invite => {
|
||||
user_ids.insert(member.state_key().to_owned());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
profiles::upsert_or_delete(context, &room_info.room_id, member);
|
||||
}
|
||||
|
||||
state_events
|
||||
.entry(event.event_type())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(event.state_key().to_owned(), raw_event.clone());
|
||||
}
|
||||
|
||||
context.state_changes.state.insert(room_info.room_id.clone(), state_events);
|
||||
|
||||
Ok(user_ids)
|
||||
}
|
||||
|
||||
@@ -28,13 +28,12 @@ use ruma::{
|
||||
};
|
||||
use tracing::{instrument, trace, warn};
|
||||
|
||||
use super::Context;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use super::{e2ee, verification};
|
||||
use super::{notification, Context};
|
||||
use crate::{
|
||||
deserialized_responses::RawAnySyncOrStrippedTimelineEvent,
|
||||
store::{BaseStateStore, StateStoreExt as _},
|
||||
sync::{Notification, Timeline},
|
||||
sync::Timeline,
|
||||
Result, Room, RoomInfo,
|
||||
};
|
||||
|
||||
@@ -51,18 +50,18 @@ pub async fn build<'notification, 'e2ee>(
|
||||
room: &Room,
|
||||
room_info: &mut RoomInfo,
|
||||
timeline_inputs: builder::Timeline,
|
||||
notification_inputs: builder::Notification<'notification>,
|
||||
#[cfg(feature = "e2e-encryption")] e2ee: builder::E2EE<'e2ee>,
|
||||
mut notification: notification::Notification<'notification>,
|
||||
#[cfg(feature = "e2e-encryption")] e2ee: e2ee::E2EE<'e2ee>,
|
||||
) -> Result<Timeline> {
|
||||
let mut timeline = Timeline::new(timeline_inputs.limited, timeline_inputs.prev_batch);
|
||||
let mut push_context =
|
||||
get_push_room_context(context, room, room_info, notification_inputs.state_store).await?;
|
||||
let mut push_condition_room_ctx =
|
||||
get_push_room_context(context, room, room_info, notification.state_store).await?;
|
||||
let room_id = room.room_id();
|
||||
|
||||
for raw_event in timeline_inputs.raw_events {
|
||||
// Start by assuming we have a plaintext event. We'll replace it with a
|
||||
// decrypted or UTD event below if necessary.
|
||||
let mut timeline_event = TimelineEvent::new(raw_event);
|
||||
let mut timeline_event = TimelineEvent::from_plaintext(raw_event);
|
||||
|
||||
// Do some special stuff on the `timeline_event` before collecting it.
|
||||
match timeline_event.raw().deserialize() {
|
||||
@@ -100,12 +99,9 @@ pub async fn build<'notification, 'e2ee>(
|
||||
) => {
|
||||
if let Some(decrypted_timeline_event) =
|
||||
Box::pin(e2ee::decrypt::sync_timeline_event(
|
||||
context,
|
||||
e2ee.olm_machine,
|
||||
e2ee.clone(),
|
||||
timeline_event.raw(),
|
||||
room_id,
|
||||
e2ee.decryption_trust_requirement,
|
||||
e2ee.verification_is_allowed,
|
||||
))
|
||||
.await?
|
||||
{
|
||||
@@ -115,10 +111,8 @@ pub async fn build<'notification, 'e2ee>(
|
||||
|
||||
_ => {
|
||||
Box::pin(verification::process_if_relevant(
|
||||
context,
|
||||
&sync_timeline_event,
|
||||
e2ee.verification_is_allowed,
|
||||
e2ee.olm_machine,
|
||||
e2ee.clone(),
|
||||
room_id,
|
||||
))
|
||||
.await?;
|
||||
@@ -131,36 +125,28 @@ pub async fn build<'notification, 'e2ee>(
|
||||
AnySyncTimelineEvent::MessageLike(_) => (),
|
||||
}
|
||||
|
||||
if let Some(push_context) = &mut push_context {
|
||||
update_push_room_context(context, push_context, room.own_user_id(), room_info)
|
||||
} else {
|
||||
push_context = get_push_room_context(
|
||||
if let Some(push_condition_room_ctx) = &mut push_condition_room_ctx {
|
||||
update_push_room_context(
|
||||
context,
|
||||
room,
|
||||
push_condition_room_ctx,
|
||||
room.own_user_id(),
|
||||
room_info,
|
||||
notification_inputs.state_store,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
push_condition_room_ctx =
|
||||
get_push_room_context(context, room, room_info, notification.state_store)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(context) = &push_context {
|
||||
let actions =
|
||||
notification_inputs.push_rules.get_actions(timeline_event.raw(), context);
|
||||
if let Some(push_condition_room_ctx) = &push_condition_room_ctx {
|
||||
let actions = notification.push_notification_from_event_if(
|
||||
room_id,
|
||||
push_condition_room_ctx,
|
||||
timeline_event.raw(),
|
||||
Action::should_notify,
|
||||
);
|
||||
|
||||
if actions.iter().any(Action::should_notify) {
|
||||
notification_inputs
|
||||
.notifications
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.push(Notification {
|
||||
actions: actions.to_owned(),
|
||||
event: RawAnySyncOrStrippedTimelineEvent::Sync(
|
||||
timeline_event.raw().clone(),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
timeline_event.push_actions = Some(actions.to_owned());
|
||||
timeline_event.set_push_actions(actions.to_owned());
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
@@ -178,20 +164,12 @@ pub async fn build<'notification, 'e2ee>(
|
||||
/// Set of types used by [`build`] to reduce the number of arguments by grouping
|
||||
/// them by thematics.
|
||||
pub mod builder {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_crypto::{OlmMachine, TrustRequirement};
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::{v3, v5},
|
||||
events::AnySyncTimelineEvent,
|
||||
push::Ruleset,
|
||||
serde::Raw,
|
||||
OwnedRoomId,
|
||||
};
|
||||
|
||||
use crate::{store::BaseStateStore, sync};
|
||||
|
||||
pub struct Timeline {
|
||||
pub limited: bool,
|
||||
pub raw_events: Vec<Raw<AnySyncTimelineEvent>>,
|
||||
@@ -213,47 +191,13 @@ pub mod builder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Notification<'a> {
|
||||
pub push_rules: &'a Ruleset,
|
||||
pub notifications: &'a mut BTreeMap<OwnedRoomId, Vec<sync::Notification>>,
|
||||
pub state_store: &'a BaseStateStore,
|
||||
}
|
||||
|
||||
impl<'a> Notification<'a> {
|
||||
pub fn new(
|
||||
push_rules: &'a Ruleset,
|
||||
notifications: &'a mut BTreeMap<OwnedRoomId, Vec<sync::Notification>>,
|
||||
state_store: &'a BaseStateStore,
|
||||
) -> Self {
|
||||
Self { push_rules, notifications, state_store }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub struct E2EE<'a> {
|
||||
pub olm_machine: Option<&'a OlmMachine>,
|
||||
pub decryption_trust_requirement: TrustRequirement,
|
||||
pub verification_is_allowed: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
impl<'a> E2EE<'a> {
|
||||
pub fn new(
|
||||
olm_machine: Option<&'a OlmMachine>,
|
||||
decryption_trust_requirement: TrustRequirement,
|
||||
verification_is_allowed: bool,
|
||||
) -> Self {
|
||||
Self { olm_machine, decryption_trust_requirement, verification_is_allowed }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the push context for the given room.
|
||||
///
|
||||
/// Updates the context data from `context.state_changes` or `room_info`.
|
||||
fn update_push_room_context(
|
||||
context: &mut Context,
|
||||
context: &Context,
|
||||
push_rules: &mut PushConditionRoomCtx,
|
||||
user_id: &UserId,
|
||||
room_info: &RoomInfo,
|
||||
@@ -291,7 +235,7 @@ fn update_push_room_context(
|
||||
/// Returns `None` if some data couldn't be found. This should only happen
|
||||
/// in brand new rooms, while we process its state.
|
||||
pub async fn get_push_room_context(
|
||||
context: &mut Context,
|
||||
context: &Context,
|
||||
room: &Room,
|
||||
room_info: &RoomInfo,
|
||||
state_store: &BaseStateStore,
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk_crypto::OlmMachine;
|
||||
use ruma::{
|
||||
events::{
|
||||
room::message::MessageType, AnySyncMessageLikeEvent, AnySyncTimelineEvent,
|
||||
@@ -21,64 +20,48 @@ use ruma::{
|
||||
RoomId,
|
||||
};
|
||||
|
||||
use super::Context;
|
||||
use super::e2ee::E2EE;
|
||||
use crate::Result;
|
||||
|
||||
/// Process the given event as a verification event if it is a candidate. The
|
||||
/// event must be decrypted.
|
||||
pub async fn process_if_relevant(
|
||||
context: &mut Context,
|
||||
event: &AnySyncTimelineEvent,
|
||||
verification_is_allowed: bool,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
e2ee: E2EE<'_>,
|
||||
room_id: &RoomId,
|
||||
) -> Result<()> {
|
||||
if let AnySyncTimelineEvent::MessageLike(event) = event {
|
||||
// That's it, we are good, the event has been decrypted successfully.
|
||||
|
||||
// However, let's run an additional action. Check if this is a verification
|
||||
// event (`m.key.verification.*`), and call `verification` accordingly.
|
||||
if match &event {
|
||||
// This is an original (i.e. non-redacted) `m.room.message` event and its
|
||||
// content is a verification request…
|
||||
AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(
|
||||
original_event,
|
||||
)) => {
|
||||
matches!(&original_event.content.msgtype, MessageType::VerificationRequest(_))
|
||||
}
|
||||
|
||||
// … or this is verification request event
|
||||
AnySyncMessageLikeEvent::KeyVerificationReady(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationStart(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationCancel(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationAccept(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationKey(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationMac(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationDone(_) => true,
|
||||
|
||||
_ => false,
|
||||
} {
|
||||
verification(context, verification_is_allowed, olm_machine, event, room_id).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn verification(
|
||||
_context: &mut Context,
|
||||
verification_is_allowed: bool,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
event: &AnySyncMessageLikeEvent,
|
||||
room_id: &RoomId,
|
||||
) -> Result<()> {
|
||||
if !verification_is_allowed {
|
||||
if !e2ee.verification_is_allowed {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(olm) = olm_machine {
|
||||
olm.receive_verification_event(&event.clone().into_full_event(room_id.to_owned())).await?;
|
||||
let Some(olm) = e2ee.olm_machine else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let AnySyncTimelineEvent::MessageLike(event) = event else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
match event {
|
||||
// This is an original (i.e. non-redacted) `m.room.message` event and its
|
||||
// content is a verification request…
|
||||
AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(original_event))
|
||||
if matches!(&original_event.content.msgtype, MessageType::VerificationRequest(_)) => {}
|
||||
|
||||
// … or this is verification request event.
|
||||
AnySyncMessageLikeEvent::KeyVerificationReady(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationStart(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationCancel(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationAccept(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationKey(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationMac(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationDone(_) => {}
|
||||
|
||||
_ => {
|
||||
// No need to handle those other event types.
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(olm.receive_verification_event(&event.clone().into_full_event(room_id.to_owned())).await?)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
// Copyright 2025 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 ruma::OwnedUserId;
|
||||
|
||||
use super::Room;
|
||||
|
||||
impl Room {
|
||||
/// Is there a non expired membership with application `m.call` and scope
|
||||
/// `m.room` in this room.
|
||||
pub fn has_active_room_call(&self) -> bool {
|
||||
self.inner.read().has_active_room_call()
|
||||
}
|
||||
|
||||
/// Returns a `Vec` of `OwnedUserId`'s that participate in the room call.
|
||||
///
|
||||
/// MatrixRTC memberships with application `m.call` and scope `m.room` are
|
||||
/// considered. A user can occur twice if they join with two devices.
|
||||
/// Convert to a set depending if the different users are required or the
|
||||
/// amount of sessions.
|
||||
///
|
||||
/// The vector is ordered by oldest membership user to newest.
|
||||
pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
|
||||
self.inner.read().active_room_call_participants()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{ops::Sub, sync::Arc, time::Duration};
|
||||
|
||||
use assign::assign;
|
||||
use matrix_sdk_test::{ALICE, BOB, CAROL};
|
||||
use ruma::{
|
||||
device_id, event_id,
|
||||
events::{
|
||||
call::member::{
|
||||
ActiveFocus, ActiveLivekitFocus, Application, CallApplicationContent,
|
||||
CallMemberEventContent, CallMemberStateKey, Focus, LegacyMembershipData,
|
||||
LegacyMembershipDataInit, LivekitFocus, OriginalSyncCallMemberEvent,
|
||||
},
|
||||
AnySyncStateEvent, StateUnsigned, SyncStateEvent,
|
||||
},
|
||||
room_id,
|
||||
time::SystemTime,
|
||||
user_id, DeviceId, EventId, MilliSecondsSinceUnixEpoch, OwnedUserId, UserId,
|
||||
};
|
||||
use similar_asserts::assert_eq;
|
||||
|
||||
use super::super::{Room, RoomState};
|
||||
use crate::store::MemoryStore;
|
||||
|
||||
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
|
||||
let store = Arc::new(MemoryStore::new());
|
||||
let user_id = user_id!("@me:example.org");
|
||||
let room_id = room_id!("!test:localhost");
|
||||
let (sender, _receiver) = tokio::sync::broadcast::channel(1);
|
||||
|
||||
(store.clone(), Room::new(user_id, store, room_id, room_type, sender))
|
||||
}
|
||||
|
||||
fn timestamp(minutes_ago: u32) -> MilliSecondsSinceUnixEpoch {
|
||||
MilliSecondsSinceUnixEpoch::from_system_time(
|
||||
SystemTime::now().sub(Duration::from_secs((60 * minutes_ago).into())),
|
||||
)
|
||||
.expect("date out of range")
|
||||
}
|
||||
|
||||
fn legacy_membership_for_my_call(
|
||||
device_id: &DeviceId,
|
||||
membership_id: &str,
|
||||
minutes_ago: u32,
|
||||
) -> LegacyMembershipData {
|
||||
let (application, foci) = foci_and_application();
|
||||
assign!(
|
||||
LegacyMembershipData::from(LegacyMembershipDataInit {
|
||||
application,
|
||||
device_id: device_id.to_owned(),
|
||||
expires: Duration::from_millis(3_600_000),
|
||||
foci_active: foci,
|
||||
membership_id: membership_id.to_owned(),
|
||||
}),
|
||||
{ created_ts: Some(timestamp(minutes_ago)) }
|
||||
)
|
||||
}
|
||||
|
||||
fn legacy_member_state_event(
|
||||
memberships: Vec<LegacyMembershipData>,
|
||||
ev_id: &EventId,
|
||||
user_id: &UserId,
|
||||
) -> AnySyncStateEvent {
|
||||
let content = CallMemberEventContent::new_legacy(memberships);
|
||||
|
||||
AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent {
|
||||
content,
|
||||
event_id: ev_id.to_owned(),
|
||||
sender: user_id.to_owned(),
|
||||
// we can simply use now here since this will be dropped when using a MinimalStateEvent
|
||||
// in the roomInfo
|
||||
origin_server_ts: timestamp(0),
|
||||
state_key: CallMemberStateKey::new(user_id.to_owned(), None, false),
|
||||
unsigned: StateUnsigned::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
struct InitData<'a> {
|
||||
device_id: &'a DeviceId,
|
||||
minutes_ago: u32,
|
||||
}
|
||||
|
||||
fn session_member_state_event(
|
||||
ev_id: &EventId,
|
||||
user_id: &UserId,
|
||||
init_data: Option<InitData<'_>>,
|
||||
) -> AnySyncStateEvent {
|
||||
let application = Application::Call(CallApplicationContent::new(
|
||||
"my_call_id_1".to_owned(),
|
||||
ruma::events::call::member::CallScope::Room,
|
||||
));
|
||||
let foci_preferred = vec![Focus::Livekit(LivekitFocus::new(
|
||||
"my_call_foci_alias".to_owned(),
|
||||
"https://lk.org".to_owned(),
|
||||
))];
|
||||
let focus_active = ActiveFocus::Livekit(ActiveLivekitFocus::new());
|
||||
let (content, state_key) = match init_data {
|
||||
Some(InitData { device_id, minutes_ago }) => (
|
||||
CallMemberEventContent::new(
|
||||
application,
|
||||
device_id.to_owned(),
|
||||
focus_active,
|
||||
foci_preferred,
|
||||
Some(timestamp(minutes_ago)),
|
||||
),
|
||||
CallMemberStateKey::new(user_id.to_owned(), Some(device_id.to_owned()), false),
|
||||
),
|
||||
None => (
|
||||
CallMemberEventContent::new_empty(None),
|
||||
CallMemberStateKey::new(user_id.to_owned(), None, false),
|
||||
),
|
||||
};
|
||||
|
||||
AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent {
|
||||
content,
|
||||
event_id: ev_id.to_owned(),
|
||||
sender: user_id.to_owned(),
|
||||
// we can simply use now here since this will be dropped when using a MinimalStateEvent
|
||||
// in the roomInfo
|
||||
origin_server_ts: timestamp(0),
|
||||
state_key,
|
||||
unsigned: StateUnsigned::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn foci_and_application() -> (Application, Vec<Focus>) {
|
||||
(
|
||||
Application::Call(CallApplicationContent::new(
|
||||
"my_call_id_1".to_owned(),
|
||||
ruma::events::call::member::CallScope::Room,
|
||||
)),
|
||||
vec![Focus::Livekit(LivekitFocus::new(
|
||||
"my_call_foci_alias".to_owned(),
|
||||
"https://lk.org".to_owned(),
|
||||
))],
|
||||
)
|
||||
}
|
||||
|
||||
fn receive_state_events(room: &Room, events: Vec<&AnySyncStateEvent>) {
|
||||
room.inner.update_if(|info| {
|
||||
let mut res = false;
|
||||
for ev in events {
|
||||
res |= info.handle_state_event(ev);
|
||||
}
|
||||
res
|
||||
});
|
||||
}
|
||||
|
||||
/// `user_a`: empty memberships
|
||||
/// `user_b`: one membership
|
||||
/// `user_c`: two memberships (two devices)
|
||||
fn legacy_create_call_with_member_events_for_user(a: &UserId, b: &UserId, c: &UserId) -> Room {
|
||||
let (_, room) = make_room_test_helper(RoomState::Joined);
|
||||
|
||||
let a_empty = legacy_member_state_event(Vec::new(), event_id!("$1234"), a);
|
||||
|
||||
// make b 10min old
|
||||
let m_init_b = legacy_membership_for_my_call(device_id!("DEVICE_0"), "0", 1);
|
||||
let b_one = legacy_member_state_event(vec![m_init_b], event_id!("$12345"), b);
|
||||
|
||||
// c1 1min old
|
||||
let m_init_c1 = legacy_membership_for_my_call(device_id!("DEVICE_0"), "0", 10);
|
||||
// c2 20min old
|
||||
let m_init_c2 = legacy_membership_for_my_call(device_id!("DEVICE_1"), "0", 20);
|
||||
let c_two = legacy_member_state_event(vec![m_init_c1, m_init_c2], event_id!("$123456"), c);
|
||||
|
||||
// Intentionally use a non time sorted receive order.
|
||||
receive_state_events(&room, vec![&c_two, &a_empty, &b_one]);
|
||||
|
||||
room
|
||||
}
|
||||
|
||||
/// `user_a`: empty memberships
|
||||
/// `user_b`: one membership
|
||||
/// `user_c`: two memberships (two devices)
|
||||
fn session_create_call_with_member_events_for_user(a: &UserId, b: &UserId, c: &UserId) -> Room {
|
||||
let (_, room) = make_room_test_helper(RoomState::Joined);
|
||||
|
||||
let a_empty = session_member_state_event(event_id!("$1234"), a, None);
|
||||
|
||||
// make b 10min old
|
||||
let b_one = session_member_state_event(
|
||||
event_id!("$12345"),
|
||||
b,
|
||||
Some(InitData { device_id: "DEVICE_0".into(), minutes_ago: 1 }),
|
||||
);
|
||||
|
||||
let m_c1 = session_member_state_event(
|
||||
event_id!("$123456_0"),
|
||||
c,
|
||||
Some(InitData { device_id: "DEVICE_0".into(), minutes_ago: 10 }),
|
||||
);
|
||||
let m_c2 = session_member_state_event(
|
||||
event_id!("$123456_1"),
|
||||
c,
|
||||
Some(InitData { device_id: "DEVICE_1".into(), minutes_ago: 20 }),
|
||||
);
|
||||
// Intentionally use a non time sorted receive order1
|
||||
receive_state_events(&room, vec![&m_c1, &m_c2, &a_empty, &b_one]);
|
||||
|
||||
room
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_show_correct_active_call_state() {
|
||||
let room_legacy = legacy_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
|
||||
|
||||
// This check also tests the ordering.
|
||||
// We want older events to be in the front.
|
||||
// user_b (Bob) is 1min old, c1 (CAROL) 10min old, c2 (CAROL) 20min old
|
||||
assert_eq!(
|
||||
vec![CAROL.to_owned(), CAROL.to_owned(), BOB.to_owned()],
|
||||
room_legacy.active_room_call_participants()
|
||||
);
|
||||
assert!(room_legacy.has_active_room_call());
|
||||
|
||||
let room_session = session_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
|
||||
assert_eq!(
|
||||
vec![CAROL.to_owned(), CAROL.to_owned(), BOB.to_owned()],
|
||||
room_session.active_room_call_participants()
|
||||
);
|
||||
assert!(room_session.has_active_room_call());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_active_call_is_false_when_everyone_left() {
|
||||
let room = legacy_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
|
||||
|
||||
let b_empty_membership = legacy_member_state_event(Vec::new(), event_id!("$1234_1"), &BOB);
|
||||
let c_empty_membership =
|
||||
legacy_member_state_event(Vec::new(), event_id!("$12345_1"), &CAROL);
|
||||
|
||||
receive_state_events(&room, vec![&b_empty_membership, &c_empty_membership]);
|
||||
|
||||
// We have no active call anymore after emptying the memberships
|
||||
assert_eq!(Vec::<OwnedUserId>::new(), room.active_room_call_participants());
|
||||
assert!(!room.has_active_room_call());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// Copyright 2025 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 ruma::{
|
||||
assign,
|
||||
events::{
|
||||
macros::EventContent,
|
||||
room::create::{PreviousRoom, RoomCreateEventContent},
|
||||
EmptyStateKey, RedactContent, RedactedStateEventContent,
|
||||
},
|
||||
room::RoomType,
|
||||
OwnedUserId, RoomVersionId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The content of an `m.room.create` event, with a required `creator` field.
|
||||
///
|
||||
/// Starting with room version 11, the `creator` field should be removed and the
|
||||
/// `sender` field of the event should be used instead. This is reflected on
|
||||
/// [`RoomCreateEventContent`].
|
||||
///
|
||||
/// This type was created as an alternative for ease of use. When it is used in
|
||||
/// the SDK, it is constructed by copying the `sender` of the original event as
|
||||
/// the `creator`.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
|
||||
#[ruma_event(type = "m.room.create", kind = State, state_key_type = EmptyStateKey, custom_redacted)]
|
||||
pub struct RoomCreateWithCreatorEventContent {
|
||||
/// The `user_id` of the room creator.
|
||||
///
|
||||
/// This is set by the homeserver.
|
||||
///
|
||||
/// While this should be optional since room version 11, we copy the sender
|
||||
/// of the event so we can still access it.
|
||||
pub creator: OwnedUserId,
|
||||
|
||||
/// Whether or not this room's data should be transferred to other
|
||||
/// homeservers.
|
||||
#[serde(
|
||||
rename = "m.federate",
|
||||
default = "ruma::serde::default_true",
|
||||
skip_serializing_if = "ruma::serde::is_true"
|
||||
)]
|
||||
pub federate: bool,
|
||||
|
||||
/// The version of the room.
|
||||
///
|
||||
/// Defaults to `RoomVersionId::V1`.
|
||||
#[serde(default = "default_create_room_version_id")]
|
||||
pub room_version: RoomVersionId,
|
||||
|
||||
/// A reference to the room this room replaces, if the previous room was
|
||||
/// upgraded.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub predecessor: Option<PreviousRoom>,
|
||||
|
||||
/// The room type.
|
||||
///
|
||||
/// This is currently only used for spaces.
|
||||
#[serde(skip_serializing_if = "Option::is_none", rename = "type")]
|
||||
pub room_type: Option<RoomType>,
|
||||
}
|
||||
|
||||
impl RoomCreateWithCreatorEventContent {
|
||||
/// Constructs a `RoomCreateWithCreatorEventContent` with the given original
|
||||
/// content and sender.
|
||||
pub fn from_event_content(content: RoomCreateEventContent, sender: OwnedUserId) -> Self {
|
||||
let RoomCreateEventContent { federate, room_version, predecessor, room_type, .. } = content;
|
||||
Self { creator: sender, federate, room_version, predecessor, room_type }
|
||||
}
|
||||
|
||||
fn into_event_content(self) -> (RoomCreateEventContent, OwnedUserId) {
|
||||
let Self { creator, federate, room_version, predecessor, room_type } = self;
|
||||
|
||||
#[allow(deprecated)]
|
||||
let content = assign!(RoomCreateEventContent::new_v11(), {
|
||||
creator: Some(creator.clone()),
|
||||
federate,
|
||||
room_version,
|
||||
predecessor,
|
||||
room_type,
|
||||
});
|
||||
|
||||
(content, creator)
|
||||
}
|
||||
}
|
||||
|
||||
/// Redacted form of [`RoomCreateWithCreatorEventContent`].
|
||||
pub type RedactedRoomCreateWithCreatorEventContent = RoomCreateWithCreatorEventContent;
|
||||
|
||||
impl RedactedStateEventContent for RedactedRoomCreateWithCreatorEventContent {
|
||||
type StateKey = EmptyStateKey;
|
||||
}
|
||||
|
||||
impl RedactContent for RoomCreateWithCreatorEventContent {
|
||||
type Redacted = RedactedRoomCreateWithCreatorEventContent;
|
||||
|
||||
fn redact(self, version: &RoomVersionId) -> Self::Redacted {
|
||||
let (content, sender) = self.into_event_content();
|
||||
// Use Ruma's redaction algorithm.
|
||||
let content = content.redact(version);
|
||||
Self::from_event_content(content, sender)
|
||||
}
|
||||
}
|
||||
|
||||
fn default_create_room_version_id() -> RoomVersionId {
|
||||
RoomVersionId::V1
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,158 @@
|
||||
// Copyright 2025 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 ruma::events::room::encryption::RoomEncryptionEventContent;
|
||||
|
||||
use super::Room;
|
||||
|
||||
impl Room {
|
||||
/// Get the encryption state of this room.
|
||||
pub fn encryption_state(&self) -> EncryptionState {
|
||||
self.inner.read().encryption_state()
|
||||
}
|
||||
|
||||
/// Get the `m.room.encryption` content that enabled end to end encryption
|
||||
/// in the room.
|
||||
pub fn encryption_settings(&self) -> Option<RoomEncryptionEventContent> {
|
||||
self.inner.read().base_info.encryption.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the state of a room encryption.
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
|
||||
pub enum EncryptionState {
|
||||
/// The room is encrypted.
|
||||
Encrypted,
|
||||
|
||||
/// The room is not encrypted.
|
||||
NotEncrypted,
|
||||
|
||||
/// The state of the room encryption is unknown, probably because the
|
||||
/// `/sync` did not provide all data needed to decide.
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl EncryptionState {
|
||||
/// Check whether `EncryptionState` is [`Encrypted`][Self::Encrypted].
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
matches!(self, Self::Encrypted)
|
||||
}
|
||||
|
||||
/// Check whether `EncryptionState` is [`Unknown`][Self::Unknown].
|
||||
pub fn is_unknown(&self) -> bool {
|
||||
matches!(self, Self::Unknown)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
ops::{Not, Sub},
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk_test::ALICE;
|
||||
use ruma::{
|
||||
events::{
|
||||
room::encryption::{OriginalSyncRoomEncryptionEvent, RoomEncryptionEventContent},
|
||||
AnySyncStateEvent, EmptyStateKey, StateUnsigned, SyncStateEvent,
|
||||
},
|
||||
room_id,
|
||||
time::SystemTime,
|
||||
user_id, EventEncryptionAlgorithm, MilliSecondsSinceUnixEpoch, OwnedEventId,
|
||||
};
|
||||
|
||||
use super::{EncryptionState, Room};
|
||||
use crate::{store::MemoryStore, RoomState};
|
||||
|
||||
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
|
||||
let store = Arc::new(MemoryStore::new());
|
||||
let user_id = user_id!("@me:example.org");
|
||||
let room_id = room_id!("!test:localhost");
|
||||
let (sender, _receiver) = tokio::sync::broadcast::channel(1);
|
||||
|
||||
(store.clone(), Room::new(user_id, store, room_id, room_type, sender))
|
||||
}
|
||||
|
||||
fn timestamp(minutes_ago: u32) -> MilliSecondsSinceUnixEpoch {
|
||||
MilliSecondsSinceUnixEpoch::from_system_time(
|
||||
SystemTime::now().sub(Duration::from_secs((60 * minutes_ago).into())),
|
||||
)
|
||||
.expect("date out of range")
|
||||
}
|
||||
|
||||
fn receive_state_events(room: &Room, events: Vec<&AnySyncStateEvent>) {
|
||||
room.inner.update_if(|info| {
|
||||
let mut res = false;
|
||||
for ev in events {
|
||||
res |= info.handle_state_event(ev);
|
||||
}
|
||||
res
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encryption_is_set_when_encryption_event_is_received_encrypted() {
|
||||
let (_store, room) = make_room_test_helper(RoomState::Joined);
|
||||
|
||||
assert_matches!(room.encryption_state(), EncryptionState::Unknown);
|
||||
|
||||
let encryption_content =
|
||||
RoomEncryptionEventContent::new(EventEncryptionAlgorithm::MegolmV1AesSha2);
|
||||
let encryption_event = AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(
|
||||
OriginalSyncRoomEncryptionEvent {
|
||||
content: encryption_content,
|
||||
event_id: OwnedEventId::from_str("$1234_1").unwrap(),
|
||||
sender: ALICE.to_owned(),
|
||||
// we can simply use now here since this will be dropped when using a
|
||||
// MinimalStateEvent in the roomInfo
|
||||
origin_server_ts: timestamp(0),
|
||||
state_key: EmptyStateKey,
|
||||
unsigned: StateUnsigned::new(),
|
||||
},
|
||||
));
|
||||
receive_state_events(&room, vec![&encryption_event]);
|
||||
|
||||
assert_matches!(room.encryption_state(), EncryptionState::Encrypted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encryption_is_set_when_encryption_event_is_received_not_encrypted() {
|
||||
let (_store, room) = make_room_test_helper(RoomState::Joined);
|
||||
|
||||
assert_matches!(room.encryption_state(), EncryptionState::Unknown);
|
||||
room.inner.update_if(|info| {
|
||||
info.mark_encryption_state_synced();
|
||||
|
||||
false
|
||||
});
|
||||
|
||||
assert_matches!(room.encryption_state(), EncryptionState::NotEncrypted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encryption_state() {
|
||||
assert!(EncryptionState::Unknown.is_unknown());
|
||||
assert!(EncryptionState::Encrypted.is_unknown().not());
|
||||
assert!(EncryptionState::NotEncrypted.is_unknown().not());
|
||||
|
||||
assert!(EncryptionState::Unknown.is_encrypted().not());
|
||||
assert!(EncryptionState::Encrypted.is_encrypted());
|
||||
assert!(EncryptionState::NotEncrypted.is_encrypted().not());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use eyeball::{AsyncLock, ObservableWriteGuard};
|
||||
use ruma::{
|
||||
events::{
|
||||
room::member::{MembershipState, RoomMemberEventContent},
|
||||
StateEventType, SyncStateEvent,
|
||||
},
|
||||
OwnedEventId, OwnedUserId,
|
||||
};
|
||||
use tracing::warn;
|
||||
|
||||
use super::Room;
|
||||
use crate::{
|
||||
deserialized_responses::{MemberEvent, RawMemberEvent, SyncOrStrippedState},
|
||||
store::{Result as StoreResult, StateStoreExt},
|
||||
StateStoreDataKey, StateStoreDataValue, StoreError,
|
||||
};
|
||||
|
||||
impl Room {
|
||||
/// Mark a list of requests to join the room as seen, given their state
|
||||
/// event ids.
|
||||
pub async fn mark_knock_requests_as_seen(&self, user_ids: &[OwnedUserId]) -> StoreResult<()> {
|
||||
let raw_user_ids: Vec<&str> = user_ids.iter().map(|id| id.as_str()).collect();
|
||||
let member_raw_events = self
|
||||
.store
|
||||
.get_state_events_for_keys(self.room_id(), StateEventType::RoomMember, &raw_user_ids)
|
||||
.await?;
|
||||
let mut event_to_user_ids = Vec::with_capacity(member_raw_events.len());
|
||||
|
||||
// Map the list of events ids to their user ids, if they are event ids for knock
|
||||
// membership events. Log an error and continue otherwise.
|
||||
for raw_event in member_raw_events {
|
||||
let event = raw_event.cast::<RoomMemberEventContent>().deserialize()?;
|
||||
match event {
|
||||
SyncOrStrippedState::Sync(SyncStateEvent::Original(event)) => {
|
||||
if event.content.membership == MembershipState::Knock {
|
||||
event_to_user_ids.push((event.event_id, event.state_key))
|
||||
} else {
|
||||
warn!("Could not mark knock event as seen: event {} for user {} is not in Knock membership state.", event.event_id, event.state_key);
|
||||
}
|
||||
}
|
||||
_ => warn!(
|
||||
"Could not mark knock event as seen: event for user {} is not valid.",
|
||||
event.state_key()
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
let current_seen_events_guard = self.get_write_guarded_current_knock_request_ids().await?;
|
||||
let mut current_seen_events = current_seen_events_guard.clone().unwrap_or_default();
|
||||
|
||||
current_seen_events.extend(event_to_user_ids);
|
||||
|
||||
self.update_seen_knock_request_ids(current_seen_events_guard, current_seen_events).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes the seen knock request ids that are no longer valid given the
|
||||
/// current room members.
|
||||
pub async fn remove_outdated_seen_knock_requests_ids(&self) -> StoreResult<()> {
|
||||
let current_seen_events_guard = self.get_write_guarded_current_knock_request_ids().await?;
|
||||
let mut current_seen_events = current_seen_events_guard.clone().unwrap_or_default();
|
||||
|
||||
// Get and deserialize the member events for the seen knock requests
|
||||
let keys: Vec<OwnedUserId> = current_seen_events.values().map(|id| id.to_owned()).collect();
|
||||
let raw_member_events: Vec<RawMemberEvent> =
|
||||
self.store.get_state_events_for_keys_static(self.room_id(), &keys).await?;
|
||||
let member_events = raw_member_events
|
||||
.into_iter()
|
||||
.map(|raw| raw.deserialize())
|
||||
.collect::<Result<Vec<MemberEvent>, _>>()?;
|
||||
|
||||
let mut ids_to_remove = Vec::new();
|
||||
|
||||
for (event_id, user_id) in current_seen_events.iter() {
|
||||
// Check the seen knock request ids against the current room member events for
|
||||
// the room members associated to them
|
||||
let matching_member = member_events.iter().find(|event| event.user_id() == user_id);
|
||||
|
||||
if let Some(member) = matching_member {
|
||||
let member_event_id = member.event_id();
|
||||
// If the member event is not a knock or it's different knock, it's outdated
|
||||
if *member.membership() != MembershipState::Knock
|
||||
|| member_event_id.is_some_and(|id| id != event_id)
|
||||
{
|
||||
ids_to_remove.push(event_id.to_owned());
|
||||
}
|
||||
} else {
|
||||
ids_to_remove.push(event_id.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no ids to remove, do nothing
|
||||
if ids_to_remove.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for event_id in ids_to_remove {
|
||||
current_seen_events.remove(&event_id);
|
||||
}
|
||||
|
||||
self.update_seen_knock_request_ids(current_seen_events_guard, current_seen_events).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the list of seen knock request event ids in this room.
|
||||
pub async fn get_seen_knock_request_ids(
|
||||
&self,
|
||||
) -> Result<BTreeMap<OwnedEventId, OwnedUserId>, StoreError> {
|
||||
Ok(self.get_write_guarded_current_knock_request_ids().await?.clone().unwrap_or_default())
|
||||
}
|
||||
|
||||
async fn get_write_guarded_current_knock_request_ids(
|
||||
&self,
|
||||
) -> StoreResult<ObservableWriteGuard<'_, Option<BTreeMap<OwnedEventId, OwnedUserId>>, AsyncLock>>
|
||||
{
|
||||
let mut guard = self.seen_knock_request_ids_map.write().await;
|
||||
// If there are no loaded request ids yet
|
||||
if guard.is_none() {
|
||||
// Load the values from the store and update the shared observable contents
|
||||
let updated_seen_ids = self
|
||||
.store
|
||||
.get_kv_data(StateStoreDataKey::SeenKnockRequests(self.room_id()))
|
||||
.await?
|
||||
.and_then(|v| v.into_seen_knock_requests())
|
||||
.unwrap_or_default();
|
||||
|
||||
ObservableWriteGuard::set(&mut guard, Some(updated_seen_ids));
|
||||
}
|
||||
Ok(guard)
|
||||
}
|
||||
|
||||
async fn update_seen_knock_request_ids(
|
||||
&self,
|
||||
mut guard: ObservableWriteGuard<'_, Option<BTreeMap<OwnedEventId, OwnedUserId>>, AsyncLock>,
|
||||
new_value: BTreeMap<OwnedEventId, OwnedUserId>,
|
||||
) -> StoreResult<()> {
|
||||
// Save the new values to the shared observable
|
||||
ObservableWriteGuard::set(&mut guard, Some(new_value.clone()));
|
||||
|
||||
// Save them into the store too
|
||||
self.store
|
||||
.set_kv_data(
|
||||
StateStoreDataKey::SeenKnockRequests(self.room_id()),
|
||||
StateStoreDataValue::SeenKnockRequests(new_value),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
// Copyright 2025 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.
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use std::{collections::BTreeMap, num::NonZeroUsize};
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::{events::AnySyncTimelineEvent, serde::Raw, OwnedRoomId};
|
||||
|
||||
use super::Room;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use super::RoomInfoNotableUpdateReasons;
|
||||
use crate::latest_event::LatestEvent;
|
||||
|
||||
impl Room {
|
||||
/// The size of the latest_encrypted_events RingBuffer
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub(super) const MAX_ENCRYPTED_EVENTS: NonZeroUsize = NonZeroUsize::new(10).unwrap();
|
||||
|
||||
/// Return the last event in this room, if one has been cached during
|
||||
/// sliding sync.
|
||||
pub fn latest_event(&self) -> Option<LatestEvent> {
|
||||
self.inner.read().latest_event.as_deref().cloned()
|
||||
}
|
||||
|
||||
/// Return the most recent few encrypted events. When the keys come through
|
||||
/// to decrypt these, the most recent relevant one will replace
|
||||
/// latest_event. (We can't tell which one is relevant until
|
||||
/// they are decrypted.)
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub(crate) fn latest_encrypted_events(&self) -> Vec<Raw<AnySyncTimelineEvent>> {
|
||||
self.latest_encrypted_events.read().unwrap().iter().cloned().collect()
|
||||
}
|
||||
|
||||
/// Replace our latest_event with the supplied event, and delete it and all
|
||||
/// older encrypted events from latest_encrypted_events, given that the
|
||||
/// new event was at the supplied index in the latest_encrypted_events
|
||||
/// list.
|
||||
///
|
||||
/// Panics if index is not a valid index in the latest_encrypted_events
|
||||
/// list.
|
||||
///
|
||||
/// It is the responsibility of the caller to apply the changes into the
|
||||
/// state store after calling this function.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub(crate) fn on_latest_event_decrypted(
|
||||
&self,
|
||||
latest_event: Box<LatestEvent>,
|
||||
index: usize,
|
||||
changes: &mut crate::StateChanges,
|
||||
room_info_notable_updates: &mut BTreeMap<OwnedRoomId, RoomInfoNotableUpdateReasons>,
|
||||
) {
|
||||
self.latest_encrypted_events.write().unwrap().drain(0..=index);
|
||||
|
||||
let room_info = changes
|
||||
.room_infos
|
||||
.entry(self.room_id().to_owned())
|
||||
.or_insert_with(|| self.clone_info());
|
||||
|
||||
room_info.latest_event = Some(latest_event);
|
||||
|
||||
room_info_notable_updates
|
||||
.entry(self.room_id().to_owned())
|
||||
.or_default()
|
||||
.insert(RoomInfoNotableUpdateReasons::LATEST_EVENT);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "e2e-encryption"))]
|
||||
mod tests_with_e2e_encryption {
|
||||
use std::sync::Arc;
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{room_id, serde::Raw, user_id};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
latest_event::LatestEvent,
|
||||
response_processors as processors,
|
||||
store::{MemoryStore, RoomLoadSettings, StoreConfig},
|
||||
BaseClient, Room, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomState,
|
||||
SessionMeta, StateChanges,
|
||||
};
|
||||
|
||||
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
|
||||
let store = Arc::new(MemoryStore::new());
|
||||
let user_id = user_id!("@me:example.org");
|
||||
let room_id = room_id!("!test:localhost");
|
||||
let (sender, _receiver) = tokio::sync::broadcast::channel(1);
|
||||
|
||||
(store.clone(), Room::new(user_id, store, room_id, room_type, sender))
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_setting_the_latest_event_doesnt_cause_a_room_info_notable_update() {
|
||||
// Given a room,
|
||||
let client =
|
||||
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
|
||||
|
||||
client
|
||||
.activate(
|
||||
SessionMeta {
|
||||
user_id: user_id!("@alice:example.org").into(),
|
||||
device_id: ruma::device_id!("AYEAYEAYE").into(),
|
||||
},
|
||||
RoomLoadSettings::default(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let room_id = room_id!("!test:localhost");
|
||||
let room = client.get_or_create_room(room_id, RoomState::Joined);
|
||||
|
||||
// That has an encrypted event,
|
||||
add_encrypted_event(&room, "$A");
|
||||
// Sanity: it has no latest_event
|
||||
assert!(room.latest_event().is_none());
|
||||
|
||||
// When I set up an observer on the latest_event,
|
||||
let mut room_info_notable_update = client.room_info_notable_update_receiver();
|
||||
|
||||
// And I provide a decrypted event to replace the encrypted one,
|
||||
let event = make_latest_event("$A");
|
||||
|
||||
let mut context = processors::Context::default();
|
||||
room.on_latest_event_decrypted(
|
||||
event.clone(),
|
||||
0,
|
||||
&mut context.state_changes,
|
||||
&mut context.room_info_notable_updates,
|
||||
);
|
||||
|
||||
assert!(context.room_info_notable_updates.contains_key(room_id));
|
||||
|
||||
// The subscriber isn't notified at this point.
|
||||
assert!(room_info_notable_update.is_empty());
|
||||
|
||||
// Then updating the room info will store the event,
|
||||
processors::changes::save_and_apply(
|
||||
context,
|
||||
&client.state_store,
|
||||
&client.ignore_user_list_changes,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
|
||||
|
||||
// And wake up the subscriber.
|
||||
assert_matches!(
|
||||
room_info_notable_update.recv().await,
|
||||
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
|
||||
assert_eq!(received_room_id, room_id);
|
||||
assert!(reasons.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_when_we_provide_a_newly_decrypted_event_it_replaces_latest_event() {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
// Given a room with an encrypted event
|
||||
let (_store, room) = make_room_test_helper(RoomState::Joined);
|
||||
add_encrypted_event(&room, "$A");
|
||||
// Sanity: it has no latest_event
|
||||
assert!(room.latest_event().is_none());
|
||||
|
||||
// When I provide a decrypted event to replace the encrypted one
|
||||
let event = make_latest_event("$A");
|
||||
let mut changes = StateChanges::default();
|
||||
let mut room_info_notable_updates = BTreeMap::new();
|
||||
room.on_latest_event_decrypted(
|
||||
event.clone(),
|
||||
0,
|
||||
&mut changes,
|
||||
&mut room_info_notable_updates,
|
||||
);
|
||||
room.set_room_info(
|
||||
changes.room_infos.get(room.room_id()).cloned().unwrap(),
|
||||
room_info_notable_updates.get(room.room_id()).copied().unwrap(),
|
||||
);
|
||||
|
||||
// Then is it stored
|
||||
assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_when_a_newly_decrypted_event_appears_we_delete_all_older_encrypted_events() {
|
||||
// Given a room with some encrypted events and a latest event
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
let (_store, room) = make_room_test_helper(RoomState::Joined);
|
||||
room.inner.update(|info| info.latest_event = Some(make_latest_event("$A")));
|
||||
add_encrypted_event(&room, "$0");
|
||||
add_encrypted_event(&room, "$1");
|
||||
add_encrypted_event(&room, "$2");
|
||||
add_encrypted_event(&room, "$3");
|
||||
|
||||
// When I provide a latest event
|
||||
let new_event = make_latest_event("$1");
|
||||
let new_event_index = 1;
|
||||
let mut changes = StateChanges::default();
|
||||
let mut room_info_notable_updates = BTreeMap::new();
|
||||
room.on_latest_event_decrypted(
|
||||
new_event.clone(),
|
||||
new_event_index,
|
||||
&mut changes,
|
||||
&mut room_info_notable_updates,
|
||||
);
|
||||
room.set_room_info(
|
||||
changes.room_infos.get(room.room_id()).cloned().unwrap(),
|
||||
room_info_notable_updates.get(room.room_id()).copied().unwrap(),
|
||||
);
|
||||
|
||||
// Then the encrypted events list is shortened to only newer events
|
||||
let enc_evs = room.latest_encrypted_events();
|
||||
assert_eq!(enc_evs.len(), 2);
|
||||
assert_eq!(enc_evs[0].get_field::<&str>("event_id").unwrap().unwrap(), "$2");
|
||||
assert_eq!(enc_evs[1].get_field::<&str>("event_id").unwrap().unwrap(), "$3");
|
||||
|
||||
// And the event is stored
|
||||
assert_eq!(room.latest_event().unwrap().event_id(), new_event.event_id());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_replacing_the_newest_event_leaves_none_left() {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
// Given a room with some encrypted events
|
||||
let (_store, room) = make_room_test_helper(RoomState::Joined);
|
||||
add_encrypted_event(&room, "$0");
|
||||
add_encrypted_event(&room, "$1");
|
||||
add_encrypted_event(&room, "$2");
|
||||
add_encrypted_event(&room, "$3");
|
||||
|
||||
// When I provide a latest event and say it was the very latest
|
||||
let new_event = make_latest_event("$3");
|
||||
let new_event_index = 3;
|
||||
let mut changes = StateChanges::default();
|
||||
let mut room_info_notable_updates = BTreeMap::new();
|
||||
room.on_latest_event_decrypted(
|
||||
new_event,
|
||||
new_event_index,
|
||||
&mut changes,
|
||||
&mut room_info_notable_updates,
|
||||
);
|
||||
room.set_room_info(
|
||||
changes.room_infos.get(room.room_id()).cloned().unwrap(),
|
||||
room_info_notable_updates.get(room.room_id()).copied().unwrap(),
|
||||
);
|
||||
|
||||
// Then the encrypted events list ie empty
|
||||
let enc_evs = room.latest_encrypted_events();
|
||||
assert_eq!(enc_evs.len(), 0);
|
||||
}
|
||||
|
||||
fn add_encrypted_event(room: &Room, event_id: &str) {
|
||||
room.latest_encrypted_events
|
||||
.write()
|
||||
.unwrap()
|
||||
.push(Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap());
|
||||
}
|
||||
|
||||
fn make_latest_event(event_id: &str) -> Box<LatestEvent> {
|
||||
Box::new(LatestEvent::new(TimelineEvent::from_plaintext(
|
||||
Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
+238
-3
@@ -13,28 +13,188 @@
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeSet, HashMap},
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
mem,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use bitflags::bitflags;
|
||||
use ruma::{
|
||||
events::{
|
||||
ignored_user_list::IgnoredUserListEventContent,
|
||||
presence::PresenceEvent,
|
||||
room::{
|
||||
member::MembershipState,
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
power_levels::{PowerLevelAction, RoomPowerLevels, RoomPowerLevelsEventContent},
|
||||
},
|
||||
MessageLikeEventType, StateEventType,
|
||||
},
|
||||
MxcUri, OwnedUserId, UserId,
|
||||
};
|
||||
use tracing::debug;
|
||||
|
||||
use super::Room;
|
||||
use crate::{
|
||||
deserialized_responses::{DisplayName, MemberEvent, SyncOrStrippedState},
|
||||
store::ambiguity_map::is_display_name_ambiguous,
|
||||
store::{ambiguity_map::is_display_name_ambiguous, Result as StoreResult, StateStoreExt},
|
||||
MinimalRoomMemberEvent,
|
||||
};
|
||||
|
||||
impl Room {
|
||||
/// Check if the room has its members fully synced.
|
||||
///
|
||||
/// Members might be missing if lazy member loading was enabled for the
|
||||
/// sync.
|
||||
///
|
||||
/// Returns true if no members are missing, false otherwise.
|
||||
pub fn are_members_synced(&self) -> bool {
|
||||
self.inner.read().members_synced
|
||||
}
|
||||
|
||||
/// Mark this Room as holding all member information.
|
||||
///
|
||||
/// Useful in tests if we want to persuade the Room not to sync when asked
|
||||
/// about its members.
|
||||
#[cfg(feature = "testing")]
|
||||
pub fn mark_members_synced(&self) {
|
||||
self.inner.update(|info| {
|
||||
info.members_synced = true;
|
||||
});
|
||||
}
|
||||
|
||||
/// Mark this Room as still missing member information.
|
||||
pub fn mark_members_missing(&self) {
|
||||
self.inner.update_if(|info| {
|
||||
// notify observable subscribers only if the previous value was false
|
||||
mem::replace(&mut info.members_synced, false)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the `RoomMember`s of this room that are known to the store, with the
|
||||
/// given memberships.
|
||||
pub async fn members(&self, memberships: RoomMemberships) -> StoreResult<Vec<RoomMember>> {
|
||||
let user_ids = self.store.get_user_ids(self.room_id(), memberships).await?;
|
||||
|
||||
if user_ids.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let member_events = self
|
||||
.store
|
||||
.get_state_events_for_keys_static::<RoomMemberEventContent, _, _>(
|
||||
self.room_id(),
|
||||
&user_ids,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|raw_event| raw_event.deserialize())
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let mut profiles = self.store.get_profiles(self.room_id(), &user_ids).await?;
|
||||
|
||||
let mut presences = self
|
||||
.store
|
||||
.get_presence_events(&user_ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter_map(|e| {
|
||||
e.deserialize().ok().map(|presence| (presence.sender.clone(), presence))
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
let display_names = member_events.iter().map(|e| e.display_name()).collect::<Vec<_>>();
|
||||
let room_info = self.member_room_info(&display_names).await?;
|
||||
|
||||
let mut members = Vec::new();
|
||||
|
||||
for event in member_events {
|
||||
let profile = profiles.remove(event.user_id());
|
||||
let presence = presences.remove(event.user_id());
|
||||
members.push(RoomMember::from_parts(event, profile, presence, &room_info))
|
||||
}
|
||||
|
||||
Ok(members)
|
||||
}
|
||||
|
||||
/// Returns the number of members who have joined or been invited to the
|
||||
/// room.
|
||||
pub fn active_members_count(&self) -> u64 {
|
||||
self.inner.read().active_members_count()
|
||||
}
|
||||
|
||||
/// Returns the number of members who have been invited to the room.
|
||||
pub fn invited_members_count(&self) -> u64 {
|
||||
self.inner.read().invited_members_count()
|
||||
}
|
||||
|
||||
/// Returns the number of members who have joined the room.
|
||||
pub fn joined_members_count(&self) -> u64 {
|
||||
self.inner.read().joined_members_count()
|
||||
}
|
||||
|
||||
/// Get the `RoomMember` with the given `user_id`.
|
||||
///
|
||||
/// Returns `None` if the member was never part of this room, otherwise
|
||||
/// return a `RoomMember` that can be in a joined, RoomState::Invited, left,
|
||||
/// banned state.
|
||||
///
|
||||
/// Async because it can read from storage.
|
||||
pub async fn get_member(&self, user_id: &UserId) -> StoreResult<Option<RoomMember>> {
|
||||
let Some(raw_event) = self.store.get_member_event(self.room_id(), user_id).await? else {
|
||||
debug!(%user_id, "Member event not found in state store");
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let event = raw_event.deserialize()?;
|
||||
|
||||
let presence =
|
||||
self.store.get_presence_event(user_id).await?.and_then(|e| e.deserialize().ok());
|
||||
|
||||
let profile = self.store.get_profile(self.room_id(), user_id).await?;
|
||||
|
||||
let display_names = [event.display_name()];
|
||||
let room_info = self.member_room_info(&display_names).await?;
|
||||
|
||||
Ok(Some(RoomMember::from_parts(event, profile, presence, &room_info)))
|
||||
}
|
||||
|
||||
/// The current `MemberRoomInfo` for this room.
|
||||
///
|
||||
/// Async because it can read from storage.
|
||||
async fn member_room_info<'a>(
|
||||
&self,
|
||||
display_names: &'a [DisplayName],
|
||||
) -> StoreResult<MemberRoomInfo<'a>> {
|
||||
let max_power_level = self.max_power_level();
|
||||
let room_creator = self.inner.read().creator().map(ToOwned::to_owned);
|
||||
|
||||
let power_levels = self
|
||||
.store
|
||||
.get_state_event_static(self.room_id())
|
||||
.await?
|
||||
.and_then(|e| e.deserialize().ok());
|
||||
|
||||
let users_display_names =
|
||||
self.store.get_users_with_display_names(self.room_id(), display_names).await?;
|
||||
|
||||
let ignored_users = self
|
||||
.store
|
||||
.get_account_data_event_static::<IgnoredUserListEventContent>()
|
||||
.await?
|
||||
.map(|c| c.deserialize())
|
||||
.transpose()?
|
||||
.map(|e| e.content.ignored_users.into_keys().collect());
|
||||
|
||||
Ok(MemberRoomInfo {
|
||||
power_levels: power_levels.into(),
|
||||
max_power_level,
|
||||
room_creator,
|
||||
users_display_names,
|
||||
ignored_users,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A member of a room.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RoomMember {
|
||||
@@ -251,3 +411,78 @@ pub(crate) struct MemberRoomInfo<'a> {
|
||||
pub(crate) users_display_names: HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>,
|
||||
pub(crate) ignored_users: Option<BTreeSet<OwnedUserId>>,
|
||||
}
|
||||
|
||||
/// The kind of room member updates that just happened.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RoomMembersUpdate {
|
||||
/// The whole list room members was reloaded.
|
||||
FullReload,
|
||||
/// A few members were updated, their user ids are included.
|
||||
Partial(BTreeSet<OwnedUserId>),
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Room membership filter as a bitset.
|
||||
///
|
||||
/// Note that [`RoomMemberships::empty()`] doesn't filter the results and
|
||||
/// [`RoomMemberships::all()`] filters out unknown memberships.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct RoomMemberships: u16 {
|
||||
/// The member joined the room.
|
||||
const JOIN = 0b00000001;
|
||||
/// The member was invited to the room.
|
||||
const INVITE = 0b00000010;
|
||||
/// The member requested to join the room.
|
||||
const KNOCK = 0b00000100;
|
||||
/// The member left the room.
|
||||
const LEAVE = 0b00001000;
|
||||
/// The member was banned.
|
||||
const BAN = 0b00010000;
|
||||
|
||||
/// The member is active in the room (i.e. joined or invited).
|
||||
const ACTIVE = Self::JOIN.bits() | Self::INVITE.bits();
|
||||
}
|
||||
}
|
||||
|
||||
impl RoomMemberships {
|
||||
/// Whether the given membership matches this `RoomMemberships`.
|
||||
pub fn matches(&self, membership: &MembershipState) -> bool {
|
||||
if self.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
let membership = match membership {
|
||||
MembershipState::Ban => Self::BAN,
|
||||
MembershipState::Invite => Self::INVITE,
|
||||
MembershipState::Join => Self::JOIN,
|
||||
MembershipState::Knock => Self::KNOCK,
|
||||
MembershipState::Leave => Self::LEAVE,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
self.contains(membership)
|
||||
}
|
||||
|
||||
/// Get this `RoomMemberships` as a list of matching [`MembershipState`]s.
|
||||
pub fn as_vec(&self) -> Vec<MembershipState> {
|
||||
let mut memberships = Vec::new();
|
||||
|
||||
if self.contains(Self::JOIN) {
|
||||
memberships.push(MembershipState::Join);
|
||||
}
|
||||
if self.contains(Self::INVITE) {
|
||||
memberships.push(MembershipState::Invite);
|
||||
}
|
||||
if self.contains(Self::KNOCK) {
|
||||
memberships.push(MembershipState::Knock);
|
||||
}
|
||||
if self.contains(Self::LEAVE) {
|
||||
memberships.push(MembershipState::Leave);
|
||||
}
|
||||
if self.contains(Self::BAN) {
|
||||
memberships.push(MembershipState::Ban);
|
||||
}
|
||||
|
||||
memberships
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
// Copyright 2025 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.
|
||||
|
||||
#![allow(clippy::assign_op_pattern)] // Triggered by bitflags! usage
|
||||
|
||||
mod call;
|
||||
mod create;
|
||||
mod display_name;
|
||||
mod encryption;
|
||||
mod knock;
|
||||
mod latest_event;
|
||||
mod members;
|
||||
mod room_info;
|
||||
mod state;
|
||||
mod tags;
|
||||
mod tombstone;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use std::sync::RwLock as SyncRwLock;
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
pub use create::*;
|
||||
pub use display_name::{RoomDisplayName, RoomHero};
|
||||
pub(crate) use display_name::{RoomSummary, UpdatedRoomDisplayName};
|
||||
pub use encryption::EncryptionState;
|
||||
use eyeball::{AsyncLock, SharedObservable};
|
||||
use futures_util::{Stream, StreamExt};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_common::ring_buffer::RingBuffer;
|
||||
pub use members::{RoomMember, RoomMembersUpdate, RoomMemberships};
|
||||
pub(crate) use room_info::SyncInfo;
|
||||
pub use room_info::{
|
||||
apply_redaction, BaseRoomInfo, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons,
|
||||
};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::{events::AnySyncTimelineEvent, serde::Raw};
|
||||
use ruma::{
|
||||
events::{
|
||||
direct::OwnedDirectUserIdentifier,
|
||||
receipt::{Receipt, ReceiptThread, ReceiptType},
|
||||
room::{
|
||||
avatar::{self},
|
||||
guest_access::GuestAccess,
|
||||
history_visibility::HistoryVisibility,
|
||||
join_rules::JoinRule,
|
||||
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
||||
},
|
||||
},
|
||||
room::RoomType,
|
||||
EventId, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use state::{RoomState, RoomStateFilter};
|
||||
pub(crate) use tags::RoomNotableTags;
|
||||
use tokio::sync::broadcast;
|
||||
pub use tombstone::{PredecessorRoom, SuccessorRoom};
|
||||
use tracing::{info, instrument, warn};
|
||||
|
||||
use crate::{
|
||||
deserialized_responses::MemberEvent,
|
||||
notification_settings::RoomNotificationMode,
|
||||
read_receipts::RoomReadReceipts,
|
||||
store::{DynStateStore, Result as StoreResult, StateStoreExt},
|
||||
sync::UnreadNotificationsCount,
|
||||
Error, MinimalStateEvent,
|
||||
};
|
||||
|
||||
/// The underlying room data structure collecting state for joined, left and
|
||||
/// invited rooms.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Room {
|
||||
/// The room ID.
|
||||
pub(super) room_id: OwnedRoomId,
|
||||
|
||||
/// Our own user ID.
|
||||
pub(super) own_user_id: OwnedUserId,
|
||||
|
||||
pub(super) inner: SharedObservable<RoomInfo>,
|
||||
pub(super) room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
|
||||
pub(super) store: Arc<DynStateStore>,
|
||||
|
||||
/// The most recent few encrypted events. When the keys come through to
|
||||
/// decrypt these, the most recent relevant one will replace
|
||||
/// `latest_event`. (We can't tell which one is relevant until
|
||||
/// they are decrypted.)
|
||||
///
|
||||
/// Currently, these are held in Room rather than RoomInfo, because we were
|
||||
/// not sure whether holding too many of them might make the cache too
|
||||
/// slow to load on startup. Keeping them here means they are not cached
|
||||
/// to disk but held in memory.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub latest_encrypted_events: Arc<SyncRwLock<RingBuffer<Raw<AnySyncTimelineEvent>>>>,
|
||||
|
||||
/// A map for ids of room membership events in the knocking state linked to
|
||||
/// the user id of the user affected by the member event, that the current
|
||||
/// user has marked as seen so they can be ignored.
|
||||
pub seen_knock_request_ids_map:
|
||||
SharedObservable<Option<BTreeMap<OwnedEventId, OwnedUserId>>, AsyncLock>,
|
||||
|
||||
/// A sender that will notify receivers when room member updates happen.
|
||||
pub room_member_updates_sender: broadcast::Sender<RoomMembersUpdate>,
|
||||
}
|
||||
|
||||
impl Room {
|
||||
pub(crate) fn new(
|
||||
own_user_id: &UserId,
|
||||
store: Arc<DynStateStore>,
|
||||
room_id: &RoomId,
|
||||
room_state: RoomState,
|
||||
room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
|
||||
) -> Self {
|
||||
let room_info = RoomInfo::new(room_id, room_state);
|
||||
Self::restore(own_user_id, store, room_info, room_info_notable_update_sender)
|
||||
}
|
||||
|
||||
pub(crate) fn restore(
|
||||
own_user_id: &UserId,
|
||||
store: Arc<DynStateStore>,
|
||||
room_info: RoomInfo,
|
||||
room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
|
||||
) -> Self {
|
||||
let (room_member_updates_sender, _) = broadcast::channel(10);
|
||||
Self {
|
||||
own_user_id: own_user_id.into(),
|
||||
room_id: room_info.room_id.clone(),
|
||||
store,
|
||||
inner: SharedObservable::new(room_info),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
latest_encrypted_events: Arc::new(SyncRwLock::new(RingBuffer::new(
|
||||
Self::MAX_ENCRYPTED_EVENTS,
|
||||
))),
|
||||
room_info_notable_update_sender,
|
||||
seen_knock_request_ids_map: SharedObservable::new_async(None),
|
||||
room_member_updates_sender,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the unique room id of the room.
|
||||
pub fn room_id(&self) -> &RoomId {
|
||||
&self.room_id
|
||||
}
|
||||
|
||||
/// Get a copy of the room creator.
|
||||
pub fn creator(&self) -> Option<OwnedUserId> {
|
||||
self.inner.read().creator().map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Get our own user id.
|
||||
pub fn own_user_id(&self) -> &UserId {
|
||||
&self.own_user_id
|
||||
}
|
||||
|
||||
/// Whether this room's [`RoomType`] is `m.space`.
|
||||
pub fn is_space(&self) -> bool {
|
||||
self.inner.read().room_type().is_some_and(|t| *t == RoomType::Space)
|
||||
}
|
||||
|
||||
/// Returns the room's type as defined in its creation event
|
||||
/// (`m.room.create`).
|
||||
pub fn room_type(&self) -> Option<RoomType> {
|
||||
self.inner.read().room_type().map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Get the unread notification counts.
|
||||
pub fn unread_notification_counts(&self) -> UnreadNotificationsCount {
|
||||
self.inner.read().notification_counts
|
||||
}
|
||||
|
||||
/// Get the number of unread messages (computed client-side).
|
||||
///
|
||||
/// This might be more precise than [`Self::unread_notification_counts`] for
|
||||
/// encrypted rooms.
|
||||
pub fn num_unread_messages(&self) -> u64 {
|
||||
self.inner.read().read_receipts.num_unread
|
||||
}
|
||||
|
||||
/// Get the detailed information about read receipts for the room.
|
||||
pub fn read_receipts(&self) -> RoomReadReceipts {
|
||||
self.inner.read().read_receipts.clone()
|
||||
}
|
||||
|
||||
/// Get the number of unread notifications (computed client-side).
|
||||
///
|
||||
/// This might be more precise than [`Self::unread_notification_counts`] for
|
||||
/// encrypted rooms.
|
||||
pub fn num_unread_notifications(&self) -> u64 {
|
||||
self.inner.read().read_receipts.num_notifications
|
||||
}
|
||||
|
||||
/// Get the number of unread mentions (computed client-side), that is,
|
||||
/// messages causing a highlight in a room.
|
||||
///
|
||||
/// This might be more precise than [`Self::unread_notification_counts`] for
|
||||
/// encrypted rooms.
|
||||
pub fn num_unread_mentions(&self) -> u64 {
|
||||
self.inner.read().read_receipts.num_mentions
|
||||
}
|
||||
|
||||
/// Check if the room states have been synced
|
||||
///
|
||||
/// States might be missing if we have only seen the room_id of this Room
|
||||
/// so far, for example as the response for a `create_room` request without
|
||||
/// being synced yet.
|
||||
///
|
||||
/// Returns true if the state is fully synced, false otherwise.
|
||||
pub fn is_state_fully_synced(&self) -> bool {
|
||||
self.inner.read().sync_info == SyncInfo::FullySynced
|
||||
}
|
||||
|
||||
/// Check if the room state has been at least partially synced.
|
||||
///
|
||||
/// See [`Room::is_state_fully_synced`] for more info.
|
||||
pub fn is_state_partially_or_fully_synced(&self) -> bool {
|
||||
self.inner.read().sync_info != SyncInfo::NoState
|
||||
}
|
||||
|
||||
/// Get the `prev_batch` token that was received from the last sync. May be
|
||||
/// `None` if the last sync contained the full room history.
|
||||
pub fn last_prev_batch(&self) -> Option<String> {
|
||||
self.inner.read().last_prev_batch.clone()
|
||||
}
|
||||
|
||||
/// Get the avatar url of this room.
|
||||
pub fn avatar_url(&self) -> Option<OwnedMxcUri> {
|
||||
self.inner.read().avatar_url().map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Get information about the avatar of this room.
|
||||
pub fn avatar_info(&self) -> Option<avatar::ImageInfo> {
|
||||
self.inner.read().avatar_info().map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Get the canonical alias of this room.
|
||||
pub fn canonical_alias(&self) -> Option<OwnedRoomAliasId> {
|
||||
self.inner.read().canonical_alias().map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Get the canonical alias of this room.
|
||||
pub fn alt_aliases(&self) -> Vec<OwnedRoomAliasId> {
|
||||
self.inner.read().alt_aliases().to_owned()
|
||||
}
|
||||
|
||||
/// Get the `m.room.create` content of this room.
|
||||
///
|
||||
/// This usually isn't optional but some servers might not send an
|
||||
/// `m.room.create` event as the first event for a given room, thus this can
|
||||
/// be optional.
|
||||
///
|
||||
/// For room versions earlier than room version 11, if the event is
|
||||
/// redacted, all fields except `creator` will be set to their default
|
||||
/// value.
|
||||
pub fn create_content(&self) -> Option<RoomCreateWithCreatorEventContent> {
|
||||
match self.inner.read().base_info.create.as_ref()? {
|
||||
MinimalStateEvent::Original(ev) => Some(ev.content.clone()),
|
||||
MinimalStateEvent::Redacted(ev) => Some(ev.content.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Is this room considered a direct message.
|
||||
///
|
||||
/// Async because it can read room info from storage.
|
||||
#[instrument(skip_all, fields(room_id = ?self.room_id))]
|
||||
pub async fn is_direct(&self) -> StoreResult<bool> {
|
||||
match self.state() {
|
||||
RoomState::Joined | RoomState::Left | RoomState::Banned => {
|
||||
Ok(!self.inner.read().base_info.dm_targets.is_empty())
|
||||
}
|
||||
|
||||
RoomState::Invited => {
|
||||
let member = self.get_member(self.own_user_id()).await?;
|
||||
|
||||
match member {
|
||||
None => {
|
||||
info!("RoomMember not found for the user's own id");
|
||||
Ok(false)
|
||||
}
|
||||
Some(member) => match member.event.as_ref() {
|
||||
MemberEvent::Sync(_) => {
|
||||
warn!("Got MemberEvent::Sync in an invited room");
|
||||
Ok(false)
|
||||
}
|
||||
MemberEvent::Stripped(event) => {
|
||||
Ok(event.content.is_direct.unwrap_or(false))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: implement logic once we have the stripped events as we'd have with an Invite
|
||||
RoomState::Knocked => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// If this room is a direct message, get the members that we're sharing the
|
||||
/// room with.
|
||||
///
|
||||
/// *Note*: The member list might have been modified in the meantime and
|
||||
/// the targets might not even be in the room anymore. This setting should
|
||||
/// only be considered as guidance. We leave members in this list to allow
|
||||
/// us to re-find a DM with a user even if they have left, since we may
|
||||
/// want to re-invite them.
|
||||
pub fn direct_targets(&self) -> HashSet<OwnedDirectUserIdentifier> {
|
||||
self.inner.read().base_info.dm_targets.clone()
|
||||
}
|
||||
|
||||
/// If this room is a direct message, returns the number of members that
|
||||
/// we're sharing the room with.
|
||||
pub fn direct_targets_length(&self) -> usize {
|
||||
self.inner.read().base_info.dm_targets.len()
|
||||
}
|
||||
|
||||
/// Get the guest access policy of this room.
|
||||
pub fn guest_access(&self) -> GuestAccess {
|
||||
self.inner.read().guest_access().clone()
|
||||
}
|
||||
|
||||
/// Get the history visibility policy of this room.
|
||||
pub fn history_visibility(&self) -> Option<HistoryVisibility> {
|
||||
self.inner.read().history_visibility().cloned()
|
||||
}
|
||||
|
||||
/// Get the history visibility policy of this room, or a sensible default if
|
||||
/// the event is missing.
|
||||
pub fn history_visibility_or_default(&self) -> HistoryVisibility {
|
||||
self.inner.read().history_visibility_or_default().clone()
|
||||
}
|
||||
|
||||
/// Is the room considered to be public.
|
||||
pub fn is_public(&self) -> bool {
|
||||
matches!(self.join_rule(), JoinRule::Public)
|
||||
}
|
||||
|
||||
/// Get the join rule policy of this room.
|
||||
pub fn join_rule(&self) -> JoinRule {
|
||||
self.inner.read().join_rule().clone()
|
||||
}
|
||||
|
||||
/// Get the maximum power level that this room contains.
|
||||
///
|
||||
/// This is useful if one wishes to normalize the power levels, e.g. from
|
||||
/// 0-100 where 100 would be the max power level.
|
||||
pub fn max_power_level(&self) -> i64 {
|
||||
self.inner.read().base_info.max_power_level
|
||||
}
|
||||
|
||||
/// Get the current power levels of this room.
|
||||
pub async fn power_levels(&self) -> Result<RoomPowerLevels, Error> {
|
||||
Ok(self
|
||||
.store
|
||||
.get_state_event_static::<RoomPowerLevelsEventContent>(self.room_id())
|
||||
.await?
|
||||
.ok_or(Error::InsufficientData)?
|
||||
.deserialize()?
|
||||
.power_levels())
|
||||
}
|
||||
|
||||
/// Get the `m.room.name` of this room.
|
||||
///
|
||||
/// The returned string may be empty if the event has been redacted, or it's
|
||||
/// missing from storage.
|
||||
pub fn name(&self) -> Option<String> {
|
||||
self.inner.read().name().map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Get the topic of the room.
|
||||
pub fn topic(&self) -> Option<String> {
|
||||
self.inner.read().topic().map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Update the cached user defined notification mode.
|
||||
///
|
||||
/// This is automatically recomputed on every successful sync, and the
|
||||
/// cached result can be retrieved in
|
||||
/// [`Self::cached_user_defined_notification_mode`].
|
||||
pub fn update_cached_user_defined_notification_mode(&self, mode: RoomNotificationMode) {
|
||||
self.inner.update_if(|info| {
|
||||
if info.cached_user_defined_notification_mode.as_ref() != Some(&mode) {
|
||||
info.cached_user_defined_notification_mode = Some(mode);
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the cached user defined notification mode, if available.
|
||||
///
|
||||
/// This cache is refilled every time we call
|
||||
/// [`Self::update_cached_user_defined_notification_mode`].
|
||||
pub fn cached_user_defined_notification_mode(&self) -> Option<RoomNotificationMode> {
|
||||
self.inner.read().cached_user_defined_notification_mode
|
||||
}
|
||||
|
||||
/// Get the list of users ids that are considered to be joined members of
|
||||
/// this room.
|
||||
pub async fn joined_user_ids(&self) -> StoreResult<Vec<OwnedUserId>> {
|
||||
self.store.get_user_ids(self.room_id(), RoomMemberships::JOIN).await
|
||||
}
|
||||
|
||||
/// Get the heroes for this room.
|
||||
pub fn heroes(&self) -> Vec<RoomHero> {
|
||||
self.inner.read().heroes().to_vec()
|
||||
}
|
||||
|
||||
/// Get the receipt as an `OwnedEventId` and `Receipt` tuple for the given
|
||||
/// `receipt_type`, `thread` and `user_id` in this room.
|
||||
pub async fn load_user_receipt(
|
||||
&self,
|
||||
receipt_type: ReceiptType,
|
||||
thread: ReceiptThread,
|
||||
user_id: &UserId,
|
||||
) -> StoreResult<Option<(OwnedEventId, Receipt)>> {
|
||||
self.store.get_user_room_receipt_event(self.room_id(), receipt_type, thread, user_id).await
|
||||
}
|
||||
|
||||
/// Load from storage the receipts as a list of `OwnedUserId` and `Receipt`
|
||||
/// tuples for the given `receipt_type`, `thread` and `event_id` in this
|
||||
/// room.
|
||||
pub async fn load_event_receipts(
|
||||
&self,
|
||||
receipt_type: ReceiptType,
|
||||
thread: ReceiptThread,
|
||||
event_id: &EventId,
|
||||
) -> StoreResult<Vec<(OwnedUserId, Receipt)>> {
|
||||
self.store
|
||||
.get_event_room_receipt_events(self.room_id(), receipt_type, thread, event_id)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns a boolean indicating if this room has been manually marked as
|
||||
/// unread
|
||||
pub fn is_marked_unread(&self) -> bool {
|
||||
self.inner.read().base_info.is_marked_unread
|
||||
}
|
||||
|
||||
/// Returns the recency stamp of the room.
|
||||
///
|
||||
/// Please read `RoomInfo::recency_stamp` to learn more.
|
||||
pub fn recency_stamp(&self) -> Option<u64> {
|
||||
self.inner.read().recency_stamp
|
||||
}
|
||||
|
||||
/// Get a `Stream` of loaded pinned events for this room.
|
||||
/// If no pinned events are found a single empty `Vec` will be returned.
|
||||
pub fn pinned_event_ids_stream(&self) -> impl Stream<Item = Vec<OwnedEventId>> {
|
||||
self.inner
|
||||
.subscribe()
|
||||
.map(|i| i.base_info.pinned_events.map(|c| c.pinned).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Returns the current pinned event ids for this room.
|
||||
pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
|
||||
self.inner.read().pinned_event_ids()
|
||||
}
|
||||
}
|
||||
|
||||
// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
|
||||
#[cfg(not(feature = "test-send-sync"))]
|
||||
unsafe impl Send for Room {}
|
||||
|
||||
// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
|
||||
#[cfg(not(feature = "test-send-sync"))]
|
||||
unsafe impl Sync for Room {}
|
||||
|
||||
#[cfg(feature = "test-send-sync")]
|
||||
#[test]
|
||||
// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
|
||||
fn test_send_sync_for_room() {
|
||||
fn assert_send_sync<
|
||||
T: matrix_sdk_common::SendOutsideWasm + matrix_sdk_common::SyncOutsideWasm,
|
||||
>() {
|
||||
}
|
||||
|
||||
assert_send_sync::<Room>();
|
||||
}
|
||||
|
||||
/// The possible sources of an account data type.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) enum AccountDataSource {
|
||||
/// The source is account data with the stable prefix.
|
||||
Stable,
|
||||
|
||||
/// The source is account data with the unstable prefix.
|
||||
#[default]
|
||||
Unstable,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,192 @@
|
||||
// Copyright 2025 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 bitflags::bitflags;
|
||||
use ruma::events::room::member::MembershipState;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::Room;
|
||||
|
||||
impl Room {
|
||||
/// Get the state of the room.
|
||||
pub fn state(&self) -> RoomState {
|
||||
self.inner.read().room_state
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum keeping track in which state the room is, e.g. if our own user is
|
||||
/// joined, RoomState::Invited, or has left the room.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub enum RoomState {
|
||||
/// The room is in a joined state.
|
||||
Joined,
|
||||
/// The room is in a left state.
|
||||
Left,
|
||||
/// The room is in an invited state.
|
||||
Invited,
|
||||
/// The room is in a knocked state.
|
||||
Knocked,
|
||||
/// The room is in a banned state.
|
||||
Banned,
|
||||
}
|
||||
|
||||
impl From<&MembershipState> for RoomState {
|
||||
fn from(membership_state: &MembershipState) -> Self {
|
||||
match membership_state {
|
||||
MembershipState::Ban => Self::Banned,
|
||||
MembershipState::Invite => Self::Invited,
|
||||
MembershipState::Join => Self::Joined,
|
||||
MembershipState::Knock => Self::Knocked,
|
||||
MembershipState::Leave => Self::Left,
|
||||
_ => panic!("Unexpected MembershipState: {membership_state}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Room state filter as a bitset.
|
||||
///
|
||||
/// Note that [`RoomStateFilter::empty()`] doesn't filter the results and
|
||||
/// is equivalent to [`RoomStateFilter::all()`].
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct RoomStateFilter: u16 {
|
||||
/// The room is in a joined state.
|
||||
const JOINED = 0b00000001;
|
||||
/// The room is in an invited state.
|
||||
const INVITED = 0b00000010;
|
||||
/// The room is in a left state.
|
||||
const LEFT = 0b00000100;
|
||||
/// The room is in a knocked state.
|
||||
const KNOCKED = 0b00001000;
|
||||
/// The room is in a banned state.
|
||||
const BANNED = 0b00010000;
|
||||
}
|
||||
}
|
||||
|
||||
impl RoomStateFilter {
|
||||
/// Whether the given room state matches this `RoomStateFilter`.
|
||||
pub fn matches(&self, state: RoomState) -> bool {
|
||||
if self.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
let bit_state = match state {
|
||||
RoomState::Joined => Self::JOINED,
|
||||
RoomState::Left => Self::LEFT,
|
||||
RoomState::Invited => Self::INVITED,
|
||||
RoomState::Knocked => Self::KNOCKED,
|
||||
RoomState::Banned => Self::BANNED,
|
||||
};
|
||||
|
||||
self.contains(bit_state)
|
||||
}
|
||||
|
||||
/// Get this `RoomStateFilter` as a list of matching [`RoomState`]s.
|
||||
pub fn as_vec(&self) -> Vec<RoomState> {
|
||||
let mut states = Vec::new();
|
||||
|
||||
if self.contains(Self::JOINED) {
|
||||
states.push(RoomState::Joined);
|
||||
}
|
||||
if self.contains(Self::LEFT) {
|
||||
states.push(RoomState::Left);
|
||||
}
|
||||
if self.contains(Self::INVITED) {
|
||||
states.push(RoomState::Invited);
|
||||
}
|
||||
if self.contains(Self::KNOCKED) {
|
||||
states.push(RoomState::Knocked);
|
||||
}
|
||||
if self.contains(Self::BANNED) {
|
||||
states.push(RoomState::Banned);
|
||||
}
|
||||
|
||||
states
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::owned_room_id;
|
||||
|
||||
use super::{RoomState, RoomStateFilter};
|
||||
use crate::test_utils::logged_in_base_client;
|
||||
|
||||
#[async_test]
|
||||
async fn test_room_state_filters() {
|
||||
let client = logged_in_base_client(None).await;
|
||||
|
||||
let joined_room_id = owned_room_id!("!joined:example.org");
|
||||
client.get_or_create_room(&joined_room_id, RoomState::Joined);
|
||||
|
||||
let invited_room_id = owned_room_id!("!invited:example.org");
|
||||
client.get_or_create_room(&invited_room_id, RoomState::Invited);
|
||||
|
||||
let left_room_id = owned_room_id!("!left:example.org");
|
||||
client.get_or_create_room(&left_room_id, RoomState::Left);
|
||||
|
||||
let knocked_room_id = owned_room_id!("!knocked:example.org");
|
||||
client.get_or_create_room(&knocked_room_id, RoomState::Knocked);
|
||||
|
||||
let banned_room_id = owned_room_id!("!banned:example.org");
|
||||
client.get_or_create_room(&banned_room_id, RoomState::Banned);
|
||||
|
||||
let joined_rooms = client.rooms_filtered(RoomStateFilter::JOINED);
|
||||
assert_eq!(joined_rooms.len(), 1);
|
||||
assert_eq!(joined_rooms[0].state(), RoomState::Joined);
|
||||
assert_eq!(joined_rooms[0].room_id, joined_room_id);
|
||||
|
||||
let invited_rooms = client.rooms_filtered(RoomStateFilter::INVITED);
|
||||
assert_eq!(invited_rooms.len(), 1);
|
||||
assert_eq!(invited_rooms[0].state(), RoomState::Invited);
|
||||
assert_eq!(invited_rooms[0].room_id, invited_room_id);
|
||||
|
||||
let left_rooms = client.rooms_filtered(RoomStateFilter::LEFT);
|
||||
assert_eq!(left_rooms.len(), 1);
|
||||
assert_eq!(left_rooms[0].state(), RoomState::Left);
|
||||
assert_eq!(left_rooms[0].room_id, left_room_id);
|
||||
|
||||
let knocked_rooms = client.rooms_filtered(RoomStateFilter::KNOCKED);
|
||||
assert_eq!(knocked_rooms.len(), 1);
|
||||
assert_eq!(knocked_rooms[0].state(), RoomState::Knocked);
|
||||
assert_eq!(knocked_rooms[0].room_id, knocked_room_id);
|
||||
|
||||
let banned_rooms = client.rooms_filtered(RoomStateFilter::BANNED);
|
||||
assert_eq!(banned_rooms.len(), 1);
|
||||
assert_eq!(banned_rooms[0].state(), RoomState::Banned);
|
||||
assert_eq!(banned_rooms[0].room_id, banned_room_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_room_state_filters_as_vec() {
|
||||
assert_eq!(RoomStateFilter::JOINED.as_vec(), vec![RoomState::Joined]);
|
||||
assert_eq!(RoomStateFilter::LEFT.as_vec(), vec![RoomState::Left]);
|
||||
assert_eq!(RoomStateFilter::INVITED.as_vec(), vec![RoomState::Invited]);
|
||||
assert_eq!(RoomStateFilter::KNOCKED.as_vec(), vec![RoomState::Knocked]);
|
||||
assert_eq!(RoomStateFilter::BANNED.as_vec(), vec![RoomState::Banned]);
|
||||
|
||||
// Check all filters are taken into account
|
||||
assert_eq!(
|
||||
RoomStateFilter::all().as_vec(),
|
||||
vec![
|
||||
RoomState::Joined,
|
||||
RoomState::Left,
|
||||
RoomState::Invited,
|
||||
RoomState::Knocked,
|
||||
RoomState::Banned
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
// Copyright 2025 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 bitflags::bitflags;
|
||||
use ruma::events::{tag::Tags, AnyRoomAccountDataEvent, RoomAccountDataEventType};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::Room;
|
||||
use crate::store::Result as StoreResult;
|
||||
|
||||
impl Room {
|
||||
/// Get the `Tags` for this room.
|
||||
pub async fn tags(&self) -> StoreResult<Option<Tags>> {
|
||||
if let Some(AnyRoomAccountDataEvent::Tag(event)) = self
|
||||
.store
|
||||
.get_room_account_data_event(self.room_id(), RoomAccountDataEventType::Tag)
|
||||
.await?
|
||||
.and_then(|raw| raw.deserialize().ok())
|
||||
{
|
||||
Ok(Some(event.content.tags))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether the room is marked as favourite.
|
||||
///
|
||||
/// A room is considered favourite if it has received the `m.favourite` tag.
|
||||
pub fn is_favourite(&self) -> bool {
|
||||
self.inner.read().base_info.notable_tags.contains(RoomNotableTags::FAVOURITE)
|
||||
}
|
||||
|
||||
/// Check whether the room is marked as low priority.
|
||||
///
|
||||
/// A room is considered low priority if it has received the `m.lowpriority`
|
||||
/// tag.
|
||||
pub fn is_low_priority(&self) -> bool {
|
||||
self.inner.read().base_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY)
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Notable tags, i.e. subset of tags that we are more interested by.
|
||||
///
|
||||
/// We are not interested by all the tags. Some tags are more important than
|
||||
/// others, and this struct describes them.
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize)]
|
||||
pub(crate) struct RoomNotableTags: u8 {
|
||||
/// The `m.favourite` tag.
|
||||
const FAVOURITE = 0b0000_0001;
|
||||
|
||||
/// THe `m.lowpriority` tag.
|
||||
const LOW_PRIORITY = 0b0000_0010;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::ops::Not;
|
||||
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{
|
||||
events::tag::{TagInfo, TagName, Tags},
|
||||
room_id,
|
||||
serde::Raw,
|
||||
user_id,
|
||||
};
|
||||
use serde_json::json;
|
||||
use stream_assert::{assert_pending, assert_ready};
|
||||
|
||||
use super::{super::BaseRoomInfo, RoomNotableTags};
|
||||
use crate::{
|
||||
response_processors as processors,
|
||||
store::{RoomLoadSettings, StoreConfig},
|
||||
BaseClient, RoomState, SessionMeta,
|
||||
};
|
||||
|
||||
#[async_test]
|
||||
async fn test_is_favourite() {
|
||||
// Given a room,
|
||||
let client =
|
||||
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
|
||||
|
||||
client
|
||||
.activate(
|
||||
SessionMeta {
|
||||
user_id: user_id!("@alice:example.org").into(),
|
||||
device_id: ruma::device_id!("AYEAYEAYE").into(),
|
||||
},
|
||||
RoomLoadSettings::default(),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let room_id = room_id!("!test:localhost");
|
||||
let room = client.get_or_create_room(room_id, RoomState::Joined);
|
||||
|
||||
// Sanity checks to ensure the room isn't marked as favourite.
|
||||
assert!(room.is_favourite().not());
|
||||
|
||||
// Subscribe to the `RoomInfo`.
|
||||
let mut room_info_subscriber = room.subscribe_info();
|
||||
|
||||
assert_pending!(room_info_subscriber);
|
||||
|
||||
// Create the tag.
|
||||
let tag_raw = Raw::new(&json!({
|
||||
"content": {
|
||||
"tags": {
|
||||
"m.favourite": {
|
||||
"order": 0.0
|
||||
},
|
||||
},
|
||||
},
|
||||
"type": "m.tag",
|
||||
}))
|
||||
.unwrap()
|
||||
.cast();
|
||||
|
||||
// When the new tag is handled and applied.
|
||||
let mut context = processors::Context::default();
|
||||
|
||||
processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
|
||||
.await;
|
||||
|
||||
processors::changes::save_and_apply(
|
||||
context.clone(),
|
||||
&client.state_store,
|
||||
&client.ignore_user_list_changes,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// The `RoomInfo` is getting notified.
|
||||
assert_ready!(room_info_subscriber);
|
||||
assert_pending!(room_info_subscriber);
|
||||
|
||||
// The room is now marked as favourite.
|
||||
assert!(room.is_favourite());
|
||||
|
||||
// Now, let's remove the tag.
|
||||
let tag_raw = Raw::new(&json!({
|
||||
"content": {
|
||||
"tags": {},
|
||||
},
|
||||
"type": "m.tag"
|
||||
}))
|
||||
.unwrap()
|
||||
.cast();
|
||||
|
||||
processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
|
||||
.await;
|
||||
|
||||
processors::changes::save_and_apply(
|
||||
context,
|
||||
&client.state_store,
|
||||
&client.ignore_user_list_changes,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// The `RoomInfo` is getting notified.
|
||||
assert_ready!(room_info_subscriber);
|
||||
assert_pending!(room_info_subscriber);
|
||||
|
||||
// The room is now marked as _not_ favourite.
|
||||
assert!(room.is_favourite().not());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_is_low_priority() {
|
||||
// Given a room,
|
||||
let client =
|
||||
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
|
||||
|
||||
client
|
||||
.activate(
|
||||
SessionMeta {
|
||||
user_id: user_id!("@alice:example.org").into(),
|
||||
device_id: ruma::device_id!("AYEAYEAYE").into(),
|
||||
},
|
||||
RoomLoadSettings::default(),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let room_id = room_id!("!test:localhost");
|
||||
let room = client.get_or_create_room(room_id, RoomState::Joined);
|
||||
|
||||
// Sanity checks to ensure the room isn't marked as low priority.
|
||||
assert!(!room.is_low_priority());
|
||||
|
||||
// Subscribe to the `RoomInfo`.
|
||||
let mut room_info_subscriber = room.subscribe_info();
|
||||
|
||||
assert_pending!(room_info_subscriber);
|
||||
|
||||
// Create the tag.
|
||||
let tag_raw = Raw::new(&json!({
|
||||
"content": {
|
||||
"tags": {
|
||||
"m.lowpriority": {
|
||||
"order": 0.0
|
||||
},
|
||||
}
|
||||
},
|
||||
"type": "m.tag"
|
||||
}))
|
||||
.unwrap()
|
||||
.cast();
|
||||
|
||||
// When the new tag is handled and applied.
|
||||
let mut context = processors::Context::default();
|
||||
|
||||
processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
|
||||
.await;
|
||||
|
||||
processors::changes::save_and_apply(
|
||||
context.clone(),
|
||||
&client.state_store,
|
||||
&client.ignore_user_list_changes,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// The `RoomInfo` is getting notified.
|
||||
assert_ready!(room_info_subscriber);
|
||||
assert_pending!(room_info_subscriber);
|
||||
|
||||
// The room is now marked as low priority.
|
||||
assert!(room.is_low_priority());
|
||||
|
||||
// Now, let's remove the tag.
|
||||
let tag_raw = Raw::new(&json!({
|
||||
"content": {
|
||||
"tags": {},
|
||||
},
|
||||
"type": "m.tag"
|
||||
}))
|
||||
.unwrap()
|
||||
.cast();
|
||||
|
||||
processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
|
||||
.await;
|
||||
|
||||
processors::changes::save_and_apply(
|
||||
context,
|
||||
&client.state_store,
|
||||
&client.ignore_user_list_changes,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// The `RoomInfo` is getting notified.
|
||||
assert_ready!(room_info_subscriber);
|
||||
assert_pending!(room_info_subscriber);
|
||||
|
||||
// The room is now marked as _not_ low priority.
|
||||
assert!(room.is_low_priority().not());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_notable_tags_favourite() {
|
||||
let mut base_room_info = BaseRoomInfo::default();
|
||||
|
||||
let mut tags = Tags::new();
|
||||
tags.insert(TagName::Favorite, TagInfo::default());
|
||||
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE).not());
|
||||
base_room_info.handle_notable_tags(&tags);
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
|
||||
tags.clear();
|
||||
base_room_info.handle_notable_tags(&tags);
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE).not());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_notable_tags_low_priority() {
|
||||
let mut base_room_info = BaseRoomInfo::default();
|
||||
|
||||
let mut tags = Tags::new();
|
||||
tags.insert(TagName::LowPriority, TagInfo::default());
|
||||
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not());
|
||||
base_room_info.handle_notable_tags(&tags);
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY));
|
||||
tags.clear();
|
||||
base_room_info.handle_notable_tags(&tags);
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
// Copyright 2025 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::ops::Not;
|
||||
|
||||
use ruma::{events::room::tombstone::RoomTombstoneEventContent, OwnedEventId, OwnedRoomId};
|
||||
|
||||
use super::Room;
|
||||
|
||||
impl Room {
|
||||
/// Has the room been tombstoned.
|
||||
///
|
||||
/// A room is tombstoned if it has received a [`m.room.tombstone`] state
|
||||
/// event; see [`Room::tombstone_content`].
|
||||
///
|
||||
/// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
|
||||
pub fn is_tombstoned(&self) -> bool {
|
||||
self.inner.read().base_info.tombstone.is_some()
|
||||
}
|
||||
|
||||
/// Get the [`m.room.tombstone`] state event's content of this room if one
|
||||
/// has been received.
|
||||
///
|
||||
/// Also see [`Room::is_tombstoned`] to check if the [`m.room.tombstone`]
|
||||
/// event has been received. It's faster than using this method.
|
||||
///
|
||||
/// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
|
||||
pub fn tombstone_content(&self) -> Option<RoomTombstoneEventContent> {
|
||||
self.inner.read().tombstone().cloned()
|
||||
}
|
||||
|
||||
/// If this room is tombstoned, return the “reference” to the successor room
|
||||
/// —i.e. the room replacing this one.
|
||||
///
|
||||
/// A room is tombstoned if it has received a [`m.room.tombstone`] state
|
||||
/// event; see [`Room::tombstone_content`].
|
||||
///
|
||||
/// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
|
||||
pub fn successor_room(&self) -> Option<SuccessorRoom> {
|
||||
self.tombstone_content().map(|tombstone_event| SuccessorRoom {
|
||||
room_id: tombstone_event.replacement_room,
|
||||
reason: tombstone_event.body.is_empty().not().then_some(tombstone_event.body),
|
||||
})
|
||||
}
|
||||
|
||||
/// If this room is the successor of a tombstoned room, return the
|
||||
/// “reference” to the predecessor room.
|
||||
///
|
||||
/// A room is tombstoned if it has received a [`m.room.tombstone`] state
|
||||
/// event; see [`Room::tombstone_content`].
|
||||
///
|
||||
/// To determine if a room is the successor of a tombstoned room, the
|
||||
/// [`m.room.create`] must have been received, **with** a `predecessor`
|
||||
/// field. See [`Room::create_content`].
|
||||
///
|
||||
/// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
|
||||
/// [`m.room.create`]: https://spec.matrix.org/v1.14/client-server-api/#mroomcreate
|
||||
pub fn predecessor_room(&self) -> Option<PredecessorRoom> {
|
||||
self.create_content().and_then(|content_event| content_event.predecessor).map(
|
||||
|predecessor| PredecessorRoom {
|
||||
room_id: predecessor.room_id,
|
||||
last_event_id: predecessor.event_id,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// When a room A is tombstoned, it is replaced by a room B. The room A is the
|
||||
/// predecessor of B, and B is the successor of A. This type holds information
|
||||
/// about the successor room. See [`Room::successor_room`].
|
||||
///
|
||||
/// A room is tombstoned if it has received a [`m.room.tombstone`] state event.
|
||||
///
|
||||
/// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
|
||||
#[derive(Debug)]
|
||||
pub struct SuccessorRoom {
|
||||
/// The ID of the next room replacing this (tombstoned) room.
|
||||
pub room_id: OwnedRoomId,
|
||||
|
||||
/// The reason why the room has been tombstoned.
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
/// When a room A is tombstoned, it is replaced by a room B. The room A is the
|
||||
/// predecessor of B, and B is the successor of A. This type holds information
|
||||
/// about the predecessor room. See [`Room::predecessor_room`].
|
||||
///
|
||||
/// To know the predecessor of a room, the [`m.room.create`] state event must
|
||||
/// have been received.
|
||||
///
|
||||
/// [`m.room.create`]: https://spec.matrix.org/v1.14/client-server-api/#mroomcreate
|
||||
#[derive(Debug)]
|
||||
pub struct PredecessorRoom {
|
||||
/// The ID of the old room.
|
||||
pub room_id: OwnedRoomId,
|
||||
|
||||
/// The event ID of the last known event in the predecesssor room.
|
||||
pub last_event_id: OwnedEventId,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::ops::Not;
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk_test::{
|
||||
async_test, event_factory::EventFactory, JoinedRoomBuilder, SyncResponseBuilder,
|
||||
};
|
||||
use ruma::{event_id, room_id, user_id, RoomVersionId};
|
||||
|
||||
use crate::{test_utils::logged_in_base_client, RoomState};
|
||||
|
||||
#[async_test]
|
||||
async fn test_no_successor_room() {
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room = client.get_or_create_room(room_id!("!r0"), RoomState::Joined);
|
||||
|
||||
assert!(room.is_tombstoned().not());
|
||||
assert!(room.tombstone_content().is_none());
|
||||
assert!(room.successor_room().is_none());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_successor_room() {
|
||||
let client = logged_in_base_client(None).await;
|
||||
let sender = user_id!("@mnt_io:matrix.org");
|
||||
let room_id = room_id!("!r0");
|
||||
let successor_room_id = room_id!("!r1");
|
||||
let room = client.get_or_create_room(room_id, RoomState::Joined);
|
||||
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
let response = sync_builder
|
||||
.add_joined_room(
|
||||
JoinedRoomBuilder::new(room_id).add_timeline_event(
|
||||
EventFactory::new()
|
||||
.sender(sender)
|
||||
.room_tombstone("traces of you", successor_room_id),
|
||||
),
|
||||
)
|
||||
.build_sync_response();
|
||||
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
assert!(room.is_tombstoned());
|
||||
assert!(room.tombstone_content().is_some());
|
||||
assert_matches!(room.successor_room(), Some(successor_room) => {
|
||||
assert_eq!(successor_room.room_id, successor_room_id);
|
||||
assert_matches!(successor_room.reason, Some(reason) => {
|
||||
assert_eq!(reason, "traces of you");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_successor_room_no_reason() {
|
||||
let client = logged_in_base_client(None).await;
|
||||
let sender = user_id!("@mnt_io:matrix.org");
|
||||
let room_id = room_id!("!r0");
|
||||
let successor_room_id = room_id!("!r1");
|
||||
let room = client.get_or_create_room(room_id, RoomState::Joined);
|
||||
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
let response = sync_builder
|
||||
.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event(
|
||||
EventFactory::new().sender(sender).room_tombstone(
|
||||
// An empty reason will result in `None` in `SuccessorRoom::reason`.
|
||||
"",
|
||||
successor_room_id,
|
||||
),
|
||||
))
|
||||
.build_sync_response();
|
||||
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
assert!(room.is_tombstoned());
|
||||
assert!(room.tombstone_content().is_some());
|
||||
assert_matches!(room.successor_room(), Some(successor_room) => {
|
||||
assert_eq!(successor_room.room_id, successor_room_id);
|
||||
assert!(successor_room.reason.is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_no_predecessor_room() {
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room = client.get_or_create_room(room_id!("!r0"), RoomState::Joined);
|
||||
|
||||
assert!(room.create_content().is_none());
|
||||
assert!(room.predecessor_room().is_none());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_no_predecessor_room_with_create_event() {
|
||||
let client = logged_in_base_client(None).await;
|
||||
let sender = user_id!("@mnt_io:matrix.org");
|
||||
let room_id = room_id!("!r1");
|
||||
let room = client.get_or_create_room(room_id, RoomState::Joined);
|
||||
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
let response = sync_builder
|
||||
.add_joined_room(
|
||||
JoinedRoomBuilder::new(room_id).add_timeline_event(
|
||||
EventFactory::new()
|
||||
.create(sender, RoomVersionId::V11)
|
||||
// No `predecessor` field!
|
||||
.no_predecessor()
|
||||
.into_raw_sync(),
|
||||
),
|
||||
)
|
||||
.build_sync_response();
|
||||
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
assert!(room.create_content().is_some());
|
||||
assert!(room.predecessor_room().is_none());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_predecessor_room() {
|
||||
let client = logged_in_base_client(None).await;
|
||||
let sender = user_id!("@mnt_io:matrix.org");
|
||||
let room_id = room_id!("!r1");
|
||||
let predecessor_room_id = room_id!("!r0");
|
||||
let predecessor_last_event_id = event_id!("$ev42");
|
||||
let room = client.get_or_create_room(room_id, RoomState::Joined);
|
||||
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
let response = sync_builder
|
||||
.add_joined_room(
|
||||
JoinedRoomBuilder::new(room_id).add_timeline_event(
|
||||
EventFactory::new()
|
||||
.create(sender, RoomVersionId::V11)
|
||||
.predecessor(predecessor_room_id, predecessor_last_event_id)
|
||||
.into_raw_sync(),
|
||||
),
|
||||
)
|
||||
.build_sync_response();
|
||||
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
assert!(room.create_content().is_some());
|
||||
assert_matches!(room.predecessor_room(), Some(predecessor_room) => {
|
||||
assert_eq!(predecessor_room.room_id, predecessor_room_id);
|
||||
assert_eq!(predecessor_room.last_event_id, predecessor_last_event_id);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,643 +0,0 @@
|
||||
#![allow(clippy::assign_op_pattern)] // Triggered by bitflags! usage
|
||||
|
||||
mod members;
|
||||
pub(crate) mod normal;
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
fmt,
|
||||
hash::Hash,
|
||||
};
|
||||
|
||||
use bitflags::bitflags;
|
||||
pub use members::RoomMember;
|
||||
pub use normal::{
|
||||
apply_redaction, EncryptionState, Room, RoomHero, RoomInfo, RoomInfoNotableUpdate,
|
||||
RoomInfoNotableUpdateReasons, RoomMembersUpdate, RoomState, RoomStateFilter,
|
||||
};
|
||||
use regex::Regex;
|
||||
use ruma::{
|
||||
assign,
|
||||
events::{
|
||||
beacon_info::BeaconInfoEventContent,
|
||||
call::member::{CallMemberEventContent, CallMemberStateKey},
|
||||
direct::OwnedDirectUserIdentifier,
|
||||
macros::EventContent,
|
||||
room::{
|
||||
avatar::RoomAvatarEventContent,
|
||||
canonical_alias::RoomCanonicalAliasEventContent,
|
||||
create::{PreviousRoom, RoomCreateEventContent},
|
||||
encryption::RoomEncryptionEventContent,
|
||||
guest_access::RoomGuestAccessEventContent,
|
||||
history_visibility::RoomHistoryVisibilityEventContent,
|
||||
join_rules::RoomJoinRulesEventContent,
|
||||
member::MembershipState,
|
||||
name::RoomNameEventContent,
|
||||
pinned_events::RoomPinnedEventsEventContent,
|
||||
tombstone::RoomTombstoneEventContent,
|
||||
topic::RoomTopicEventContent,
|
||||
},
|
||||
tag::{TagName, Tags},
|
||||
AnyStrippedStateEvent, AnySyncStateEvent, EmptyStateKey, RedactContent,
|
||||
RedactedStateEventContent, StaticStateEventContent, SyncStateEvent,
|
||||
},
|
||||
room::RoomType,
|
||||
EventId, OwnedUserId, RoomVersionId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::MinimalStateEvent;
|
||||
|
||||
/// The name of the room, either from the metadata or calculated
|
||||
/// according to [matrix specification](https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room)
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub enum RoomDisplayName {
|
||||
/// The room has been named explicitly as
|
||||
Named(String),
|
||||
/// The room has a canonical alias that should be used
|
||||
Aliased(String),
|
||||
/// The room has not given an explicit name but a name could be
|
||||
/// calculated
|
||||
Calculated(String),
|
||||
/// The room doesn't have a name right now, but used to have one
|
||||
/// e.g. because it was a DM and everyone has left the room
|
||||
EmptyWas(String),
|
||||
/// No useful name could be calculated or ever found
|
||||
Empty,
|
||||
}
|
||||
|
||||
const WHITESPACE_REGEX: &str = r"\s+";
|
||||
const INVALID_SYMBOLS_REGEX: &str = r"[#,:\{\}\\]+";
|
||||
|
||||
impl RoomDisplayName {
|
||||
/// Transforms the current display name into the name part of a
|
||||
/// `RoomAliasId`.
|
||||
pub fn to_room_alias_name(&self) -> String {
|
||||
let room_name = match self {
|
||||
Self::Named(name) => name,
|
||||
Self::Aliased(name) => name,
|
||||
Self::Calculated(name) => name,
|
||||
Self::EmptyWas(name) => name,
|
||||
Self::Empty => "",
|
||||
};
|
||||
|
||||
let whitespace_regex =
|
||||
Regex::new(WHITESPACE_REGEX).expect("`WHITESPACE_REGEX` should be valid");
|
||||
let symbol_regex =
|
||||
Regex::new(INVALID_SYMBOLS_REGEX).expect("`INVALID_SYMBOLS_REGEX` should be valid");
|
||||
|
||||
// Replace whitespaces with `-`
|
||||
let sanitised = whitespace_regex.replace_all(room_name, "-");
|
||||
// Remove non-ASCII characters and ASCII control characters
|
||||
let sanitised =
|
||||
String::from_iter(sanitised.chars().filter(|c| c.is_ascii() && !c.is_ascii_control()));
|
||||
// Remove other problematic ASCII symbols
|
||||
let sanitised = symbol_regex.replace_all(&sanitised, "");
|
||||
// Lowercased
|
||||
sanitised.to_lowercase()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RoomDisplayName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
RoomDisplayName::Named(s)
|
||||
| RoomDisplayName::Calculated(s)
|
||||
| RoomDisplayName::Aliased(s) => {
|
||||
write!(f, "{s}")
|
||||
}
|
||||
RoomDisplayName::EmptyWas(s) => write!(f, "Empty Room (was {s})"),
|
||||
RoomDisplayName::Empty => write!(f, "Empty Room"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A base room info struct that is the backbone of normal as well as stripped
|
||||
/// rooms. Holds all the state events that are important to present a room to
|
||||
/// users.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct BaseRoomInfo {
|
||||
/// The avatar URL of this room.
|
||||
pub(crate) avatar: Option<MinimalStateEvent<RoomAvatarEventContent>>,
|
||||
/// All shared live location beacons of this room.
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
|
||||
pub(crate) beacons: BTreeMap<OwnedUserId, MinimalStateEvent<BeaconInfoEventContent>>,
|
||||
/// The canonical alias of this room.
|
||||
pub(crate) canonical_alias: Option<MinimalStateEvent<RoomCanonicalAliasEventContent>>,
|
||||
/// The `m.room.create` event content of this room.
|
||||
pub(crate) create: Option<MinimalStateEvent<RoomCreateWithCreatorEventContent>>,
|
||||
/// A list of user ids this room is considered as direct message, if this
|
||||
/// room is a DM.
|
||||
pub(crate) dm_targets: HashSet<OwnedDirectUserIdentifier>,
|
||||
/// The `m.room.encryption` event content that enabled E2EE in this room.
|
||||
pub(crate) encryption: Option<RoomEncryptionEventContent>,
|
||||
/// The guest access policy of this room.
|
||||
pub(crate) guest_access: Option<MinimalStateEvent<RoomGuestAccessEventContent>>,
|
||||
/// The history visibility policy of this room.
|
||||
pub(crate) history_visibility: Option<MinimalStateEvent<RoomHistoryVisibilityEventContent>>,
|
||||
/// The join rule policy of this room.
|
||||
pub(crate) join_rules: Option<MinimalStateEvent<RoomJoinRulesEventContent>>,
|
||||
/// The maximal power level that can be found in this room.
|
||||
pub(crate) max_power_level: i64,
|
||||
/// The `m.room.name` of this room.
|
||||
pub(crate) name: Option<MinimalStateEvent<RoomNameEventContent>>,
|
||||
/// The `m.room.tombstone` event content of this room.
|
||||
pub(crate) tombstone: Option<MinimalStateEvent<RoomTombstoneEventContent>>,
|
||||
/// The topic of this room.
|
||||
pub(crate) topic: Option<MinimalStateEvent<RoomTopicEventContent>>,
|
||||
/// All minimal state events that containing one or more running matrixRTC
|
||||
/// memberships.
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
|
||||
pub(crate) rtc_member_events:
|
||||
BTreeMap<CallMemberStateKey, MinimalStateEvent<CallMemberEventContent>>,
|
||||
/// Whether this room has been manually marked as unread.
|
||||
#[serde(default)]
|
||||
pub(crate) is_marked_unread: bool,
|
||||
/// Some notable tags.
|
||||
///
|
||||
/// We are not interested by all the tags. Some tags are more important than
|
||||
/// others, and this field collects them.
|
||||
#[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
|
||||
pub(crate) notable_tags: RoomNotableTags,
|
||||
/// The `m.room.pinned_events` of this room.
|
||||
pub(crate) pinned_events: Option<RoomPinnedEventsEventContent>,
|
||||
}
|
||||
|
||||
impl BaseRoomInfo {
|
||||
/// Create a new, empty base room info.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Get the room version of this room.
|
||||
///
|
||||
/// For room versions earlier than room version 11, if the event is
|
||||
/// redacted, this will return the default of [`RoomVersionId::V1`].
|
||||
pub fn room_version(&self) -> Option<&RoomVersionId> {
|
||||
match self.create.as_ref()? {
|
||||
MinimalStateEvent::Original(ev) => Some(&ev.content.room_version),
|
||||
MinimalStateEvent::Redacted(ev) => Some(&ev.content.room_version),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a state event for this room and update our info accordingly.
|
||||
///
|
||||
/// Returns true if the event modified the info, false otherwise.
|
||||
pub fn handle_state_event(&mut self, ev: &AnySyncStateEvent) -> bool {
|
||||
match ev {
|
||||
AnySyncStateEvent::BeaconInfo(b) => {
|
||||
self.beacons.insert(b.state_key().clone(), b.into());
|
||||
}
|
||||
// No redacted branch - enabling encryption cannot be undone.
|
||||
AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(encryption)) => {
|
||||
self.encryption = Some(encryption.content.clone());
|
||||
}
|
||||
AnySyncStateEvent::RoomAvatar(a) => {
|
||||
self.avatar = Some(a.into());
|
||||
}
|
||||
AnySyncStateEvent::RoomName(n) => {
|
||||
self.name = Some(n.into());
|
||||
}
|
||||
AnySyncStateEvent::RoomCreate(c) if self.create.is_none() => {
|
||||
self.create = Some(c.into());
|
||||
}
|
||||
AnySyncStateEvent::RoomHistoryVisibility(h) => {
|
||||
self.history_visibility = Some(h.into());
|
||||
}
|
||||
AnySyncStateEvent::RoomGuestAccess(g) => {
|
||||
self.guest_access = Some(g.into());
|
||||
}
|
||||
AnySyncStateEvent::RoomJoinRules(c) => {
|
||||
self.join_rules = Some(c.into());
|
||||
}
|
||||
AnySyncStateEvent::RoomCanonicalAlias(a) => {
|
||||
self.canonical_alias = Some(a.into());
|
||||
}
|
||||
AnySyncStateEvent::RoomTopic(t) => {
|
||||
self.topic = Some(t.into());
|
||||
}
|
||||
AnySyncStateEvent::RoomTombstone(t) => {
|
||||
self.tombstone = Some(t.into());
|
||||
}
|
||||
AnySyncStateEvent::RoomPowerLevels(p) => {
|
||||
self.max_power_level = p.power_levels().max().into();
|
||||
}
|
||||
AnySyncStateEvent::CallMember(m) => {
|
||||
let Some(o_ev) = m.as_original() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// we modify the event so that `origin_sever_ts` gets copied into
|
||||
// `content.created_ts`
|
||||
let mut o_ev = o_ev.clone();
|
||||
o_ev.content.set_created_ts_if_none(o_ev.origin_server_ts);
|
||||
|
||||
// Add the new event.
|
||||
self.rtc_member_events
|
||||
.insert(m.state_key().clone(), SyncStateEvent::Original(o_ev).into());
|
||||
|
||||
// Remove all events that don't contain any memberships anymore.
|
||||
self.rtc_member_events.retain(|_, ev| {
|
||||
ev.as_original().is_some_and(|o| !o.content.active_memberships(None).is_empty())
|
||||
});
|
||||
}
|
||||
AnySyncStateEvent::RoomPinnedEvents(p) => {
|
||||
self.pinned_events = p.as_original().map(|p| p.content.clone());
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Handle a stripped state event for this room and update our info
|
||||
/// accordingly.
|
||||
///
|
||||
/// Returns true if the event modified the info, false otherwise.
|
||||
pub fn handle_stripped_state_event(&mut self, ev: &AnyStrippedStateEvent) -> bool {
|
||||
match ev {
|
||||
AnyStrippedStateEvent::RoomEncryption(encryption) => {
|
||||
if let Some(algorithm) = &encryption.content.algorithm {
|
||||
let content = assign!(RoomEncryptionEventContent::new(algorithm.clone()), {
|
||||
rotation_period_ms: encryption.content.rotation_period_ms,
|
||||
rotation_period_msgs: encryption.content.rotation_period_msgs,
|
||||
});
|
||||
self.encryption = Some(content);
|
||||
}
|
||||
// If encryption event is redacted, we don't care much. When
|
||||
// entering the room, we will fetch the proper event before
|
||||
// sending any messages.
|
||||
}
|
||||
AnyStrippedStateEvent::RoomAvatar(a) => {
|
||||
self.avatar = Some(a.into());
|
||||
}
|
||||
AnyStrippedStateEvent::RoomName(n) => {
|
||||
self.name = Some(n.into());
|
||||
}
|
||||
AnyStrippedStateEvent::RoomCreate(c) if self.create.is_none() => {
|
||||
self.create = Some(c.into());
|
||||
}
|
||||
AnyStrippedStateEvent::RoomHistoryVisibility(h) => {
|
||||
self.history_visibility = Some(h.into());
|
||||
}
|
||||
AnyStrippedStateEvent::RoomGuestAccess(g) => {
|
||||
self.guest_access = Some(g.into());
|
||||
}
|
||||
AnyStrippedStateEvent::RoomJoinRules(c) => {
|
||||
self.join_rules = Some(c.into());
|
||||
}
|
||||
AnyStrippedStateEvent::RoomCanonicalAlias(a) => {
|
||||
self.canonical_alias = Some(a.into());
|
||||
}
|
||||
AnyStrippedStateEvent::RoomTopic(t) => {
|
||||
self.topic = Some(t.into());
|
||||
}
|
||||
AnyStrippedStateEvent::RoomTombstone(t) => {
|
||||
self.tombstone = Some(t.into());
|
||||
}
|
||||
AnyStrippedStateEvent::RoomPowerLevels(p) => {
|
||||
self.max_power_level = p.power_levels().max().into();
|
||||
}
|
||||
AnyStrippedStateEvent::CallMember(_) => {
|
||||
// Ignore stripped call state events. Rooms that are not in Joined or Left state
|
||||
// wont have call information.
|
||||
return false;
|
||||
}
|
||||
AnyStrippedStateEvent::RoomPinnedEvents(p) => {
|
||||
if let Some(pinned) = p.content.pinned.clone() {
|
||||
self.pinned_events = Some(RoomPinnedEventsEventContent::new(pinned));
|
||||
}
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn handle_redaction(&mut self, redacts: &EventId) {
|
||||
let room_version = self.room_version().unwrap_or(&RoomVersionId::V1).to_owned();
|
||||
|
||||
// FIXME: Use let chains once available to get rid of unwrap()s
|
||||
if self.avatar.has_event_id(redacts) {
|
||||
self.avatar.as_mut().unwrap().redact(&room_version);
|
||||
} else if self.canonical_alias.has_event_id(redacts) {
|
||||
self.canonical_alias.as_mut().unwrap().redact(&room_version);
|
||||
} else if self.create.has_event_id(redacts) {
|
||||
self.create.as_mut().unwrap().redact(&room_version);
|
||||
} else if self.guest_access.has_event_id(redacts) {
|
||||
self.guest_access.as_mut().unwrap().redact(&room_version);
|
||||
} else if self.history_visibility.has_event_id(redacts) {
|
||||
self.history_visibility.as_mut().unwrap().redact(&room_version);
|
||||
} else if self.join_rules.has_event_id(redacts) {
|
||||
self.join_rules.as_mut().unwrap().redact(&room_version);
|
||||
} else if self.name.has_event_id(redacts) {
|
||||
self.name.as_mut().unwrap().redact(&room_version);
|
||||
} else if self.tombstone.has_event_id(redacts) {
|
||||
self.tombstone.as_mut().unwrap().redact(&room_version);
|
||||
} else if self.topic.has_event_id(redacts) {
|
||||
self.topic.as_mut().unwrap().redact(&room_version);
|
||||
} else {
|
||||
self.rtc_member_events
|
||||
.retain(|_, member_event| member_event.event_id() != Some(redacts));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_notable_tags(&mut self, tags: &Tags) {
|
||||
let mut notable_tags = RoomNotableTags::empty();
|
||||
|
||||
if tags.contains_key(&TagName::Favorite) {
|
||||
notable_tags.insert(RoomNotableTags::FAVOURITE);
|
||||
}
|
||||
|
||||
if tags.contains_key(&TagName::LowPriority) {
|
||||
notable_tags.insert(RoomNotableTags::LOW_PRIORITY);
|
||||
}
|
||||
|
||||
self.notable_tags = notable_tags;
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Notable tags, i.e. subset of tags that we are more interested by.
|
||||
///
|
||||
/// We are not interested by all the tags. Some tags are more important than
|
||||
/// others, and this struct describes them.
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize)]
|
||||
pub(crate) struct RoomNotableTags: u8 {
|
||||
/// The `m.favourite` tag.
|
||||
const FAVOURITE = 0b0000_0001;
|
||||
|
||||
/// THe `m.lowpriority` tag.
|
||||
const LOW_PRIORITY = 0b0000_0010;
|
||||
}
|
||||
}
|
||||
|
||||
trait OptionExt {
|
||||
fn has_event_id(&self, ev_id: &EventId) -> bool;
|
||||
}
|
||||
|
||||
impl<C> OptionExt for Option<MinimalStateEvent<C>>
|
||||
where
|
||||
C: StaticStateEventContent + RedactContent,
|
||||
C::Redacted: RedactedStateEventContent,
|
||||
{
|
||||
fn has_event_id(&self, ev_id: &EventId) -> bool {
|
||||
self.as_ref().is_some_and(|ev| ev.event_id() == Some(ev_id))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BaseRoomInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
avatar: None,
|
||||
beacons: BTreeMap::new(),
|
||||
canonical_alias: None,
|
||||
create: None,
|
||||
dm_targets: Default::default(),
|
||||
encryption: None,
|
||||
guest_access: None,
|
||||
history_visibility: None,
|
||||
join_rules: None,
|
||||
max_power_level: 100,
|
||||
name: None,
|
||||
tombstone: None,
|
||||
topic: None,
|
||||
rtc_member_events: BTreeMap::new(),
|
||||
is_marked_unread: false,
|
||||
notable_tags: RoomNotableTags::empty(),
|
||||
pinned_events: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The content of an `m.room.create` event, with a required `creator` field.
|
||||
///
|
||||
/// Starting with room version 11, the `creator` field should be removed and the
|
||||
/// `sender` field of the event should be used instead. This is reflected on
|
||||
/// [`RoomCreateEventContent`].
|
||||
///
|
||||
/// This type was created as an alternative for ease of use. When it is used in
|
||||
/// the SDK, it is constructed by copying the `sender` of the original event as
|
||||
/// the `creator`.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
|
||||
#[ruma_event(type = "m.room.create", kind = State, state_key_type = EmptyStateKey, custom_redacted)]
|
||||
pub struct RoomCreateWithCreatorEventContent {
|
||||
/// The `user_id` of the room creator.
|
||||
///
|
||||
/// This is set by the homeserver.
|
||||
///
|
||||
/// While this should be optional since room version 11, we copy the sender
|
||||
/// of the event so we can still access it.
|
||||
pub creator: OwnedUserId,
|
||||
|
||||
/// Whether or not this room's data should be transferred to other
|
||||
/// homeservers.
|
||||
#[serde(
|
||||
rename = "m.federate",
|
||||
default = "ruma::serde::default_true",
|
||||
skip_serializing_if = "ruma::serde::is_true"
|
||||
)]
|
||||
pub federate: bool,
|
||||
|
||||
/// The version of the room.
|
||||
///
|
||||
/// Defaults to `RoomVersionId::V1`.
|
||||
#[serde(default = "default_create_room_version_id")]
|
||||
pub room_version: RoomVersionId,
|
||||
|
||||
/// A reference to the room this room replaces, if the previous room was
|
||||
/// upgraded.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub predecessor: Option<PreviousRoom>,
|
||||
|
||||
/// The room type.
|
||||
///
|
||||
/// This is currently only used for spaces.
|
||||
#[serde(skip_serializing_if = "Option::is_none", rename = "type")]
|
||||
pub room_type: Option<RoomType>,
|
||||
}
|
||||
|
||||
impl RoomCreateWithCreatorEventContent {
|
||||
/// Constructs a `RoomCreateWithCreatorEventContent` with the given original
|
||||
/// content and sender.
|
||||
pub fn from_event_content(content: RoomCreateEventContent, sender: OwnedUserId) -> Self {
|
||||
let RoomCreateEventContent { federate, room_version, predecessor, room_type, .. } = content;
|
||||
Self { creator: sender, federate, room_version, predecessor, room_type }
|
||||
}
|
||||
|
||||
fn into_event_content(self) -> (RoomCreateEventContent, OwnedUserId) {
|
||||
let Self { creator, federate, room_version, predecessor, room_type } = self;
|
||||
|
||||
#[allow(deprecated)]
|
||||
let content = assign!(RoomCreateEventContent::new_v11(), {
|
||||
creator: Some(creator.clone()),
|
||||
federate,
|
||||
room_version,
|
||||
predecessor,
|
||||
room_type,
|
||||
});
|
||||
|
||||
(content, creator)
|
||||
}
|
||||
}
|
||||
|
||||
/// Redacted form of [`RoomCreateWithCreatorEventContent`].
|
||||
pub type RedactedRoomCreateWithCreatorEventContent = RoomCreateWithCreatorEventContent;
|
||||
|
||||
impl RedactedStateEventContent for RedactedRoomCreateWithCreatorEventContent {
|
||||
type StateKey = EmptyStateKey;
|
||||
}
|
||||
|
||||
impl RedactContent for RoomCreateWithCreatorEventContent {
|
||||
type Redacted = RedactedRoomCreateWithCreatorEventContent;
|
||||
|
||||
fn redact(self, version: &RoomVersionId) -> Self::Redacted {
|
||||
let (content, sender) = self.into_event_content();
|
||||
// Use Ruma's redaction algorithm.
|
||||
let content = content.redact(version);
|
||||
Self::from_event_content(content, sender)
|
||||
}
|
||||
}
|
||||
|
||||
fn default_create_room_version_id() -> RoomVersionId {
|
||||
RoomVersionId::V1
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Room membership filter as a bitset.
|
||||
///
|
||||
/// Note that [`RoomMemberships::empty()`] doesn't filter the results and
|
||||
/// [`RoomMemberships::all()`] filters out unknown memberships.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct RoomMemberships: u16 {
|
||||
/// The member joined the room.
|
||||
const JOIN = 0b00000001;
|
||||
/// The member was invited to the room.
|
||||
const INVITE = 0b00000010;
|
||||
/// The member requested to join the room.
|
||||
const KNOCK = 0b00000100;
|
||||
/// The member left the room.
|
||||
const LEAVE = 0b00001000;
|
||||
/// The member was banned.
|
||||
const BAN = 0b00010000;
|
||||
|
||||
/// The member is active in the room (i.e. joined or invited).
|
||||
const ACTIVE = Self::JOIN.bits() | Self::INVITE.bits();
|
||||
}
|
||||
}
|
||||
|
||||
impl RoomMemberships {
|
||||
/// Whether the given membership matches this `RoomMemberships`.
|
||||
pub fn matches(&self, membership: &MembershipState) -> bool {
|
||||
if self.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
let membership = match membership {
|
||||
MembershipState::Ban => Self::BAN,
|
||||
MembershipState::Invite => Self::INVITE,
|
||||
MembershipState::Join => Self::JOIN,
|
||||
MembershipState::Knock => Self::KNOCK,
|
||||
MembershipState::Leave => Self::LEAVE,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
self.contains(membership)
|
||||
}
|
||||
|
||||
/// Get this `RoomMemberships` as a list of matching [`MembershipState`]s.
|
||||
pub fn as_vec(&self) -> Vec<MembershipState> {
|
||||
let mut memberships = Vec::new();
|
||||
|
||||
if self.contains(Self::JOIN) {
|
||||
memberships.push(MembershipState::Join);
|
||||
}
|
||||
if self.contains(Self::INVITE) {
|
||||
memberships.push(MembershipState::Invite);
|
||||
}
|
||||
if self.contains(Self::KNOCK) {
|
||||
memberships.push(MembershipState::Knock);
|
||||
}
|
||||
if self.contains(Self::LEAVE) {
|
||||
memberships.push(MembershipState::Leave);
|
||||
}
|
||||
if self.contains(Self::BAN) {
|
||||
memberships.push(MembershipState::Ban);
|
||||
}
|
||||
|
||||
memberships
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::ops::Not;
|
||||
|
||||
use ruma::events::tag::{TagInfo, TagName, Tags};
|
||||
|
||||
use super::{BaseRoomInfo, RoomNotableTags};
|
||||
use crate::RoomDisplayName;
|
||||
|
||||
#[test]
|
||||
fn test_handle_notable_tags_favourite() {
|
||||
let mut base_room_info = BaseRoomInfo::default();
|
||||
|
||||
let mut tags = Tags::new();
|
||||
tags.insert(TagName::Favorite, TagInfo::default());
|
||||
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE).not());
|
||||
base_room_info.handle_notable_tags(&tags);
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
|
||||
tags.clear();
|
||||
base_room_info.handle_notable_tags(&tags);
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE).not());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_notable_tags_low_priority() {
|
||||
let mut base_room_info = BaseRoomInfo::default();
|
||||
|
||||
let mut tags = Tags::new();
|
||||
tags.insert(TagName::LowPriority, TagInfo::default());
|
||||
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not());
|
||||
base_room_info.handle_notable_tags(&tags);
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY));
|
||||
tags.clear();
|
||||
base_room_info.handle_notable_tags(&tags);
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_room_alias_from_room_display_name_lowercases() {
|
||||
assert_eq!(
|
||||
"roomalias",
|
||||
RoomDisplayName::Named("RoomAlias".to_owned()).to_room_alias_name()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_room_alias_from_room_display_name_removes_whitespace() {
|
||||
assert_eq!(
|
||||
"room-alias",
|
||||
RoomDisplayName::Named("Room Alias".to_owned()).to_room_alias_name()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_room_alias_from_room_display_name_removes_non_ascii_symbols() {
|
||||
assert_eq!(
|
||||
"roomalias",
|
||||
RoomDisplayName::Named("Room±Alias√".to_owned()).to_room_alias_name()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_room_alias_from_room_display_name_removes_invalid_ascii_symbols() {
|
||||
assert_eq!(
|
||||
"roomalias",
|
||||
RoomDisplayName::Named("#Room,{Alias}:".to_owned()).to_room_alias_name()
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,6 @@ use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use assert_matches2::assert_let;
|
||||
use async_trait::async_trait;
|
||||
use growable_bloom_filter::GrowableBloomBuilder;
|
||||
use matrix_sdk_test::{event_factory::EventFactory, test_json};
|
||||
use ruma::{
|
||||
@@ -47,8 +46,7 @@ use crate::{
|
||||
///
|
||||
/// This trait is not meant to be used directly, but will be used with the
|
||||
/// `statestore_integration_tests!` macro.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait StateStoreIntegrationTests {
|
||||
/// Populate the given `StateStore`.
|
||||
async fn populate(&self) -> Result<()>;
|
||||
@@ -98,8 +96,6 @@ pub trait StateStoreIntegrationTests {
|
||||
async fn test_get_room_infos(&self);
|
||||
}
|
||||
|
||||
#[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();
|
||||
|
||||
@@ -138,8 +138,8 @@ impl MemoryStore {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
impl StateStore for MemoryStore {
|
||||
type Error = StoreError;
|
||||
|
||||
|
||||
@@ -44,10 +44,7 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::{
|
||||
deserialized_responses::SyncOrStrippedState,
|
||||
latest_event::LatestEvent,
|
||||
rooms::{
|
||||
normal::{RoomSummary, SyncInfo},
|
||||
BaseRoomInfo, RoomNotableTags,
|
||||
},
|
||||
room::{BaseRoomInfo, RoomSummary, SyncInfo},
|
||||
sync::UnreadNotificationsCount,
|
||||
MinimalStateEvent, OriginalMinimalStateEvent, RoomInfo, RoomState,
|
||||
};
|
||||
@@ -108,10 +105,9 @@ impl RoomInfoV1 {
|
||||
} = self;
|
||||
|
||||
RoomInfo {
|
||||
version: 0,
|
||||
data_format_version: 0,
|
||||
room_id,
|
||||
room_state: room_type,
|
||||
prev_room_state: None,
|
||||
notification_counts,
|
||||
summary,
|
||||
members_synced,
|
||||
@@ -214,10 +210,7 @@ impl BaseRoomInfoV1 {
|
||||
name,
|
||||
tombstone,
|
||||
topic,
|
||||
rtc_member_events: BTreeMap::new(),
|
||||
is_marked_unread: false,
|
||||
notable_tags: RoomNotableTags::empty(),
|
||||
pinned_events: None,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user