Compare commits
1359 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 | |||
| 1e938df90d | |||
| 5d55bb4955 | |||
| 8abd9fb303 | |||
| d5ee644443 | |||
| 06022aa23a | |||
| 8ea0d9542d | |||
| 63938bf2c7 | |||
| acff6c2e1d | |||
| 1013843071 | |||
| a0bfd3e21e | |||
| 68352e8339 | |||
| 6a5e24a64a | |||
| db6d1b4cd6 | |||
| b0c1eda682 | |||
| afecd6d508 | |||
| 52b490f5b6 | |||
| 097558ca1b | |||
| 2194a81d74 | |||
| 37146da27a | |||
| 9300f47b40 | |||
| 52f0aafb1e | |||
| bfbbe89989 | |||
| d25e236a75 | |||
| ab90c1b945 | |||
| 005e506c9b | |||
| 3fe457db83 | |||
| 69ab855efa | |||
| 66c7ba60a9 | |||
| 3e320b8289 | |||
| 2922389037 | |||
| 68e3cdebdd | |||
| 511cf78d51 | |||
| f4eea708fa | |||
| 1d3107ebcb | |||
| 073b4bae03 | |||
| 35e13b8730 | |||
| f266fb9c38 | |||
| 2dfd334ade | |||
| 89d0cc5a76 | |||
| 628440632d | |||
| 16a3d9d78b | |||
| d89a7d6c18 | |||
| 87c70789fe | |||
| 9cde7f46bc | |||
| 3e2b95cdb9 | |||
| dc3bd69af1 | |||
| da14cef6c1 | |||
| a1ac363383 | |||
| 9e5ef57a5f | |||
| da86552648 | |||
| c0f4e90965 | |||
| cf393bf8e1 | |||
| 2ee0e175fa | |||
| 5c011a8400 | |||
| b6b0c556b9 | |||
| 0c5f0b8d26 | |||
| e7e15ca280 | |||
| 222cffd502 | |||
| cbef772eaa | |||
| e7e9c7bcf2 | |||
| f1ea3e64d0 | |||
| 257deb4b94 | |||
| 3ece8e62b5 | |||
| f7f07e7389 | |||
| 8d0928ff7c | |||
| 39212db9ce | |||
| 056d4a79d0 | |||
| 5753ca3a64 | |||
| f3291d15c5 | |||
| 413070aecb | |||
| 288251b1b5 | |||
| 6c3e3bb519 | |||
| 2e8b396c09 | |||
| ed048af903 | |||
| 28e475b1fc | |||
| 5a13bd5e76 | |||
| 6966302467 | |||
| fd6ce02d70 | |||
| 2debfd4c4d | |||
| 19c40fd2da | |||
| d5d0368ba8 | |||
| 899eb04f05 | |||
| 187280d573 | |||
| 036d14e9e3 | |||
| 226229d63b | |||
| 0cf018cc1b | |||
| c3d7a760f7 | |||
| d51cf1e76e | |||
| 71b6b213c4 | |||
| ec23638567 | |||
| 688a56a077 | |||
| 8413e856fe | |||
| 3f16e77686 | |||
| a4779299f6 | |||
| 50934f5bc9 | |||
| fad63b9a64 | |||
| d999cf9180 | |||
| 3cd60c5b01 | |||
| 2e4587f824 | |||
| 3996f7c0d6 | |||
| 774bff00a0 | |||
| d6196e6c5c | |||
| b49fd2b473 | |||
| f31119a013 | |||
| fb4caf40aa | |||
| 238fbdbe82 | |||
| a1d42cdf06 | |||
| 1c134a78de | |||
| b5d1c14e29 | |||
| a28ec70816 | |||
| e1b393c39f | |||
| a345c47a31 | |||
| 64feee41ef | |||
| 9f947e019f | |||
| be74cb4a16 | |||
| 409f08dc2b | |||
| a94a03766b | |||
| 988fd18b78 | |||
| 68b848602a | |||
| f7d6fe2dbf | |||
| c2a9523cbb | |||
| ee879354b7 | |||
| c3fd571623 | |||
| 52ec6a4539 | |||
| a57322466c | |||
| 90ce6e85ad | |||
| e94fd64276 | |||
| 0c7cf58d4d | |||
| e3b2e0fa3e | |||
| 4619221429 | |||
| 9b316ed405 | |||
| 0a633ca75c | |||
| 2e57733f05 | |||
| b94be8d509 | |||
| 24e6d780fc | |||
| d9157e5b83 | |||
| b4a8089b40 | |||
| a736dc9f96 | |||
| dd094ea38e | |||
| b2cd81a992 | |||
| d5ceb5f99a | |||
| 956386a3ed | |||
| b2a4032432 | |||
| 38378f7bae | |||
| e12264bcb6 | |||
| 385df955c3 | |||
| 47a1db9e16 | |||
| ed82f07d7d | |||
| 4ddd1468c2 | |||
| b274d36e11 | |||
| 7ae82f3afb | |||
| 111306b411 | |||
| c8fb8ad9ca | |||
| 2f7525c3c8 | |||
| 511fc96835 | |||
| cb539bc72b | |||
| 2b450a0a6a | |||
| fc93690d1f | |||
| 43431b88da | |||
| 13ca27d66a | |||
| 9f7179263a | |||
| c8da9cb462 | |||
| 678938951e | |||
| 8883e081af | |||
| c4d9ec98c3 | |||
| dccd836dc6 | |||
| c719cd11f3 | |||
| 42133a60c8 | |||
| d30dc7177f | |||
| e42be87798 | |||
| 524040b33c | |||
| 0227b3f554 | |||
| 6a076b0989 | |||
| 9b1a5b7102 | |||
| 0bb72064b5 | |||
| 8af68d7389 | |||
| cde0a9e24b | |||
| 9423e41a06 | |||
| 3489cbd5d7 | |||
| 65be779bb0 | |||
| 45f1dca6a3 | |||
| 913b2a5f78 | |||
| 627e2ca5a6 | |||
| 3d8af1b972 | |||
| 03f5d0222e | |||
| af85447328 | |||
| d34e11b9f6 | |||
| 231073c9c3 | |||
| 34a3fb4efb | |||
| a38c3b5dc5 | |||
| 1a1310e205 | |||
| d7b6fae2a3 | |||
| bb87a728ac | |||
| d60810c2af | |||
| a6a4579ef9 | |||
| a084a5b08b | |||
| 4112162092 | |||
| f341572616 | |||
| a047784278 | |||
| 7ac3fa1f4a | |||
| c30ec0ed8a | |||
| d6f2fd4304 | |||
| cf5b8d3b33 | |||
| 2a67d7472a | |||
| 9f74be26c3 | |||
| fb04539418 | |||
| 192cf0154a | |||
| 9acd649742 | |||
| e8fcdf4360 | |||
| 43dbb6a021 | |||
| de615f2ffe | |||
| f11eec4caf | |||
| f445a5ca57 | |||
| 68605de596 | |||
| db97d616f6 | |||
| e2e5b39afa | |||
| 6fec953ff0 | |||
| 95befc9a25 | |||
| cb92971657 | |||
| 5a35fec894 | |||
| 43b8f83e4b | |||
| 0952255a50 | |||
| 8738c4dbfd | |||
| 99436f8e79 | |||
| 339b220488 | |||
| 661f381e34 | |||
| 8d4ccf6442 | |||
| bd6b7c2ce1 | |||
| 9152d84b06 | |||
| c044f81d7b | |||
| 5730f0e00e | |||
| 76f92ba9af | |||
| d599c72278 | |||
| e5243e32be | |||
| db18e7fd74 | |||
| f3baf7efd2 | |||
| f6e223edf6 | |||
| 4f3b40d6fb | |||
| 6e480271d3 | |||
| e60cf18337 | |||
| 6409adb879 | |||
| 915e0e83bc | |||
| 8323ecdc8b | |||
| e0e9c06ca4 | |||
| eb313efdeb | |||
| bc22ff1221 | |||
| 8c988beaf2 | |||
| 752c9baf7c | |||
| 766772f654 | |||
| f5b6767253 | |||
| 5ce045ee02 | |||
| b83889dcba | |||
| cfc839f71b | |||
| 660d4e7ccb | |||
| 404dd3949f | |||
| 693c8df8d0 | |||
| d587d5f145 | |||
| be6daa5930 | |||
| acee5415c5 | |||
| 6399a99452 | |||
| b5aa2113db | |||
| 1585b0c32e | |||
| 3c01f88ab8 | |||
| 785312856e | |||
| fc8a6dc9b1 | |||
| 7b8694e465 | |||
| 655f62c331 | |||
| 53732e0ff2 | |||
| 4cae122854 | |||
| 2a11494c33 | |||
| 1dddd97d96 | |||
| f8236a8b96 | |||
| aa07108c98 | |||
| 9aed0cc933 | |||
| f6c5addf55 | |||
| 9434a112d9 | |||
| c4ec32cb78 | |||
| c5b8c812b3 | |||
| fdc2ca0c9e | |||
| dcd0e078f6 | |||
| 78b79a758f | |||
| 29f6606d99 | |||
| 94f0beec51 | |||
| 590d1d7890 | |||
| 400c92fc89 | |||
| b3e82a05db | |||
| a8aa364757 | |||
| 7457ecb1a8 | |||
| 01caf56edc | |||
| 6f07d008c9 | |||
| b408087320 | |||
| 22cbce82ce | |||
| ecdc68aa1c | |||
| 4a0bf80ab0 | |||
| 095425f664 | |||
| ca4e212e98 | |||
| 0b0f84b784 | |||
| fbd4a7dc38 | |||
| cb90d7fee6 | |||
| 1a79ea94ed | |||
| c3328a03f6 | |||
| 6803538c2e | |||
| 9d27e9b379 | |||
| 8683ca4d13 | |||
| d4f5ac152a | |||
| 31a1724390 | |||
| c034818c92 | |||
| e1fe479008 | |||
| 530659b59d | |||
| 45dd96e30a | |||
| 3f4c1fd1bb | |||
| 5acaaf5865 | |||
| 156501dbbd | |||
| a0eb9340d5 | |||
| dbdbfd0b38 | |||
| 1d9d4d3b3a | |||
| 8d16b3265c | |||
| 9c37a0393c | |||
| 82ef6232e7 | |||
| 3b9ae3e65e | |||
| a539518cd4 | |||
| f61cd60147 | |||
| b9c970dc43 | |||
| ba5e395a59 | |||
| c46e6623fe | |||
| 7ad1b113dc | |||
| c0d3ed1a90 | |||
| 00d7a77ebe | |||
| f29d3fd666 | |||
| 47204830a9 | |||
| f4bb14a30e | |||
| 0a345a3124 | |||
| 450a66ad11 | |||
| 6f3694cfa9 | |||
| 1658610f93 | |||
| f9b1bdb22d | |||
| f8abb85e9e | |||
| 1b5e6462ee | |||
| fbdd8839e6 | |||
| d86117ac70 | |||
| 914b7125cf | |||
| ad0223cafb | |||
| a870c02eab | |||
| 002e77616d | |||
| d777e68c4a | |||
| cabb345a1c | |||
| 2f08f27b59 | |||
| 3a7b0e9404 | |||
| 7713ce768a | |||
| aea573d001 | |||
| 7ca6494efa | |||
| 6f44853bf7 | |||
| 2c6c818005 | |||
| abc4fbc2f7 | |||
| 9adff21f78 | |||
| 91f9ef85ae | |||
| 17c6ad6b70 | |||
| 8f61bdb046 | |||
| 53c36226cb | |||
| 5a22944f52 | |||
| 494f93d2a4 | |||
| d7849a1aa5 | |||
| bc9adaab06 | |||
| 1ec47ca24f | |||
| faa2fa2ef0 | |||
| 33e8b453ee | |||
| b9fadd0a10 | |||
| 294fd79947 | |||
| 8b6096729c | |||
| 6f370daaed | |||
| 3c694e7909 | |||
| c3245a4f22 | |||
| da89a53605 | |||
| 968582af01 | |||
| 07c7b6ab2a | |||
| 5aae0cbcd9 | |||
| 3ea842dae4 | |||
| 31e0bfa400 | |||
| d32b10de80 | |||
| 215853cf67 | |||
| a941cc824d | |||
| d3daa18bf8 | |||
| 2ac3b6e9a2 | |||
| e81817c1b2 | |||
| 01bb8093d0 | |||
| 1565067cee | |||
| ecc603171b | |||
| 7f3308bd2b | |||
| 06d5fdb5ff | |||
| 6047d369a6 | |||
| 961a893b8c | |||
| 2927974396 | |||
| 8c780fc5d5 | |||
| 8867d203e7 | |||
| cf5f14ef5d | |||
| 132f063769 | |||
| 915cb13d45 | |||
| 0089da10cc | |||
| 28293d0f2b | |||
| d3e64295cf | |||
| 6cd3217c2e | |||
| eba2a7a6e3 | |||
| a98b822eeb | |||
| 0a80021742 | |||
| 63e8fc84a3 | |||
| fe0fb641f3 | |||
| 1c43bc7e29 | |||
| d03ed3063c | |||
| ea8664c487 | |||
| ca025f8cca | |||
| 78e19fce32 | |||
| c8536e9e46 | |||
| 1caa6069db | |||
| abe8338e5c | |||
| 5373e39ce5 | |||
| 5875973c13 | |||
| 3fbf159d0e | |||
| b5c4fe3f7d | |||
| 516d066d4c | |||
| fbcd5a71aa | |||
| b5a23086fd | |||
| a9ce3f6963 | |||
| a27f8f79a4 | |||
| dd01479c6b | |||
| e7f85ba545 | |||
| 48767da6cc | |||
| 73754399be | |||
| 18f5668e3e | |||
| bc92e55b53 | |||
| 230feff430 | |||
| 8bb4387dc4 | |||
| 2506ba8364 | |||
| daad6d662f | |||
| 53853c2d9a | |||
| 40de714e81 | |||
| 27bde16843 | |||
| 5e8f8d5513 | |||
| 120970c4ea | |||
| 740e729606 | |||
| 60b140b684 | |||
| 9a165468eb | |||
| e15897b3f1 | |||
| 52f98582f1 | |||
| 2e72c23868 | |||
| 0967027feb | |||
| 6c9b1ef3c1 | |||
| 8cceded0ae | |||
| ff181475a0 | |||
| 074c0e59e0 | |||
| 1d7c60c46a | |||
| 377f34fae2 | |||
| 26cb805e0f | |||
| 81dbe2060c | |||
| fd0fca436b | |||
| 3d653d3fdc | |||
| b22bb3ee9f | |||
| 7f17b4be7b | |||
| fa3a9d81e3 | |||
| 892c99f0f3 | |||
| 8d8846a259 | |||
| 9d63af6271 | |||
| 37ad82adfc | |||
| 57953b9ae9 | |||
| 777fb920f6 | |||
| 05750e871b | |||
| 5a11b8b836 | |||
| 8a785ea855 | |||
| 1874a76f67 | |||
| 0b2b528962 | |||
| 2036c3da9d | |||
| 7694b016da | |||
| 6fdd59157a | |||
| 0d7096fa94 | |||
| a94dc4e89b | |||
| bf965b2a17 | |||
| 9c87625910 | |||
| 3f1543504a | |||
| 3773968d19 | |||
| d28d4ce799 | |||
| bffb19b23a | |||
| 6aea4c827a | |||
| ac3250c58b | |||
| 6fe0880e11 | |||
| 78282bf1e1 | |||
| 43d25127c3 | |||
| c33c61a256 | |||
| def4be5a9f | |||
| 9bc0d8b0d9 | |||
| 0924b2e343 | |||
| 8b6e75980b | |||
| 5fd0cb0ddb | |||
| b5edc86a52 | |||
| d09655989d | |||
| 83415ac6ca | |||
| cc7fb63c6d | |||
| f5195222a7 | |||
| cecf15a34a | |||
| 95b53d7e01 | |||
| 8cd70854ba | |||
| dbaa36ec3e | |||
| 8976233905 | |||
| 82d47d800c | |||
| e84ad97edf | |||
| d447342cbd | |||
| c74ecff3f0 | |||
| a0282ec71b | |||
| a67f9d5bbf | |||
| f7297edd61 | |||
| 87a6037924 | |||
| ee710e34dd | |||
| 55143e1790 | |||
| 7a0bf9b9b9 | |||
| b422b93c78 | |||
| 4742aa298a | |||
| f9f389d9ec | |||
| 7dba05f4c5 | |||
| f02a7d15ab | |||
| 54ab46dcb4 | |||
| 9b406cff87 | |||
| 5791ac9b76 | |||
| 6026b0c4b7 | |||
| 52909b0eeb | |||
| 1feb77bbef | |||
| 2e1b051a4d | |||
| 15fd892b63 | |||
| 4833403d65 | |||
| 061a2f739a | |||
| 86b5cb4dba | |||
| 74bc3dfb6e | |||
| 7841ed8637 | |||
| 19df945155 | |||
| 3e3bff76de | |||
| ea073f55f0 | |||
| c1e28aa156 | |||
| af62f09e37 | |||
| 9a33385697 | |||
| bfa89bc73f | |||
| e1d05fa53c | |||
| b0ccc94b26 | |||
| b2356a0232 | |||
| 3bb883387e | |||
| 506a36b210 | |||
| 8c1966a237 | |||
| 09513eaa5e | |||
| fda9177a70 | |||
| 21960a5ba2 | |||
| 0819ab1dad | |||
| 475ad79360 | |||
| 7b52306ff2 | |||
| e5f6d026ff | |||
| 5dd5710758 | |||
| 37b62dfed1 | |||
| d21a4152de | |||
| 8c2dcd7b5d | |||
| 019b4a20f6 | |||
| 30a9a972ce | |||
| 22ba1684b2 | |||
| 0b12ec2b38 | |||
| a71f5bf21f | |||
| 9bd7cfda5f | |||
| c1a13f7f98 | |||
| a362584bb3 | |||
| f9ce7628ff | |||
| 43c066e837 | |||
| f3f37a33fd | |||
| 39c6481f96 | |||
| 66b9d334ef | |||
| e64cb2c4f1 | |||
| 4f47868930 | |||
| 4c115b6ad5 | |||
| 242a1047bd | |||
| 2f3cab431f | |||
| 55f514897b | |||
| d4b92de8e4 | |||
| 25d39997a4 | |||
| 254ce8923b | |||
| 0a4db305b9 | |||
| 90ac2181e9 | |||
| bdf5fad992 | |||
| 05be62183a | |||
| d545419684 | |||
| f900db49dd | |||
| 1373f99288 | |||
| f56bc4c0d6 | |||
| 60efcbc55d | |||
| 30589ca899 | |||
| 61fa339163 | |||
| 3f5efc1ff6 | |||
| 23f72ba15f | |||
| a25acf7e62 | |||
| c3fc310f29 | |||
| b9c7ffe7c3 | |||
| 017a947fc1 | |||
| 5c57631a6c | |||
| 3495cab7ad | |||
| 7a06bdb695 | |||
| 6c57003d17 | |||
| 2eb2ae7959 | |||
| a055aa3e57 | |||
| 5c7a733f49 | |||
| 00ae386b74 | |||
| 9ad7ca8f11 | |||
| a43ce05200 | |||
| 7d4dfb5c2d | |||
| d9e9006e61 | |||
| bfec34db20 | |||
| 0aae72c161 | |||
| fbd8b9c816 | |||
| d6120a5985 | |||
| a8f7939126 | |||
| 217429c3fe | |||
| 716958bb86 | |||
| 1319558eb6 | |||
| 096d478593 | |||
| 8fcd5a91c4 | |||
| 155042e46c | |||
| e03d40e946 | |||
| 2671769d9f | |||
| 8d9d83f15f | |||
| 6bc9dc5c6a | |||
| d6566484a1 | |||
| 0e4d8ec62f | |||
| f9c6f897c8 | |||
| 7252a685a6 | |||
| bed4d5034e | |||
| e2a2f32e82 | |||
| 334c66b0a0 | |||
| ca392b08c9 | |||
| e9a34f6359 | |||
| 6411d27096 | |||
| 07f0017d30 | |||
| 59f9d12da5 | |||
| 1c114978e4 | |||
| 629421214f | |||
| 97d772dd05 | |||
| f28c64ba21 | |||
| 20dd15e256 | |||
| f33d10468d | |||
| d3b3b4db10 | |||
| 38e28643f1 | |||
| 9de6d28270 | |||
| 9f47201bab | |||
| 0b7140c123 | |||
| 28a4918ff6 | |||
| 534cd599f4 | |||
| 910a5ce90a | |||
| dadd01a4ea | |||
| c4a9059814 | |||
| 51a1cd3c67 | |||
| c6d2ab4637 | |||
| c6c7307d6e | |||
| 9c9944aa0c | |||
| b311197d41 | |||
| 1068d88c3e | |||
| 861078a95e | |||
| aa9aef44f7 | |||
| f2ad11a56a | |||
| 12c327292f | |||
| 31e78c2a1b | |||
| 8a64922130 | |||
| 2ae142f257 | |||
| faa0e6e554 | |||
| b95cf79a6d | |||
| 28cd8beb77 | |||
| 1918bd5f6b | |||
| d45addee10 | |||
| ed16e91aed | |||
| 714caae545 | |||
| 25bb607b27 | |||
| c9a6ae9549 | |||
| 58099fd6b5 | |||
| c5856a33f0 | |||
| a5f115f21f | |||
| ddd84e231b | |||
| 51e9df87f5 | |||
| aec4d37a2e | |||
| ceafc2155f | |||
| 4a37d6ebe2 | |||
| 10095f8627 | |||
| 84bb1ab595 | |||
| fce7999890 | |||
| 10b72ef4b4 | |||
| bfbb354c39 | |||
| 9db137af44 | |||
| 2999d10fb9 | |||
| 654885a925 | |||
| 8042abe5f5 | |||
| 65ee18a52d | |||
| 69588d5266 | |||
| 7b77b19bc0 | |||
| 357b36b287 | |||
| a5f0473e1b | |||
| 8d74d46d80 | |||
| 9f2c572709 | |||
| 4b6dd5c857 | |||
| 83dd11ea7d | |||
| 6c2a88cdc0 | |||
| e00d57fee2 | |||
| ce44c6e4e7 | |||
| f9ff4fff50 | |||
| 2291a61379 | |||
| d8f37509af | |||
| dddbcfbabb | |||
| ed8c1d543a | |||
| 3e02d90a27 | |||
| 954b16ad39 | |||
| ed18c5113f | |||
| 0f4b3aa187 | |||
| 8a7658745d | |||
| 2ea39877cc | |||
| 4212691cf0 | |||
| d66fe79579 | |||
| 88caf11842 | |||
| 5320d952e5 | |||
| 7eae832b8c | |||
| 1d18ab03d7 | |||
| 337bc2c097 | |||
| 7e59ae99d0 | |||
| 51feda1042 | |||
| 0a520e4f9f | |||
| e07212d356 | |||
| 1d52073b45 | |||
| d85d6cfbca | |||
| 892cb9116c | |||
| 5e9d291ca3 | |||
| b74d64a456 | |||
| cd8f5cf5d4 | |||
| 1dc20aa9aa | |||
| bdab9951af | |||
| f641a639cd | |||
| 049021fe27 | |||
| 4db32b15ba | |||
| 619346acad | |||
| 525f9866a4 | |||
| d7dc1c9b5b | |||
| 7da3aaaa8a | |||
| 5aaa6bf187 | |||
| af9a5edd59 | |||
| 6e764644b3 | |||
| 8dc2ec9dc4 | |||
| 4e1ae3d5e9 | |||
| 582b3a91d6 | |||
| f7467ff57a | |||
| 2e16021f14 |
+3
-4
@@ -9,8 +9,7 @@ exclude = [
|
||||
[advisories]
|
||||
version = 2
|
||||
ignore = [
|
||||
{ id = "RUSTSEC-2023-0071", reason = "We are not using RSA directly, nor do we depend on the RSA crate directly" },
|
||||
{ id = "RUSTSEC-2024-0384", reason = "Unmaintained backoff crate, not critical. We'll migrate soon." },
|
||||
{ id = "RUSTSEC-2024-0436", reason = "Unmaintained paste crate, not critical." },
|
||||
]
|
||||
|
||||
[licenses]
|
||||
@@ -53,14 +52,14 @@ 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",
|
||||
# A patch override for the bindings: https://github.com/rodrimati1992/const_panic/pull/10
|
||||
"https://github.com/jplatte/const_panic",
|
||||
# A patch override for the bindings: https://github.com/smol-rs/async-compat/pull/22
|
||||
"https://github.com/jplatte/async-compat",
|
||||
"https://github.com/element-hq/async-compat",
|
||||
# We can release vodozemac whenever we need but let's not block development
|
||||
# on releases.
|
||||
"https://github.com/matrix-org/vodozemac",
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-11-26
|
||||
toolchain: nightly-2025-02-20
|
||||
components: rustfmt
|
||||
|
||||
- name: Run Benchmarks
|
||||
|
||||
+25
-28
@@ -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.29.5
|
||||
uses: crate-ci/typos@v1.33.1
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
@@ -314,7 +309,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-11-26
|
||||
toolchain: nightly-2025-02-20
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Load cache
|
||||
@@ -342,27 +337,9 @@ jobs:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# run several docker containers with the same networking stack so the hostname 'postgres'
|
||||
# maps to the postgres container, etc.
|
||||
# run several docker containers with the same networking stack so the hostname 'synapse'
|
||||
# maps to the synapse container, etc.
|
||||
services:
|
||||
# synapse needs a postgres container
|
||||
postgres:
|
||||
# Docker Hub image
|
||||
image: postgres
|
||||
# Provide the password for postgres
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: syncv3
|
||||
# Set health checks to wait until postgres has started
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
# Maps tcp port 5432 on service container to the host
|
||||
- 5432:5432
|
||||
# tests need a synapse: this is a service and not michaelkaye/setup-matrix-synapse@main as the
|
||||
# latter does not provide networking for services to communicate with it.
|
||||
synapse:
|
||||
@@ -398,6 +375,26 @@ 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
|
||||
|
||||
compile-bench:
|
||||
name: 🚄 Compile benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Compile benchmarks (no run)
|
||||
run: |
|
||||
cargo bench --profile dev --no-run
|
||||
|
||||
|
||||
@@ -5,11 +5,6 @@ on:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -26,26 +21,9 @@ jobs:
|
||||
name: Code Coverage
|
||||
runs-on: "ubuntu-latest"
|
||||
|
||||
# run several docker containers with the same networking stack so the hostname 'postgres'
|
||||
# maps to the postgres container, etc.
|
||||
# run several docker containers with the same networking stack so the hostname 'synapse'
|
||||
# maps to the synapse container, etc.
|
||||
services:
|
||||
postgres:
|
||||
# Docker Hub image
|
||||
image: postgres
|
||||
# Provide the password for postgres
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: syncv3
|
||||
# Set health checks to wait until postgres has started
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
# Maps tcp port 5432 on service container to the host
|
||||
- 5432:5432
|
||||
# tests need a synapse: this is a service and not michaelkaye/setup-matrix-synapse@main as the
|
||||
# latter does not provide networking for services to communicate with it.
|
||||
synapse:
|
||||
@@ -100,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
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Check if the path of changed file is longer than 260 characters
|
||||
# that windows filesystem allows
|
||||
|
||||
name: Detect long path among changed files
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request: # focus on the changed files in current PR
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
long-path:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
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
|
||||
MAX_LENGTH: 120 # set max length to 120, considering the base path of app project that uses matrix-sdk
|
||||
run: |
|
||||
for file in ${ALL_CHANGED_FILES}; do
|
||||
if [ ${#file} -gt $MAX_LENGTH ]; then
|
||||
echo "File path is too long. Length: ${#file}, Path: $file"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
exit 0
|
||||
@@ -9,4 +9,4 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Machete
|
||||
uses: bnjbvr/cargo-machete@main
|
||||
uses: bnjbvr/cargo-machete@v0.8.0
|
||||
|
||||
@@ -4,11 +4,6 @@ on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -36,7 +31,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-11-26
|
||||
toolchain: nightly-2025-02-20
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
@@ -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
+603
-992
File diff suppressed because it is too large
Load Diff
+73
-66
@@ -4,38 +4,38 @@ 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"
|
||||
|
||||
[workspace.package]
|
||||
rust-version = "1.83"
|
||||
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.2.0"
|
||||
base64 = "0.22.1"
|
||||
bitflags = "2.8.0"
|
||||
byteorder = "1.5.0"
|
||||
chrono = "0.4.39"
|
||||
eyeball = { version = "0.8.8", features = ["tracing"] }
|
||||
eyeball-im = { version = "0.6.0", features = ["tracing"] }
|
||||
eyeball-im-util = "0.8.0"
|
||||
eyeball-im = { version = "0.7.0", features = ["tracing"] }
|
||||
eyeball-im-util = "0.9.0"
|
||||
futures-core = "0.3.31"
|
||||
futures-executor = "0.3.31"
|
||||
futures-util = "0.3.31"
|
||||
@@ -45,9 +45,9 @@ growable-bloom-filter = "2.1.1"
|
||||
hkdf = "0.12.4"
|
||||
hmac = "0.12.1"
|
||||
http = "1.2.0"
|
||||
imbl = "4.0.1"
|
||||
imbl = "5.0.0"
|
||||
indexmap = "2.7.1"
|
||||
insta = { version = "1.42.1", features = ["json"] }
|
||||
insta = { version = "1.42.1", features = ["json", "redactions"] }
|
||||
itertools = "0.14.0"
|
||||
js-sys = "0.3.69"
|
||||
mime = "0.3.17"
|
||||
@@ -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.1", features = [
|
||||
ruma = { version = "0.12.3", features = [
|
||||
"client-api-c",
|
||||
"compat-upload-signatures",
|
||||
"compat-user-id",
|
||||
@@ -74,9 +74,13 @@ ruma = { version = "0.12.1", features = [
|
||||
"unstable-msc4075",
|
||||
"unstable-msc4140",
|
||||
"unstable-msc4171",
|
||||
] }
|
||||
ruma-common = { version = "0.15.1" }
|
||||
serde = "1.0.217"
|
||||
"unstable-msc4278",
|
||||
"unstable-msc4286",
|
||||
] }
|
||||
ruma-common = "0.15.2"
|
||||
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,62 +100,22 @@ 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.10.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.10.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.10.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.10.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.10.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.10.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.10.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.10.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.10.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.10.0", default-features = false }
|
||||
|
||||
# Default release profile, select with `--release`
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
# Default development profile; default for most Cargo commands, otherwise
|
||||
# selected with `--debug`
|
||||
[profile.dev]
|
||||
# Saves a lot of disk space. If symbols are needed, use the dbg profile.
|
||||
debug = 0
|
||||
|
||||
[profile.dev.package]
|
||||
# Optimize quote even in debug mode. Speeds up proc-macros enough to account
|
||||
# for the extra time of optimizing it for a clean build of matrix-sdk-ffi.
|
||||
quote = { opt-level = 2 }
|
||||
sha2 = { opt-level = 2 }
|
||||
# faster runs for insta.rs snapshot testing
|
||||
insta.opt-level = 3
|
||||
similar.opt-level = 3
|
||||
|
||||
# Custom profile with full debugging info, use `--profile dbg` to select
|
||||
[profile.dbg]
|
||||
inherits = "dev"
|
||||
debug = 2
|
||||
|
||||
# Custom profile for use in (debug) builds of the binding crates, use
|
||||
# `--profile reldbg` to select
|
||||
[profile.reldbg]
|
||||
inherits = "dbg"
|
||||
opt-level = 3
|
||||
|
||||
[patch.crates-io]
|
||||
async-compat = { git = "https://github.com/jplatte/async-compat", rev = "16dc8597ec09a6102d58d4e7b67714a35dd0ecb8" }
|
||||
const_panic = { git = "https://github.com/jplatte/const_panic", rev = "9024a4cb3eac45c1d2d980f17aaee287b17be498" }
|
||||
# Needed to fix rotation log issue on Android (https://github.com/tokio-rs/tracing/issues/2937)
|
||||
tracing = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
tracing-core = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
tracing-subscriber = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
tracing-appender = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
paranoid-android = { git = "https://github.com/element-hq/paranoid-android.git", rev = "69388ac5b4afeed7be4401c70ce17f6d9a2cf19b" }
|
||||
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"
|
||||
@@ -180,3 +144,46 @@ str_to_string = "warn"
|
||||
todo = "warn"
|
||||
unused_async = "warn"
|
||||
redundant_clone = "warn"
|
||||
|
||||
# Default development profile; default for most Cargo commands, otherwise
|
||||
# selected with `--debug`
|
||||
[profile.dev]
|
||||
# Saves a lot of disk space. If symbols are needed, use the dbg profile.
|
||||
debug = 0
|
||||
|
||||
[profile.dev.package]
|
||||
# Optimize quote even in debug mode. Speeds up proc-macros enough to account
|
||||
# for the extra time of optimizing it for a clean build of matrix-sdk-ffi.
|
||||
quote = { opt-level = 2 }
|
||||
sha2 = { opt-level = 2 }
|
||||
# faster runs for insta.rs snapshot testing
|
||||
insta.opt-level = 3
|
||||
similar.opt-level = 3
|
||||
|
||||
# Custom profile with full debugging info, use `--profile dbg` to select
|
||||
[profile.dbg]
|
||||
inherits = "dev"
|
||||
debug = 2
|
||||
|
||||
# Custom profile for use in (debug) builds of the binding crates, use
|
||||
# `--profile reldbg` to select
|
||||
[profile.reldbg]
|
||||
inherits = "dbg"
|
||||
opt-level = 3
|
||||
|
||||
[profile.profiling]
|
||||
inherits = "release"
|
||||
# LTO is too slow to compile.
|
||||
lto = false
|
||||
# Get symbol names for profiling purposes.
|
||||
debug = true
|
||||
|
||||
[patch.crates-io]
|
||||
async-compat = { git = "https://github.com/element-hq/async-compat", rev = "5a27c8b290f1f1dcfc0c4ec22c464e38528aa591" }
|
||||
const_panic = { git = "https://github.com/jplatte/const_panic", rev = "9024a4cb3eac45c1d2d980f17aaee287b17be498" }
|
||||
# Needed to fix rotation log issue on Android (https://github.com/tokio-rs/tracing/issues/2937)
|
||||
tracing = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
tracing-core = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
tracing-subscriber = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
tracing-appender = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
paranoid-android = { git = "https://github.com/element-hq/paranoid-android.git", rev = "69388ac5b4afeed7be4401c70ce17f6d9a2cf19b" }
|
||||
|
||||
@@ -1,40 +1,68 @@
|
||||

|
||||
[](https://codecov.io/gh/matrix-org/matrix-rust-sdk)
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://matrix.to/#/#matrix-rust-sdk:matrix.org)
|
||||
[](https://matrix-org.github.io/matrix-rust-sdk/matrix_sdk/)
|
||||
[](https://docs.rs/matrix-sdk)
|
||||
<h1 align="center">Matrix Rust SDK</h1>
|
||||
<div align="center">
|
||||
<i>Your all-in-one toolkit for creating Matrix clients with Rust, from simple bots to full-featured apps.</i>
|
||||
<br/><br/>
|
||||
<img src="contrib/logo.svg">
|
||||
<br>
|
||||
<hr>
|
||||
<a href="https://github.com/matrix-org/matrix-rust-sdk/releases">
|
||||
<img src="https://img.shields.io/github/v/release/matrix-org/matrix-rust-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=GitHub&logoColor=white"></a>
|
||||
<a href="https://crates.io/crates/matrix-sdk/">
|
||||
<img src="https://img.shields.io/crates/v/matrix-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Rust&logoColor=white"></a>
|
||||
<a href="https://codecov.io/gh/matrix-org/matrix-rust-sdk">
|
||||
<img src="https://img.shields.io/codecov/c/gh/matrix-org/matrix-rust-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Codecov&logoColor=white"></a>
|
||||
<br>
|
||||
<a href="https://docs.rs/matrix-sdk/">
|
||||
<img src="https://img.shields.io/docsrs/matrix-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Rust&logoColor=white"></a>
|
||||
<a href="https://github.com/matrix-org/matrix-rust-sdk/actions/workflows/ci.yml">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/matrix-org/matrix-rust-sdk/ci.yml?style=flat&labelColor=1C2E27&color=66845F&logo=GitHub%20Actions&logoColor=white"></a>
|
||||
<br>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
# matrix-rust-sdk
|
||||
|
||||
**matrix-rust-sdk** is an implementation of a [Matrix][] client-server library in [Rust][].
|
||||
The Matrix Rust SDK is a collection of libraries that make it easier to build
|
||||
[Matrix] clients in [Rust]. It takes care of the low-level details like encryption,
|
||||
syncing, and room state, so you can focus on your app's logic and UI. Whether
|
||||
you're writing a small bot, a desktop client, or something in between, the SDK
|
||||
is designed to be flexible, async-friendly, and ready to use out of the box.
|
||||
|
||||
[Matrix]: https://matrix.org/
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
|
||||
## Project structure
|
||||
|
||||
The rust-sdk consists of multiple crates that can be picked at your convenience:
|
||||
The Matrix Rust SDK is made up of several crates that build on top of each other. Here are the key ones:
|
||||
|
||||
- **matrix-sdk** - High level client library, with batteries included, you're most likely
|
||||
interested in this.
|
||||
- **matrix-sdk-base** - No (network) IO client state machine that can be used to embed a
|
||||
Matrix client in your project or build a full fledged network enabled client
|
||||
lib on top of it.
|
||||
- **matrix-sdk-crypto** - No (network) IO encryption state machine that can be
|
||||
used to add Matrix E2EE support to your client or client library.
|
||||
- [matrix-sdk-ui](https://docs.rs/matrix-sdk-ui/latest/matrix_sdk_ui/) – A high-level client library that makes it easy to build
|
||||
full-featured UI clients with minimal setup. Check out our reference client,
|
||||
[multiverse](https://github.com/matrix-org/matrix-rust-sdk/tree/main/labs/multiverse), for an example.
|
||||
- [matrix-sdk](https://docs.rs/matrix-sdk/latest/matrix_sdk/) – A mid-level client library, ideal for building bots, custom
|
||||
clients, or higher-level abstractions. You can find example usage in the
|
||||
[examples directory](https://github.com/matrix-org/matrix-rust-sdk/tree/main/examples).
|
||||
- [matrix-sdk-crypto](https://docs.rs/matrix-sdk-crypto/latest/matrix_sdk_crypto/) – A standalone encryption state machine with no network I/O,
|
||||
providing end-to-end encryption support for Matrix clients and libraries.
|
||||
See the [crypto tutorial](https://docs.rs/matrix-sdk-crypto/latest/matrix_sdk_crypto/tutorial/index.html)
|
||||
for a step-by-step introduction.
|
||||
|
||||
## Status
|
||||
|
||||
The library is considered production ready and backs multiple client implementations such as Element X [[1]](https://github.com/element-hq/element-x-ios) [[2]](https://github.com/element-hq/element-x-android) and [Fractal](https://gitlab.gnome.org/World/fractal). Client developers should feel confident to build upon it.
|
||||
The library is considered production ready and backs multiple client
|
||||
implementations such as Element X
|
||||
[[1]](https://github.com/element-hq/element-x-ios)
|
||||
[[2]](https://github.com/element-hq/element-x-android),
|
||||
[Fractal](https://gitlab.gnome.org/World/fractal) and [iamb](https://github.com/ulyssa/iamb). Client developers should feel
|
||||
confident to build upon it.
|
||||
|
||||
Development of the SDK has been primarily sponsored by Element though accepts contributions from all.
|
||||
Development of the SDK has been primarily sponsored by Element though accepts
|
||||
contributions from all.
|
||||
|
||||
## Bindings
|
||||
|
||||
Some crates of the **matrix-rust-sdk** can be embedded inside other
|
||||
environments, like Swift, Kotlin, JavaScript, Node.js etc. Please,
|
||||
explore the [`bindings/`](./bindings/) directory to learn more.
|
||||
The higher-level crates of the Matrix Rust SDK can be embedded in other
|
||||
environments such as Swift, Kotlin, JavaScript, and Node.js. Check out the
|
||||
[bindings/](./bindings/) directory to learn more about how to integrate the SDK
|
||||
into your language of choice.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+19
-18
@@ -13,35 +13,36 @@ The procedure is as follows:
|
||||
|
||||
1. Switch to a release branch:
|
||||
|
||||
```bash
|
||||
git switch -c release-x.y.z
|
||||
```
|
||||
```bash
|
||||
git switch -c release-x.y.z
|
||||
```
|
||||
|
||||
2. Prepare the release. This will update the `README.md`, set the versions in
|
||||
the `CHANGELOG.md` file, and bump the version in the `Cargo.toml` file.
|
||||
|
||||
```bash
|
||||
cargo xtask release prepare --execute minor|patch|rc
|
||||
```
|
||||
```bash
|
||||
cargo xtask release prepare --execute minor|patch|rc
|
||||
```
|
||||
|
||||
3. Double-check and edit the `CHANGELOG.md` and `README.md` if necessary. Once you are
|
||||
satisfied, push the branch and open a PR.
|
||||
|
||||
```bash
|
||||
git push --set-upstream origin/release-x.y.z
|
||||
```
|
||||
```bash
|
||||
git push --set-upstream origin/release-x.y.z
|
||||
```
|
||||
|
||||
4. Pass the review and merge the branch as you would with any other branch.
|
||||
|
||||
5. Create tags for your new release, publish the release on crates.io and push
|
||||
the tags:
|
||||
|
||||
```bash
|
||||
# Switch to main first.
|
||||
git switch main
|
||||
# Pull in the now-merged release commit(s).
|
||||
git pull
|
||||
# Create tags, publish the release on crates.io, and push the tags.
|
||||
cargo xtask release publish --execute
|
||||
```
|
||||
For more information on cargo-release: https://github.com/crate-ci/cargo-release
|
||||
```bash
|
||||
# Switch to main first.
|
||||
git switch main
|
||||
# Pull in the now-merged release commit(s).
|
||||
git pull
|
||||
# Create tags, publish the release on crates.io, and push the tags.
|
||||
cargo xtask release publish --execute
|
||||
```
|
||||
|
||||
For more information on cargo-release: https://github.com/crate-ci/cargo-release
|
||||
|
||||
+20
-12
@@ -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 = { workspace = true, features = ["native-tls", "e2e-encryption", "sqlite", "testing"] }
|
||||
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"] }
|
||||
ruma = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
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"] }
|
||||
@@ -29,6 +32,10 @@ pprof = { version = "0.14.0", features = ["flamegraph", "criterion"] }
|
||||
name = "crypto_bench"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "linked_chunk"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "store_bench"
|
||||
harness = false
|
||||
@@ -37,5 +44,6 @@ harness = false
|
||||
name = "room_bench"
|
||||
harness = false
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
[[bench]]
|
||||
name = "timeline"
|
||||
harness = false
|
||||
|
||||
+10
-3
@@ -8,7 +8,7 @@ can be found [here](https://bheisler.github.io/criterion.rs/book/criterion_rs.ht
|
||||
|
||||
## Running the benchmarks
|
||||
|
||||
The benchmark can be simply run by using the `bench` command of `cargo`:
|
||||
The benchmark can be run by using the `bench` command of `cargo`:
|
||||
|
||||
```bash
|
||||
$ cargo bench
|
||||
@@ -16,6 +16,13 @@ $ cargo bench
|
||||
|
||||
This will work from the workspace directory of the rust-sdk.
|
||||
|
||||
To lower compile times, you might be interested in using the `profiling` profile, that's optimized
|
||||
for a fair tradeoff between compile times and runtime performance:
|
||||
|
||||
```bash
|
||||
$ cargo bench --profile profiling
|
||||
```
|
||||
|
||||
If you want to pass options to the benchmark [you'll need to specify the name of
|
||||
the benchmark](https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options):
|
||||
|
||||
@@ -23,7 +30,7 @@ the benchmark](https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench
|
||||
$ cargo bench --bench crypto_bench -- # Your options go here
|
||||
```
|
||||
|
||||
If you want to run only a specific benchmark, simply pass the name of the
|
||||
If you want to run only a specific benchmark, pass the name of the
|
||||
benchmark as an argument:
|
||||
|
||||
```bash
|
||||
@@ -65,7 +72,7 @@ permisive value is `-1`:
|
||||
$ echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid
|
||||
```
|
||||
|
||||
To generate flame graphs feature simply enable the profiling mode using the
|
||||
To generate flame graphs feature, enable the profiling mode using the
|
||||
`--profile-time` command line flag:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::{
|
||||
linked_chunk::{lazy_loader, LinkedChunk, LinkedChunkId, Update},
|
||||
SqliteEventCacheStore,
|
||||
};
|
||||
use matrix_sdk_base::event_cache::{
|
||||
store::{DynEventCacheStore, IntoEventCacheStore, MemoryStore, DEFAULT_CHUNK_CAPACITY},
|
||||
Event, Gap,
|
||||
};
|
||||
use matrix_sdk_test::{event_factory::EventFactory, ALICE};
|
||||
use ruma::{room_id, EventId};
|
||||
use tempfile::tempdir;
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Operation {
|
||||
PushItemsBack(Vec<Event>),
|
||||
PushGapBack(Gap),
|
||||
}
|
||||
|
||||
fn writing(c: &mut Criterion) {
|
||||
// Create a new asynchronous runtime.
|
||||
let runtime = Builder::new_multi_thread()
|
||||
.enable_time()
|
||||
.enable_io()
|
||||
.build()
|
||||
.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");
|
||||
group.sample_size(10).measurement_time(Duration::from_secs(30));
|
||||
|
||||
for number_of_events in [10, 100, 1000, 10_000, 100_000] {
|
||||
let sqlite_temp_dir = tempdir().unwrap();
|
||||
|
||||
// Declare new stores for this set of events.
|
||||
let stores: [(&str, Option<Arc<DynEventCacheStore>>); 3] = [
|
||||
("none", None),
|
||||
("memory store", Some(MemoryStore::default().into_event_cache_store())),
|
||||
(
|
||||
"sqlite store",
|
||||
runtime.block_on(async {
|
||||
Some(
|
||||
SqliteEventCacheStore::open(sqlite_temp_dir.path().join("bench"), None)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_event_cache_store(),
|
||||
)
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
for (store_name, store) in stores {
|
||||
// Create the operations we want to bench.
|
||||
let mut operations = Vec::new();
|
||||
|
||||
{
|
||||
let mut events = (0..number_of_events)
|
||||
.map(|nth| {
|
||||
event_factory
|
||||
.text_msg("foo")
|
||||
.event_id(&EventId::parse(format!("$ev{nth}")).unwrap())
|
||||
.into_event()
|
||||
})
|
||||
.peekable();
|
||||
|
||||
let mut gap_nth = 0;
|
||||
|
||||
while events.peek().is_some() {
|
||||
{
|
||||
let events_to_push_back = events.by_ref().take(80).collect::<Vec<_>>();
|
||||
|
||||
if events_to_push_back.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
operations.push(Operation::PushItemsBack(events_to_push_back));
|
||||
}
|
||||
|
||||
{
|
||||
operations.push(Operation::PushGapBack(Gap {
|
||||
prev_token: format!("gap{gap_nth}"),
|
||||
}));
|
||||
gap_nth += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Define the throughput.
|
||||
group.throughput(Throughput::Elements(number_of_events));
|
||||
|
||||
// Get a bencher.
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new(store_name, number_of_events),
|
||||
&operations,
|
||||
|bencher, operations| {
|
||||
// Bench the routine.
|
||||
bencher.to_async(&runtime).iter_batched(
|
||||
|| operations.clone(),
|
||||
|operations| async {
|
||||
// The routine to bench!
|
||||
|
||||
let mut linked_chunk = LinkedChunk::<DEFAULT_CHUNK_CAPACITY, Event, Gap>::new_with_update_history();
|
||||
|
||||
for operation in operations {
|
||||
match operation {
|
||||
Operation::PushItemsBack(events) => linked_chunk.push_items_back(events),
|
||||
Operation::PushGapBack(gap) => linked_chunk.push_gap_back(gap),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(store) = &store {
|
||||
let updates = linked_chunk.updates().unwrap().take();
|
||||
store.handle_linked_chunk_updates(linked_chunk_id, updates).await.unwrap();
|
||||
// Empty the store.
|
||||
store.handle_linked_chunk_updates(linked_chunk_id, vec![Update::Clear]).await.unwrap();
|
||||
}
|
||||
|
||||
},
|
||||
BatchSize::SmallInput
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(store);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.finish()
|
||||
}
|
||||
|
||||
fn reading(c: &mut Criterion) {
|
||||
// Create a new asynchronous runtime.
|
||||
let runtime = Builder::new_multi_thread()
|
||||
.enable_time()
|
||||
.enable_io()
|
||||
.build()
|
||||
.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");
|
||||
group.sample_size(10);
|
||||
|
||||
for num_events in [10, 100, 1000, 10_000, 100_000] {
|
||||
let sqlite_temp_dir = tempdir().unwrap();
|
||||
|
||||
// Declare new stores for this set of events.
|
||||
let stores: [(&str, Arc<DynEventCacheStore>); 2] = [
|
||||
("memory store", MemoryStore::default().into_event_cache_store()),
|
||||
(
|
||||
"sqlite store",
|
||||
runtime.block_on(async {
|
||||
SqliteEventCacheStore::open(sqlite_temp_dir.path().join("bench"), None)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_event_cache_store()
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
for (store_name, store) in stores {
|
||||
// Store some events and gap chunks in the store.
|
||||
{
|
||||
let mut events = (0..num_events)
|
||||
.map(|nth| {
|
||||
event_factory
|
||||
.text_msg("foo")
|
||||
.event_id(&EventId::parse(format!("$ev{nth}")).unwrap())
|
||||
.into_event()
|
||||
})
|
||||
.peekable();
|
||||
|
||||
let mut lc =
|
||||
LinkedChunk::<DEFAULT_CHUNK_CAPACITY, Event, Gap>::new_with_update_history();
|
||||
let mut num_gaps = 0;
|
||||
|
||||
while events.peek().is_some() {
|
||||
let events_chunk = events.by_ref().take(80).collect::<Vec<_>>();
|
||||
if events_chunk.is_empty() {
|
||||
break;
|
||||
}
|
||||
lc.push_items_back(events_chunk);
|
||||
lc.push_gap_back(Gap { prev_token: format!("gap{num_gaps}") });
|
||||
num_gaps += 1;
|
||||
}
|
||||
|
||||
// Now persist the updates to recreate this full linked chunk.
|
||||
let updates = lc.updates().unwrap().take();
|
||||
runtime
|
||||
.block_on(store.handle_linked_chunk_updates(linked_chunk_id, updates))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Define the throughput.
|
||||
group.throughput(Throughput::Elements(num_events));
|
||||
|
||||
// Get a bencher.
|
||||
group.bench_function(BenchmarkId::new(store_name, num_events), |bencher| {
|
||||
// Bench the routine.
|
||||
bencher.to_async(&runtime).iter(|| async {
|
||||
// Load the last chunk first,
|
||||
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)
|
||||
.expect("no error when reconstructing the linked chunk")
|
||||
.expect("there is a linked chunk in the store");
|
||||
|
||||
// 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(linked_chunk_id, cur_chunk_id).await.unwrap()
|
||||
{
|
||||
cur_chunk_id = prev.identifier;
|
||||
lazy_loader::insert_new_first_chunk(&mut lc, prev)
|
||||
.expect("no error when linking the previous lazy-loaded chunk");
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(store);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.finish()
|
||||
}
|
||||
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
let criterion = Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
100,
|
||||
pprof::criterion::Output::Flamegraph(None),
|
||||
));
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let criterion = Criterion::default();
|
||||
|
||||
criterion
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = event_cache;
|
||||
config = criterion();
|
||||
targets = writing, reading,
|
||||
}
|
||||
|
||||
criterion_main!(event_cache);
|
||||
@@ -1,15 +1,13 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use std::time::Duration;
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server};
|
||||
use matrix_sdk::{store::RoomLoadSettings, test_utils::mocks::MatrixMockServer};
|
||||
use matrix_sdk_base::{
|
||||
store::StoreConfig, BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore,
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteStateStore;
|
||||
use matrix_sdk_test::{
|
||||
event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder,
|
||||
};
|
||||
use matrix_sdk_ui::{timeline::TimelineFocus, Timeline};
|
||||
use matrix_sdk_test::{event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent};
|
||||
use matrix_sdk_ui::timeline::{TimelineBuilder, TimelineFocus};
|
||||
use ruma::{
|
||||
api::client::membership::get_member_events,
|
||||
device_id,
|
||||
@@ -18,13 +16,9 @@ use ruma::{
|
||||
serde::Raw,
|
||||
user_id, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use tokio::runtime::Builder;
|
||||
use wiremock::{
|
||||
matchers::{header, method, path, path_regex, query_param, query_param_is_missing},
|
||||
Mock, MockServer, Request, ResponseTemplate,
|
||||
};
|
||||
use wiremock::{Request, ResponseTemplate};
|
||||
|
||||
pub fn receive_all_members_benchmark(c: &mut Criterion) {
|
||||
const MEMBERS_IN_ROOM: usize = 100000;
|
||||
@@ -35,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)
|
||||
@@ -61,17 +55,18 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
|
||||
.block_on(sqlite_store.save_changes(&changes))
|
||||
.expect("initial filling of sqlite failed");
|
||||
|
||||
let base_client = BaseClient::with_store_config(
|
||||
let base_client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
|
||||
.state_store(sqlite_store),
|
||||
);
|
||||
|
||||
runtime
|
||||
.block_on(base_client.set_session_meta(
|
||||
.block_on(base_client.activate(
|
||||
SessionMeta {
|
||||
user_id: user_id!("@somebody:example.com").to_owned(),
|
||||
device_id: device_id!("DEVICE_ID").to_owned(),
|
||||
},
|
||||
RoomLoadSettings::default(),
|
||||
None,
|
||||
))
|
||||
.expect("Could not set session meta");
|
||||
@@ -109,9 +104,7 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
let sender_id = owned_user_id!("@sender:example.com");
|
||||
|
||||
let f = EventFactory::new().room(&room_id).sender(&sender_id);
|
||||
let (client, server) = runtime.block_on(logged_in_client_with_server());
|
||||
|
||||
let mut sync_response_builder = SyncResponseBuilder::new();
|
||||
let mut joined_room_builder =
|
||||
JoinedRoomBuilder::new(&room_id).add_state_event(StateTestEvent::Encryption);
|
||||
|
||||
@@ -133,17 +126,15 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
}
|
||||
}
|
||||
)));
|
||||
let response_json =
|
||||
sync_response_builder.add_joined_room(joined_room_builder).build_json_sync_response();
|
||||
runtime.block_on(mock_sync(&server, response_json, None));
|
||||
|
||||
let sync_settings = SyncSettings::default();
|
||||
runtime.block_on(client.sync_once(sync_settings)).expect("Could not sync");
|
||||
runtime.block_on(server.reset());
|
||||
let (server, client, room) = runtime.block_on(async move {
|
||||
let server = MatrixMockServer::new().await;
|
||||
let client = server.client_builder().build().await;
|
||||
|
||||
runtime.block_on(
|
||||
Mock::given(method("GET"))
|
||||
.and(path_regex(r"/_matrix/client/r0/rooms/.*/event/.*"))
|
||||
let room = server.sync_room(&client, joined_room_builder).await;
|
||||
|
||||
server
|
||||
.mock_room_event()
|
||||
.respond_with(move |r: &Request| {
|
||||
let segments: Vec<&str> = r.url.path_segments().expect("Invalid path").collect();
|
||||
let event_id_str = segments[6];
|
||||
@@ -157,10 +148,14 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
.set_delay(Duration::from_millis(50))
|
||||
.set_body_json(event.json())
|
||||
})
|
||||
.mount(&server),
|
||||
);
|
||||
.mount()
|
||||
.await;
|
||||
|
||||
client.event_cache().subscribe().unwrap();
|
||||
|
||||
(server, client, room)
|
||||
});
|
||||
|
||||
let room = client.get_room(&room_id).expect("Room not found");
|
||||
let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
|
||||
assert!(!pinned_event_ids.is_empty());
|
||||
assert_eq!(pinned_event_ids.len(), PINNED_EVENTS_COUNT);
|
||||
@@ -171,15 +166,6 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
group.throughput(Throughput::Elements(count as u64));
|
||||
group.sample_size(10);
|
||||
|
||||
let client = Arc::new(client);
|
||||
|
||||
{
|
||||
let client = client.clone();
|
||||
runtime.spawn_blocking(move || {
|
||||
client.event_cache().subscribe().unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
group.bench_function(BenchmarkId::new("load_pinned_events", name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
|
||||
@@ -187,9 +173,16 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
assert_eq!(pinned_event_ids.len(), PINNED_EVENTS_COUNT);
|
||||
|
||||
// Reset cache so it always loads the events from the mocked endpoint
|
||||
client.event_cache().empty_immutable_cache().await;
|
||||
client
|
||||
.event_cache_store()
|
||||
.lock()
|
||||
.await
|
||||
.unwrap()
|
||||
.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,
|
||||
@@ -206,40 +199,25 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
runtime.block_on(server.reset());
|
||||
drop(server);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
async fn mock_sync(server: &MockServer, response_body: impl Serialize, since: Option<String>) {
|
||||
let mut mock_builder = Mock::given(method("GET"))
|
||||
.and(path("/_matrix/client/r0/sync"))
|
||||
.and(header("authorization", "Bearer 1234"));
|
||||
|
||||
if let Some(since) = since {
|
||||
mock_builder = mock_builder.and(query_param("since", since));
|
||||
} else {
|
||||
mock_builder = mock_builder.and(query_param_is_missing("since"));
|
||||
}
|
||||
|
||||
mock_builder
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(response_body))
|
||||
.mount(server)
|
||||
.await;
|
||||
}
|
||||
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
let criterion = Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
100,
|
||||
pprof::criterion::Output::Flamegraph(None),
|
||||
));
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let criterion = Criterion::default();
|
||||
{
|
||||
Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
100,
|
||||
pprof::criterion::Output::Flamegraph(None),
|
||||
))
|
||||
}
|
||||
|
||||
criterion
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
Criterion::default()
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
|
||||
@@ -2,9 +2,8 @@ use std::sync::Arc;
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::{
|
||||
authentication::matrix::{MatrixSession, MatrixSessionTokens},
|
||||
config::StoreConfig,
|
||||
Client, RoomInfo, RoomState, StateChanges,
|
||||
authentication::matrix::MatrixSession, config::StoreConfig, Client, RoomInfo, RoomState,
|
||||
SessionTokens, StateChanges,
|
||||
};
|
||||
use matrix_sdk_base::{store::MemoryStore, SessionMeta, StateStore as _};
|
||||
use matrix_sdk_sqlite::SqliteStateStore;
|
||||
@@ -51,7 +50,7 @@ pub fn restore_session(c: &mut Criterion) {
|
||||
user_id: user_id!("@somebody:example.com").to_owned(),
|
||||
device_id: device_id!("DEVICE_ID").to_owned(),
|
||||
},
|
||||
tokens: MatrixSessionTokens { access_token: "OHEY".to_owned(), refresh_token: None },
|
||||
tokens: SessionTokens { access_token: "OHEY".to_owned(), refresh_token: None },
|
||||
};
|
||||
|
||||
// Start the benchmark.
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
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::TimelineBuilder;
|
||||
use ruma::{
|
||||
events::room::message::RoomMessageEventContentWithoutRelation, owned_room_id, owned_user_id,
|
||||
EventId,
|
||||
};
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
/// Benchmark the time it takes to create a timeline (with read receipt
|
||||
/// support), when there are many initial events at rest in the event cache.
|
||||
///
|
||||
/// `NUM_EVENTS` is the number of events that will be stored initially in the
|
||||
/// event cache. It will be a mix of messages, reactions, edits and redactions,
|
||||
/// so there are some aggregations to take into account by the timeline as well.
|
||||
pub fn create_timeline_with_initial_events(c: &mut Criterion) {
|
||||
const NUM_EVENTS: usize = 10000;
|
||||
|
||||
let runtime = Builder::new_multi_thread().enable_all().build().expect("Can't create runtime");
|
||||
let room_id = owned_room_id!("!room:example.com");
|
||||
|
||||
let sender_id = owned_user_id!("@sender:example.com");
|
||||
let other_sender_id = owned_user_id!("@other_sender:example.com");
|
||||
let another_sender_id = owned_user_id!("@another_sender:example.com");
|
||||
|
||||
let f = EventFactory::new().room(&room_id);
|
||||
|
||||
let mut events = Vec::new();
|
||||
for i in 0..NUM_EVENTS {
|
||||
let sender = match i % 3 {
|
||||
0 => &sender_id,
|
||||
1 => &other_sender_id,
|
||||
2 => &another_sender_id,
|
||||
_ => unreachable!("math genius over here"),
|
||||
};
|
||||
|
||||
let event_id = EventId::parse(format!("$event{i}")).unwrap();
|
||||
|
||||
let j = i % 10;
|
||||
if j < 6 {
|
||||
// Messages.
|
||||
events.push(
|
||||
f.text_msg(format!("Message {i}"))
|
||||
.sender(sender)
|
||||
.event_id(&event_id)
|
||||
.into_raw_sync(),
|
||||
);
|
||||
} else if j < 8 {
|
||||
// Reactions.
|
||||
let prev_event = EventId::parse(format!("$event{}", i - 2)).unwrap();
|
||||
events.push(
|
||||
f.reaction(&prev_event, "👍").sender(sender).event_id(&event_id).into_raw_sync(),
|
||||
);
|
||||
} else if j == 8 {
|
||||
// Edit.
|
||||
// Note: (i-3)%3 is the same as i%3 -> same sender!
|
||||
let prev_event = EventId::parse(format!("$event{}", i - 3)).unwrap();
|
||||
events.push(
|
||||
f.text_msg(format!("* Message {}v2", i - 3))
|
||||
.edit(
|
||||
&prev_event,
|
||||
RoomMessageEventContentWithoutRelation::text_plain(format!(
|
||||
"Message {}v2",
|
||||
i - 3
|
||||
)),
|
||||
)
|
||||
.sender(sender)
|
||||
.event_id(&event_id)
|
||||
.into_raw_sync(),
|
||||
);
|
||||
} else if j == 9 {
|
||||
// Redaction.
|
||||
// Note: (i-6)%3 is the same as i%6 -> same sender!
|
||||
let prev_event = EventId::parse(format!("$event{}", i - 6)).unwrap();
|
||||
events
|
||||
.push(f.redaction(&prev_event).sender(sender).event_id(&event_id).into_raw_sync());
|
||||
}
|
||||
}
|
||||
|
||||
let builder = JoinedRoomBuilder::new(&room_id)
|
||||
.add_state_event(StateTestEvent::Encryption)
|
||||
.add_timeline_bulk(events);
|
||||
|
||||
let room = runtime.block_on(async move {
|
||||
let server = MatrixMockServer::new().await;
|
||||
let client = server.client_builder().build().await;
|
||||
|
||||
client.event_cache().subscribe().unwrap();
|
||||
|
||||
let room = server.sync_room(&client, builder).await;
|
||||
drop(server);
|
||||
|
||||
room
|
||||
});
|
||||
|
||||
let mut group = c.benchmark_group("Test");
|
||||
group.throughput(Throughput::Elements(NUM_EVENTS as _));
|
||||
group.sample_size(10);
|
||||
|
||||
group.bench_function(
|
||||
BenchmarkId::new("create_timeline_with_initial_events", format!("{NUM_EVENTS} events")),
|
||||
|b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let timeline = TimelineBuilder::new(&room)
|
||||
.track_read_marker_and_receipts()
|
||||
.build()
|
||||
.await
|
||||
.expect("Could not create timeline");
|
||||
|
||||
let (items, _) = timeline.subscribe().await;
|
||||
assert_eq!(items.len(), 20);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
100,
|
||||
pprof::criterion::Output::Flamegraph(None),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
Criterion::default()
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = room;
|
||||
config = criterion();
|
||||
targets = create_timeline_with_initial_events
|
||||
}
|
||||
criterion_main!(room);
|
||||
@@ -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() },
|
||||
}
|
||||
|
||||
@@ -507,6 +507,7 @@ fn collect_sessions(
|
||||
imported: session.imported,
|
||||
backed_up: session.backed_up,
|
||||
history_visibility: None,
|
||||
shared_history: false,
|
||||
algorithm: RustEventEncryptionAlgorithm::MegolmV1AesSha2,
|
||||
};
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -1,9 +1,68 @@
|
||||
# unreleased
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
<!-- next-header -->
|
||||
|
||||
## [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:
|
||||
|
||||
- `TracingConfiguration` now includes a new field `trace_log_packs`, which gives a convenient way
|
||||
to set the TRACE log level for multiple targets related to a given feature.
|
||||
([#4824](https://github.com/matrix-org/matrix-rust-sdk/pull/4824))
|
||||
|
||||
- `setup_tracing` has been renamed `init_platform`; in addition to the `TracingConfiguration`
|
||||
parameter it also now takes a boolean indicating whether to spawn a minimal tokio runtime for the
|
||||
application; in general for main app processes this can be set to `false`, and memory-constrained
|
||||
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
|
||||
@@ -29,12 +88,59 @@ Breaking changes:
|
||||
- There is a new `abortOidcLogin` method that should be called if the webview is dismissed without a callback (
|
||||
or fails to present).
|
||||
- The rest of `AuthenticationError` is now found in the OidcError type.
|
||||
|
||||
- `OidcAuthenticationData` is now called `OidcAuthorizationData`.
|
||||
|
||||
- The `get_element_call_required_permissions` function now requires the device_id.
|
||||
|
||||
- Some `OidcPrompt` cases have been removed (`None`, `SelectAccount`).
|
||||
|
||||
- `Room::is_encrypted` is replaced by `Room::latest_encryption_state`
|
||||
which returns a value of the new `EncryptionState` enum; another
|
||||
`Room::encryption_state` non-async and infallible method is added to get the
|
||||
`EncryptionState` without running a network request.
|
||||
([#4777](https://github.com/matrix-org/matrix-rust-sdk/pull/4777)). One can
|
||||
safely replace:
|
||||
|
||||
```rust
|
||||
room.is_encrypted().await?
|
||||
```
|
||||
|
||||
by
|
||||
|
||||
```rust
|
||||
room.latest_encryption_state().await?.is_encrypted()
|
||||
```
|
||||
|
||||
- `ClientBuilder::passphrase` is renamed `session_passphrase`
|
||||
([#4870](https://github.com/matrix-org/matrix-rust-sdk/pull/4870/))
|
||||
|
||||
- Merge `Timeline::send_thread_reply` into `Timeline::send_reply`. This
|
||||
changes the parameters of `send_reply` which now requires passing the
|
||||
event ID (and thread reply behaviour) inside a `ReplyParameters` struct.
|
||||
([#4880](https://github.com/matrix-org/matrix-rust-sdk/pull/4880/))
|
||||
|
||||
- The `dynamic_registrations_file` field of `OidcConfiguration` was removed.
|
||||
Clients are supposed to re-register with the homeserver for every login.
|
||||
|
||||
- `RoomPreview::own_membership_details` is now `RoomPreview::member_with_sender_info`, takes any user id and returns an `Option<RoomMemberWithSenderInfo>`.
|
||||
|
||||
Additions:
|
||||
|
||||
- Add `Encryption::get_user_identity` which returns `UserIdentity`
|
||||
- Add `ClientBuilder::room_key_recipient_strategy`
|
||||
- Add `Room::send_raw`
|
||||
- Add `NotificationSettings::set_custom_push_rule`
|
||||
- Expose `withdraw_verification` to `UserIdentity`
|
||||
- Expose `report_room` to `Room`
|
||||
- Add `RoomInfo::encryption_state`
|
||||
([#4788](https://github.com/matrix-org/matrix-rust-sdk/pull/4788))
|
||||
- Add `Timeline::send_thread_reply` for clients that need to start threads
|
||||
themselves.
|
||||
([4819](https://github.com/matrix-org/matrix-rust-sdk/pull/4819))
|
||||
- Add `ClientBuilder::session_pool_max_size`, `::session_cache_size` and `::session_journal_size_limit` to control the stores configuration, especially their memory consumption
|
||||
([#4870](https://github.com/matrix-org/matrix-rust-sdk/pull/4870/))
|
||||
- Add `ClientBuilder::system_is_memory_constrained` to indicate that the system
|
||||
has less memory available than the current standard
|
||||
([#4894](https://github.com/matrix-org/matrix-rust-sdk/pull/4894))
|
||||
- Add `Room::member_with_sender_info` to get both a room member's info and for the user who sent the `m.room.member` event the `RoomMember` is based on.
|
||||
|
||||
@@ -1,51 +1,81 @@
|
||||
[package]
|
||||
name = "matrix-sdk-ffi"
|
||||
version = "0.2.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 }
|
||||
async-compat = "0.2.1"
|
||||
eyeball-im = { workspace = true }
|
||||
anyhow.workspace = true
|
||||
as_variant.workspace = true
|
||||
async-compat = "0.2.4"
|
||||
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"
|
||||
@@ -55,31 +85,31 @@ workspace = true
|
||||
features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
"experimental-oidc",
|
||||
"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-oidc",
|
||||
"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 = false
|
||||
|
||||
@@ -5,18 +5,14 @@ use std::{
|
||||
};
|
||||
|
||||
use matrix_sdk::{
|
||||
authentication::oidc::{
|
||||
registrations::OidcRegistrationsError,
|
||||
types::{
|
||||
iana::oauth::OAuthClientAuthenticationMethod,
|
||||
oidc::ApplicationType,
|
||||
registration::{ClientMetadata, Localized, VerifiedClientMetadata},
|
||||
requests::GrantType,
|
||||
},
|
||||
OidcError as SdkOidcError,
|
||||
authentication::oauth::{
|
||||
error::OAuthAuthorizationCodeError,
|
||||
registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType},
|
||||
ClientId, ClientRegistrationData, OAuthError as SdkOAuthError,
|
||||
},
|
||||
Error,
|
||||
};
|
||||
use ruma::serde::Raw;
|
||||
use url::Url;
|
||||
|
||||
use crate::client::{Client, OidcPrompt, SlidingSyncVersion};
|
||||
@@ -116,60 +112,76 @@ pub struct OidcConfiguration {
|
||||
/// successful.
|
||||
pub redirect_uri: String,
|
||||
/// A URI that contains information about the client.
|
||||
pub client_uri: Option<String>,
|
||||
pub client_uri: String,
|
||||
/// A URI that contains the client's logo.
|
||||
pub logo_uri: Option<String>,
|
||||
/// A URI that contains the client's terms of service.
|
||||
pub tos_uri: Option<String>,
|
||||
/// A URI that contains the client's privacy policy.
|
||||
pub policy_uri: Option<String>,
|
||||
/// An array of e-mail addresses of people responsible for this client.
|
||||
pub contacts: Option<Vec<String>>,
|
||||
|
||||
/// Pre-configured registrations for use with issuers that don't support
|
||||
/// Pre-configured registrations for use with homeservers that don't support
|
||||
/// dynamic client registration.
|
||||
pub static_registrations: HashMap<String, String>,
|
||||
|
||||
/// A file path where any dynamic registrations should be stored.
|
||||
///
|
||||
/// Suggested value: `{base_path}/oidc/registrations.json`
|
||||
pub dynamic_registrations_file: String,
|
||||
/// The keys of the map should be the URLs of the homeservers, but keys
|
||||
/// using `issuer` URLs are also supported.
|
||||
pub static_registrations: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl TryInto<VerifiedClientMetadata> for &OidcConfiguration {
|
||||
type Error = OidcError;
|
||||
impl OidcConfiguration {
|
||||
pub(crate) fn redirect_uri(&self) -> Result<Url, OidcError> {
|
||||
Url::parse(&self.redirect_uri).map_err(|_| OidcError::CallbackUrlInvalid)
|
||||
}
|
||||
|
||||
fn try_into(self) -> Result<VerifiedClientMetadata, Self::Error> {
|
||||
let redirect_uri =
|
||||
Url::parse(&self.redirect_uri).map_err(|_| OidcError::CallbackUrlInvalid)?;
|
||||
pub(crate) fn client_metadata(&self) -> Result<Raw<ClientMetadata>, OidcError> {
|
||||
let redirect_uri = self.redirect_uri()?;
|
||||
let client_name = self.client_name.as_ref().map(|n| Localized::new(n.to_owned(), []));
|
||||
let client_uri = self.client_uri.localized_url()?;
|
||||
let logo_uri = self.logo_uri.localized_url()?;
|
||||
let policy_uri = self.policy_uri.localized_url()?;
|
||||
let tos_uri = self.tos_uri.localized_url()?;
|
||||
let contacts = self.contacts.clone();
|
||||
|
||||
ClientMetadata {
|
||||
application_type: Some(ApplicationType::Native),
|
||||
redirect_uris: Some(vec![redirect_uri]),
|
||||
grant_types: Some(vec![
|
||||
GrantType::RefreshToken,
|
||||
GrantType::AuthorizationCode,
|
||||
GrantType::DeviceCode,
|
||||
]),
|
||||
// A native client shouldn't use authentication as the credentials could be intercepted.
|
||||
token_endpoint_auth_method: Some(OAuthClientAuthenticationMethod::None),
|
||||
let metadata = ClientMetadata {
|
||||
// The server should display the following fields when getting the user's consent.
|
||||
client_name,
|
||||
contacts,
|
||||
client_uri,
|
||||
logo_uri,
|
||||
policy_uri,
|
||||
tos_uri,
|
||||
..Default::default()
|
||||
..ClientMetadata::new(
|
||||
ApplicationType::Native,
|
||||
vec![
|
||||
OAuthGrantType::AuthorizationCode { redirect_uris: vec![redirect_uri] },
|
||||
OAuthGrantType::DeviceCode,
|
||||
],
|
||||
client_uri,
|
||||
)
|
||||
};
|
||||
|
||||
Raw::new(&metadata).map_err(|_| OidcError::MetadataInvalid)
|
||||
}
|
||||
|
||||
pub(crate) fn registration_data(&self) -> Result<ClientRegistrationData, OidcError> {
|
||||
let client_metadata = self.client_metadata()?;
|
||||
|
||||
let mut registration_data = ClientRegistrationData::new(client_metadata);
|
||||
|
||||
if !self.static_registrations.is_empty() {
|
||||
let static_registrations = self
|
||||
.static_registrations
|
||||
.iter()
|
||||
.filter_map(|(issuer, client_id)| {
|
||||
let Ok(issuer) = Url::parse(issuer) else {
|
||||
tracing::error!("Failed to parse {issuer:?}");
|
||||
return None;
|
||||
};
|
||||
Some((issuer, ClientId::new(client_id.clone())))
|
||||
})
|
||||
.collect();
|
||||
|
||||
registration_data.static_registrations = Some(static_registrations);
|
||||
}
|
||||
.validate()
|
||||
.map_err(|_| OidcError::MetadataInvalid)
|
||||
|
||||
Ok(registration_data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,8 +194,6 @@ pub enum OidcError {
|
||||
NotSupported,
|
||||
#[error("Unable to use OIDC as the supplied client metadata is invalid.")]
|
||||
MetadataInvalid,
|
||||
#[error("Failed to use the supplied registrations file path.")]
|
||||
RegistrationsPathInvalid,
|
||||
#[error("The supplied callback URL used to complete OIDC is invalid.")]
|
||||
CallbackUrlInvalid,
|
||||
#[error("The OIDC login was cancelled by the user.")]
|
||||
@@ -193,23 +203,17 @@ pub enum OidcError {
|
||||
Generic { message: String },
|
||||
}
|
||||
|
||||
impl From<SdkOidcError> for OidcError {
|
||||
fn from(e: SdkOidcError) -> OidcError {
|
||||
impl From<SdkOAuthError> for OidcError {
|
||||
fn from(e: SdkOAuthError) -> OidcError {
|
||||
match e {
|
||||
SdkOidcError::MissingAuthenticationIssuer => OidcError::NotSupported,
|
||||
SdkOidcError::MissingRedirectUri => OidcError::MetadataInvalid,
|
||||
SdkOidcError::InvalidCallbackUrl => OidcError::CallbackUrlInvalid,
|
||||
SdkOidcError::InvalidState => OidcError::CallbackUrlInvalid,
|
||||
SdkOidcError::CancelledAuthorization => OidcError::Cancelled,
|
||||
_ => OidcError::Generic { message: e.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OidcRegistrationsError> for OidcError {
|
||||
fn from(e: OidcRegistrationsError) -> OidcError {
|
||||
match e {
|
||||
OidcRegistrationsError::InvalidFilePath => OidcError::RegistrationsPathInvalid,
|
||||
SdkOAuthError::Discovery(error) if error.is_not_supported() => OidcError::NotSupported,
|
||||
SdkOAuthError::AuthorizationCode(OAuthAuthorizationCodeError::RedirectUri(_))
|
||||
| SdkOAuthError::AuthorizationCode(OAuthAuthorizationCodeError::InvalidState) => {
|
||||
OidcError::CallbackUrlInvalid
|
||||
}
|
||||
SdkOAuthError::AuthorizationCode(OAuthAuthorizationCodeError::Cancelled) => {
|
||||
OidcError::Cancelled
|
||||
}
|
||||
_ => OidcError::Generic { message: e.to_string() },
|
||||
}
|
||||
}
|
||||
@@ -218,7 +222,7 @@ impl From<OidcRegistrationsError> for OidcError {
|
||||
impl From<Error> for OidcError {
|
||||
fn from(e: Error) -> OidcError {
|
||||
match e {
|
||||
Error::Oidc(e) => e.into(),
|
||||
Error::OAuth(e) => (*e).into(),
|
||||
_ => OidcError::Generic { message: e.to_string() },
|
||||
}
|
||||
}
|
||||
@@ -227,17 +231,25 @@ impl From<Error> for OidcError {
|
||||
/* Helpers */
|
||||
|
||||
trait OptionExt {
|
||||
/// Convenience method to convert a string to a URL and returns it as a
|
||||
/// Localized URL. No localization is actually performed.
|
||||
/// Convenience method to convert an `Option<String>` to a URL and returns
|
||||
/// it as a Localized URL. No localization is actually performed.
|
||||
fn localized_url(&self) -> Result<Option<Localized<Url>>, OidcError>;
|
||||
}
|
||||
|
||||
impl OptionExt for Option<String> {
|
||||
fn localized_url(&self) -> Result<Option<Localized<Url>>, OidcError> {
|
||||
self.as_deref()
|
||||
.map(|uri| -> Result<Localized<Url>, OidcError> {
|
||||
Ok(Localized::new(Url::parse(uri).map_err(|_| OidcError::MetadataInvalid)?, []))
|
||||
})
|
||||
.transpose()
|
||||
self.as_deref().map(StrExt::localized_url).transpose()
|
||||
}
|
||||
}
|
||||
|
||||
trait StrExt {
|
||||
/// Convenience method to convert a string to a URL and returns it as a
|
||||
/// Localized URL. No localization is actually performed.
|
||||
fn localized_url(&self) -> Result<Localized<Url>, OidcError>;
|
||||
}
|
||||
|
||||
impl StrExt for str {
|
||||
fn localized_url(&self) -> Result<Localized<Url>, OidcError> {
|
||||
Ok(Localized::new(Url::parse(self).map_err(|_| OidcError::MetadataInvalid)?, []))
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@ use std::{fs, num::NonZeroUsize, path::Path, sync::Arc, time::Duration};
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
authentication::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason},
|
||||
authentication::oauth::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason},
|
||||
crypto::{
|
||||
types::qr_login::{LoginQrCodeDecodeError, QrCodeModeData},
|
||||
CollectStrategy, TrustRequirement,
|
||||
@@ -16,17 +16,17 @@ use matrix_sdk::{
|
||||
VersionBuilderError,
|
||||
},
|
||||
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
|
||||
RumaApiError,
|
||||
RumaApiError, SqliteStoreConfig,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::api::error::{DeserializationError, FromHttpResponseError};
|
||||
use tracing::{debug, error};
|
||||
use url::Url;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use super::{client::Client, RUNTIME};
|
||||
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`].
|
||||
@@ -104,7 +116,7 @@ impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
|
||||
_ => HumanQrLoginError::Unknown,
|
||||
},
|
||||
|
||||
QRCodeLoginError::Oidc(e) => {
|
||||
QRCodeLoginError::OAuth(e) => {
|
||||
if let Some(e) = e.as_request_token_error() {
|
||||
match e {
|
||||
DeviceCodeErrorResponseType::AccessDenied => HumanQrLoginError::Declined,
|
||||
@@ -153,15 +165,15 @@ pub enum QrLoginProgress {
|
||||
/// first digit is a zero.
|
||||
check_code_string: String,
|
||||
},
|
||||
/// We are waiting for the login and for the OIDC provider to give us an
|
||||
/// access token.
|
||||
/// We are waiting for the login and for the OAuth 2.0 authorization server
|
||||
/// to give us an access token.
|
||||
WaitingForToken { user_code: String },
|
||||
/// The login has successfully finished.
|
||||
Done,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait QrLoginProgressListener: Sync + Send {
|
||||
pub trait QrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, state: QrLoginProgress);
|
||||
}
|
||||
|
||||
@@ -255,9 +267,13 @@ impl From<ClientError> for ClientBuildError {
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct ClientBuilder {
|
||||
session_paths: Option<SessionPaths>,
|
||||
session_passphrase: Zeroizing<Option<String>>,
|
||||
session_pool_max_size: Option<usize>,
|
||||
session_cache_size: Option<u32>,
|
||||
session_journal_size_limit: Option<u32>,
|
||||
system_is_memory_constrained: bool,
|
||||
username: Option<String>,
|
||||
homeserver_cfg: Option<HomeserverConfig>,
|
||||
passphrase: Zeroizing<Option<String>>,
|
||||
user_agent: Option<String>,
|
||||
sliding_sync_version_builder: SlidingSyncVersionBuilder,
|
||||
proxy: Option<String>,
|
||||
@@ -271,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]
|
||||
@@ -284,9 +297,13 @@ impl ClientBuilder {
|
||||
pub fn new() -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
session_paths: None,
|
||||
session_passphrase: Zeroizing::new(None),
|
||||
session_pool_max_size: None,
|
||||
session_cache_size: None,
|
||||
session_journal_size_limit: None,
|
||||
system_is_memory_constrained: false,
|
||||
username: None,
|
||||
homeserver_cfg: None,
|
||||
passphrase: Zeroizing::new(None),
|
||||
user_agent: None,
|
||||
sliding_sync_version_builder: SlidingSyncVersionBuilder::None,
|
||||
proxy: None,
|
||||
@@ -305,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,
|
||||
@@ -363,6 +363,74 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set the passphrase for the stores given to
|
||||
/// [`ClientBuilder::session_paths`].
|
||||
pub fn session_passphrase(self: Arc<Self>, passphrase: Option<String>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.session_passphrase = Zeroizing::new(passphrase);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set the pool max size for the SQLite stores given to
|
||||
/// [`ClientBuilder::session_paths`].
|
||||
///
|
||||
/// Each store exposes an async pool of connections. This method controls
|
||||
/// the size of the pool. The larger the pool is, the more memory is
|
||||
/// consumed, but also the more the app is reactive because it doesn't need
|
||||
/// to wait on a pool to be available to run queries.
|
||||
///
|
||||
/// See [`SqliteStoreConfig::pool_max_size`] to learn more.
|
||||
pub fn session_pool_max_size(self: Arc<Self>, pool_max_size: Option<u32>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.session_pool_max_size = pool_max_size
|
||||
.map(|size| size.try_into().expect("`pool_max_size` is too large to fit in `usize`"));
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set the cache size for the SQLite stores given to
|
||||
/// [`ClientBuilder::session_paths`].
|
||||
///
|
||||
/// Each store exposes a SQLite connection. This method controls the cache
|
||||
/// size, in **bytes (!)**.
|
||||
///
|
||||
/// The cache represents data SQLite holds in memory at once per open
|
||||
/// database file. The default cache implementation does not allocate the
|
||||
/// full amount of cache memory all at once. Cache memory is allocated
|
||||
/// in smaller chunks on an as-needed basis.
|
||||
///
|
||||
/// See [`SqliteStoreConfig::cache_size`] to learn more.
|
||||
pub fn session_cache_size(self: Arc<Self>, cache_size: Option<u32>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.session_cache_size = cache_size;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set the size limit for the SQLite WAL files of stores given to
|
||||
/// [`ClientBuilder::session_paths`].
|
||||
///
|
||||
/// Each store uses the WAL journal mode. This method controls the size
|
||||
/// limit of the WAL files, in **bytes (!)**.
|
||||
///
|
||||
/// See [`SqliteStoreConfig::journal_size_limit`] to learn more.
|
||||
pub fn session_journal_size_limit(self: Arc<Self>, limit: Option<u32>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.session_journal_size_limit = limit;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Tell the client that the system is memory constrained, like in a push
|
||||
/// notification process for example.
|
||||
///
|
||||
/// So far, at the time of writing (2025-04-07), it changes the defaults of
|
||||
/// [`SqliteStoreConfig`], so one might not need to call
|
||||
/// [`ClientBuilder::session_cache_size`] and siblings for example. Please
|
||||
/// check [`SqliteStoreConfig::with_low_memory_config`].
|
||||
pub fn system_is_memory_constrained(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.system_is_memory_constrained = true;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn username(self: Arc<Self>, username: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.username = Some(username);
|
||||
@@ -387,12 +455,6 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn passphrase(self: Arc<Self>, passphrase: Option<String>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.passphrase = Zeroizing::new(passphrase);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn user_agent(self: Arc<Self>, user_agent: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.user_agent = Some(user_agent);
|
||||
@@ -492,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);
|
||||
@@ -508,27 +583,50 @@ 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)?;
|
||||
fs::create_dir_all(cache_path)?;
|
||||
|
||||
inner_builder = inner_builder.sqlite_store_with_cache_path(
|
||||
data_path,
|
||||
cache_path,
|
||||
builder.passphrase.as_deref(),
|
||||
);
|
||||
let mut sqlite_store_config = if builder.system_is_memory_constrained {
|
||||
SqliteStoreConfig::with_low_memory_config(data_path)
|
||||
} else {
|
||||
SqliteStoreConfig::new(data_path)
|
||||
};
|
||||
|
||||
sqlite_store_config =
|
||||
sqlite_store_config.passphrase(builder.session_passphrase.as_deref());
|
||||
|
||||
if let Some(size) = builder.session_pool_max_size {
|
||||
sqlite_store_config = sqlite_store_config.pool_max_size(size);
|
||||
}
|
||||
|
||||
if let Some(size) = builder.session_cache_size {
|
||||
sqlite_store_config = sqlite_store_config.cache_size(size);
|
||||
}
|
||||
|
||||
if let Some(limit) = builder.session_journal_size_limit {
|
||||
sqlite_store_config = sqlite_store_config.journal_size_limit(limit);
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -597,29 +695,18 @@ 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 => {
|
||||
inner_builder = inner_builder
|
||||
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::None)
|
||||
}
|
||||
SlidingSyncVersionBuilder::Proxy { url } => {
|
||||
inner_builder = inner_builder.sliding_sync_version_builder(
|
||||
MatrixSlidingSyncVersionBuilder::Proxy {
|
||||
url: Url::parse(&url)
|
||||
.map_err(|e| ClientBuildError::Generic { message: e.to_string() })?,
|
||||
},
|
||||
)
|
||||
}
|
||||
SlidingSyncVersionBuilder::Native => {
|
||||
inner_builder = inner_builder
|
||||
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::Native)
|
||||
}
|
||||
SlidingSyncVersionBuilder::DiscoverProxy => {
|
||||
inner_builder = inner_builder
|
||||
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::DiscoverProxy)
|
||||
}
|
||||
SlidingSyncVersionBuilder::DiscoverNative => {
|
||||
inner_builder = inner_builder
|
||||
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::DiscoverNative)
|
||||
@@ -629,7 +716,8 @@ impl ClientBuilder {
|
||||
if let Some(config) = builder.request_config {
|
||||
let mut updated_config = matrix_sdk::config::RequestConfig::default();
|
||||
if let Some(retry_limit) = config.retry_limit {
|
||||
updated_config = updated_config.retry_limit(retry_limit);
|
||||
updated_config =
|
||||
updated_config.retry_limit(retry_limit.try_into().unwrap_or(usize::MAX));
|
||||
}
|
||||
if let Some(timeout) = config.timeout {
|
||||
updated_config = updated_config.timeout(Duration::from_millis(timeout));
|
||||
@@ -641,30 +729,23 @@ impl ClientBuilder {
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(retry_timeout) = config.retry_timeout {
|
||||
updated_config = updated_config.retry_timeout(Duration::from_millis(retry_timeout));
|
||||
if let Some(max_retry_time) = config.max_retry_time {
|
||||
updated_config =
|
||||
updated_config.max_retry_time(Duration::from_millis(max_retry_time));
|
||||
}
|
||||
inner_builder = inner_builder.request_config(updated_config);
|
||||
}
|
||||
|
||||
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?,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -673,8 +754,8 @@ impl ClientBuilder {
|
||||
///
|
||||
/// This method will build the client and immediately attempt to log the
|
||||
/// client in using the provided [`QrCodeData`] using the login
|
||||
/// mechanism described in [MSC4108]. As such this methods requires OIDC
|
||||
/// support as well as sliding sync support.
|
||||
/// mechanism described in [MSC4108]. As such this methods requires OAuth
|
||||
/// 2.0 support as well as sliding sync support.
|
||||
///
|
||||
/// The usage of the progress_listener is required to transfer the
|
||||
/// [`CheckCode`] to the existing client.
|
||||
@@ -700,17 +781,18 @@ impl ClientBuilder {
|
||||
}
|
||||
})?;
|
||||
|
||||
let client_metadata =
|
||||
oidc_configuration.try_into().map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
|
||||
let registration_data = oidc_configuration
|
||||
.registration_data()
|
||||
.map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
|
||||
|
||||
let oidc = client.inner.oidc();
|
||||
let login = oidc.login_with_qr_code(&qr_code_data.inner, client_metadata);
|
||||
let oauth = client.inner.oauth();
|
||||
let login = oauth.login_with_qr_code(&qr_code_data.inner, Some(®istration_data));
|
||||
|
||||
let mut progress = login.subscribe_to_progress();
|
||||
|
||||
// We create this task, which will get cancelled once it's dropped, just in case
|
||||
// the progress stream doesn't end.
|
||||
let _progress_task = TaskHandle::new(RUNTIME.spawn(async move {
|
||||
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(state) = progress.next().await {
|
||||
progress_listener.on_update(state.into());
|
||||
}
|
||||
@@ -722,8 +804,8 @@ impl ClientBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
/// The store paths the client will use when built.
|
||||
#[derive(Clone)]
|
||||
struct SessionPaths {
|
||||
/// The path that the client will use to store its data.
|
||||
data_path: String,
|
||||
@@ -742,14 +824,12 @@ pub struct RequestConfig {
|
||||
/// Max number of concurrent requests. No value means no limits.
|
||||
max_concurrent_requests: Option<u64>,
|
||||
/// Base delay between retries.
|
||||
retry_timeout: Option<u64>,
|
||||
max_retry_time: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum SlidingSyncVersionBuilder {
|
||||
None,
|
||||
Proxy { url: String },
|
||||
Native,
|
||||
DiscoverProxy,
|
||||
DiscoverNative,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -5,12 +5,15 @@ use matrix_sdk::{
|
||||
encryption,
|
||||
encryption::{backups, recovery},
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use thiserror::Error;
|
||||
use tracing::{error, info};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::{client::Client, error::ClientError, ruma::AuthData, task_handle::TaskHandle};
|
||||
use crate::{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -230,7 +233,7 @@ impl Encryption {
|
||||
pub fn backup_state_listener(&self, listener: Box<dyn BackupStateListener>) -> Arc<TaskHandle> {
|
||||
let mut stream = self.inner.backups().state_stream();
|
||||
|
||||
let stream_task = TaskHandle::new(RUNTIME.spawn(async move {
|
||||
let stream_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(state) = stream.next().await {
|
||||
let Ok(state) = state else { continue };
|
||||
listener.on_update(state.into());
|
||||
@@ -267,7 +270,7 @@ impl Encryption {
|
||||
) -> Arc<TaskHandle> {
|
||||
let mut stream = self.inner.recovery().state_stream();
|
||||
|
||||
let stream_task = TaskHandle::new(RUNTIME.spawn(async move {
|
||||
let stream_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(state) = stream.next().await {
|
||||
listener.on_update(state.into());
|
||||
}
|
||||
@@ -294,7 +297,7 @@ impl Encryption {
|
||||
let task = if let Some(listener) = progress_listener {
|
||||
let mut progress_stream = wait_for_steady_state.subscribe_to_progress();
|
||||
|
||||
Some(RUNTIME.spawn(async move {
|
||||
Some(get_runtime_handle().spawn(async move {
|
||||
while let Some(progress) = progress_stream.next().await {
|
||||
let Ok(progress) = progress else { continue };
|
||||
listener.on_update(progress.into());
|
||||
@@ -335,7 +338,7 @@ impl Encryption {
|
||||
|
||||
let mut progress_stream = enable.subscribe_to_progress();
|
||||
|
||||
let task = RUNTIME.spawn(async move {
|
||||
let task = get_runtime_handle().spawn(async move {
|
||||
while let Some(progress) = progress_stream.next().await {
|
||||
let Ok(progress) = progress else { continue };
|
||||
progress_listener.on_update(progress.into());
|
||||
@@ -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 })));
|
||||
}
|
||||
@@ -400,7 +399,7 @@ impl Encryption {
|
||||
) -> Arc<TaskHandle> {
|
||||
let mut subscriber = self.inner.verification_state();
|
||||
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(verification_state) = subscriber.next().await {
|
||||
listener.on_update(verification_state.into());
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -478,15 +477,6 @@ impl UserIdentity {
|
||||
Ok(self.inner.pin().await?)
|
||||
}
|
||||
|
||||
/// Remove the requirement for this identity to be verified.
|
||||
///
|
||||
/// If an identity was previously verified and is not anymore it will be
|
||||
/// reported to the user. In order to remove this notice users have to
|
||||
/// verify again or to withdraw the verification requirement.
|
||||
pub(crate) async fn withdraw_verification(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.withdraw_verification().await?)
|
||||
}
|
||||
|
||||
/// Get the public part of the Master key of this user identity.
|
||||
///
|
||||
/// The public part of the Master key is usually used to uniquely identify
|
||||
@@ -504,6 +494,28 @@ impl UserIdentity {
|
||||
pub fn is_verified(&self) -> bool {
|
||||
self.inner.is_verified()
|
||||
}
|
||||
|
||||
/// True if we verified this identity at some point in the past.
|
||||
///
|
||||
/// To reset this latch back to `false`, one must call
|
||||
/// [`UserIdentity::withdraw_verification()`].
|
||||
pub fn was_previously_verified(&self) -> bool {
|
||||
self.inner.was_previously_verified()
|
||||
}
|
||||
|
||||
/// Remove the requirement for this identity to be verified.
|
||||
///
|
||||
/// If an identity was previously verified and is not anymore it will be
|
||||
/// reported to the user. In order to remove this notice users have to
|
||||
/// verify again or to withdraw the verification requirement.
|
||||
pub(crate) async fn withdraw_verification(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.withdraw_verification().await?)
|
||||
}
|
||||
|
||||
/// Was this identity previously verified, and is no longer?
|
||||
pub fn has_verification_violation(&self) -> bool {
|
||||
self.inner.has_verification_violation()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
@@ -528,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,7 +566,7 @@ impl From<&matrix_sdk::encryption::CrossSigningResetAuthType> for CrossSigningRe
|
||||
fn from(value: &matrix_sdk::encryption::CrossSigningResetAuthType) -> Self {
|
||||
match value {
|
||||
encryption::CrossSigningResetAuthType::Uiaa(_) => Self::Uiaa,
|
||||
encryption::CrossSigningResetAuthType::Oidc(info) => Self::Oidc { info: info.into() },
|
||||
encryption::CrossSigningResetAuthType::OAuth(info) => Self::Oidc { info: info.into() },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -568,8 +577,8 @@ pub struct OidcCrossSigningResetInfo {
|
||||
pub approval_url: String,
|
||||
}
|
||||
|
||||
impl From<&matrix_sdk::encryption::OidcCrossSigningResetInfo> for OidcCrossSigningResetInfo {
|
||||
fn from(value: &matrix_sdk::encryption::OidcCrossSigningResetInfo) -> Self {
|
||||
impl From<&matrix_sdk::encryption::OAuthCrossSigningResetInfo> for OidcCrossSigningResetInfo {
|
||||
fn from(value: &matrix_sdk::encryption::OAuthCrossSigningResetInfo) -> Self {
|
||||
Self { approval_url: value.approval_url.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use std::{collections::HashMap, fmt, fmt::Display, time::SystemTime};
|
||||
use std::{collections::HashMap, error::Error, fmt, fmt::Display, time::SystemTime};
|
||||
|
||||
use matrix_sdk::{
|
||||
authentication::oidc::OidcError, encryption::CryptoStoreError, event_cache::EventCacheError,
|
||||
authentication::oauth::OAuthError, encryption::CryptoStoreError, event_cache::EventCacheError,
|
||||
reqwest, room::edit::EditError, send_queue::RoomSendQueueError, HttpError, IdParseError,
|
||||
NotificationSettingsError as SdkNotificationSettingsError,
|
||||
QueueWedgeError as SdkQueueWedgeError, StoreError,
|
||||
};
|
||||
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<OidcError> for ClientError {
|
||||
fn from(e: OidcError) -> Self {
|
||||
Self::new(e)
|
||||
impl From<OAuthError> for ClientError {
|
||||
fn from(e: OAuthError) -> Self {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,6 +301,8 @@ pub enum RoomError {
|
||||
TimelineUnavailable,
|
||||
#[error("Invalid thumbnail data")]
|
||||
InvalidThumbnailData,
|
||||
#[error("Invalid replied to event ID")]
|
||||
InvalidRepliedToEventId,
|
||||
#[error("Failed sending attachment")]
|
||||
FailedSendingAttachment,
|
||||
}
|
||||
|
||||
@@ -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,16 +26,16 @@ 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 timeline_event_filter;
|
||||
mod tracing;
|
||||
mod utd;
|
||||
mod utils;
|
||||
mod widget;
|
||||
|
||||
use async_compat::TOKIO1 as RUNTIME;
|
||||
use matrix_sdk::ruma::events::room::message::RoomMessageEventContentWithoutRelation;
|
||||
|
||||
use self::{
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
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, error::ClientError, event::TimelineEvent};
|
||||
use crate::{
|
||||
client::{Client, JoinRule},
|
||||
error::ClientError,
|
||||
event::TimelineEvent,
|
||||
room::Room,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum NotificationEvent {
|
||||
@@ -25,9 +31,11 @@ pub struct NotificationRoomInfo {
|
||||
pub display_name: String,
|
||||
pub avatar_url: Option<String>,
|
||||
pub canonical_alias: Option<String>,
|
||||
pub join_rule: Option<JoinRule>,
|
||||
pub joined_members_count: u64,
|
||||
pub is_encrypted: Option<bool>,
|
||||
pub is_direct: bool,
|
||||
pub is_public: bool,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
@@ -42,6 +50,7 @@ pub struct NotificationItem {
|
||||
/// information to create a push context.
|
||||
pub is_noisy: Option<bool>,
|
||||
pub has_mention: Option<bool>,
|
||||
pub thread_id: Option<String>,
|
||||
}
|
||||
|
||||
impl NotificationItem {
|
||||
@@ -54,7 +63,6 @@ impl NotificationItem {
|
||||
NotificationEvent::Invite { sender: event.sender.to_string() }
|
||||
}
|
||||
};
|
||||
|
||||
Self {
|
||||
event,
|
||||
sender_info: NotificationSenderInfo {
|
||||
@@ -66,12 +74,15 @@ impl NotificationItem {
|
||||
display_name: item.room_computed_display_name,
|
||||
avatar_url: item.room_avatar_url,
|
||||
canonical_alias: item.room_canonical_alias,
|
||||
join_rule: item.room_join_rule.try_into().ok(),
|
||||
joined_members_count: item.joined_members_count,
|
||||
is_encrypted: item.is_room_encrypted,
|
||||
is_direct: item.is_direct_message_room,
|
||||
is_public: item.is_room_public,
|
||||
},
|
||||
is_noisy: item.is_noisy,
|
||||
has_mention: item.has_mention,
|
||||
thread_id: item.thread_id.map(|t| t.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,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(
|
||||
@@ -109,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,14 +9,352 @@ use matrix_sdk::{
|
||||
ruma::events::push_rules::PushRulesEvent,
|
||||
Client as MatrixClient,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::{
|
||||
push::{PredefinedOverrideRuleId, PredefinedUnderrideRuleId, RuleKind},
|
||||
RoomId,
|
||||
push::{
|
||||
Action as SdkAction, ComparisonOperator as SdkComparisonOperator, PredefinedOverrideRuleId,
|
||||
PredefinedUnderrideRuleId, PushCondition as SdkPushCondition, RoomMemberCountIs,
|
||||
RuleKind as SdkRuleKind, ScalarJsonValue as SdkJsonValue, Tweak as SdkTweak,
|
||||
},
|
||||
Int, RoomId, UInt,
|
||||
};
|
||||
use tokio::sync::RwLock as AsyncRwLock;
|
||||
|
||||
use crate::error::NotificationSettingsError;
|
||||
|
||||
#[derive(Clone, Default, uniffi::Enum)]
|
||||
pub enum ComparisonOperator {
|
||||
/// Equals
|
||||
#[default]
|
||||
Eq,
|
||||
|
||||
/// Less than
|
||||
Lt,
|
||||
|
||||
/// Greater than
|
||||
Gt,
|
||||
|
||||
/// Greater or equal
|
||||
Ge,
|
||||
|
||||
/// Less or equal
|
||||
Le,
|
||||
}
|
||||
|
||||
impl From<SdkComparisonOperator> for ComparisonOperator {
|
||||
fn from(value: SdkComparisonOperator) -> Self {
|
||||
match value {
|
||||
SdkComparisonOperator::Eq => Self::Eq,
|
||||
SdkComparisonOperator::Lt => Self::Lt,
|
||||
SdkComparisonOperator::Gt => Self::Gt,
|
||||
SdkComparisonOperator::Ge => Self::Ge,
|
||||
SdkComparisonOperator::Le => Self::Le,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ComparisonOperator> for SdkComparisonOperator {
|
||||
fn from(value: ComparisonOperator) -> Self {
|
||||
match value {
|
||||
ComparisonOperator::Eq => Self::Eq,
|
||||
ComparisonOperator::Lt => Self::Lt,
|
||||
ComparisonOperator::Gt => Self::Gt,
|
||||
ComparisonOperator::Ge => Self::Ge,
|
||||
ComparisonOperator::Le => Self::Le,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, uniffi::Enum)]
|
||||
pub enum JsonValue {
|
||||
/// Represents a `null` value.
|
||||
#[default]
|
||||
Null,
|
||||
|
||||
/// Represents a boolean.
|
||||
Bool { value: bool },
|
||||
|
||||
/// Represents an integer.
|
||||
Integer { value: i64 },
|
||||
|
||||
/// Represents a string.
|
||||
String { value: String },
|
||||
}
|
||||
|
||||
impl From<SdkJsonValue> for JsonValue {
|
||||
fn from(value: SdkJsonValue) -> Self {
|
||||
match value {
|
||||
SdkJsonValue::Null => Self::Null,
|
||||
SdkJsonValue::Bool(b) => Self::Bool { value: b },
|
||||
SdkJsonValue::Integer(i) => Self::Integer { value: i.into() },
|
||||
SdkJsonValue::String(s) => Self::String { value: s },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JsonValue> for SdkJsonValue {
|
||||
fn from(value: JsonValue) -> Self {
|
||||
match value {
|
||||
JsonValue::Null => Self::Null,
|
||||
JsonValue::Bool { value } => Self::Bool(value),
|
||||
JsonValue::Integer { value } => Self::Integer(Int::new(value).unwrap_or_default()),
|
||||
JsonValue::String { value } => Self::String(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum PushCondition {
|
||||
/// A glob pattern match on a field of the event.
|
||||
EventMatch {
|
||||
/// The [dot-separated path] of the property of the event to match.
|
||||
///
|
||||
/// [dot-separated path]: https://spec.matrix.org/latest/appendices/#dot-separated-property-paths
|
||||
key: String,
|
||||
|
||||
/// The glob-style pattern to match against.
|
||||
///
|
||||
/// Patterns with no special glob characters should be treated as having
|
||||
/// asterisks prepended and appended when testing the condition.
|
||||
pattern: String,
|
||||
},
|
||||
|
||||
/// Matches unencrypted messages where `content.body` contains the owner's
|
||||
/// display name in that room.
|
||||
ContainsDisplayName,
|
||||
|
||||
/// Matches the current number of members in the room.
|
||||
RoomMemberCount { prefix: ComparisonOperator, count: u64 },
|
||||
|
||||
/// Takes into account the current power levels in the room, ensuring the
|
||||
/// sender of the event has high enough power to trigger the
|
||||
/// notification.
|
||||
SenderNotificationPermission {
|
||||
/// The field in the power level event the user needs a minimum power
|
||||
/// level for.
|
||||
///
|
||||
/// Fields must be specified under the `notifications` property in the
|
||||
/// power level event's `content`.
|
||||
key: String,
|
||||
},
|
||||
|
||||
/// Exact value match on a property of the event.
|
||||
EventPropertyIs {
|
||||
/// The [dot-separated path] of the property of the event to match.
|
||||
///
|
||||
/// [dot-separated path]: https://spec.matrix.org/latest/appendices/#dot-separated-property-paths
|
||||
key: String,
|
||||
|
||||
/// The value to match against.
|
||||
value: JsonValue,
|
||||
},
|
||||
|
||||
/// Exact value match on a value in an array property of the event.
|
||||
EventPropertyContains {
|
||||
/// The [dot-separated path] of the property of the event to match.
|
||||
///
|
||||
/// [dot-separated path]: https://spec.matrix.org/latest/appendices/#dot-separated-property-paths
|
||||
key: String,
|
||||
|
||||
/// The value to match against.
|
||||
value: JsonValue,
|
||||
},
|
||||
}
|
||||
|
||||
impl TryFrom<SdkPushCondition> for PushCondition {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: SdkPushCondition) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
SdkPushCondition::EventMatch { key, pattern } => Self::EventMatch { key, pattern },
|
||||
SdkPushCondition::ContainsDisplayName => Self::ContainsDisplayName,
|
||||
SdkPushCondition::RoomMemberCount { is } => {
|
||||
Self::RoomMemberCount { prefix: is.prefix.into(), count: is.count.into() }
|
||||
}
|
||||
SdkPushCondition::SenderNotificationPermission { key } => {
|
||||
Self::SenderNotificationPermission { key }
|
||||
}
|
||||
SdkPushCondition::EventPropertyIs { key, value } => {
|
||||
Self::EventPropertyIs { key, value: value.into() }
|
||||
}
|
||||
SdkPushCondition::EventPropertyContains { key, value } => {
|
||||
Self::EventPropertyContains { key, value: value.into() }
|
||||
}
|
||||
_ => return Err("Unsupported condition type".to_owned()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PushCondition> for SdkPushCondition {
|
||||
fn from(value: PushCondition) -> Self {
|
||||
match value {
|
||||
PushCondition::EventMatch { key, pattern } => Self::EventMatch { key, pattern },
|
||||
PushCondition::ContainsDisplayName => Self::ContainsDisplayName,
|
||||
PushCondition::RoomMemberCount { prefix, count } => Self::RoomMemberCount {
|
||||
is: RoomMemberCountIs {
|
||||
prefix: prefix.into(),
|
||||
count: UInt::new(count).unwrap_or_default(),
|
||||
},
|
||||
},
|
||||
PushCondition::SenderNotificationPermission { key } => {
|
||||
Self::SenderNotificationPermission { key }
|
||||
}
|
||||
PushCondition::EventPropertyIs { key, value } => {
|
||||
Self::EventPropertyIs { key, value: value.into() }
|
||||
}
|
||||
PushCondition::EventPropertyContains { key, value } => {
|
||||
Self::EventPropertyContains { key, value: value.into() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum RuleKind {
|
||||
/// User-configured rules that override all other kinds.
|
||||
Override,
|
||||
|
||||
/// Lowest priority user-defined rules.
|
||||
Underride,
|
||||
|
||||
/// Sender-specific rules.
|
||||
Sender,
|
||||
|
||||
/// Room-specific rules.
|
||||
Room,
|
||||
|
||||
/// Content-specific rules.
|
||||
Content,
|
||||
|
||||
Custom {
|
||||
value: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<SdkRuleKind> for RuleKind {
|
||||
fn from(value: SdkRuleKind) -> Self {
|
||||
match value {
|
||||
SdkRuleKind::Override => Self::Override,
|
||||
SdkRuleKind::Underride => Self::Underride,
|
||||
SdkRuleKind::Sender => Self::Sender,
|
||||
SdkRuleKind::Room => Self::Room,
|
||||
SdkRuleKind::Content => Self::Content,
|
||||
SdkRuleKind::_Custom(_) => Self::Custom { value: value.as_str().to_owned() },
|
||||
_ => Self::Custom { value: value.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RuleKind> for SdkRuleKind {
|
||||
fn from(value: RuleKind) -> Self {
|
||||
match value {
|
||||
RuleKind::Override => Self::Override,
|
||||
RuleKind::Underride => Self::Underride,
|
||||
RuleKind::Sender => Self::Sender,
|
||||
RuleKind::Room => Self::Room,
|
||||
RuleKind::Content => Self::Content,
|
||||
RuleKind::Custom { value } => SdkRuleKind::from(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
/// Enum representing the push notification tweaks for a rule.
|
||||
pub enum Tweak {
|
||||
/// A string representing the sound to be played when this notification
|
||||
/// arrives.
|
||||
///
|
||||
/// A value of "default" means to play a default sound. A device may choose
|
||||
/// to alert the user by some other means if appropriate, eg. vibration.
|
||||
Sound { value: String },
|
||||
|
||||
/// A boolean representing whether or not this message should be highlighted
|
||||
/// in the UI.
|
||||
Highlight { value: bool },
|
||||
|
||||
/// A custom tweak
|
||||
Custom {
|
||||
/// The name of the custom tweak (`set_tweak` field)
|
||||
name: String,
|
||||
|
||||
/// The value of the custom tweak as an encoded JSON string
|
||||
value: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl TryFrom<SdkTweak> for Tweak {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: SdkTweak) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
SdkTweak::Sound(sound) => Self::Sound { value: sound },
|
||||
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}"))?;
|
||||
|
||||
Self::Custom { name, value: json_string }
|
||||
}
|
||||
_ => return Err("Unsupported tweak type".to_owned()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Tweak> for SdkTweak {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: Tweak) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
Tweak::Sound { value } => Self::Sound(value),
|
||||
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}"))?;
|
||||
let value = serde_json::from_value(json_value)
|
||||
.map_err(|e| format!("Failed to convert JSON value: {e}"))?;
|
||||
|
||||
Self::Custom { name, value }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
/// Enum representing the push notification actions for a rule.
|
||||
pub enum Action {
|
||||
/// Causes matching events to generate a notification.
|
||||
Notify,
|
||||
/// Sets an entry in the 'tweaks' dictionary sent to the push gateway.
|
||||
SetTweak { value: Tweak },
|
||||
}
|
||||
|
||||
impl TryFrom<SdkAction> for Action {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: SdkAction) -> Result<Self, Self::Error> {
|
||||
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}"))?,
|
||||
},
|
||||
_ => return Err("Unsupported action type".to_owned()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Action> for SdkAction {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: Action) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
Action::Notify => Self::Notify,
|
||||
Action::SetTweak { value } => Self::SetTweak(
|
||||
value.try_into().map_err(|e| format!("Failed to convert tweak: {e}"))?,
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum representing the push notification modes for a room.
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum RoomNotificationMode {
|
||||
@@ -50,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);
|
||||
}
|
||||
|
||||
@@ -267,7 +605,7 @@ impl NotificationSettings {
|
||||
pub async fn is_room_mention_enabled(&self) -> Result<bool, NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
let enabled = notification_settings
|
||||
.is_push_rule_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention)
|
||||
.is_push_rule_enabled(SdkRuleKind::Override, PredefinedOverrideRuleId::IsRoomMention)
|
||||
.await?;
|
||||
Ok(enabled)
|
||||
}
|
||||
@@ -280,7 +618,7 @@ impl NotificationSettings {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
notification_settings
|
||||
.set_push_rule_enabled(
|
||||
RuleKind::Override,
|
||||
SdkRuleKind::Override,
|
||||
PredefinedOverrideRuleId::IsRoomMention,
|
||||
enabled,
|
||||
)
|
||||
@@ -292,7 +630,7 @@ impl NotificationSettings {
|
||||
pub async fn is_user_mention_enabled(&self) -> Result<bool, NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
let enabled = notification_settings
|
||||
.is_push_rule_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention)
|
||||
.is_push_rule_enabled(SdkRuleKind::Override, PredefinedOverrideRuleId::IsUserMention)
|
||||
.await?;
|
||||
Ok(enabled)
|
||||
}
|
||||
@@ -304,14 +642,14 @@ impl NotificationSettings {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
// Check stable identifier
|
||||
if let Ok(enabled) = notification_settings
|
||||
.is_push_rule_enabled(RuleKind::Override, ".m.rule.encrypted_event")
|
||||
.is_push_rule_enabled(SdkRuleKind::Override, ".m.rule.encrypted_event")
|
||||
.await
|
||||
{
|
||||
enabled
|
||||
} else {
|
||||
// Check unstable identifier
|
||||
notification_settings
|
||||
.is_push_rule_enabled(RuleKind::Override, ".org.matrix.msc4028.encrypted_event")
|
||||
.is_push_rule_enabled(SdkRuleKind::Override, ".org.matrix.msc4028.encrypted_event")
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
}
|
||||
@@ -332,7 +670,7 @@ impl NotificationSettings {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
notification_settings
|
||||
.set_push_rule_enabled(
|
||||
RuleKind::Override,
|
||||
SdkRuleKind::Override,
|
||||
PredefinedOverrideRuleId::IsUserMention,
|
||||
enabled,
|
||||
)
|
||||
@@ -344,7 +682,7 @@ impl NotificationSettings {
|
||||
pub async fn is_call_enabled(&self) -> Result<bool, NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
let enabled = notification_settings
|
||||
.is_push_rule_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::Call)
|
||||
.is_push_rule_enabled(SdkRuleKind::Underride, PredefinedUnderrideRuleId::Call)
|
||||
.await?;
|
||||
Ok(enabled)
|
||||
}
|
||||
@@ -353,7 +691,7 @@ impl NotificationSettings {
|
||||
pub async fn set_call_enabled(&self, enabled: bool) -> Result<(), NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
notification_settings
|
||||
.set_push_rule_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::Call, enabled)
|
||||
.set_push_rule_enabled(SdkRuleKind::Underride, PredefinedUnderrideRuleId::Call, enabled)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -363,7 +701,7 @@ impl NotificationSettings {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
let enabled = notification_settings
|
||||
.is_push_rule_enabled(
|
||||
RuleKind::Override,
|
||||
SdkRuleKind::Override,
|
||||
PredefinedOverrideRuleId::InviteForMe.as_str(),
|
||||
)
|
||||
.await?;
|
||||
@@ -378,7 +716,7 @@ impl NotificationSettings {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
notification_settings
|
||||
.set_push_rule_enabled(
|
||||
RuleKind::Override,
|
||||
SdkRuleKind::Override,
|
||||
PredefinedOverrideRuleId::InviteForMe.as_str(),
|
||||
enabled,
|
||||
)
|
||||
@@ -386,6 +724,30 @@ impl NotificationSettings {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets a custom push rule with the given actions and conditions.
|
||||
pub async fn set_custom_push_rule(
|
||||
&self,
|
||||
rule_id: String,
|
||||
rule_kind: RuleKind,
|
||||
actions: Vec<Action>,
|
||||
conditions: Vec<PushCondition>,
|
||||
) -> Result<(), NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
let actions: Result<Vec<_>, _> =
|
||||
actions.into_iter().map(|action| action.try_into()).collect();
|
||||
let actions = actions.map_err(|e| NotificationSettingsError::Generic { msg: e })?;
|
||||
|
||||
notification_settings
|
||||
.create_custom_conditional_push_rule(
|
||||
rule_id,
|
||||
rule_kind.into(),
|
||||
actions,
|
||||
conditions.into_iter().map(|condition| condition.into()).collect(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unmute a room.
|
||||
///
|
||||
/// # Arguments
|
||||
|
||||
@@ -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
|
||||
@@ -233,20 +230,34 @@ pub struct TracingFileConfiguration {
|
||||
|
||||
#[derive(PartialEq, PartialOrd)]
|
||||
enum LogTarget {
|
||||
// External crates.
|
||||
Hyper,
|
||||
|
||||
// FFI modules.
|
||||
MatrixSdkFfi,
|
||||
|
||||
// SDK base modules.
|
||||
MatrixSdkBaseEventCache,
|
||||
MatrixSdkBaseSlidingSync,
|
||||
MatrixSdkBaseStoreAmbiguityMap,
|
||||
|
||||
// SDK common modules.
|
||||
MatrixSdkCommonStoreLocks,
|
||||
|
||||
// SDK modules.
|
||||
MatrixSdk,
|
||||
MatrixSdkClient,
|
||||
MatrixSdkCrypto,
|
||||
MatrixSdkCryptoAccount,
|
||||
MatrixSdkOidc,
|
||||
MatrixSdkHttpClient,
|
||||
MatrixSdkSlidingSync,
|
||||
MatrixSdkBaseSlidingSync,
|
||||
MatrixSdkUiTimeline,
|
||||
MatrixSdkEventCache,
|
||||
MatrixSdkBaseEventCache,
|
||||
MatrixSdkEventCacheStore,
|
||||
MatrixSdkHttpClient,
|
||||
MatrixSdkOidc,
|
||||
MatrixSdkSendQueue,
|
||||
MatrixSdkSlidingSync,
|
||||
|
||||
// SDK UI modules.
|
||||
MatrixSdkUiTimeline,
|
||||
}
|
||||
|
||||
impl LogTarget {
|
||||
@@ -254,6 +265,10 @@ impl LogTarget {
|
||||
match self {
|
||||
LogTarget::Hyper => "hyper",
|
||||
LogTarget::MatrixSdkFfi => "matrix_sdk_ffi",
|
||||
LogTarget::MatrixSdkBaseEventCache => "matrix_sdk_base::event_cache",
|
||||
LogTarget::MatrixSdkBaseSlidingSync => "matrix_sdk_base::sliding_sync",
|
||||
LogTarget::MatrixSdkBaseStoreAmbiguityMap => "matrix_sdk_base::store::ambiguity_map",
|
||||
LogTarget::MatrixSdkCommonStoreLocks => "matrix_sdk_common::store_locks",
|
||||
LogTarget::MatrixSdk => "matrix_sdk",
|
||||
LogTarget::MatrixSdkClient => "matrix_sdk::client",
|
||||
LogTarget::MatrixSdkCrypto => "matrix_sdk_crypto",
|
||||
@@ -261,11 +276,10 @@ impl LogTarget {
|
||||
LogTarget::MatrixSdkOidc => "matrix_sdk::oidc",
|
||||
LogTarget::MatrixSdkHttpClient => "matrix_sdk::http_client",
|
||||
LogTarget::MatrixSdkSlidingSync => "matrix_sdk::sliding_sync",
|
||||
LogTarget::MatrixSdkBaseSlidingSync => "matrix_sdk_base::sliding_sync",
|
||||
LogTarget::MatrixSdkUiTimeline => "matrix_sdk_ui::timeline",
|
||||
LogTarget::MatrixSdkEventCache => "matrix_sdk::event_cache",
|
||||
LogTarget::MatrixSdkBaseEventCache => "matrix_sdk_base::event_cache",
|
||||
LogTarget::MatrixSdkSendQueue => "matrix_sdk::send_queue",
|
||||
LogTarget::MatrixSdkEventCacheStore => "matrix_sdk_sqlite::event_cache_store",
|
||||
LogTarget::MatrixSdkUiTimeline => "matrix_sdk_ui::timeline",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -282,31 +296,167 @@ const DEFAULT_TARGET_LOG_LEVELS: &[(LogTarget, LogLevel)] = &[
|
||||
(LogTarget::MatrixSdkSlidingSync, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkBaseSlidingSync, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkUiTimeline, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkSendQueue, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkEventCache, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkBaseEventCache, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkEventCacheStore, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkCommonStoreLocks, LogLevel::Warn),
|
||||
(LogTarget::MatrixSdkBaseStoreAmbiguityMap, LogLevel::Warn),
|
||||
];
|
||||
|
||||
const IMMUTABLE_TARGET_LOG_LEVELS: &[LogTarget] = &[
|
||||
LogTarget::Hyper, // Too verbose
|
||||
LogTarget::MatrixSdk, // Too generic
|
||||
LogTarget::MatrixSdkFfi, // Too verbose
|
||||
const IMMUTABLE_LOG_TARGETS: &[LogTarget] = &[
|
||||
LogTarget::Hyper, // Too verbose
|
||||
LogTarget::MatrixSdk, // Too generic
|
||||
LogTarget::MatrixSdkFfi, // Too verbose
|
||||
LogTarget::MatrixSdkCommonStoreLocks, // Too verbose
|
||||
LogTarget::MatrixSdkBaseStoreAmbiguityMap, // Too verbose
|
||||
];
|
||||
|
||||
/// A log pack can be used to set the trace log level for a group of multiple
|
||||
/// log targets at once, for debugging purposes.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum TraceLogPacks {
|
||||
/// Enables all the logs relevant to the event cache.
|
||||
EventCache,
|
||||
/// Enables all the logs relevant to the send queue.
|
||||
SendQueue,
|
||||
/// Enables all the logs relevant to the timeline.
|
||||
Timeline,
|
||||
}
|
||||
|
||||
impl TraceLogPacks {
|
||||
// Note: all the log targets returned here must be part of
|
||||
// `DEFAULT_TARGET_LOG_LEVELS`.
|
||||
fn targets(&self) -> &[LogTarget] {
|
||||
match self {
|
||||
TraceLogPacks::EventCache => &[
|
||||
LogTarget::MatrixSdkEventCache,
|
||||
LogTarget::MatrixSdkBaseEventCache,
|
||||
LogTarget::MatrixSdkEventCacheStore,
|
||||
],
|
||||
TraceLogPacks::SendQueue => &[LogTarget::MatrixSdkSendQueue],
|
||||
TraceLogPacks::Timeline => &[LogTarget::MatrixSdkUiTimeline],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
/// The desired log level.
|
||||
log_level: LogLevel,
|
||||
|
||||
/// Additional targets that the FFI client would like to use e.g.
|
||||
/// the target names for created [`crate::tracing::Span`]
|
||||
extra_targets: Option<Vec<String>>,
|
||||
/// All the log packs, that will be set to `TRACE` when they're enabled.
|
||||
trace_log_packs: Vec<TraceLogPacks>,
|
||||
|
||||
/// Additional targets that the FFI client would like to use.
|
||||
///
|
||||
/// This can include, for instance, the target names for created
|
||||
/// [`crate::tracing::Span`]. These targets will use the global log level by
|
||||
/// default.
|
||||
extra_targets: Vec<String>,
|
||||
|
||||
/// Whether to log to stdout, or in the logcat on Android.
|
||||
write_to_stdout_or_system: bool,
|
||||
|
||||
/// 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 {
|
||||
@@ -316,49 +466,128 @@ fn build_tracing_filter(config: &TracingConfiguration) -> String {
|
||||
// On 2025-01-08, `log_panics` uses the `panic` target, at the error log level.
|
||||
let mut filters = vec!["panic=error".to_owned()];
|
||||
|
||||
DEFAULT_TARGET_LOG_LEVELS.iter().for_each(|(target, level)| {
|
||||
// Use the default if the log level shouldn't be changed for this target or
|
||||
// if it's already logging more than requested
|
||||
let level = if IMMUTABLE_TARGET_LOG_LEVELS.contains(target) || level > &config.log_level {
|
||||
level.as_str()
|
||||
let global_level = config.log_level;
|
||||
|
||||
DEFAULT_TARGET_LOG_LEVELS.iter().for_each(|(target, default_level)| {
|
||||
let level = if IMMUTABLE_LOG_TARGETS.contains(target) {
|
||||
// If the target is immutable, keep the log level.
|
||||
*default_level
|
||||
} else if config.trace_log_packs.iter().any(|pack| pack.targets().contains(target)) {
|
||||
// If a log pack includes that target, set the associated log level to TRACE.
|
||||
LogLevel::Trace
|
||||
} else if *default_level > global_level {
|
||||
// If the default level is more verbose than the global level, keep the default.
|
||||
*default_level
|
||||
} else {
|
||||
config.log_level.as_str()
|
||||
// Otherwise, use the global level.
|
||||
global_level
|
||||
};
|
||||
|
||||
filters.push(format!("{}={}", target.as_str(), level));
|
||||
filters.push(format!("{}={}", target.as_str(), level.as_str()));
|
||||
});
|
||||
|
||||
// Finally append the extra targets requested by the client
|
||||
if let Some(extra_targets) = &config.extra_targets {
|
||||
for target in extra_targets {
|
||||
filters.push(format!("{}={}", target, config.log_level.as_str()));
|
||||
}
|
||||
// Finally append the extra targets requested by the client.
|
||||
for target in &config.extra_targets {
|
||||
filters.push(format!("{}={}", target, config.log_level.as_str()));
|
||||
}
|
||||
|
||||
filters.join(",")
|
||||
}
|
||||
|
||||
/// Sets up logs and the tokio runtime for the current application.
|
||||
///
|
||||
/// If `use_lightweight_tokio_runtime` is set to true, this will set up a
|
||||
/// lightweight tokio runtime, for processes that have memory limitations (like
|
||||
/// 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 setup_tracing(config: TracingConfiguration) {
|
||||
log_panics();
|
||||
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,
|
||||
})?;
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(EnvFilter::new(build_tracing_filter(&config)))
|
||||
.with(text_layers(config))
|
||||
.init();
|
||||
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() {
|
||||
async_compat::set_runtime_builder(Box::new(|| {
|
||||
eprintln!("spawning a multithreaded tokio runtime");
|
||||
|
||||
let mut builder = tokio::runtime::Builder::new_multi_thread();
|
||||
builder.enable_all();
|
||||
builder
|
||||
}));
|
||||
}
|
||||
|
||||
fn setup_lightweight_tokio_runtime() {
|
||||
async_compat::set_runtime_builder(Box::new(|| {
|
||||
eprintln!("spawning a lightweight tokio runtime");
|
||||
|
||||
// Get the number of available cores through the system, if possible.
|
||||
let num_available_cores =
|
||||
std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
|
||||
|
||||
// The number of worker threads will be either that or 4, whichever is smaller.
|
||||
let num_worker_threads = num_available_cores.min(4);
|
||||
|
||||
// Chosen by a fair dice roll.
|
||||
let num_blocking_threads = 2;
|
||||
|
||||
// 1 MiB of memory per worker thread. Should be enough for everyone™.
|
||||
let max_memory_bytes = 1024 * 1024;
|
||||
|
||||
let mut builder = tokio::runtime::Builder::new_multi_thread();
|
||||
|
||||
builder
|
||||
.enable_all()
|
||||
.worker_threads(num_worker_threads)
|
||||
.thread_stack_size(max_memory_bytes)
|
||||
.max_blocking_threads(num_blocking_threads);
|
||||
|
||||
builder
|
||||
}));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::build_tracing_filter;
|
||||
use crate::platform::TraceLogPacks;
|
||||
|
||||
#[test]
|
||||
fn test_default_tracing_filter() {
|
||||
let config = super::TracingConfiguration {
|
||||
log_level: super::LogLevel::Error,
|
||||
extra_targets: Some(vec!["super_duper_app".to_owned()]),
|
||||
trace_log_packs: Vec::new(),
|
||||
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);
|
||||
@@ -377,9 +606,12 @@ mod tests {
|
||||
matrix_sdk::sliding_sync=info,\
|
||||
matrix_sdk_base::sliding_sync=info,\
|
||||
matrix_sdk_ui::timeline=info,\
|
||||
matrix_sdk::send_queue=info,\
|
||||
matrix_sdk::event_cache=info,\
|
||||
matrix_sdk_base::event_cache=info,\
|
||||
matrix_sdk_sqlite::event_cache_store=info,\
|
||||
matrix_sdk_common::store_locks=warn,\
|
||||
matrix_sdk_base::store::ambiguity_map=warn,\
|
||||
super_duper_app=error"
|
||||
);
|
||||
}
|
||||
@@ -388,9 +620,11 @@ mod tests {
|
||||
fn test_trace_tracing_filter() {
|
||||
let config = super::TracingConfiguration {
|
||||
log_level: super::LogLevel::Trace,
|
||||
extra_targets: Some(vec!["super_duper_app".to_owned(), "some_other_span".to_owned()]),
|
||||
trace_log_packs: Vec::new(),
|
||||
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);
|
||||
@@ -409,11 +643,55 @@ mod tests {
|
||||
matrix_sdk::sliding_sync=trace,\
|
||||
matrix_sdk_base::sliding_sync=trace,\
|
||||
matrix_sdk_ui::timeline=trace,\
|
||||
matrix_sdk::send_queue=trace,\
|
||||
matrix_sdk::event_cache=trace,\
|
||||
matrix_sdk_base::event_cache=trace,\
|
||||
matrix_sdk_sqlite::event_cache_store=trace,\
|
||||
matrix_sdk_common::store_locks=warn,\
|
||||
matrix_sdk_base::store::ambiguity_map=warn,\
|
||||
super_duper_app=trace,\
|
||||
some_other_span=trace"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trace_log_packs() {
|
||||
let config = super::TracingConfiguration {
|
||||
log_level: super::LogLevel::Info,
|
||||
trace_log_packs: vec![TraceLogPacks::EventCache, TraceLogPacks::SendQueue],
|
||||
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);
|
||||
|
||||
assert_eq!(
|
||||
filter,
|
||||
r#"panic=error,
|
||||
hyper=warn,
|
||||
matrix_sdk_ffi=info,
|
||||
matrix_sdk=info,
|
||||
matrix_sdk::client=trace,
|
||||
matrix_sdk_crypto=debug,
|
||||
matrix_sdk_crypto::olm::account=trace,
|
||||
matrix_sdk::oidc=trace,
|
||||
matrix_sdk::http_client=debug,
|
||||
matrix_sdk::sliding_sync=info,
|
||||
matrix_sdk_base::sliding_sync=info,
|
||||
matrix_sdk_ui::timeline=info,
|
||||
matrix_sdk::send_queue=trace,
|
||||
matrix_sdk::event_cache=trace,
|
||||
matrix_sdk_base::event_cache=trace,
|
||||
matrix_sdk_sqlite::event_cache_store=trace,
|
||||
matrix_sdk_common::store_locks=warn,
|
||||
matrix_sdk_base::store::ambiguity_map=warn,
|
||||
super_duper_app=info"#
|
||||
.split('\n')
|
||||
.map(|s| s.trim())
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+291
-115
@@ -6,14 +6,19 @@ use matrix_sdk::{
|
||||
crypto::LocalTrust,
|
||||
room::{
|
||||
edit::EditedContent, power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole,
|
||||
TryFromReportedContentScoreError,
|
||||
},
|
||||
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType,
|
||||
RoomHero as SdkRoomHero, RoomMemberships, RoomState,
|
||||
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType, EncryptionState,
|
||||
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::{
|
||||
api::client::room::report_content,
|
||||
assign,
|
||||
events::{
|
||||
call::notify,
|
||||
@@ -25,12 +30,11 @@ 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 super::RUNTIME;
|
||||
use crate::{
|
||||
chunk_iterator::ChunkIterator,
|
||||
client::{JoinRule, RoomVisibility},
|
||||
@@ -39,13 +43,15 @@ use crate::{
|
||||
identity_status_change::IdentityStatusChange,
|
||||
live_location_share::{LastLocation, LiveLocationShare},
|
||||
room_info::RoomInfo,
|
||||
room_member::RoomMember,
|
||||
room_member::{RoomMember, RoomMemberWithSenderInfo},
|
||||
room_preview::RoomPreview,
|
||||
ruma::{ImageInfo, LocationContent, Mentions, NotifyType},
|
||||
runtime::get_runtime_handle,
|
||||
timeline::{
|
||||
configuration::{AllowedMessageTypes, TimelineConfiguration},
|
||||
ReceiptType, SendHandle, Timeline,
|
||||
configuration::{TimelineConfiguration, TimelineFilter},
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,8 +110,8 @@ impl Room {
|
||||
self.inner.avatar_url().map(|m| m.to_string())
|
||||
}
|
||||
|
||||
pub fn is_direct(&self) -> bool {
|
||||
RUNTIME.block_on(self.inner.is_direct()).unwrap_or(false)
|
||||
pub async fn is_direct(&self) -> bool {
|
||||
self.inner.is_direct().await.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn is_public(&self) -> bool {
|
||||
@@ -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()
|
||||
}
|
||||
@@ -161,21 +195,6 @@ impl Room {
|
||||
self.inner.active_room_call_participants().iter().map(|u| u.to_string()).collect()
|
||||
}
|
||||
|
||||
/// For rooms one is invited to, retrieves the room member information for
|
||||
/// the user who invited the logged-in user to a room.
|
||||
pub async fn inviter(&self) -> Option<RoomMember> {
|
||||
if self.inner.state() == RoomState::Invited {
|
||||
self.inner
|
||||
.invite_details()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|a| a.inviter)
|
||||
.and_then(|m| m.try_into().ok())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Forces the currently active room key, which is used to encrypt messages,
|
||||
/// to be rotated.
|
||||
///
|
||||
@@ -188,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.
|
||||
@@ -204,32 +218,60 @@ 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()?);
|
||||
builder = builder
|
||||
.with_focus(configuration.focus.try_into()?)
|
||||
.with_date_divider_mode(configuration.date_divider_mode.into());
|
||||
|
||||
if let AllowedMessageTypes::Only { types } = configuration.allowed_message_types {
|
||||
builder = builder.event_filter(move |event, room_version_id| {
|
||||
default_event_filter(event, room_version_id)
|
||||
&& match event {
|
||||
AnySyncTimelineEvent::MessageLike(msg) => match msg.original_content() {
|
||||
Some(AnyMessageLikeEventContent::RoomMessage(content)) => {
|
||||
types.contains(&content.msgtype.into())
|
||||
if configuration.track_read_receipts {
|
||||
builder = builder.track_read_marker_and_receipts();
|
||||
}
|
||||
|
||||
match configuration.filter {
|
||||
TimelineFilter::All => {
|
||||
// #nofilter.
|
||||
}
|
||||
|
||||
TimelineFilter::OnlyMessage { types } => {
|
||||
builder = builder.event_filter(move |event, room_version_id| {
|
||||
default_event_filter(event, room_version_id)
|
||||
&& match event {
|
||||
AnySyncTimelineEvent::MessageLike(msg) => {
|
||||
match msg.original_content() {
|
||||
Some(AnyMessageLikeEventContent::RoomMessage(content)) => {
|
||||
types.contains(&content.msgtype.into())
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
TimelineFilter::EventTypeFilter { filter: event_type_filter } => {
|
||||
builder = 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) = configuration.internal_id_prefix {
|
||||
builder = builder.with_internal_id_prefix(internal_id_prefix);
|
||||
}
|
||||
|
||||
builder = builder.with_date_divider_mode(configuration.date_divider_mode.into());
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -237,8 +279,28 @@ impl Room {
|
||||
self.inner.room_id().to_string()
|
||||
}
|
||||
|
||||
pub fn is_encrypted(&self) -> Result<bool, ClientError> {
|
||||
Ok(RUNTIME.block_on(self.inner.is_encrypted())?)
|
||||
pub fn encryption_state(&self) -> EncryptionState {
|
||||
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?)
|
||||
}
|
||||
|
||||
pub async fn members(&self) -> Result<Arc<RoomMembersIterator>, ClientError> {
|
||||
@@ -252,13 +314,13 @@ impl Room {
|
||||
}
|
||||
|
||||
pub async fn member(&self, user_id: String) -> Result<RoomMember, ClientError> {
|
||||
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
|
||||
let user_id = UserId::parse(&*user_id)?;
|
||||
let member = self.inner.get_member(&user_id).await?.context("User not found")?;
|
||||
Ok(member.try_into().context("Unknown state membership")?)
|
||||
}
|
||||
|
||||
pub async fn member_avatar_url(&self, user_id: String) -> Result<Option<String>, ClientError> {
|
||||
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
|
||||
let user_id = UserId::parse(&*user_id)?;
|
||||
let member = self.inner.get_member(&user_id).await?.context("User not found")?;
|
||||
let avatar_url_string = member.avatar_url().map(|m| m.to_string());
|
||||
Ok(avatar_url_string)
|
||||
@@ -268,12 +330,28 @@ impl Room {
|
||||
&self,
|
||||
user_id: String,
|
||||
) -> Result<Option<String>, ClientError> {
|
||||
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
|
||||
let user_id = UserId::parse(&*user_id)?;
|
||||
let member = self.inner.get_member(&user_id).await?.context("User not found")?;
|
||||
let avatar_url_string = member.display_name().map(|m| m.to_owned());
|
||||
Ok(avatar_url_string)
|
||||
}
|
||||
|
||||
/// Get the membership details for the current user.
|
||||
///
|
||||
/// Returns:
|
||||
/// - If the user was present in the room, a
|
||||
/// [`matrix_sdk::room::RoomMemberWithSenderInfo`] containing both the
|
||||
/// user info and the member info of the sender of the `m.room.member`
|
||||
/// event.
|
||||
/// - If the current user is not present, an error.
|
||||
pub async fn member_with_sender_info(
|
||||
&self,
|
||||
user_id: String,
|
||||
) -> Result<RoomMemberWithSenderInfo, ClientError> {
|
||||
let user_id = UserId::parse(&*user_id)?;
|
||||
self.inner.member_with_sender_info(&user_id).await?.try_into()
|
||||
}
|
||||
|
||||
pub async fn room_info(&self) -> Result<RoomInfo, ClientError> {
|
||||
RoomInfo::new(&self.inner).await
|
||||
}
|
||||
@@ -283,7 +361,7 @@ impl Room {
|
||||
listener: Box<dyn RoomInfoListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let mut subscriber = self.inner.subscribe_info();
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while subscriber.next().await.is_some() {
|
||||
match self.room_info().await {
|
||||
Ok(room_info) => listener.call(room_info),
|
||||
@@ -321,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?;
|
||||
|
||||
@@ -375,17 +456,32 @@ impl Room {
|
||||
score: Option<i32>,
|
||||
reason: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
let int_score = score.map(|value| value.into());
|
||||
self.inner
|
||||
.client()
|
||||
.send(report_content::v3::Request::new(
|
||||
self.inner.room_id().into(),
|
||||
event_id,
|
||||
int_score,
|
||||
.report_content(
|
||||
EventId::parse(event_id)?,
|
||||
score.map(TryFrom::try_from).transpose().map_err(
|
||||
|error: TryFromReportedContentScoreError| ClientError::from_err(error),
|
||||
)?,
|
||||
reason,
|
||||
))
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reports a room as inappropriate to the server.
|
||||
/// The caller is not required to be joined to the room to report it.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `reason` - The reason the room is being reported.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the room is not found or on rate limit
|
||||
pub async fn report_room(&self, reason: Option<String>) -> Result<(), ClientError> {
|
||||
self.inner.report_room(reason).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -569,7 +665,7 @@ impl Room {
|
||||
self: Arc<Self>,
|
||||
listener: Box<dyn TypingNotificationsListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
let (_event_handler_drop_guard, mut subscriber) =
|
||||
self.inner.subscribe_to_typing_notifications();
|
||||
while let Ok(typing_user_ids) = subscriber.recv().await {
|
||||
@@ -580,29 +676,28 @@ impl Room {
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn subscribe_to_identity_status_changes(
|
||||
pub async fn subscribe_to_identity_status_changes(
|
||||
&self,
|
||||
listener: Box<dyn IdentityStatusChangeListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
) -> Result<Arc<TaskHandle>, ClientError> {
|
||||
let room = self.inner.clone();
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
let status_changes = room.subscribe_to_identity_status_changes().await;
|
||||
if let Ok(status_changes) = status_changes {
|
||||
// TODO: what to do with failures?
|
||||
let mut status_changes = pin!(status_changes);
|
||||
while let Some(identity_status_changes) = status_changes.next().await {
|
||||
listener.call(
|
||||
identity_status_changes
|
||||
.into_iter()
|
||||
.map(|change| {
|
||||
let user_id = change.user_id.to_string();
|
||||
IdentityStatusChange { user_id, changed_to: change.changed_to }
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
let status_changes = room.subscribe_to_identity_status_changes().await?;
|
||||
|
||||
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
let mut status_changes = pin!(status_changes);
|
||||
while let Some(identity_status_changes) = status_changes.next().await {
|
||||
listener.call(
|
||||
identity_status_changes
|
||||
.into_iter()
|
||||
.map(|change| {
|
||||
let user_id = change.user_id.to_string();
|
||||
IdentityStatusChange { user_id, changed_to: change.changed_to }
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
})))
|
||||
}))))
|
||||
}
|
||||
|
||||
/// Set (or unset) a flag on the room to indicate that the user has
|
||||
@@ -614,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(())
|
||||
}
|
||||
|
||||
@@ -648,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(())
|
||||
}
|
||||
|
||||
@@ -685,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.
|
||||
@@ -853,7 +949,7 @@ impl Room {
|
||||
) -> Result<Arc<TaskHandle>, ClientError> {
|
||||
let (stream, seen_ids_cleanup_handle) = self.inner.subscribe_to_knock_requests().await?;
|
||||
|
||||
let handle = Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
let handle = Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(stream);
|
||||
while let Some(requests) = stream.next().await {
|
||||
listener.call(requests.into_iter().map(Into::into).collect());
|
||||
@@ -1003,9 +1099,9 @@ impl Room {
|
||||
) -> Arc<TaskHandle> {
|
||||
let room = self.inner.clone();
|
||||
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
let subscription = room.observe_live_location_shares();
|
||||
let mut stream = subscription.subscribe();
|
||||
let stream = subscription.subscribe();
|
||||
let mut pinned_stream = pin!(stream);
|
||||
|
||||
while let Some(event) = pinned_stream.next().await {
|
||||
@@ -1043,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>);
|
||||
}
|
||||
|
||||
@@ -1069,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>);
|
||||
}
|
||||
|
||||
@@ -1189,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>);
|
||||
}
|
||||
|
||||
@@ -1421,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() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ use std::{fmt::Debug, sync::Arc};
|
||||
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 super::RUNTIME;
|
||||
use crate::{error::ClientError, task_handle::TaskHandle};
|
||||
use crate::{error::ClientError, runtime::get_runtime_handle, task_handle::TaskHandle};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum PublicRoomJoinRule {
|
||||
@@ -137,11 +137,11 @@ impl RoomDirectorySearch {
|
||||
) -> Arc<TaskHandle> {
|
||||
let (initial_values, mut stream) = self.inner.read().await.results();
|
||||
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
listener.on_update(vec![RoomDirectorySearchEntryUpdate::Reset {
|
||||
values: initial_values.into_iter().map(Into::into).collect(),
|
||||
}]);
|
||||
listener.on_update(vec![RoomDirectorySearchEntryUpdate::Reset {
|
||||
values: initial_values.into_iter().map(Into::into).collect(),
|
||||
}]);
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(diffs) = stream.next().await {
|
||||
listener.on_update(diffs.into_iter().map(|diff| diff.into()).collect());
|
||||
}
|
||||
@@ -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>);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use matrix_sdk::RoomState;
|
||||
use matrix_sdk::{EncryptionState, RoomState};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
client::JoinRule,
|
||||
error::ClientError,
|
||||
notification_settings::RoomNotificationMode,
|
||||
room::{Membership, RoomHero, RoomHistoryVisibility},
|
||||
room::{Membership, RoomHero, RoomHistoryVisibility, SuccessorRoom},
|
||||
room_member::RoomMember,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomInfo {
|
||||
id: String,
|
||||
encryption_state: EncryptionState,
|
||||
creator: Option<String>,
|
||||
/// The room's name from the room state event if received from sync, or one
|
||||
/// that's been computed otherwise.
|
||||
@@ -25,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>,
|
||||
@@ -79,11 +81,12 @@ 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 {
|
||||
id: room.room_id().to_string(),
|
||||
encryption_state: room.encryption_state(),
|
||||
creator: room.creator().as_ref().map(ToString::to_string),
|
||||
display_name: room.cached_display_name().map(|name| name.to_string()),
|
||||
raw_name: room.name(),
|
||||
@@ -92,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(),
|
||||
|
||||
@@ -3,33 +3,29 @@
|
||||
use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Duration};
|
||||
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{pin_mut, StreamExt, TryFutureExt};
|
||||
use matrix_sdk::ruma::{
|
||||
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
RoomId,
|
||||
use 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::{EventTimelineItem, Timeline},
|
||||
timeline_event_filter::TimelineEventTypeFilter,
|
||||
utils::AsyncRuntimeDropped,
|
||||
TaskHandle, RUNTIME,
|
||||
runtime::get_runtime_handle,
|
||||
TaskHandle,
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
@@ -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() },
|
||||
}
|
||||
}
|
||||
@@ -92,7 +76,7 @@ impl RoomListService {
|
||||
fn state(&self, listener: Box<dyn RoomListServiceStateListener>) -> Arc<TaskHandle> {
|
||||
let state_stream = self.inner.state();
|
||||
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(state_stream);
|
||||
|
||||
while let Some(state) = state_stream.next().await {
|
||||
@@ -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> {
|
||||
@@ -128,7 +109,7 @@ impl RoomListService {
|
||||
Duration::from_millis(delay_before_hiding_in_ms.into()),
|
||||
);
|
||||
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(sync_indicator_stream);
|
||||
|
||||
while let Some(sync_indicator) = sync_indicator_stream.next().await {
|
||||
@@ -167,7 +148,7 @@ impl RoomList {
|
||||
|
||||
Ok(RoomListLoadingStateResult {
|
||||
state: loading_state.get().into(),
|
||||
state_stream: Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
state_stream: Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(loading_state);
|
||||
|
||||
while let Some(loading_state) = loading_state.next().await {
|
||||
@@ -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,14 +217,15 @@ impl RoomList {
|
||||
let dynamic_entries_controller =
|
||||
Arc::new(RoomListDynamicEntriesController::new(dynamic_entries_controller));
|
||||
|
||||
let entries_stream = Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
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);
|
||||
|
||||
while let Some(diffs) = entries_stream.next().await {
|
||||
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,204 +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())
|
||||
}
|
||||
|
||||
fn is_direct(&self) -> bool {
|
||||
RUNTIME.block_on(self.inner.inner_room().is_direct()).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 `Room` FFI from an invited room without initializing its
|
||||
/// internal timeline.
|
||||
///
|
||||
/// An error will be returned if the room is a state different than invited.
|
||||
///
|
||||
/// ⚠️ Holding on to this room instance after it has been joined is not
|
||||
/// safe. Use `full_room` instead.
|
||||
#[deprecated(note = "Please use `preview_room` instead.")]
|
||||
fn invited_room(&self) -> Result<Arc<Room>, RoomListError> {
|
||||
if !matches!(self.membership(), Membership::Invited) {
|
||||
return Err(RoomListError::IncorrectRoomMembership {
|
||||
expected: vec![Membership::Invited],
|
||||
actual: self.membership(),
|
||||
});
|
||||
}
|
||||
Ok(Arc::new(Room::new(self.inner.inner_room().clone())))
|
||||
}
|
||||
|
||||
/// Builds a `RoomPreview` from a room list item. This is intended for
|
||||
/// invited or knocked rooms.
|
||||
///
|
||||
/// An error will be returned if the room is in a state other than invited
|
||||
/// or knocked.
|
||||
async fn preview_room(&self, via: Vec<String>) -> Result<Arc<RoomPreview>, ClientError> {
|
||||
// Validate parameters first.
|
||||
let server_names: Vec<OwnedServerName> = via
|
||||
.into_iter()
|
||||
.map(|server| ServerName::parse(server).map_err(ClientError::from))
|
||||
.collect::<Result<_, ClientError>>()?;
|
||||
|
||||
// Validate internal room state.
|
||||
let membership = self.membership();
|
||||
if !matches!(membership, Membership::Invited | Membership::Knocked) {
|
||||
return Err(RoomListError::IncorrectRoomMembership {
|
||||
expected: vec![Membership::Invited, Membership::Knocked],
|
||||
actual: membership,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
// 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.is_encrypted().await.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,
|
||||
|
||||
@@ -108,3 +108,25 @@ impl TryFrom<SdkRoomMember> for RoomMember {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains the current user's room member info and the optional room member
|
||||
/// info of the sender of the `m.room.member` event that this info represents.
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct RoomMemberWithSenderInfo {
|
||||
/// The room member.
|
||||
room_member: RoomMember,
|
||||
/// The info of the sender of the event `room_member` is based on, if
|
||||
/// available.
|
||||
sender_info: Option<RoomMember>,
|
||||
}
|
||||
|
||||
impl TryFrom<matrix_sdk::room::RoomMemberWithSenderInfo> for RoomMemberWithSenderInfo {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: matrix_sdk::room::RoomMemberWithSenderInfo) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
room_member: value.room_member.try_into()?,
|
||||
sender_info: value.sender_info.map(|member| member.try_into()).transpose()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::{
|
||||
client::JoinRule,
|
||||
error::ClientError,
|
||||
room::{Membership, RoomHero},
|
||||
room_member::RoomMember,
|
||||
room_member::{RoomMember, RoomMemberWithSenderInfo},
|
||||
utils::AsyncRuntimeDropped,
|
||||
};
|
||||
|
||||
@@ -51,11 +51,15 @@ impl RoomPreview {
|
||||
/// Leave the room if the room preview state is either joined, invited or
|
||||
/// knocked.
|
||||
///
|
||||
/// If rejecting an invite then also forget it as an extra layer of
|
||||
/// protection against spam attacks.
|
||||
///
|
||||
/// Will return an error otherwise.
|
||||
pub async fn leave(&self) -> Result<(), ClientError> {
|
||||
let room =
|
||||
self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?;
|
||||
room.leave().await.map_err(Into::into)
|
||||
|
||||
Ok(room.leave().await?)
|
||||
}
|
||||
|
||||
/// Get the user who created the invite, if any.
|
||||
@@ -74,32 +78,12 @@ impl RoomPreview {
|
||||
}
|
||||
|
||||
/// Get the membership details for the current user.
|
||||
pub async fn own_membership_details(&self) -> Option<RoomMembershipDetails> {
|
||||
pub async fn own_membership_details(&self) -> Option<RoomMemberWithSenderInfo> {
|
||||
let room = self.client.get_room(&self.inner.room_id)?;
|
||||
|
||||
let (own_member, sender_member) = match room.own_membership_details().await {
|
||||
Ok(memberships) => memberships,
|
||||
Err(error) => {
|
||||
warn!("Couldn't get membership info: {error}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
Some(RoomMembershipDetails {
|
||||
own_room_member: own_member.try_into().ok()?,
|
||||
sender_room_member: sender_member.and_then(|member| member.try_into().ok()),
|
||||
})
|
||||
room.member_with_sender_info(self.client.user_id()?).await.ok()?.try_into().ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains the current user's room member info and the optional room member
|
||||
/// info of the sender of the `m.room.member` event that this info represents.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomMembershipDetails {
|
||||
pub own_room_member: RoomMember,
|
||||
pub sender_room_member: Option<RoomMember>,
|
||||
}
|
||||
|
||||
impl RoomPreview {
|
||||
pub(crate) fn new(client: AsyncRuntimeDropped<Client>, inner: SdkRoomPreview) -> Self {
|
||||
Self { client, inner }
|
||||
|
||||
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::*;
|
||||
@@ -7,13 +7,16 @@ use matrix_sdk::{
|
||||
verification::{SasState, SasVerification, VerificationRequest, VerificationRequestState},
|
||||
Encryption,
|
||||
},
|
||||
ruma::events::{key::verification::VerificationMethod, AnyToDeviceEvent},
|
||||
ruma::events::key::verification::VerificationMethod,
|
||||
Account,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::UserId;
|
||||
use tracing::{error, info};
|
||||
use tracing::{error, warn};
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::{error::ClientError, utils::Timestamp};
|
||||
use crate::{
|
||||
client::UserProfile, error::ClientError, runtime::get_runtime_handle, utils::Timestamp,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SessionVerificationEmoji {
|
||||
@@ -39,18 +42,18 @@ pub enum SessionVerificationData {
|
||||
}
|
||||
|
||||
/// Details about the incoming verification request
|
||||
#[derive(Debug, uniffi::Record)]
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct SessionVerificationRequestDetails {
|
||||
sender_id: String,
|
||||
sender_profile: UserProfile,
|
||||
flow_id: String,
|
||||
device_id: String,
|
||||
display_name: Option<String>,
|
||||
device_display_name: Option<String>,
|
||||
/// First time this device was seen in milliseconds since epoch.
|
||||
first_seen_timestamp: Timestamp,
|
||||
}
|
||||
|
||||
#[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);
|
||||
@@ -66,6 +69,7 @@ pub type Delegate = Arc<RwLock<Option<Box<dyn SessionVerificationControllerDeleg
|
||||
pub struct SessionVerificationController {
|
||||
encryption: Encryption,
|
||||
user_identity: UserIdentity,
|
||||
account: Account,
|
||||
delegate: Delegate,
|
||||
verification_request: Arc<RwLock<Option<VerificationRequest>>>,
|
||||
sas_verification: Arc<RwLock<Option<SasVerification>>>,
|
||||
@@ -92,17 +96,9 @@ 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.verification_request.write().unwrap() = Some(verification_request.clone());
|
||||
|
||||
RUNTIME.spawn(Self::listen_to_verification_request_changes(
|
||||
verification_request,
|
||||
self.sas_verification.clone(),
|
||||
self.delegate.clone(),
|
||||
));
|
||||
|
||||
Ok(())
|
||||
self.set_ongoing_verification_request(verification_request)
|
||||
}
|
||||
|
||||
/// Accept the previously acknowledged verification request
|
||||
@@ -118,7 +114,7 @@ impl SessionVerificationController {
|
||||
}
|
||||
|
||||
/// Request verification for the current device
|
||||
pub async fn request_verification(&self) -> Result<(), ClientError> {
|
||||
pub async fn request_device_verification(&self) -> Result<(), ClientError> {
|
||||
let methods = vec![VerificationMethod::SasV1];
|
||||
let verification_request = self
|
||||
.user_identity
|
||||
@@ -126,15 +122,31 @@ impl SessionVerificationController {
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?;
|
||||
|
||||
*self.verification_request.write().unwrap() = Some(verification_request.clone());
|
||||
self.set_ongoing_verification_request(verification_request)
|
||||
}
|
||||
|
||||
RUNTIME.spawn(Self::listen_to_verification_request_changes(
|
||||
verification_request,
|
||||
self.sas_verification.clone(),
|
||||
self.delegate.clone(),
|
||||
));
|
||||
/// Request verification for the given user
|
||||
pub async fn request_user_verification(&self, user_id: String) -> Result<(), ClientError> {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
|
||||
Ok(())
|
||||
let user_identity = self
|
||||
.encryption
|
||||
.get_user_identity(&user_id)
|
||||
.await?
|
||||
.ok_or(ClientError::from_str("Unknown user identity", None))?;
|
||||
|
||||
if user_identity.is_verified() {
|
||||
return Err(ClientError::from_str("User is already verified", None));
|
||||
}
|
||||
|
||||
let methods = vec![VerificationMethod::SasV1];
|
||||
|
||||
let verification_request = user_identity
|
||||
.request_verification_with_methods(methods)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?;
|
||||
|
||||
self.set_ongoing_verification_request(verification_request)
|
||||
}
|
||||
|
||||
/// Transition the current verification request into a SAS verification
|
||||
@@ -143,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 {
|
||||
@@ -155,7 +167,8 @@ impl SessionVerificationController {
|
||||
}
|
||||
|
||||
let delegate = self.delegate.clone();
|
||||
RUNTIME.spawn(Self::listen_to_sas_verification_changes(verification, delegate));
|
||||
get_runtime_handle()
|
||||
.spawn(Self::listen_to_sas_verification_changes(verification, delegate));
|
||||
}
|
||||
_ => {
|
||||
if let Some(delegate) = &*self.delegate.read().unwrap() {
|
||||
@@ -172,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?)
|
||||
@@ -183,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?)
|
||||
@@ -194,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?)
|
||||
@@ -202,50 +215,94 @@ impl SessionVerificationController {
|
||||
}
|
||||
|
||||
impl SessionVerificationController {
|
||||
pub(crate) fn new(encryption: Encryption, user_identity: UserIdentity) -> Self {
|
||||
pub(crate) fn new(
|
||||
encryption: Encryption,
|
||||
user_identity: UserIdentity,
|
||||
account: Account,
|
||||
) -> Self {
|
||||
SessionVerificationController {
|
||||
encryption,
|
||||
user_identity,
|
||||
account,
|
||||
delegate: Arc::new(RwLock::new(None)),
|
||||
verification_request: Arc::new(RwLock::new(None)),
|
||||
sas_verification: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn process_to_device_message(&self, event: AnyToDeviceEvent) {
|
||||
if let AnyToDeviceEvent::KeyVerificationRequest(event) = event {
|
||||
info!("Received verification request: {:}", event.sender);
|
||||
|
||||
let Some(request) = self
|
||||
.encryption
|
||||
.get_verification_request(&event.sender, &event.content.transaction_id)
|
||||
.await
|
||||
else {
|
||||
error!("Failed retrieving verification request");
|
||||
return;
|
||||
};
|
||||
|
||||
if !request.is_self_verification() {
|
||||
info!("Received non-self verification request. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
let VerificationRequestState::Requested { other_device_data, .. } = request.state()
|
||||
else {
|
||||
error!("Received key verification event but the request is in the wrong state.");
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(delegate) = &*self.delegate.read().unwrap() {
|
||||
delegate.did_receive_verification_request(SessionVerificationRequestDetails {
|
||||
sender_id: request.other_user_id().into(),
|
||||
flow_id: request.flow_id().into(),
|
||||
device_id: other_device_data.device_id().into(),
|
||||
display_name: other_device_data.display_name().map(str::to_string),
|
||||
first_seen_timestamp: other_device_data.first_time_seen_ts().into(),
|
||||
});
|
||||
/// Ask the controller to process an incoming request based on the sender
|
||||
/// and flow identifier. It will fetch the request, verify that it's in the
|
||||
/// correct state and then and notify the delegate.
|
||||
pub(crate) async fn process_incoming_verification_request(
|
||||
&self,
|
||||
sender: &UserId,
|
||||
flow_id: impl AsRef<str>,
|
||||
) {
|
||||
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:?}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let Some(request) = self.encryption.get_verification_request(sender, flow_id).await else {
|
||||
error!("Failed retrieving verification request");
|
||||
return;
|
||||
};
|
||||
|
||||
let VerificationRequestState::Requested { other_device_data, .. } = request.state() else {
|
||||
error!("Received verification request event but the request is in the wrong state.");
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(sender_profile) = self.account.fetch_user_profile_of(sender).await else {
|
||||
error!("Failed fetching user profile for verification request");
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(delegate) = &*self.delegate.read().unwrap() {
|
||||
delegate.did_receive_verification_request(SessionVerificationRequestDetails {
|
||||
sender_profile: UserProfile {
|
||||
user_id: request.other_user_id().to_string(),
|
||||
display_name: sender_profile.displayname,
|
||||
avatar_url: sender_profile.avatar_url.as_ref().map(|url| url.to_string()),
|
||||
},
|
||||
flow_id: request.flow_id().into(),
|
||||
device_id: other_device_data.device_id().into(),
|
||||
device_display_name: other_device_data.display_name().map(str::to_string),
|
||||
first_seen_timestamp: other_device_data.first_time_seen_ts().into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn set_ongoing_verification_request(
|
||||
&self,
|
||||
verification_request: VerificationRequest,
|
||||
) -> Result<(), ClientError> {
|
||||
if let Some(ongoing_verification_request) =
|
||||
self.verification_request.read().unwrap().clone()
|
||||
{
|
||||
if !ongoing_verification_request.is_done()
|
||||
&& !ongoing_verification_request.is_cancelled()
|
||||
{
|
||||
return Err(ClientError::from_str(
|
||||
"There is another verification flow ongoing.",
|
||||
None,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
*self.verification_request.write().unwrap() = Some(verification_request.clone());
|
||||
|
||||
get_runtime_handle().spawn(Self::listen_to_verification_request_changes(
|
||||
verification_request,
|
||||
self.sas_verification.clone(),
|
||||
self.delegate.clone(),
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn listen_to_verification_request_changes(
|
||||
@@ -271,7 +328,7 @@ impl SessionVerificationController {
|
||||
}
|
||||
|
||||
let delegate = delegate.clone();
|
||||
RUNTIME.spawn(Self::listen_to_sas_verification_changes(
|
||||
get_runtime_handle().spawn(Self::listen_to_sas_verification_changes(
|
||||
verification,
|
||||
delegate,
|
||||
));
|
||||
|
||||
@@ -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 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,
|
||||
RUNTIME,
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -84,7 +82,7 @@ impl SyncService {
|
||||
pub fn state(&self, listener: Box<dyn SyncServiceStateObserver>) -> Arc<TaskHandle> {
|
||||
let state_stream = self.inner.state();
|
||||
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(state_stream);
|
||||
|
||||
while let Some(state) = state_stream.next().await {
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,90 @@
|
||||
use ruma::EventId;
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk_ui::timeline::event_type_filter::TimelineEventTypeFilter as InnerTimelineEventTypeFilter;
|
||||
use ruma::{
|
||||
events::{AnySyncTimelineEvent, TimelineEventType},
|
||||
EventId,
|
||||
};
|
||||
|
||||
use super::FocusEventError;
|
||||
use crate::{error::ClientError, event::RoomMessageEventMessageType};
|
||||
use crate::{
|
||||
error::ClientError,
|
||||
event::{MessageLikeEventType, RoomMessageEventMessageType, StateEventType},
|
||||
};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct TimelineEventTypeFilter {
|
||||
inner: InnerTimelineEventTypeFilter,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl TimelineEventTypeFilter {
|
||||
#[uniffi::constructor]
|
||||
pub fn include(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
|
||||
let event_types: Vec<TimelineEventType> =
|
||||
event_types.iter().map(|t| t.clone().into()).collect();
|
||||
Arc::new(Self { inner: InnerTimelineEventTypeFilter::Include(event_types) })
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn exclude(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
|
||||
let event_types: Vec<TimelineEventType> =
|
||||
event_types.iter().map(|t| t.clone().into()).collect();
|
||||
Arc::new(Self { inner: InnerTimelineEventTypeFilter::Exclude(event_types) })
|
||||
}
|
||||
}
|
||||
|
||||
impl TimelineEventTypeFilter {
|
||||
/// Filters an [`event`] to decide whether it should be part of the timeline
|
||||
/// based on [`AnySyncTimelineEvent::event_type()`].
|
||||
pub(crate) fn filter(&self, event: &AnySyncTimelineEvent) -> bool {
|
||||
self.inner.filter(event)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum, Clone)]
|
||||
pub enum FilterTimelineEventType {
|
||||
MessageLike { event_type: MessageLikeEventType },
|
||||
State { event_type: StateEventType },
|
||||
}
|
||||
|
||||
impl From<FilterTimelineEventType> for TimelineEventType {
|
||||
fn from(value: FilterTimelineEventType) -> TimelineEventType {
|
||||
match value {
|
||||
FilterTimelineEventType::MessageLike { event_type } => {
|
||||
ruma::events::MessageLikeEventType::from(event_type).into()
|
||||
}
|
||||
FilterTimelineEventType::State { event_type } => {
|
||||
ruma::events::StateEventType::from(event_type).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 {
|
||||
@@ -17,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 })
|
||||
@@ -52,28 +143,27 @@ impl From<DateDividerMode> for matrix_sdk_ui::timeline::DateDividerMode {
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum AllowedMessageTypes {
|
||||
pub enum TimelineFilter {
|
||||
/// Show all the events in the timeline, independent of their type.
|
||||
All,
|
||||
Only { types: Vec<RoomMessageEventMessageType> },
|
||||
/// Show only `m.room.messages` of the given room message types.
|
||||
OnlyMessage {
|
||||
/// A list of [`RoomMessageEventMessageType`] that will be allowed to
|
||||
/// appear in the timeline.
|
||||
types: Vec<RoomMessageEventMessageType>,
|
||||
},
|
||||
/// Show only events which match this filter.
|
||||
EventTypeFilter { filter: Arc<TimelineEventTypeFilter> },
|
||||
}
|
||||
|
||||
/// Various options used to configure the timeline's behavior.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `internal_id_prefix` -
|
||||
///
|
||||
/// * `allowed_message_types` -
|
||||
///
|
||||
/// * `date_divider_mode` -
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct TimelineConfiguration {
|
||||
/// What should the timeline focus on?
|
||||
pub focus: TimelineFocus,
|
||||
|
||||
/// A list of [`RoomMessageEventMessageType`] that will be allowed to appear
|
||||
/// in the timeline
|
||||
pub allowed_message_types: AllowedMessageTypes,
|
||||
/// How should we filter out events from the timeline?
|
||||
pub filter: TimelineFilter,
|
||||
|
||||
/// An optional String that will be prepended to
|
||||
/// all the timeline item's internal IDs, making it possible to
|
||||
@@ -82,4 +172,15 @@ pub struct TimelineConfiguration {
|
||||
|
||||
/// How often to insert date dividers
|
||||
pub date_divider_mode: DateDividerMode,
|
||||
|
||||
/// Should the read receipts and read markers be tracked for the timeline
|
||||
/// items in this instance?
|
||||
///
|
||||
/// 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,
|
||||
}
|
||||
|
||||
@@ -12,73 +12,31 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use matrix_sdk::{crypto::types::events::UtdCause, room::power_levels::power_level_user_changes};
|
||||
use matrix_sdk_ui::timeline::{PollResult, RoomPinnedEventsChange, TimelineDetails};
|
||||
use ruma::events::{room::MediaSource as RumaMediaSource, EventContent, FullStateEventContent};
|
||||
use matrix_sdk::room::power_levels::power_level_user_changes;
|
||||
use matrix_sdk_ui::timeline::RoomPinnedEventsChange;
|
||||
use ruma::events::FullStateEventContent;
|
||||
|
||||
use super::ProfileDetails;
|
||||
use crate::{
|
||||
error::ClientError,
|
||||
ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind},
|
||||
utils::Timestamp,
|
||||
};
|
||||
use crate::{timeline::msg_like::MsgLikeContent, utils::Timestamp};
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent {
|
||||
fn from(value: matrix_sdk_ui::timeline::TimelineItemContent) -> Self {
|
||||
use matrix_sdk_ui::timeline::TimelineItemContent as Content;
|
||||
|
||||
match value {
|
||||
Content::Message(message) => {
|
||||
let msgtype = message.msgtype().msgtype().to_owned();
|
||||
|
||||
match TryInto::<MessageContent>::try_into(message) {
|
||||
Ok(message) => TimelineItemContent::Message { content: message },
|
||||
Err(error) => TimelineItemContent::FailedToParseMessageLike {
|
||||
event_type: msgtype,
|
||||
error: error.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Content::RedactedMessage => TimelineItemContent::RedactedMessage,
|
||||
|
||||
Content::Sticker(sticker) => {
|
||||
let content = sticker.content();
|
||||
|
||||
let media_source = RumaMediaSource::from(content.source.clone());
|
||||
|
||||
if let Err(error) = media_source.verify() {
|
||||
return TimelineItemContent::FailedToParseMessageLike {
|
||||
event_type: sticker.content().event_type().to_string(),
|
||||
error: error.to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
match TryInto::<ImageInfo>::try_into(&content.info) {
|
||||
Ok(info) => TimelineItemContent::Sticker {
|
||||
body: content.body.clone(),
|
||||
info,
|
||||
source: Arc::new(MediaSource { media_source }),
|
||||
},
|
||||
Err(error) => TimelineItemContent::FailedToParseMessageLike {
|
||||
event_type: sticker.content().event_type().to_string(),
|
||||
error: error.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Content::Poll(poll_state) => TimelineItemContent::from(poll_state.results()),
|
||||
Content::MsgLike(msg_like) => match msg_like.try_into() {
|
||||
Ok(content) => TimelineItemContent::MsgLike { content },
|
||||
Err((error, event_type)) => TimelineItemContent::FailedToParseMessageLike {
|
||||
event_type,
|
||||
error: error.to_string(),
|
||||
},
|
||||
},
|
||||
|
||||
Content::CallInvite => TimelineItemContent::CallInvite,
|
||||
|
||||
Content::CallNotify => TimelineItemContent::CallNotify,
|
||||
|
||||
Content::UnableToDecrypt(msg) => {
|
||||
TimelineItemContent::UnableToDecrypt { msg: EncryptedMessage::new(&msg) }
|
||||
}
|
||||
|
||||
Content::MembershipChange(membership) => {
|
||||
let reason = match membership.content() {
|
||||
FullStateEventContent::Original { content, .. } => content.reason.clone(),
|
||||
@@ -137,65 +95,21 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct MessageContent {
|
||||
pub msg_type: MessageType,
|
||||
pub body: String,
|
||||
pub in_reply_to: Option<Arc<InReplyToDetails>>,
|
||||
pub thread_root: Option<String>,
|
||||
pub is_edited: bool,
|
||||
pub mentions: Option<Mentions>,
|
||||
}
|
||||
|
||||
impl TryFrom<matrix_sdk_ui::timeline::Message> for MessageContent {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: matrix_sdk_ui::timeline::Message) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
msg_type: value.msgtype().clone().try_into()?,
|
||||
body: value.body().to_owned(),
|
||||
in_reply_to: value.in_reply_to().map(|r| Arc::new(r.clone().into())),
|
||||
is_edited: value.is_edited(),
|
||||
thread_root: value.thread_root().map(|id| id.to_string()),
|
||||
mentions: value.mentions().cloned().map(|m| m.into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ruma::events::Mentions> for Mentions {
|
||||
fn from(value: ruma::events::Mentions) -> Self {
|
||||
Self {
|
||||
user_ids: value.user_ids.iter().map(|id| id.to_string()).collect(),
|
||||
room: value.room,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
// 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 {
|
||||
Message {
|
||||
content: MessageContent,
|
||||
},
|
||||
RedactedMessage,
|
||||
Sticker {
|
||||
body: String,
|
||||
info: ImageInfo,
|
||||
source: Arc<MediaSource>,
|
||||
},
|
||||
Poll {
|
||||
question: String,
|
||||
kind: PollKind,
|
||||
max_selections: u64,
|
||||
answers: Vec<PollAnswer>,
|
||||
votes: HashMap<String, Vec<String>>,
|
||||
end_time: Option<Timestamp>,
|
||||
has_been_edited: bool,
|
||||
MsgLike {
|
||||
content: MsgLikeContent,
|
||||
},
|
||||
CallInvite,
|
||||
CallNotify,
|
||||
UnableToDecrypt {
|
||||
msg: EncryptedMessage,
|
||||
},
|
||||
RoomMembership {
|
||||
user_id: String,
|
||||
user_display_name: Option<String>,
|
||||
@@ -223,94 +137,6 @@ pub enum TimelineItemContent {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct InReplyToDetails {
|
||||
event_id: String,
|
||||
event: RepliedToEventDetails,
|
||||
}
|
||||
|
||||
impl InReplyToDetails {
|
||||
pub(crate) fn new(event_id: String, event: RepliedToEventDetails) -> Self {
|
||||
Self { event_id, event }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl InReplyToDetails {
|
||||
pub fn event_id(&self) -> String {
|
||||
self.event_id.clone()
|
||||
}
|
||||
|
||||
pub fn event(&self) -> RepliedToEventDetails {
|
||||
self.event.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
|
||||
fn from(inner: matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum RepliedToEventDetails {
|
||||
Unavailable,
|
||||
Pending,
|
||||
Ready { content: TimelineItemContent, sender: String, sender_profile: ProfileDetails },
|
||||
Error { message: String },
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum EncryptedMessage {
|
||||
OlmV1Curve25519AesSha2 {
|
||||
/// The Curve25519 key of the sender.
|
||||
sender_key: String,
|
||||
},
|
||||
// Other fields not included because UniFFI doesn't have the concept of
|
||||
// deprecated fields right now.
|
||||
MegolmV1AesSha2 {
|
||||
/// The ID of the session used to encrypt the message.
|
||||
session_id: String,
|
||||
|
||||
/// What we know about what caused this UTD. E.g. was this event sent
|
||||
/// when we were not a member of this room?
|
||||
cause: UtdCause,
|
||||
},
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl EncryptedMessage {
|
||||
fn new(msg: &matrix_sdk_ui::timeline::EncryptedMessage) -> Self {
|
||||
use matrix_sdk_ui::timeline::EncryptedMessage as Message;
|
||||
|
||||
match msg {
|
||||
Message::OlmV1Curve25519AesSha2 { sender_key } => {
|
||||
let sender_key = sender_key.clone();
|
||||
Self::OlmV1Curve25519AesSha2 { sender_key }
|
||||
}
|
||||
Message::MegolmV1AesSha2 { session_id, cause, .. } => {
|
||||
let session_id = session_id.clone();
|
||||
Self::MegolmV1AesSha2 { session_id, cause: *cause }
|
||||
}
|
||||
Message::Unknown => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct Reaction {
|
||||
pub key: String,
|
||||
@@ -463,27 +289,3 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct PollAnswer {
|
||||
pub id: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
impl From<PollResult> for TimelineItemContent {
|
||||
fn from(value: PollResult) -> Self {
|
||||
TimelineItemContent::Poll {
|
||||
question: value.question,
|
||||
kind: PollKind::from(value.kind),
|
||||
max_selections: value.max_selections,
|
||||
answers: value
|
||||
.answers
|
||||
.into_iter()
|
||||
.map(|i| PollAnswer { id: i.id, text: i.text })
|
||||
.collect(),
|
||||
votes: value.votes,
|
||||
end_time: value.end_time.map(|t| t.into()),
|
||||
has_been_edited: value.has_been_edited,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 content::{InReplyToDetails, RepliedToEventDetails};
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{pin_mut, StreamExt as _};
|
||||
use matrix_sdk::{
|
||||
@@ -25,14 +24,18 @@ use matrix_sdk::{
|
||||
BaseVideoInfo, Thumbnail,
|
||||
},
|
||||
deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode},
|
||||
room::edit::EditedContent as SdkEditedContent,
|
||||
Error,
|
||||
event_cache::RoomPaginationStatus,
|
||||
room::{
|
||||
edit::EditedContent as SdkEditedContent,
|
||||
reply::{EnforceThread, Reply},
|
||||
},
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{
|
||||
self, EventItemOrigin, LiveBackPaginationStatus, Profile, RepliedToEvent, TimelineDetails,
|
||||
self, AttachmentSource, EventItemOrigin, Profile, TimelineDetails,
|
||||
TimelineUniqueId as SdkTimelineUniqueId,
|
||||
};
|
||||
use mime::Mime;
|
||||
use reply::{EmbeddedEventDetails, InReplyToDetails};
|
||||
use ruma::{
|
||||
events::{
|
||||
location::{AssetType as RumaAssetType, LocationContent, ZoomLevel},
|
||||
@@ -46,7 +49,7 @@ use ruma::{
|
||||
},
|
||||
receipt::ReceiptThread,
|
||||
room::message::{
|
||||
ForwardThread, LocationMessageEventContent, MessageType,
|
||||
LocationMessageEventContent, MessageType, ReplyWithinThread,
|
||||
RoomMessageEventContentWithoutRelation,
|
||||
},
|
||||
AnyMessageLikeEventContent,
|
||||
@@ -60,7 +63,8 @@ use tokio::{
|
||||
use tracing::{error, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use self::content::{Reaction, ReactionSenderData, TimelineItemContent};
|
||||
use self::content::TimelineItemContent;
|
||||
pub use self::msg_like::MessageContent;
|
||||
use crate::{
|
||||
client::ProgressWatcher,
|
||||
error::{ClientError, RoomError},
|
||||
@@ -70,16 +74,18 @@ use crate::{
|
||||
AssetType, AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, PollKind,
|
||||
ThumbnailInfo, VideoInfo,
|
||||
},
|
||||
runtime::get_runtime_handle,
|
||||
task_handle::TaskHandle,
|
||||
utils::Timestamp,
|
||||
RUNTIME,
|
||||
};
|
||||
|
||||
pub mod configuration;
|
||||
mod content;
|
||||
mod msg_like;
|
||||
mod reply;
|
||||
|
||||
pub use content::MessageContent;
|
||||
use matrix_sdk::utils::formatted_body_from;
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
|
||||
use crate::error::QueueWedgeError;
|
||||
|
||||
@@ -94,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,
|
||||
@@ -121,11 +122,12 @@ impl Timeline {
|
||||
.info(attachment_info)
|
||||
.caption(params.caption)
|
||||
.formatted_caption(formatted_caption)
|
||||
.mentions(params.mentions.map(Into::into));
|
||||
.mentions(params.mentions.map(Into::into))
|
||||
.reply(params.reply_params.map(|p| p.try_into()).transpose()?);
|
||||
|
||||
let handle = SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
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();
|
||||
@@ -133,7 +135,7 @@ impl Timeline {
|
||||
|
||||
if let Some(progress_watcher) = progress_watcher {
|
||||
let mut subscriber = request.subscribe_to_send_progress();
|
||||
RUNTIME.spawn(async move {
|
||||
get_runtime_handle().spawn(async move {
|
||||
while let Some(progress) = subscriber.next().await {
|
||||
progress_watcher.transmission_progress(progress.into());
|
||||
}
|
||||
@@ -195,38 +197,97 @@ 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.
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
// Optional intentional mentions to be sent with the media.
|
||||
/// Optional intentional mentions to be sent with the media.
|
||||
mentions: Option<Mentions>,
|
||||
/// Optional parameters for sending the media as (threaded) reply.
|
||||
reply_params: Option<ReplyParameters>,
|
||||
/// Should the media be sent with the send queue, or synchronously?
|
||||
///
|
||||
/// Watching progress only works with the synchronous method, at the moment.
|
||||
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.
|
||||
event_id: String,
|
||||
/// Whether to enforce a thread relation.
|
||||
enforce_thread: bool,
|
||||
/// If enforcing a threaded relation, whether the message is a reply on a
|
||||
/// thread.
|
||||
reply_within_thread: bool,
|
||||
}
|
||||
|
||||
impl TryInto<Reply> for ReplyParameters {
|
||||
type Error = RoomError;
|
||||
|
||||
fn try_into(self) -> Result<Reply, Self::Error> {
|
||||
let event_id =
|
||||
EventId::parse(&self.event_id).map_err(|_| RoomError::InvalidRepliedToEventId)?;
|
||||
let enforce_thread = if self.enforce_thread {
|
||||
EnforceThread::Threaded(if self.reply_within_thread {
|
||||
ReplyWithinThread::Yes
|
||||
} else {
|
||||
ReplyWithinThread::No
|
||||
})
|
||||
} else {
|
||||
EnforceThread::MaybeThreaded
|
||||
};
|
||||
|
||||
Ok(Reply { event_id, enforce_thread })
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Timeline {
|
||||
pub async fn add_listener(&self, listener: Box<dyn TimelineListener>) -> Arc<TaskHandle> {
|
||||
let (timeline_items, timeline_stream) = self.inner.subscribe().await;
|
||||
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
// It's important that the initial items are passed *before* we forward the
|
||||
// stream updates, with a guaranteed ordering. Otherwise, it could
|
||||
// be that the listener be called before the initial items have been
|
||||
// handled by the caller. See #3535 for details.
|
||||
|
||||
// First, pass all the items as a reset update.
|
||||
listener.on_update(vec![Arc::new(TimelineDiff::new(VectorDiff::Reset {
|
||||
values: timeline_items,
|
||||
}))]);
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(timeline_stream);
|
||||
|
||||
// It's important that the initial items are passed *before* we forward the
|
||||
// stream updates, with a guaranteed ordering. Otherwise, it could
|
||||
// be that the listener be called before the initial items have been
|
||||
// handled by the caller. See #3535 for details.
|
||||
|
||||
// First, pass all the items as a reset update.
|
||||
listener.on_update(vec![Arc::new(TimelineDiff::new(VectorDiff::Reset {
|
||||
values: timeline_items,
|
||||
}))]);
|
||||
|
||||
// Then forward new items.
|
||||
while let Some(diffs) = timeline_stream.next().await {
|
||||
listener
|
||||
@@ -236,7 +297,7 @@ impl Timeline {
|
||||
}
|
||||
|
||||
pub fn retry_decryption(self: Arc<Self>, session_ids: Vec<String>) {
|
||||
RUNTIME.spawn(async move {
|
||||
get_runtime_handle().spawn(async move {
|
||||
self.inner.retry_decryption(&session_ids).await;
|
||||
});
|
||||
}
|
||||
@@ -255,10 +316,14 @@ impl Timeline {
|
||||
.await
|
||||
.context("can't subscribe to the back-pagination status on a focused timeline")?;
|
||||
|
||||
Ok(Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
// Send the current state even if it hasn't changed right away.
|
||||
listener.on_update(initial);
|
||||
// Send the current state even if it hasn't changed right away.
|
||||
//
|
||||
// Note: don't do it in the spawned function, so that the caller is immediately
|
||||
// aware of the current state, and this doesn't depend on the async runtime
|
||||
// having an available worker
|
||||
listener.on_update(initial);
|
||||
|
||||
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(status) = subscriber.next().await {
|
||||
listener.on_update(status);
|
||||
}
|
||||
@@ -441,7 +506,7 @@ impl Timeline {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn end_poll(
|
||||
pub async fn end_poll(
|
||||
self: Arc<Self>,
|
||||
poll_start_event_id: String,
|
||||
text: String,
|
||||
@@ -451,29 +516,25 @@ impl Timeline {
|
||||
let poll_end_event_content = UnstablePollEndEventContent::new(text, poll_start_event_id);
|
||||
let event_content = AnyMessageLikeEventContent::UnstablePollEnd(poll_end_event_content);
|
||||
|
||||
RUNTIME.spawn(async move {
|
||||
if let Err(err) = self.inner.send(event_content).await {
|
||||
error!("unable to end poll: {err}");
|
||||
}
|
||||
});
|
||||
if let Err(err) = self.inner.send(event_content).await {
|
||||
error!("unable to end poll: {err}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a reply.
|
||||
///
|
||||
/// If the replied to event has a thread relation, it is forwarded on the
|
||||
/// reply so that clients that support threads can render the reply
|
||||
/// inside the thread.
|
||||
pub async fn send_reply(
|
||||
&self,
|
||||
msg: Arc<RoomMessageEventContentWithoutRelation>,
|
||||
event_id: String,
|
||||
reply_params: ReplyParameters,
|
||||
) -> Result<(), ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
let replied_to_info = self
|
||||
.inner
|
||||
.replied_to_info_from_event_id(&event_id)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!(err))?;
|
||||
|
||||
self.inner
|
||||
.send_reply((*msg).clone(), replied_to_info, ForwardThread::Yes)
|
||||
.send_reply((*msg).clone(), reply_params.try_into()?)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!(err))?;
|
||||
Ok(())
|
||||
@@ -621,33 +682,29 @@ impl Timeline {
|
||||
) -> Result<Arc<InReplyToDetails>, ClientError> {
|
||||
let event_id = EventId::parse(&event_id_str)?;
|
||||
|
||||
let replied_to: Result<RepliedToEvent, Error> =
|
||||
if let Some(event) = self.inner.item_by_event_id(&event_id).await {
|
||||
Ok(RepliedToEvent::from_timeline_item(&event))
|
||||
} else {
|
||||
match self.inner.room().event(&event_id, None).await {
|
||||
Ok(timeline_event) => Ok(RepliedToEvent::try_from_timeline_event_for_room(
|
||||
timeline_event,
|
||||
self.inner.room(),
|
||||
)
|
||||
.await?),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
};
|
||||
let replied_to = match self.inner.room().load_or_fetch_event(&event_id, None).await {
|
||||
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() },
|
||||
))),
|
||||
}
|
||||
}
|
||||
@@ -751,13 +808,13 @@ 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 {
|
||||
fn on_update(&self, status: LiveBackPaginationStatus);
|
||||
pub trait PaginationStatusListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, status: RoomPaginationStatus);
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
@@ -934,6 +991,7 @@ impl TimelineItem {
|
||||
match self.0.as_virtual()? {
|
||||
VItem::DateDivider(ts) => Some(VirtualTimelineItem::DateDivider { ts: (*ts).into() }),
|
||||
VItem::ReadMarker => Some(VirtualTimelineItem::ReadMarker),
|
||||
VItem::TimelineStart => Some(VirtualTimelineItem::TimelineStart),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1028,7 +1086,6 @@ pub struct EventTimelineItem {
|
||||
is_editable: bool,
|
||||
content: TimelineItemContent,
|
||||
timestamp: Timestamp,
|
||||
reactions: Vec<Reaction>,
|
||||
local_send_state: Option<EventSendState>,
|
||||
local_created_at: Option<u64>,
|
||||
read_receipts: HashMap<String, Receipt>,
|
||||
@@ -1039,20 +1096,6 @@ pub struct EventTimelineItem {
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
|
||||
fn from(item: matrix_sdk_ui::timeline::EventTimelineItem) -> Self {
|
||||
let reactions = item
|
||||
.reactions()
|
||||
.iter()
|
||||
.map(|(k, v)| Reaction {
|
||||
key: k.to_owned(),
|
||||
senders: v
|
||||
.into_iter()
|
||||
.map(|(sender_id, info)| ReactionSenderData {
|
||||
sender_id: sender_id.to_string(),
|
||||
timestamp: info.timestamp.into(),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect();
|
||||
let item = Arc::new(item);
|
||||
let lazy_provider = Arc::new(LazyTimelineItemProvider(item.clone()));
|
||||
let read_receipts =
|
||||
@@ -1061,12 +1104,11 @@ 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(),
|
||||
timestamp: item.timestamp().into(),
|
||||
reactions,
|
||||
local_send_state: item.send_state().map(|s| s.into()),
|
||||
local_created_at: item.local_created_at().map(|t| t.0.into()),
|
||||
read_receipts,
|
||||
@@ -1103,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),
|
||||
},
|
||||
@@ -1159,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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1177,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 {
|
||||
@@ -1196,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1213,6 +1255,9 @@ pub enum VirtualTimelineItem {
|
||||
|
||||
/// The user's own read marker.
|
||||
ReadMarker,
|
||||
|
||||
/// The timeline start, that is, the *oldest* event in time for that room.
|
||||
TimelineStart,
|
||||
}
|
||||
|
||||
/// A [`TimelineItem`](super::TimelineItem) that doesn't correspond to an event.
|
||||
@@ -1323,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use matrix_sdk::crypto::types::events::UtdCause;
|
||||
use ruma::events::{room::MediaSource as RumaMediaSource, EventContent};
|
||||
|
||||
use super::{
|
||||
content::Reaction,
|
||||
reply::{EmbeddedEventDetails, InReplyToDetails},
|
||||
};
|
||||
use crate::{
|
||||
error::ClientError,
|
||||
ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind},
|
||||
timeline::content::ReactionSenderData,
|
||||
utils::Timestamp,
|
||||
};
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum MsgLikeKind {
|
||||
/// An `m.room.message` event or extensible event, including edits.
|
||||
Message { content: MessageContent },
|
||||
/// An `m.sticker` event.
|
||||
Sticker { body: String, info: ImageInfo, source: Arc<MediaSource> },
|
||||
/// An `m.poll.start` event.
|
||||
Poll {
|
||||
question: String,
|
||||
kind: PollKind,
|
||||
max_selections: u64,
|
||||
answers: Vec<PollAnswer>,
|
||||
votes: HashMap<String, Vec<String>>,
|
||||
end_time: Option<Timestamp>,
|
||||
has_been_edited: bool,
|
||||
},
|
||||
|
||||
/// A redacted message.
|
||||
Redacted,
|
||||
|
||||
/// An `m.room.encrypted` event that could not be decrypted.
|
||||
UnableToDecrypt { msg: EncryptedMessage },
|
||||
}
|
||||
|
||||
/// A special kind of [`super::TimelineItemContent`] that groups together
|
||||
/// different room message types with their respective reactions and thread
|
||||
/// information.
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct MsgLikeContent {
|
||||
pub kind: MsgLikeKind,
|
||||
pub reactions: Vec<Reaction>,
|
||||
/// 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)]
|
||||
pub struct MessageContent {
|
||||
pub msg_type: MessageType,
|
||||
pub body: String,
|
||||
pub is_edited: bool,
|
||||
pub mentions: Option<Mentions>,
|
||||
}
|
||||
|
||||
impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
|
||||
type Error = (ClientError, String);
|
||||
|
||||
fn try_from(value: matrix_sdk_ui::timeline::MsgLikeContent) -> Result<Self, Self::Error> {
|
||||
use matrix_sdk_ui::timeline::MsgLikeKind as Kind;
|
||||
|
||||
let reactions = value
|
||||
.reactions
|
||||
.iter()
|
||||
.map(|(k, v)| Reaction {
|
||||
key: k.to_owned(),
|
||||
senders: v
|
||||
.into_iter()
|
||||
.map(|(sender_id, info)| ReactionSenderData {
|
||||
sender_id: sender_id.to_string(),
|
||||
timestamp: info.timestamp.into(),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let in_reply_to = value.in_reply_to.map(|r| Arc::new(r.into()));
|
||||
|
||||
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())
|
||||
.map_err(|e| (e, message.msgtype().msgtype().to_owned()))?;
|
||||
|
||||
Self {
|
||||
kind: MsgLikeKind::Message {
|
||||
content: MessageContent {
|
||||
msg_type,
|
||||
body: message.body().to_owned(),
|
||||
is_edited: message.is_edited(),
|
||||
mentions: message.mentions().cloned().map(|m| m.into()),
|
||||
},
|
||||
},
|
||||
reactions,
|
||||
in_reply_to,
|
||||
thread_root,
|
||||
thread_summary,
|
||||
}
|
||||
}
|
||||
Kind::Sticker(sticker) => {
|
||||
let content = sticker.content();
|
||||
|
||||
let media_source = RumaMediaSource::from(content.source.clone());
|
||||
media_source
|
||||
.verify()
|
||||
.map_err(|e| (e, sticker.content().event_type().to_string()))?;
|
||||
|
||||
let image_info = TryInto::<ImageInfo>::try_into(&content.info)
|
||||
.map_err(|e| (e, sticker.content().event_type().to_string()))?;
|
||||
|
||||
Self {
|
||||
kind: MsgLikeKind::Sticker {
|
||||
body: content.body.clone(),
|
||||
info: image_info,
|
||||
source: Arc::new(MediaSource { media_source }),
|
||||
},
|
||||
reactions,
|
||||
in_reply_to,
|
||||
thread_root,
|
||||
thread_summary,
|
||||
}
|
||||
}
|
||||
Kind::Poll(poll_state) => {
|
||||
let results = poll_state.results();
|
||||
|
||||
Self {
|
||||
kind: MsgLikeKind::Poll {
|
||||
question: results.question,
|
||||
kind: PollKind::from(results.kind),
|
||||
max_selections: results.max_selections,
|
||||
answers: results
|
||||
.answers
|
||||
.into_iter()
|
||||
.map(|i| PollAnswer { id: i.id, text: i.text })
|
||||
.collect(),
|
||||
votes: results.votes,
|
||||
end_time: results.end_time.map(|t| t.into()),
|
||||
has_been_edited: results.has_been_edited,
|
||||
},
|
||||
reactions,
|
||||
in_reply_to,
|
||||
thread_root,
|
||||
thread_summary,
|
||||
}
|
||||
}
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ruma::events::Mentions> for Mentions {
|
||||
fn from(value: ruma::events::Mentions) -> Self {
|
||||
Self {
|
||||
user_ids: value.user_ids.iter().map(|id| id.to_string()).collect(),
|
||||
room: value.room,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum EncryptedMessage {
|
||||
OlmV1Curve25519AesSha2 {
|
||||
/// The Curve25519 key of the sender.
|
||||
sender_key: String,
|
||||
},
|
||||
// Other fields not included because UniFFI doesn't have the concept of
|
||||
// deprecated fields right now.
|
||||
MegolmV1AesSha2 {
|
||||
/// The ID of the session used to encrypt the message.
|
||||
session_id: String,
|
||||
|
||||
/// What we know about what caused this UTD. E.g. was this event sent
|
||||
/// when we were not a member of this room?
|
||||
cause: UtdCause,
|
||||
},
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl EncryptedMessage {
|
||||
pub(crate) fn new(msg: &matrix_sdk_ui::timeline::EncryptedMessage) -> Self {
|
||||
use matrix_sdk_ui::timeline::EncryptedMessage as Message;
|
||||
|
||||
match msg {
|
||||
Message::OlmV1Curve25519AesSha2 { sender_key } => {
|
||||
let sender_key = sender_key.clone();
|
||||
Self::OlmV1Curve25519AesSha2 { sender_key }
|
||||
}
|
||||
Message::MegolmV1AesSha2 { session_id, cause, .. } => {
|
||||
let session_id = session_id.clone();
|
||||
Self::MegolmV1AesSha2 { session_id, cause: *cause }
|
||||
}
|
||||
Message::Unknown => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk_ui::timeline::{EmbeddedEvent, TimelineDetails};
|
||||
|
||||
use super::{content::TimelineItemContent, ProfileDetails};
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct InReplyToDetails {
|
||||
event_id: String,
|
||||
event: EmbeddedEventDetails,
|
||||
}
|
||||
|
||||
impl InReplyToDetails {
|
||||
pub(crate) fn new(event_id: String, event: EmbeddedEventDetails) -> Self {
|
||||
Self { event_id, event }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl InReplyToDetails {
|
||||
pub fn event_id(&self) -> String {
|
||||
self.event_id.clone()
|
||||
}
|
||||
|
||||
pub fn event(&self) -> EmbeddedEventDetails {
|
||||
self.event.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
|
||||
fn from(inner: matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
|
||||
Self { event_id: inner.event_id.to_string(), event: inner.event.into() }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
#[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() },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk_ui::timeline::event_type_filter::TimelineEventTypeFilter as InnerTimelineEventTypeFilter;
|
||||
use ruma::events::{AnySyncTimelineEvent, TimelineEventType};
|
||||
|
||||
use crate::event::{MessageLikeEventType, StateEventType};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct TimelineEventTypeFilter {
|
||||
inner: InnerTimelineEventTypeFilter,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl TimelineEventTypeFilter {
|
||||
#[uniffi::constructor]
|
||||
pub fn include(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
|
||||
let event_types: Vec<TimelineEventType> =
|
||||
event_types.iter().map(|t| t.clone().into()).collect();
|
||||
Arc::new(Self { inner: InnerTimelineEventTypeFilter::Include(event_types) })
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn exclude(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
|
||||
let event_types: Vec<TimelineEventType> =
|
||||
event_types.iter().map(|t| t.clone().into()).collect();
|
||||
Arc::new(Self { inner: InnerTimelineEventTypeFilter::Exclude(event_types) })
|
||||
}
|
||||
}
|
||||
|
||||
impl TimelineEventTypeFilter {
|
||||
/// Filters an [`event`] to decide whether it should be part of the timeline
|
||||
/// based on [`AnySyncTimelineEvent::event_type()`].
|
||||
pub(crate) fn filter(&self, event: &AnySyncTimelineEvent) -> bool {
|
||||
self.inner.filter(event)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum, Clone)]
|
||||
pub enum FilterTimelineEventType {
|
||||
MessageLike { event_type: MessageLikeEventType },
|
||||
State { event_type: StateEventType },
|
||||
}
|
||||
|
||||
impl From<FilterTimelineEventType> for TimelineEventType {
|
||||
fn from(value: FilterTimelineEventType) -> TimelineEventType {
|
||||
match value {
|
||||
FilterTimelineEventType::MessageLike { event_type } => {
|
||||
ruma::events::MessageLikeEventType::from(event_type).into()
|
||||
}
|
||||
FilterTimelineEventType::State { event_type } => {
|
||||
ruma::events::StateEventType::from(event_type).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,7 +166,7 @@ impl Span {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, uniffi::Enum)]
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, uniffi::Enum)]
|
||||
pub enum LogLevel {
|
||||
Error,
|
||||
Warn,
|
||||
@@ -176,7 +176,7 @@ pub enum LogLevel {
|
||||
}
|
||||
|
||||
impl LogLevel {
|
||||
fn to_tracing_level(&self) -> tracing::Level {
|
||||
fn to_tracing_level(self) -> tracing::Level {
|
||||
match self {
|
||||
LogLevel::Error => tracing::Level::ERROR,
|
||||
LogLevel::Warn => tracing::Level::WARN,
|
||||
|
||||
@@ -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::TOKIO1 as RUNTIME;
|
||||
use ruma::{MilliSecondsSinceUnixEpoch, UInt};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::runtime::get_runtime_handle;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Timestamp(u64);
|
||||
|
||||
@@ -54,7 +55,7 @@ impl<T> AsyncRuntimeDropped<T> {
|
||||
|
||||
impl<T> Drop for AsyncRuntimeDropped<T> {
|
||||
fn drop(&mut self) {
|
||||
let _guard = RUNTIME.enter();
|
||||
let _guard = get_runtime_handle().enter();
|
||||
// SAFETY: self.inner is never used again, which is the only requirement
|
||||
// for ManuallyDrop::drop to be used safely.
|
||||
unsafe {
|
||||
|
||||
@@ -3,12 +3,13 @@ use std::sync::{Arc, Mutex};
|
||||
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, RUNTIME};
|
||||
use crate::{room::Room, runtime::get_runtime_handle};
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct WidgetDriverAndHandle {
|
||||
@@ -94,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]
|
||||
@@ -140,12 +141,31 @@ impl From<EncryptionSystem> for matrix_sdk::widget::EncryptionSystem {
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the intent of showing the call.
|
||||
///
|
||||
/// This controls whether to show or skip the lobby.
|
||||
#[derive(uniffi::Enum, Clone)]
|
||||
pub enum Intent {
|
||||
/// The user wants to start a call.
|
||||
StartCall,
|
||||
/// The user wants to join an existing call.
|
||||
JoinExisting,
|
||||
}
|
||||
impl From<Intent> for matrix_sdk::widget::Intent {
|
||||
fn from(value: Intent) -> Self {
|
||||
match value {
|
||||
Intent::StartCall => Self::StartCall,
|
||||
Intent::JoinExisting => Self::JoinExisting,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Properties to create a new virtual Element Call widget.
|
||||
#[derive(uniffi::Record, Clone)]
|
||||
pub struct VirtualElementCallWidgetOptions {
|
||||
/// The url to the app.
|
||||
/// The url to the Element Call app including any `/room` path if required.
|
||||
///
|
||||
/// E.g. <https://call.element.io>, <https://call.element.dev>
|
||||
/// E.g. <https://call.element.io>, <https://call.element.dev>, <https://call.element.dev/room>
|
||||
pub element_call_url: String,
|
||||
|
||||
/// The widget id.
|
||||
@@ -188,11 +208,6 @@ pub struct VirtualElementCallWidgetOptions {
|
||||
/// Default: `false`
|
||||
pub app_prompt: Option<bool>,
|
||||
|
||||
/// Don't show the lobby and join the call immediately.
|
||||
///
|
||||
/// Default: `false`
|
||||
pub skip_lobby: Option<bool>,
|
||||
|
||||
/// Make it not possible to get to the calls list in the webview.
|
||||
///
|
||||
/// Default: `true`
|
||||
@@ -201,13 +216,43 @@ pub struct VirtualElementCallWidgetOptions {
|
||||
/// The font to use, to adapt to the system font.
|
||||
pub font: Option<String>,
|
||||
|
||||
/// Can be used to pass a PostHog id to element call.
|
||||
pub analytics_id: Option<String>,
|
||||
|
||||
/// The encryption system to use.
|
||||
///
|
||||
/// Use `EncryptionSystem::Unencrypted` to disable encryption.
|
||||
pub encryption: EncryptionSystem,
|
||||
|
||||
/// The intent of showing the call.
|
||||
/// If the user wants to start a call or join an existing one.
|
||||
/// Controls if the lobby is skipped or not.
|
||||
pub intent: Option<Intent>,
|
||||
|
||||
/// Do not show the screenshare button.
|
||||
pub hide_screensharing: bool,
|
||||
|
||||
/// Can be used to pass a PostHog id to element call.
|
||||
pub posthog_user_id: Option<String>,
|
||||
/// The host of the posthog api.
|
||||
/// Supported since Element Call v0.9.0. Only used by the embedded package.
|
||||
pub posthog_api_host: Option<String>,
|
||||
/// The key for the posthog api.
|
||||
/// Supported since Element Call v0.9.0. Only used by the embedded package.
|
||||
pub posthog_api_key: Option<String>,
|
||||
|
||||
/// The url to use for submitting rageshakes.
|
||||
/// Supported since Element Call v0.9.0. Only used by the embedded package.
|
||||
pub rageshake_submit_url: Option<String>,
|
||||
|
||||
/// Sentry [DSN](https://docs.sentry.io/concepts/key-terms/dsn-explainer/)
|
||||
/// Supported since Element Call v0.9.0. Only used by the embedded package.
|
||||
pub sentry_dsn: Option<String>,
|
||||
/// 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 {
|
||||
@@ -220,11 +265,18 @@ impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElemen
|
||||
preload: value.preload,
|
||||
font_scale: value.font_scale,
|
||||
app_prompt: value.app_prompt,
|
||||
skip_lobby: value.skip_lobby,
|
||||
confine_to_room: value.confine_to_room,
|
||||
font: value.font,
|
||||
analytics_id: value.analytics_id,
|
||||
posthog_user_id: value.posthog_user_id,
|
||||
encryption: value.encryption.into(),
|
||||
intent: value.intent.map(Into::into),
|
||||
hide_screensharing: value.hide_screensharing,
|
||||
posthog_api_host: value.posthog_api_host,
|
||||
posthog_api_key: value.posthog_api_key,
|
||||
rageshake_submit_url: value.rageshake_submit_url,
|
||||
sentry_dsn: value.sentry_dsn,
|
||||
sentry_environment: value.sentry_environment,
|
||||
controlled_media_devices: value.controlled_media_devices,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,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(),
|
||||
},
|
||||
@@ -327,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(),
|
||||
@@ -396,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.
|
||||
@@ -442,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 } => {
|
||||
@@ -459,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)) => {
|
||||
@@ -480,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,
|
||||
@@ -501,7 +564,7 @@ impl matrix_sdk::widget::CapabilitiesProvider for CapabilitiesProviderWrap {
|
||||
// This could require a prompt to the user. Ideally the callback
|
||||
// interface would just be async, but that's not supported yet so use
|
||||
// one of tokio's blocking task threads instead.
|
||||
RUNTIME
|
||||
get_runtime_handle()
|
||||
.spawn_blocking(move || this.acquire_capabilities(capabilities.into()).into())
|
||||
.await
|
||||
// propagate panics from the blocking task
|
||||
@@ -585,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."
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="250" viewBox="0 0 512 512">
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {
|
||||
rect.bg { fill: none; }
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
rect.bg { fill: #17191C; }
|
||||
}
|
||||
</style>
|
||||
<g clip-path="url(#clip0_7151_5134)">
|
||||
<rect class="bg" width="512" height="512" />
|
||||
<path d="M437.539 522.597L437.062 517.672L451.585 504.129C454.537 501.365 453.431 495.837 449.663 494.43L431.107 487.495L429.649 482.721L441.207 466.64C443.569 463.374 441.396 458.16 437.438 457.507L417.865 454.328L415.516 449.931L423.744 431.878C425.428 428.197 422.3 423.498 418.254 423.649L398.404 424.34L395.264 420.533L399.824 401.186C400.741 397.253 396.759 393.271 392.826 394.188L373.479 398.748L369.66 395.608L370.351 375.758C370.501 371.737 365.803 368.597 362.134 370.268L344.093 378.496L339.696 376.135L336.505 356.561C335.877 352.591 330.638 350.43 327.372 352.792L311.291 364.35L306.517 362.905L299.582 344.35C298.175 340.581 292.634 339.475 289.883 342.415L276.34 356.938L271.415 356.461L260.962 339.563C258.852 336.146 253.173 336.146 251.075 339.563L240.622 356.461L235.697 356.938L222.129 342.39C219.365 339.45 213.837 340.543 212.43 344.324L205.495 362.88L200.721 364.325L184.64 352.767C181.374 350.405 176.148 352.578 175.507 356.536L172.316 376.109L167.919 378.471L149.878 370.242C146.209 368.571 141.498 371.712 141.661 375.733L142.352 395.583L138.533 398.723L119.186 394.163C115.253 393.246 111.271 397.228 112.188 401.161L116.748 420.508L113.608 424.315L93.7577 423.624C89.7374 423.498 86.5966 428.172 88.2675 431.853L96.4965 449.906L94.1346 454.303L74.561 457.482C70.591 458.11 68.4301 463.349 70.792 466.615L82.3502 482.696L80.8929 487.47L62.3369 494.405C58.568 495.812 57.4624 501.353 60.4148 504.104L74.9379 517.647L74.4605 522.572L57.5629 533.025C54.1457 535.135 54.1457 540.814 57.5629 542.912L74.4605 553.365L74.9379 558.289L60.3771 571.883C57.4373 574.647 58.5303 580.175 62.2993 581.582L80.8552 588.517L82.3125 593.291L70.7543 609.372C68.405 612.638 70.5659 617.864 74.5233 618.505L94.0843 621.684L96.4462 626.081L88.2173 644.122C86.5464 647.79 89.6997 652.501 93.7074 652.351L113.557 651.66L116.698 655.479L112.138 674.826C111.221 678.746 115.203 682.741 119.135 681.811L138.483 677.251L142.302 680.392L141.611 700.242C141.46 704.262 146.159 707.403 149.828 705.732L167.868 697.503L172.266 699.865L175.457 719.426C176.085 723.408 181.324 725.557 184.59 723.22L200.671 711.637L205.445 713.094L212.38 731.65C213.787 735.419 219.328 736.524 222.079 733.572L235.622 719.049L240.547 719.551L251 736.449C253.11 739.841 258.764 739.866 260.887 736.449L271.339 719.551L276.264 719.049L289.807 733.572C292.571 736.524 298.099 735.419 299.506 731.65L306.441 713.094L311.215 711.637L327.296 723.22C330.563 725.569 335.789 723.408 336.43 719.426L339.621 699.865L344.018 697.503L362.059 705.732C365.727 707.403 370.426 704.275 370.275 700.242L369.584 680.392L373.391 677.251L392.738 681.811C396.671 682.729 400.653 678.746 399.736 674.826L395.176 655.479L398.316 651.66L418.166 652.351C422.187 652.514 425.327 647.79 423.657 644.122L415.428 626.081L417.777 621.684L437.351 618.505C441.333 617.877 443.506 612.651 441.119 609.372L429.561 593.291L431.019 588.517L449.575 581.582C453.344 580.162 454.449 574.634 451.497 571.883L436.974 558.34L437.451 553.415L454.349 542.962C457.766 540.852 457.778 535.198 454.349 533.075L437.539 522.597Z" fill="#F74C00"/>
|
||||
<ellipse cx="197.264" cy="427.256" rx="36.8364" ry="41.9173" fill="#17191C"/>
|
||||
<ellipse cx="314.125" cy="427.256" rx="36.8364" ry="41.9173" fill="#17191C"/>
|
||||
<path d="M230.597 485.777C236.497 497.197 246.672 504.739 258.235 504.739C271.41 504.739 282.782 494.948 288.083 480.787L230.597 485.777Z" fill="#17191C"/>
|
||||
<ellipse cx="210.602" cy="442.181" rx="15.8777" ry="20.6411" fill="white"/>
|
||||
<ellipse cx="326.827" cy="442.181" rx="15.8777" ry="20.6411" fill="white"/>
|
||||
<g clip-path="url(#clip1_7151_5134)">
|
||||
<path d="M186.081 188.249V206.978H186.604C191.603 199.815 197.647 194.293 204.66 190.413C211.674 186.459 219.807 184.519 228.91 184.519C237.64 184.519 245.624 186.235 252.862 189.592C260.1 192.95 265.547 198.994 269.352 207.5C273.456 201.456 279.052 196.084 286.066 191.458C293.08 186.832 301.437 184.519 311.062 184.519C318.374 184.519 325.164 185.414 331.432 187.205C337.7 188.995 342.997 191.831 347.474 195.785C351.951 199.74 355.384 204.814 357.92 211.156C360.383 217.499 361.651 225.109 361.651 234.063V326.661H323.672V248.24C323.672 243.614 323.523 239.212 323.15 235.108C322.777 231.004 321.807 227.422 320.24 224.438C318.598 221.379 316.285 218.991 313.151 217.2C310.017 215.409 305.764 214.514 300.467 214.514C295.094 214.514 290.767 215.559 287.484 217.573C284.2 219.662 281.589 222.274 279.724 225.632C277.858 228.915 276.59 232.645 275.993 236.899C275.396 241.077 275.023 245.33 275.023 249.583V326.661H237.044V249.061C237.044 244.957 236.969 240.928 236.745 236.899C236.596 232.869 235.775 229.213 234.432 225.781C233.089 222.423 230.85 219.662 227.717 217.648C224.583 215.633 220.031 214.589 213.913 214.589C212.122 214.589 209.734 214.962 206.824 215.782C203.914 216.603 201.004 218.095 198.244 220.334C195.483 222.572 193.095 225.781 191.155 229.959C189.215 234.138 188.245 239.659 188.245 246.449V326.735H150.266V188.249H186.081Z" fill="white"/>
|
||||
<path d="M72.2223 70.8792V441.121H98.86V450H62V62H98.86V70.8792H72.2223Z" fill="white"/>
|
||||
<path d="M439.785 441.121V70.8792H413.147V62H450.007V450H413.147V441.121H439.785Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7151_5134">
|
||||
<rect width="512" height="512" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_7151_5134">
|
||||
<rect width="388" height="388" fill="white" transform="translate(62 62)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
@@ -6,6 +6,53 @@ 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
|
||||
|
||||
- [**breaking**] The `Client::subscribe_to_ignore_user_list_changes()`
|
||||
method will now only trigger whenever the ignored user list has
|
||||
changed from what was previously known, instead of triggering
|
||||
every time an ignore-user-list event has been received from sync.
|
||||
([#4779](https://github.com/matrix-org/matrix-rust-sdk/pull/4779))
|
||||
- [**breaking**] The `MediaRetentionPolicy` can now trigger regular cleanups
|
||||
with its new `cleanup_frequency` setting.
|
||||
([#4603](https://github.com/matrix-org/matrix-rust-sdk/pull/4603))
|
||||
- `Clone` is a supertrait of `EventCacheStoreMedia`.
|
||||
- `EventCacheStoreMedia` has a new method `last_media_cleanup_time_inner`
|
||||
- There are new `'static` bounds in `MediaService` for the media cache stores
|
||||
- `event_cache::store::MemoryStore` implements `Clone`.
|
||||
- `BaseClient` now has a `handle_verification_events` field which is `true` by
|
||||
default and can be negated so the `NotificationClient` won't handle received
|
||||
verification events too, causing errors in the `VerificationMachine`.
|
||||
- [**breaking**] `Room::is_encryption_state_synced` has been removed
|
||||
([#4777](https://github.com/matrix-org/matrix-rust-sdk/pull/4777))
|
||||
- [**breaking**] `Room::is_encrypted` is replaced by `Room::encryption_state`
|
||||
which returns a value of the new `EncryptionState` enum
|
||||
([#4777](https://github.com/matrix-org/matrix-rust-sdk/pull/4777))
|
||||
|
||||
### Refactor
|
||||
|
||||
- [**breaking**] `BaseClient::store` is renamed `state_store`
|
||||
([#4851](https://github.com/matrix-org/matrix-rust-sdk/pull/4851))
|
||||
- [**breaking**] `BaseClient::with_store_config` is renamed `new`
|
||||
([#4847](https://github.com/matrix-org/matrix-rust-sdk/pull/4847))
|
||||
- [**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
|
||||
|
||||
### Features
|
||||
|
||||
@@ -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.10.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,55 +49,57 @@ 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",
|
||||
"unstable-msc2867",
|
||||
"unstable-msc3381",
|
||||
"unstable-msc3575",
|
||||
"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
|
||||
|
||||
+642
-1139
File diff suppressed because it is too large
Load Diff
@@ -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)]
|
||||
@@ -277,8 +289,10 @@ impl RawAnySyncOrStrippedState {
|
||||
/// Try to deserialize the inner JSON as the expected type.
|
||||
pub fn deserialize(&self) -> serde_json::Result<AnySyncOrStrippedState> {
|
||||
match self {
|
||||
Self::Sync(raw) => Ok(AnySyncOrStrippedState::Sync(raw.deserialize()?)),
|
||||
Self::Stripped(raw) => Ok(AnySyncOrStrippedState::Stripped(raw.deserialize()?)),
|
||||
Self::Sync(raw) => Ok(AnySyncOrStrippedState::Sync(Box::new(raw.deserialize()?))),
|
||||
Self::Stripped(raw) => {
|
||||
Ok(AnySyncOrStrippedState::Stripped(Box::new(raw.deserialize()?)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,9 +314,15 @@ impl RawAnySyncOrStrippedState {
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum AnySyncOrStrippedState {
|
||||
/// An event from a room in joined or left state.
|
||||
Sync(AnySyncStateEvent),
|
||||
///
|
||||
/// The value is `Box`ed because it is quite large. Let's keep the size of
|
||||
/// `Self` as small as possible.
|
||||
Sync(Box<AnySyncStateEvent>),
|
||||
/// An event from a room in invited state.
|
||||
Stripped(AnyStrippedStateEvent),
|
||||
///
|
||||
/// The value is `Box`ed because it is quite large. Let's keep the size of
|
||||
/// `Self` as small as possible.
|
||||
Stripped(Box<AnyStrippedStateEvent>),
|
||||
}
|
||||
|
||||
impl AnySyncOrStrippedState {
|
||||
|
||||
@@ -14,27 +14,34 @@
|
||||
|
||||
//! 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::{
|
||||
ChunkContent, ChunkIdentifier as CId, LinkedChunk, LinkedChunkBuilder, Position, RawChunk,
|
||||
Update,
|
||||
lazy_loader, ChunkContent, ChunkIdentifier as CId, LinkedChunkId, Position, Update,
|
||||
},
|
||||
};
|
||||
use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID};
|
||||
use ruma::{
|
||||
api::client::media::get_content_thumbnail::v3::Method, events::room::MediaSource, mxc_uri,
|
||||
push::Action, room_id, uint, RoomId,
|
||||
api::client::media::get_content_thumbnail::v3::Method,
|
||||
event_id,
|
||||
events::{
|
||||
relation::RelationType,
|
||||
room::{message::RoomMessageEventContentWithoutRelation, MediaSource},
|
||||
},
|
||||
mxc_uri,
|
||||
push::Action,
|
||||
room_id, uint, EventId, RoomId,
|
||||
};
|
||||
|
||||
use super::{media::IgnoreMediaRetentionPolicy, DynEventCacheStore};
|
||||
use crate::{
|
||||
event_cache::{Event, Gap},
|
||||
event_cache::{store::DEFAULT_CHUNK_CAPACITY, Gap},
|
||||
media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},
|
||||
};
|
||||
|
||||
@@ -43,31 +50,36 @@ use crate::{
|
||||
///
|
||||
/// Keep in sync with [`check_test_event`].
|
||||
pub fn make_test_event(room_id: &RoomId, content: &str) -> TimelineEvent {
|
||||
let encryption_info = EncryptionInfo {
|
||||
make_test_event_with_event_id(room_id, content, None)
|
||||
}
|
||||
|
||||
/// Same as [`make_test_event`], with an extra event id.
|
||||
pub fn make_test_event_with_event_id(
|
||||
room_id: &RoomId,
|
||||
content: &str,
|
||||
event_id: Option<&EventId>,
|
||||
) -> TimelineEvent {
|
||||
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,
|
||||
};
|
||||
});
|
||||
|
||||
let event = EventFactory::new()
|
||||
.text_msg(content)
|
||||
.room(room_id)
|
||||
.sender(*ALICE)
|
||||
.into_raw_timeline()
|
||||
.cast();
|
||||
|
||||
TimelineEvent {
|
||||
kind: TimelineEventKind::Decrypted(DecryptedRoomEvent {
|
||||
event,
|
||||
encryption_info,
|
||||
unsigned_encryption_info: None,
|
||||
}),
|
||||
push_actions: Some(vec![Action::Notify]),
|
||||
let mut builder = EventFactory::new().text_msg(content).room(room_id).sender(*ALICE);
|
||||
if let Some(event_id) = event_id {
|
||||
builder = builder.event_id(event_id);
|
||||
}
|
||||
let event = builder.into_raw_timeline().cast();
|
||||
|
||||
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
|
||||
@@ -77,7 +89,7 @@ pub fn make_test_event(room_id: &RoomId, content: &str) -> TimelineEvent {
|
||||
#[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);
|
||||
|
||||
@@ -100,9 +112,8 @@ pub fn check_test_event(event: &TimelineEvent, text: &str) {
|
||||
/// `EventCacheStore` integration tests.
|
||||
///
|
||||
/// This trait is not meant to be used directly, but will be used with the
|
||||
/// [`event_cache_store_integration_tests!`] macro.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
/// `event_cache_store_integration_tests!` macro.
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait EventCacheStoreIntegrationTests {
|
||||
/// Test media content storage.
|
||||
async fn test_media_content(&self);
|
||||
@@ -114,23 +125,33 @@ pub trait EventCacheStoreIntegrationTests {
|
||||
/// the store.
|
||||
async fn test_handle_updates_and_rebuild_linked_chunk(&self);
|
||||
|
||||
/// Test loading a linked chunk incrementally (chunk by chunk) from the
|
||||
/// store.
|
||||
async fn test_linked_chunk_incremental_loading(&self);
|
||||
|
||||
/// Test that rebuilding a linked chunk from an empty store doesn't return
|
||||
/// anything.
|
||||
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);
|
||||
|
||||
/// Test that filtering duplicated events works as expected.
|
||||
async fn test_filter_duplicated_events(&self);
|
||||
|
||||
/// Test that an event can be found or not.
|
||||
async fn test_find_event(&self);
|
||||
|
||||
/// Test that finding event relations works as expected.
|
||||
async fn test_find_event_relations(&self);
|
||||
|
||||
/// Test that saving an event works as expected.
|
||||
async fn test_save_event(&self);
|
||||
}
|
||||
|
||||
fn rebuild_linked_chunk(raws: Vec<RawChunk<Event, Gap>>) -> Option<LinkedChunk<3, Event, Gap>> {
|
||||
LinkedChunkBuilder::from_raw_parts(raws).build().unwrap()
|
||||
}
|
||||
|
||||
#[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");
|
||||
@@ -318,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 },
|
||||
@@ -341,7 +363,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
},
|
||||
// another items chunk
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
// new items on 0
|
||||
// new items on 2
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(2), 0),
|
||||
items: vec![make_test_event(room_id, "sup")],
|
||||
@@ -352,8 +374,11 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
.unwrap();
|
||||
|
||||
// The linked chunk is correctly reloaded.
|
||||
let raws = self.reload_linked_chunk(room_id).await.unwrap();
|
||||
let lc = rebuild_linked_chunk(raws).expect("linked chunk not empty");
|
||||
let lc = lazy_loader::from_all_chunks::<3, _, _>(
|
||||
self.load_all_chunks(linked_chunk_id).await.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let mut chunks = lc.chunks();
|
||||
|
||||
@@ -392,19 +417,241 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
assert!(chunks.next().is_none());
|
||||
}
|
||||
|
||||
async fn test_rebuild_empty_linked_chunk(&self) {
|
||||
// When I rebuild a linked chunk from an empty store, it's empty.
|
||||
let raw_parts = self.reload_linked_chunk(&DEFAULT_TEST_ROOM_ID).await.unwrap();
|
||||
assert!(rebuild_linked_chunk(raw_parts).is_none());
|
||||
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(linked_chunk_id).await.unwrap();
|
||||
|
||||
assert!(last_chunk.is_none());
|
||||
assert_eq!(chunk_identifier_generator.current(), 0);
|
||||
}
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
// new chunk for items
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![event("a"), event("b")],
|
||||
},
|
||||
// new chunk for a gap
|
||||
Update::NewGapChunk {
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "morbier".to_owned() },
|
||||
},
|
||||
// new chunk for items
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
// new items on 2
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(2), 0),
|
||||
items: vec![event("c"), event("d"), event("e")],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Load the last chunk.
|
||||
let mut linked_chunk = {
|
||||
let (last_chunk, chunk_identifier_generator) =
|
||||
self.load_last_chunk(linked_chunk_id).await.unwrap();
|
||||
|
||||
assert_eq!(chunk_identifier_generator.current(), 2);
|
||||
|
||||
let linked_chunk = lazy_loader::from_last_chunk::<DEFAULT_CHUNK_CAPACITY, _, _>(
|
||||
last_chunk,
|
||||
chunk_identifier_generator,
|
||||
)
|
||||
.unwrap() // unwrap the `Result`
|
||||
.unwrap(); // unwrap the `Option`
|
||||
|
||||
let mut rchunks = linked_chunk.rchunks();
|
||||
|
||||
// A unique chunk.
|
||||
assert_matches!(rchunks.next(), Some(chunk) => {
|
||||
assert_eq!(chunk.identifier(), 2);
|
||||
assert_eq!(chunk.lazy_previous(), Some(CId::new(1)));
|
||||
|
||||
assert_matches!(chunk.content(), ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 3);
|
||||
check_test_event(&events[0], "c");
|
||||
check_test_event(&events[1], "d");
|
||||
check_test_event(&events[2], "e");
|
||||
});
|
||||
});
|
||||
|
||||
assert!(rchunks.next().is_none());
|
||||
|
||||
linked_chunk
|
||||
};
|
||||
|
||||
// Load the previous chunk: this is a gap.
|
||||
{
|
||||
let first_chunk = linked_chunk.chunks().next().unwrap().identifier();
|
||||
let previous_chunk =
|
||||
self.load_previous_chunk(linked_chunk_id, first_chunk).await.unwrap().unwrap();
|
||||
|
||||
lazy_loader::insert_new_first_chunk(&mut linked_chunk, previous_chunk).unwrap();
|
||||
|
||||
let mut rchunks = linked_chunk.rchunks();
|
||||
|
||||
// The last chunk.
|
||||
assert_matches!(rchunks.next(), Some(chunk) => {
|
||||
assert_eq!(chunk.identifier(), 2);
|
||||
assert!(chunk.lazy_previous().is_none());
|
||||
|
||||
// Already asserted, but let's be sure nothing breaks.
|
||||
assert_matches!(chunk.content(), ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 3);
|
||||
check_test_event(&events[0], "c");
|
||||
check_test_event(&events[1], "d");
|
||||
check_test_event(&events[2], "e");
|
||||
});
|
||||
});
|
||||
|
||||
// The new chunk.
|
||||
assert_matches!(rchunks.next(), Some(chunk) => {
|
||||
assert_eq!(chunk.identifier(), 1);
|
||||
assert_eq!(chunk.lazy_previous(), Some(CId::new(0)));
|
||||
|
||||
assert_matches!(chunk.content(), ChunkContent::Gap(gap) => {
|
||||
assert_eq!(gap.prev_token, "morbier");
|
||||
});
|
||||
});
|
||||
|
||||
assert!(rchunks.next().is_none());
|
||||
}
|
||||
|
||||
// Load the previous chunk: these are items.
|
||||
{
|
||||
let first_chunk = linked_chunk.chunks().next().unwrap().identifier();
|
||||
let previous_chunk =
|
||||
self.load_previous_chunk(linked_chunk_id, first_chunk).await.unwrap().unwrap();
|
||||
|
||||
lazy_loader::insert_new_first_chunk(&mut linked_chunk, previous_chunk).unwrap();
|
||||
|
||||
let mut rchunks = linked_chunk.rchunks();
|
||||
|
||||
// The last chunk.
|
||||
assert_matches!(rchunks.next(), Some(chunk) => {
|
||||
assert_eq!(chunk.identifier(), 2);
|
||||
assert!(chunk.lazy_previous().is_none());
|
||||
|
||||
// Already asserted, but let's be sure nothing breaks.
|
||||
assert_matches!(chunk.content(), ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 3);
|
||||
check_test_event(&events[0], "c");
|
||||
check_test_event(&events[1], "d");
|
||||
check_test_event(&events[2], "e");
|
||||
});
|
||||
});
|
||||
|
||||
// Its previous chunk.
|
||||
assert_matches!(rchunks.next(), Some(chunk) => {
|
||||
assert_eq!(chunk.identifier(), 1);
|
||||
assert!(chunk.lazy_previous().is_none());
|
||||
|
||||
// Already asserted, but let's be sure nothing breaks.
|
||||
assert_matches!(chunk.content(), ChunkContent::Gap(gap) => {
|
||||
assert_eq!(gap.prev_token, "morbier");
|
||||
});
|
||||
});
|
||||
|
||||
// The new chunk.
|
||||
assert_matches!(rchunks.next(), Some(chunk) => {
|
||||
assert_eq!(chunk.identifier(), 0);
|
||||
assert!(chunk.lazy_previous().is_none());
|
||||
|
||||
assert_matches!(chunk.content(), ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 2);
|
||||
check_test_event(&events[0], "a");
|
||||
check_test_event(&events[1], "b");
|
||||
});
|
||||
});
|
||||
|
||||
assert!(rchunks.next().is_none());
|
||||
}
|
||||
|
||||
// Load the previous chunk: there is none.
|
||||
{
|
||||
let first_chunk = linked_chunk.chunks().next().unwrap().identifier();
|
||||
let previous_chunk =
|
||||
self.load_previous_chunk(linked_chunk_id, first_chunk).await.unwrap();
|
||||
|
||||
assert!(previous_chunk.is_none());
|
||||
}
|
||||
|
||||
// One last check: a round of assert by using the forwards chunk iterator
|
||||
// instead of the backwards chunk iterator.
|
||||
{
|
||||
let mut chunks = linked_chunk.chunks();
|
||||
|
||||
// The first chunk.
|
||||
assert_matches!(chunks.next(), Some(chunk) => {
|
||||
assert_eq!(chunk.identifier(), 0);
|
||||
assert!(chunk.lazy_previous().is_none());
|
||||
|
||||
assert_matches!(chunk.content(), ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 2);
|
||||
check_test_event(&events[0], "a");
|
||||
check_test_event(&events[1], "b");
|
||||
});
|
||||
});
|
||||
|
||||
// The second chunk.
|
||||
assert_matches!(chunks.next(), Some(chunk) => {
|
||||
assert_eq!(chunk.identifier(), 1);
|
||||
assert!(chunk.lazy_previous().is_none());
|
||||
|
||||
assert_matches!(chunk.content(), ChunkContent::Gap(gap) => {
|
||||
assert_eq!(gap.prev_token, "morbier");
|
||||
});
|
||||
});
|
||||
|
||||
// The third and last chunk.
|
||||
assert_matches!(chunks.next(), Some(chunk) => {
|
||||
assert_eq!(chunk.identifier(), 2);
|
||||
assert!(chunk.lazy_previous().is_none());
|
||||
|
||||
assert_matches!(chunk.content(), ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 3);
|
||||
check_test_event(&events[0], "c");
|
||||
check_test_event(&events[1], "d");
|
||||
check_test_event(&events[2], "e");
|
||||
});
|
||||
});
|
||||
|
||||
assert!(chunks.next().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
async fn test_clear_all_rooms_chunks(&self) {
|
||||
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(LinkedChunkId::Room(&DEFAULT_TEST_ROOM_ID)).await.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(linked_chunk.is_none());
|
||||
}
|
||||
|
||||
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 },
|
||||
@@ -420,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 },
|
||||
@@ -444,24 +691,42 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
.unwrap();
|
||||
|
||||
// Sanity check: both linked chunks can be reloaded.
|
||||
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r0).await.unwrap()).is_some());
|
||||
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r1).await.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!(rebuild_linked_chunk(self.reload_linked_chunk(r0).await.unwrap()).is_none());
|
||||
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r1).await.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 },
|
||||
@@ -477,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 },
|
||||
@@ -495,13 +760,275 @@ 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.reload_linked_chunk(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.reload_linked_chunk(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é");
|
||||
let event_brigand = event("brigand du jorat");
|
||||
let event_raclette = event("raclette");
|
||||
let event_morbier = event("morbier");
|
||||
let event_gruyere = event("gruyère");
|
||||
let event_tome = event("tome");
|
||||
let event_mont_dor = event("mont d'or");
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![event_comte.clone(), event_brigand.clone()],
|
||||
},
|
||||
Update::NewGapChunk {
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "brillat-savarin".to_owned() },
|
||||
},
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(2), 0),
|
||||
items: vec![event_morbier.clone(), event_mont_dor.clone()],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Add other events in another room, to ensure filtering take the `room_id` into
|
||||
// account.
|
||||
self.handle_linked_chunk_updates(
|
||||
another_linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![event_tome.clone()],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let duplicated_events = self
|
||||
.filter_duplicated_events(
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
event_comte.event_id().unwrap().to_owned(),
|
||||
event_raclette.event_id().unwrap().to_owned(),
|
||||
event_morbier.event_id().unwrap().to_owned(),
|
||||
event_gruyere.event_id().unwrap().to_owned(),
|
||||
event_tome.event_id().unwrap().to_owned(),
|
||||
event_mont_dor.event_id().unwrap().to_owned(),
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(duplicated_events.len(), 3);
|
||||
assert_eq!(
|
||||
duplicated_events[0],
|
||||
(event_comte.event_id().unwrap(), Position::new(CId::new(0), 0))
|
||||
);
|
||||
assert_eq!(
|
||||
duplicated_events[1],
|
||||
(event_morbier.event_id().unwrap(), Position::new(CId::new(2), 0))
|
||||
);
|
||||
assert_eq!(
|
||||
duplicated_events[2],
|
||||
(event_mont_dor.event_id().unwrap(), Position::new(CId::new(2), 1))
|
||||
);
|
||||
}
|
||||
|
||||
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é");
|
||||
let event_gruyere = event("gruyère");
|
||||
|
||||
// Add one event in one room.
|
||||
self.handle_linked_chunk_updates(
|
||||
LinkedChunkId::Room(room_id),
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![event_comte.clone()],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Add another event in another room.
|
||||
self.handle_linked_chunk_updates(
|
||||
another_linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![event_gruyere.clone()],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Now let's find the event.
|
||||
let event = self
|
||||
.find_event(room_id, event_comte.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.expect("failed to find an event");
|
||||
|
||||
assert_eq!(event.event_id(), event_comte.event_id());
|
||||
|
||||
// Now let's try to find an event that exists, but not in the expected room.
|
||||
assert!(self
|
||||
.find_event(room_id, event_gruyere.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.is_none());
|
||||
|
||||
// Clearing the rooms also clears the event's storage.
|
||||
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
|
||||
.expect("failed to query for finding an event")
|
||||
.is_none());
|
||||
}
|
||||
|
||||
async fn test_find_event_relations(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let another_room_id = room_id!("!r1:matrix.org");
|
||||
|
||||
let f = EventFactory::new().room(room_id).sender(*ALICE);
|
||||
|
||||
// Create event and related events for the first room.
|
||||
let eid1 = event_id!("$event1:matrix.org");
|
||||
let e1 = f.text_msg("comter").event_id(eid1).into_event();
|
||||
|
||||
let edit_eid1 = event_id!("$edit_event1:matrix.org");
|
||||
let edit_e1 = f
|
||||
.text_msg("* comté")
|
||||
.event_id(edit_eid1)
|
||||
.edit(eid1, RoomMessageEventContentWithoutRelation::text_plain("comté"))
|
||||
.into_event();
|
||||
|
||||
let reaction_eid1 = event_id!("$reaction_event1:matrix.org");
|
||||
let reaction_e1 = f.reaction(eid1, "👍").event_id(reaction_eid1).into_event();
|
||||
|
||||
let eid2 = event_id!("$event2:matrix.org");
|
||||
let e2 = f.text_msg("galette saucisse").event_id(eid2).into_event();
|
||||
|
||||
// Create events for the second room.
|
||||
let f = f.room(another_room_id);
|
||||
|
||||
let eid3 = event_id!("$event3:matrix.org");
|
||||
let e3 = f.text_msg("gruyère").event_id(eid3).into_event();
|
||||
|
||||
let reaction_eid3 = event_id!("$reaction_event3:matrix.org");
|
||||
let reaction_e3 = f.reaction(eid3, "👍").event_id(reaction_eid3).into_event();
|
||||
|
||||
// Save All The Things!
|
||||
self.save_event(room_id, e1).await.unwrap();
|
||||
self.save_event(room_id, edit_e1).await.unwrap();
|
||||
self.save_event(room_id, reaction_e1).await.unwrap();
|
||||
self.save_event(room_id, e2).await.unwrap();
|
||||
self.save_event(another_room_id, e3).await.unwrap();
|
||||
self.save_event(another_room_id, reaction_e3).await.unwrap();
|
||||
|
||||
// Finding relations without a filter returns all of them.
|
||||
let relations = self.find_event_relations(room_id, eid1, None).await.unwrap();
|
||||
assert_eq!(relations.len(), 2);
|
||||
assert!(relations.iter().any(|r| r.event_id().as_deref() == Some(edit_eid1)));
|
||||
assert!(relations.iter().any(|r| r.event_id().as_deref() == Some(reaction_eid1)));
|
||||
|
||||
// Finding relations with a filter only returns a subset.
|
||||
let relations = self
|
||||
.find_event_relations(room_id, eid1, Some(&[RelationType::Replacement]))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(relations.len(), 1);
|
||||
assert_eq!(relations[0].event_id().as_deref(), Some(edit_eid1));
|
||||
|
||||
let relations = self
|
||||
.find_event_relations(
|
||||
room_id,
|
||||
eid1,
|
||||
Some(&[RelationType::Replacement, RelationType::Annotation]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(relations.len(), 2);
|
||||
assert!(relations.iter().any(|r| r.event_id().as_deref() == Some(edit_eid1)));
|
||||
assert!(relations.iter().any(|r| r.event_id().as_deref() == Some(reaction_eid1)));
|
||||
|
||||
// We can't find relations using the wrong room.
|
||||
let relations = self
|
||||
.find_event_relations(another_room_id, eid1, Some(&[RelationType::Replacement]))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(relations.is_empty());
|
||||
}
|
||||
|
||||
async fn test_save_event(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let another_room_id = room_id!("!r1:matrix.org");
|
||||
|
||||
let event = |msg: &str| make_test_event(room_id, msg);
|
||||
let event_comte = event("comté");
|
||||
let event_gruyere = event("gruyère");
|
||||
|
||||
// Add one event in one room.
|
||||
self.save_event(room_id, event_comte.clone()).await.unwrap();
|
||||
|
||||
// Add another event in another room.
|
||||
self.save_event(another_room_id, event_gruyere.clone()).await.unwrap();
|
||||
|
||||
// Events can be found, when searched in their own rooms.
|
||||
let event = self
|
||||
.find_event(room_id, event_comte.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.expect("failed to find an event");
|
||||
assert_eq!(event.event_id(), event_comte.event_id());
|
||||
|
||||
let event = self
|
||||
.find_event(another_room_id, event_gruyere.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.expect("failed to find an event");
|
||||
assert_eq!(event.event_id(), event_gruyere.event_id());
|
||||
|
||||
// But they won't be returned when searching in the wrong room.
|
||||
assert!(self
|
||||
.find_event(another_room_id, event_comte.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.is_none());
|
||||
assert!(self
|
||||
.find_event(room_id, event_gruyere.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
/// Macro building to allow your `EventCacheStore` implementation to run the
|
||||
@@ -564,6 +1091,13 @@ macro_rules! event_cache_store_integration_tests {
|
||||
event_cache_store.test_handle_updates_and_rebuild_linked_chunk().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_linked_chunk_incremental_loading() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_linked_chunk_incremental_loading().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_rebuild_empty_linked_chunk() {
|
||||
let event_cache_store =
|
||||
@@ -572,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]
|
||||
@@ -584,6 +1118,34 @@ macro_rules! event_cache_store_integration_tests {
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_remove_room().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_filter_duplicated_events() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_filter_duplicated_events().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_find_event() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_find_event().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_find_event_relations() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_find_event_relations().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_save_event() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_save_event().await;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -594,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,
|
||||
@@ -30,9 +29,8 @@ use crate::media::{MediaFormat, MediaRequestParameters};
|
||||
/// [`EventCacheStoreMedia`] integration tests.
|
||||
///
|
||||
/// 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)]
|
||||
/// `event_cache_store_media_integration_tests!` macro.
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait EventCacheStoreMediaIntegrationTests {
|
||||
/// Test media retention policy storage.
|
||||
async fn test_store_media_retention_policy(&self);
|
||||
@@ -53,10 +51,11 @@ pub trait EventCacheStoreMediaIntegrationTests {
|
||||
/// Test [`IgnoreMediaRetentionPolicy`] with the media content's retention
|
||||
/// policy expiry.
|
||||
async fn test_media_ignore_expiry(&self);
|
||||
|
||||
/// Test last media cleanup time storage.
|
||||
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,
|
||||
@@ -941,6 +940,25 @@ where
|
||||
let stored = self.get_media_content_inner(&request_5, time).await.unwrap();
|
||||
assert!(stored.is_none());
|
||||
}
|
||||
|
||||
async fn test_store_last_media_cleanup_time(&self) {
|
||||
let initial = self.last_media_cleanup_time_inner().await.unwrap();
|
||||
let new_time = initial.unwrap_or_else(SystemTime::now) + Duration::from_secs(60);
|
||||
|
||||
// With an empty policy.
|
||||
let policy = MediaRetentionPolicy::empty();
|
||||
self.clean_up_media_cache_inner(policy, new_time).await.unwrap();
|
||||
|
||||
let stored = self.last_media_cleanup_time_inner().await.unwrap();
|
||||
assert_eq!(stored, initial);
|
||||
|
||||
// With the default policy.
|
||||
let policy = MediaRetentionPolicy::default();
|
||||
self.clean_up_media_cache_inner(policy, new_time).await.unwrap();
|
||||
|
||||
let stored = self.last_media_cleanup_time_inner().await.unwrap();
|
||||
assert_eq!(stored, Some(new_time));
|
||||
}
|
||||
}
|
||||
|
||||
/// Macro building to allow your [`EventCacheStoreMedia`] implementation to run
|
||||
@@ -1031,5 +1049,11 @@ macro_rules! event_cache_store_media_integration_tests {
|
||||
let event_cache_store_media = get_event_cache_store().await.unwrap();
|
||||
event_cache_store_media.test_media_ignore_expiry().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_store_last_media_cleanup_time() {
|
||||
let event_cache_store_media = get_event_cache_store().await.unwrap();
|
||||
event_cache_store_media.test_store_last_media_cleanup_time().await;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ use serde::{Deserialize, Serialize};
|
||||
///
|
||||
/// [`EventCacheStore`]: crate::event_cache::store::EventCacheStore
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
|
||||
#[non_exhaustive]
|
||||
pub struct MediaRetentionPolicy {
|
||||
/// The maximum authorized size of the overall media cache, in bytes.
|
||||
@@ -50,7 +51,7 @@ pub struct MediaRetentionPolicy {
|
||||
///
|
||||
/// Defaults to 400 MiB.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_cache_size: Option<usize>,
|
||||
pub max_cache_size: Option<u64>,
|
||||
|
||||
/// The maximum authorized size of a single media content, in bytes.
|
||||
///
|
||||
@@ -68,7 +69,7 @@ pub struct MediaRetentionPolicy {
|
||||
///
|
||||
/// Defaults to 20 MiB.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_file_size: Option<usize>,
|
||||
pub max_file_size: Option<u64>,
|
||||
|
||||
/// The duration after which unaccessed media content is considered
|
||||
/// expired.
|
||||
@@ -79,6 +80,17 @@ pub struct MediaRetentionPolicy {
|
||||
/// Defaults to 60 days.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub last_access_expiry: Option<Duration>,
|
||||
|
||||
/// The duration between two automatic media cache cleanups.
|
||||
///
|
||||
/// If this is set, a cleanup will be triggered after the given duration
|
||||
/// is elapsed, at the next call to the media cache API. If this is set to
|
||||
/// zero, each call to the media cache API will trigger a cleanup. If this
|
||||
/// is `None`, cleanups will only occur if they are triggered manually.
|
||||
///
|
||||
/// Defaults to running cleanups daily.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cleanup_frequency: Option<Duration>,
|
||||
}
|
||||
|
||||
impl MediaRetentionPolicy {
|
||||
@@ -91,17 +103,22 @@ impl MediaRetentionPolicy {
|
||||
///
|
||||
/// This means that all media will be cached and cleanups have no effect.
|
||||
pub fn empty() -> Self {
|
||||
Self { max_cache_size: None, max_file_size: None, last_access_expiry: None }
|
||||
Self {
|
||||
max_cache_size: None,
|
||||
max_file_size: None,
|
||||
last_access_expiry: None,
|
||||
cleanup_frequency: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the maximum authorized size of the overall media cache, in bytes.
|
||||
pub fn with_max_cache_size(mut self, size: Option<usize>) -> Self {
|
||||
pub fn with_max_cache_size(mut self, size: Option<u64>) -> Self {
|
||||
self.max_cache_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the maximum authorized size of a single media content, in bytes.
|
||||
pub fn with_max_file_size(mut self, size: Option<usize>) -> Self {
|
||||
pub fn with_max_file_size(mut self, size: Option<u64>) -> Self {
|
||||
self.max_file_size = size;
|
||||
self
|
||||
}
|
||||
@@ -113,6 +130,12 @@ impl MediaRetentionPolicy {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the duration between two automatic media cache cleanups.
|
||||
pub fn with_cleanup_frequency(mut self, duration: Option<Duration>) -> Self {
|
||||
self.cleanup_frequency = duration;
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether this policy has limitations.
|
||||
///
|
||||
/// If this policy has no limitations, a cleanup job would have no effect.
|
||||
@@ -130,7 +153,7 @@ impl MediaRetentionPolicy {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `size` - The overall size of the media cache to check, in bytes.
|
||||
pub fn exceeds_max_cache_size(&self, size: usize) -> bool {
|
||||
pub fn exceeds_max_cache_size(&self, size: u64) -> bool {
|
||||
self.max_cache_size.is_some_and(|max_size| size > max_size)
|
||||
}
|
||||
|
||||
@@ -138,7 +161,7 @@ impl MediaRetentionPolicy {
|
||||
/// bytes.
|
||||
///
|
||||
/// This is the lowest value between `max_cache_size` and `max_file_size`.
|
||||
pub fn computed_max_file_size(&self) -> Option<usize> {
|
||||
pub fn computed_max_file_size(&self) -> Option<u64> {
|
||||
match (self.max_cache_size, self.max_file_size) {
|
||||
(None, None) => None,
|
||||
(None, Some(size)) => Some(size),
|
||||
@@ -153,7 +176,7 @@ impl MediaRetentionPolicy {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `size` - The size of the media content to check, in bytes.
|
||||
pub fn exceeds_max_file_size(&self, size: usize) -> bool {
|
||||
pub fn exceeds_max_file_size(&self, size: u64) -> bool {
|
||||
self.computed_max_file_size().is_some_and(|max_size| size > max_size)
|
||||
}
|
||||
|
||||
@@ -178,6 +201,24 @@ impl MediaRetentionPolicy {
|
||||
.is_ok_and(|elapsed| elapsed >= max_duration)
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether an automatic media cache cleanup should be triggered given the
|
||||
/// time of the last cleanup.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `current_time` - The current time.
|
||||
///
|
||||
/// * `last_cleanup_time` - The time of the last media cache cleanup.
|
||||
pub fn should_clean_up(&self, current_time: SystemTime, last_cleanup_time: SystemTime) -> bool {
|
||||
self.cleanup_frequency.is_some_and(|max_duration| {
|
||||
current_time
|
||||
.duration_since(last_cleanup_time)
|
||||
// If this returns an error, the last cleanup time is newer than the current time.
|
||||
// This shouldn't happen but in this case no cleanup job is needed.
|
||||
.is_ok_and(|elapsed| elapsed >= max_duration)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MediaRetentionPolicy {
|
||||
@@ -189,6 +230,8 @@ impl Default for MediaRetentionPolicy {
|
||||
max_file_size: Some(20 * 1024 * 1024),
|
||||
// 60 days.
|
||||
last_access_expiry: Some(Duration::from_secs(60 * 24 * 60 * 60)),
|
||||
// 1 day.
|
||||
cleanup_frequency: Some(Duration::from_secs(24 * 60 * 60)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -327,4 +370,36 @@ mod tests {
|
||||
assert!(policy.has_content_expired(epoch_plus_60, last_access_time));
|
||||
assert!(policy.has_content_expired(epoch_plus_120, last_access_time));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_media_retention_policy_cleanup_frequency() {
|
||||
let epoch = SystemTime::UNIX_EPOCH;
|
||||
let epoch_plus_60 = epoch + Duration::from_secs(60);
|
||||
let epoch_plus_120 = epoch + Duration::from_secs(120);
|
||||
|
||||
let mut policy = MediaRetentionPolicy::empty();
|
||||
assert!(!policy.should_clean_up(epoch_plus_60, epoch));
|
||||
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_60));
|
||||
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
|
||||
|
||||
policy = policy.with_cleanup_frequency(Some(Duration::from_secs(0)));
|
||||
assert!(policy.should_clean_up(epoch_plus_60, epoch));
|
||||
assert!(policy.should_clean_up(epoch_plus_60, epoch_plus_60));
|
||||
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
|
||||
|
||||
policy = policy.with_cleanup_frequency(Some(Duration::from_secs(30)));
|
||||
assert!(policy.should_clean_up(epoch_plus_60, epoch));
|
||||
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_60));
|
||||
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
|
||||
|
||||
policy = policy.with_cleanup_frequency(Some(Duration::from_secs(60)));
|
||||
assert!(policy.should_clean_up(epoch_plus_60, epoch));
|
||||
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_60));
|
||||
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
|
||||
|
||||
policy = policy.with_cleanup_frequency(Some(Duration::from_secs(90)));
|
||||
assert!(!policy.should_clean_up(epoch_plus_60, epoch));
|
||||
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_60));
|
||||
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,17 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::fmt;
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{locks::Mutex, AsyncTraitDeps};
|
||||
use matrix_sdk_common::{
|
||||
executor::{spawn, JoinHandle},
|
||||
locks::Mutex,
|
||||
AsyncTraitDeps, SendOutsideWasm, SyncOutsideWasm,
|
||||
};
|
||||
use ruma::{time::SystemTime, MxcUri};
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
use tracing::error;
|
||||
|
||||
use super::MediaRetentionPolicy;
|
||||
use crate::{event_cache::store::EventCacheStoreError, media::MediaRequestParameters};
|
||||
@@ -28,6 +33,11 @@ use crate::{event_cache::store::EventCacheStoreError, media::MediaRequestParamet
|
||||
/// [`EventCacheStore`]: crate::event_cache::store::EventCacheStore
|
||||
#[derive(Debug)]
|
||||
pub struct MediaService<Time: TimeProvider = DefaultTimeProvider> {
|
||||
inner: Arc<MediaServiceInner<Time>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MediaServiceInner<Time: TimeProvider = DefaultTimeProvider> {
|
||||
/// The time provider.
|
||||
time_provider: Time,
|
||||
|
||||
@@ -36,6 +46,15 @@ pub struct MediaService<Time: TimeProvider = DefaultTimeProvider> {
|
||||
|
||||
/// A mutex to ensure a single cleanup is running at a time.
|
||||
cleanup_guard: AsyncMutex<()>,
|
||||
|
||||
/// The time of the last media cache cleanup.
|
||||
last_media_cleanup_time: Mutex<Option<SystemTime>>,
|
||||
|
||||
/// The [`JoinHandle`] for an automatic media cleanup task.
|
||||
///
|
||||
/// Used to ensure that only one automatic cleanup is running at a time, and
|
||||
/// to stop the cleanup when the [`MediaServiceInner`] is dropped.
|
||||
automatic_media_cleanup_join_handle: Mutex<Option<JoinHandle<()>>>,
|
||||
}
|
||||
|
||||
impl MediaService {
|
||||
@@ -56,16 +75,20 @@ impl Default for MediaService {
|
||||
|
||||
impl<Time> MediaService<Time>
|
||||
where
|
||||
Time: TimeProvider,
|
||||
Time: TimeProvider + 'static,
|
||||
{
|
||||
/// Construct a new `MediaService` with the given `TimeProvider` and an
|
||||
/// empty `MediaRetentionPolicy`.
|
||||
fn with_time_provider(time_provider: Time) -> Self {
|
||||
Self {
|
||||
let inner = MediaServiceInner {
|
||||
time_provider,
|
||||
policy: Mutex::new(MediaRetentionPolicy::empty()),
|
||||
cleanup_guard: AsyncMutex::new(()),
|
||||
}
|
||||
last_media_cleanup_time: Mutex::new(None),
|
||||
automatic_media_cleanup_join_handle: Mutex::new(None),
|
||||
};
|
||||
|
||||
Self { inner: Arc::new(inner) }
|
||||
}
|
||||
|
||||
/// Restore the previous state of the [`MediaRetentionPolicy`] from data
|
||||
@@ -76,10 +99,23 @@ where
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `policy` - The `MediaRetentionPolicy` that was persisted in the store.
|
||||
pub fn restore(&self, policy: Option<MediaRetentionPolicy>) {
|
||||
pub fn restore(
|
||||
&self,
|
||||
policy: Option<MediaRetentionPolicy>,
|
||||
last_media_cleanup_time: Option<SystemTime>,
|
||||
) {
|
||||
if let Some(policy) = policy {
|
||||
*self.policy.lock() = policy;
|
||||
*self.inner.policy.lock() = policy;
|
||||
}
|
||||
|
||||
if let Some(time) = last_media_cleanup_time {
|
||||
*self.inner.last_media_cleanup_time.lock() = Some(time);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current time from the inner [`TimeProvider`].
|
||||
fn now(&self) -> SystemTime {
|
||||
self.inner.time_provider.now()
|
||||
}
|
||||
|
||||
/// Set the `MediaRetentionPolicy` of this service.
|
||||
@@ -89,21 +125,23 @@ where
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
///
|
||||
/// * `policy` - The `MediaRetentionPolicy` to use.
|
||||
pub async fn set_media_retention_policy<Store: EventCacheStoreMedia>(
|
||||
pub async fn set_media_retention_policy<Store: EventCacheStoreMedia + 'static>(
|
||||
&self,
|
||||
store: &Store,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Store::Error> {
|
||||
store.set_media_retention_policy_inner(policy).await?;
|
||||
|
||||
*self.policy.lock() = policy;
|
||||
*self.inner.policy.lock() = policy;
|
||||
|
||||
self.maybe_spawn_automatic_media_cache_cleanup(store, self.now());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the `MediaRetentionPolicy` of this service.
|
||||
pub fn media_retention_policy(&self) -> MediaRetentionPolicy {
|
||||
*self.policy.lock()
|
||||
*self.inner.policy.lock()
|
||||
}
|
||||
|
||||
/// Add a media file's content in the media store.
|
||||
@@ -118,7 +156,7 @@ where
|
||||
///
|
||||
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
|
||||
/// ignored.
|
||||
pub async fn add_media_content<Store: EventCacheStoreMedia>(
|
||||
pub async fn add_media_content<Store: EventCacheStoreMedia + 'static>(
|
||||
&self,
|
||||
store: &Store,
|
||||
request: &MediaRequestParameters,
|
||||
@@ -128,21 +166,20 @@ where
|
||||
let policy = self.media_retention_policy();
|
||||
|
||||
if ignore_policy == IgnoreMediaRetentionPolicy::No
|
||||
&& policy.exceeds_max_file_size(content.len())
|
||||
&& policy.exceeds_max_file_size(content.len() as u64)
|
||||
{
|
||||
// We do not cache the content.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let current_time = self.now();
|
||||
store
|
||||
.add_media_content_inner(
|
||||
request,
|
||||
content,
|
||||
self.time_provider.now(),
|
||||
policy,
|
||||
ignore_policy,
|
||||
)
|
||||
.await
|
||||
.add_media_content_inner(request, content, current_time, policy, ignore_policy)
|
||||
.await?;
|
||||
|
||||
self.maybe_spawn_automatic_media_cache_cleanup(store, current_time);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set whether the current [`MediaRetentionPolicy`] should be ignored for
|
||||
@@ -174,12 +211,17 @@ where
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
pub async fn get_media_content<Store: EventCacheStoreMedia>(
|
||||
pub async fn get_media_content<Store: EventCacheStoreMedia + 'static>(
|
||||
&self,
|
||||
store: &Store,
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<Option<Vec<u8>>, Store::Error> {
|
||||
store.get_media_content_inner(request, self.time_provider.now()).await
|
||||
let current_time = self.now();
|
||||
let content = store.get_media_content_inner(request, current_time).await?;
|
||||
|
||||
self.maybe_spawn_automatic_media_cache_cleanup(store, current_time);
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
/// Get a media file's content associated to an `MxcUri` from the
|
||||
@@ -190,12 +232,17 @@ where
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
///
|
||||
/// * `uri` - The `MxcUri` of the media file.
|
||||
pub async fn get_media_content_for_uri<Store: EventCacheStoreMedia>(
|
||||
pub async fn get_media_content_for_uri<Store: EventCacheStoreMedia + 'static>(
|
||||
&self,
|
||||
store: &Store,
|
||||
uri: &MxcUri,
|
||||
) -> Result<Option<Vec<u8>>, Store::Error> {
|
||||
store.get_media_content_for_uri_inner(uri, self.time_provider.now()).await
|
||||
let current_time = self.now();
|
||||
let content = store.get_media_content_for_uri_inner(uri, current_time).await?;
|
||||
|
||||
self.maybe_spawn_automatic_media_cache_cleanup(store, current_time);
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
/// Clean up the media cache with the current `MediaRetentionPolicy`.
|
||||
@@ -209,7 +256,15 @@ where
|
||||
&self,
|
||||
store: &Store,
|
||||
) -> Result<(), Store::Error> {
|
||||
let Ok(_guard) = self.cleanup_guard.try_lock() else {
|
||||
self.clean_up_media_cache_inner(store, self.now()).await
|
||||
}
|
||||
|
||||
async fn clean_up_media_cache_inner<Store: EventCacheStoreMedia>(
|
||||
&self,
|
||||
store: &Store,
|
||||
current_time: SystemTime,
|
||||
) -> Result<(), Store::Error> {
|
||||
let Ok(_guard) = self.inner.cleanup_guard.try_lock() else {
|
||||
// There is another ongoing cleanup.
|
||||
return Ok(());
|
||||
};
|
||||
@@ -221,7 +276,76 @@ where
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
store.clean_up_media_cache_inner(policy, self.time_provider.now()).await
|
||||
store.clean_up_media_cache_inner(policy, current_time).await?;
|
||||
|
||||
*self.inner.last_media_cleanup_time.lock() = Some(current_time);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Spawn an automatic media cache cleanup, according to the media retention
|
||||
/// policy.
|
||||
///
|
||||
/// A cleanup will be spawned if:
|
||||
/// * The media retention policy's `cleanup_frequency` is set and enough
|
||||
/// time has passed since the last cleanup.
|
||||
/// * No other cleanup is running,
|
||||
fn maybe_spawn_automatic_media_cache_cleanup<Store: EventCacheStoreMedia + 'static>(
|
||||
&self,
|
||||
store: &Store,
|
||||
current_time: SystemTime,
|
||||
) {
|
||||
let mut join_handle = self.inner.automatic_media_cleanup_join_handle.lock();
|
||||
|
||||
if join_handle.as_ref().is_some_and(|join_handle| !join_handle.is_finished()) {
|
||||
// There is an ongoing automatic media cache cleanup.
|
||||
return;
|
||||
}
|
||||
|
||||
let policy = self.media_retention_policy();
|
||||
if policy.cleanup_frequency.is_none() || !policy.has_limitations() {
|
||||
// Automatic cleanups are disabled or have no effect.
|
||||
return;
|
||||
}
|
||||
|
||||
let last_media_cleanup_time = *self.inner.last_media_cleanup_time.lock();
|
||||
if last_media_cleanup_time.is_some_and(|last_cleanup_time| {
|
||||
!policy.should_clean_up(current_time, last_cleanup_time)
|
||||
}) {
|
||||
// It is not time to clean up.
|
||||
return;
|
||||
}
|
||||
|
||||
let this = self.clone();
|
||||
let store = store.clone();
|
||||
|
||||
let handle = spawn(async move {
|
||||
if let Err(error) = this.clean_up_media_cache_inner(&store, current_time).await {
|
||||
error!("Failed to run automatic media cache cleanup: {error}");
|
||||
}
|
||||
});
|
||||
|
||||
*join_handle = Some(handle);
|
||||
}
|
||||
}
|
||||
|
||||
impl<Time> Clone for MediaService<Time>
|
||||
where
|
||||
Time: TimeProvider,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self { inner: self.inner.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<Time> Drop for MediaServiceInner<Time>
|
||||
where
|
||||
Time: TimeProvider,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
if let Some(join_handle) = self.automatic_media_cleanup_join_handle.lock().take() {
|
||||
join_handle.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,11 +356,11 @@ 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)]
|
||||
pub trait EventCacheStoreMedia: AsyncTraitDeps {
|
||||
#[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 + Into<EventCacheStoreError>;
|
||||
type Error: fmt::Debug + fmt::Display + Into<EventCacheStoreError>;
|
||||
|
||||
/// The persisted media retention policy in the media cache.
|
||||
async fn media_retention_policy_inner(
|
||||
@@ -340,12 +464,15 @@ pub trait EventCacheStoreMedia: AsyncTraitDeps {
|
||||
/// `cleanup_frequency` will be ignored.
|
||||
///
|
||||
/// * `current_time` - The current time, to be used to check for expired
|
||||
/// content.
|
||||
/// content and to be stored as the time of the last media cache cleanup.
|
||||
async fn clean_up_media_cache_inner(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
current_time: SystemTime,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// The time of the last media cache cleanup.
|
||||
async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error>;
|
||||
}
|
||||
|
||||
/// Whether the [`MediaRetentionPolicy`] should be ignored for the current
|
||||
@@ -385,7 +512,7 @@ impl IgnoreMediaRetentionPolicy {
|
||||
|
||||
/// An abstract trait to provide the current `SystemTime` for the
|
||||
/// [`MediaService`].
|
||||
pub trait TimeProvider {
|
||||
pub trait TimeProvider: SendOutsideWasm + SyncOutsideWasm {
|
||||
/// The current time.
|
||||
fn now(&self) -> SystemTime;
|
||||
}
|
||||
@@ -402,7 +529,10 @@ impl TimeProvider for DefaultTimeProvider {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fmt, sync::MutexGuard};
|
||||
use std::{
|
||||
fmt,
|
||||
sync::{Arc, MutexGuard},
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::locks::Mutex;
|
||||
@@ -420,9 +550,9 @@ mod tests {
|
||||
media::{MediaFormat, MediaRequestParameters, UniqueKey},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct MockEventCacheStoreMedia {
|
||||
inner: Mutex<MockEventCacheStoreMediaInner>,
|
||||
inner: Arc<Mutex<MockEventCacheStoreMediaInner>>,
|
||||
}
|
||||
|
||||
impl MockEventCacheStoreMedia {
|
||||
@@ -500,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;
|
||||
|
||||
@@ -529,7 +659,7 @@ mod tests {
|
||||
) -> Result<(), Self::Error> {
|
||||
let ignore_policy = ignore_policy.is_yes();
|
||||
|
||||
if !ignore_policy && policy.exceeds_max_file_size(content.len()) {
|
||||
if !ignore_policy && policy.exceeds_max_file_size(content.len() as u64) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -617,6 +747,10 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error> {
|
||||
Ok(self.inner().cleanup_time)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -658,7 +792,7 @@ mod tests {
|
||||
|
||||
// By default an empty policy is used.
|
||||
assert!(!service.media_retention_policy().has_limitations());
|
||||
service.restore(None);
|
||||
service.restore(None, None);
|
||||
assert!(!service.media_retention_policy().has_limitations());
|
||||
assert!(!store.accessed());
|
||||
|
||||
@@ -676,7 +810,7 @@ mod tests {
|
||||
assert_eq!(media_content.last_access, now);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
service.inner.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
// Get media from request.
|
||||
@@ -689,7 +823,7 @@ mod tests {
|
||||
assert_eq!(media.last_access, now);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
service.inner.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
// Get media from URI.
|
||||
@@ -712,12 +846,12 @@ mod tests {
|
||||
assert!(media_content.ignore_policy);
|
||||
|
||||
// Try a cleanup. With the empty policy the store should not be accessed.
|
||||
assert_eq!(store.inner().cleanup_time, None);
|
||||
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), None);
|
||||
store.reset_accessed();
|
||||
|
||||
service.clean_up_media_cache(&store).await.unwrap();
|
||||
assert!(!store.accessed());
|
||||
assert_eq!(store.inner().cleanup_time, None);
|
||||
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), None);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
@@ -747,7 +881,7 @@ mod tests {
|
||||
let service = MediaService::with_time_provider(MockTimeProvider::new(now));
|
||||
|
||||
// Check that restoring the policy works.
|
||||
service.restore(Some(MediaRetentionPolicy::default()));
|
||||
service.restore(Some(MediaRetentionPolicy::default()), None);
|
||||
assert_eq!(service.media_retention_policy(), MediaRetentionPolicy::default());
|
||||
assert!(!store.accessed());
|
||||
|
||||
@@ -779,7 +913,7 @@ mod tests {
|
||||
assert_eq!(media_content.last_access, now);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
service.inner.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
// Get media from request.
|
||||
@@ -792,7 +926,7 @@ mod tests {
|
||||
assert_eq!(media.last_access, now);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
service.inner.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
// Get media from URI.
|
||||
@@ -805,7 +939,7 @@ mod tests {
|
||||
assert_eq!(media.last_access, now);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
service.inner.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
// Add big media, it will not work because it is bigger than the max file size.
|
||||
@@ -858,7 +992,7 @@ mod tests {
|
||||
assert_eq!(media.last_access, now);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
service.inner.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
// Get media from URI.
|
||||
@@ -871,14 +1005,101 @@ mod tests {
|
||||
assert_eq!(media.last_access, now);
|
||||
|
||||
// Try a cleanup, the store should be accessed.
|
||||
assert_eq!(store.inner().cleanup_time, None);
|
||||
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), None);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
service.inner.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
service.clean_up_media_cache(&store).await.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(store.inner().cleanup_time, Some(now));
|
||||
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), Some(now));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_media_service_automatic_cleanup() {
|
||||
// 64 bytes content.
|
||||
let content = vec![0; 64];
|
||||
|
||||
let uri_1 = mxc_uri!("mxc://localhost/media-1");
|
||||
let request_1 = MediaRequestParameters {
|
||||
source: MediaSource::Plain(uri_1.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
let uri_2 = mxc_uri!("mxc://localhost/media-2");
|
||||
let request_2 = MediaRequestParameters {
|
||||
source: MediaSource::Plain(uri_2.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
|
||||
let now = SystemTime::UNIX_EPOCH;
|
||||
|
||||
let store = MockEventCacheStoreMedia::default();
|
||||
let service = MediaService::with_time_provider(MockTimeProvider::new(now));
|
||||
|
||||
// Set an empty policy.
|
||||
let policy = MediaRetentionPolicy::empty();
|
||||
service.set_media_retention_policy(&store, policy).await.unwrap();
|
||||
|
||||
// Add the contents.
|
||||
service
|
||||
.add_media_content(&store, &request_1, content.clone(), IgnoreMediaRetentionPolicy::No)
|
||||
.await
|
||||
.unwrap();
|
||||
service
|
||||
.add_media_content(&store, &request_2, content, IgnoreMediaRetentionPolicy::No)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(service.inner.automatic_media_cleanup_join_handle.lock().is_none());
|
||||
|
||||
// Try to launch an automatic cleanup.
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.inner.time_provider.set_now(now);
|
||||
service.maybe_spawn_automatic_media_cache_cleanup(&store, now);
|
||||
|
||||
// No cleanup was spawned since automatic cleanups are disabled.
|
||||
assert!(service.inner.automatic_media_cleanup_join_handle.lock().is_none());
|
||||
|
||||
// Set a policy with automatic cleanup every hour.
|
||||
let policy = MediaRetentionPolicy::empty()
|
||||
.with_cleanup_frequency(Some(Duration::from_secs(60 * 60)));
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.inner.time_provider.set_now(now);
|
||||
service.set_media_retention_policy(&store, policy).await.unwrap();
|
||||
|
||||
// No cleanup was spawned since the policy has no limitations.
|
||||
assert!(service.inner.automatic_media_cleanup_join_handle.lock().is_none());
|
||||
|
||||
// Set a policy with automatic cleanup every hour and a max file size.
|
||||
let policy = MediaRetentionPolicy::empty()
|
||||
.with_cleanup_frequency(Some(Duration::from_secs(60 * 60)))
|
||||
.with_max_file_size(Some(512));
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.inner.time_provider.set_now(now);
|
||||
service.set_media_retention_policy(&store, policy).await.unwrap();
|
||||
|
||||
// A cleanup was spawned since there was no last_media_cleanup_time.
|
||||
let join_handle = service.inner.automatic_media_cleanup_join_handle.lock().take().unwrap();
|
||||
join_handle.await.unwrap();
|
||||
|
||||
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), Some(now));
|
||||
|
||||
// Try again one minute in the future, nothing is spawned because we need to
|
||||
// wait for one hour.
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.inner.time_provider.set_now(now);
|
||||
service.get_media_content(&store, &request_1).await.unwrap();
|
||||
|
||||
assert!(service.inner.automatic_media_cleanup_join_handle.lock().is_none());
|
||||
|
||||
// Try again 2 hours in the future, another cleanup is spawned.
|
||||
let now = now + Duration::from_secs(2 * 60 * 60);
|
||||
service.inner.time_provider.set_now(now);
|
||||
service.get_media_content_for_uri(&store, uri_1).await.unwrap();
|
||||
|
||||
let join_handle = service.inner.automatic_media_cleanup_join_handle.lock().take().unwrap();
|
||||
join_handle.await.unwrap();
|
||||
|
||||
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), Some(now));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,20 +12,30 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
num::NonZeroUsize,
|
||||
sync::{Arc, RwLock as StdRwLock},
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
linked_chunk::{relational::RelationalLinkedChunk, RawChunk, Update},
|
||||
linked_chunk::{
|
||||
relational::RelationalLinkedChunk, ChunkIdentifier, ChunkIdentifierGenerator,
|
||||
LinkedChunkId, Position, RawChunk, Update,
|
||||
},
|
||||
ring_buffer::RingBuffer,
|
||||
store_locks::memory_store_helper::try_take_leased_lock,
|
||||
};
|
||||
use ruma::{
|
||||
events::relation::RelationType,
|
||||
time::{Instant, SystemTime},
|
||||
MxcUri, OwnedMxcUri, RoomId,
|
||||
EventId, MxcUri, OwnedEventId, OwnedMxcUri, RoomId,
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
use super::{
|
||||
compute_filters_string, extract_event_relation,
|
||||
media::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService},
|
||||
EventCacheStore, EventCacheStoreError, Result,
|
||||
};
|
||||
@@ -37,10 +47,9 @@ use crate::{
|
||||
/// In-memory, non-persistent implementation of the `EventCacheStore`.
|
||||
///
|
||||
/// Default if no other is configured at startup.
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MemoryStore {
|
||||
inner: StdRwLock<MemoryStoreInner>,
|
||||
inner: Arc<StdRwLock<MemoryStoreInner>>,
|
||||
media_service: MediaService,
|
||||
}
|
||||
|
||||
@@ -48,8 +57,9 @@ pub struct MemoryStore {
|
||||
struct MemoryStoreInner {
|
||||
media: RingBuffer<MediaContent>,
|
||||
leases: HashMap<String, (String, Instant)>,
|
||||
events: RelationalLinkedChunk<Event, Gap>,
|
||||
events: RelationalLinkedChunk<OwnedEventId, Event, Gap>,
|
||||
media_retention_policy: Option<MediaRetentionPolicy>,
|
||||
last_media_cleanup_time: SystemTime,
|
||||
}
|
||||
|
||||
/// A media content in the `MemoryStore`.
|
||||
@@ -71,20 +81,24 @@ struct MediaContent {
|
||||
last_access: SystemTime,
|
||||
}
|
||||
|
||||
// SAFETY: `new_unchecked` is safe because 20 is not zero.
|
||||
const NUMBER_OF_MEDIAS: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(20) };
|
||||
const NUMBER_OF_MEDIAS: NonZeroUsize = NonZeroUsize::new(20).unwrap();
|
||||
|
||||
impl Default for MemoryStore {
|
||||
fn default() -> Self {
|
||||
// Given that the store is empty, we won't need to clean it up right away.
|
||||
let last_media_cleanup_time = SystemTime::now();
|
||||
let media_service = MediaService::new();
|
||||
media_service.restore(None, Some(last_media_cleanup_time));
|
||||
|
||||
Self {
|
||||
inner: StdRwLock::new(MemoryStoreInner {
|
||||
inner: Arc::new(StdRwLock::new(MemoryStoreInner {
|
||||
media: RingBuffer::new(NUMBER_OF_MEDIAS),
|
||||
leases: Default::default(),
|
||||
events: RelationalLinkedChunk::new(),
|
||||
media_retention_policy: None,
|
||||
}),
|
||||
// No need to call `restore()` since nothing is persisted.
|
||||
media_service: MediaService::new(),
|
||||
last_media_cleanup_time,
|
||||
})),
|
||||
media_service,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,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;
|
||||
|
||||
@@ -114,31 +128,146 @@ 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 reload_linked_chunk(
|
||||
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
|
||||
.reload_chunks(room_id)
|
||||
.load_all_chunks(linked_chunk_id)
|
||||
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
|
||||
}
|
||||
|
||||
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error> {
|
||||
async fn load_last_chunk(
|
||||
&self,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<(Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator), Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
inner
|
||||
.events
|
||||
.load_last_chunk(linked_chunk_id)
|
||||
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
|
||||
}
|
||||
|
||||
async fn load_previous_chunk(
|
||||
&self,
|
||||
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(linked_chunk_id, before_chunk_identifier)
|
||||
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
|
||||
}
|
||||
|
||||
async fn clear_all_linked_chunks(&self) -> Result<(), Self::Error> {
|
||||
self.inner.write().unwrap().events.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn filter_duplicated_events(
|
||||
&self,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
mut events: Vec<OwnedEventId>,
|
||||
) -> Result<Vec<(OwnedEventId, Position)>, Self::Error> {
|
||||
// Collect all duplicated events.
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
let mut duplicated_events = Vec::new();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if let Some(known_event_id) = event.event_id() {
|
||||
// This event is a duplicate!
|
||||
if let Some(index) =
|
||||
events.iter().position(|new_event_id| &known_event_id == new_event_id)
|
||||
{
|
||||
duplicated_events.push((events.remove(index), position));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(duplicated_events)
|
||||
}
|
||||
|
||||
async fn find_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_id: &EventId,
|
||||
) -> Result<Option<Event>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
async fn find_event_relations(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_id: &EventId,
|
||||
filters: Option<&[RelationType]>,
|
||||
) -> Result<Vec<Event>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
let filters = compute_filters_string(filters);
|
||||
|
||||
let related_events = inner
|
||||
.events
|
||||
.items()
|
||||
.filter_map(|(event, this_linked_chunk_id)| {
|
||||
// Must be in the same room.
|
||||
if room_id != this_linked_chunk_id.room_id() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Must have a relation.
|
||||
let (related_to, rel_type) = extract_event_relation(event.raw())?;
|
||||
|
||||
// Must relate to the target item.
|
||||
if related_to != event_id {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Must not be filtered out.
|
||||
if let Some(filters) = &filters {
|
||||
filters.contains(&rel_type).then_some(event.clone())
|
||||
} else {
|
||||
Some(event.clone())
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(related_events)
|
||||
}
|
||||
|
||||
async fn save_event(&self, room_id: &RoomId, event: Event) -> Result<(), Self::Error> {
|
||||
if event.event_id().is_none() {
|
||||
error!(%room_id, "Trying to save an event with no ID");
|
||||
return Ok(());
|
||||
}
|
||||
self.inner.write().unwrap().events.save_item(room_id.to_owned(), event);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
@@ -236,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;
|
||||
|
||||
@@ -268,7 +397,7 @@ impl EventCacheStoreMedia for MemoryStore {
|
||||
|
||||
let ignore_policy = ignore_policy.is_yes();
|
||||
|
||||
if !ignore_policy && policy.exceeds_max_file_size(data.len()) {
|
||||
if !ignore_policy && policy.exceeds_max_file_size(data.len() as u64) {
|
||||
// Do not store it.
|
||||
return Ok(());
|
||||
};
|
||||
@@ -374,7 +503,7 @@ impl EventCacheStoreMedia for MemoryStore {
|
||||
// First, check media content that exceed the max filesize.
|
||||
if policy.computed_max_file_size().is_some() {
|
||||
inner.media.retain(|content| {
|
||||
content.ignore_policy || !policy.exceeds_max_file_size(content.data.len())
|
||||
content.ignore_policy || !policy.exceeds_max_file_size(content.data.len() as u64)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -392,7 +521,7 @@ impl EventCacheStoreMedia for MemoryStore {
|
||||
// to count the number of old items to remove. Items are sorted by last access
|
||||
// and old items are at the start.
|
||||
let (_, items_to_remove) = inner.media.iter().enumerate().rev().fold(
|
||||
(0usize, Vec::with_capacity(NUMBER_OF_MEDIAS.into())),
|
||||
(0u64, Vec::with_capacity(NUMBER_OF_MEDIAS.into())),
|
||||
|(mut cache_size, mut items_to_remove), (index, content)| {
|
||||
if content.ignore_policy {
|
||||
// Do not count it.
|
||||
@@ -401,7 +530,7 @@ impl EventCacheStoreMedia for MemoryStore {
|
||||
|
||||
let remove_item = if items_to_remove.is_empty() {
|
||||
// We have not reached the max cache size yet.
|
||||
if let Some(sum) = cache_size.checked_add(content.data.len()) {
|
||||
if let Some(sum) = cache_size.checked_add(content.data.len() as u64) {
|
||||
cache_size = sum;
|
||||
// Start removing items if we have exceeded the max cache size.
|
||||
cache_size > max_cache_size
|
||||
@@ -431,8 +560,14 @@ impl EventCacheStoreMedia for MemoryStore {
|
||||
}
|
||||
}
|
||||
|
||||
inner.last_media_cleanup_time = current_time;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error> {
|
||||
Ok(Some(self.inner.read().unwrap().last_media_cleanup_time))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -32,6 +32,12 @@ use matrix_sdk_common::store_locks::{
|
||||
BackingStore, CrossProcessStoreLock, CrossProcessStoreLockGuard, LockStoreError,
|
||||
};
|
||||
pub use matrix_sdk_store_encryption::Error as StoreEncryptionError;
|
||||
use ruma::{
|
||||
events::{relation::RelationType, AnySyncTimelineEvent},
|
||||
serde::Raw,
|
||||
OwnedEventId,
|
||||
};
|
||||
use tracing::trace;
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
pub use self::integration_tests::EventCacheStoreIntegrationTests;
|
||||
@@ -120,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")]
|
||||
@@ -163,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.
|
||||
@@ -179,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;
|
||||
|
||||
@@ -193,3 +216,51 @@ impl BackingStore for LockableEventCacheStore {
|
||||
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to extract the relation information from an event.
|
||||
///
|
||||
/// If the event isn't in relation to another event, then this will return
|
||||
/// `None`. Otherwise, returns both the event id this event relates to, and the
|
||||
/// kind of relation as a string (e.g. `m.replace`).
|
||||
pub fn extract_event_relation(event: &Raw<AnySyncTimelineEvent>) -> Option<(OwnedEventId, String)> {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct RelatesTo {
|
||||
event_id: OwnedEventId,
|
||||
rel_type: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct EventContent {
|
||||
#[serde(rename = "m.relates_to")]
|
||||
rel: Option<RelatesTo>,
|
||||
}
|
||||
|
||||
match event.get_field::<EventContent>("content") {
|
||||
Ok(event_content) => {
|
||||
event_content.and_then(|c| c.rel).map(|rel| (rel.event_id, rel.rel_type))
|
||||
}
|
||||
Err(err) => {
|
||||
trace!("when extracting relation data from an event: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the list of string filters to be applied when looking for an event's
|
||||
/// relations.
|
||||
// TODO: get Ruma fix from https://github.com/ruma/ruma/pull/2052, and get rid of this function
|
||||
// then.
|
||||
pub fn compute_filters_string(filters: Option<&[RelationType]>) -> Option<Vec<String>> {
|
||||
filters.map(|filter| {
|
||||
filter
|
||||
.iter()
|
||||
.map(|f| {
|
||||
if *f == RelationType::Replacement {
|
||||
"m.replace".to_owned()
|
||||
} else {
|
||||
f.to_string()
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,10 +16,12 @@ use std::{fmt, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
linked_chunk::{RawChunk, Update},
|
||||
linked_chunk::{
|
||||
ChunkIdentifier, ChunkIdentifierGenerator, LinkedChunkId, Position, RawChunk, Update,
|
||||
},
|
||||
AsyncTraitDeps,
|
||||
};
|
||||
use ruma::{MxcUri, RoomId};
|
||||
use ruma::{events::relation::RelationType, EventId, MxcUri, OwnedEventId, RoomId};
|
||||
|
||||
use super::{
|
||||
media::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy},
|
||||
@@ -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,21 +66,89 @@ 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
|
||||
/// reconstruct the linked chunk later.
|
||||
async fn reload_linked_chunk(
|
||||
#[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
|
||||
/// identified by `room_id`.
|
||||
///
|
||||
/// This is used to iteratively load events for the `EventCache`.
|
||||
async fn load_last_chunk(
|
||||
&self,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<(Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator), Self::Error>;
|
||||
|
||||
/// Load the chunk before the chunk identified by `before_chunk_identifier`
|
||||
/// of the `LinkedChunk` holding all events of the room identified by
|
||||
/// `room_id`
|
||||
///
|
||||
/// This is used to iteratively load events for the `EventCache`.
|
||||
async fn load_previous_chunk(
|
||||
&self,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
before_chunk_identifier: ChunkIdentifier,
|
||||
) -> Result<Option<RawChunk<Event, Gap>>, Self::Error>;
|
||||
|
||||
/// Clear persisted events for all the rooms.
|
||||
///
|
||||
/// This will empty and remove all the linked chunks stored previously,
|
||||
/// using the above [`Self::handle_linked_chunk_updates`] methods.
|
||||
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error>;
|
||||
/// using the above [`Self::handle_linked_chunk_updates`] methods. It
|
||||
/// must *also* delete all the events' content, if they were stored in a
|
||||
/// separate table.
|
||||
///
|
||||
/// ⚠ 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_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,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
events: Vec<OwnedEventId>,
|
||||
) -> Result<Vec<(OwnedEventId, Position)>, Self::Error>;
|
||||
|
||||
/// Find an event by its ID in a room.
|
||||
async fn find_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_id: &EventId,
|
||||
) -> Result<Option<Event>, Self::Error>;
|
||||
|
||||
/// 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(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_id: &EventId,
|
||||
filter: Option<&[RelationType]>,
|
||||
) -> Result<Vec<Event>, Self::Error>;
|
||||
|
||||
/// Save an event, that might or might not be part of an existing linked
|
||||
/// chunk.
|
||||
///
|
||||
/// If the event has no event id, it will not be saved, and the function
|
||||
/// must return an Ok result early.
|
||||
///
|
||||
/// If the event was already stored with the same id, it must be replaced,
|
||||
/// without causing an error.
|
||||
async fn save_event(&self, room_id: &RoomId, event: Event) -> Result<(), Self::Error>;
|
||||
|
||||
/// Add a media file's content in the media store.
|
||||
///
|
||||
@@ -214,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;
|
||||
|
||||
@@ -230,21 +300,68 @@ 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 reload_linked_chunk(
|
||||
async fn load_all_chunks(
|
||||
&self,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
|
||||
self.0.load_all_chunks(linked_chunk_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn load_last_chunk(
|
||||
&self,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<(Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator), Self::Error> {
|
||||
self.0.load_last_chunk(linked_chunk_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn load_previous_chunk(
|
||||
&self,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
before_chunk_identifier: ChunkIdentifier,
|
||||
) -> Result<Option<RawChunk<Event, Gap>>, Self::Error> {
|
||||
self.0
|
||||
.load_previous_chunk(linked_chunk_id, before_chunk_identifier)
|
||||
.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,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
events: Vec<OwnedEventId>,
|
||||
) -> Result<Vec<(OwnedEventId, Position)>, Self::Error> {
|
||||
self.0.filter_duplicated_events(linked_chunk_id, events).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn find_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
|
||||
self.0.reload_linked_chunk(room_id).await.map_err(Into::into)
|
||||
event_id: &EventId,
|
||||
) -> Result<Option<Event>, Self::Error> {
|
||||
self.0.find_event(room_id, event_id).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 find_event_relations(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_id: &EventId,
|
||||
filter: Option<&[RelationType]>,
|
||||
) -> Result<Vec<Event>, Self::Error> {
|
||||
self.0.find_event_relations(room_id, event_id, filter).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn save_event(&self, room_id: &RoomId, event: Event) -> Result<(), Self::Error> {
|
||||
self.0.save_event(room_id, event).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn add_media_content(
|
||||
|
||||
@@ -10,7 +10,7 @@ use ruma::{
|
||||
relation::RelationType,
|
||||
room::{
|
||||
member::{MembershipState, SyncRoomMemberEvent},
|
||||
message::SyncRoomMessageEvent,
|
||||
message::{MessageType, SyncRoomMessageEvent},
|
||||
power_levels::RoomPowerLevels,
|
||||
},
|
||||
sticker::SyncStickerEvent,
|
||||
@@ -67,8 +67,13 @@ pub fn is_suitable_for_latest_event<'a>(
|
||||
match event {
|
||||
// Suitable - we have an m.room.message that was not redacted or edited
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => {
|
||||
// Check if this is a replacement for another message. If it is, ignore it
|
||||
if let Some(original_message) = message.as_original() {
|
||||
// Don't show incoming verification requests
|
||||
if let MessageType::VerificationRequest(_) = original_message.content.msgtype {
|
||||
return PossibleLatestEvent::NoUnsupportedMessageLikeType;
|
||||
}
|
||||
|
||||
// Check if this is a replacement for another message. If it is, ignore it
|
||||
let is_replacement =
|
||||
original_message.content.relates_to.as_ref().is_some_and(|relates_to| {
|
||||
if let Some(relation_type) = relates_to.rel_type() {
|
||||
@@ -589,6 +594,34 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_verification_requests_are_unsuitable() {
|
||||
use ruma::{device_id, events::room::message::KeyVerificationRequestEventContent, user_id};
|
||||
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
|
||||
SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: RoomMessageEventContent::new(MessageType::VerificationRequest(
|
||||
KeyVerificationRequestEventContent::new(
|
||||
"body".to_owned(),
|
||||
vec![],
|
||||
device_id!("device_id").to_owned(),
|
||||
user_id!("@user_id:example.com").to_owned(),
|
||||
),
|
||||
)),
|
||||
event_id: owned_event_id!("$1"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(123).unwrap()),
|
||||
unsigned: MessageLikeUnsigned::new(),
|
||||
}),
|
||||
));
|
||||
|
||||
assert_let!(
|
||||
PossibleLatestEvent::NoUnsupportedMessageLikeType =
|
||||
is_suitable_for_latest_event(&event, None)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_latest_event() {
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
@@ -596,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(),
|
||||
);
|
||||
|
||||
@@ -621,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::*;
|
||||
@@ -25,6 +25,7 @@ use serde::{Deserialize, Serialize};
|
||||
pub use crate::error::{Error, Result};
|
||||
|
||||
mod client;
|
||||
pub use client::RequestedRequiredStates;
|
||||
pub mod debug;
|
||||
pub mod deserialized_responses;
|
||||
mod error;
|
||||
@@ -33,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;
|
||||
@@ -54,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, 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,
|
||||
|
||||
@@ -110,8 +110,8 @@
|
||||
//! events ids in both sets. As a matter of fact, we have to manually handle
|
||||
//! this edge case here. I hope that having an event database will help avoid
|
||||
//! this kind of workaround here later.
|
||||
//! - In addition to that, and as noted in the timeline code, it seems that the
|
||||
//! sliding-sync proxy could return the same event multiple times in a sync
|
||||
//! - In addition to that, and as noted in the timeline code, it seems that
|
||||
//! sliding sync could return the same event multiple times in a sync
|
||||
//! timeline, leading to incorrect results. We have to take that into account
|
||||
//! by resetting the read counts *every* time we see an event that was the
|
||||
//! target of the latest active read receipt.
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -245,10 +244,9 @@ impl RoomReadReceipts {
|
||||
let mut counting_receipts = false;
|
||||
|
||||
for event in events {
|
||||
// The sliding sync proxy sometimes sends the same event multiple times, so it
|
||||
// can be at the beginning and end of a batch, for instance. In that
|
||||
// case, just reset every time we see the event matching the
|
||||
// receipt. NOTE: SS proxy workaround.
|
||||
// Sliding sync sometimes sends the same event multiple times, so it can be at
|
||||
// the beginning and end of a batch, for instance. In that case, just reset
|
||||
// every time we see the event matching the receipt.
|
||||
if let Some(event_id) = event.event_id() {
|
||||
if event_id == receipt_event_id {
|
||||
// Bingo! Switch over to the counting state, after resetting the
|
||||
@@ -269,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 {
|
||||
@@ -295,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 =
|
||||
@@ -458,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 = {
|
||||
@@ -482,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 {
|
||||
@@ -623,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::{
|
||||
@@ -730,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
|
||||
}
|
||||
|
||||
@@ -916,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();
|
||||
@@ -925,7 +906,7 @@ mod tests {
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(receipt_event_id, user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
|
||||
.build();
|
||||
.into_content();
|
||||
|
||||
let mut read_receipts = Default::default();
|
||||
compute_unread_counts(
|
||||
@@ -941,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();
|
||||
@@ -959,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"));
|
||||
@@ -977,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
|
||||
@@ -1007,7 +988,7 @@ mod tests {
|
||||
receipt_type_1.clone(),
|
||||
receipt_thread_2.clone(),
|
||||
)
|
||||
.build();
|
||||
.into_content();
|
||||
|
||||
// When I compute the notifications for this room (with no new events),
|
||||
let mut read_receipts = RoomReadReceipts::default();
|
||||
@@ -1070,7 +1051,7 @@ mod tests {
|
||||
let receipt_event = EventFactory::new()
|
||||
.read_receipts()
|
||||
.add(event_id!("$6"), &user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
|
||||
.build();
|
||||
.into_content();
|
||||
|
||||
let mut read_receipts = RoomReadReceipts::default();
|
||||
assert!(read_receipts.pending.is_empty());
|
||||
@@ -1104,7 +1085,7 @@ mod tests {
|
||||
let receipt_event = EventFactory::new()
|
||||
.read_receipts()
|
||||
.add(event_id!("$1"), &user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
|
||||
.build();
|
||||
.into_content();
|
||||
|
||||
// Sync with a read receipt *and* a single event that was already known: in that
|
||||
// case, only consider the new events in isolation, and compute the
|
||||
@@ -1166,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"));
|
||||
@@ -1204,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);
|
||||
@@ -1222,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);
|
||||
@@ -1240,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"));
|
||||
@@ -1258,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"));
|
||||
@@ -1277,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"));
|
||||
@@ -1297,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"));
|
||||
@@ -1319,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"));
|
||||
@@ -1340,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"));
|
||||
@@ -1372,7 +1353,7 @@ mod tests {
|
||||
ReceiptType::Read,
|
||||
ReceiptThread::Thread(owned_event_id!("$2")),
|
||||
)
|
||||
.build();
|
||||
.into_content();
|
||||
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert!(pending.is_empty());
|
||||
@@ -1391,7 +1372,7 @@ mod tests {
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$6"), myself, receipt_type.clone(), receipt_thread.clone())
|
||||
.build();
|
||||
.into_content();
|
||||
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert_eq!(pending[0], event_id!("$6"));
|
||||
@@ -1409,7 +1390,7 @@ mod tests {
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$3"), myself, receipt_type.clone(), receipt_thread.clone())
|
||||
.build();
|
||||
.into_content();
|
||||
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert!(pending.is_empty());
|
||||
@@ -1426,7 +1407,7 @@ mod tests {
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$3"), myself, receipt_type.clone(), receipt_thread.clone())
|
||||
.build();
|
||||
.into_content();
|
||||
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert!(pending.is_empty());
|
||||
@@ -1443,7 +1424,7 @@ mod tests {
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$3"), myself, receipt_type.clone(), receipt_thread.clone())
|
||||
.build();
|
||||
.into_content();
|
||||
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert!(pending.is_empty());
|
||||
@@ -1464,7 +1445,7 @@ mod tests {
|
||||
.add(event_id!("$4"), myself, ReceiptType::ReadPrivate, ReceiptThread::Unthreaded)
|
||||
.add(event_id!("$6"), myself, ReceiptType::ReadPrivate, ReceiptThread::Main)
|
||||
.add(event_id!("$3"), myself, ReceiptType::Read, ReceiptThread::Main)
|
||||
.build();
|
||||
.into_content();
|
||||
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert_eq!(pending.len(), 1);
|
||||
@@ -1485,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"));
|
||||
@@ -1521,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"))
|
||||
@@ -1529,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(),
|
||||
);
|
||||
|
||||
@@ -1542,7 +1523,7 @@ mod tests {
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$3"), user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
|
||||
.build();
|
||||
.into_content();
|
||||
|
||||
let mut read_receipts = RoomReadReceipts::default();
|
||||
|
||||
@@ -1552,7 +1533,7 @@ mod tests {
|
||||
user_id,
|
||||
room_id,
|
||||
Some(&receipt_event),
|
||||
Vector::new(),
|
||||
Vec::new(),
|
||||
&events,
|
||||
&mut read_receipts,
|
||||
);
|
||||
|
||||
+44
-33
@@ -26,36 +26,23 @@ use ruma::{
|
||||
};
|
||||
use tracing::{debug, instrument, trace, warn};
|
||||
|
||||
use crate::{store::Store, RoomInfo, StateChanges};
|
||||
use super::super::Context;
|
||||
use crate::{store::BaseStateStore, RoomInfo, StateChanges};
|
||||
|
||||
/// Applies a function to an existing `RoomInfo` if present in changes, or one
|
||||
/// loaded from the database.
|
||||
fn map_info<F: FnOnce(&mut RoomInfo)>(
|
||||
room_id: &RoomId,
|
||||
changes: &mut StateChanges,
|
||||
store: &Store,
|
||||
f: F,
|
||||
) {
|
||||
if let Some(info) = changes.room_infos.get_mut(room_id) {
|
||||
f(info);
|
||||
} else if let Some(room) = store.room(room_id) {
|
||||
let mut info = room.clone_info();
|
||||
f(&mut info);
|
||||
changes.add_room(info);
|
||||
} else {
|
||||
debug!(room = %room_id, "couldn't find room in state changes or store");
|
||||
}
|
||||
/// Create the [`Global`] account data processor.
|
||||
pub fn global(events: &[Raw<AnyGlobalAccountDataEvent>]) -> Global {
|
||||
Global::process(events)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) struct AccountDataProcessor {
|
||||
pub struct Global {
|
||||
parsed_events: Vec<AnyGlobalAccountDataEvent>,
|
||||
raw_by_type: BTreeMap<GlobalAccountDataEventType, Raw<AnyGlobalAccountDataEvent>>,
|
||||
}
|
||||
|
||||
impl AccountDataProcessor {
|
||||
impl Global {
|
||||
/// Creates a new processor for global account data.
|
||||
pub fn process(events: &[Raw<AnyGlobalAccountDataEvent>]) -> Self {
|
||||
fn process(events: &[Raw<AnyGlobalAccountDataEvent>]) -> Self {
|
||||
let mut raw_by_type = BTreeMap::new();
|
||||
let mut parsed_events = Vec::new();
|
||||
|
||||
@@ -87,23 +74,24 @@ impl AccountDataProcessor {
|
||||
/// from the global account data and adds it to the room infos to
|
||||
/// save.
|
||||
#[instrument(skip_all)]
|
||||
pub(crate) fn process_direct_rooms(
|
||||
fn process_direct_rooms(
|
||||
&self,
|
||||
events: &[AnyGlobalAccountDataEvent],
|
||||
store: &Store,
|
||||
changes: &mut StateChanges,
|
||||
state_store: &BaseStateStore,
|
||||
state_changes: &mut StateChanges,
|
||||
) {
|
||||
for event in events {
|
||||
let AnyGlobalAccountDataEvent::Direct(direct_event) = event else { continue };
|
||||
|
||||
let mut new_dms = HashMap::<&RoomId, HashSet<OwnedDirectUserIdentifier>>::new();
|
||||
|
||||
for (user_identifier, rooms) in direct_event.content.iter() {
|
||||
for room_id in rooms {
|
||||
new_dms.entry(room_id).or_default().insert(user_identifier.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let rooms = store.rooms();
|
||||
let rooms = state_store.rooms();
|
||||
let mut old_dms = rooms
|
||||
.iter()
|
||||
.filter_map(|r| {
|
||||
@@ -120,7 +108,7 @@ impl AccountDataProcessor {
|
||||
}
|
||||
}
|
||||
trace!(?room_id, targets = ?new_direct_targets, "Marking room as direct room");
|
||||
map_info(room_id, changes, store, |info| {
|
||||
map_info(room_id, state_changes, state_store, |info| {
|
||||
info.base_info.dm_targets = new_direct_targets;
|
||||
});
|
||||
}
|
||||
@@ -128,17 +116,17 @@ impl AccountDataProcessor {
|
||||
// Remove the targets of old direct chats.
|
||||
for room_id in old_dms.keys() {
|
||||
trace!(?room_id, "Unmarking room as direct room");
|
||||
map_info(room_id, changes, store, |info| {
|
||||
map_info(room_id, state_changes, state_store, |info| {
|
||||
info.base_info.dm_targets.clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies the processed data to the state changes.
|
||||
pub async fn apply(mut self, changes: &mut StateChanges, store: &Store) {
|
||||
/// Applies the processed data to the state changes and the state store.
|
||||
pub async fn apply(mut self, context: &mut Context, state_store: &BaseStateStore) {
|
||||
// Fill in the content of `changes.account_data`.
|
||||
mem::swap(&mut changes.account_data, &mut self.raw_by_type);
|
||||
mem::swap(&mut context.state_changes.account_data, &mut self.raw_by_type);
|
||||
|
||||
// Process direct rooms.
|
||||
let has_new_direct_room_data = self
|
||||
@@ -147,16 +135,39 @@ impl AccountDataProcessor {
|
||||
.any(|event| event.event_type() == GlobalAccountDataEventType::Direct);
|
||||
|
||||
if has_new_direct_room_data {
|
||||
self.process_direct_rooms(&self.parsed_events, store, changes);
|
||||
self.process_direct_rooms(&self.parsed_events, state_store, &mut context.state_changes);
|
||||
} else if let Ok(Some(direct_account_data)) =
|
||||
store.get_account_data_event(GlobalAccountDataEventType::Direct).await
|
||||
state_store.get_account_data_event(GlobalAccountDataEventType::Direct).await
|
||||
{
|
||||
debug!("Found direct room data in the Store, applying it");
|
||||
if let Ok(direct_account_data) = direct_account_data.deserialize() {
|
||||
self.process_direct_rooms(&[direct_account_data], store, changes);
|
||||
self.process_direct_rooms(
|
||||
&[direct_account_data],
|
||||
state_store,
|
||||
&mut context.state_changes,
|
||||
);
|
||||
} else {
|
||||
warn!("Failed to deserialize direct room account data");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies a function to an existing `RoomInfo` if present in changes, or one
|
||||
/// loaded from the database.
|
||||
fn map_info<F: FnOnce(&mut RoomInfo)>(
|
||||
room_id: &RoomId,
|
||||
changes: &mut StateChanges,
|
||||
store: &BaseStateStore,
|
||||
f: F,
|
||||
) {
|
||||
if let Some(info) = changes.room_infos.get_mut(room_id) {
|
||||
f(info);
|
||||
} else if let Some(room) = store.room(room_id) {
|
||||
let mut info = room.clone_info();
|
||||
f(&mut info);
|
||||
changes.add_room(info);
|
||||
} else {
|
||||
debug!(room = %room_id, "couldn't find room in state changes or store");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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.
|
||||
|
||||
mod global;
|
||||
mod room;
|
||||
|
||||
pub use global::{global, Global};
|
||||
pub use room::for_room;
|
||||
@@ -0,0 +1,157 @@
|
||||
// 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::{marked_unread::MarkedUnreadEventContent, AnyRoomAccountDataEvent},
|
||||
serde::Raw,
|
||||
RoomId,
|
||||
};
|
||||
use tracing::{instrument, warn};
|
||||
|
||||
use super::super::{Context, RoomInfoNotableUpdates};
|
||||
use crate::{
|
||||
room::AccountDataSource, store::BaseStateStore, RoomInfo, RoomInfoNotableUpdateReasons,
|
||||
StateChanges,
|
||||
};
|
||||
|
||||
#[instrument(skip_all, fields(?room_id))]
|
||||
pub async fn for_room(
|
||||
context: &mut Context,
|
||||
room_id: &RoomId,
|
||||
events: &[Raw<AnyRoomAccountDataEvent>],
|
||||
state_store: &BaseStateStore,
|
||||
) {
|
||||
// Handle new events.
|
||||
for raw_event in events {
|
||||
match raw_event.deserialize() {
|
||||
Ok(event) => {
|
||||
context.state_changes.add_room_account_data(
|
||||
room_id,
|
||||
event.clone(),
|
||||
raw_event.clone(),
|
||||
);
|
||||
|
||||
match event {
|
||||
AnyRoomAccountDataEvent::MarkedUnread(event) => {
|
||||
on_room_info(
|
||||
room_id,
|
||||
&mut context.state_changes,
|
||||
state_store,
|
||||
|room_info| {
|
||||
on_unread_marker(
|
||||
room_id,
|
||||
&event.content,
|
||||
AccountDataSource::Stable,
|
||||
room_info,
|
||||
&mut context.room_info_notable_updates,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
AnyRoomAccountDataEvent::UnstableMarkedUnread(event) => {
|
||||
on_room_info(
|
||||
room_id,
|
||||
&mut context.state_changes,
|
||||
state_store,
|
||||
|room_info| {
|
||||
on_unread_marker(
|
||||
room_id,
|
||||
&event.content.0,
|
||||
AccountDataSource::Unstable,
|
||||
room_info,
|
||||
&mut context.room_info_notable_updates,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
AnyRoomAccountDataEvent::Tag(event) => {
|
||||
on_room_info(
|
||||
room_id,
|
||||
&mut context.state_changes,
|
||||
state_store,
|
||||
|room_info| {
|
||||
room_info.base_info.handle_notable_tags(&event.content.tags);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Nothing.
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Err(err) => {
|
||||
warn!("unable to deserialize account data event: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Small helper to make the code easier to read.
|
||||
//
|
||||
// It finds the appropriate `RoomInfo`, allowing the caller to modify it, and
|
||||
// save it in the correct place.
|
||||
fn on_room_info<F>(
|
||||
room_id: &RoomId,
|
||||
state_changes: &mut StateChanges,
|
||||
state_store: &BaseStateStore,
|
||||
mut on_room_info: F,
|
||||
) where
|
||||
F: FnMut(&mut RoomInfo),
|
||||
{
|
||||
// `StateChanges` has the `RoomInfo`.
|
||||
if let Some(room_info) = state_changes.room_infos.get_mut(room_id) {
|
||||
// Show time.
|
||||
on_room_info(room_info);
|
||||
}
|
||||
// The `BaseStateStore` has the `Room`, which has the `RoomInfo`.
|
||||
else if let Some(room) = state_store.room(room_id) {
|
||||
// Clone the `RoomInfo`.
|
||||
let mut room_info = room.clone_info();
|
||||
|
||||
// Show time.
|
||||
on_room_info(&mut room_info);
|
||||
|
||||
// Update the `RoomInfo` via `StateChanges`.
|
||||
state_changes.add_room(room_info);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to update the unread marker for stable and unstable prefixes.
|
||||
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.
|
||||
room_info_notable_updates
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.insert(RoomInfoNotableUpdateReasons::UNREAD_MARKER);
|
||||
}
|
||||
|
||||
room_info.base_info.is_marked_unread = content.unread;
|
||||
room_info.base_info.is_marked_unread_source = source;
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// 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 eyeball::SharedObservable;
|
||||
use ruma::{
|
||||
events::{ignored_user_list::IgnoredUserListEvent, GlobalAccountDataEventType},
|
||||
serde::Raw,
|
||||
};
|
||||
use tracing::{error, instrument, trace};
|
||||
|
||||
use super::Context;
|
||||
use crate::{
|
||||
store::{BaseStateStore, StateStoreExt as _},
|
||||
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)]
|
||||
pub async fn save_and_apply(
|
||||
context: Context,
|
||||
state_store: &BaseStateStore,
|
||||
ignore_user_list_changes: &SharedObservable<Vec<String>>,
|
||||
sync_token: Option<String>,
|
||||
) -> Result<()> {
|
||||
trace!("ready to submit changes to store");
|
||||
|
||||
let previous_ignored_user_list =
|
||||
state_store.get_account_data_event_static().await.ok().flatten();
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
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>>,
|
||||
) {
|
||||
if let Some(event) =
|
||||
context.state_changes.account_data.get(&GlobalAccountDataEventType::IgnoredUserList)
|
||||
{
|
||||
match event.deserialize_as::<IgnoredUserListEvent>() {
|
||||
Ok(event) => {
|
||||
let user_ids: Vec<String> =
|
||||
event.content.ignored_users.keys().map(|id| id.to_string()).collect();
|
||||
|
||||
// Try to only trigger the observable if the ignored user list has changed,
|
||||
// from the previous time we've seen it. If we couldn't load the previous event
|
||||
// for any reason, always trigger.
|
||||
if let Some(prev_user_ids) =
|
||||
previous_ignored_user_list.and_then(|raw| raw.deserialize().ok()).map(|event| {
|
||||
event
|
||||
.content
|
||||
.ignored_users
|
||||
.keys()
|
||||
.map(|id| id.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
{
|
||||
if user_ids != prev_user_ids {
|
||||
ignore_user_list_changes.set(user_ids);
|
||||
}
|
||||
} else {
|
||||
ignore_user_list_changes.set(user_ids);
|
||||
}
|
||||
}
|
||||
|
||||
Err(error) => {
|
||||
error!("Failed to deserialize ignored user list event: {error}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 =
|
||||
context.room_info_notable_updates.get(room_id).copied().unwrap_or_default();
|
||||
|
||||
room.set_room_info(room_info.clone(), room_info_notable_update_reasons)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// 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 matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use matrix_sdk_crypto::{DecryptionSettings, RoomEventDecryptionResult};
|
||||
use ruma::{events::AnySyncTimelineEvent, serde::Raw, RoomId};
|
||||
|
||||
use super::{super::verification, E2EE};
|
||||
use crate::Result;
|
||||
|
||||
/// Attempt to decrypt the given raw event into a [`TimelineEvent`].
|
||||
///
|
||||
/// In the case of a decryption error, returns a [`TimelineEvent`]
|
||||
/// representing the decryption error; in the case of problems with our
|
||||
/// application, returns `Err`.
|
||||
///
|
||||
/// Returns `Ok(None)` if encryption is not configured.
|
||||
pub async fn sync_timeline_event(
|
||||
e2ee: E2EE<'_>,
|
||||
event: &Raw<AnySyncTimelineEvent>,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Option<TimelineEvent>> {
|
||||
let Some(olm) = e2ee.olm_machine else { return Ok(None) };
|
||||
|
||||
let decryption_settings =
|
||||
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) => {
|
||||
// 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(&sync_timeline_event, e2ee, room_id).await?;
|
||||
}
|
||||
|
||||
timeline_event
|
||||
}
|
||||
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
|
||||
TimelineEvent::from_utd(event.clone(), utd_info)
|
||||
}
|
||||
},
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// 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 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 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// 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 matrix_sdk_crypto::{store::RoomKeyInfo, EncryptionSyncChanges, OlmMachine};
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::{v3, v5, DeviceLists},
|
||||
events::AnyToDeviceEvent,
|
||||
serde::Raw,
|
||||
OneTimeKeyAlgorithm, UInt,
|
||||
};
|
||||
|
||||
use crate::Result;
|
||||
|
||||
/// Process the to-device events and other related e2ee data based on a response
|
||||
/// from a [MSC4186 request][`v5`].
|
||||
///
|
||||
/// 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(
|
||||
to_device: Option<&v5::response::ToDevice>,
|
||||
e2ee: &v5::response::E2EE,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
) -> Result<Output> {
|
||||
process(
|
||||
olm_machine,
|
||||
to_device.as_ref().map(|to_device| to_device.events.clone()).unwrap_or_default(),
|
||||
&e2ee.device_lists,
|
||||
&e2ee.device_one_time_keys_count,
|
||||
e2ee.device_unused_fallback_key_types.as_deref(),
|
||||
to_device.as_ref().map(|to_device| to_device.next_batch.clone()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Process the to-device events and other related e2ee data based on a response
|
||||
/// from a [`/v3/sync` request][`v3`].
|
||||
///
|
||||
/// 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(
|
||||
response: &v3::Response,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
) -> Result<Output> {
|
||||
process(
|
||||
olm_machine,
|
||||
response.to_device.events.clone(),
|
||||
&response.device_lists,
|
||||
&response.device_one_time_keys_count,
|
||||
response.device_unused_fallback_key_types.as_deref(),
|
||||
Some(response.next_batch.clone()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Process the to-device events and other related e2ee data.
|
||||
///
|
||||
/// 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(
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
to_device_events: Vec<Raw<AnyToDeviceEvent>>,
|
||||
device_lists: &DeviceLists,
|
||||
one_time_keys_counts: &BTreeMap<OneTimeKeyAlgorithm, UInt>,
|
||||
unused_fallback_keys: Option<&[OneTimeKeyAlgorithm]>,
|
||||
next_batch_token: Option<String>,
|
||||
) -> Result<Output> {
|
||||
let encryption_sync_changes = EncryptionSyncChanges {
|
||||
to_device_events,
|
||||
changed_devices: device_lists,
|
||||
one_time_keys_counts,
|
||||
unused_fallback_keys,
|
||||
next_batch_token,
|
||||
};
|
||||
|
||||
Ok(if let Some(olm_machine) = olm_machine {
|
||||
// Let the crypto machine handle the sync response, this
|
||||
// decrypts to-device events, but leaves room events alone.
|
||||
// This makes sure that we have the decryption keys for the room
|
||||
// events at hand.
|
||||
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.
|
||||
// This should not happen unless we forget to set things up by calling
|
||||
// `Self::activate()`.
|
||||
Output {
|
||||
decrypted_to_device_events: encryption_sync_changes.to_device_events,
|
||||
room_key_updates: None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub struct Output {
|
||||
pub decrypted_to_device_events: Vec<Raw<AnyToDeviceEvent>>,
|
||||
pub room_key_updates: Option<Vec<RoomKeyInfo>>,
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// 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::BTreeSet;
|
||||
|
||||
use matrix_sdk_crypto::OlmMachine;
|
||||
use ruma::{OwnedUserId, RoomId};
|
||||
|
||||
use crate::{store::BaseStateStore, EncryptionState, Result, RoomMemberships};
|
||||
|
||||
/// Update tracked users, if the room is encrypted.
|
||||
pub async fn update(
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
room_encryption_state: EncryptionState,
|
||||
user_ids_to_track: &BTreeSet<OwnedUserId>,
|
||||
) -> Result<()> {
|
||||
if room_encryption_state.is_encrypted() {
|
||||
if let Some(olm) = olm_machine {
|
||||
if !user_ids_to_track.is_empty() {
|
||||
olm.update_tracked_users(user_ids_to_track.iter().map(AsRef::as_ref)).await?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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(
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
user_ids_to_track: &BTreeSet<OwnedUserId>,
|
||||
new_room_encryption_state: EncryptionState,
|
||||
previous_room_encryption_state: EncryptionState,
|
||||
room_id: &RoomId,
|
||||
state_store: &BaseStateStore,
|
||||
) -> Result<()> {
|
||||
if new_room_encryption_state.is_encrypted() {
|
||||
if let Some(olm) = olm_machine {
|
||||
if !previous_room_encryption_state.is_encrypted() {
|
||||
// The room turned on encryption in this sync, we need
|
||||
// to also get all the existing users and mark them for
|
||||
// tracking.
|
||||
let user_ids = state_store.get_user_ids(room_id, RoomMemberships::ACTIVE).await?;
|
||||
olm.update_tracked_users(user_ids.iter().map(AsRef::as_ref)).await?
|
||||
}
|
||||
|
||||
if !user_ids_to_track.is_empty() {
|
||||
olm.update_tracked_users(user_ids_to_track.iter().map(AsRef::as_ref)).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
// 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 matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use matrix_sdk_crypto::{DecryptionSettings, RoomEventDecryptionResult};
|
||||
use ruma::{events::AnySyncTimelineEvent, serde::Raw, RoomId};
|
||||
|
||||
use super::{e2ee::E2EE, verification, Context};
|
||||
use crate::{
|
||||
latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent},
|
||||
Result, Room,
|
||||
};
|
||||
|
||||
/// Decrypt any [`Room::latest_encrypted_events`] for a particular set of
|
||||
/// [`Room`]s.
|
||||
///
|
||||
/// If we can decrypt them, change [`Room::latest_event`] to reflect what we
|
||||
/// found, and remove any older encrypted events from
|
||||
/// [`Room::latest_encrypted_events`].
|
||||
pub async fn decrypt_from_rooms(
|
||||
context: &mut Context,
|
||||
rooms: Vec<Room>,
|
||||
e2ee: E2EE<'_>,
|
||||
) -> Result<()> {
|
||||
// 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(&room, &e2ee).await {
|
||||
room.on_latest_event_decrypted(
|
||||
found,
|
||||
found_index,
|
||||
&mut context.state_changes,
|
||||
&mut context.room_info_notable_updates,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn find_suitable_and_decrypt(
|
||||
room: &Room,
|
||||
e2ee: &E2EE<'_>,
|
||||
) -> Option<(Box<LatestEvent>, usize)> {
|
||||
let enc_events = room.latest_encrypted_events();
|
||||
let power_levels = room.power_levels().await.ok();
|
||||
let power_levels_info = Some(room.own_user_id()).zip(power_levels.as_ref());
|
||||
|
||||
// Walk backwards through the encrypted events, looking for one we can decrypt
|
||||
for (i, event) in enc_events.iter().enumerate().rev() {
|
||||
// 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(event, e2ee, room.room_id()));
|
||||
|
||||
if let Ok(decrypted) = decrypt_sync_room_event.await {
|
||||
// We found an event we can decrypt
|
||||
if let Ok(any_sync_event) = decrypted.raw().deserialize() {
|
||||
// We can deserialize it to find its type
|
||||
match is_suitable_for_latest_event(&any_sync_event, power_levels_info) {
|
||||
PossibleLatestEvent::YesRoomMessage(_)
|
||||
| PossibleLatestEvent::YesPoll(_)
|
||||
| PossibleLatestEvent::YesCallInvite(_)
|
||||
| PossibleLatestEvent::YesCallNotify(_)
|
||||
| PossibleLatestEvent::YesSticker(_)
|
||||
| PossibleLatestEvent::YesKnockedStateEvent(_) => {
|
||||
return Some((Box::new(LatestEvent::new(decrypted)), i));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Attempt to decrypt the given raw event into a [`TimelineEvent`].
|
||||
///
|
||||
/// In the case of a decryption error, returns a [`TimelineEvent`]
|
||||
/// representing the decryption error; in the case of problems with our
|
||||
/// application, returns `Err`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if there is no [`OlmMachine`] in [`E2EE`].
|
||||
async fn decrypt_sync_room_event(
|
||||
event: &Raw<AnySyncTimelineEvent>,
|
||||
e2ee: &E2EE<'_>,
|
||||
room_id: &RoomId,
|
||||
) -> Result<TimelineEvent> {
|
||||
let decryption_settings =
|
||||
DecryptionSettings { sender_device_trust_requirement: e2ee.decryption_trust_requirement };
|
||||
|
||||
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) => {
|
||||
// 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(&sync_timeline_event, e2ee.clone(), room_id)
|
||||
.await?;
|
||||
}
|
||||
|
||||
event
|
||||
}
|
||||
|
||||
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
|
||||
TimelineEvent::from_utd(event.clone(), utd_info)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use matrix_sdk_test::{
|
||||
async_test, event_factory::EventFactory, JoinedRoomBuilder, SyncResponseBuilder,
|
||||
};
|
||||
use ruma::{event_id, events::room::member::MembershipState, room_id, user_id};
|
||||
|
||||
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() {
|
||||
// Given a room
|
||||
let user_id = user_id!("@u:u.to");
|
||||
let room_id = room_id!("!r:u.to");
|
||||
|
||||
let client = logged_in_base_client(Some(user_id)).await;
|
||||
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
|
||||
let response = sync_builder
|
||||
.add_joined_room(
|
||||
JoinedRoomBuilder::new(room_id).add_timeline_event(
|
||||
EventFactory::new()
|
||||
.member(user_id)
|
||||
.display_name("Alice")
|
||||
.membership(MembershipState::Join)
|
||||
.event_id(event_id!("$1")),
|
||||
),
|
||||
)
|
||||
.build_sync_response();
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
let room = client.get_room(room_id).expect("Just-created room not found!");
|
||||
|
||||
// Sanity: it has no latest_encrypted_events or latest_event
|
||||
assert!(room.latest_encrypted_events().is_empty());
|
||||
assert!(room.latest_event().is_none());
|
||||
|
||||
// When I tell it to do some decryption
|
||||
let mut context = Context::default();
|
||||
|
||||
decrypt_from_rooms(
|
||||
&mut context,
|
||||
vec![room.clone()],
|
||||
E2EE::new(
|
||||
client.olm_machine().await.as_ref(),
|
||||
client.decryption_trust_requirement,
|
||||
client.handle_verification_events,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Then nothing changed
|
||||
assert!(room.latest_encrypted_events().is_empty());
|
||||
assert!(room.latest_event().is_none());
|
||||
assert!(context.state_changes.room_infos.is_empty());
|
||||
assert!(!context
|
||||
.room_info_notable_updates
|
||||
.get(room_id)
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// 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 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")]
|
||||
pub mod verification;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use ruma::OwnedRoomId;
|
||||
|
||||
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) -> 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,58 @@
|
||||
// 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::member::{MembershipState, RoomMemberEventContent},
|
||||
SyncStateEvent,
|
||||
},
|
||||
RoomId,
|
||||
};
|
||||
|
||||
use super::Context;
|
||||
|
||||
/// Decide whether the profile must be created, updated or deleted based on the
|
||||
/// [`RoomMemberEventContent`].
|
||||
pub fn upsert_or_delete(
|
||||
context: &mut Context,
|
||||
room_id: &RoomId,
|
||||
event: &SyncStateEvent<RoomMemberEventContent>,
|
||||
) {
|
||||
// Senders can fake the profile easily so we keep track of profiles that the
|
||||
// member set themselves to avoid having confusing profile changes when a
|
||||
// member gets kicked/banned.
|
||||
if event.state_key() == event.sender() {
|
||||
context
|
||||
.state_changes
|
||||
.profiles
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.insert(event.sender().to_owned(), event.into());
|
||||
}
|
||||
|
||||
if *event.membership() == MembershipState::Invite {
|
||||
// Remove any profile previously stored for the invited user.
|
||||
//
|
||||
// A room member could have joined the room and left it later; in that case, the
|
||||
// server may return a dummy, empty profile along the `leave` event. We
|
||||
// don't want to reuse that empty profile when the member has been
|
||||
// re-invited, so we remove it from the database.
|
||||
context
|
||||
.state_changes
|
||||
.profiles_to_delete
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.push(event.state_key().clone());
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
// 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::AnySyncStateEvent, serde::Raw};
|
||||
use serde::Deserialize;
|
||||
use tracing::warn;
|
||||
|
||||
use super::Context;
|
||||
|
||||
/// Collect [`AnySyncStateEvent`].
|
||||
pub mod sync {
|
||||
use std::{collections::BTreeSet, iter};
|
||||
|
||||
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(
|
||||
raw_events: &[Raw<AnySyncStateEvent>],
|
||||
) -> (Vec<Raw<AnySyncStateEvent>>, Vec<AnySyncStateEvent>) {
|
||||
super::collect(raw_events)
|
||||
}
|
||||
|
||||
/// Collect [`AnySyncTimelineEvent`] to [`AnySyncStateEvent`].
|
||||
///
|
||||
/// A [`AnySyncTimelineEvent`] can represent either message-like events or
|
||||
/// state events. The message-like events are filtered out.
|
||||
pub fn collect_from_timeline(
|
||||
raw_events: &[Raw<AnySyncTimelineEvent>],
|
||||
) -> (Vec<Raw<AnySyncStateEvent>>, Vec<AnySyncStateEvent>) {
|
||||
super::collect(raw_events.iter().filter_map(|raw_event| {
|
||||
// Only state events have a `state_key` field.
|
||||
match raw_event.get_field::<&str>("state_key") {
|
||||
Ok(Some(_)) => Some(raw_event.cast_ref()),
|
||||
_ => None,
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/// 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 std::{collections::BTreeMap, iter};
|
||||
|
||||
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(
|
||||
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>)
|
||||
where
|
||||
I: IntoIterator<Item = &'a Raw<T>>,
|
||||
T: Deserialize<'a> + 'a,
|
||||
{
|
||||
raw_events
|
||||
.into_iter()
|
||||
.filter_map(|raw_event| match raw_event.deserialize() {
|
||||
Ok(event) => Some((raw_event.clone(), event)),
|
||||
Err(e) => {
|
||||
warn!("Couldn't deserialize stripped state event: {e}");
|
||||
None
|
||||
}
|
||||
})
|
||||
.unzip()
|
||||
}
|
||||
|
||||
#[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};
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
// 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 matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::events::SyncMessageLikeEvent;
|
||||
use ruma::{
|
||||
events::{
|
||||
room::power_levels::{
|
||||
RoomPowerLevelsEvent, RoomPowerLevelsEventContent, StrippedRoomPowerLevelsEvent,
|
||||
},
|
||||
AnyStrippedStateEvent, AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
|
||||
StateEventType,
|
||||
},
|
||||
push::{Action, PushConditionRoomCtx},
|
||||
RoomVersionId, UInt, UserId,
|
||||
};
|
||||
use tracing::{instrument, trace, warn};
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use super::{e2ee, verification};
|
||||
use super::{notification, Context};
|
||||
use crate::{
|
||||
store::{BaseStateStore, StateStoreExt as _},
|
||||
sync::Timeline,
|
||||
Result, Room, RoomInfo,
|
||||
};
|
||||
|
||||
/// Process a set of sync timeline event, and create a [`Timeline`].
|
||||
///
|
||||
/// For each event:
|
||||
/// - will try to decrypt it,
|
||||
/// - will process verification,
|
||||
/// - will process redaction,
|
||||
/// - will process notification.
|
||||
#[instrument(skip_all, fields(room_id = ?room_info.room_id))]
|
||||
pub async fn build<'notification, 'e2ee>(
|
||||
context: &mut Context,
|
||||
room: &Room,
|
||||
room_info: &mut RoomInfo,
|
||||
timeline_inputs: builder::Timeline,
|
||||
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_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::from_plaintext(raw_event);
|
||||
|
||||
// Do some special stuff on the `timeline_event` before collecting it.
|
||||
match timeline_event.raw().deserialize() {
|
||||
Ok(sync_timeline_event) => {
|
||||
match &sync_timeline_event {
|
||||
// State events are ignored. They must be processed separately.
|
||||
AnySyncTimelineEvent::State(_) => {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
// A room redaction.
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomRedaction(
|
||||
redaction_event,
|
||||
)) => {
|
||||
let room_version = room_info.room_version().unwrap_or(&RoomVersionId::V1);
|
||||
|
||||
if let Some(redacts) = redaction_event.redacts(room_version) {
|
||||
room_info
|
||||
.handle_redaction(redaction_event, timeline_event.raw().cast_ref());
|
||||
|
||||
context.state_changes.add_redaction(
|
||||
room_id,
|
||||
redacts,
|
||||
timeline_event.raw().clone().cast(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Decrypt encrypted event, or process verification event.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
AnySyncTimelineEvent::MessageLike(sync_message_like_event) => {
|
||||
match sync_message_like_event {
|
||||
AnySyncMessageLikeEvent::RoomEncrypted(
|
||||
SyncMessageLikeEvent::Original(_),
|
||||
) => {
|
||||
if let Some(decrypted_timeline_event) =
|
||||
Box::pin(e2ee::decrypt::sync_timeline_event(
|
||||
e2ee.clone(),
|
||||
timeline_event.raw(),
|
||||
room_id,
|
||||
))
|
||||
.await?
|
||||
{
|
||||
timeline_event = decrypted_timeline_event;
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
Box::pin(verification::process_if_relevant(
|
||||
&sync_timeline_event,
|
||||
e2ee.clone(),
|
||||
room_id,
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing particular to do.
|
||||
#[cfg(not(feature = "e2e-encryption"))]
|
||||
AnySyncTimelineEvent::MessageLike(_) => (),
|
||||
}
|
||||
|
||||
if let Some(push_condition_room_ctx) = &mut push_condition_room_ctx {
|
||||
update_push_room_context(
|
||||
context,
|
||||
push_condition_room_ctx,
|
||||
room.own_user_id(),
|
||||
room_info,
|
||||
)
|
||||
} else {
|
||||
push_condition_room_ctx =
|
||||
get_push_room_context(context, room, room_info, notification.state_store)
|
||||
.await?;
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
timeline_event.set_push_actions(actions.to_owned());
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
warn!("Error deserializing event: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, we have process the timeline event. We can collect it.
|
||||
timeline.events.push(timeline_event);
|
||||
}
|
||||
|
||||
Ok(timeline)
|
||||
}
|
||||
|
||||
/// Set of types used by [`build`] to reduce the number of arguments by grouping
|
||||
/// them by thematics.
|
||||
pub mod builder {
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::{v3, v5},
|
||||
events::AnySyncTimelineEvent,
|
||||
serde::Raw,
|
||||
};
|
||||
|
||||
pub struct Timeline {
|
||||
pub limited: bool,
|
||||
pub raw_events: Vec<Raw<AnySyncTimelineEvent>>,
|
||||
pub prev_batch: Option<String>,
|
||||
}
|
||||
|
||||
impl From<v3::Timeline> for Timeline {
|
||||
fn from(value: v3::Timeline) -> Self {
|
||||
Self { limited: value.limited, raw_events: value.events, prev_batch: value.prev_batch }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&v5::response::Room> for Timeline {
|
||||
fn from(value: &v5::response::Room) -> Self {
|
||||
Self {
|
||||
limited: value.limited,
|
||||
raw_events: value.timeline.clone(),
|
||||
prev_batch: value.prev_batch.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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: &Context,
|
||||
push_rules: &mut PushConditionRoomCtx,
|
||||
user_id: &UserId,
|
||||
room_info: &RoomInfo,
|
||||
) {
|
||||
let room_id = &*room_info.room_id;
|
||||
|
||||
push_rules.member_count = UInt::new(room_info.active_members_count()).unwrap_or(UInt::MAX);
|
||||
|
||||
// TODO: Use if let chain once stable
|
||||
if let Some(AnySyncStateEvent::RoomMember(member)) =
|
||||
context.state_changes.state.get(room_id).and_then(|events| {
|
||||
events.get(&StateEventType::RoomMember)?.get(user_id.as_str())?.deserialize().ok()
|
||||
})
|
||||
{
|
||||
push_rules.user_display_name = member
|
||||
.as_original()
|
||||
.and_then(|ev| ev.content.displayname.clone())
|
||||
.unwrap_or_else(|| user_id.localpart().to_owned())
|
||||
}
|
||||
|
||||
if let Some(AnySyncStateEvent::RoomPowerLevels(event)) =
|
||||
context.state_changes.state.get(room_id).and_then(|types| {
|
||||
types.get(&StateEventType::RoomPowerLevels)?.get("")?.deserialize().ok()
|
||||
})
|
||||
{
|
||||
push_rules.power_levels = Some(event.power_levels().into());
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the push context for the given room.
|
||||
///
|
||||
/// Tries to get the data from `changes` or the up to date `room_info`.
|
||||
/// Loads the data from the store otherwise.
|
||||
///
|
||||
/// 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: &Context,
|
||||
room: &Room,
|
||||
room_info: &RoomInfo,
|
||||
state_store: &BaseStateStore,
|
||||
) -> Result<Option<PushConditionRoomCtx>> {
|
||||
let room_id = room.room_id();
|
||||
let user_id = room.own_user_id();
|
||||
|
||||
let member_count = room_info.active_members_count();
|
||||
|
||||
// TODO: Use if let chain once stable
|
||||
let user_display_name = if let Some(AnySyncStateEvent::RoomMember(member)) =
|
||||
context.state_changes.state.get(room_id).and_then(|events| {
|
||||
events.get(&StateEventType::RoomMember)?.get(user_id.as_str())?.deserialize().ok()
|
||||
}) {
|
||||
member
|
||||
.as_original()
|
||||
.and_then(|ev| ev.content.displayname.clone())
|
||||
.unwrap_or_else(|| user_id.localpart().to_owned())
|
||||
} else if let Some(AnyStrippedStateEvent::RoomMember(member)) =
|
||||
context.state_changes.stripped_state.get(room_id).and_then(|events| {
|
||||
events.get(&StateEventType::RoomMember)?.get(user_id.as_str())?.deserialize().ok()
|
||||
})
|
||||
{
|
||||
member.content.displayname.unwrap_or_else(|| user_id.localpart().to_owned())
|
||||
} else if let Some(member) = Box::pin(room.get_member(user_id)).await? {
|
||||
member.name().to_owned()
|
||||
} else {
|
||||
trace!("Couldn't get push context because of missing own member information");
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let power_levels = if let Some(event) =
|
||||
context.state_changes.state.get(room_id).and_then(|types| {
|
||||
types
|
||||
.get(&StateEventType::RoomPowerLevels)?
|
||||
.get("")?
|
||||
.deserialize_as::<RoomPowerLevelsEvent>()
|
||||
.ok()
|
||||
}) {
|
||||
Some(event.power_levels().into())
|
||||
} else if let Some(event) =
|
||||
context.state_changes.stripped_state.get(room_id).and_then(|types| {
|
||||
types
|
||||
.get(&StateEventType::RoomPowerLevels)?
|
||||
.get("")?
|
||||
.deserialize_as::<StrippedRoomPowerLevelsEvent>()
|
||||
.ok()
|
||||
})
|
||||
{
|
||||
Some(event.power_levels().into())
|
||||
} else {
|
||||
state_store
|
||||
.get_state_event_static::<RoomPowerLevelsEventContent>(room_id)
|
||||
.await?
|
||||
.and_then(|e| e.deserialize().ok())
|
||||
.map(|event| event.power_levels().into())
|
||||
};
|
||||
|
||||
Ok(Some(PushConditionRoomCtx {
|
||||
user_id: user_id.to_owned(),
|
||||
room_id: room_id.to_owned(),
|
||||
member_count: UInt::new(member_count).unwrap_or(UInt::MAX),
|
||||
user_display_name,
|
||||
power_levels,
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// 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::message::MessageType, AnySyncMessageLikeEvent, AnySyncTimelineEvent,
|
||||
SyncMessageLikeEvent,
|
||||
},
|
||||
RoomId,
|
||||
};
|
||||
|
||||
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(
|
||||
event: &AnySyncTimelineEvent,
|
||||
e2ee: E2EE<'_>,
|
||||
room_id: &RoomId,
|
||||
) -> Result<()> {
|
||||
if !e2ee.verification_is_allowed {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
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(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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user