Compare commits
1027 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 18b169ca7e | |||
| b9ce4059fb | |||
| e8dcb5d250 | |||
| d0c01006e4 | |||
| dc98bf7633 | |||
| ae57156252 | |||
| 3b09c60e20 | |||
| fcdb63dcbe | |||
| a095872083 | |||
| d68895f24a | |||
| ab81388018 | |||
| c5436ed73e | |||
| 2e7721b36c | |||
| a514019d7c | |||
| 2f8f39795f | |||
| b1ef15c346 | |||
| 829b6a7624 | |||
| 7b2cd8e434 | |||
| d567a45bee | |||
| 9c08cd8973 | |||
| e0ceef33f8 | |||
| 72d133260c | |||
| 91e0c76a2f | |||
| 7fac1d246d | |||
| 6762e70880 | |||
| 6b93f6698b | |||
| c92a89d571 | |||
| 684f228e70 | |||
| 0f9faad48a | |||
| d7550ec645 | |||
| ea5645869e | |||
| 8eed17bbfd | |||
| 40c08335ee | |||
| ae5ec0fa26 | |||
| 880f754f32 | |||
| 4d23b6490d | |||
| e0e531c737 | |||
| 82926d6f08 | |||
| f45c9aa3a7 | |||
| 7966dd0544 | |||
| 54e6a7d8d1 | |||
| 995ec618df | |||
| f9864b7ef4 | |||
| d23eae262e | |||
| 52090bb199 | |||
| 40e3cd3c22 | |||
| a58a74eaa7 | |||
| 1c549a3ca1 | |||
| a7bef8870f | |||
| 5e946108fe | |||
| 34ccd26ee6 | |||
| 3eeb046e62 | |||
| 6f84a44a1c | |||
| cf16978b15 | |||
| 6a30a802bb | |||
| db477a84bf | |||
| 96fbbd3cd8 | |||
| bbbcec5963 | |||
| df98b71836 | |||
| 0d901e4a86 | |||
| 9d7afaaa1c | |||
| 2fc616645f | |||
| b8f6ab066d | |||
| bb7e6cb562 | |||
| 3e81514d07 | |||
| bfcf47743e | |||
| 69448cca61 | |||
| 5daf2922b7 | |||
| c81a56c22b | |||
| f891bd13cb | |||
| a50a570fc1 | |||
| ec112ca32d | |||
| 15fdf1e86e | |||
| 3d6d798ca3 | |||
| 935ffa5aea | |||
| 1c19e7477c | |||
| 6a1576a085 | |||
| a1f028c54a | |||
| f2576e80ec | |||
| e8f705d76f | |||
| 049993d37e | |||
| 14366e85b1 | |||
| 849d705cd1 | |||
| d4adc81fe0 | |||
| 577a8feb12 | |||
| 7fb3d216f6 | |||
| 07808b4301 | |||
| f9e7d16347 | |||
| c98b2a1b3f | |||
| b44a1e46c4 | |||
| f17c3c5af4 | |||
| 5448192ea4 | |||
| 74800e20b4 | |||
| 8157193aef | |||
| 679c99aa76 | |||
| 6fa76e4b12 | |||
| 8b33806496 | |||
| 3d114aea50 | |||
| 72dcf5ed46 | |||
| 58748bec3a | |||
| f0b6225e40 | |||
| 17a58684f6 | |||
| f8c468d6fa | |||
| 39d1ed9bc6 | |||
| 3d1d1c8f6d | |||
| 5ad958722f | |||
| bedcbfd7ff | |||
| 743dec9a65 | |||
| f1a2093cfc | |||
| 8057991aee | |||
| d45ef567d4 | |||
| 42ee967b46 | |||
| 4c7575bc9e | |||
| 5e927f8109 | |||
| f91ee36245 | |||
| 0f8fc53019 | |||
| be3af5e0d4 | |||
| 38bbdf0547 | |||
| b188a157af | |||
| 9ccbac0c0e | |||
| 137fc9cfbb | |||
| 40a4c9a7e1 | |||
| ad358955fd | |||
| 0ee89c86ab | |||
| 216f0df945 | |||
| 129e9e173e | |||
| f7df0ebf97 | |||
| 2f78701374 | |||
| 872bded711 | |||
| 5196298e5f | |||
| 308526a6bc | |||
| 12f94a3fd2 | |||
| 3c873262c7 | |||
| 9689c4a40a | |||
| 57e7ae488e | |||
| a9ffe5fd72 | |||
| 59c29801e5 | |||
| 0a822c1a06 | |||
| 4b5e1c6676 | |||
| ecb9d4d2e8 | |||
| 9ade32fcd0 | |||
| 8b4a01ea54 | |||
| d5d5b9ee01 | |||
| a3dd594c9e | |||
| 98c331466e | |||
| e8877fd987 | |||
| babf16f15a | |||
| 000419cdf3 | |||
| 89d661ca8c | |||
| 1624d798ee | |||
| 726218000a | |||
| 08dcb267b3 | |||
| da0a32b088 | |||
| e22a833057 | |||
| 8aba664578 | |||
| e117a3d22f | |||
| c2f50fd8a5 | |||
| dc90c77c7d | |||
| 6c9038eb4f | |||
| 2b9b4cc589 | |||
| dd0336ee72 | |||
| 0095912091 | |||
| e6774a34da | |||
| 8ad52e34ea | |||
| 60a7bf0c3f | |||
| 8b31d8f6a3 | |||
| e21dd763e8 | |||
| 31df84f5a1 | |||
| e68bdf8460 | |||
| 6ca1f16f48 | |||
| 8c5d878172 | |||
| d6239d614a | |||
| 5af084c8c9 | |||
| dc450ac25a | |||
| cf375dd753 | |||
| ff935df136 | |||
| 0d080935cf | |||
| 4d140d8155 | |||
| cef1f8c5cb | |||
| 6a054d6c74 | |||
| 1f89efb88d | |||
| e83c09e425 | |||
| a85dac1f52 | |||
| a0bc9aafcf | |||
| 8217f967d4 | |||
| 47c9585606 | |||
| 20e09531fb | |||
| d92b33f959 | |||
| a74bcfab8f | |||
| 6dcc744b48 | |||
| 9d1c296657 | |||
| 3c7683ea53 | |||
| cd03a58083 | |||
| 4a1249fa96 | |||
| 06732ca71a | |||
| 115c7578d4 | |||
| 9f3e7debb1 | |||
| 58d2ae4c39 | |||
| 8a847a99d4 | |||
| 3d642356c6 | |||
| b4b0f3a203 | |||
| ca99977207 | |||
| dd02274883 | |||
| ad2e3a3b8f | |||
| 96119f9a30 | |||
| 737e06b581 | |||
| 4ecd599c15 | |||
| c6521a8aaf | |||
| 4046a59786 | |||
| d931cd0ea7 | |||
| a14488617e | |||
| 1de51614f1 | |||
| 6cc98ee9f7 | |||
| 3a98d46bfa | |||
| 1558858bde | |||
| e4d2f62d48 | |||
| 70f48be582 | |||
| 836c643769 | |||
| a48099d5ac | |||
| 09d8be7b4c | |||
| 03b8cabc22 | |||
| 07372c475c | |||
| a00e4089e8 | |||
| 1a24b21d42 | |||
| f51496fa0f | |||
| c6ce9c560b | |||
| 60af16ada8 | |||
| 159fb73b0a | |||
| 0fa0f2329d | |||
| 3b64d18c99 | |||
| f67fd87e57 | |||
| 5e64da660c | |||
| d152ce13a0 | |||
| d021020ee6 | |||
| 7e5f22ba9e | |||
| 6bc6ea4e72 | |||
| 585ae29868 | |||
| 3919c2a89a | |||
| 7c85e7aa4f | |||
| 4b845e17c8 | |||
| 394124cda5 | |||
| bbf9bf2c0b | |||
| 67327a0365 | |||
| 5e40426b99 | |||
| 0f264cac6e | |||
| 3c1d0b37e5 | |||
| 62231878cc | |||
| 22c99f30f3 | |||
| a7efff9849 | |||
| bc9192f818 | |||
| 0722ed9d8f | |||
| 1aa933cfd6 | |||
| e0ab16f979 | |||
| a9c999af72 | |||
| 8156413132 | |||
| e551efec8d | |||
| 877a7d678f | |||
| 457af2a2f8 | |||
| 7b38c442c7 | |||
| 201b818cc8 | |||
| 1f98e0cd19 | |||
| 21c59c95c4 | |||
| 4025c11e73 | |||
| dc6130562a | |||
| d1a14f895e | |||
| b680705d15 | |||
| 2b1ee853fc | |||
| ef137730cb | |||
| 45caaffb26 | |||
| 50c3217353 | |||
| b5dafd9798 | |||
| 5a39fd051b | |||
| ff52cf36dd | |||
| 25d217cc6f | |||
| 2116ad82df | |||
| fe4109cb9a | |||
| 41f107e5ba | |||
| ab699a90f1 | |||
| ddee7f8ccd | |||
| 2ab5ab527b | |||
| 53e3b90436 | |||
| 08e1d3876b | |||
| c3179ea5ed | |||
| 9676daee5a | |||
| 798cece4a2 | |||
| 06b387101b | |||
| 675963ec4b | |||
| d30dae3322 | |||
| bdb640a126 | |||
| ea28234d95 | |||
| c74295c604 | |||
| ec30e7b85c | |||
| fd17c28ebb | |||
| 841131f127 | |||
| a22d592bf1 | |||
| 1a32aa59a6 | |||
| a99df7e1d8 | |||
| 3e37f9d0ad | |||
| 2689e2d25a | |||
| 2cfba4cd9b | |||
| 1bce2af93c | |||
| 47c8df0ef8 | |||
| 9f32dfe9a0 | |||
| 040fd6c736 | |||
| 4dac175db0 | |||
| 5faf97cf99 | |||
| 7236b80b3b | |||
| 79b0941687 | |||
| ad001e475f | |||
| 5106d55be9 | |||
| 9771b99395 | |||
| 2c287e706f | |||
| fffff783d4 | |||
| b047bd0dc6 | |||
| 28b3b6aedf | |||
| 171974a44b | |||
| f53302a7a0 | |||
| dd709682d7 | |||
| 991e0cd395 | |||
| abcc05f889 | |||
| f3e636ea42 | |||
| 1d47507faa | |||
| cc7f6243c6 | |||
| e7e9d5b746 | |||
| b4146caac8 | |||
| 1cb51f49be | |||
| fea0e0d373 | |||
| 72911c66ad | |||
| dc047854d4 | |||
| f51a008921 | |||
| 3c5bcce217 | |||
| 422fd19d10 | |||
| 0ea07e11e9 | |||
| 059a6fa573 | |||
| 07656c2e26 | |||
| 940325574b | |||
| 9f8824b9a5 | |||
| cd141c5b84 | |||
| 9596aa0830 | |||
| 11424ce443 | |||
| cd4ec90b38 | |||
| 4680354abd | |||
| 145d6c5782 | |||
| a955af61e1 | |||
| 2a78b5b67a | |||
| 9d29c36531 | |||
| ed9c7d90b4 | |||
| 2af23d052c | |||
| fb80e06839 | |||
| b8f9cba5e7 | |||
| d119b01322 | |||
| 85833c74ba | |||
| 5b20136a50 | |||
| 362ca2bd59 | |||
| 0a9a849826 | |||
| 7126fc8a29 | |||
| f4e612ca9e | |||
| 6ab11a0323 | |||
| 76626db613 | |||
| bcea1d32e6 | |||
| 346f11319c | |||
| 937b223627 | |||
| 000d8514f6 | |||
| 72692b7b33 | |||
| 0f84d482b9 | |||
| c609150a3e | |||
| 2f46a6c8a0 | |||
| 7bdddc9d35 | |||
| 5113f114a7 | |||
| 9d96d6ead2 | |||
| c340a7187a | |||
| 0aece695dc | |||
| b2210292bf | |||
| f0ab6cb1a4 | |||
| c2eeca3f33 | |||
| cc974dd3c9 | |||
| 8b2a8e7265 | |||
| 7cad237dc6 | |||
| 72a3972303 | |||
| 2e590e2f67 | |||
| 224e437a78 | |||
| 8a9cae4af3 | |||
| 22a15f1342 | |||
| 3ab4584dfe | |||
| a3238cdadf | |||
| a884b2c696 | |||
| ec0d7b4311 | |||
| e8c2d27c9e | |||
| bff600a937 | |||
| 404a982503 | |||
| e904a98735 | |||
| b55e79fdac | |||
| 717116cc05 | |||
| 0ad4df2031 | |||
| 891e9813b1 | |||
| 19b21fdd49 | |||
| 307fa355ad | |||
| 351053fef5 | |||
| 8c735c602a | |||
| 7ffc390cea | |||
| 05b67df6e2 | |||
| f3f3d968b5 | |||
| bde1d4a353 | |||
| 4f6ddcd072 | |||
| b99188dd59 | |||
| e2fee14ced | |||
| 9fca8f0007 | |||
| ca0fc3cf6d | |||
| 378f50d8b5 | |||
| 485bb0790e | |||
| 0e9ce0271e | |||
| c0294d5e33 | |||
| 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 |
@@ -2,3 +2,12 @@
|
||||
retries = { backoff = "exponential", count = 3, delay = "1s", jitter = true }
|
||||
# kill the slow tests if they still aren't up after 180s
|
||||
slow-timeout = { period = "60s", terminate-after = 3 }
|
||||
|
||||
[profile.ci]
|
||||
retries = { backoff = "exponential", count = 4, delay = "1s", jitter = true }
|
||||
# kill the slow tests if they still aren't up after 180s
|
||||
slow-timeout = { period = "60s", terminate-after = 3 }
|
||||
|
||||
[profile.ci.junit]
|
||||
path = "junit.xml"
|
||||
store-success-output = true
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ allow-git = [
|
||||
# A patch override for the bindings fixing a bug for Android before upstream
|
||||
# releases a new version.
|
||||
"https://github.com/element-hq/tracing.git",
|
||||
# Sam as for the tracing dependency.
|
||||
# Same as for the tracing dependency.
|
||||
"https://github.com/element-hq/paranoid-android.git",
|
||||
# Well, it's Ruma.
|
||||
"https://github.com/ruma/ruma",
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2025-02-20
|
||||
toolchain: nightly-2025-06-27
|
||||
components: rustfmt
|
||||
|
||||
- name: Run Benchmarks
|
||||
|
||||
@@ -175,7 +175,7 @@ jobs:
|
||||
run: swift test
|
||||
|
||||
- name: Build Framework
|
||||
run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=reldbg
|
||||
run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=dev --ios-deployment-target=18.0
|
||||
|
||||
complement-crypto:
|
||||
name: "Run Complement Crypto tests"
|
||||
|
||||
@@ -6,11 +6,6 @@ on:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -251,7 +246,7 @@ jobs:
|
||||
components: clippy
|
||||
|
||||
- name: Install wasm-pack
|
||||
uses: qmaru/wasm-pack-action@v0.5.0
|
||||
uses: qmaru/wasm-pack-action@v0.5.1
|
||||
if: '!matrix.check_only'
|
||||
with:
|
||||
version: v0.10.3
|
||||
@@ -295,7 +290,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check the spelling of the files in our repo
|
||||
uses: crate-ci/typos@v1.31.1
|
||||
uses: crate-ci/typos@v1.34.0
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
@@ -314,7 +309,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2025-02-20
|
||||
toolchain: nightly-2025-06-27
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Load cache
|
||||
@@ -380,7 +375,6 @@ jobs:
|
||||
RUST_LOG: "info,matrix_sdk=trace"
|
||||
HOMESERVER_URL: "http://localhost:8008"
|
||||
HOMESERVER_DOMAIN: "synapse"
|
||||
SLIDING_SYNC_PROXY_URL: "http://localhost:8118"
|
||||
run: |
|
||||
cargo nextest run -p matrix-sdk-integration-testing
|
||||
|
||||
@@ -403,4 +397,3 @@ jobs:
|
||||
- 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 }}
|
||||
@@ -22,8 +17,12 @@ env:
|
||||
RUST_LOG: info,matrix_sdk=trace
|
||||
|
||||
jobs:
|
||||
xtask:
|
||||
uses: ./.github/workflows/xtask.yml
|
||||
|
||||
code_coverage:
|
||||
name: Code Coverage
|
||||
needs: xtask
|
||||
runs-on: "ubuntu-latest"
|
||||
|
||||
# run several docker containers with the same networking stack so the hostname 'synapse'
|
||||
@@ -40,6 +39,11 @@ jobs:
|
||||
- 8008:8008
|
||||
|
||||
steps:
|
||||
# This CI workflow can run into space issue, so we're cleaning up some
|
||||
# space here.
|
||||
- name: Create some more space
|
||||
run: rm -rf /opt/hostedtoolcache
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -61,29 +65,30 @@ jobs:
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: "coverage"
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install tarpaulin
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-tarpaulin
|
||||
- name: Install cargo-llvm-cov
|
||||
uses: taiki-e/install-action@cargo-llvm-cov
|
||||
|
||||
# set up backend for integration tests
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Run tarpaulin
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Create the coverage report
|
||||
run: |
|
||||
rustup run stable cargo tarpaulin \
|
||||
--skip-clean --profile cov --out xml \
|
||||
--features experimental-widgets,testing
|
||||
target/debug/xtask ci coverage -o codecov
|
||||
env:
|
||||
CARGO_PROFILE_COV_INHERITS: 'dev'
|
||||
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
|
||||
@@ -95,6 +100,11 @@ jobs:
|
||||
echo "Storing commit SHA ${{ github.event.pull_request.head.sha }}"
|
||||
echo "${{ github.event.pull_request.head.sha }}" > commit_sha.txt
|
||||
|
||||
- name: Move the JUnit file into the root directory
|
||||
shell: bash
|
||||
run: |
|
||||
mv target/nextest/ci/junit.xml ./junit.xml
|
||||
|
||||
# This stores the coverage report and metadata in artifacts.
|
||||
# The actual upload to Codecov is executed by a different workflow `upload_coverage.yml`.
|
||||
# The reason for this split is because `on.pull_request` workflows don't have access to secrets.
|
||||
@@ -103,7 +113,8 @@ jobs:
|
||||
with:
|
||||
name: codecov_report
|
||||
path: |
|
||||
cobertura.xml
|
||||
coverage.xml
|
||||
junit.xml
|
||||
pr_number.txt
|
||||
commit_sha.txt
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -7,11 +7,6 @@ on:
|
||||
workflow_dispatch:
|
||||
pull_request: # focus on the changed files in current PR
|
||||
branches: [main]
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -25,7 +20,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@6f67ee9ac810f0192ea7b3d2086406f97847bcf9 # v45
|
||||
uses: tj-actions/changed-files@v46.0.5
|
||||
- name: Detect long path
|
||||
env:
|
||||
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} # ignore the deleted files
|
||||
|
||||
@@ -4,11 +4,6 @@ on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -36,7 +31,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2025-02-20
|
||||
toolchain: nightly-2025-06-27
|
||||
|
||||
- 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:
|
||||
|
||||
@@ -77,3 +77,20 @@ jobs:
|
||||
working-directory: ${{ github.workspace }}/repo_root
|
||||
# Location where coverage report files are searched for
|
||||
directory: ${{ github.workspace }}
|
||||
|
||||
- name: Upload test results to Codecov
|
||||
uses: codecov/test-results-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
|
||||
# Manual overrides for these parameters are needed because automatic detection
|
||||
# in codecov-action does not work for non-`pull_request` workflows.
|
||||
# In `main` branch push, these default to empty strings since we want to run
|
||||
# the analysis on HEAD.
|
||||
override_commit: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
|
||||
override_pr: ${{ steps.parse_previous_artifacts.outputs.override_pr || '' }}
|
||||
working-directory: ${{ github.workspace }}/repo_root
|
||||
|
||||
# Location where coverage report files are searched for
|
||||
directory: ${{ github.workspace }}
|
||||
|
||||
@@ -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
+422
-155
File diff suppressed because it is too large
Load Diff
+57
-49
@@ -4,15 +4,13 @@ members = [
|
||||
"bindings/matrix-sdk-crypto-ffi",
|
||||
"bindings/matrix-sdk-ffi",
|
||||
"crates/*",
|
||||
"testing/*",
|
||||
"examples/*",
|
||||
"labs/*",
|
||||
"testing/*",
|
||||
"uniffi-bindgen",
|
||||
"xtask",
|
||||
]
|
||||
exclude = [
|
||||
"testing/data",
|
||||
]
|
||||
exclude = ["testing/data"]
|
||||
# xtask, testing and the bindings should only be built when invoked explicitly.
|
||||
default-members = ["benchmarks", "crates/*", "labs/*"]
|
||||
resolver = "2"
|
||||
@@ -23,14 +21,16 @@ rust-version = "1.85"
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.95"
|
||||
aquamarine = "0.6.0"
|
||||
as_variant = "1.3.0"
|
||||
assert-json-diff = "2.0.2"
|
||||
assert_matches = "1.5.0"
|
||||
assert_matches2 = "0.1.2"
|
||||
async-compat = "0.2.4"
|
||||
async-rx = "0.1.3"
|
||||
async-stream = "0.3.5"
|
||||
async-trait = "0.1.85"
|
||||
as_variant = "1.3.0"
|
||||
base64 = "0.22.1"
|
||||
bitflags = "2.8.0"
|
||||
byteorder = "1.5.0"
|
||||
chrono = "0.4.39"
|
||||
eyeball = { version = "0.8.8", features = ["tracing"] }
|
||||
@@ -60,23 +60,29 @@ reqwest = { version = "0.12.12", default-features = false }
|
||||
rmp-serde = "1.3.0"
|
||||
# Be careful to use commits from the https://github.com/ruma/ruma/tree/ruma-0.12
|
||||
# branch until a proper release with breaking changes happens.
|
||||
ruma = { version = "0.12.2", features = [
|
||||
ruma = { version = "0.12.5", features = [
|
||||
"client-api-c",
|
||||
"compat-upload-signatures",
|
||||
"compat-user-id",
|
||||
"compat-arbitrary-length-ids",
|
||||
"compat-tag-info",
|
||||
"compat-encrypted-stickers",
|
||||
"compat-lax-room-create-deser",
|
||||
"compat-lax-room-topic-deser",
|
||||
"unstable-msc3401",
|
||||
"unstable-msc3266",
|
||||
"unstable-msc3488",
|
||||
"unstable-msc3489",
|
||||
"unstable-msc4075",
|
||||
"unstable-msc4140",
|
||||
"unstable-msc4143",
|
||||
"unstable-msc4171",
|
||||
"unstable-msc4278",
|
||||
"unstable-msc4286",
|
||||
] }
|
||||
ruma-common = "0.15.2"
|
||||
serde = "1.0.217"
|
||||
ruma-common = "0.15.4"
|
||||
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 +90,7 @@ similar-asserts = "1.6.1"
|
||||
stream_assert = "0.1.1"
|
||||
tempfile = "3.16.0"
|
||||
thiserror = "2.0.11"
|
||||
tokio = { version = "1.43.0", default-features = false, features = ["sync"] }
|
||||
tokio = { version = "1.43.1", default-features = false, features = ["sync"] }
|
||||
tokio-stream = "0.1.17"
|
||||
tracing = { version = "0.1.40", default-features = false, features = ["std"] }
|
||||
tracing-core = "0.1.32"
|
||||
@@ -96,22 +102,52 @@ url = "2.5.4"
|
||||
uuid = "1.12.1"
|
||||
vodozemac = { version = "0.9.0", features = ["insecure-pk-encryption"] }
|
||||
wasm-bindgen = "0.2.84"
|
||||
wasm-bindgen-test = "0.3.33"
|
||||
wasm-bindgen-test = "0.3.50"
|
||||
web-sys = "0.3.69"
|
||||
wiremock = "0.6.2"
|
||||
zeroize = "1.8.1"
|
||||
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.11.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.11.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.11.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.11.0" }
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.13.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.13.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.13.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.13.0" }
|
||||
matrix-sdk-ffi-macros = { path = "bindings/matrix-sdk-ffi-macros", version = "0.7.0" }
|
||||
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.11.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.11.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.11.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.11.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.11.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.11.0", default-features = false }
|
||||
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.13.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.13.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.13.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.13.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.13.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.13.0", default-features = false }
|
||||
|
||||
[workspace.lints.rust]
|
||||
rust_2018_idioms = "warn"
|
||||
semicolon_in_expressions_from_macros = "warn"
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(tarpaulin_include)', # Used by tarpaulin (code coverage)
|
||||
'cfg(ruma_unstable_exhaustive_types)', # Used by Ruma's EventContent derive macro
|
||||
] }
|
||||
unused_extern_crates = "warn"
|
||||
unused_import_braces = "warn"
|
||||
unused_qualifications = "warn"
|
||||
trivial_casts = "warn"
|
||||
trivial_numeric_casts = "warn"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
assigning_clones = "allow"
|
||||
box_default = "allow"
|
||||
cloned_instead_of_copied = "warn"
|
||||
dbg_macro = "warn"
|
||||
inefficient_to_string = "warn"
|
||||
macro_use_imports = "warn"
|
||||
manual_let_else = "warn"
|
||||
mut_mut = "warn"
|
||||
needless_borrow = "warn"
|
||||
nonstandard_macro_braces = "warn"
|
||||
redundant_clone = "warn"
|
||||
str_to_string = "warn"
|
||||
todo = "warn"
|
||||
unnecessary_semicolon = "warn"
|
||||
unused_async = "warn"
|
||||
|
||||
# Default development profile; default for most Cargo commands, otherwise
|
||||
# selected with `--debug`
|
||||
@@ -155,31 +191,3 @@ tracing-core = { git = "https://github.com/element-hq/tracing.git", rev = "ca943
|
||||
tracing-subscriber = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
tracing-appender = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
paranoid-android = { git = "https://github.com/element-hq/paranoid-android.git", rev = "69388ac5b4afeed7be4401c70ce17f6d9a2cf19b" }
|
||||
|
||||
[workspace.lints.rust]
|
||||
rust_2018_idioms = "warn"
|
||||
semicolon_in_expressions_from_macros = "warn"
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(tarpaulin_include)', # Used by tarpaulin (code coverage)
|
||||
'cfg(ruma_unstable_exhaustive_types)', # Used by Ruma's EventContent derive macro
|
||||
] }
|
||||
unused_extern_crates = "warn"
|
||||
unused_import_braces = "warn"
|
||||
unused_qualifications = "warn"
|
||||
trivial_casts = "warn"
|
||||
trivial_numeric_casts = "warn"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
assigning_clones = "allow"
|
||||
box_default = "allow"
|
||||
cloned_instead_of_copied = "warn"
|
||||
dbg_macro = "warn"
|
||||
inefficient_to_string = "warn"
|
||||
macro_use_imports = "warn"
|
||||
mut_mut = "warn"
|
||||
needless_borrow = "warn"
|
||||
nonstandard_macro_braces = "warn"
|
||||
str_to_string = "warn"
|
||||
todo = "warn"
|
||||
unused_async = "warn"
|
||||
redundant_clone = "warn"
|
||||
|
||||
@@ -1,38 +1,55 @@
|
||||
<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>
|
||||
<em>Your all-in-one toolkit for creating Matrix clients with Rust, from simple bots to full-featured apps.</em>
|
||||
<br />
|
||||
<img src="contrib/logo.svg">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
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,
|
||||
The Matrix Rust SDK is a collection of libraries that make it easier to build [Matrix] clients in [Rust].
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<picture>
|
||||
<source srcset="contrib/element-logo-light.png" media="(prefers-color-scheme: dark)">
|
||||
<source srcset="contrib/element-logo-dark.png" media="(prefers-color-scheme: light)">
|
||||
<img src="contrib/element-logo-fallback.png" alt="Element logo">
|
||||
</picture>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
Development of the SDK is proudly sponsored and maintained by [Element](https://element.io). Element uses the SDK in their next-generation mobile apps Element X on [iOS](https://github.com/element-hq/element-x-ios) and [Android](https://github.com/element-hq/element-x-android) and has plans to introduce it to the web and desktop clients as well.
|
||||
|
||||
The SDK is also the basis for multiple Matrix projects and we welcome contributions from all.
|
||||
|
||||
</div>
|
||||
|
||||
## Purpose
|
||||
|
||||
The SDK 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 Matrix Rust SDK is made up of several crates that build on top of each other. Here are the key ones:
|
||||
The Matrix Rust SDK is made up of several crates that build on top of each
|
||||
other. The following crates are expected to be usable as direct dependencies:
|
||||
|
||||
- [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,
|
||||
@@ -45,6 +62,9 @@ The Matrix Rust SDK is made up of several crates that build on top of each other
|
||||
See the [crypto tutorial](https://docs.rs/matrix-sdk-crypto/latest/matrix_sdk_crypto/tutorial/index.html)
|
||||
for a step-by-step introduction.
|
||||
|
||||
All other crates are effectively internal-only and only structured as crates
|
||||
for organizational purposes and to improve compilation times. Direct usage of them is discouraged.
|
||||
|
||||
## Status
|
||||
|
||||
The library is considered production ready and backs multiple client
|
||||
@@ -54,9 +74,6 @@ implementations such as Element X
|
||||
[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.
|
||||
|
||||
## Bindings
|
||||
|
||||
The higher-level crates of the Matrix Rust SDK can be embedded in other
|
||||
@@ -67,3 +84,7 @@ into your language of choice.
|
||||
## License
|
||||
|
||||
[Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
|
||||
[Matrix]: https://matrix.org/
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
|
||||
+13
-13
@@ -3,24 +3,27 @@ name = "benchmarks"
|
||||
description = "Matrix SDK benchmarks"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
rust-version = { workspace = true }
|
||||
rust-version.workspace = true
|
||||
version = "1.0.0"
|
||||
publish = false
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
[dependencies]
|
||||
criterion = { version = "0.5.1", features = ["async", "async_tokio", "html_reports"] }
|
||||
matrix-sdk-base = { workspace = true }
|
||||
matrix-sdk-crypto = { workspace = true }
|
||||
matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
|
||||
matrix-sdk-test = { workspace = true }
|
||||
matrix-sdk-ui = { workspace = true }
|
||||
matrix-sdk = { workspace = true, features = ["native-tls", "e2e-encryption", "sqlite", "testing"] }
|
||||
ruma = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
matrix-sdk-base.workspace = true
|
||||
matrix-sdk-crypto.workspace = true
|
||||
matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
|
||||
matrix-sdk-test.workspace = true
|
||||
matrix-sdk-ui.workspace = true
|
||||
ruma.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tempfile = "3.3.0"
|
||||
tokio = { workspace = true, default-features = false, features = ["rt-multi-thread"] }
|
||||
wiremock = { workspace = true }
|
||||
wiremock.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
pprof = { version = "0.14.0", features = ["flamegraph", "criterion"] }
|
||||
@@ -44,6 +47,3 @@ harness = false
|
||||
[[bench]]
|
||||
name = "timeline"
|
||||
harness = false
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::{sync::Arc, time::Duration};
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::{
|
||||
linked_chunk::{lazy_loader, LinkedChunk, Update},
|
||||
linked_chunk::{lazy_loader, LinkedChunk, LinkedChunkId, Update},
|
||||
SqliteEventCacheStore,
|
||||
};
|
||||
use matrix_sdk_base::event_cache::{
|
||||
@@ -29,6 +29,7 @@ fn writing(c: &mut Criterion) {
|
||||
.expect("Failed to create an asynchronous runtime");
|
||||
|
||||
let room_id = room_id!("!foo:bar.baz");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let event_factory = EventFactory::new().room(room_id).sender(&ALICE);
|
||||
|
||||
let mut group = c.benchmark_group("writing");
|
||||
@@ -115,9 +116,9 @@ fn writing(c: &mut Criterion) {
|
||||
|
||||
if let Some(store) = &store {
|
||||
let updates = linked_chunk.updates().unwrap().take();
|
||||
store.handle_linked_chunk_updates(room_id, updates).await.unwrap();
|
||||
store.handle_linked_chunk_updates(linked_chunk_id, updates).await.unwrap();
|
||||
// Empty the store.
|
||||
store.handle_linked_chunk_updates(room_id, vec![Update::Clear]).await.unwrap();
|
||||
store.handle_linked_chunk_updates(linked_chunk_id, vec![Update::Clear]).await.unwrap();
|
||||
}
|
||||
|
||||
},
|
||||
@@ -145,6 +146,7 @@ fn reading(c: &mut Criterion) {
|
||||
.expect("Failed to create an asynchronous runtime");
|
||||
|
||||
let room_id = room_id!("!foo:bar.baz");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let event_factory = EventFactory::new().room(room_id).sender(&ALICE);
|
||||
|
||||
let mut group = c.benchmark_group("reading");
|
||||
@@ -195,7 +197,9 @@ fn reading(c: &mut Criterion) {
|
||||
|
||||
// Now persist the updates to recreate this full linked chunk.
|
||||
let updates = lc.updates().unwrap().take();
|
||||
runtime.block_on(store.handle_linked_chunk_updates(room_id, updates)).unwrap();
|
||||
runtime
|
||||
.block_on(store.handle_linked_chunk_updates(linked_chunk_id, updates))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Define the throughput.
|
||||
@@ -206,7 +210,8 @@ fn reading(c: &mut Criterion) {
|
||||
// Bench the routine.
|
||||
bencher.to_async(&runtime).iter(|| async {
|
||||
// Load the last chunk first,
|
||||
let (last_chunk, chunk_id_gen) = store.load_last_chunk(room_id).await.unwrap();
|
||||
let (last_chunk, chunk_id_gen) =
|
||||
store.load_last_chunk(linked_chunk_id).await.unwrap();
|
||||
|
||||
let mut lc =
|
||||
lazy_loader::from_last_chunk::<128, _, _>(last_chunk, chunk_id_gen)
|
||||
@@ -216,7 +221,7 @@ fn reading(c: &mut Criterion) {
|
||||
// Then load until the start of the linked chunk.
|
||||
let mut cur_chunk_id = lc.chunks().next().unwrap().identifier();
|
||||
while let Some(prev) =
|
||||
store.load_previous_chunk(room_id, cur_chunk_id).await.unwrap()
|
||||
store.load_previous_chunk(linked_chunk_id, cur_chunk_id).await.unwrap()
|
||||
{
|
||||
cur_chunk_id = prev.identifier;
|
||||
lazy_loader::insert_new_first_chunk(&mut lc, prev)
|
||||
|
||||
@@ -4,10 +4,11 @@ use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Through
|
||||
use matrix_sdk::{store::RoomLoadSettings, test_utils::mocks::MatrixMockServer};
|
||||
use matrix_sdk_base::{
|
||||
store::StoreConfig, BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore,
|
||||
ThreadingSupport,
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteStateStore;
|
||||
use matrix_sdk_test::{event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent};
|
||||
use matrix_sdk_ui::{timeline::TimelineFocus, Timeline};
|
||||
use matrix_sdk_ui::timeline::{TimelineBuilder, TimelineFocus};
|
||||
use ruma::{
|
||||
api::client::membership::get_member_events,
|
||||
device_id,
|
||||
@@ -29,7 +30,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)
|
||||
@@ -58,6 +59,7 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
|
||||
let base_client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
|
||||
.state_store(sqlite_store),
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
runtime
|
||||
@@ -178,11 +180,11 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
.lock()
|
||||
.await
|
||||
.unwrap()
|
||||
.clear_all_rooms_chunks()
|
||||
.clear_all_linked_chunks()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let timeline = Timeline::builder(&room)
|
||||
let timeline = TimelineBuilder::new(&room)
|
||||
.with_focus(TimelineFocus::PinnedEvents {
|
||||
max_events_to_load: 100,
|
||||
max_concurrent_requests: 10,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::test_utils::mocks::MatrixMockServer;
|
||||
use matrix_sdk_test::{event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent};
|
||||
use matrix_sdk_ui::Timeline;
|
||||
use matrix_sdk_ui::timeline::TimelineBuilder;
|
||||
use ruma::{
|
||||
events::room::message::RoomMessageEventContentWithoutRelation, owned_room_id, owned_user_id,
|
||||
EventId,
|
||||
@@ -102,7 +102,7 @@ pub fn create_timeline_with_initial_events(c: &mut Criterion) {
|
||||
BenchmarkId::new("create_timeline_with_initial_events", format!("{NUM_EVENTS} events")),
|
||||
|b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let timeline = Timeline::builder(&room)
|
||||
let timeline = TimelineBuilder::new(&room)
|
||||
.track_read_marker_and_receipts()
|
||||
.build()
|
||||
.await
|
||||
|
||||
@@ -3,12 +3,15 @@ name = "matrix-sdk-crypto-ffi"
|
||||
version = "0.1.0"
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk>"]
|
||||
edition = "2021"
|
||||
rust-version = { workspace = true }
|
||||
rust-version.workspace = true
|
||||
description = "Uniffi based bindings for the Rust SDK crypto crate"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
license = "Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "staticlib"]
|
||||
|
||||
@@ -21,23 +24,23 @@ default = ["bundled-sqlite"]
|
||||
bundled-sqlite = ["matrix-sdk-sqlite/bundled"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
anyhow.workspace = true
|
||||
futures-util.workspace = true
|
||||
hmac = "0.12.1"
|
||||
http = { workspace = true }
|
||||
http.workspace = true
|
||||
matrix-sdk-common = { workspace = true, features = ["uniffi"] }
|
||||
matrix-sdk-ffi-macros = { workspace = true }
|
||||
matrix-sdk-ffi-macros.workspace = true
|
||||
pbkdf2 = "0.12.2"
|
||||
rand = { workspace = true }
|
||||
ruma = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
rand.workspace = true
|
||||
ruma.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
# keep in sync with uniffi dependency in matrix-sdk-ffi, and uniffi_bindgen in ffi CI job
|
||||
uniffi = { workspace = true, features = ["cli"] }
|
||||
vodozemac = { workspace = true }
|
||||
vodozemac.workspace = true
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
[dependencies.js_int]
|
||||
@@ -53,20 +56,17 @@ workspace = true
|
||||
features = ["crypto-store"]
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.33.0"
|
||||
version = "1.43.1"
|
||||
default-features = false
|
||||
features = ["rt-multi-thread"]
|
||||
|
||||
[build-dependencies]
|
||||
vergen = { version = "8.2.5", features = ["build", "git", "gitcl"] }
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
vergen = { version = "8.2.5", features = ["build", "git", "gitcl"] }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches2.workspace = true
|
||||
tempfile = "3.8.0"
|
||||
assert_matches2 = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{collections::HashMap, iter, ops::DerefMut, sync::Arc};
|
||||
use hmac::Hmac;
|
||||
use matrix_sdk_crypto::{
|
||||
backups::DecryptionError,
|
||||
store::{BackupDecryptionKey, CryptoStoreError as InnerStoreError},
|
||||
store::{types::BackupDecryptionKey, CryptoStoreError as InnerStoreError},
|
||||
};
|
||||
use pbkdf2::pbkdf2;
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use std::{mem::ManuallyDrop, sync::Arc};
|
||||
|
||||
use matrix_sdk_common::executor::Handle;
|
||||
use matrix_sdk_crypto::{
|
||||
dehydrated_devices::{
|
||||
DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices,
|
||||
RehydratedDevice as InnerRehydratedDevice,
|
||||
},
|
||||
store::DehydratedDeviceKey as InnerDehydratedDeviceKey,
|
||||
store::types::DehydratedDeviceKey as InnerDehydratedDeviceKey,
|
||||
};
|
||||
use ruma::{api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw, OwnedDeviceId};
|
||||
use serde_json::json;
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
use crate::{CryptoStoreError, DehydratedDeviceKey};
|
||||
|
||||
|
||||
@@ -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() },
|
||||
}
|
||||
|
||||
@@ -37,8 +37,11 @@ use matrix_sdk_common::deserialized_responses::{ShieldState as RustShieldState,
|
||||
use matrix_sdk_crypto::{
|
||||
olm::{IdentityKeys, InboundGroupSession, SenderData, Session},
|
||||
store::{
|
||||
Changes, CryptoStore, DehydratedDeviceKey as InnerDehydratedDeviceKey, PendingChanges,
|
||||
RoomSettings as RustRoomSettings,
|
||||
types::{
|
||||
Changes, DehydratedDeviceKey as InnerDehydratedDeviceKey, PendingChanges,
|
||||
RoomSettings as RustRoomSettings,
|
||||
},
|
||||
CryptoStore,
|
||||
},
|
||||
types::{
|
||||
DeviceKey, DeviceKeys, EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey,
|
||||
@@ -221,7 +224,7 @@ async fn migrate_data(
|
||||
passphrase: Option<String>,
|
||||
progress_listener: Box<dyn ProgressListener>,
|
||||
) -> anyhow::Result<()> {
|
||||
use matrix_sdk_crypto::{olm::PrivateCrossSigningIdentity, store::BackupDecryptionKey};
|
||||
use matrix_sdk_crypto::{olm::PrivateCrossSigningIdentity, store::types::BackupDecryptionKey};
|
||||
use vodozemac::olm::Account;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
@@ -818,10 +821,10 @@ impl BackupKeys {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<matrix_sdk_crypto::store::BackupKeys> for BackupKeys {
|
||||
impl TryFrom<matrix_sdk_crypto::store::types::BackupKeys> for BackupKeys {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(keys: matrix_sdk_crypto::store::BackupKeys) -> Result<Self, Self::Error> {
|
||||
fn try_from(keys: matrix_sdk_crypto::store::types::BackupKeys) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
recovery_key: BackupRecoveryKey {
|
||||
inner: keys.decryption_key.ok_or(())?,
|
||||
@@ -866,8 +869,8 @@ impl From<InnerDehydratedDeviceKey> for DehydratedDeviceKey {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::store::RoomKeyCounts> for RoomKeyCounts {
|
||||
fn from(count: matrix_sdk_crypto::store::RoomKeyCounts) -> Self {
|
||||
impl From<matrix_sdk_crypto::store::types::RoomKeyCounts> for RoomKeyCounts {
|
||||
fn from(count: matrix_sdk_crypto::store::types::RoomKeyCounts) -> Self {
|
||||
Self { total: count.total as i64, backed_up: count.backed_up as i64 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use matrix_sdk_crypto::{
|
||||
},
|
||||
decrypt_room_key_export, encrypt_room_key_export,
|
||||
olm::ExportedRoomKey,
|
||||
store::{BackupDecryptionKey, Changes},
|
||||
store::types::{BackupDecryptionKey, Changes},
|
||||
types::requests::ToDeviceRequest,
|
||||
DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, UserIdentity as SdkUserIdentity,
|
||||
};
|
||||
@@ -96,8 +96,8 @@ pub struct RoomKeyInfo {
|
||||
pub session_id: String,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::store::RoomKeyInfo> for RoomKeyInfo {
|
||||
fn from(value: matrix_sdk_crypto::store::RoomKeyInfo) -> Self {
|
||||
impl From<matrix_sdk_crypto::store::types::RoomKeyInfo> for RoomKeyInfo {
|
||||
fn from(value: matrix_sdk_crypto::store::types::RoomKeyInfo) -> Self {
|
||||
Self {
|
||||
algorithm: value.algorithm.to_string(),
|
||||
room_id: value.room_id.to_string(),
|
||||
@@ -354,7 +354,7 @@ impl OlmMachine {
|
||||
.map(|d| d.into()))
|
||||
}
|
||||
|
||||
/// Manually the device of the given user with the given device ID.
|
||||
/// Manually verify the device of the given user with the given device ID.
|
||||
///
|
||||
/// This method will attempt to sign the device using our private cross
|
||||
/// signing key.
|
||||
@@ -554,8 +554,10 @@ impl OlmMachine {
|
||||
}),
|
||||
)?;
|
||||
|
||||
let to_device_events =
|
||||
to_device_events.into_iter().map(|event| event.json().get().to_owned()).collect();
|
||||
let to_device_events = to_device_events
|
||||
.into_iter()
|
||||
.map(|event| event.to_raw().json().get().to_owned())
|
||||
.collect();
|
||||
let room_key_infos = room_key_infos.into_iter().map(|info| info.into()).collect();
|
||||
|
||||
Ok(SyncChangesResult { to_device_events, room_key_infos })
|
||||
@@ -915,20 +917,25 @@ impl OlmMachine {
|
||||
let event_json: Event<'_> = serde_json::from_str(decrypted.event.json().get())?;
|
||||
|
||||
Ok(match &encryption_info.algorithm_info {
|
||||
AlgorithmInfo::MegolmV1AesSha2 { curve25519_key, sender_claimed_keys } => {
|
||||
DecryptedEvent {
|
||||
clear_event: serde_json::to_string(&event_json)?,
|
||||
sender_curve25519_key: curve25519_key.to_owned(),
|
||||
claimed_ed25519_key: sender_claimed_keys
|
||||
.get(&DeviceKeyAlgorithm::Ed25519)
|
||||
.cloned(),
|
||||
forwarding_curve25519_chain: vec![],
|
||||
shield_state: if strict_shields {
|
||||
encryption_info.verification_state.to_shield_state_strict().into()
|
||||
} else {
|
||||
encryption_info.verification_state.to_shield_state_lax().into()
|
||||
},
|
||||
}
|
||||
AlgorithmInfo::MegolmV1AesSha2 {
|
||||
curve25519_key,
|
||||
sender_claimed_keys,
|
||||
session_id: _,
|
||||
} => DecryptedEvent {
|
||||
clear_event: serde_json::to_string(&event_json)?,
|
||||
sender_curve25519_key: curve25519_key.to_owned(),
|
||||
claimed_ed25519_key: sender_claimed_keys.get(&DeviceKeyAlgorithm::Ed25519).cloned(),
|
||||
forwarding_curve25519_chain: vec![],
|
||||
shield_state: if strict_shields {
|
||||
encryption_info.verification_state.to_shield_state_strict().into()
|
||||
} else {
|
||||
encryption_info.verification_state.to_shield_state_lax().into()
|
||||
},
|
||||
},
|
||||
AlgorithmInfo::OlmV1Curve25519AesSha2 { .. } => {
|
||||
// cannot happen because `decrypt_room_event` would have fail to decrypt olm for
|
||||
// a room (EventError::UnsupportedAlgorithm)
|
||||
panic!("Unsupported olm algorithm in room")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1335,7 +1342,8 @@ impl OlmMachine {
|
||||
let (sas, request) = self.runtime.block_on(device.start_verification())?;
|
||||
|
||||
Some(StartSasResult {
|
||||
sas: Sas { inner: sas, runtime: self.runtime.handle().to_owned() }.into(),
|
||||
sas: Sas { inner: Box::new(sas), runtime: self.runtime.handle().to_owned() }
|
||||
.into(),
|
||||
request: request.into(),
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -224,8 +224,8 @@ impl From<&ToDeviceRequest> for Request {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RoomMessageRequest> for Request {
|
||||
fn from(r: &RoomMessageRequest) -> Self {
|
||||
impl From<&Box<RoomMessageRequest>> for Request {
|
||||
fn from(r: &Box<RoomMessageRequest>) -> Self {
|
||||
Self::RoomMessage {
|
||||
request_id: r.txn_id.to_string(),
|
||||
room_id: r.room_id.to_string(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::{Stream, StreamExt};
|
||||
use matrix_sdk_common::executor::Handle;
|
||||
use matrix_sdk_crypto::{
|
||||
matrix_sdk_qrcode::QrVerificationData, CancelInfo as RustCancelInfo, QrVerification as InnerQr,
|
||||
QrVerificationState, Sas as InnerSas, SasState as RustSasState,
|
||||
@@ -8,7 +9,6 @@ use matrix_sdk_crypto::{
|
||||
VerificationRequestState as RustVerificationRequestState,
|
||||
};
|
||||
use ruma::events::key::verification::VerificationMethod;
|
||||
use tokio::runtime::Handle;
|
||||
use vodozemac::{base64_decode, base64_encode};
|
||||
|
||||
use crate::{CryptoStoreError, OutgoingVerificationRequest, SignatureUploadRequest};
|
||||
@@ -88,7 +88,7 @@ impl Verification {
|
||||
/// returns `None` if the verification is not a `Sas` verification.
|
||||
pub fn as_sas(&self) -> Option<Arc<Sas>> {
|
||||
if let InnerVerification::SasV1(sas) = &self.inner {
|
||||
Some(Sas { inner: sas.to_owned(), runtime: self.runtime.to_owned() }.into())
|
||||
Some(Sas { inner: sas.clone(), runtime: self.runtime.to_owned() }.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -98,7 +98,7 @@ impl Verification {
|
||||
/// returns `None` if the verification is not a `QrCode` verification.
|
||||
pub fn as_qr(&self) -> Option<Arc<QrCode>> {
|
||||
if let InnerVerification::QrV1(qr) = &self.inner {
|
||||
Some(QrCode { inner: qr.to_owned(), runtime: self.runtime.to_owned() }.into())
|
||||
Some(QrCode { inner: qr.clone(), runtime: self.runtime.to_owned() }.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -108,7 +108,7 @@ impl Verification {
|
||||
/// The `m.sas.v1` verification flow.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Sas {
|
||||
pub(crate) inner: InnerSas,
|
||||
pub(crate) inner: Box<InnerSas>,
|
||||
pub(crate) runtime: Handle,
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@ impl From<QrVerificationState> for QrCodeState {
|
||||
/// verification flow.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct QrCode {
|
||||
pub(crate) inner: InnerQr,
|
||||
pub(crate) inner: Box<InnerQr>,
|
||||
pub(crate) runtime: Handle,
|
||||
}
|
||||
|
||||
@@ -669,7 +669,7 @@ impl VerificationRequest {
|
||||
/// verification flow.
|
||||
pub fn start_sas_verification(&self) -> Result<Option<StartSasResult>, CryptoStoreError> {
|
||||
Ok(self.runtime.block_on(self.inner.start_sas())?.map(|(sas, r)| StartSasResult {
|
||||
sas: Arc::new(Sas { inner: sas, runtime: self.runtime.clone() }),
|
||||
sas: Arc::new(Sas { inner: Box::new(sas), runtime: self.runtime.clone() }),
|
||||
request: r.into(),
|
||||
}))
|
||||
}
|
||||
@@ -690,7 +690,7 @@ impl VerificationRequest {
|
||||
Ok(self
|
||||
.runtime
|
||||
.block_on(self.inner.generate_qr_code())?
|
||||
.map(|qr| QrCode { inner: qr, runtime: self.runtime.clone() }.into()))
|
||||
.map(|qr| QrCode { inner: Box::new(qr), runtime: self.runtime.clone() }.into()))
|
||||
}
|
||||
|
||||
/// Pass data from a scanned QR code to an active verification request and
|
||||
@@ -717,7 +717,7 @@ impl VerificationRequest {
|
||||
let request = qr.reciprocate()?;
|
||||
|
||||
Some(ScanResult {
|
||||
qr: QrCode { inner: qr, runtime: self.runtime.clone() }.into(),
|
||||
qr: QrCode { inner: Box::new(qr), runtime: self.runtime.clone() }.into(),
|
||||
request: request.into(),
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -7,7 +7,7 @@ license = "Apache-2.0"
|
||||
name = "matrix-sdk-ffi-macros"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = { workspace = true }
|
||||
rust-version.workspace = true
|
||||
version = "0.7.0"
|
||||
publish = false
|
||||
|
||||
|
||||
@@ -51,7 +51,12 @@ pub fn export(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
|
||||
let res = match syn::parse(item) {
|
||||
Ok(item) => match has_async_fn(item) {
|
||||
true => quote! { #[uniffi::export(async_runtime = "tokio", #attr2)] },
|
||||
true => {
|
||||
quote! {
|
||||
#[cfg_attr(target_family = "wasm", uniffi::export(#attr2))]
|
||||
#[cfg_attr(not(target_family = "wasm"), uniffi::export(async_runtime = "tokio", #attr2))]
|
||||
}
|
||||
}
|
||||
false => quote! { #[uniffi::export(#attr2)] },
|
||||
},
|
||||
Err(e) => e.into_compile_error(),
|
||||
|
||||
@@ -6,6 +6,90 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
## [0.13.0] - 2025-07-10
|
||||
|
||||
### Features
|
||||
|
||||
- Add `NotificationRoomInfo::topic` to the `NotificationRoomInfo` struct, which
|
||||
contains the topic of the room. This is useful for displaying the room topic
|
||||
in notifications. ([#5300](https://github.com/matrix-org/matrix-rust-sdk/pull/5300))
|
||||
- Add `EmbeddedEventDetails::timestamp` and `EmbeddedEventDetails::event_or_transaction_id`
|
||||
which are already available in regular timeline items.
|
||||
([#5331](https://github.com/matrix-org/matrix-rust-sdk/pull/5331))
|
||||
- `RoomListService::subscribe_to_rooms` becomes `async` and automatically calls
|
||||
`matrix_sdk::latest_events::LatestEvents::listen_to_room`
|
||||
([#5369](https://github.com/matrix-org/matrix-rust-sdk/pull/5369))
|
||||
|
||||
### Refactor
|
||||
|
||||
- Adjust features in the `matrix-sdk-ffi` crate to expose more platform-specific knobs.
|
||||
Previously the `matrix-sdk-ffi` was configured primarily by target configs, choosing
|
||||
between the tls flavor (`rustls-tls` or `native-tls`) and features like `sentry` based
|
||||
purely on the target. As we work to add an additional Wasm target to this crate,
|
||||
the cross product of target specific features has become somewhat chaotic, and we
|
||||
have shifted to externalize these choices as feature flags.
|
||||
|
||||
To maintain existing compatibility on the major platforms, these features should be used:
|
||||
Android: `"bundled-sqlite,unstable-msc4274,rustls-tls,sentry"`
|
||||
iOS: `"bundled-sqlite,unstable-msc4274,native-tls,sentry"`
|
||||
Javascript/Wasm: `"unstable-msc4274,native-tls"`
|
||||
|
||||
In the future additional choices (such as session storage, `sqlite` and `indexeddb`)
|
||||
will likely be added as well.
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- `Client::reset_server_capabilities` has been renamed to `Client::reset_server_info`.
|
||||
([#5167](https://github.com/matrix-org/matrix-rust-sdk/pull/5167))
|
||||
- `RoomPreview::join_rule`, `NotificationItem::join_rule`, `RoomInfo::is_public`, and
|
||||
`Room::is_public()` return values are now optional. They will be set to `None` if the join rule
|
||||
state event is missing for a given room. `NotificationRoomInfo::is_public` has been removed;
|
||||
callers can inspect the value of `NotificationItem::join_rule` to determine if the room is public
|
||||
(i.e. if the join rule is `Public`).
|
||||
([#5278](https://github.com/matrix-org/matrix-rust-sdk/pull/5278))
|
||||
|
||||
## [0.12.0] - 2025-06-10
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- `Client::send_call_notification_if_needed` now returns `Result<bool>` instead of `Result<()>` so we can check if
|
||||
the event was sent.
|
||||
- `Client::upload_avatar` and `Timeline::send_attachment` now may fail if a file too large for the homeserver media
|
||||
config is uploaded.
|
||||
- `UploadParameters` replaces field `filename: String` with `source: UploadSource`.
|
||||
`UploadSource` is an enum which may take a filename or a filename and bytes, which
|
||||
allows a foreign language to read file contents natively and then pass those contents to
|
||||
the foreign function when uploading a file through the `Timeline`.
|
||||
([#4948](https://github.com/matrix-org/matrix-rust-sdk/pull/4948))
|
||||
- `RoomInfo` replaces its field `is_tombstoned: bool` with `tombstone: Option<RoomTombstoneInfo>`,
|
||||
containing the data needed to implement the room migration UI, a message and the replacement room id.
|
||||
([#5027](https://github.com/matrix-org/matrix-rust-sdk/pull/5027))
|
||||
|
||||
Additions:
|
||||
|
||||
- `Client::subscribe_to_room_info` allows clients to subscribe to room info updates in rooms which may not be known yet.
|
||||
This is useful when displaying a room preview for an unknown room, so when we receive any membership change for it,
|
||||
we can automatically update the UI.
|
||||
- `Client::get_max_media_upload_size` to get the max size of a request sent to the homeserver so we can tweak our media
|
||||
uploads by compressing/transcoding the media.
|
||||
- Add `ClientBuilder::enable_share_history_on_invite` to enable experimental support for sharing encrypted room history on invite, per [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268).
|
||||
([#5141](https://github.com/matrix-org/matrix-rust-sdk/pull/5141))
|
||||
- Support for adding a Sentry layer to the FFI bindings has been added. Only `tracing` statements with
|
||||
the field `sentry=true` will be forwarded to Sentry, in addition to default Sentry filters.
|
||||
- Add room topic string to `StateEventContent`
|
||||
- Add `UploadSource` for representing upload data - this is analogous to `matrix_sdk_ui::timeline::AttachmentSource`
|
||||
- Add `Client::observe_account_data_event` and `Client::observe_room_account_data_event` to
|
||||
subscribe to global and room account data changes.
|
||||
([#4994](https://github.com/matrix-org/matrix-rust-sdk/pull/4994))
|
||||
- Add `Timeline::send_gallery` to send MSC4274-style galleries.
|
||||
([#5163](https://github.com/matrix-org/matrix-rust-sdk/pull/5163))
|
||||
- Add `reply_params` to `GalleryUploadParameters` to allow sending galleries as (threaded) replies.
|
||||
([#5173](https://github.com/matrix-org/matrix-rust-sdk/pull/5173))
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- `contacts` has been removed from `OidcConfiguration` (it was unused since the switch to OAuth).
|
||||
|
||||
## [0.11.0] - 2025-04-11
|
||||
|
||||
Breaking changes:
|
||||
@@ -20,7 +104,7 @@ Breaking changes:
|
||||
programs can set it to `true`.
|
||||
|
||||
- Matrix client API errors coming from API responses will now be mapped to `ClientError::MatrixApi`, containing both the
|
||||
original message and the associated error code and kind.
|
||||
original message and the associated error code and kind.
|
||||
|
||||
- `EventSendState` now has two additional variants: `CrossSigningNotSetup` and
|
||||
`SendingFromUnverifiedDevice`. These indicate that your own device is not
|
||||
|
||||
@@ -1,84 +1,100 @@
|
||||
[package]
|
||||
name = "matrix-sdk-ffi"
|
||||
version = "0.11.0"
|
||||
version = "0.13.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"]
|
||||
crate-type = [
|
||||
# Needed by uniffi for Android bindings
|
||||
"cdylib",
|
||||
# Needed by uniffi for iOS bindings
|
||||
"staticlib",
|
||||
# Needed by uniffi for JS/Wasm bindings, which use rust as an intermediate language
|
||||
"lib"
|
||||
]
|
||||
|
||||
[features]
|
||||
default = ["bundled-sqlite"]
|
||||
default = ["bundled-sqlite", "unstable-msc4274"]
|
||||
bundled-sqlite = ["matrix-sdk/bundled-sqlite"]
|
||||
unstable-msc4274 = ["matrix-sdk-ui/unstable-msc4274"]
|
||||
# Required when targeting a Javascript environment, like Wasm in a browser.
|
||||
js = ["matrix-sdk-ui/js"]
|
||||
# Use the TLS implementation provided by the host system, necessary on iOS and Wasm platforms.
|
||||
native-tls = ["matrix-sdk/native-tls", "sentry?/native-tls"]
|
||||
# Use Rustls as the TLS implementation, necessary on Android platforms.
|
||||
rustls-tls = ["matrix-sdk/rustls-tls", "sentry?/rustls"]
|
||||
# Enable sentry error monitoring, not compatible with Wasm platforms.
|
||||
sentry = ["dep:sentry", "dep:sentry-tracing"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
as_variant.workspace = true
|
||||
extension-trait = "1.0.1"
|
||||
eyeball-im.workspace = true
|
||||
futures-util.workspace = true
|
||||
language-tags = "0.3.2"
|
||||
log-panics = { version = "2", features = ["with-backtrace"] }
|
||||
matrix-sdk = { workspace = true, features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
"experimental-widgets",
|
||||
"markdown",
|
||||
"socks",
|
||||
"sqlite",
|
||||
"uniffi",
|
||||
] }
|
||||
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", "unstable-msc4278"] }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sentry = { version = "0.36.0", optional = true, default-features = false, features = [
|
||||
# Most default features enabled otherwise.
|
||||
"backtrace",
|
||||
"contexts",
|
||||
"panic",
|
||||
"reqwest",
|
||||
"sentry-debug-images",
|
||||
] }
|
||||
sentry-tracing = { version = "0.36.0", optional = true }
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-appender = { version = "0.2.2" }
|
||||
tracing-core.workspace = true
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
url.workspace = true
|
||||
uuid = { version = "1.4.1", features = ["v4"] }
|
||||
zeroize.workspace = true
|
||||
|
||||
[target.'cfg(target_family = "wasm")'.dependencies]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
tokio = { workspace = true, features = ["sync", "macros"] }
|
||||
uniffi.workspace = true
|
||||
|
||||
[target.'cfg(not(target_family = "wasm"))'.dependencies]
|
||||
async-compat.workspace = true
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
uniffi = { workspace = true, features = ["tokio"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
paranoid-android = "0.2.1"
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
vergen = { version = "8.1.3", features = ["build", "git", "gitcl"] }
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
as_variant = { workspace = true }
|
||||
async-compat = "0.2.4"
|
||||
eyeball-im = { workspace = true }
|
||||
extension-trait = "1.0.1"
|
||||
futures-util = { workspace = true }
|
||||
log-panics = { version = "2", features = ["with-backtrace"] }
|
||||
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" }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
uniffi = { workspace = true, features = ["tokio"] }
|
||||
url = { workspace = true }
|
||||
zeroize = { workspace = true }
|
||||
uuid = { version = "1.4.1", features = ["v4"] }
|
||||
language-tags = "0.3.2"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
paranoid-android = "0.2.1"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies.matrix-sdk]
|
||||
workspace = true
|
||||
features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
"experimental-widgets",
|
||||
"markdown",
|
||||
"rustls-tls", # note: differ from block below
|
||||
"socks",
|
||||
"sqlite",
|
||||
"uniffi",
|
||||
]
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies.matrix-sdk]
|
||||
workspace = true
|
||||
features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
"experimental-widgets",
|
||||
"markdown",
|
||||
"native-tls", # note: differ from block above
|
||||
"socks",
|
||||
"sqlite",
|
||||
"uniffi",
|
||||
]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.release]
|
||||
release = true
|
||||
|
||||
@@ -2,8 +2,28 @@
|
||||
|
||||
This uses [`uniffi`](https://mozilla.github.io/uniffi-rs/Overview.html) to build the matrix bindings for native support and wasm-bindgen for web-browser assembly support. Please refer to the specific section to figure out how to build and use the bindings for your platform.
|
||||
|
||||
## Features
|
||||
Given the number of platforms targeted, we have broken out a number of features
|
||||
|
||||
### Platform specific
|
||||
- `rustls-tls`: Use Rustls as the TLS implementation, necessary on Android platforms.
|
||||
- `native-tls`: Use the TLS implementation provided by the host system, necessary on iOS and Wasm platforms.
|
||||
|
||||
### Functionality
|
||||
- `sentry`: Enable error monitoring using Sentry, not supports on Wasm platforms.
|
||||
- `bundled-sqlite`: Use an embedded version of sqlite instead of the system provided one.
|
||||
|
||||
### Unstable specs
|
||||
- `unstable-msc4274`: Adds support for gallery message types, which contain multiple media elements.
|
||||
|
||||
## Platforms
|
||||
|
||||
Each supported target should use features to select the relevant TLS system. Here are some suggested feature flags for the major platforms:
|
||||
|
||||
- Android: `"bundled-sqlite,unstable-msc4274,rustls-tls,sentry"`
|
||||
- iOS: `"bundled-sqlite,unstable-msc4274,native-tls,sentry"`
|
||||
- Javascript/Wasm: `"unstable-msc4274,native-tls"`
|
||||
|
||||
### Swift/iOS sync
|
||||
|
||||
|
||||
|
||||
@@ -119,8 +119,6 @@ pub struct OidcConfiguration {
|
||||
pub tos_uri: Option<String>,
|
||||
/// A URI that contains the client's privacy policy.
|
||||
pub policy_uri: Option<String>,
|
||||
/// An array of e-mail addresses of people responsible for this client.
|
||||
pub contacts: Option<Vec<String>>,
|
||||
|
||||
/// Pre-configured registrations for use with homeservers that don't support
|
||||
/// dynamic client registration.
|
||||
@@ -173,7 +171,7 @@ impl OidcConfiguration {
|
||||
.iter()
|
||||
.filter_map(|(issuer, client_id)| {
|
||||
let Ok(issuer) = Url::parse(issuer) else {
|
||||
tracing::error!("Failed to parse {:?}", issuer);
|
||||
tracing::error!("Failed to parse {issuer:?}");
|
||||
return None;
|
||||
};
|
||||
Some((issuer, ClientId::new(client_id.clone())))
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::Debug,
|
||||
sync::{Arc, RwLock},
|
||||
path::PathBuf,
|
||||
sync::{Arc, OnceLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use async_compat::get_runtime_handle;
|
||||
use futures_util::pin_mut;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use matrix_sdk::media::MediaFileHandle as SdkMediaFileHandle;
|
||||
use matrix_sdk::{
|
||||
authentication::oauth::{
|
||||
AccountManagementActionFull, ClientId, OAuthAuthorizationData, OAuthSession,
|
||||
},
|
||||
event_cache::EventCacheError,
|
||||
media::{
|
||||
MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequestParameters,
|
||||
MediaRetentionPolicy, MediaThumbnailSettings,
|
||||
},
|
||||
media::{MediaFormat, MediaRequestParameters, MediaRetentionPolicy, MediaThumbnailSettings},
|
||||
ruma::{
|
||||
api::client::{
|
||||
discovery::get_authorization_server_metadata::msc2965::Prompt as RumaOidcPrompt,
|
||||
discovery::{
|
||||
discover_homeserver::RtcFocusInfo,
|
||||
get_authorization_server_metadata::v1::Prompt as RumaOidcPrompt,
|
||||
},
|
||||
push::{EmailPusherData, PusherIds, PusherInit, PusherKind as RumaPusherKind},
|
||||
room::{create_room, Visibility},
|
||||
session::get_login_types,
|
||||
@@ -36,17 +40,27 @@ use matrix_sdk::{
|
||||
sliding_sync::Version as SdkSlidingSyncVersion,
|
||||
store::RoomLoadSettings as SdkRoomLoadSettings,
|
||||
AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens,
|
||||
STATE_STORE_DATABASE_NAME,
|
||||
};
|
||||
use matrix_sdk_ui::notification_client::{
|
||||
NotificationClient as MatrixNotificationClient,
|
||||
NotificationProcessSetup as MatrixNotificationProcessSetup,
|
||||
use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::{
|
||||
notification_client::{
|
||||
NotificationClient as MatrixNotificationClient,
|
||||
NotificationProcessSetup as MatrixNotificationProcessSetup,
|
||||
},
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
api::client::{alias::get_alias, error::ErrorKind, uiaa::UserIdentifier},
|
||||
events::{
|
||||
direct::DirectEventContent,
|
||||
fully_read::FullyReadEventContent,
|
||||
identity_server::IdentityServerEventContent,
|
||||
ignored_user_list::IgnoredUserListEventContent,
|
||||
key::verification::request::ToDeviceKeyVerificationRequestEvent,
|
||||
marked_unread::{MarkedUnreadEventContent, UnstableMarkedUnreadEventContent},
|
||||
push_rules::PushRulesEventContent,
|
||||
room::{
|
||||
history_visibility::RoomHistoryVisibilityEventContent,
|
||||
join_rules::{
|
||||
@@ -55,7 +69,13 @@ use ruma::{
|
||||
message::OriginalSyncRoomMessageEvent,
|
||||
power_levels::RoomPowerLevelsEventContent,
|
||||
},
|
||||
GlobalAccountDataEventType,
|
||||
secret_storage::{
|
||||
default_key::SecretStorageDefaultKeyEventContent, key::SecretStorageKeyEventContent,
|
||||
},
|
||||
tag::TagEventContent,
|
||||
GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
|
||||
GlobalAccountDataEventType as RumaGlobalAccountDataEventType,
|
||||
RoomAccountDataEvent as RumaRoomAccountDataEvent,
|
||||
},
|
||||
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
|
||||
OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
|
||||
@@ -66,19 +86,27 @@ use tokio::sync::broadcast::error::RecvError;
|
||||
use tracing::{debug, error};
|
||||
use url::Url;
|
||||
|
||||
use super::{room::Room, session_verification::SessionVerificationController};
|
||||
use super::{
|
||||
room::{room_info::RoomInfo, Room},
|
||||
session_verification::SessionVerificationController,
|
||||
};
|
||||
use crate::{
|
||||
authentication::{HomeserverLoginDetails, OidcConfiguration, OidcError, SsoError, SsoHandler},
|
||||
client,
|
||||
encryption::Encryption,
|
||||
notification::NotificationClient,
|
||||
notification_settings::NotificationSettings,
|
||||
room::RoomHistoryVisibility,
|
||||
room::{RoomHistoryVisibility, RoomInfoListener},
|
||||
room_directory_search::RoomDirectorySearch,
|
||||
room_preview::RoomPreview,
|
||||
ruma::{AuthData, MediaSource},
|
||||
ruma::{
|
||||
AccountDataEvent, AccountDataEventType, AuthData, InviteAvatars, MediaPreviewConfig,
|
||||
MediaPreviews, MediaSource, RoomAccountDataEvent, RoomAccountDataEventType,
|
||||
},
|
||||
runtime::get_runtime_handle,
|
||||
sync_service::{SyncService, SyncServiceBuilder},
|
||||
task_handle::TaskHandle,
|
||||
utd::{UnableToDecryptDelegate, UtdHook},
|
||||
utils::AsyncRuntimeDropped,
|
||||
ClientError,
|
||||
};
|
||||
@@ -144,30 +172,43 @@ impl From<PushFormat> for RumaPushFormat {
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait ClientDelegate: Sync + Send {
|
||||
pub trait ClientDelegate: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn did_receive_auth_error(&self, is_soft_logout: bool);
|
||||
fn did_refresh_tokens(&self);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait ClientSessionDelegate: Sync + Send {
|
||||
pub trait ClientSessionDelegate: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn retrieve_session_from_keychain(&self, user_id: String) -> Result<Session, ClientError>;
|
||||
fn save_session_in_keychain(&self, session: Session);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait ProgressWatcher: Send + Sync {
|
||||
pub trait ProgressWatcher: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn transmission_progress(&self, progress: TransmissionProgress);
|
||||
}
|
||||
|
||||
/// A listener to the global (client-wide) error reporter of the send queue.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SendQueueRoomErrorListener: Sync + Send {
|
||||
pub trait SendQueueRoomErrorListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
/// Called every time the send queue has ran into an error for a given room,
|
||||
/// which will disable the send queue for that particular room.
|
||||
fn on_error(&self, room_id: String, error: ClientError);
|
||||
}
|
||||
|
||||
/// A listener for changes of global account data events.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait AccountDataListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
/// Called when a global account data event has changed.
|
||||
fn on_change(&self, event: AccountDataEvent);
|
||||
}
|
||||
|
||||
/// A listener for changes of room account data events.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomAccountDataListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
/// Called when a room account data event was changed.
|
||||
fn on_change(&self, event: RoomAccountDataEvent, room_id: String);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, uniffi::Record)]
|
||||
pub struct TransmissionProgress {
|
||||
pub current: u64,
|
||||
@@ -186,9 +227,14 @@ impl From<matrix_sdk::TransmissionProgress> for TransmissionProgress {
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Client {
|
||||
pub(crate) inner: AsyncRuntimeDropped<MatrixClient>,
|
||||
delegate: RwLock<Option<Arc<dyn ClientDelegate>>>,
|
||||
delegate: OnceLock<Arc<dyn ClientDelegate>>,
|
||||
pub(crate) utd_hook_manager: OnceLock<Arc<UtdHookManager>>,
|
||||
session_verification_controller:
|
||||
Arc<tokio::sync::RwLock<Option<SessionVerificationController>>>,
|
||||
/// The path to the directory where the state store and the crypto store are
|
||||
/// located, if the `Client` instance has been built with a SQLite store
|
||||
/// backend.
|
||||
store_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
@@ -196,6 +242,7 @@ impl Client {
|
||||
sdk_client: MatrixClient,
|
||||
enable_oidc_refresh_lock: bool,
|
||||
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
|
||||
store_path: Option<PathBuf>,
|
||||
) -> Result<Self, ClientError> {
|
||||
let session_verification_controller: Arc<
|
||||
tokio::sync::RwLock<Option<SessionVerificationController>>,
|
||||
@@ -229,9 +276,11 @@ impl Client {
|
||||
sdk_client.cross_process_store_locks_holder_name().to_owned();
|
||||
|
||||
let client = Client {
|
||||
inner: AsyncRuntimeDropped::new(sdk_client),
|
||||
delegate: RwLock::new(None),
|
||||
inner: AsyncRuntimeDropped::new(sdk_client.clone()),
|
||||
delegate: OnceLock::new(),
|
||||
utd_hook_manager: OnceLock::new(),
|
||||
session_verification_controller,
|
||||
store_path,
|
||||
};
|
||||
|
||||
if enable_oidc_refresh_lock {
|
||||
@@ -400,10 +449,18 @@ impl Client {
|
||||
/// * `prompt` - The desired user experience in the web UI. No value means
|
||||
/// that the user wishes to login into an existing account, and a value of
|
||||
/// `Create` means that the user wishes to register a new account.
|
||||
///
|
||||
/// * `login_hint` - A generic login hint that an identity provider can use
|
||||
/// to pre-fill the login form. The format of this hint is not restricted
|
||||
/// by the spec as external providers all have their own way to handle the hint.
|
||||
/// However, it should be noted that when providing a user ID as a hint
|
||||
/// for MAS (with no upstream provider), then the format to use is defined
|
||||
/// by [MSC4198]: https://github.com/matrix-org/matrix-spec-proposals/pull/4198
|
||||
pub async fn url_for_oidc(
|
||||
&self,
|
||||
oidc_configuration: &OidcConfiguration,
|
||||
prompt: Option<OidcPrompt>,
|
||||
login_hint: Option<String>,
|
||||
) -> Result<Arc<OAuthAuthorizationData>, OidcError> {
|
||||
let registration_data = oidc_configuration.registration_data()?;
|
||||
let redirect_uri = oidc_configuration.redirect_uri()?;
|
||||
@@ -413,6 +470,9 @@ impl Client {
|
||||
if let Some(prompt) = prompt {
|
||||
url_builder = url_builder.prompt(vec![prompt.into()]);
|
||||
}
|
||||
if let Some(login_hint) = login_hint {
|
||||
url_builder = url_builder.login_hint(login_hint);
|
||||
}
|
||||
|
||||
let data = url_builder.build().await?;
|
||||
|
||||
@@ -434,32 +494,6 @@ impl Client {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_media_file(
|
||||
&self,
|
||||
media_source: Arc<MediaSource>,
|
||||
filename: Option<String>,
|
||||
mime_type: String,
|
||||
use_cache: bool,
|
||||
temp_dir: Option<String>,
|
||||
) -> Result<Arc<MediaFileHandle>, ClientError> {
|
||||
let source = (*media_source).clone();
|
||||
let mime_type: mime::Mime = mime_type.parse()?;
|
||||
|
||||
let handle = self
|
||||
.inner
|
||||
.media()
|
||||
.get_media_file(
|
||||
&MediaRequestParameters { source: source.media_source, format: MediaFormat::File },
|
||||
filename,
|
||||
&mime_type,
|
||||
use_cache,
|
||||
temp_dir,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Arc::new(MediaFileHandle::new(handle)))
|
||||
}
|
||||
|
||||
/// Restores the client from a `Session`.
|
||||
///
|
||||
/// It reloads the entire set of rooms from the previous session.
|
||||
@@ -486,7 +520,7 @@ impl Client {
|
||||
auth_session,
|
||||
room_load_settings
|
||||
.try_into()
|
||||
.map_err(|error| ClientError::Generic { msg: error })?,
|
||||
.map_err(|error| ClientError::from_str(error, None))?,
|
||||
)
|
||||
.await?;
|
||||
self.inner.set_sliding_sync_version(sliding_sync_version.try_into()?);
|
||||
@@ -525,7 +559,7 @@ impl Client {
|
||||
loop {
|
||||
match subscriber.recv().await {
|
||||
Ok(report) => listener
|
||||
.on_error(report.room_id.to_string(), ClientError::new(report.error)),
|
||||
.on_error(report.room_id.to_string(), ClientError::from_err(report.error)),
|
||||
Err(err) => {
|
||||
error!("error when listening to the send queue error reporter: {err}");
|
||||
}
|
||||
@@ -534,6 +568,132 @@ impl Client {
|
||||
})))
|
||||
}
|
||||
|
||||
/// Subscribe to updates of global account data events.
|
||||
///
|
||||
/// Be careful that only the most recent value can be observed. Subscribers
|
||||
/// are notified when a new value is sent, but there is no guarantee that
|
||||
/// they will see all values.
|
||||
pub fn observe_account_data_event(
|
||||
&self,
|
||||
event_type: AccountDataEventType,
|
||||
listener: Box<dyn AccountDataListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
macro_rules! observe {
|
||||
($t:ty, $cb: expr) => {{
|
||||
// Using an Arc here is mandatory or else the subscriber will never trigger
|
||||
let observer =
|
||||
Arc::new(self.inner.observe_events::<RumaGlobalAccountDataEvent<$t>, ()>());
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
let mut subscriber = observer.subscribe();
|
||||
loop {
|
||||
if let Some(next) = subscriber.next().await {
|
||||
$cb(next.0);
|
||||
}
|
||||
}
|
||||
})))
|
||||
}};
|
||||
|
||||
($t:ty) => {{
|
||||
observe!($t, |event: RumaGlobalAccountDataEvent<$t>| {
|
||||
listener.on_change(event.into());
|
||||
})
|
||||
}};
|
||||
}
|
||||
|
||||
match event_type {
|
||||
AccountDataEventType::Direct => {
|
||||
observe!(DirectEventContent)
|
||||
}
|
||||
AccountDataEventType::IdentityServer => {
|
||||
observe!(IdentityServerEventContent)
|
||||
}
|
||||
AccountDataEventType::IgnoredUserList => {
|
||||
observe!(IgnoredUserListEventContent)
|
||||
}
|
||||
AccountDataEventType::PushRules => {
|
||||
observe!(PushRulesEventContent, |event: RumaGlobalAccountDataEvent<
|
||||
PushRulesEventContent,
|
||||
>| {
|
||||
if let Ok(event) = event.try_into() {
|
||||
listener.on_change(event);
|
||||
}
|
||||
})
|
||||
}
|
||||
AccountDataEventType::SecretStorageDefaultKey => {
|
||||
observe!(SecretStorageDefaultKeyEventContent)
|
||||
}
|
||||
AccountDataEventType::SecretStorageKey { key_id } => {
|
||||
observe!(SecretStorageKeyEventContent, |event: RumaGlobalAccountDataEvent<
|
||||
SecretStorageKeyEventContent,
|
||||
>| {
|
||||
if event.content.key_id != key_id {
|
||||
return;
|
||||
}
|
||||
if let Ok(event) = event.try_into() {
|
||||
listener.on_change(event);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribe to updates of room account data events.
|
||||
///
|
||||
/// Be careful that only the most recent value can be observed. Subscribers
|
||||
/// are notified when a new value is sent, but there is no guarantee that
|
||||
/// they will see all values.
|
||||
pub fn observe_room_account_data_event(
|
||||
&self,
|
||||
room_id: String,
|
||||
event_type: RoomAccountDataEventType,
|
||||
listener: Box<dyn RoomAccountDataListener>,
|
||||
) -> Result<Arc<TaskHandle>, ClientError> {
|
||||
macro_rules! observe {
|
||||
($t:ty, $cb: expr) => {{
|
||||
// Using an Arc here is mandatory or else the subscriber will never trigger
|
||||
let observer =
|
||||
Arc::new(self.inner.observe_room_events::<RumaRoomAccountDataEvent<$t>, ()>(
|
||||
&RoomId::parse(&room_id)?,
|
||||
));
|
||||
|
||||
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
let mut subscriber = observer.subscribe();
|
||||
loop {
|
||||
if let Some(next) = subscriber.next().await {
|
||||
$cb(next.0);
|
||||
}
|
||||
}
|
||||
}))))
|
||||
}};
|
||||
|
||||
($t:ty) => {{
|
||||
observe!($t, |event: RumaRoomAccountDataEvent<$t>| {
|
||||
listener.on_change(event.into(), room_id.clone());
|
||||
})
|
||||
}};
|
||||
}
|
||||
|
||||
match event_type {
|
||||
RoomAccountDataEventType::FullyRead => {
|
||||
observe!(FullyReadEventContent)
|
||||
}
|
||||
RoomAccountDataEventType::MarkedUnread => {
|
||||
observe!(MarkedUnreadEventContent)
|
||||
}
|
||||
RoomAccountDataEventType::Tag => {
|
||||
observe!(TagEventContent, |event: RumaRoomAccountDataEvent<TagEventContent>| {
|
||||
if let Ok(event) = event.try_into() {
|
||||
listener.on_change(event, room_id.clone());
|
||||
}
|
||||
})
|
||||
}
|
||||
RoomAccountDataEventType::UnstableMarkedUnread => {
|
||||
observe!(UnstableMarkedUnreadEventContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows generic GET requests to be made through the SDKs internal HTTP
|
||||
/// client
|
||||
pub async fn get_url(&self, url: String) -> Result<String, ClientError> {
|
||||
@@ -543,11 +703,44 @@ impl Client {
|
||||
|
||||
/// Empty the server version and unstable features cache.
|
||||
///
|
||||
/// Since the SDK caches server capabilities (versions and unstable
|
||||
/// features), it's possible to have a stale entry in the cache. This
|
||||
/// functions makes it possible to force reset it.
|
||||
pub async fn reset_server_capabilities(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.reset_server_capabilities().await?)
|
||||
/// Since the SDK caches server info (versions, unstable features,
|
||||
/// well-known etc), it's possible to have a stale entry in the cache.
|
||||
/// This functions makes it possible to force reset it.
|
||||
pub async fn reset_server_info(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.reset_server_info().await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Client {
|
||||
/// Retrieves a media file from the media source
|
||||
///
|
||||
/// Not available on Wasm platforms, due to lack of accessible file system.
|
||||
pub async fn get_media_file(
|
||||
&self,
|
||||
media_source: Arc<MediaSource>,
|
||||
filename: Option<String>,
|
||||
mime_type: String,
|
||||
use_cache: bool,
|
||||
temp_dir: Option<String>,
|
||||
) -> Result<Arc<MediaFileHandle>, ClientError> {
|
||||
let source = (*media_source).clone();
|
||||
let mime_type: mime::Mime = mime_type.parse()?;
|
||||
|
||||
let handle = self
|
||||
.inner
|
||||
.media()
|
||||
.get_media_file(
|
||||
&MediaRequestParameters { source: source.media_source, format: MediaFormat::File },
|
||||
filename,
|
||||
&mime_type,
|
||||
use_cache,
|
||||
temp_dir,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Arc::new(MediaFileHandle::new(handle)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,11 +774,20 @@ impl Client {
|
||||
self.inner.available_sliding_sync_versions().await.into_iter().map(Into::into).collect()
|
||||
}
|
||||
|
||||
/// Sets the [ClientDelegate] which will inform about authentication errors.
|
||||
/// Returns an error if the delegate was already set.
|
||||
pub fn set_delegate(
|
||||
self: Arc<Self>,
|
||||
delegate: Option<Box<dyn ClientDelegate>>,
|
||||
) -> Option<Arc<TaskHandle>> {
|
||||
delegate.map(|delegate| {
|
||||
) -> Result<Option<Arc<TaskHandle>>, ClientError> {
|
||||
if self.delegate.get().is_some() {
|
||||
return Err(ClientError::Generic {
|
||||
msg: "Delegate already initialized".to_owned(),
|
||||
details: None,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(delegate.map(|delegate| {
|
||||
let mut session_change_receiver = self.inner.subscribe_to_session_changes();
|
||||
let client_clone = self.clone();
|
||||
let session_change_task = get_runtime_handle().spawn(async move {
|
||||
@@ -601,9 +803,44 @@ impl Client {
|
||||
}
|
||||
});
|
||||
|
||||
*self.delegate.write().unwrap() = Some(Arc::from(delegate));
|
||||
self.delegate.get_or_init(|| Arc::from(delegate));
|
||||
|
||||
Arc::new(TaskHandle::new(session_change_task))
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
/// Sets the [UnableToDecryptDelegate] which will inform about UTDs.
|
||||
/// Returns an error if the delegate was already set.
|
||||
pub async fn set_utd_delegate(
|
||||
self: Arc<Self>,
|
||||
utd_delegate: Box<dyn UnableToDecryptDelegate>,
|
||||
) -> Result<(), ClientError> {
|
||||
if self.utd_hook_manager.get().is_some() {
|
||||
return Err(ClientError::Generic {
|
||||
msg: "UTD delegate already initialized".to_owned(),
|
||||
details: None,
|
||||
});
|
||||
}
|
||||
|
||||
// UTDs detected before this duration may be reclassified as "late decryption"
|
||||
// events (or discarded, if they get decrypted fast enough).
|
||||
const UTD_HOOK_GRACE_PERIOD: Duration = Duration::from_secs(60);
|
||||
|
||||
let mut utd_hook_manager = UtdHookManager::new(
|
||||
Arc::new(UtdHook { delegate: utd_delegate.into() }),
|
||||
(*self.inner).clone(),
|
||||
)
|
||||
.with_max_delay(UTD_HOOK_GRACE_PERIOD);
|
||||
|
||||
if let Err(e) = utd_hook_manager.reload_from_store().await {
|
||||
error!("Unable to reload UTD hook data from data store: {e}");
|
||||
// Carry on with the setup anyway; we shouldn't fail setup just
|
||||
// because the UTD hook failed to load its data.
|
||||
}
|
||||
|
||||
self.utd_hook_manager.get_or_init(|| Arc::new(utd_hook_manager));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn session(&self) -> Result<Session, ClientError> {
|
||||
@@ -862,7 +1099,11 @@ impl Client {
|
||||
}
|
||||
|
||||
pub fn rooms(&self) -> Vec<Arc<Room>> {
|
||||
self.inner.rooms().into_iter().map(|room| Arc::new(Room::new(room))).collect()
|
||||
self.inner
|
||||
.rooms()
|
||||
.into_iter()
|
||||
.map(|room| Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get a room by its ID.
|
||||
@@ -879,14 +1120,17 @@ impl Client {
|
||||
pub fn get_room(&self, room_id: String) -> Result<Option<Arc<Room>>, ClientError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
let sdk_room = self.inner.get_room(&room_id);
|
||||
let room = sdk_room.map(|room| Arc::new(Room::new(room)));
|
||||
|
||||
let room =
|
||||
sdk_room.map(|room| Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())));
|
||||
Ok(room)
|
||||
}
|
||||
|
||||
pub fn get_dm_room(&self, user_id: String) -> Result<Option<Arc<Room>>, ClientError> {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
let sdk_room = self.inner.get_dm_room(&user_id);
|
||||
let dm = sdk_room.map(|room| Arc::new(Room::new(room)));
|
||||
let dm =
|
||||
sdk_room.map(|room| Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())));
|
||||
Ok(dm)
|
||||
}
|
||||
|
||||
@@ -918,12 +1162,12 @@ impl Client {
|
||||
Ok(Arc::new(NotificationClient {
|
||||
inner: MatrixNotificationClient::new((*self.inner).clone(), process_setup.into())
|
||||
.await?,
|
||||
_client: self.clone(),
|
||||
client: self.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn sync_service(&self) -> Arc<SyncServiceBuilder> {
|
||||
SyncServiceBuilder::new((*self.inner).clone())
|
||||
SyncServiceBuilder::new((*self.inner).clone(), self.utd_hook_manager.get().cloned())
|
||||
}
|
||||
|
||||
pub async fn get_notification_settings(&self) -> Arc<NotificationSettings> {
|
||||
@@ -942,7 +1186,7 @@ impl Client {
|
||||
if let Some(raw_content) = self
|
||||
.inner
|
||||
.account()
|
||||
.fetch_account_data(GlobalAccountDataEventType::IgnoredUserList)
|
||||
.fetch_account_data(RumaGlobalAccountDataEventType::IgnoredUserList)
|
||||
.await?
|
||||
{
|
||||
let content = raw_content.deserialize_as::<IgnoredUserListEventContent>()?;
|
||||
@@ -993,7 +1237,7 @@ impl Client {
|
||||
pub async fn join_room_by_id(&self, room_id: String) -> Result<Arc<Room>, ClientError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
let room = self.inner.join_room_by_id(room_id.as_ref()).await?;
|
||||
Ok(Arc::new(Room::new(room)))
|
||||
Ok(Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())))
|
||||
}
|
||||
|
||||
/// Join a room by its ID or alias.
|
||||
@@ -1014,7 +1258,7 @@ impl Client {
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let room =
|
||||
self.inner.join_room_by_id_or_alias(room_id.as_ref(), server_names.as_ref()).await?;
|
||||
Ok(Arc::new(Room::new(room)))
|
||||
Ok(Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())))
|
||||
}
|
||||
|
||||
/// Knock on a room to join it using its ID or alias.
|
||||
@@ -1028,7 +1272,7 @@ impl Client {
|
||||
let server_names =
|
||||
server_names.iter().map(ServerName::parse).collect::<Result<Vec<_>, _>>()?;
|
||||
let room = self.inner.knock(room_id, reason, server_names).await?;
|
||||
Ok(Arc::new(Room::new(room)))
|
||||
Ok(Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())))
|
||||
}
|
||||
|
||||
pub async fn get_recently_visited_rooms(&self) -> Result<Vec<String>, ClientError> {
|
||||
@@ -1122,7 +1366,10 @@ impl Client {
|
||||
/// or an externally set timeout happens.**
|
||||
pub async fn await_room_remote_echo(&self, room_id: String) -> Result<Arc<Room>, ClientError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
Ok(Arc::new(Room::new(self.inner.await_room_remote_echo(&room_id).await)))
|
||||
Ok(Arc::new(Room::new(
|
||||
self.inner.await_room_remote_echo(&room_id).await,
|
||||
self.utd_hook_manager.get().cloned(),
|
||||
)))
|
||||
}
|
||||
|
||||
/// Lets the user know whether this is an `m.login.password` based
|
||||
@@ -1182,30 +1429,233 @@ impl Client {
|
||||
|
||||
/// Clear all the non-critical caches for this Client instance.
|
||||
///
|
||||
/// WARNING: This will clear all the caches, including the base store (state
|
||||
/// store), so callers must make sure that the Client is at rest before
|
||||
/// calling it.
|
||||
///
|
||||
/// In particular, if a [`SyncService`] is running, it must be passed here
|
||||
/// as a parameter, or stopped before calling this method. Ideally, the
|
||||
/// send queues should have been disabled and must all be inactive (i.e.
|
||||
/// not sending events); this method will disable them, but it might not
|
||||
/// be enough if the queues are still processing events.
|
||||
///
|
||||
/// After the method returns, the Client will be in an unstable
|
||||
/// state, and it is required that the caller reinstantiates a new
|
||||
/// Client instance, be it via dropping the previous and re-creating it,
|
||||
/// restarting their application, or any other similar means.
|
||||
///
|
||||
/// - This will get rid of the backing state store file, if provided.
|
||||
/// - This will empty all the room's persisted event caches, so all rooms
|
||||
/// will start as if they were empty.
|
||||
/// - This will empty the media cache according to the current media
|
||||
/// retention policy.
|
||||
pub async fn clear_caches(&self) -> Result<(), ClientError> {
|
||||
let closure = async || -> Result<_, EventCacheError> {
|
||||
pub async fn clear_caches(
|
||||
&self,
|
||||
sync_service: Option<Arc<SyncService>>,
|
||||
) -> Result<(), ClientError> {
|
||||
let closure = async || -> Result<_, ClientError> {
|
||||
// First, make sure to expire sessions in the sync service.
|
||||
if let Some(sync_service) = sync_service {
|
||||
sync_service.inner.expire_sessions().await;
|
||||
}
|
||||
|
||||
// Disable the send queues, as they might read and write to the state store.
|
||||
// Events being send might still be active, and cause errors if
|
||||
// processing finishes, so this will only minimize damage. Since
|
||||
// this method should only be called in exceptional cases, this has
|
||||
// been deemed acceptable.
|
||||
self.inner.send_queue().set_enabled(false).await;
|
||||
|
||||
// Clean up the media cache according to the current media retention policy.
|
||||
self.inner.event_cache_store().lock().await?.clean_up_media_cache().await?;
|
||||
self.inner
|
||||
.event_cache_store()
|
||||
.lock()
|
||||
.await
|
||||
.map_err(EventCacheError::from)?
|
||||
.clean_up_media_cache()
|
||||
.await
|
||||
.map_err(EventCacheError::from)?;
|
||||
|
||||
// Clear all the room chunks. It's important to *not* call
|
||||
// `EventCacheStore::clear_all_rooms_chunks` here, because there might be live
|
||||
// `EventCacheStore::clear_all_linked_chunks` here, because there might be live
|
||||
// observers of the linked chunks, and that would cause some very bad state
|
||||
// mismatch.
|
||||
self.inner.event_cache().clear_all_rooms().await?;
|
||||
|
||||
// Delete the state store file, if it exists.
|
||||
if let Some(store_path) = &self.store_path {
|
||||
debug!("Removing the state store: {}", store_path.display());
|
||||
|
||||
// The state store and the crypto store both live in the same store path, so we
|
||||
// can't blindly delete the directory.
|
||||
//
|
||||
// Delete the state store SQLite file, as well as the write-ahead log (WAL) and
|
||||
// shared-memory (SHM) files, if they exist.
|
||||
|
||||
for file_name in [
|
||||
PathBuf::from(STATE_STORE_DATABASE_NAME),
|
||||
PathBuf::from(format!("{STATE_STORE_DATABASE_NAME}.wal")),
|
||||
PathBuf::from(format!("{STATE_STORE_DATABASE_NAME}.shm")),
|
||||
] {
|
||||
let file_path = store_path.join(file_name);
|
||||
if file_path.exists() {
|
||||
debug!("Removing file: {}", file_path.display());
|
||||
std::fs::remove_file(&file_path).map_err(|err| ClientError::Generic {
|
||||
msg: format!(
|
||||
"couldn't delete the state store file {}: {err}",
|
||||
file_path.display()
|
||||
),
|
||||
details: None,
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
Ok(closure().await?)
|
||||
closure().await
|
||||
}
|
||||
|
||||
/// Checks if the server supports the report room API.
|
||||
pub async fn is_report_room_api_supported(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.server_versions().await?.contains(&ruma::api::MatrixVersion::V1_13))
|
||||
}
|
||||
|
||||
/// Checks if the server supports the LiveKit RTC focus for placing calls.
|
||||
pub async fn is_livekit_rtc_supported(&self) -> Result<bool, ClientError> {
|
||||
Ok(self
|
||||
.inner
|
||||
.rtc_foci()
|
||||
.await?
|
||||
.iter()
|
||||
.any(|focus| matches!(focus, RtcFocusInfo::LiveKit(_))))
|
||||
}
|
||||
|
||||
/// Subscribe to changes in the media preview configuration.
|
||||
pub async fn subscribe_to_media_preview_config(
|
||||
&self,
|
||||
listener: Box<dyn MediaPreviewConfigListener>,
|
||||
) -> Result<Arc<TaskHandle>, ClientError> {
|
||||
let (initial_value, stream) = self.inner.account().observe_media_preview_config().await?;
|
||||
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
// Send the initial value to the listener.
|
||||
listener.on_change(initial_value.map(|config| config.into()));
|
||||
// Listen for changes and notify the listener.
|
||||
pin_mut!(stream);
|
||||
while let Some(media_preview_config) = stream.next().await {
|
||||
listener.on_change(Some(media_preview_config.into()));
|
||||
}
|
||||
}))))
|
||||
}
|
||||
|
||||
/// Set the media previews timeline display policy
|
||||
pub async fn set_media_preview_display_policy(
|
||||
&self,
|
||||
policy: MediaPreviews,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner.account().set_media_previews_display_policy(policy.into()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the media previews timeline display policy
|
||||
/// currently stored in the cache.
|
||||
pub async fn get_media_preview_display_policy(
|
||||
&self,
|
||||
) -> Result<Option<MediaPreviews>, ClientError> {
|
||||
let configuration = self.inner.account().get_media_preview_config_event_content().await?;
|
||||
match configuration {
|
||||
Some(configuration) => Ok(Some(configuration.media_previews.into())),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the invite request avatars display policy
|
||||
pub async fn set_invite_avatars_display_policy(
|
||||
&self,
|
||||
policy: InviteAvatars,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner.account().set_invite_avatars_display_policy(policy.into()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the invite request avatars display policy
|
||||
/// currently stored in the cache.
|
||||
pub async fn get_invite_avatars_display_policy(
|
||||
&self,
|
||||
) -> Result<Option<InviteAvatars>, ClientError> {
|
||||
let configuration = self.inner.account().get_media_preview_config_event_content().await?;
|
||||
match configuration {
|
||||
Some(configuration) => Ok(Some(configuration.invite_avatars.into())),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch the media preview configuration from the server.
|
||||
pub async fn fetch_media_preview_config(
|
||||
&self,
|
||||
) -> Result<Option<MediaPreviewConfig>, ClientError> {
|
||||
Ok(self.inner.account().fetch_media_preview_config_event_content().await?.map(Into::into))
|
||||
}
|
||||
|
||||
/// Gets the `max_upload_size` value from the homeserver, which controls the
|
||||
/// max size a media upload request can have.
|
||||
pub async fn get_max_media_upload_size(&self) -> Result<u64, ClientError> {
|
||||
let max_upload_size = self.inner.load_or_fetch_max_upload_size().await?;
|
||||
Ok(max_upload_size.into())
|
||||
}
|
||||
|
||||
/// Subscribe to [`RoomInfo`] updates given a provided [`RoomId`].
|
||||
///
|
||||
/// This works even for rooms we haven't received yet, so we can subscribe
|
||||
/// to this and wait until we receive updates from them when sync responses
|
||||
/// are processed.
|
||||
///
|
||||
/// Note this method should be used sparingly since using callback
|
||||
/// interfaces is expensive, as well as keeping them alive for a long
|
||||
/// time. Usages of this method should be short-lived and dropped as
|
||||
/// soon as possible.
|
||||
pub async fn subscribe_to_room_info(
|
||||
&self,
|
||||
room_id: String,
|
||||
listener: Box<dyn RoomInfoListener>,
|
||||
) -> Result<Arc<TaskHandle>, ClientError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
|
||||
// Emit the initial event, if present
|
||||
if let Some(room) = self.inner.get_room(&room_id) {
|
||||
if let Ok(room_info) = RoomInfo::new(&room).await {
|
||||
listener.call(room_info);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn({
|
||||
let client = self.inner.clone();
|
||||
let mut receiver = client.room_info_notable_update_receiver();
|
||||
async move {
|
||||
while let Ok(room_update) = receiver.recv().await {
|
||||
if room_update.room_id != room_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(room) = client.get_room(&room_id) {
|
||||
if let Ok(room_info) = RoomInfo::new(&room).await {
|
||||
listener.call(room_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))))
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait IgnoredUsersListener: Sync + Send {
|
||||
pub trait MediaPreviewConfigListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_change(&self, media_preview_config: Option<MediaPreviewConfig>);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait IgnoredUsersListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn call(&self, ignored_user_ids: Vec<String>);
|
||||
}
|
||||
|
||||
@@ -1280,15 +1730,13 @@ impl From<&search_users::v3::User> for UserProfile {
|
||||
|
||||
impl Client {
|
||||
fn process_session_change(&self, session_change: SessionChange) {
|
||||
if let Some(delegate) = self.delegate.read().unwrap().clone() {
|
||||
if let Some(delegate) = self.delegate.get().cloned() {
|
||||
debug!("Applying session change: {session_change:?}");
|
||||
get_runtime_handle().spawn_blocking(move || match session_change {
|
||||
SessionChange::UnknownToken { soft_logout } => {
|
||||
delegate.did_receive_auth_error(soft_logout);
|
||||
}
|
||||
SessionChange::TokensRefreshed => {
|
||||
delegate.did_refresh_tokens();
|
||||
}
|
||||
SessionChange::TokensRefreshed => {}
|
||||
});
|
||||
} else {
|
||||
debug!(
|
||||
@@ -1519,7 +1967,8 @@ impl TryFrom<CreateRoomParameters> for create_room::v3::Request {
|
||||
Err(e) => {
|
||||
return Err(ClientError::Generic {
|
||||
msg: format!("Failed to serialize power levels, error: {e}"),
|
||||
})
|
||||
details: Some(format!("{e:?}")),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1747,17 +2196,20 @@ fn gen_transaction_id() -> String {
|
||||
|
||||
/// A file handle that takes ownership of a media file on disk. When the handle
|
||||
/// is dropped, the file will be removed from the disk.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct MediaFileHandle {
|
||||
inner: RwLock<Option<SdkMediaFileHandle>>,
|
||||
inner: std::sync::RwLock<Option<SdkMediaFileHandle>>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
impl MediaFileHandle {
|
||||
fn new(handle: SdkMediaFileHandle) -> Self {
|
||||
Self { inner: RwLock::new(Some(handle)) }
|
||||
Self { inner: std::sync::RwLock::new(Some(handle)) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl MediaFileHandle {
|
||||
/// Get the media file's path.
|
||||
@@ -1984,7 +2436,7 @@ impl TryFrom<RumaJoinRule> for JoinRule {
|
||||
}
|
||||
RumaJoinRule::Invite => Ok(JoinRule::Invite),
|
||||
RumaJoinRule::_Custom(_) => Ok(JoinRule::Custom { repr: value.as_str().to_owned() }),
|
||||
_ => Err(format!("Unknown JoinRule: {:?}", value)),
|
||||
_ => Err(format!("Unknown JoinRule: {value:?}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2001,7 +2453,7 @@ impl TryFrom<RumaAllowRule> for AllowRule {
|
||||
.map_err(|e| format!("Couldn't serialize custom AllowRule: {e:?}"))?;
|
||||
Ok(Self::Custom { json })
|
||||
}
|
||||
_ => Err(format!("Invalid AllowRule: {:?}", value)),
|
||||
_ => Err(format!("Invalid AllowRule: {value:?}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
use std::{fs, num::NonZeroUsize, path::Path, sync::Arc, time::Duration};
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use futures_util::StreamExt;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use matrix_sdk::reqwest::Certificate;
|
||||
use matrix_sdk::{
|
||||
authentication::oauth::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason},
|
||||
crypto::{
|
||||
types::qr_login::{LoginQrCodeDecodeError, QrCodeModeData},
|
||||
CollectStrategy, TrustRequirement,
|
||||
types::qr_login::QrCodeModeData, CollectStrategy, DecryptionSettings, TrustRequirement,
|
||||
},
|
||||
encryption::{BackupDownloadStrategy, EncryptionSettings},
|
||||
event_cache::EventCacheError,
|
||||
reqwest::Certificate,
|
||||
ruma::{ServerName, UserId},
|
||||
sliding_sync::{
|
||||
Error as MatrixSlidingSyncError, VersionBuilder as MatrixSlidingSyncVersionBuilder,
|
||||
VersionBuilderError,
|
||||
},
|
||||
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
|
||||
RumaApiError, SqliteStoreConfig,
|
||||
RumaApiError, SqliteStoreConfig, ThreadingSupport,
|
||||
};
|
||||
use ruma::api::error::{DeserializationError, FromHttpResponseError};
|
||||
use tracing::{debug, error};
|
||||
@@ -25,8 +23,13 @@ use zeroize::Zeroizing;
|
||||
|
||||
use super::client::Client;
|
||||
use crate::{
|
||||
authentication::OidcConfiguration, client::ClientSessionDelegate, error::ClientError,
|
||||
helpers::unwrap_or_clone_arc, task_handle::TaskHandle,
|
||||
authentication::OidcConfiguration,
|
||||
client::ClientSessionDelegate,
|
||||
error::ClientError,
|
||||
helpers::unwrap_or_clone_arc,
|
||||
qr_code::{HumanQrLoginError, QrCodeData, QrLoginProgressListener},
|
||||
runtime::get_runtime_handle,
|
||||
task_handle::TaskHandle,
|
||||
};
|
||||
|
||||
/// A list of bytes containing a certificate in DER or PEM form.
|
||||
@@ -39,152 +42,6 @@ enum HomeserverConfig {
|
||||
ServerNameOrUrl(String),
|
||||
}
|
||||
|
||||
/// Data for the QR code login mechanism.
|
||||
///
|
||||
/// The [`QrCodeData`] can be serialized and encoded as a QR code or it can be
|
||||
/// decoded from a QR code.
|
||||
#[derive(Debug, uniffi::Object)]
|
||||
pub struct QrCodeData {
|
||||
inner: qrcode::QrCodeData,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl QrCodeData {
|
||||
/// Attempt to decode a slice of bytes into a [`QrCodeData`] object.
|
||||
///
|
||||
/// The slice of bytes would generally be returned by a QR code decoder.
|
||||
#[uniffi::constructor]
|
||||
pub fn from_bytes(bytes: Vec<u8>) -> Result<Arc<Self>, QrCodeDecodeError> {
|
||||
Ok(Self { inner: qrcode::QrCodeData::from_bytes(&bytes)? }.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for the decoding of the [`QrCodeData`].
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum QrCodeDecodeError {
|
||||
#[error("Error decoding QR code: {error:?}")]
|
||||
Crypto {
|
||||
#[from]
|
||||
error: LoginQrCodeDecodeError,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
pub enum HumanQrLoginError {
|
||||
#[error("Linking with this device is not supported.")]
|
||||
LinkingNotSupported,
|
||||
#[error("The sign in was cancelled.")]
|
||||
Cancelled,
|
||||
#[error("The sign in was not completed in the required time.")]
|
||||
Expired,
|
||||
#[error("A secure connection could not have been established between the two devices.")]
|
||||
ConnectionInsecure,
|
||||
#[error("The sign in was declined.")]
|
||||
Declined,
|
||||
#[error("An unknown error has happened.")]
|
||||
Unknown,
|
||||
#[error("The homeserver doesn't provide sliding sync in its configuration.")]
|
||||
SlidingSyncNotAvailable,
|
||||
#[error("Unable to use OIDC as the supplied client metadata is invalid.")]
|
||||
OidcMetadataInvalid,
|
||||
#[error("The other device is not signed in and as such can't sign in other devices.")]
|
||||
OtherDeviceNotSignedIn,
|
||||
}
|
||||
|
||||
impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
|
||||
fn from(value: qrcode::QRCodeLoginError) -> Self {
|
||||
use qrcode::{QRCodeLoginError, SecureChannelError};
|
||||
|
||||
match value {
|
||||
QRCodeLoginError::LoginFailure { reason, .. } => match reason {
|
||||
LoginFailureReason::UnsupportedProtocol => HumanQrLoginError::LinkingNotSupported,
|
||||
LoginFailureReason::AuthorizationExpired => HumanQrLoginError::Expired,
|
||||
LoginFailureReason::UserCancelled => HumanQrLoginError::Cancelled,
|
||||
_ => HumanQrLoginError::Unknown,
|
||||
},
|
||||
|
||||
QRCodeLoginError::OAuth(e) => {
|
||||
if let Some(e) = e.as_request_token_error() {
|
||||
match e {
|
||||
DeviceCodeErrorResponseType::AccessDenied => HumanQrLoginError::Declined,
|
||||
DeviceCodeErrorResponseType::ExpiredToken => HumanQrLoginError::Expired,
|
||||
_ => HumanQrLoginError::Unknown,
|
||||
}
|
||||
} else {
|
||||
HumanQrLoginError::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
QRCodeLoginError::SecureChannel(e) => match e {
|
||||
SecureChannelError::Utf8(_)
|
||||
| SecureChannelError::MessageDecode(_)
|
||||
| SecureChannelError::Json(_)
|
||||
| SecureChannelError::RendezvousChannel(_) => HumanQrLoginError::Unknown,
|
||||
SecureChannelError::SecureChannelMessage { .. }
|
||||
| SecureChannelError::Ecies(_)
|
||||
| SecureChannelError::InvalidCheckCode => HumanQrLoginError::ConnectionInsecure,
|
||||
SecureChannelError::InvalidIntent => HumanQrLoginError::OtherDeviceNotSignedIn,
|
||||
},
|
||||
|
||||
QRCodeLoginError::UnexpectedMessage { .. }
|
||||
| QRCodeLoginError::CrossProcessRefreshLock(_)
|
||||
| QRCodeLoginError::DeviceKeyUpload(_)
|
||||
| QRCodeLoginError::SessionTokens(_)
|
||||
| QRCodeLoginError::UserIdDiscovery(_)
|
||||
| QRCodeLoginError::SecretImport(_) => HumanQrLoginError::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum describing the progress of the QR-code login.
|
||||
#[derive(Debug, Default, Clone, uniffi::Enum)]
|
||||
pub enum QrLoginProgress {
|
||||
/// The login process is starting.
|
||||
#[default]
|
||||
Starting,
|
||||
/// We established a secure channel with the other device.
|
||||
EstablishingSecureChannel {
|
||||
/// The check code that the device should display so the other device
|
||||
/// can confirm that the channel is secure as well.
|
||||
check_code: u8,
|
||||
/// The string representation of the check code, will be guaranteed to
|
||||
/// be 2 characters long, preserving the leading zero if the
|
||||
/// first digit is a zero.
|
||||
check_code_string: String,
|
||||
},
|
||||
/// 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 {
|
||||
fn on_update(&self, state: QrLoginProgress);
|
||||
}
|
||||
|
||||
impl From<qrcode::LoginProgress> for QrLoginProgress {
|
||||
fn from(value: qrcode::LoginProgress) -> Self {
|
||||
use qrcode::LoginProgress;
|
||||
|
||||
match value {
|
||||
LoginProgress::Starting => Self::Starting,
|
||||
LoginProgress::EstablishingSecureChannel { check_code } => {
|
||||
let check_code = check_code.to_digit();
|
||||
|
||||
Self::EstablishingSecureChannel {
|
||||
check_code,
|
||||
check_code_string: format!("{check_code:02}"),
|
||||
}
|
||||
}
|
||||
LoginProgress::WaitingForToken { user_code } => Self::WaitingForToken { user_code },
|
||||
LoginProgress::Done => Self::Done,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum ClientBuildError {
|
||||
@@ -262,24 +119,29 @@ pub struct ClientBuilder {
|
||||
system_is_memory_constrained: bool,
|
||||
username: Option<String>,
|
||||
homeserver_cfg: Option<HomeserverConfig>,
|
||||
user_agent: Option<String>,
|
||||
sliding_sync_version_builder: SlidingSyncVersionBuilder,
|
||||
proxy: Option<String>,
|
||||
disable_ssl_verification: bool,
|
||||
disable_automatic_token_refresh: bool,
|
||||
cross_process_store_locks_holder_name: Option<String>,
|
||||
enable_oidc_refresh_lock: bool,
|
||||
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
|
||||
additional_root_certificates: Vec<Vec<u8>>,
|
||||
disable_built_in_root_certificates: bool,
|
||||
encryption_settings: EncryptionSettings,
|
||||
room_key_recipient_strategy: CollectStrategy,
|
||||
decryption_trust_requirement: TrustRequirement,
|
||||
decryption_settings: DecryptionSettings,
|
||||
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,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
user_agent: Option<String>,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
proxy: Option<String>,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
disable_ssl_verification: bool,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
disable_built_in_root_certificates: bool,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
additional_root_certificates: Vec<Vec<u8>>,
|
||||
|
||||
threads_enabled: bool,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
@@ -312,29 +174,15 @@ impl ClientBuilder {
|
||||
auto_enable_backups: false,
|
||||
},
|
||||
room_key_recipient_strategy: Default::default(),
|
||||
decryption_trust_requirement: TrustRequirement::Untrusted,
|
||||
decryption_settings: DecryptionSettings {
|
||||
sender_device_trust_requirement: TrustRequirement::Untrusted,
|
||||
},
|
||||
enable_share_history_on_invite: false,
|
||||
request_config: Default::default(),
|
||||
use_event_cache_persistent_storage: false,
|
||||
threads_enabled: 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,
|
||||
@@ -463,12 +311,6 @@ impl ClientBuilder {
|
||||
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);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn sliding_sync_version_builder(
|
||||
self: Arc<Self>,
|
||||
version_builder: SlidingSyncVersionBuilder,
|
||||
@@ -478,43 +320,12 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn proxy(self: Arc<Self>, url: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.proxy = Some(url);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn disable_ssl_verification(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.disable_ssl_verification = true;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn disable_automatic_token_refresh(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.disable_automatic_token_refresh = true;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn add_root_certificates(
|
||||
self: Arc<Self>,
|
||||
certificates: Vec<CertificateBytes>,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.additional_root_certificates = certificates;
|
||||
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Don't trust any system root certificates, only trust the certificates
|
||||
/// provided through
|
||||
/// [`add_root_certificates`][ClientBuilder::add_root_certificates].
|
||||
pub fn disable_built_in_root_certificates(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.disable_built_in_root_certificates = true;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn auto_enable_cross_signing(
|
||||
self: Arc<Self>,
|
||||
auto_enable_cross_signing: bool,
|
||||
@@ -553,12 +364,25 @@ impl ClientBuilder {
|
||||
}
|
||||
|
||||
/// Set the trust requirement to be used when decrypting events.
|
||||
pub fn room_decryption_trust_requirement(
|
||||
pub fn decryption_settings(
|
||||
self: Arc<Self>,
|
||||
trust_requirement: TrustRequirement,
|
||||
decryption_settings: DecryptionSettings,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.decryption_trust_requirement = trust_requirement;
|
||||
builder.decryption_settings = decryption_settings;
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -569,6 +393,12 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn threads_enabled(self: Arc<Self>, enabled: bool) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.threads_enabled = enabled;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub async fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientBuildError> {
|
||||
let builder = unwrap_or_clone_arc(self);
|
||||
let mut inner_builder = MatrixClient::builder();
|
||||
@@ -578,14 +408,16 @@ impl ClientBuilder {
|
||||
inner_builder.cross_process_store_locks_holder_name(holder_name.clone());
|
||||
}
|
||||
|
||||
if let Some(session_paths) = &builder.session_paths {
|
||||
let store_path = if let Some(session_paths) = &builder.session_paths {
|
||||
// This is the path where both the state store and the crypto store will live.
|
||||
let data_path = Path::new(&session_paths.data_path);
|
||||
// This is the path where the event cache store will live.
|
||||
let cache_path = Path::new(&session_paths.cache_path);
|
||||
|
||||
debug!(
|
||||
data_path = %data_path.to_string_lossy(),
|
||||
cache_path = %cache_path.to_string_lossy(),
|
||||
"Creating directories for data and cache stores.",
|
||||
event_cache_path = %cache_path.to_string_lossy(),
|
||||
"Creating directories for data (state and crypto) and cache stores.",
|
||||
);
|
||||
|
||||
fs::create_dir_all(data_path)?;
|
||||
@@ -614,9 +446,12 @@ impl ClientBuilder {
|
||||
|
||||
inner_builder = inner_builder
|
||||
.sqlite_store_with_config_and_cache_path(sqlite_store_config, Some(cache_path));
|
||||
|
||||
Some(data_path.to_owned())
|
||||
} else {
|
||||
debug!("Not using a store path.");
|
||||
}
|
||||
None
|
||||
};
|
||||
|
||||
// Determine server either from URL, server name or user ID.
|
||||
inner_builder = match builder.homeserver_cfg {
|
||||
@@ -640,52 +475,56 @@ impl ClientBuilder {
|
||||
}
|
||||
};
|
||||
|
||||
let mut certificates = Vec::new();
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
let mut certificates = Vec::new();
|
||||
|
||||
for certificate in builder.additional_root_certificates {
|
||||
// We don't really know what type of certificate we may get here, so let's try
|
||||
// first one type, then the other.
|
||||
match Certificate::from_der(&certificate) {
|
||||
Ok(cert) => {
|
||||
certificates.push(cert);
|
||||
}
|
||||
Err(der_error) => {
|
||||
let cert = Certificate::from_pem(&certificate).map_err(|pem_error| {
|
||||
ClientBuildError::Generic {
|
||||
message: format!("Failed to add a root certificate as DER ({der_error:?}) or PEM ({pem_error:?})"),
|
||||
}
|
||||
})?;
|
||||
certificates.push(cert);
|
||||
for certificate in builder.additional_root_certificates {
|
||||
// We don't really know what type of certificate we may get here, so let's try
|
||||
// first one type, then the other.
|
||||
match Certificate::from_der(&certificate) {
|
||||
Ok(cert) => {
|
||||
certificates.push(cert);
|
||||
}
|
||||
Err(der_error) => {
|
||||
let cert = Certificate::from_pem(&certificate).map_err(|pem_error| {
|
||||
ClientBuildError::Generic {
|
||||
message: format!("Failed to add a root certificate as DER ({der_error:?}) or PEM ({pem_error:?})"),
|
||||
}
|
||||
})?;
|
||||
certificates.push(cert);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner_builder = inner_builder.add_root_certificates(certificates);
|
||||
inner_builder = inner_builder.add_root_certificates(certificates);
|
||||
|
||||
if builder.disable_built_in_root_certificates {
|
||||
inner_builder = inner_builder.disable_built_in_root_certificates();
|
||||
}
|
||||
if builder.disable_built_in_root_certificates {
|
||||
inner_builder = inner_builder.disable_built_in_root_certificates();
|
||||
}
|
||||
|
||||
if let Some(proxy) = builder.proxy {
|
||||
inner_builder = inner_builder.proxy(proxy);
|
||||
}
|
||||
if let Some(proxy) = builder.proxy {
|
||||
inner_builder = inner_builder.proxy(proxy);
|
||||
}
|
||||
|
||||
if builder.disable_ssl_verification {
|
||||
inner_builder = inner_builder.disable_ssl_verification();
|
||||
if builder.disable_ssl_verification {
|
||||
inner_builder = inner_builder.disable_ssl_verification();
|
||||
}
|
||||
|
||||
if let Some(user_agent) = builder.user_agent {
|
||||
inner_builder = inner_builder.user_agent(user_agent);
|
||||
}
|
||||
}
|
||||
|
||||
if !builder.disable_automatic_token_refresh {
|
||||
inner_builder = inner_builder.handle_refresh_tokens();
|
||||
}
|
||||
|
||||
if let Some(user_agent) = builder.user_agent {
|
||||
inner_builder = inner_builder.user_agent(user_agent);
|
||||
}
|
||||
|
||||
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_settings(builder.decryption_settings)
|
||||
.with_enable_share_history_on_invite(builder.enable_share_history_on_invite);
|
||||
|
||||
match builder.sliding_sync_version_builder {
|
||||
SlidingSyncVersionBuilder::None => {
|
||||
@@ -725,24 +564,22 @@ impl ClientBuilder {
|
||||
inner_builder = inner_builder.request_config(updated_config);
|
||||
}
|
||||
|
||||
inner_builder = inner_builder.with_threading_support(if builder.threads_enabled {
|
||||
ThreadingSupport::Enabled
|
||||
} else {
|
||||
ThreadingSupport::Disabled
|
||||
});
|
||||
|
||||
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?,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -801,6 +638,47 @@ impl ClientBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl ClientBuilder {
|
||||
pub fn proxy(self: Arc<Self>, url: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.proxy = Some(url);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn disable_ssl_verification(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.disable_ssl_verification = true;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn add_root_certificates(
|
||||
self: Arc<Self>,
|
||||
certificates: Vec<CertificateBytes>,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.additional_root_certificates = certificates;
|
||||
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Don't trust any system root certificates, only trust the certificates
|
||||
/// provided through
|
||||
/// [`add_root_certificates`][ClientBuilder::add_root_certificates].
|
||||
pub fn disable_built_in_root_certificates(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.disable_built_in_root_certificates = true;
|
||||
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);
|
||||
Arc::new(builder)
|
||||
}
|
||||
}
|
||||
|
||||
/// The store paths the client will use when built.
|
||||
#[derive(Clone)]
|
||||
struct SessionPaths {
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::ClientError;
|
||||
|
||||
/// Well-known settings specific to ElementCall
|
||||
#[derive(Deserialize, uniffi::Record)]
|
||||
pub struct ElementCallWellKnown {
|
||||
widget_url: String,
|
||||
}
|
||||
|
||||
/// Element specific well-known settings
|
||||
#[derive(Deserialize, uniffi::Record)]
|
||||
pub struct ElementWellKnown {
|
||||
call: Option<ElementCallWellKnown>,
|
||||
registration_helper_url: Option<String>,
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
encryption,
|
||||
encryption::{backups, recovery},
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use thiserror::Error;
|
||||
use tracing::{error, info};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{client::Client, error::ClientError, ruma::AuthData, task_handle::TaskHandle};
|
||||
use crate::{
|
||||
client::Client, error::ClientError, ruma::AuthData, runtime::get_runtime_handle,
|
||||
task_handle::TaskHandle,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Encryption {
|
||||
@@ -25,22 +28,22 @@ pub struct Encryption {
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait BackupStateListener: Sync + Send {
|
||||
pub trait BackupStateListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, status: BackupState);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait BackupSteadyStateListener: Sync + Send {
|
||||
pub trait BackupSteadyStateListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, status: BackupUploadState);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RecoveryStateListener: Sync + Send {
|
||||
pub trait RecoveryStateListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, status: RecoveryState);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait VerificationStateListener: Sync + Send {
|
||||
pub trait VerificationStateListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, status: VerificationState);
|
||||
}
|
||||
|
||||
@@ -164,7 +167,7 @@ impl From<recovery::RecoveryState> for RecoveryState {
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait EnableRecoveryProgressListener: Sync + Send {
|
||||
pub trait EnableRecoveryProgressListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, status: EnableRecoveryProgress);
|
||||
}
|
||||
|
||||
@@ -369,12 +372,8 @@ impl Encryption {
|
||||
/// Completely reset the current user's crypto identity: reset the cross
|
||||
/// signing keys, delete the existing backup and recovery key.
|
||||
pub async fn reset_identity(&self) -> Result<Option<Arc<IdentityResetHandle>>, ClientError> {
|
||||
if let Some(reset_handle) = self
|
||||
.inner
|
||||
.recovery()
|
||||
.reset_identity()
|
||||
.await
|
||||
.map_err(|e| ClientError::Generic { msg: e.to_string() })?
|
||||
if let Some(reset_handle) =
|
||||
self.inner.recovery().reset_identity().await.map_err(ClientError::from_err)?
|
||||
{
|
||||
return Ok(Some(Arc::new(IdentityResetHandle { inner: reset_handle })));
|
||||
}
|
||||
@@ -441,9 +440,9 @@ 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}");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
info!("Requesting identity from the server.");
|
||||
|
||||
@@ -541,12 +540,9 @@ impl IdentityResetHandle {
|
||||
/// 4. Finally, re-enable key backups only if they were enabled before
|
||||
pub async fn reset(&self, auth: Option<AuthData>) -> Result<(), ClientError> {
|
||||
if let Some(auth) = auth {
|
||||
self.inner
|
||||
.reset(Some(auth.into()))
|
||||
.await
|
||||
.map_err(|e| ClientError::Generic { msg: e.to_string() })
|
||||
self.inner.reset(Some(auth.into())).await.map_err(ClientError::from_err)
|
||||
} else {
|
||||
self.inner.reset(None).await.map_err(|e| ClientError::Generic { msg: e.to_string() })
|
||||
self.inner.reset(None).await.map_err(ClientError::from_err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
use std::{collections::HashMap, fmt, fmt::Display, time::SystemTime};
|
||||
use std::{collections::HashMap, error::Error, fmt, fmt::Display};
|
||||
|
||||
use matrix_sdk::{
|
||||
authentication::oauth::OAuthError, encryption::CryptoStoreError, event_cache::EventCacheError,
|
||||
reqwest, room::edit::EditError, send_queue::RoomSendQueueError, HttpError, IdParseError,
|
||||
NotificationSettingsError as SdkNotificationSettingsError,
|
||||
authentication::oauth::OAuthError,
|
||||
encryption::{identities::RequestVerificationError, 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 ruma::{
|
||||
api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter},
|
||||
MilliSecondsSinceUnixEpoch,
|
||||
};
|
||||
use tracing::warn;
|
||||
use uniffi::UnexpectedUniFFICallbackError;
|
||||
|
||||
use crate::{room_list::RoomListError, timeline::FocusEventError};
|
||||
@@ -15,32 +23,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 +68,146 @@ impl From<matrix_sdk::Error> for ClientError {
|
||||
let code = kind.errcode().to_string();
|
||||
let Ok(kind) = kind.to_owned().try_into() else {
|
||||
// We couldn't parse the API error, so we return a generic one instead
|
||||
return Self::Generic { msg: message.to_string() };
|
||||
return (*http_error).into();
|
||||
};
|
||||
return Self::MatrixApi {
|
||||
kind,
|
||||
code,
|
||||
msg: message.to_owned(),
|
||||
details: Some(format!("{api_error:?}")),
|
||||
};
|
||||
return Self::MatrixApi { kind, code, msg: message.to_owned() };
|
||||
}
|
||||
}
|
||||
Self::Generic { msg: http_error.to_string() }
|
||||
(*http_error).into()
|
||||
}
|
||||
_ => Self::Generic { msg: e.to_string() },
|
||||
_ => Self::from_err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StoreError> for ClientError {
|
||||
fn from(e: StoreError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CryptoStoreError> for ClientError {
|
||||
fn from(e: CryptoStoreError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HttpError> for ClientError {
|
||||
fn from(e: HttpError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IdParseError> for ClientError {
|
||||
fn from(e: IdParseError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for ClientError {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<url::ParseError> for ClientError {
|
||||
fn from(e: url::ParseError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<mime::FromStrError> for ClientError {
|
||||
fn from(e: mime::FromStrError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<encryption_sync_service::Error> for ClientError {
|
||||
fn from(e: encryption_sync_service::Error) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<timeline::Error> for ClientError {
|
||||
fn from(e: timeline::Error) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<timeline::UnsupportedEditItem> for ClientError {
|
||||
fn from(e: timeline::UnsupportedEditItem) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<notification_client::Error> for ClientError {
|
||||
fn from(e: notification_client::Error) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sync_service::Error> for ClientError {
|
||||
fn from(e: sync_service::Error) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OAuthError> for ClientError {
|
||||
fn from(e: OAuthError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomError> for ClientError {
|
||||
fn from(e: RoomError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomListError> for ClientError {
|
||||
fn from(e: RoomListError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EventCacheError> for ClientError {
|
||||
fn from(e: EventCacheError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EditError> for ClientError {
|
||||
fn from(e: EditError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomSendQueueError> for ClientError {
|
||||
fn from(e: RoomSendQueueError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NotYetImplemented> for ClientError {
|
||||
fn from(_: NotYetImplemented) -> Self {
|
||||
Self::new("This functionality is not implemented yet.")
|
||||
Self::from_str("This functionality is not implemented yet.", None)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FocusEventError> for ClientError {
|
||||
fn from(e: FocusEventError) -> Self {
|
||||
Self::new(e)
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RequestVerificationError> for ClientError {
|
||||
fn from(e: RequestVerificationError) -> Self {
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -736,7 +762,9 @@ impl TryFrom<RumaApiErrorKind> for ErrorKind {
|
||||
let retry_after_ms = match retry_after {
|
||||
Some(RetryAfter::Delay(duration)) => Some(duration.as_millis() as u64),
|
||||
Some(RetryAfter::DateTime(system_time)) => {
|
||||
let duration = system_time.duration_since(SystemTime::now()).ok();
|
||||
let duration = MilliSecondsSinceUnixEpoch::now()
|
||||
.to_system_time()
|
||||
.and_then(|now| system_time.duration_since(now).ok());
|
||||
duration.map(|duration| duration.as_millis() as u64)
|
||||
}
|
||||
None => None,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
// TODO: target-os conditional would be good.
|
||||
|
||||
#![allow(unused_qualifications, clippy::new_without_default)]
|
||||
#![allow(clippy::empty_line_after_doc_comments)] // Needed because uniffi macros contain empty
|
||||
// lines after docs.
|
||||
// Needed because uniffi macros contain empty lines after docs.
|
||||
#![allow(clippy::empty_line_after_doc_comments)]
|
||||
|
||||
mod authentication;
|
||||
mod chunk_iterator;
|
||||
mod client;
|
||||
mod client_builder;
|
||||
mod element;
|
||||
mod encryption;
|
||||
mod error;
|
||||
mod event;
|
||||
@@ -18,19 +15,21 @@ mod live_location_share;
|
||||
mod notification;
|
||||
mod notification_settings;
|
||||
mod platform;
|
||||
mod qr_code;
|
||||
mod room;
|
||||
mod room_alias;
|
||||
mod room_directory_search;
|
||||
mod room_info;
|
||||
mod room_list;
|
||||
mod room_member;
|
||||
mod room_preview;
|
||||
mod ruma;
|
||||
mod runtime;
|
||||
mod session_verification;
|
||||
mod sync_service;
|
||||
mod task_handle;
|
||||
mod timeline;
|
||||
mod tracing;
|
||||
mod utd;
|
||||
mod utils;
|
||||
mod widget;
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
use std::sync::Arc;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use matrix_sdk_ui::notification_client::{
|
||||
NotificationClient as MatrixNotificationClient, NotificationItem as MatrixNotificationItem,
|
||||
NotificationClient as SdkNotificationClient, NotificationEvent as SdkNotificationEvent,
|
||||
NotificationItem as SdkNotificationItem, NotificationStatus as SdkNotificationStatus,
|
||||
};
|
||||
use ruma::{EventId, RoomId};
|
||||
use ruma::{EventId, OwnedEventId, OwnedRoomId, RoomId};
|
||||
|
||||
use crate::{
|
||||
client::{Client, JoinRule},
|
||||
error::ClientError,
|
||||
event::TimelineEvent,
|
||||
room::Room,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
@@ -29,11 +31,11 @@ pub struct NotificationRoomInfo {
|
||||
pub display_name: String,
|
||||
pub avatar_url: Option<String>,
|
||||
pub canonical_alias: Option<String>,
|
||||
pub topic: 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)]
|
||||
@@ -52,12 +54,12 @@ pub struct NotificationItem {
|
||||
}
|
||||
|
||||
impl NotificationItem {
|
||||
fn from_inner(item: MatrixNotificationItem) -> Self {
|
||||
fn from_inner(item: SdkNotificationItem) -> Self {
|
||||
let event = match item.event {
|
||||
matrix_sdk_ui::notification_client::NotificationEvent::Timeline(event) => {
|
||||
SdkNotificationEvent::Timeline(event) => {
|
||||
NotificationEvent::Timeline { event: Arc::new(TimelineEvent(event)) }
|
||||
}
|
||||
matrix_sdk_ui::notification_client::NotificationEvent::Invite(event) => {
|
||||
SdkNotificationEvent::Invite(event) => {
|
||||
NotificationEvent::Invite { sender: event.sender.to_string() }
|
||||
}
|
||||
};
|
||||
@@ -72,11 +74,11 @@ 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(),
|
||||
topic: item.room_topic,
|
||||
join_rule: item.room_join_rule.map(TryInto::try_into).transpose().ok().flatten(),
|
||||
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,
|
||||
@@ -85,37 +87,147 @@ impl NotificationItem {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum NotificationStatus {
|
||||
/// The event has been found and was not filtered out.
|
||||
Event { item: NotificationItem },
|
||||
/// The event couldn't be found in the network queries used to find it.
|
||||
EventNotFound,
|
||||
/// The event has been filtered out, either because of the user's push
|
||||
/// rules, or because the user which triggered it is ignored by the
|
||||
/// current user.
|
||||
EventFilteredOut,
|
||||
}
|
||||
|
||||
impl From<SdkNotificationStatus> for NotificationStatus {
|
||||
fn from(item: SdkNotificationStatus) -> Self {
|
||||
match item {
|
||||
SdkNotificationStatus::Event(item) => {
|
||||
NotificationStatus::Event { item: NotificationItem::from_inner(*item) }
|
||||
}
|
||||
SdkNotificationStatus::EventNotFound => NotificationStatus::EventNotFound,
|
||||
SdkNotificationStatus::EventFilteredOut => NotificationStatus::EventFilteredOut,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum BatchNotificationResult {
|
||||
/// We have more detailed information about the notification.
|
||||
Ok { status: NotificationStatus },
|
||||
/// An error occurred while trying to fetch the notification.
|
||||
Error {
|
||||
/// The error message observed while handling a specific notification.
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct NotificationClient {
|
||||
pub(crate) inner: MatrixNotificationClient,
|
||||
pub(crate) inner: SdkNotificationClient,
|
||||
|
||||
/// A reference to the FFI client.
|
||||
///
|
||||
/// 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 {
|
||||
/// See also documentation of
|
||||
/// `MatrixNotificationClient::get_notification`.
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// Fetches the content of a notification.
|
||||
///
|
||||
/// This will first try to get the notification using a short-lived sliding
|
||||
/// sync, and if the sliding-sync can't find the event, then it'll use a
|
||||
/// `/context` query to find the event with associated member information.
|
||||
///
|
||||
/// An error result means that we couldn't resolve the notification; in that
|
||||
/// case, a dummy notification may be displayed instead.
|
||||
pub async fn get_notification(
|
||||
&self,
|
||||
room_id: String,
|
||||
event_id: String,
|
||||
) -> Result<Option<NotificationItem>, ClientError> {
|
||||
) -> Result<NotificationStatus, ClientError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
|
||||
let item =
|
||||
self.inner.get_notification(&room_id, &event_id).await.map_err(ClientError::from)?;
|
||||
|
||||
if let Some(item) = item {
|
||||
Ok(Some(NotificationItem::from_inner(item)))
|
||||
} else {
|
||||
Ok(None)
|
||||
Ok(item.into())
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// [`BatchNotificationResult`], that indicates if the notification was
|
||||
/// successfully fetched (in which case, it's a [`NotificationStatus`]), or
|
||||
/// an error message if it couldn't be fetched.
|
||||
pub async fn get_notifications(
|
||||
&self,
|
||||
requests: Vec<NotificationItemsRequest>,
|
||||
) -> Result<HashMap<String, BatchNotificationResult>, ClientError> {
|
||||
let requests =
|
||||
requests.into_iter().map(TryInto::try_into).collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let items = self.inner.get_notifications(&requests).await?;
|
||||
|
||||
let mut batch_result = HashMap::new();
|
||||
for (key, value) in items.into_iter() {
|
||||
let result = match value {
|
||||
Ok(status) => BatchNotificationResult::Ok { status: status.into() },
|
||||
Err(error) => BatchNotificationResult::Error { message: error.to_string() },
|
||||
};
|
||||
batch_result.insert(key.to_string(), result);
|
||||
}
|
||||
|
||||
Ok(batch_result)
|
||||
}
|
||||
}
|
||||
|
||||
/// A request for notification items grouped by their room.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct NotificationItemsRequest {
|
||||
room_id: String,
|
||||
event_ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl NotificationItemsRequest {
|
||||
/// The parsed [`OwnedRoomId`] to use with the SDK crates.
|
||||
pub fn room_id(&self) -> Result<OwnedRoomId, ClientError> {
|
||||
RoomId::parse(&self.room_id).map_err(ClientError::from)
|
||||
}
|
||||
|
||||
/// The parsed [`OwnedEventId`] list to use with the SDK crates.
|
||||
pub fn event_ids(&self) -> Result<Vec<OwnedEventId>, ClientError> {
|
||||
self.event_ids
|
||||
.iter()
|
||||
.map(|id| EventId::parse(id).map_err(ClientError::from))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<NotificationItemsRequest>
|
||||
for matrix_sdk_ui::notification_client::NotificationItemsRequest
|
||||
{
|
||||
type Error = ClientError;
|
||||
fn try_from(value: NotificationItemsRequest) -> Result<Self, Self::Error> {
|
||||
Ok(Self { room_id: value.room_id()?, event_ids: value.event_ids()? })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use matrix_sdk::{
|
||||
ruma::events::push_rules::PushRulesEvent,
|
||||
Client as MatrixClient,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::{
|
||||
push::{
|
||||
Action as SdkAction, ComparisonOperator as SdkComparisonOperator, PredefinedOverrideRuleId,
|
||||
@@ -161,7 +162,7 @@ pub enum PushCondition {
|
||||
}
|
||||
|
||||
impl TryFrom<SdkPushCondition> for PushCondition {
|
||||
type Error = ();
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: SdkPushCondition) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
@@ -179,7 +180,7 @@ impl TryFrom<SdkPushCondition> for PushCondition {
|
||||
SdkPushCondition::EventPropertyContains { key, value } => {
|
||||
Self::EventPropertyContains { key, value: value.into() }
|
||||
}
|
||||
_ => return Err(()),
|
||||
_ => return Err("Unsupported condition type".to_owned()),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -290,7 +291,7 @@ impl TryFrom<SdkTweak> for Tweak {
|
||||
SdkTweak::Highlight(highlight) => Self::Highlight { value: highlight },
|
||||
SdkTweak::Custom { name, value } => {
|
||||
let json_string = serde_json::to_string(&value)
|
||||
.map_err(|e| format!("Failed to serialize custom tweak value: {}", e))?;
|
||||
.map_err(|e| format!("Failed to serialize custom tweak value: {e}"))?;
|
||||
|
||||
Self::Custom { name, value: json_string }
|
||||
}
|
||||
@@ -308,9 +309,9 @@ impl TryFrom<Tweak> for SdkTweak {
|
||||
Tweak::Highlight { value } => Self::Highlight(value),
|
||||
Tweak::Custom { name, value } => {
|
||||
let json_value: serde_json::Value = serde_json::from_str(&value)
|
||||
.map_err(|e| format!("Failed to deserialize custom tweak value: {}", e))?;
|
||||
.map_err(|e| format!("Failed to deserialize custom tweak value: {e}"))?;
|
||||
let value = serde_json::from_value(json_value)
|
||||
.map_err(|e| format!("Failed to convert JSON value: {}", e))?;
|
||||
.map_err(|e| format!("Failed to convert JSON value: {e}"))?;
|
||||
|
||||
Self::Custom { name, value }
|
||||
}
|
||||
@@ -334,7 +335,7 @@ impl TryFrom<SdkAction> for Action {
|
||||
Ok(match value {
|
||||
SdkAction::Notify => Self::Notify,
|
||||
SdkAction::SetTweak(tweak) => Self::SetTweak {
|
||||
value: tweak.try_into().map_err(|e| format!("Failed to convert tweak: {}", e))?,
|
||||
value: tweak.try_into().map_err(|e| format!("Failed to convert tweak: {e}"))?,
|
||||
},
|
||||
_ => return Err("Unsupported action type".to_owned()),
|
||||
})
|
||||
@@ -348,7 +349,7 @@ impl TryFrom<Action> for SdkAction {
|
||||
Ok(match value {
|
||||
Action::Notify => Self::Notify,
|
||||
Action::SetTweak { value } => Self::SetTweak(
|
||||
value.try_into().map_err(|e| format!("Failed to convert tweak: {}", e))?,
|
||||
value.try_into().map_err(|e| format!("Failed to convert tweak: {e}"))?,
|
||||
),
|
||||
})
|
||||
}
|
||||
@@ -387,7 +388,7 @@ impl From<RoomNotificationMode> for SdkRoomNotificationMode {
|
||||
|
||||
/// Delegate to notify of changes in push rules
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait NotificationSettingsDelegate: Sync + Send {
|
||||
pub trait NotificationSettingsDelegate: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn settings_did_change(&self);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
use std::sync::OnceLock;
|
||||
#[cfg(feature = "sentry")]
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
|
||||
#[cfg(feature = "sentry")]
|
||||
use tracing::warn;
|
||||
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
||||
use tracing_core::Subscriber;
|
||||
use tracing_subscriber::{
|
||||
@@ -8,170 +14,158 @@ use tracing_subscriber::{
|
||||
time::FormatTime,
|
||||
FormatEvent, FormatFields, FormattedFields,
|
||||
},
|
||||
layer::SubscriberExt,
|
||||
layer::{Layered, SubscriberExt as _},
|
||||
registry::LookupSpan,
|
||||
util::SubscriberInitExt,
|
||||
EnvFilter, Layer,
|
||||
reload::{self, Handle},
|
||||
util::SubscriberInitExt as _,
|
||||
EnvFilter, Layer, Registry,
|
||||
};
|
||||
|
||||
use crate::tracing::LogLevel;
|
||||
use crate::{error::ClientError, tracing::LogLevel};
|
||||
|
||||
pub fn log_panics() {
|
||||
std::env::set_var("RUST_BACKTRACE", "1");
|
||||
|
||||
log_panics::init();
|
||||
// Adjusted version of tracing_subscriber::fmt::Format
|
||||
struct EventFormatter {
|
||||
display_timestamp: bool,
|
||||
display_level: bool,
|
||||
}
|
||||
|
||||
fn text_layers<S>(config: TracingConfiguration) -> impl Layer<S>
|
||||
impl EventFormatter {
|
||||
fn new() -> Self {
|
||||
Self { display_timestamp: true, display_level: true }
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn for_logcat() -> Self {
|
||||
// Level and time are already captured by logcat separately
|
||||
Self { display_timestamp: false, display_level: false }
|
||||
}
|
||||
|
||||
fn format_timestamp(&self, writer: &mut fmt::format::Writer<'_>) -> std::fmt::Result {
|
||||
if fmt::time::SystemTime.format_time(writer).is_err() {
|
||||
writer.write_str("<unknown time>")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_filename(
|
||||
&self,
|
||||
writer: &mut fmt::format::Writer<'_>,
|
||||
filename: &str,
|
||||
) -> std::fmt::Result {
|
||||
const CRATES_IO_PATH_MATCHER: &str = ".cargo/registry/src/index.crates.io";
|
||||
let crates_io_filename = filename
|
||||
.split_once(CRATES_IO_PATH_MATCHER)
|
||||
.and_then(|(_, rest)| rest.split_once('/').map(|(_, rest)| rest));
|
||||
|
||||
if let Some(filename) = crates_io_filename {
|
||||
writer.write_str("<crates.io>/")?;
|
||||
writer.write_str(filename)
|
||||
} else {
|
||||
writer.write_str(filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, N> FormatEvent<S, N> for EventFormatter
|
||||
where
|
||||
S: Subscriber + for<'a> LookupSpan<'a>,
|
||||
N: for<'a> FormatFields<'a> + 'static,
|
||||
{
|
||||
// Adjusted version of tracing_subscriber::fmt::Format
|
||||
struct EventFormatter {
|
||||
display_timestamp: bool,
|
||||
display_level: bool,
|
||||
}
|
||||
fn format_event(
|
||||
&self,
|
||||
ctx: &fmt::FmtContext<'_, S, N>,
|
||||
mut writer: fmt::format::Writer<'_>,
|
||||
event: &tracing_core::Event<'_>,
|
||||
) -> std::fmt::Result {
|
||||
let meta = event.metadata();
|
||||
|
||||
impl EventFormatter {
|
||||
fn new() -> Self {
|
||||
Self { display_timestamp: true, display_level: true }
|
||||
if self.display_timestamp {
|
||||
self.format_timestamp(&mut writer)?;
|
||||
writer.write_char(' ')?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn for_logcat() -> Self {
|
||||
// Level and time are already captured by logcat separately
|
||||
Self { display_timestamp: false, display_level: false }
|
||||
if self.display_level {
|
||||
// For info and warn, add a padding space to the left
|
||||
write!(writer, "{:>5} ", meta.level())?;
|
||||
}
|
||||
|
||||
fn format_timestamp(&self, writer: &mut fmt::format::Writer<'_>) -> std::fmt::Result {
|
||||
if fmt::time::SystemTime.format_time(writer).is_err() {
|
||||
writer.write_str("<unknown time>")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
write!(writer, "{}: ", meta.target())?;
|
||||
|
||||
fn write_filename(
|
||||
&self,
|
||||
writer: &mut fmt::format::Writer<'_>,
|
||||
filename: &str,
|
||||
) -> std::fmt::Result {
|
||||
const CRATES_IO_PATH_MATCHER: &str = ".cargo/registry/src/index.crates.io";
|
||||
let crates_io_filename = filename
|
||||
.split_once(CRATES_IO_PATH_MATCHER)
|
||||
.and_then(|(_, rest)| rest.split_once('/').map(|(_, rest)| rest));
|
||||
ctx.format_fields(writer.by_ref(), event)?;
|
||||
|
||||
if let Some(filename) = crates_io_filename {
|
||||
writer.write_str("<crates.io>/")?;
|
||||
writer.write_str(filename)
|
||||
} else {
|
||||
writer.write_str(filename)
|
||||
if let Some(filename) = meta.file() {
|
||||
writer.write_str(" | ")?;
|
||||
self.write_filename(&mut writer, filename)?;
|
||||
if let Some(line_number) = meta.line() {
|
||||
write!(writer, ":{line_number}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, N> FormatEvent<S, N> for EventFormatter
|
||||
where
|
||||
S: Subscriber + for<'a> LookupSpan<'a>,
|
||||
N: for<'a> FormatFields<'a> + 'static,
|
||||
{
|
||||
fn format_event(
|
||||
&self,
|
||||
ctx: &fmt::FmtContext<'_, S, N>,
|
||||
mut writer: fmt::format::Writer<'_>,
|
||||
event: &tracing_core::Event<'_>,
|
||||
) -> std::fmt::Result {
|
||||
let meta = event.metadata();
|
||||
if let Some(scope) = ctx.event_scope() {
|
||||
writer.write_str(" | spans: ")?;
|
||||
|
||||
if self.display_timestamp {
|
||||
self.format_timestamp(&mut writer)?;
|
||||
writer.write_char(' ')?;
|
||||
}
|
||||
let mut first = true;
|
||||
|
||||
if self.display_level {
|
||||
// For info and warn, add a padding space to the left
|
||||
write!(writer, "{:>5} ", meta.level())?;
|
||||
}
|
||||
|
||||
write!(writer, "{}: ", meta.target())?;
|
||||
|
||||
ctx.format_fields(writer.by_ref(), event)?;
|
||||
|
||||
if let Some(filename) = meta.file() {
|
||||
writer.write_str(" | ")?;
|
||||
self.write_filename(&mut writer, filename)?;
|
||||
if let Some(line_number) = meta.line() {
|
||||
write!(writer, ":{line_number}")?;
|
||||
for span in scope.from_root() {
|
||||
if !first {
|
||||
writer.write_str(" > ")?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(scope) = ctx.event_scope() {
|
||||
writer.write_str(" | spans: ")?;
|
||||
first = false;
|
||||
|
||||
let mut first = true;
|
||||
write!(writer, "{}", span.name())?;
|
||||
|
||||
for span in scope.from_root() {
|
||||
if !first {
|
||||
writer.write_str(" > ")?;
|
||||
}
|
||||
|
||||
first = false;
|
||||
|
||||
write!(writer, "{}", span.name())?;
|
||||
|
||||
if let Some(fields) = &span.extensions().get::<FormattedFields<N>>() {
|
||||
if !fields.is_empty() {
|
||||
write!(writer, "{{{fields}}}")?;
|
||||
}
|
||||
if let Some(fields) = &span.extensions().get::<FormattedFields<N>>() {
|
||||
if !fields.is_empty() {
|
||||
write!(writer, "{{{fields}}}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(writer)
|
||||
}
|
||||
|
||||
writeln!(writer)
|
||||
}
|
||||
}
|
||||
|
||||
let file_layer = config.write_to_files.map(|c| {
|
||||
let mut builder = RollingFileAppender::builder()
|
||||
.rotation(Rotation::HOURLY)
|
||||
.filename_prefix(&c.file_prefix);
|
||||
// Another fields formatter is necessary because of this bug
|
||||
// https://github.com/tokio-rs/tracing/issues/1372. Using a new
|
||||
// formatter for the fields forces to record them in different span
|
||||
// extensions, and thus remove the duplicated fields in the span.
|
||||
#[derive(Default)]
|
||||
struct FieldsFormatterForFiles(DefaultFields);
|
||||
|
||||
if let Some(max_files) = c.max_files {
|
||||
builder = builder.max_log_files(max_files as usize)
|
||||
};
|
||||
if let Some(file_suffix) = c.file_suffix {
|
||||
builder = builder.filename_suffix(file_suffix)
|
||||
}
|
||||
impl<'writer> FormatFields<'writer> for FieldsFormatterForFiles {
|
||||
fn format_fields<R: RecordFields>(
|
||||
&self,
|
||||
writer: Writer<'writer>,
|
||||
fields: R,
|
||||
) -> std::fmt::Result {
|
||||
self.0.format_fields(writer, fields)
|
||||
}
|
||||
}
|
||||
|
||||
let writer = builder.build(&c.path).expect("Failed to create a rolling file appender.");
|
||||
type ReloadHandle = Handle<
|
||||
tracing_subscriber::fmt::Layer<
|
||||
Layered<EnvFilter, Registry>,
|
||||
FieldsFormatterForFiles,
|
||||
EventFormatter,
|
||||
RollingFileAppender,
|
||||
>,
|
||||
Layered<EnvFilter, Registry>,
|
||||
>;
|
||||
|
||||
// Another fields formatter is necessary because of this bug
|
||||
// https://github.com/tokio-rs/tracing/issues/1372. Using a new
|
||||
// formatter for the fields forces to record them in different span
|
||||
// extensions, and thus remove the duplicated fields in the span.
|
||||
#[derive(Default)]
|
||||
struct FieldsFormatterForFiles(DefaultFields);
|
||||
fn text_layers(
|
||||
config: TracingConfiguration,
|
||||
) -> (impl Layer<Layered<EnvFilter, Registry>>, Option<ReloadHandle>) {
|
||||
let (file_layer, reload_handle) = config
|
||||
.write_to_files
|
||||
.map(|c| {
|
||||
let layer = make_file_layer(c);
|
||||
reload::Layer::new(layer)
|
||||
})
|
||||
.unzip();
|
||||
|
||||
impl<'writer> FormatFields<'writer> for FieldsFormatterForFiles {
|
||||
fn format_fields<R: RecordFields>(
|
||||
&self,
|
||||
writer: Writer<'writer>,
|
||||
fields: R,
|
||||
) -> std::fmt::Result {
|
||||
self.0.format_fields(writer, fields)
|
||||
}
|
||||
}
|
||||
|
||||
fmt::layer()
|
||||
.fmt_fields(FieldsFormatterForFiles::default())
|
||||
.event_format(EventFormatter::new())
|
||||
// EventFormatter doesn't support ANSI colors anyways, but the
|
||||
// default field formatter does, which is unhelpful for iOS +
|
||||
// Android logs, but enabled by default.
|
||||
.with_ansi(false)
|
||||
.with_writer(writer)
|
||||
});
|
||||
|
||||
Layer::and_then(
|
||||
let layers = Layer::and_then(
|
||||
file_layer,
|
||||
config.write_to_stdout_or_system.then(|| {
|
||||
// Another fields formatter is necessary because of this bug
|
||||
@@ -209,7 +203,41 @@ where
|
||||
"org.matrix.rust.sdk".to_owned(),
|
||||
));
|
||||
}),
|
||||
)
|
||||
);
|
||||
|
||||
(layers, reload_handle)
|
||||
}
|
||||
|
||||
fn make_file_layer(
|
||||
file_configuration: TracingFileConfiguration,
|
||||
) -> tracing_subscriber::fmt::Layer<
|
||||
Layered<EnvFilter, Registry, Registry>,
|
||||
FieldsFormatterForFiles,
|
||||
EventFormatter,
|
||||
RollingFileAppender,
|
||||
> {
|
||||
let mut builder = RollingFileAppender::builder()
|
||||
.rotation(Rotation::HOURLY)
|
||||
.filename_prefix(&file_configuration.file_prefix);
|
||||
|
||||
if let Some(max_files) = file_configuration.max_files {
|
||||
builder = builder.max_log_files(max_files as usize)
|
||||
}
|
||||
if let Some(file_suffix) = file_configuration.file_suffix {
|
||||
builder = builder.filename_suffix(file_suffix)
|
||||
}
|
||||
|
||||
let writer =
|
||||
builder.build(&file_configuration.path).expect("Failed to create a rolling file appender.");
|
||||
|
||||
fmt::layer()
|
||||
.fmt_fields(FieldsFormatterForFiles::default())
|
||||
.event_format(EventFormatter::new())
|
||||
// EventFormatter doesn't support ANSI colors anyways, but the
|
||||
// default field formatter does, which is unhelpful for iOS +
|
||||
// Android logs, but enabled by default.
|
||||
.with_ansi(false)
|
||||
.with_writer(writer)
|
||||
}
|
||||
|
||||
/// Configuration to save logs to (rotated) log-files.
|
||||
@@ -261,6 +289,7 @@ enum LogTarget {
|
||||
|
||||
// SDK UI modules.
|
||||
MatrixSdkUiTimeline,
|
||||
MatrixSdkUiNotificationClient,
|
||||
}
|
||||
|
||||
impl LogTarget {
|
||||
@@ -283,6 +312,7 @@ impl LogTarget {
|
||||
LogTarget::MatrixSdkSendQueue => "matrix_sdk::send_queue",
|
||||
LogTarget::MatrixSdkEventCacheStore => "matrix_sdk_sqlite::event_cache_store",
|
||||
LogTarget::MatrixSdkUiTimeline => "matrix_sdk_ui::timeline",
|
||||
LogTarget::MatrixSdkUiNotificationClient => "matrix_sdk_ui::notification_client",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -305,6 +335,7 @@ const DEFAULT_TARGET_LOG_LEVELS: &[(LogTarget, LogLevel)] = &[
|
||||
(LogTarget::MatrixSdkEventCacheStore, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkCommonStoreLocks, LogLevel::Warn),
|
||||
(LogTarget::MatrixSdkBaseStoreAmbiguityMap, LogLevel::Warn),
|
||||
(LogTarget::MatrixSdkUiNotificationClient, LogLevel::Info),
|
||||
];
|
||||
|
||||
const IMMUTABLE_LOG_TARGETS: &[LogTarget] = &[
|
||||
@@ -325,6 +356,8 @@ pub enum TraceLogPacks {
|
||||
SendQueue,
|
||||
/// Enables all the logs relevant to the timeline.
|
||||
Timeline,
|
||||
/// Enables all the logs relevant to the notification client.
|
||||
NotificationClient,
|
||||
}
|
||||
|
||||
impl TraceLogPacks {
|
||||
@@ -339,10 +372,28 @@ impl TraceLogPacks {
|
||||
],
|
||||
TraceLogPacks::SendQueue => &[LogTarget::MatrixSdkSendQueue],
|
||||
TraceLogPacks::Timeline => &[LogTarget::MatrixSdkUiTimeline],
|
||||
TraceLogPacks::NotificationClient => &[LogTarget::MatrixSdkUiNotificationClient],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sentry")]
|
||||
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 {
|
||||
reload_handle: Option<ReloadHandle>,
|
||||
#[cfg(feature = "sentry")]
|
||||
sentry: Option<SentryLoggingCtx>,
|
||||
}
|
||||
|
||||
static LOGGING: OnceLock<LoggingCtx> = OnceLock::new();
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct TracingConfiguration {
|
||||
/// The desired log level.
|
||||
@@ -363,6 +414,108 @@ pub struct TracingConfiguration {
|
||||
|
||||
/// If set, configures rotated log files where to write additional logs.
|
||||
write_to_files: Option<TracingFileConfiguration>,
|
||||
|
||||
/// If set, the Sentry DSN to use for error reporting.
|
||||
#[cfg(feature = "sentry")]
|
||||
sentry_dsn: Option<String>,
|
||||
}
|
||||
|
||||
impl TracingConfiguration {
|
||||
/// Sets up the tracing configuration and return a [`Logger`] instance
|
||||
/// holding onto it.
|
||||
#[cfg_attr(not(feature = "sentry"), allow(unused_mut))]
|
||||
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();
|
||||
|
||||
let env_filter = build_tracing_filter(&self);
|
||||
|
||||
let logging_ctx;
|
||||
#[cfg(feature = "sentry")]
|
||||
{
|
||||
// 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,
|
||||
release: Some(env!("VERGEN_GIT_SHA").into()),
|
||||
..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 (text_layers, reload_handle) = crate::platform::text_layers(self);
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::EnvFilter::new(&env_filter))
|
||||
.with(text_layers)
|
||||
.with(sentry_layer)
|
||||
.init();
|
||||
logging_ctx = LoggingCtx { reload_handle, sentry: sentry_logging_ctx };
|
||||
}
|
||||
#[cfg(not(feature = "sentry"))]
|
||||
{
|
||||
let (text_layers, reload_handle) = crate::platform::text_layers(self);
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::EnvFilter::new(&env_filter))
|
||||
.with(text_layers)
|
||||
.init();
|
||||
logging_ctx = LoggingCtx { reload_handle };
|
||||
}
|
||||
|
||||
// Log the log levels 🧠.
|
||||
tracing::info!(env_filter, "Logging has been set up");
|
||||
|
||||
logging_ctx
|
||||
}
|
||||
}
|
||||
|
||||
fn build_tracing_filter(config: &TracingConfiguration) -> String {
|
||||
@@ -407,26 +560,79 @@ fn build_tracing_filter(config: &TracingConfiguration) -> String {
|
||||
/// the NSE process on iOS). Otherwise, this can remain false, in which case a
|
||||
/// multithreaded tokio runtime will be set up.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn init_platform(config: TracingConfiguration, use_lightweight_tokio_runtime: bool) {
|
||||
log_panics();
|
||||
|
||||
let env_filter = build_tracing_filter(&config);
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(EnvFilter::new(&env_filter))
|
||||
.with(text_layers(config))
|
||||
.init();
|
||||
|
||||
// Log the log levels 🧠.
|
||||
tracing::info!(env_filter, "Logging has been set up");
|
||||
|
||||
if use_lightweight_tokio_runtime {
|
||||
setup_lightweight_tokio_runtime();
|
||||
} else {
|
||||
setup_multithreaded_tokio_runtime();
|
||||
pub fn init_platform(
|
||||
config: TracingConfiguration,
|
||||
use_lightweight_tokio_runtime: bool,
|
||||
) -> Result<(), ClientError> {
|
||||
#[cfg(all(feature = "js", target_family = "wasm"))]
|
||||
{
|
||||
console_error_panic_hook::set_once();
|
||||
}
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
LOGGING.set(config.build()).map_err(|_| ClientError::Generic {
|
||||
msg: "logger already initialized".to_owned(),
|
||||
details: None,
|
||||
})?;
|
||||
|
||||
if use_lightweight_tokio_runtime {
|
||||
setup_lightweight_tokio_runtime();
|
||||
} else {
|
||||
setup_multithreaded_tokio_runtime();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the global enablement level for the Sentry layer (after the logs have
|
||||
/// been set up).
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[cfg(feature = "sentry")]
|
||||
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");
|
||||
};
|
||||
}
|
||||
|
||||
/// Updates the tracing subscriber with a new file writer based on the provided
|
||||
/// configuration.
|
||||
///
|
||||
/// This method will throw if `init_platform` hasn't been called, or if it was
|
||||
/// called with `write_to_files` set to `None`.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn reload_tracing_file_writer(
|
||||
configuration: TracingFileConfiguration,
|
||||
) -> Result<(), ClientError> {
|
||||
let Some(logging_context) = LOGGING.get() else {
|
||||
return Err(ClientError::Generic {
|
||||
msg: "Logging hasn't been initialized yet".to_owned(),
|
||||
details: None,
|
||||
});
|
||||
};
|
||||
|
||||
let Some(reload_handle) = logging_context.reload_handle.as_ref() else {
|
||||
return Err(ClientError::Generic {
|
||||
msg: "Logging wasn't initialized with a file config".to_owned(),
|
||||
details: None,
|
||||
});
|
||||
};
|
||||
|
||||
let layer = make_file_layer(configuration);
|
||||
reload_handle.reload(layer).map_err(|error| ClientError::Generic {
|
||||
msg: format!("Failed to reload file config: {error}"),
|
||||
details: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn setup_multithreaded_tokio_runtime() {
|
||||
async_compat::set_runtime_builder(Box::new(|| {
|
||||
eprintln!("spawning a multithreaded tokio runtime");
|
||||
@@ -437,6 +643,7 @@ fn setup_multithreaded_tokio_runtime() {
|
||||
}));
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn setup_lightweight_tokio_runtime() {
|
||||
async_compat::set_runtime_builder(Box::new(|| {
|
||||
eprintln!("spawning a lightweight tokio runtime");
|
||||
@@ -479,31 +686,38 @@ mod tests {
|
||||
extra_targets: vec!["super_duper_app".to_owned()],
|
||||
write_to_stdout_or_system: true,
|
||||
write_to_files: None,
|
||||
#[cfg(feature = "sentry")]
|
||||
sentry_dsn: None,
|
||||
};
|
||||
|
||||
let filter = build_tracing_filter(&config);
|
||||
|
||||
assert_eq!(
|
||||
filter,
|
||||
"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=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"
|
||||
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=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,
|
||||
matrix_sdk_ui::notification_client=info,
|
||||
super_duper_app=error"#
|
||||
.split('\n')
|
||||
.map(|s| s.trim())
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -515,32 +729,39 @@ mod tests {
|
||||
extra_targets: vec!["super_duper_app".to_owned(), "some_other_span".to_owned()],
|
||||
write_to_stdout_or_system: true,
|
||||
write_to_files: None,
|
||||
#[cfg(feature = "sentry")]
|
||||
sentry_dsn: None,
|
||||
};
|
||||
|
||||
let filter = build_tracing_filter(&config);
|
||||
|
||||
assert_eq!(
|
||||
filter,
|
||||
"panic=error,\
|
||||
hyper=warn,\
|
||||
matrix_sdk_ffi=info,\
|
||||
matrix_sdk=info,\
|
||||
matrix_sdk::client=trace,\
|
||||
matrix_sdk_crypto=trace,\
|
||||
matrix_sdk_crypto::olm::account=trace,\
|
||||
matrix_sdk::oidc=trace,\
|
||||
matrix_sdk::http_client=trace,\
|
||||
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"
|
||||
r#"panic=error,
|
||||
hyper=warn,
|
||||
matrix_sdk_ffi=info,
|
||||
matrix_sdk=info,
|
||||
matrix_sdk::client=trace,
|
||||
matrix_sdk_crypto=trace,
|
||||
matrix_sdk_crypto::olm::account=trace,
|
||||
matrix_sdk::oidc=trace,
|
||||
matrix_sdk::http_client=trace,
|
||||
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,
|
||||
matrix_sdk_ui::notification_client=trace,
|
||||
super_duper_app=trace,
|
||||
some_other_span=trace"#
|
||||
.split('\n')
|
||||
.map(|s| s.trim())
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -552,6 +773,8 @@ mod tests {
|
||||
extra_targets: vec!["super_duper_app".to_owned()],
|
||||
write_to_stdout_or_system: true,
|
||||
write_to_files: None,
|
||||
#[cfg(feature = "sentry")]
|
||||
sentry_dsn: None,
|
||||
};
|
||||
|
||||
let filter = build_tracing_filter(&config);
|
||||
@@ -576,6 +799,7 @@ mod tests {
|
||||
matrix_sdk_sqlite::event_cache_store=trace,
|
||||
matrix_sdk_common::store_locks=warn,
|
||||
matrix_sdk_base::store::ambiguity_map=warn,
|
||||
matrix_sdk_ui::notification_client=info,
|
||||
super_duper_app=info"#
|
||||
.split('\n')
|
||||
.map(|s| s.trim())
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk::{
|
||||
authentication::oauth::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason},
|
||||
crypto::types::qr_login::{LoginQrCodeDecodeError, QrCodeModeData},
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use tracing::error;
|
||||
|
||||
/// Data for the QR code login mechanism.
|
||||
///
|
||||
/// The [`QrCodeData`] can be serialized and encoded as a QR code or it can be
|
||||
/// decoded from a QR code.
|
||||
#[derive(Debug, uniffi::Object)]
|
||||
pub struct QrCodeData {
|
||||
pub(crate) inner: qrcode::QrCodeData,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl QrCodeData {
|
||||
/// Attempt to decode a slice of bytes into a [`QrCodeData`] object.
|
||||
///
|
||||
/// The slice of bytes would generally be returned by a QR code decoder.
|
||||
#[uniffi::constructor]
|
||||
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`].
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum QrCodeDecodeError {
|
||||
#[error("Error decoding QR code: {error:?}")]
|
||||
Crypto {
|
||||
#[from]
|
||||
error: LoginQrCodeDecodeError,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
pub enum HumanQrLoginError {
|
||||
#[error("Linking with this device is not supported.")]
|
||||
LinkingNotSupported,
|
||||
#[error("The sign in was cancelled.")]
|
||||
Cancelled,
|
||||
#[error("The sign in was not completed in the required time.")]
|
||||
Expired,
|
||||
#[error("A secure connection could not have been established between the two devices.")]
|
||||
ConnectionInsecure,
|
||||
#[error("The sign in was declined.")]
|
||||
Declined,
|
||||
#[error("An unknown error has happened.")]
|
||||
Unknown,
|
||||
#[error("The homeserver doesn't provide sliding sync in its configuration.")]
|
||||
SlidingSyncNotAvailable,
|
||||
#[error("Unable to use OIDC as the supplied client metadata is invalid.")]
|
||||
OidcMetadataInvalid,
|
||||
#[error("The other device is not signed in and as such can't sign in other devices.")]
|
||||
OtherDeviceNotSignedIn,
|
||||
}
|
||||
|
||||
impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
|
||||
fn from(value: qrcode::QRCodeLoginError) -> Self {
|
||||
use qrcode::{QRCodeLoginError, SecureChannelError};
|
||||
|
||||
match value {
|
||||
QRCodeLoginError::LoginFailure { reason, .. } => match reason {
|
||||
LoginFailureReason::UnsupportedProtocol => HumanQrLoginError::LinkingNotSupported,
|
||||
LoginFailureReason::AuthorizationExpired => HumanQrLoginError::Expired,
|
||||
LoginFailureReason::UserCancelled => HumanQrLoginError::Cancelled,
|
||||
_ => HumanQrLoginError::Unknown,
|
||||
},
|
||||
|
||||
QRCodeLoginError::OAuth(e) => {
|
||||
if let Some(e) = e.as_request_token_error() {
|
||||
match e {
|
||||
DeviceCodeErrorResponseType::AccessDenied => HumanQrLoginError::Declined,
|
||||
DeviceCodeErrorResponseType::ExpiredToken => HumanQrLoginError::Expired,
|
||||
_ => HumanQrLoginError::Unknown,
|
||||
}
|
||||
} else {
|
||||
HumanQrLoginError::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
QRCodeLoginError::SecureChannel(e) => match e {
|
||||
SecureChannelError::Utf8(_)
|
||||
| SecureChannelError::MessageDecode(_)
|
||||
| SecureChannelError::Json(_)
|
||||
| SecureChannelError::RendezvousChannel(_) => HumanQrLoginError::Unknown,
|
||||
SecureChannelError::SecureChannelMessage { .. }
|
||||
| SecureChannelError::Ecies(_)
|
||||
| SecureChannelError::InvalidCheckCode => HumanQrLoginError::ConnectionInsecure,
|
||||
SecureChannelError::InvalidIntent => HumanQrLoginError::OtherDeviceNotSignedIn,
|
||||
},
|
||||
|
||||
QRCodeLoginError::UnexpectedMessage { .. }
|
||||
| QRCodeLoginError::CrossProcessRefreshLock(_)
|
||||
| QRCodeLoginError::DeviceKeyUpload(_)
|
||||
| QRCodeLoginError::SessionTokens(_)
|
||||
| QRCodeLoginError::UserIdDiscovery(_)
|
||||
| QRCodeLoginError::SecretImport(_) => HumanQrLoginError::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum describing the progress of the QR-code login.
|
||||
#[derive(Debug, Default, Clone, uniffi::Enum)]
|
||||
pub enum QrLoginProgress {
|
||||
/// The login process is starting.
|
||||
#[default]
|
||||
Starting,
|
||||
/// We established a secure channel with the other device.
|
||||
EstablishingSecureChannel {
|
||||
/// The check code that the device should display so the other device
|
||||
/// can confirm that the channel is secure as well.
|
||||
check_code: u8,
|
||||
/// The string representation of the check code, will be guaranteed to
|
||||
/// be 2 characters long, preserving the leading zero if the
|
||||
/// first digit is a zero.
|
||||
check_code_string: String,
|
||||
},
|
||||
/// 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: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, state: QrLoginProgress);
|
||||
}
|
||||
|
||||
impl From<qrcode::LoginProgress> for QrLoginProgress {
|
||||
fn from(value: qrcode::LoginProgress) -> Self {
|
||||
use qrcode::LoginProgress;
|
||||
|
||||
match value {
|
||||
LoginProgress::Starting => Self::Starting,
|
||||
LoginProgress::EstablishingSecureChannel { check_code } => {
|
||||
let check_code = check_code.to_digit();
|
||||
|
||||
Self::EstablishingSecureChannel {
|
||||
check_code,
|
||||
check_code_string: format!("{check_code:02}"),
|
||||
}
|
||||
}
|
||||
LoginProgress::WaitingForToken { user_code } => Self::WaitingForToken { user_code },
|
||||
LoginProgress::Done => Self::Done,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::{collections::HashMap, pin::pin, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use async_compat::get_runtime_handle;
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use matrix_sdk::{
|
||||
crypto::LocalTrust,
|
||||
@@ -10,9 +9,14 @@ use matrix_sdk::{
|
||||
TryFromReportedContentScoreError,
|
||||
},
|
||||
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType, EncryptionState,
|
||||
RoomHero as SdkRoomHero, RoomMemberships, RoomState,
|
||||
PredecessorRoom as SdkPredecessorRoom, RoomHero as SdkRoomHero, RoomMemberships, RoomState,
|
||||
SuccessorRoom as SdkSuccessorRoom,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::{
|
||||
timeline::{default_event_filter, RoomExt, TimelineBuilder},
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{default_event_filter, RoomExt};
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
assign,
|
||||
@@ -22,33 +26,37 @@ use ruma::{
|
||||
avatar::ImageInfo as RumaAvatarImageInfo,
|
||||
history_visibility::HistoryVisibility as RumaHistoryVisibility,
|
||||
join_rules::JoinRule as RumaJoinRule, message::RoomMessageEventContentWithoutRelation,
|
||||
power_levels::RoomPowerLevels as RumaPowerLevels, MediaSource,
|
||||
MediaSource,
|
||||
},
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent, TimelineEventType,
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent,
|
||||
},
|
||||
EventId, Int, OwnedDeviceId, OwnedUserId, RoomAliasId, UserId,
|
||||
EventId, Int, OwnedDeviceId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomAliasId,
|
||||
ServerName, UserId,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{error, warn};
|
||||
|
||||
use self::{power_levels::RoomPowerLevels, room_info::RoomInfo};
|
||||
use crate::{
|
||||
chunk_iterator::ChunkIterator,
|
||||
client::{JoinRule, RoomVisibility},
|
||||
error::{ClientError, MediaInfoError, NotYetImplemented, RoomError},
|
||||
event::{MessageLikeEventType, StateEventType},
|
||||
identity_status_change::IdentityStatusChange,
|
||||
live_location_share::{LastLocation, LiveLocationShare},
|
||||
room_info::RoomInfo,
|
||||
room_member::{RoomMember, RoomMemberWithSenderInfo},
|
||||
room_preview::RoomPreview,
|
||||
ruma::{ImageInfo, LocationContent, Mentions, NotifyType},
|
||||
runtime::get_runtime_handle,
|
||||
timeline::{
|
||||
configuration::{TimelineConfiguration, TimelineFilter},
|
||||
ReceiptType, SendHandle, Timeline,
|
||||
EventTimelineItem, ReceiptType, SendHandle, Timeline,
|
||||
},
|
||||
utils::u64_to_uint,
|
||||
utils::{u64_to_uint, AsyncRuntimeDropped},
|
||||
TaskHandle,
|
||||
};
|
||||
|
||||
mod power_levels;
|
||||
pub mod room_info;
|
||||
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum Membership {
|
||||
Invited,
|
||||
@@ -70,21 +78,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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +116,10 @@ impl Room {
|
||||
self.inner.is_direct().await.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn is_public(&self) -> bool {
|
||||
/// Whether the room can be publicly joined or not, based on its join rule.
|
||||
///
|
||||
/// Can return `None` if the join rule state event is missing.
|
||||
pub fn is_public(&self) -> Option<bool> {
|
||||
self.inner.is_public()
|
||||
}
|
||||
|
||||
@@ -122,8 +127,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 +162,17 @@ impl Room {
|
||||
self.inner.alt_aliases().iter().map(|a| a.to_string()).collect()
|
||||
}
|
||||
|
||||
/// Get the user who created the invite, if any.
|
||||
pub async fn inviter(&self) -> Result<Option<RoomMember>, ClientError> {
|
||||
let invite_details = self.inner.invite_details().await?;
|
||||
|
||||
match invite_details.inviter {
|
||||
Some(inviter) => Ok(Some(inviter.try_into()?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// The room's current membership state.
|
||||
pub fn membership(&self) -> Membership {
|
||||
self.inner.state().into()
|
||||
}
|
||||
@@ -173,15 +212,10 @@ impl Room {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a timeline with a default configuration, i.e. a live timeline
|
||||
/// with read receipts and read marker tracking.
|
||||
pub async fn timeline(&self) -> Result<Arc<Timeline>, ClientError> {
|
||||
let mut write_guard = self.timeline.write().await;
|
||||
if let Some(timeline) = &*write_guard {
|
||||
Ok(timeline.clone())
|
||||
} else {
|
||||
let timeline = Timeline::new(self.inner.timeline().await?);
|
||||
*write_guard = Some(timeline.clone());
|
||||
Ok(timeline)
|
||||
}
|
||||
Ok(Timeline::new(self.inner.timeline().await?))
|
||||
}
|
||||
|
||||
/// Build a new timeline instance with the given configuration.
|
||||
@@ -189,7 +223,7 @@ impl Room {
|
||||
&self,
|
||||
configuration: TimelineConfiguration,
|
||||
) -> Result<Arc<Timeline>, ClientError> {
|
||||
let mut builder = matrix_sdk_ui::timeline::Timeline::builder(&self.inner);
|
||||
let mut builder = matrix_sdk_ui::timeline::TimelineBuilder::new(&self.inner);
|
||||
|
||||
builder = builder
|
||||
.with_focus(configuration.focus.try_into()?)
|
||||
@@ -233,6 +267,14 @@ impl Room {
|
||||
builder = builder.with_internal_id_prefix(internal_id_prefix);
|
||||
}
|
||||
|
||||
if configuration.report_utds {
|
||||
if let Some(utd_hook_manager) = self.utd_hook_manager.clone() {
|
||||
builder = builder.with_unable_to_decrypt_hook(utd_hook_manager);
|
||||
} else {
|
||||
return Err(ClientError::Generic { msg: "Failed creating timeline because the configuration is set to report UTDs but no hook manager is set".to_owned(), details: None });
|
||||
}
|
||||
}
|
||||
|
||||
let timeline = builder.build().await?;
|
||||
|
||||
Ok(Timeline::new(timeline))
|
||||
@@ -246,6 +288,22 @@ impl Room {
|
||||
self.inner.encryption_state()
|
||||
}
|
||||
|
||||
/// Checks whether the room is encrypted or not.
|
||||
///
|
||||
/// **Note**: this info may not be reliable if you don't set up
|
||||
/// `m.room.encryption` as required state.
|
||||
async fn is_encrypted(&self) -> bool {
|
||||
self.inner
|
||||
.latest_encryption_state()
|
||||
.await
|
||||
.map(|state| state.is_encrypted())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn latest_event(&self) -> Option<EventTimelineItem> {
|
||||
self.inner.latest_event_item().await.map(Into::into)
|
||||
}
|
||||
|
||||
pub async fn latest_encryption_state(&self) -> Result<EncryptionState, ClientError> {
|
||||
Ok(self.inner.latest_encryption_state().await?)
|
||||
}
|
||||
@@ -346,8 +404,11 @@ impl Room {
|
||||
///
|
||||
/// * `content` - The content of the event to send encoded as JSON string.
|
||||
pub async fn send_raw(&self, event_type: String, content: String) -> Result<(), ClientError> {
|
||||
let content_json: serde_json::Value = serde_json::from_str(&content)
|
||||
.map_err(|e| ClientError::Generic { msg: format!("Failed to parse JSON: {e}") })?;
|
||||
let content_json: serde_json::Value =
|
||||
serde_json::from_str(&content).map_err(|e| ClientError::Generic {
|
||||
msg: format!("Failed to parse JSON: {e}"),
|
||||
details: Some(format!("{e:?}")),
|
||||
})?;
|
||||
|
||||
self.inner.send_raw(&event_type, content_json).await?;
|
||||
|
||||
@@ -404,9 +465,7 @@ impl Room {
|
||||
.report_content(
|
||||
EventId::parse(event_id)?,
|
||||
score.map(TryFrom::try_from).transpose().map_err(
|
||||
|error: TryFromReportedContentScoreError| ClientError::Generic {
|
||||
msg: error.to_string(),
|
||||
},
|
||||
|error: TryFromReportedContentScoreError| ClientError::from_err(error),
|
||||
)?,
|
||||
reason,
|
||||
)
|
||||
@@ -516,21 +575,6 @@ impl Room {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn can_user_redact_own(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_redact_own(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_redact_other(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_redact_other(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_ban(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_ban(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn ban_user(
|
||||
&self,
|
||||
user_id: String,
|
||||
@@ -549,16 +593,6 @@ impl Room {
|
||||
Ok(self.inner.unban_user(&user_id, reason.as_deref()).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_invite(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_invite(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_kick(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_kick(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn kick_user(
|
||||
&self,
|
||||
user_id: String,
|
||||
@@ -568,37 +602,6 @@ impl Room {
|
||||
Ok(self.inner.kick_user(&user_id, reason.as_deref()).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_send_state(
|
||||
&self,
|
||||
user_id: String,
|
||||
state_event: StateEventType,
|
||||
) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_send_state(&user_id, state_event.into()).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_send_message(
|
||||
&self,
|
||||
user_id: String,
|
||||
message: MessageLikeEventType,
|
||||
) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_send_message(&user_id, message.into()).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_pin_unpin(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_pin_unpin(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_trigger_room_notification(
|
||||
&self,
|
||||
user_id: String,
|
||||
) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_trigger_room_notification(&user_id).await?)
|
||||
}
|
||||
|
||||
pub fn own_user_id(&self) -> String {
|
||||
self.inner.own_user_id().to_string()
|
||||
}
|
||||
@@ -655,17 +658,17 @@ 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(())
|
||||
}
|
||||
|
||||
pub async fn get_power_levels(&self) -> Result<RoomPowerLevels, ClientError> {
|
||||
pub async fn get_power_levels(&self) -> Result<Arc<RoomPowerLevels>, ClientError> {
|
||||
let power_levels = self.inner.power_levels().await.map_err(matrix_sdk::Error::from)?;
|
||||
Ok(RoomPowerLevels::from(power_levels))
|
||||
Ok(Arc::new(RoomPowerLevels::new(power_levels, self.inner.own_user_id().to_owned())))
|
||||
}
|
||||
|
||||
pub async fn apply_power_level_changes(
|
||||
@@ -689,10 +692,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(())
|
||||
}
|
||||
|
||||
@@ -704,8 +704,11 @@ impl Room {
|
||||
Ok(self.inner.get_suggested_user_role(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn reset_power_levels(&self) -> Result<RoomPowerLevels, ClientError> {
|
||||
Ok(RoomPowerLevels::from(self.inner.reset_power_levels().await?))
|
||||
pub async fn reset_power_levels(&self) -> Result<Arc<RoomPowerLevels>, ClientError> {
|
||||
Ok(Arc::new(RoomPowerLevels::new(
|
||||
self.inner.reset_power_levels().await?,
|
||||
self.inner.own_user_id().to_owned(),
|
||||
)))
|
||||
}
|
||||
|
||||
pub async fn matrix_to_permalink(&self) -> Result<String, ClientError> {
|
||||
@@ -726,9 +729,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.
|
||||
@@ -773,18 +780,31 @@ impl Room {
|
||||
|
||||
/// Store the given `ComposerDraft` in the state store using the current
|
||||
/// room id, as identifier.
|
||||
pub async fn save_composer_draft(&self, draft: ComposerDraft) -> Result<(), ClientError> {
|
||||
Ok(self.inner.save_composer_draft(draft.try_into()?).await?)
|
||||
pub async fn save_composer_draft(
|
||||
&self,
|
||||
draft: ComposerDraft,
|
||||
thread_root: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let thread_root = thread_root.map(EventId::parse).transpose()?;
|
||||
Ok(self.inner.save_composer_draft(draft.try_into()?, thread_root.as_deref()).await?)
|
||||
}
|
||||
|
||||
/// Retrieve the `ComposerDraft` stored in the state store for this room.
|
||||
pub async fn load_composer_draft(&self) -> Result<Option<ComposerDraft>, ClientError> {
|
||||
Ok(self.inner.load_composer_draft().await?.map(Into::into))
|
||||
pub async fn load_composer_draft(
|
||||
&self,
|
||||
thread_root: Option<String>,
|
||||
) -> Result<Option<ComposerDraft>, ClientError> {
|
||||
let thread_root = thread_root.map(EventId::parse).transpose()?;
|
||||
Ok(self.inner.load_composer_draft(thread_root.as_deref()).await?.map(Into::into))
|
||||
}
|
||||
|
||||
/// Remove the `ComposerDraft` stored in the state store for this room.
|
||||
pub async fn clear_composer_draft(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.clear_composer_draft().await?)
|
||||
pub async fn clear_composer_draft(
|
||||
&self,
|
||||
thread_root: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let thread_root = thread_root.map(EventId::parse).transpose()?;
|
||||
Ok(self.inner.clear_composer_draft(thread_root.as_deref()).await?)
|
||||
}
|
||||
|
||||
/// Edit an event given its event id.
|
||||
@@ -1084,11 +1104,46 @@ impl Room {
|
||||
self.inner.forget().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Builds a `RoomPreview` from a room list item. This is intended for
|
||||
/// invited, knocked or banned rooms.
|
||||
async fn preview_room(&self, via: Vec<String>) -> Result<Arc<RoomPreview>, ClientError> {
|
||||
// Validate parameters first.
|
||||
let server_names: Vec<OwnedServerName> = via
|
||||
.into_iter()
|
||||
.map(|server| ServerName::parse(server).map_err(ClientError::from))
|
||||
.collect::<Result<_, ClientError>>()?;
|
||||
|
||||
// Do the thing.
|
||||
let client = self.inner.client();
|
||||
let (room_or_alias_id, mut server_names) = if let Some(alias) = self.inner.canonical_alias()
|
||||
{
|
||||
let room_or_alias_id: OwnedRoomOrAliasId = alias.into();
|
||||
(room_or_alias_id, Vec::new())
|
||||
} else {
|
||||
let room_or_alias_id: OwnedRoomOrAliasId = self.inner.room_id().to_owned().into();
|
||||
(room_or_alias_id, server_names)
|
||||
};
|
||||
|
||||
// If no server names are provided and the room's membership is invited,
|
||||
// add the server name from the sender's user id as a fallback value
|
||||
if server_names.is_empty() {
|
||||
if let Ok(invite_details) = self.inner.invite_details().await {
|
||||
if let Some(inviter) = invite_details.inviter {
|
||||
server_names.push(inviter.user_id().server_name().to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let room_preview = client.get_room_preview(&room_or_alias_id, server_names).await?;
|
||||
|
||||
Ok(Arc::new(RoomPreview::new(AsyncRuntimeDropped::new(client), room_preview)))
|
||||
}
|
||||
}
|
||||
|
||||
/// A listener for receiving new live location shares in a room.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait LiveLocationShareListener: Sync + Send {
|
||||
pub trait LiveLocationShareListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn call(&self, live_location_shares: Vec<LiveLocationShare>);
|
||||
}
|
||||
|
||||
@@ -1110,7 +1165,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>);
|
||||
}
|
||||
|
||||
@@ -1181,66 +1236,18 @@ pub fn matrix_to_room_alias_permalink(
|
||||
Ok(room_alias.matrix_to_uri().to_string())
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomPowerLevels {
|
||||
/// The level required to ban a user.
|
||||
pub ban: i64,
|
||||
/// The level required to invite a user.
|
||||
pub invite: i64,
|
||||
/// The level required to kick a user.
|
||||
pub kick: i64,
|
||||
/// The level required to redact an event.
|
||||
pub redact: i64,
|
||||
/// The default level required to send message events.
|
||||
pub events_default: i64,
|
||||
/// The default level required to send state events.
|
||||
pub state_default: i64,
|
||||
/// The default power level for every user in the room.
|
||||
pub users_default: i64,
|
||||
/// The level required to change the room's name.
|
||||
pub room_name: i64,
|
||||
/// The level required to change the room's avatar.
|
||||
pub room_avatar: i64,
|
||||
/// The level required to change the room's topic.
|
||||
pub room_topic: i64,
|
||||
}
|
||||
|
||||
impl From<RumaPowerLevels> for RoomPowerLevels {
|
||||
fn from(value: RumaPowerLevels) -> Self {
|
||||
fn state_event_level_for(
|
||||
power_levels: &RumaPowerLevels,
|
||||
event_type: &TimelineEventType,
|
||||
) -> i64 {
|
||||
let default_state: i64 = power_levels.state_default.into();
|
||||
power_levels.events.get(event_type).map_or(default_state, |&level| level.into())
|
||||
}
|
||||
Self {
|
||||
ban: value.ban.into(),
|
||||
invite: value.invite.into(),
|
||||
kick: value.kick.into(),
|
||||
redact: value.redact.into(),
|
||||
events_default: value.events_default.into(),
|
||||
state_default: value.state_default.into(),
|
||||
users_default: value.users_default.into(),
|
||||
room_name: state_event_level_for(&value, &TimelineEventType::RoomName),
|
||||
room_avatar: state_event_level_for(&value, &TimelineEventType::RoomAvatar),
|
||||
room_topic: state_event_level_for(&value, &TimelineEventType::RoomTopic),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomInfoListener: Sync + Send {
|
||||
pub trait RoomInfoListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn call(&self, room_info: RoomInfo);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait TypingNotificationsListener: Sync + Send {
|
||||
pub trait TypingNotificationsListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn call(&self, typing_user_ids: Vec<String>);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait IdentityStatusChangeListener: Sync + Send {
|
||||
pub trait IdentityStatusChangeListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn call(&self, identity_status_change: Vec<IdentityStatusChange>);
|
||||
}
|
||||
|
||||
@@ -1462,3 +1469,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() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use ruma::{
|
||||
events::{room::power_levels::RoomPowerLevels as RumaPowerLevels, TimelineEventType},
|
||||
OwnedUserId, UserId,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::ClientError,
|
||||
event::{MessageLikeEventType, StateEventType},
|
||||
};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomPowerLevels {
|
||||
inner: RumaPowerLevels,
|
||||
own_user_id: OwnedUserId,
|
||||
}
|
||||
|
||||
impl RoomPowerLevels {
|
||||
pub fn new(value: RumaPowerLevels, own_user_id: OwnedUserId) -> Self {
|
||||
Self { inner: value, own_user_id }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomPowerLevels {
|
||||
fn values(&self) -> RoomPowerLevelsValues {
|
||||
self.inner.clone().into()
|
||||
}
|
||||
|
||||
/// Gets a map with the `UserId` of users with power levels other than `0`
|
||||
/// and their power level.
|
||||
pub fn user_power_levels(&self) -> HashMap<String, i64> {
|
||||
let mut user_power_levels = HashMap::<String, i64>::new();
|
||||
|
||||
for (id, level) in self.inner.users.iter() {
|
||||
user_power_levels.insert(id.to_string(), (*level).into());
|
||||
}
|
||||
|
||||
user_power_levels
|
||||
}
|
||||
|
||||
/// Returns true if the current user is able to ban in the room.
|
||||
pub fn can_own_user_ban(&self) -> bool {
|
||||
self.inner.user_can_ban(&self.own_user_id)
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to ban in the
|
||||
/// room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_ban(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_ban(&user_id))
|
||||
}
|
||||
|
||||
/// Returns true if the current user is able to redact their own messages in
|
||||
/// the room.
|
||||
pub fn can_own_user_redact_own(&self) -> bool {
|
||||
self.inner.user_can_redact_own_event(&self.own_user_id)
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to redact
|
||||
/// their own messages in the room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_redact_own(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_redact_own_event(&user_id))
|
||||
}
|
||||
|
||||
/// Returns true if the current user user is able to redact messages of
|
||||
/// other users in the room.
|
||||
pub fn can_own_user_redact_other(&self) -> bool {
|
||||
self.inner.user_can_redact_event_of_other(&self.own_user_id)
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to redact
|
||||
/// messages of other users in the room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_redact_other(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_redact_event_of_other(&user_id))
|
||||
}
|
||||
|
||||
/// Returns true if the current user is able to invite in the room.
|
||||
pub fn can_own_user_invite(&self) -> bool {
|
||||
self.inner.user_can_invite(&self.own_user_id)
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to invite in the
|
||||
/// room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_invite(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_invite(&user_id))
|
||||
}
|
||||
|
||||
/// Returns true if the current user is able to kick in the room.
|
||||
pub fn can_own_user_kick(&self) -> bool {
|
||||
self.inner.user_can_kick(&self.own_user_id)
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to kick in the
|
||||
/// room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_kick(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_kick(&user_id))
|
||||
}
|
||||
|
||||
/// Returns true if the current user is able to send a specific state event
|
||||
/// type in the room.
|
||||
pub fn can_own_user_send_state(&self, state_event: StateEventType) -> bool {
|
||||
self.inner.user_can_send_state(&self.own_user_id, state_event.into())
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to send a
|
||||
/// specific state event type in the room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_send_state(
|
||||
&self,
|
||||
user_id: String,
|
||||
state_event: StateEventType,
|
||||
) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_send_state(&user_id, state_event.into()))
|
||||
}
|
||||
|
||||
/// Returns true if the current user is able to send a specific message type
|
||||
/// in the room.
|
||||
pub fn can_own_user_send_message(&self, message: MessageLikeEventType) -> bool {
|
||||
self.inner.user_can_send_message(&self.own_user_id, message.into())
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to send a
|
||||
/// specific message type in the room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_send_message(
|
||||
&self,
|
||||
user_id: String,
|
||||
message: MessageLikeEventType,
|
||||
) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_send_message(&user_id, message.into()))
|
||||
}
|
||||
|
||||
/// Returns true if the current user is able to pin or unpin events in the
|
||||
/// room.
|
||||
pub fn can_own_user_pin_unpin(&self) -> bool {
|
||||
self.inner.user_can_send_state(&self.own_user_id, StateEventType::RoomPinnedEvents.into())
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to pin or unpin
|
||||
/// events in the room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_pin_unpin(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_send_state(&user_id, StateEventType::RoomPinnedEvents.into()))
|
||||
}
|
||||
|
||||
/// Returns true if the current user is able to trigger a notification in
|
||||
/// the room.
|
||||
pub fn can_own_user_trigger_room_notification(&self) -> bool {
|
||||
self.inner.user_can_trigger_room_notification(&self.own_user_id)
|
||||
}
|
||||
|
||||
/// Returns true if the user with the given user_id is able to trigger a
|
||||
/// notification in the room.
|
||||
///
|
||||
/// The call may fail if there is an error in getting the power levels.
|
||||
pub fn can_user_trigger_room_notification(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.user_can_trigger_room_notification(&user_id))
|
||||
}
|
||||
}
|
||||
|
||||
/// This intermediary struct is used to expose the power levels values through
|
||||
/// FFI and work around it not exposing public exported object fields.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomPowerLevelsValues {
|
||||
/// The level required to ban a user.
|
||||
pub ban: i64,
|
||||
/// The level required to invite a user.
|
||||
pub invite: i64,
|
||||
/// The level required to kick a user.
|
||||
pub kick: i64,
|
||||
/// The level required to redact an event.
|
||||
pub redact: i64,
|
||||
/// The default level required to send message events.
|
||||
pub events_default: i64,
|
||||
/// The default level required to send state events.
|
||||
pub state_default: i64,
|
||||
/// The default power level for every user in the room.
|
||||
pub users_default: i64,
|
||||
/// The level required to change the room's name.
|
||||
pub room_name: i64,
|
||||
/// The level required to change the room's avatar.
|
||||
pub room_avatar: i64,
|
||||
/// The level required to change the room's topic.
|
||||
pub room_topic: i64,
|
||||
}
|
||||
|
||||
impl From<RumaPowerLevels> for RoomPowerLevelsValues {
|
||||
fn from(value: RumaPowerLevels) -> Self {
|
||||
fn state_event_level_for(
|
||||
power_levels: &RumaPowerLevels,
|
||||
event_type: &TimelineEventType,
|
||||
) -> i64 {
|
||||
let default_state: i64 = power_levels.state_default.into();
|
||||
power_levels.events.get(event_type).map_or(default_state, |&level| level.into())
|
||||
}
|
||||
Self {
|
||||
ban: value.ban.into(),
|
||||
invite: value.invite.into(),
|
||||
kick: value.kick.into(),
|
||||
redact: value.redact.into(),
|
||||
events_default: value.events_default.into(),
|
||||
state_default: value.state_default.into(),
|
||||
users_default: value.users_default.into(),
|
||||
room_name: state_event_level_for(&value, &TimelineEventType::RoomName),
|
||||
room_avatar: state_event_level_for(&value, &TimelineEventType::RoomAvatar),
|
||||
room_topic: state_event_level_for(&value, &TimelineEventType::RoomTopic),
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
-17
@@ -1,4 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk::{EncryptionState, RoomState};
|
||||
use tracing::warn;
|
||||
@@ -7,7 +7,9 @@ use crate::{
|
||||
client::JoinRule,
|
||||
error::ClientError,
|
||||
notification_settings::RoomNotificationMode,
|
||||
room::{Membership, RoomHero, RoomHistoryVisibility},
|
||||
room::{
|
||||
power_levels::RoomPowerLevels, Membership, RoomHero, RoomHistoryVisibility, SuccessorRoom,
|
||||
},
|
||||
room_member::RoomMember,
|
||||
};
|
||||
|
||||
@@ -24,9 +26,14 @@ pub struct RoomInfo {
|
||||
topic: Option<String>,
|
||||
avatar_url: Option<String>,
|
||||
is_direct: bool,
|
||||
is_public: bool,
|
||||
/// Whether the room is public or not, based on the join rules.
|
||||
///
|
||||
/// Can be `None` if the join rules state event is not available for this
|
||||
/// room.
|
||||
is_public: Option<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>,
|
||||
@@ -41,7 +48,6 @@ pub struct RoomInfo {
|
||||
active_members_count: u64,
|
||||
invited_members_count: u64,
|
||||
joined_members_count: u64,
|
||||
user_power_levels: HashMap<String, i64>,
|
||||
highlight_count: u64,
|
||||
notification_count: u64,
|
||||
cached_user_defined_notification_mode: Option<RoomNotificationMode>,
|
||||
@@ -64,24 +70,34 @@ pub struct RoomInfo {
|
||||
join_rule: Option<JoinRule>,
|
||||
/// The history visibility for this room, if known.
|
||||
history_visibility: RoomHistoryVisibility,
|
||||
/// This room's current power levels.
|
||||
///
|
||||
/// Can be missing if the room power levels event is missing from the store.
|
||||
power_levels: Option<Arc<RoomPowerLevels>>,
|
||||
}
|
||||
|
||||
impl RoomInfo {
|
||||
pub(crate) async fn new(room: &matrix_sdk::Room) -> Result<Self, ClientError> {
|
||||
let unread_notification_counts = room.unread_notification_counts();
|
||||
|
||||
let power_levels_map = room.users_with_power_levels().await;
|
||||
let mut user_power_levels = HashMap::<String, i64>::new();
|
||||
for (id, level) in power_levels_map.iter() {
|
||||
user_power_levels.insert(id.to_string(), *level);
|
||||
}
|
||||
let pinned_event_ids =
|
||||
room.pinned_event_ids().unwrap_or_default().iter().map(|id| id.to_string()).collect();
|
||||
|
||||
let join_rule = room.join_rule().try_into();
|
||||
if let Err(e) = &join_rule {
|
||||
warn!("Failed to parse join rule: {:?}", e);
|
||||
}
|
||||
let join_rule = room
|
||||
.join_rule()
|
||||
.map(TryInto::try_into)
|
||||
.transpose()
|
||||
.inspect_err(|err| {
|
||||
warn!("Failed to parse join rule: {err}");
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
let power_levels = room
|
||||
.power_levels()
|
||||
.await
|
||||
.ok()
|
||||
.map(|p| RoomPowerLevels::new(p, room.own_user_id().to_owned()));
|
||||
|
||||
Ok(Self {
|
||||
id: room.room_id().to_string(),
|
||||
@@ -94,7 +110,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(),
|
||||
@@ -115,7 +131,6 @@ impl RoomInfo {
|
||||
active_members_count: room.active_members_count(),
|
||||
invited_members_count: room.invited_members_count(),
|
||||
joined_members_count: room.joined_members_count(),
|
||||
user_power_levels,
|
||||
highlight_count: unread_notification_counts.highlight_count,
|
||||
notification_count: unread_notification_counts.notification_count,
|
||||
cached_user_defined_notification_mode: room
|
||||
@@ -132,8 +147,9 @@ impl RoomInfo {
|
||||
num_unread_notifications: room.num_unread_notifications(),
|
||||
num_unread_mentions: room.num_unread_mentions(),
|
||||
pinned_event_ids,
|
||||
join_rule: join_rule.ok(),
|
||||
join_rule,
|
||||
history_visibility: room.history_visibility_or_default().try_into()?,
|
||||
power_levels: power_levels.map(Arc::new),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -15,14 +15,14 @@
|
||||
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::room_directory_search::RoomDirectorySearch as SdkRoomDirectorySearch;
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::ServerName;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{error::ClientError, task_handle::TaskHandle};
|
||||
use crate::{error::ClientError, runtime::get_runtime_handle, task_handle::TaskHandle};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum PublicRoomJoinRule {
|
||||
@@ -198,6 +198,6 @@ impl From<VectorDiff<matrix_sdk::room_directory_search::RoomDescription>>
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomDirectorySearchEntriesListener: Send + Sync + Debug {
|
||||
pub trait RoomDirectorySearchEntriesListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
fn on_update(&self, room_entries_update: Vec<RoomDirectorySearchEntryUpdate>);
|
||||
}
|
||||
|
||||
@@ -2,33 +2,29 @@
|
||||
|
||||
use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Duration};
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{pin_mut, StreamExt, TryFutureExt};
|
||||
use matrix_sdk::ruma::{
|
||||
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
RoomId,
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use matrix_sdk::{
|
||||
ruma::{
|
||||
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
RoomId,
|
||||
},
|
||||
Room as SdkRoom,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::{
|
||||
room_list_service::filters::{
|
||||
new_filter_all, new_filter_any, new_filter_category, new_filter_favourite,
|
||||
new_filter_fuzzy_match_room_name, new_filter_invite, new_filter_joined,
|
||||
new_filter_non_left, new_filter_none, new_filter_normalized_match_room_name,
|
||||
new_filter_unread, BoxedFilterFn, RoomCategory,
|
||||
new_filter_all, new_filter_any, new_filter_category, new_filter_deduplicate_versions,
|
||||
new_filter_favourite, new_filter_fuzzy_match_room_name, new_filter_invite,
|
||||
new_filter_joined, new_filter_non_left, new_filter_none,
|
||||
new_filter_normalized_match_room_name, new_filter_unread, BoxedFilterFn, RoomCategory,
|
||||
},
|
||||
timeline::default_event_filter,
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
use ruma::{OwnedRoomOrAliasId, OwnedServerName, ServerName};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
error::ClientError,
|
||||
room::{Membership, Room},
|
||||
room_info::RoomInfo,
|
||||
room_preview::RoomPreview,
|
||||
timeline::{configuration::TimelineEventTypeFilter, EventTimelineItem, Timeline},
|
||||
utils::AsyncRuntimeDropped,
|
||||
runtime::get_runtime_handle,
|
||||
TaskHandle,
|
||||
};
|
||||
|
||||
@@ -44,15 +40,12 @@ 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:?}")]
|
||||
#[error(
|
||||
"The requested room doesn't match the membership requirements {expected:?}, \
|
||||
observed {actual:?}"
|
||||
)]
|
||||
IncorrectRoomMembership { expected: Vec<Membership>, actual: Membership },
|
||||
}
|
||||
|
||||
@@ -64,12 +57,6 @@ impl From<matrix_sdk_ui::room_list_service::Error> for RoomListError {
|
||||
SlidingSync(error) => Self::SlidingSync { error: error.to_string() },
|
||||
UnknownList(list_name) => Self::UnknownList { list_name },
|
||||
RoomNotFound(room_id) => Self::RoomNotFound { room_name: room_id.to_string() },
|
||||
TimelineAlreadyExists(room_id) => {
|
||||
Self::TimelineAlreadyExists { room_name: room_id.to_string() }
|
||||
}
|
||||
InitializingTimeline(source) => {
|
||||
Self::InitializingTimeline { error: source.to_string() }
|
||||
}
|
||||
EventCache(error) => Self::EventCache { error: error.to_string() },
|
||||
}
|
||||
}
|
||||
@@ -101,13 +88,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> {
|
||||
@@ -137,7 +121,7 @@ impl RoomListService {
|
||||
})))
|
||||
}
|
||||
|
||||
fn subscribe_to_rooms(&self, room_ids: Vec<String>) -> Result<(), RoomListError> {
|
||||
async fn subscribe_to_rooms(&self, room_ids: Vec<String>) -> Result<(), RoomListError> {
|
||||
let room_ids = room_ids
|
||||
.into_iter()
|
||||
.map(|room_id| {
|
||||
@@ -145,7 +129,9 @@ impl RoomListService {
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
self.inner.subscribe_to_rooms(&room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>());
|
||||
self.inner
|
||||
.subscribe_to_rooms(&room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -182,8 +168,7 @@ impl RoomList {
|
||||
page_size: u32,
|
||||
listener: Box<dyn RoomListEntriesListener>,
|
||||
) -> Arc<RoomListEntriesWithDynamicAdaptersResult> {
|
||||
let this = self.clone();
|
||||
let utd_hook = self.room_list_service.utd_hook.clone();
|
||||
let this = self;
|
||||
|
||||
// The following code deserves a bit of explanation.
|
||||
// `matrix_sdk_ui::room_list_service::RoomList::entries_with_dynamic_adapters`
|
||||
@@ -237,6 +222,7 @@ impl RoomList {
|
||||
let dynamic_entries_controller =
|
||||
Arc::new(RoomListDynamicEntriesController::new(dynamic_entries_controller));
|
||||
|
||||
let utd_hook = this.room_list_service.utd_hook.clone();
|
||||
let entries_stream = Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(entries_stream);
|
||||
|
||||
@@ -244,7 +230,7 @@ impl RoomList {
|
||||
listener.on_update(
|
||||
diffs
|
||||
.into_iter()
|
||||
.map(|diff| RoomListEntriesUpdate::from(diff, utd_hook.clone()))
|
||||
.map(|room| RoomListEntriesUpdate::from(utd_hook.clone(), room))
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
@@ -271,7 +257,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 +348,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 +410,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 +418,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 +463,7 @@ pub enum RoomListEntriesDynamicFilterKind {
|
||||
None,
|
||||
NormalizedMatchRoomName { pattern: String },
|
||||
FuzzyMatchRoomName { pattern: String },
|
||||
DeduplicateVersions,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
@@ -521,177 +505,11 @@ impl From<RoomListEntriesDynamicFilterKind> for BoxedFilterFn {
|
||||
Kind::FuzzyMatchRoomName { pattern } => {
|
||||
Box::new(new_filter_fuzzy_match_room_name(&pattern))
|
||||
}
|
||||
Kind::DeduplicateVersions => Box::new(new_filter_deduplicate_versions()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomListItem {
|
||||
inner: Arc<matrix_sdk_ui::room_list_service::Room>,
|
||||
utd_hook: Option<Arc<UtdHookManager>>,
|
||||
}
|
||||
|
||||
impl RoomListItem {
|
||||
fn from(
|
||||
value: matrix_sdk_ui::room_list_service::Room,
|
||||
utd_hook: Option<Arc<UtdHookManager>>,
|
||||
) -> Self {
|
||||
Self { inner: Arc::new(value), utd_hook }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomListItem {
|
||||
fn id(&self) -> String {
|
||||
self.inner.id().to_string()
|
||||
}
|
||||
|
||||
/// Returns the room's name from the state event if available, otherwise
|
||||
/// compute a room name based on the room's nature (DM or not) and number of
|
||||
/// members.
|
||||
fn display_name(&self) -> Option<String> {
|
||||
self.inner.cached_display_name()
|
||||
}
|
||||
|
||||
fn avatar_url(&self) -> Option<String> {
|
||||
self.inner.avatar_url().map(|uri| uri.to_string())
|
||||
}
|
||||
|
||||
async fn is_direct(&self) -> bool {
|
||||
self.inner.inner_room().is_direct().await.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn canonical_alias(&self) -> Option<String> {
|
||||
self.inner.inner_room().canonical_alias().map(|alias| alias.to_string())
|
||||
}
|
||||
|
||||
async fn room_info(&self) -> Result<RoomInfo, ClientError> {
|
||||
RoomInfo::new(self.inner.inner_room()).await
|
||||
}
|
||||
|
||||
/// The room's current membership state.
|
||||
fn membership(&self) -> Membership {
|
||||
self.inner.inner_room().state().into()
|
||||
}
|
||||
|
||||
/// Builds a `RoomPreview` from a room list item. This is intended for
|
||||
/// invited, knocked or banned rooms.
|
||||
async fn preview_room(&self, via: Vec<String>) -> Result<Arc<RoomPreview>, ClientError> {
|
||||
// Validate parameters first.
|
||||
let server_names: Vec<OwnedServerName> = via
|
||||
.into_iter()
|
||||
.map(|server| ServerName::parse(server).map_err(ClientError::from))
|
||||
.collect::<Result<_, ClientError>>()?;
|
||||
|
||||
// Do the thing.
|
||||
let client = self.inner.client();
|
||||
let (room_or_alias_id, mut server_names) = if let Some(alias) = self.inner.canonical_alias()
|
||||
{
|
||||
let room_or_alias_id: OwnedRoomOrAliasId = alias.into();
|
||||
(room_or_alias_id, Vec::new())
|
||||
} else {
|
||||
let room_or_alias_id: OwnedRoomOrAliasId = self.inner.id().to_owned().into();
|
||||
(room_or_alias_id, server_names)
|
||||
};
|
||||
|
||||
// If no server names are provided and the room's membership is invited,
|
||||
// add the server name from the sender's user id as a fallback value
|
||||
if server_names.is_empty() {
|
||||
if let Ok(invite_details) = self.inner.invite_details().await {
|
||||
if let Some(inviter) = invite_details.inviter {
|
||||
server_names.push(inviter.user_id().server_name().to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let room_preview = client.get_room_preview(&room_or_alias_id, server_names).await?;
|
||||
|
||||
Ok(Arc::new(RoomPreview::new(AsyncRuntimeDropped::new(client), room_preview)))
|
||||
}
|
||||
|
||||
/// Build a full `Room` FFI object, filling its associated timeline.
|
||||
///
|
||||
/// An error will be returned if the room is a state different than joined
|
||||
/// or if its internal timeline hasn't been initialized.
|
||||
fn full_room(&self) -> Result<Arc<Room>, RoomListError> {
|
||||
if !matches!(self.membership(), Membership::Joined) {
|
||||
return Err(RoomListError::IncorrectRoomMembership {
|
||||
expected: vec![Membership::Joined],
|
||||
actual: self.membership(),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(timeline) = self.inner.timeline() {
|
||||
Ok(Arc::new(Room::with_timeline(
|
||||
self.inner.inner_room().clone(),
|
||||
Arc::new(RwLock::new(Some(Timeline::from_arc(timeline)))),
|
||||
)))
|
||||
} else {
|
||||
Err(RoomListError::TimelineNotInitialized {
|
||||
room_name: self.inner.inner_room().room_id().to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks whether the Room's timeline has been initialized before.
|
||||
fn is_timeline_initialized(&self) -> bool {
|
||||
self.inner.is_timeline_initialized()
|
||||
}
|
||||
|
||||
/// Initializes the timeline for this room using the provided parameters.
|
||||
///
|
||||
/// * `event_type_filter` - An optional [`TimelineEventTypeFilter`] to be
|
||||
/// used to filter timeline events besides the default timeline filter. If
|
||||
/// `None` is passed, only the default timeline filter will be used.
|
||||
/// * `internal_id_prefix` - An optional String that will be prepended to
|
||||
/// all the timeline item's internal IDs, making it possible to
|
||||
/// distinguish different timeline instances from each other.
|
||||
async fn init_timeline(
|
||||
&self,
|
||||
event_type_filter: Option<Arc<TimelineEventTypeFilter>>,
|
||||
internal_id_prefix: Option<String>,
|
||||
) -> Result<(), RoomListError> {
|
||||
let mut timeline_builder = self
|
||||
.inner
|
||||
.default_room_timeline_builder()
|
||||
.await
|
||||
.map_err(|err| RoomListError::InitializingTimeline { error: err.to_string() })?;
|
||||
|
||||
if let Some(event_type_filter) = event_type_filter {
|
||||
timeline_builder = timeline_builder.event_filter(move |event, room_version_id| {
|
||||
// Always perform the default filter first
|
||||
default_event_filter(event, room_version_id) && event_type_filter.filter(event)
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(internal_id_prefix) = internal_id_prefix {
|
||||
timeline_builder = timeline_builder.with_internal_id_prefix(internal_id_prefix);
|
||||
}
|
||||
|
||||
if let Some(utd_hook) = self.utd_hook.clone() {
|
||||
timeline_builder = timeline_builder.with_unable_to_decrypt_hook(utd_hook);
|
||||
}
|
||||
|
||||
self.inner.init_timeline_with_builder(timeline_builder).map_err(RoomListError::from).await
|
||||
}
|
||||
|
||||
/// Checks whether the room is encrypted or not.
|
||||
///
|
||||
/// **Note**: this info may not be reliable if you don't set up
|
||||
/// `m.room.encryption` as required state.
|
||||
async fn is_encrypted(&self) -> bool {
|
||||
self.inner
|
||||
.latest_encryption_state()
|
||||
.await
|
||||
.map(|state| state.is_encrypted())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn latest_event(&self) -> Option<EventTimelineItem> {
|
||||
self.inner.latest_event().await.map(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct UnreadNotificationsCount {
|
||||
highlight_count: u32,
|
||||
|
||||
@@ -37,8 +37,9 @@ impl RoomPreview {
|
||||
membership: info.state.map(|state| state.into()),
|
||||
join_rule: info
|
||||
.join_rule
|
||||
.clone()
|
||||
.try_into()
|
||||
.as_ref()
|
||||
.map(TryInto::try_into)
|
||||
.transpose()
|
||||
.map_err(|_| anyhow::anyhow!("unhandled SpaceRoomJoinRule kind"))?,
|
||||
is_direct: info.is_direct,
|
||||
heroes: info
|
||||
@@ -59,15 +60,7 @@ impl RoomPreview {
|
||||
let room =
|
||||
self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?;
|
||||
|
||||
let should_forget = matches!(room.state(), matrix_sdk::RoomState::Invited);
|
||||
|
||||
room.leave().await.map_err(ClientError::from)?;
|
||||
|
||||
if should_forget {
|
||||
_ = self.forget().await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(room.leave().await?)
|
||||
}
|
||||
|
||||
/// Get the user who created the invite, if any.
|
||||
@@ -122,17 +115,17 @@ pub struct RoomPreviewInfo {
|
||||
/// The membership state for the current user, if known.
|
||||
pub membership: Option<Membership>,
|
||||
/// The join rule for this room (private, public, knock, etc.).
|
||||
pub join_rule: JoinRule,
|
||||
pub join_rule: Option<JoinRule>,
|
||||
/// Whether the room is direct or not, if known.
|
||||
pub is_direct: Option<bool>,
|
||||
/// Room heroes.
|
||||
pub heroes: Option<Vec<RoomHero>>,
|
||||
}
|
||||
|
||||
impl TryFrom<SpaceRoomJoinRule> for JoinRule {
|
||||
impl TryFrom<&SpaceRoomJoinRule> for JoinRule {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(join_rule: SpaceRoomJoinRule) -> Result<Self, ()> {
|
||||
fn try_from(join_rule: &SpaceRoomJoinRule) -> Result<Self, ()> {
|
||||
Ok(match join_rule {
|
||||
SpaceRoomJoinRule::Invite => JoinRule::Invite,
|
||||
SpaceRoomJoinRule::Knock => JoinRule::Knock,
|
||||
|
||||
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 matrix_sdk_common::executor::{spawn, JoinHandle};
|
||||
|
||||
/// A dummy guard that does nothing when dropped.
|
||||
/// This is used for the Wasm implementation to match
|
||||
/// tokio::runtime::EnterGuard.
|
||||
#[derive(Debug)]
|
||||
pub struct RuntimeGuard;
|
||||
|
||||
/// A runtime handle implementation for WebAssembly targets.
|
||||
///
|
||||
/// This implements a minimal subset of the tokio::runtime::Handle API
|
||||
/// that is needed for the matrix-rust-sdk to function on Wasm.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Handle;
|
||||
pub type Runtime = Handle;
|
||||
|
||||
impl Handle {
|
||||
/// Spawns a future in the wasm32 bindgen runtime.
|
||||
#[track_caller]
|
||||
pub fn spawn<F>(&self, future: F) -> JoinHandle<F::Output>
|
||||
where
|
||||
F: Future + 'static,
|
||||
F::Output: 'static,
|
||||
{
|
||||
spawn(future)
|
||||
}
|
||||
|
||||
/// Runs the provided function on an executor dedicated to blocking
|
||||
/// operations.
|
||||
#[track_caller]
|
||||
pub fn spawn_blocking<F, R>(&self, func: F) -> JoinHandle<R>
|
||||
where
|
||||
F: FnOnce() -> R + 'static,
|
||||
R: 'static,
|
||||
{
|
||||
spawn(async move { func() })
|
||||
}
|
||||
|
||||
/// Runs a future to completion on the current thread.
|
||||
pub fn block_on<F, T>(&self, future: F) -> T
|
||||
where
|
||||
F: Future<Output = T>,
|
||||
{
|
||||
futures_executor::block_on(future)
|
||||
}
|
||||
|
||||
/// Enters the runtime context.
|
||||
///
|
||||
/// For WebAssembly, this is a no-op that returns a dummy guard.
|
||||
pub fn enter(&self) -> RuntimeGuard {
|
||||
RuntimeGuard
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a runtime handle appropriate for the current target platform.
|
||||
///
|
||||
/// This function returns a unified `Handle` type that works across both
|
||||
/// Wasm and non-Wasm platforms, allowing code to be written that is
|
||||
/// agnostic to the platform-specific runtime implementation.
|
||||
///
|
||||
/// Returns:
|
||||
/// - A `tokio::runtime::Handle` on non-Wasm platforms
|
||||
/// - A `WasmRuntimeHandle` on Wasm platforms
|
||||
pub fn get_runtime_handle() -> Handle {
|
||||
Handle
|
||||
}
|
||||
}
|
||||
|
||||
pub use sys::*;
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
encryption::{
|
||||
@@ -11,10 +10,13 @@ use matrix_sdk::{
|
||||
ruma::events::key::verification::VerificationMethod,
|
||||
Account,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::UserId;
|
||||
use tracing::{error, warn};
|
||||
|
||||
use crate::{client::UserProfile, error::ClientError, utils::Timestamp};
|
||||
use crate::{
|
||||
client::UserProfile, error::ClientError, runtime::get_runtime_handle, utils::Timestamp,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SessionVerificationEmoji {
|
||||
@@ -51,7 +53,7 @@ pub struct SessionVerificationRequestDetails {
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SessionVerificationControllerDelegate: Sync + Send {
|
||||
pub trait SessionVerificationControllerDelegate: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn did_receive_verification_request(&self, details: SessionVerificationRequestDetails);
|
||||
fn did_accept_verification_request(&self);
|
||||
fn did_start_sas_verification(&self);
|
||||
@@ -94,7 +96,7 @@ impl SessionVerificationController {
|
||||
.encryption
|
||||
.get_verification_request(&sender_id, flow_id)
|
||||
.await
|
||||
.ok_or(ClientError::new("Unknown session verification request"))?;
|
||||
.ok_or(ClientError::from_str("Unknown session verification request", None))?;
|
||||
|
||||
self.set_ongoing_verification_request(verification_request)
|
||||
}
|
||||
@@ -114,11 +116,8 @@ impl SessionVerificationController {
|
||||
/// Request verification for the current device
|
||||
pub async fn request_device_verification(&self) -> Result<(), ClientError> {
|
||||
let methods = vec![VerificationMethod::SasV1];
|
||||
let verification_request = self
|
||||
.user_identity
|
||||
.request_verification_with_methods(methods)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?;
|
||||
let verification_request =
|
||||
self.user_identity.request_verification_with_methods(methods).await?;
|
||||
|
||||
self.set_ongoing_verification_request(verification_request)
|
||||
}
|
||||
@@ -131,18 +130,15 @@ impl SessionVerificationController {
|
||||
.encryption
|
||||
.get_user_identity(&user_id)
|
||||
.await?
|
||||
.ok_or(ClientError::new("Unknown user identity"))?;
|
||||
.ok_or(ClientError::from_str("Unknown user identity", None))?;
|
||||
|
||||
if user_identity.is_verified() {
|
||||
return Err(ClientError::new("User is already verified"));
|
||||
return Err(ClientError::from_str("User is already verified", None));
|
||||
}
|
||||
|
||||
let methods = vec![VerificationMethod::SasV1];
|
||||
|
||||
let verification_request = user_identity
|
||||
.request_verification_with_methods(methods)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?;
|
||||
let verification_request = user_identity.request_verification_with_methods(methods).await?;
|
||||
|
||||
self.set_ongoing_verification_request(verification_request)
|
||||
}
|
||||
@@ -153,7 +149,7 @@ impl SessionVerificationController {
|
||||
let verification_request = self.verification_request.read().unwrap().clone();
|
||||
|
||||
let Some(verification_request) = verification_request else {
|
||||
return Err(ClientError::new("Verification request missing."));
|
||||
return Err(ClientError::from_str("Verification request missing.", None));
|
||||
};
|
||||
|
||||
match verification_request.start_sas().await {
|
||||
@@ -183,7 +179,7 @@ impl SessionVerificationController {
|
||||
let sas_verification = self.sas_verification.read().unwrap().clone();
|
||||
|
||||
let Some(sas_verification) = sas_verification else {
|
||||
return Err(ClientError::new("SAS verification missing"));
|
||||
return Err(ClientError::from_str("SAS verification missing", None));
|
||||
};
|
||||
|
||||
Ok(sas_verification.confirm().await?)
|
||||
@@ -194,7 +190,7 @@ impl SessionVerificationController {
|
||||
let sas_verification = self.sas_verification.read().unwrap().clone();
|
||||
|
||||
let Some(sas_verification) = sas_verification else {
|
||||
return Err(ClientError::new("SAS verification missing"));
|
||||
return Err(ClientError::from_str("SAS verification missing", None));
|
||||
};
|
||||
|
||||
Ok(sas_verification.mismatch().await?)
|
||||
@@ -205,7 +201,7 @@ impl SessionVerificationController {
|
||||
let verification_request = self.verification_request.read().unwrap().clone();
|
||||
|
||||
let Some(verification_request) = verification_request else {
|
||||
return Err(ClientError::new("Verification request missing."));
|
||||
return Err(ClientError::from_str("Verification request missing.", None));
|
||||
};
|
||||
|
||||
Ok(verification_request.cancel().await?)
|
||||
@@ -239,7 +235,10 @@ impl SessionVerificationController {
|
||||
if sender != self.user_identity.user_id() {
|
||||
if let Some(status) = self.encryption.cross_signing_status().await {
|
||||
if !status.is_complete() {
|
||||
warn!("Cannot verify other users until our own device's cross-signing status is complete: {:?}", status);
|
||||
warn!(
|
||||
"Cannot verify other users until our own device's cross-signing status \
|
||||
is complete: {status:?}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -285,7 +284,10 @@ impl SessionVerificationController {
|
||||
if !ongoing_verification_request.is_done()
|
||||
&& !ongoing_verification_request.is_cancelled()
|
||||
{
|
||||
return Err(ClientError::new("There is another verification flow ongoing."));
|
||||
return Err(ClientError::from_str(
|
||||
"There is another verification flow ongoing.",
|
||||
None,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,24 +12,22 @@
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{fmt::Debug, sync::Arc, time::Duration};
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use futures_util::pin_mut;
|
||||
use matrix_sdk::{crypto::types::events::UtdCause, Client};
|
||||
use matrix_sdk::Client;
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::{
|
||||
sync_service::{
|
||||
State as MatrixSyncServiceState, SyncService as MatrixSyncService,
|
||||
SyncServiceBuilder as MatrixSyncServiceBuilder,
|
||||
},
|
||||
unable_to_decrypt_hook::{
|
||||
UnableToDecryptHook, UnableToDecryptInfo as SdkUnableToDecryptInfo, UtdHookManager,
|
||||
},
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
error::ClientError, helpers::unwrap_or_clone_arc, room_list::RoomListService, TaskHandle,
|
||||
error::ClientError, helpers::unwrap_or_clone_arc, room_list::RoomListService,
|
||||
runtime::get_runtime_handle, TaskHandle,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
@@ -54,7 +52,7 @@ impl From<MatrixSyncServiceState> for SyncServiceState {
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SyncServiceStateObserver: Send + Sync + Debug {
|
||||
pub trait SyncServiceStateObserver: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
fn on_update(&self, state: SyncServiceState);
|
||||
}
|
||||
|
||||
@@ -96,19 +94,13 @@ impl SyncService {
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct SyncServiceBuilder {
|
||||
client: Client,
|
||||
builder: MatrixSyncServiceBuilder,
|
||||
|
||||
utd_hook: Option<Arc<UtdHookManager>>,
|
||||
}
|
||||
|
||||
impl SyncServiceBuilder {
|
||||
pub(crate) fn new(client: Client) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
client: client.clone(),
|
||||
builder: MatrixSyncService::builder(client),
|
||||
utd_hook: None,
|
||||
})
|
||||
pub(crate) fn new(client: Client, utd_hook: Option<Arc<UtdHookManager>>) -> Arc<Self> {
|
||||
Arc::new(Self { builder: MatrixSyncService::builder(client), utd_hook })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,40 +109,20 @@ 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 })
|
||||
Arc::new(Self { builder, ..this })
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
pub fn with_share_pos(self: Arc<Self>, enable: bool) -> Arc<Self> {
|
||||
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)),
|
||||
})
|
||||
let builder = this.builder.with_share_pos(enable);
|
||||
Arc::new(Self { builder, ..this })
|
||||
}
|
||||
|
||||
pub async fn finish(self: Arc<Self>) -> Result<Arc<SyncService>, ClientError> {
|
||||
@@ -161,83 +133,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,4 +1,4 @@
|
||||
use tokio::task::JoinHandle;
|
||||
use matrix_sdk_common::executor::JoinHandle;
|
||||
use tracing::debug;
|
||||
|
||||
/// A task handle is a way to keep the handle a task running by itself in
|
||||
|
||||
@@ -63,9 +63,27 @@ impl From<FilterTimelineEventType> for TimelineEventType {
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum TimelineFocus {
|
||||
Live,
|
||||
Event { event_id: String, num_context_events: u16 },
|
||||
PinnedEvents { max_events_to_load: u16, max_concurrent_requests: u16 },
|
||||
Live {
|
||||
/// Whether to hide in-thread replies from the live timeline.
|
||||
hide_threaded_events: bool,
|
||||
},
|
||||
Event {
|
||||
/// The initial event to focus on. This is usually the target of a
|
||||
/// permalink.
|
||||
event_id: String,
|
||||
/// The number of context events to load around the focused event.
|
||||
num_context_events: u16,
|
||||
/// Whether to hide in-thread replies from the live timeline.
|
||||
hide_threaded_events: bool,
|
||||
},
|
||||
Thread {
|
||||
/// The thread root event ID to focus on.
|
||||
root_event_id: String,
|
||||
},
|
||||
PinnedEvents {
|
||||
max_events_to_load: u16,
|
||||
max_concurrent_requests: u16,
|
||||
},
|
||||
}
|
||||
|
||||
impl TryFrom<TimelineFocus> for matrix_sdk_ui::timeline::TimelineFocus {
|
||||
@@ -75,15 +93,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 } => {
|
||||
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 })
|
||||
}
|
||||
TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests } => {
|
||||
Ok(Self::PinnedEvents { max_events_to_load, max_concurrent_requests })
|
||||
@@ -146,4 +178,8 @@ pub struct TimelineConfiguration {
|
||||
/// As this has a non negligible performance impact, make sure to enable it
|
||||
/// only when you need it.
|
||||
pub track_read_receipts: bool,
|
||||
|
||||
/// Whether this timeline instance should report UTDs through the client's
|
||||
/// delegate.
|
||||
pub report_utds: bool,
|
||||
}
|
||||
|
||||
@@ -96,6 +96,14 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
// A note about this `allow(clippy::large_enum_variant)`.
|
||||
// In order to reduce the size of `TimelineItemContent`, we would need to
|
||||
// put some parts in a `Box`, or an `Arc`. Sadly, it doesn't play well with
|
||||
// UniFFI. We would need to change the `uniffi::Record` of the subtypes into
|
||||
// `uniffi::Object`, which is a radical change. It would simplify the memory
|
||||
// usage, but it would slow down the performance around the FFI border. Thus,
|
||||
// let's consider this is a false-positive lint in this particular case.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum TimelineItemContent {
|
||||
MsgLike {
|
||||
content: MsgLikeContent,
|
||||
|
||||
@@ -16,9 +16,8 @@ use std::{collections::HashMap, fmt::Write as _, fs, panic, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use as_variant::as_variant;
|
||||
use async_compat::get_runtime_handle;
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{pin_mut, StreamExt as _};
|
||||
use futures_util::pin_mut;
|
||||
use matrix_sdk::{
|
||||
attachment::{
|
||||
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
|
||||
@@ -31,12 +30,16 @@ use matrix_sdk::{
|
||||
reply::{EnforceThread, Reply},
|
||||
},
|
||||
};
|
||||
use matrix_sdk_common::{
|
||||
executor::{AbortHandle, JoinHandle},
|
||||
stream::StreamExt,
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{
|
||||
self, EventItemOrigin, Profile, RepliedToEvent, TimelineDetails,
|
||||
self, AttachmentSource, EventItemOrigin, Profile, TimelineDetails,
|
||||
TimelineUniqueId as SdkTimelineUniqueId,
|
||||
};
|
||||
use mime::Mime;
|
||||
use reply::{InReplyToDetails, RepliedToEventDetails};
|
||||
use reply::{EmbeddedEventDetails, InReplyToDetails};
|
||||
use ruma::{
|
||||
events::{
|
||||
location::{AssetType as RumaAssetType, LocationContent, ZoomLevel},
|
||||
@@ -48,7 +51,6 @@ use ruma::{
|
||||
UnstablePollStartContentBlock,
|
||||
},
|
||||
},
|
||||
receipt::ReceiptThread,
|
||||
room::message::{
|
||||
LocationMessageEventContent, MessageType, ReplyWithinThread,
|
||||
RoomMessageEventContentWithoutRelation,
|
||||
@@ -57,10 +59,7 @@ use ruma::{
|
||||
},
|
||||
EventId, UInt,
|
||||
};
|
||||
use tokio::{
|
||||
sync::Mutex,
|
||||
task::{AbortHandle, JoinHandle},
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{error, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -75,6 +74,7 @@ use crate::{
|
||||
AssetType, AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, PollKind,
|
||||
ThumbnailInfo, VideoInfo,
|
||||
},
|
||||
runtime::get_runtime_handle,
|
||||
task_handle::TaskHandle,
|
||||
utils::Timestamp,
|
||||
};
|
||||
@@ -85,6 +85,7 @@ mod msg_like;
|
||||
mod reply;
|
||||
|
||||
use matrix_sdk::utils::formatted_body_from;
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
|
||||
use crate::error::QueueWedgeError;
|
||||
|
||||
@@ -99,11 +100,6 @@ impl Timeline {
|
||||
Arc::new(Self { inner })
|
||||
}
|
||||
|
||||
pub(crate) fn from_arc(inner: Arc<matrix_sdk_ui::timeline::Timeline>) -> Arc<Self> {
|
||||
// SAFETY: repr(transparent) means transmuting the arc this way is allowed
|
||||
unsafe { Arc::from_raw(Arc::into_raw(inner) as _) }
|
||||
}
|
||||
|
||||
fn send_attachment(
|
||||
self: Arc<Self>,
|
||||
params: UploadParameters,
|
||||
@@ -131,7 +127,7 @@ impl Timeline {
|
||||
|
||||
let handle = SendAttachmentJoinHandle::new(get_runtime_handle().spawn(async move {
|
||||
let mut request =
|
||||
self.inner.send_attachment(params.filename, mime_type, attachment_config);
|
||||
self.inner.send_attachment(params.source, mime_type, attachment_config);
|
||||
|
||||
if params.use_send_queue {
|
||||
request = request.use_send_queue();
|
||||
@@ -201,8 +197,8 @@ fn build_thumbnail_info(
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct UploadParameters {
|
||||
/// Filename (previously called "url") for the media to be sent.
|
||||
filename: String,
|
||||
/// Source from which to upload data
|
||||
source: UploadSource,
|
||||
/// Optional non-formatted caption, for clients that support it.
|
||||
caption: Option<String>,
|
||||
/// Optional HTML-formatted caption, for clients that support it.
|
||||
@@ -217,6 +213,32 @@ pub struct UploadParameters {
|
||||
use_send_queue: bool,
|
||||
}
|
||||
|
||||
/// A source for uploading a file
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum UploadSource {
|
||||
/// Upload source is a file on disk
|
||||
File {
|
||||
/// Path to file
|
||||
filename: String,
|
||||
},
|
||||
/// Upload source is data in memory
|
||||
Data {
|
||||
/// Bytes being uploaded
|
||||
bytes: Vec<u8>,
|
||||
/// Filename to associate with bytes
|
||||
filename: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<UploadSource> for AttachmentSource {
|
||||
fn from(value: UploadSource) -> Self {
|
||||
match value {
|
||||
UploadSource::File { filename } => Self::File(filename.into()),
|
||||
UploadSource::Data { bytes, filename } => Self::Data { bytes, filename },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct ReplyParameters {
|
||||
/// The ID of the event to reply to.
|
||||
@@ -328,9 +350,7 @@ impl Timeline {
|
||||
event_id: String,
|
||||
) -> Result<(), ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
self.inner
|
||||
.send_single_receipt(receipt_type.into(), ReceiptThread::Unthreaded, event_id)
|
||||
.await?;
|
||||
self.inner.send_single_receipt(receipt_type.into(), event_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -358,7 +378,7 @@ impl Timeline {
|
||||
Ok(handle) => Ok(Arc::new(SendHandle::new(handle))),
|
||||
Err(err) => {
|
||||
error!("error when sending a message: {err}");
|
||||
Err(anyhow::anyhow!(err).into())
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -511,10 +531,7 @@ impl Timeline {
|
||||
msg: Arc<RoomMessageEventContentWithoutRelation>,
|
||||
reply_params: ReplyParameters,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner
|
||||
.send_reply((*msg).clone(), reply_params.try_into()?)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!(err))?;
|
||||
self.inner.send_reply((*msg).clone(), reply_params.try_into()?).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -544,7 +561,10 @@ impl Timeline {
|
||||
let event_id = match event_or_transaction_id {
|
||||
EventOrTransactionId::EventId { event_id } => EventId::parse(event_id)?,
|
||||
EventOrTransactionId::TransactionId { .. } => {
|
||||
warn!("trying to apply an edit to a local echo that doesn't exist in this timeline, aborting");
|
||||
warn!(
|
||||
"trying to apply an edit to a local echo that doesn't exist \
|
||||
in this timeline, aborting"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -565,7 +585,8 @@ impl Timeline {
|
||||
description: Option<String>,
|
||||
zoom_level: Option<u8>,
|
||||
asset_type: Option<AssetType>,
|
||||
) {
|
||||
reply_params: Option<ReplyParameters>,
|
||||
) -> Result<(), ClientError> {
|
||||
let mut location_event_message_content =
|
||||
LocationMessageEventContent::new(body, geo_uri.clone());
|
||||
|
||||
@@ -582,8 +603,13 @@ impl Timeline {
|
||||
let room_message_event_content = RoomMessageEventContentWithoutRelation::new(
|
||||
MessageType::Location(location_event_message_content),
|
||||
);
|
||||
// Errors are logged in `Self::send` already.
|
||||
let _ = self.send(Arc::new(room_message_event_content)).await;
|
||||
|
||||
if let Some(reply_params) = reply_params {
|
||||
self.send_reply(Arc::new(room_message_event_content), reply_params).await
|
||||
} else {
|
||||
self.send(Arc::new(room_message_event_content)).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle a reaction on an event.
|
||||
@@ -608,7 +634,10 @@ impl Timeline {
|
||||
|
||||
pub async fn fetch_details_for_event(&self, event_id: String) -> Result<(), ClientError> {
|
||||
let event_id = <&EventId>::try_from(event_id.as_str())?;
|
||||
self.inner.fetch_details_for_event(event_id).await.context("Fetching event details")?;
|
||||
self.inner
|
||||
.fetch_details_for_event(event_id)
|
||||
.await
|
||||
.map_err(|e| ClientError::from_str(e, Some("Fetching event details".to_owned())))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -661,25 +690,30 @@ impl Timeline {
|
||||
let event_id = EventId::parse(&event_id_str)?;
|
||||
|
||||
let replied_to = match self.inner.room().load_or_fetch_event(&event_id, None).await {
|
||||
Ok(event) => RepliedToEvent::try_from_timeline_event_for_room(event, self.inner.room())
|
||||
.await
|
||||
.map_err(ClientError::from),
|
||||
Ok(event) => self.inner.make_replied_to(event).await.map_err(ClientError::from),
|
||||
Err(e) => Err(ClientError::from(e)),
|
||||
};
|
||||
|
||||
match replied_to {
|
||||
Ok(replied_to) => Ok(Arc::new(InReplyToDetails::new(
|
||||
Ok(Some(replied_to)) => Ok(Arc::new(InReplyToDetails::new(
|
||||
event_id_str,
|
||||
RepliedToEventDetails::Ready {
|
||||
content: replied_to.content().clone().into(),
|
||||
sender: replied_to.sender().to_string(),
|
||||
sender_profile: replied_to.sender_profile().into(),
|
||||
EmbeddedEventDetails::Ready {
|
||||
content: replied_to.content.clone().into(),
|
||||
sender: replied_to.sender.to_string(),
|
||||
sender_profile: replied_to.sender_profile.into(),
|
||||
timestamp: replied_to.timestamp.into(),
|
||||
event_or_transaction_id: replied_to.identifier.into(),
|
||||
},
|
||||
))),
|
||||
|
||||
Ok(None) => Ok(Arc::new(InReplyToDetails::new(
|
||||
event_id_str,
|
||||
EmbeddedEventDetails::Error { message: "unsupported event".to_owned() },
|
||||
))),
|
||||
|
||||
Err(e) => Ok(Arc::new(InReplyToDetails::new(
|
||||
event_id_str,
|
||||
RepliedToEventDetails::Error { message: e.to_string() },
|
||||
EmbeddedEventDetails::Error { message: e.to_string() },
|
||||
))),
|
||||
}
|
||||
}
|
||||
@@ -783,12 +817,12 @@ pub enum FocusEventError {
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait TimelineListener: Sync + Send {
|
||||
pub trait TimelineListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, diff: Vec<Arc<TimelineDiff>>);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait PaginationStatusListener: Sync + Send {
|
||||
pub trait PaginationStatusListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, status: RoomPaginationStatus);
|
||||
}
|
||||
|
||||
@@ -1079,7 +1113,7 @@ impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
|
||||
is_remote: !item.is_local_echo(),
|
||||
event_or_transaction_id: item.identifier().into(),
|
||||
sender: item.sender().to_string(),
|
||||
sender_profile: item.sender_profile().into(),
|
||||
sender_profile: item.sender_profile().clone().into(),
|
||||
is_own: item.is_own(),
|
||||
is_editable: item.is_editable(),
|
||||
content: item.content().clone().into(),
|
||||
@@ -1120,13 +1154,13 @@ pub enum ProfileDetails {
|
||||
Error { message: String },
|
||||
}
|
||||
|
||||
impl From<&TimelineDetails<Profile>> for ProfileDetails {
|
||||
fn from(details: &TimelineDetails<Profile>) -> Self {
|
||||
impl From<TimelineDetails<Profile>> for ProfileDetails {
|
||||
fn from(details: TimelineDetails<Profile>) -> Self {
|
||||
match details {
|
||||
TimelineDetails::Unavailable => Self::Unavailable,
|
||||
TimelineDetails::Pending => Self::Pending,
|
||||
TimelineDetails::Ready(profile) => Self::Ready {
|
||||
display_name: profile.display_name.clone(),
|
||||
display_name: profile.display_name,
|
||||
display_name_ambiguous: profile.display_name_ambiguous,
|
||||
avatar_url: profile.avatar_url.as_ref().map(ToString::to_string),
|
||||
},
|
||||
@@ -1176,15 +1210,15 @@ impl TryFrom<PollData> for UnstablePollStartContentBlock {
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SendAttachmentJoinHandle {
|
||||
join_hdl: Arc<Mutex<JoinHandle<Result<(), RoomError>>>>,
|
||||
abort_hdl: AbortHandle,
|
||||
join_handle: Arc<Mutex<JoinHandle<Result<(), RoomError>>>>,
|
||||
abort_handle: AbortHandle,
|
||||
}
|
||||
|
||||
impl SendAttachmentJoinHandle {
|
||||
fn new(join_hdl: JoinHandle<Result<(), RoomError>>) -> Arc<Self> {
|
||||
let abort_hdl = join_hdl.abort_handle();
|
||||
let join_hdl = Arc::new(Mutex::new(join_hdl));
|
||||
Arc::new(Self { join_hdl, abort_hdl })
|
||||
fn new(join_handle: JoinHandle<Result<(), RoomError>>) -> Arc<Self> {
|
||||
let abort_handle = join_handle.abort_handle();
|
||||
let join_handle = Arc::new(Mutex::new(join_handle));
|
||||
Arc::new(Self { join_handle, abort_handle })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1194,7 +1228,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 {
|
||||
@@ -1204,7 +1238,10 @@ impl SendAttachmentJoinHandle {
|
||||
return Ok(());
|
||||
}
|
||||
error!("task panicked! resuming panic from here.");
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
panic::resume_unwind(err.into_panic());
|
||||
#[cfg(target_family = "wasm")]
|
||||
panic!("task panicked! {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1213,7 +1250,7 @@ impl SendAttachmentJoinHandle {
|
||||
///
|
||||
/// A subsequent call to [`Self::join`] will return immediately.
|
||||
pub fn cancel(&self) {
|
||||
self.abort_hdl.abort();
|
||||
self.abort_handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1343,3 +1380,241 @@ impl LazyTimelineItemProvider {
|
||||
self.0.contains_only_emojis()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "unstable-msc4274")]
|
||||
mod galleries {
|
||||
use std::{panic, sync::Arc};
|
||||
|
||||
use matrix_sdk::{
|
||||
attachment::{
|
||||
AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo, Thumbnail,
|
||||
},
|
||||
utils::formatted_body_from,
|
||||
};
|
||||
use matrix_sdk_common::executor::{AbortHandle, JoinHandle};
|
||||
use matrix_sdk_ui::timeline::GalleryConfig;
|
||||
use mime::Mime;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
error::RoomError,
|
||||
ruma::{AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, VideoInfo},
|
||||
runtime::get_runtime_handle,
|
||||
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.");
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
panic::resume_unwind(err.into_panic());
|
||||
#[cfg(target_family = "wasm")]
|
||||
panic!("task panicked! {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel the current sending task.
|
||||
///
|
||||
/// A subsequent call to [`Self::join`] will return immediately.
|
||||
pub fn cancel(&self) {
|
||||
self.abort_handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Timeline {
|
||||
pub fn send_gallery(
|
||||
self: Arc<Self>,
|
||||
params: GalleryUploadParameters,
|
||||
item_infos: Vec<GalleryItemInfo>,
|
||||
) -> Result<Arc<SendGalleryJoinHandle>, RoomError> {
|
||||
let formatted_caption = formatted_body_from(
|
||||
params.caption.as_deref(),
|
||||
params.formatted_caption.map(Into::into),
|
||||
);
|
||||
|
||||
let mut gallery_config = GalleryConfig::new()
|
||||
.caption(params.caption)
|
||||
.formatted_caption(formatted_caption)
|
||||
.mentions(params.mentions.map(Into::into))
|
||||
.reply(params.reply_params.map(|p| p.try_into()).transpose()?);
|
||||
|
||||
for item_info in item_infos {
|
||||
gallery_config = gallery_config.add_item(item_info.try_into()?);
|
||||
}
|
||||
|
||||
let handle = SendGalleryJoinHandle::new(get_runtime_handle().spawn(async move {
|
||||
let request = self.inner.send_gallery(gallery_config);
|
||||
request.await.map_err(|_| RoomError::FailedSendingAttachment)?;
|
||||
Ok(())
|
||||
}));
|
||||
|
||||
Ok(handle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,10 @@ use std::{collections::HashMap, sync::Arc};
|
||||
use matrix_sdk::crypto::types::events::UtdCause;
|
||||
use ruma::events::{room::MediaSource as RumaMediaSource, EventContent};
|
||||
|
||||
use super::{content::Reaction, reply::InReplyToDetails};
|
||||
use super::{
|
||||
content::Reaction,
|
||||
reply::{EmbeddedEventDetails, InReplyToDetails},
|
||||
};
|
||||
use crate::{
|
||||
error::ClientError,
|
||||
ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind},
|
||||
@@ -56,10 +59,12 @@ pub enum MsgLikeKind {
|
||||
pub struct MsgLikeContent {
|
||||
pub kind: MsgLikeKind,
|
||||
pub reactions: Vec<Reaction>,
|
||||
/// Event ID of the thread root, if this is a threaded message.
|
||||
pub thread_root: Option<String>,
|
||||
/// The event this message is replying to, if any.
|
||||
pub in_reply_to: Option<Arc<InReplyToDetails>>,
|
||||
/// Event ID of the thread root, if this is a message in a thread.
|
||||
pub thread_root: Option<String>,
|
||||
/// Details about the thread this message is the root of.
|
||||
pub thread_summary: Option<Arc<ThreadSummary>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
@@ -95,6 +100,8 @@ impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
|
||||
|
||||
let thread_root = value.thread_root.map(|id| id.to_string());
|
||||
|
||||
let thread_summary = value.thread_summary.map(|t| Arc::new(t.into()));
|
||||
|
||||
Ok(match value.kind {
|
||||
Kind::Message(message) => {
|
||||
let msg_type = TryInto::<MessageType>::try_into(message.msgtype().clone())
|
||||
@@ -112,6 +119,7 @@ impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
|
||||
reactions,
|
||||
in_reply_to,
|
||||
thread_root,
|
||||
thread_summary,
|
||||
}
|
||||
}
|
||||
Kind::Sticker(sticker) => {
|
||||
@@ -134,6 +142,7 @@ impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
|
||||
reactions,
|
||||
in_reply_to,
|
||||
thread_root,
|
||||
thread_summary,
|
||||
}
|
||||
}
|
||||
Kind::Poll(poll_state) => {
|
||||
@@ -156,16 +165,22 @@ impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
|
||||
reactions,
|
||||
in_reply_to,
|
||||
thread_root,
|
||||
thread_summary,
|
||||
}
|
||||
}
|
||||
Kind::Redacted => {
|
||||
Self { kind: MsgLikeKind::Redacted, reactions, in_reply_to, thread_root }
|
||||
}
|
||||
Kind::Redacted => Self {
|
||||
kind: MsgLikeKind::Redacted,
|
||||
reactions,
|
||||
in_reply_to,
|
||||
thread_root,
|
||||
thread_summary,
|
||||
},
|
||||
Kind::UnableToDecrypt(msg) => Self {
|
||||
kind: MsgLikeKind::UnableToDecrypt { msg: EncryptedMessage::new(&msg) },
|
||||
reactions,
|
||||
in_reply_to,
|
||||
thread_root,
|
||||
thread_summary,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -222,3 +237,29 @@ pub struct PollAnswer {
|
||||
pub id: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct ThreadSummary {
|
||||
pub latest_event: EmbeddedEventDetails,
|
||||
pub num_replies: u32,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl ThreadSummary {
|
||||
pub fn latest_event(&self) -> EmbeddedEventDetails {
|
||||
self.latest_event.clone()
|
||||
}
|
||||
|
||||
pub fn num_replies(&self) -> u64 {
|
||||
self.num_replies as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::ThreadSummary> for ThreadSummary {
|
||||
fn from(value: matrix_sdk_ui::timeline::ThreadSummary) -> Self {
|
||||
Self {
|
||||
latest_event: EmbeddedEventDetails::from(value.latest_event),
|
||||
num_replies: value.num_replies,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,18 +12,19 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk_ui::timeline::TimelineDetails;
|
||||
use matrix_sdk_ui::timeline::{EmbeddedEvent, TimelineDetails};
|
||||
|
||||
use super::{content::TimelineItemContent, ProfileDetails};
|
||||
use crate::{event::EventOrTransactionId, utils::Timestamp};
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct InReplyToDetails {
|
||||
event_id: String,
|
||||
event: RepliedToEventDetails,
|
||||
event: EmbeddedEventDetails,
|
||||
}
|
||||
|
||||
impl InReplyToDetails {
|
||||
pub(crate) fn new(event_id: String, event: RepliedToEventDetails) -> Self {
|
||||
pub(crate) fn new(event_id: String, event: EmbeddedEventDetails) -> Self {
|
||||
Self { event_id, event }
|
||||
}
|
||||
}
|
||||
@@ -34,35 +35,47 @@ impl InReplyToDetails {
|
||||
self.event_id.clone()
|
||||
}
|
||||
|
||||
pub fn event(&self) -> RepliedToEventDetails {
|
||||
pub fn event(&self) -> EmbeddedEventDetails {
|
||||
self.event.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
|
||||
fn from(inner: matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
|
||||
let event_id = inner.event_id.to_string();
|
||||
let event = match &inner.event {
|
||||
TimelineDetails::Unavailable => RepliedToEventDetails::Unavailable,
|
||||
TimelineDetails::Pending => RepliedToEventDetails::Pending,
|
||||
TimelineDetails::Ready(event) => RepliedToEventDetails::Ready {
|
||||
content: event.content().clone().into(),
|
||||
sender: event.sender().to_string(),
|
||||
sender_profile: event.sender_profile().into(),
|
||||
},
|
||||
TimelineDetails::Error(err) => {
|
||||
RepliedToEventDetails::Error { message: err.to_string() }
|
||||
}
|
||||
};
|
||||
|
||||
Self { event_id, event }
|
||||
Self { event_id: inner.event_id.to_string(), event: inner.event.into() }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum RepliedToEventDetails {
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum EmbeddedEventDetails {
|
||||
Unavailable,
|
||||
Pending,
|
||||
Ready { content: TimelineItemContent, sender: String, sender_profile: ProfileDetails },
|
||||
Error { message: String },
|
||||
Ready {
|
||||
content: TimelineItemContent,
|
||||
sender: String,
|
||||
sender_profile: ProfileDetails,
|
||||
timestamp: Timestamp,
|
||||
event_or_transaction_id: EventOrTransactionId,
|
||||
},
|
||||
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(),
|
||||
timestamp: event.timestamp.into(),
|
||||
event_or_transaction_id: event.identifier.into(),
|
||||
},
|
||||
TimelineDetails::Error(err) => EmbeddedEventDetails::Error { message: err.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use tracing_core::{identify_callsite, metadata::Kind as MetadataKind};
|
||||
/// Log an event.
|
||||
///
|
||||
/// The target should be something like a module path, and can be referenced in
|
||||
/// the filter string given to `setup_tracing`. `level` and `target` for a
|
||||
/// the filter string given to `init_platform`. `level` and `target` for a
|
||||
/// callsite are fixed at the first `log_event` call for that callsite and can
|
||||
/// not be changed afterwards, i.e. the level and target passed for second and
|
||||
/// following `log_event`s with the same callsite will be ignored.
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{fmt::Debug, sync::Arc, time::Duration};
|
||||
|
||||
use matrix_sdk::crypto::types::events::UtdCause;
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::unable_to_decrypt_hook::{
|
||||
UnableToDecryptHook, UnableToDecryptInfo as SdkUnableToDecryptInfo,
|
||||
};
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait UnableToDecryptDelegate: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_utd(&self, info: UnableToDecryptInfo);
|
||||
}
|
||||
|
||||
pub struct UtdHook {
|
||||
pub delegate: Arc<dyn UnableToDecryptDelegate>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for UtdHook {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("UtdHook").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl UnableToDecryptHook for UtdHook {
|
||||
fn on_utd(&self, info: SdkUnableToDecryptInfo) {
|
||||
const IGNORE_UTD_PERIOD: Duration = Duration::from_secs(4);
|
||||
|
||||
// UTDs that have been decrypted in the `IGNORE_UTD_PERIOD` are just ignored and
|
||||
// not considered UTDs.
|
||||
if let Some(duration) = &info.time_to_decrypt {
|
||||
if *duration < IGNORE_UTD_PERIOD {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Report the UTD to the client.
|
||||
self.delegate.on_utd(info.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct UnableToDecryptInfo {
|
||||
/// The identifier of the event that couldn't get decrypted.
|
||||
event_id: String,
|
||||
|
||||
/// If the event could be decrypted late (that is, the event was encrypted
|
||||
/// at first, but could be decrypted later on), then this indicates the
|
||||
/// time it took to decrypt the event. If it is not set, this is
|
||||
/// considered a definite UTD.
|
||||
///
|
||||
/// If set, this is in milliseconds.
|
||||
pub time_to_decrypt_ms: Option<u64>,
|
||||
|
||||
/// What we know about what caused this UTD. E.g. was this event sent when
|
||||
/// we were not a member of this room?
|
||||
pub cause: UtdCause,
|
||||
|
||||
/// The difference between the event creation time (`origin_server_ts`) and
|
||||
/// the time our device was created. If negative, this event was sent
|
||||
/// *before* our device was created.
|
||||
pub event_local_age_millis: i64,
|
||||
|
||||
/// Whether the user had verified their own identity at the point they
|
||||
/// received the UTD event.
|
||||
pub user_trusts_own_identity: bool,
|
||||
|
||||
/// The homeserver of the user that sent the undecryptable event.
|
||||
pub sender_homeserver: String,
|
||||
|
||||
/// Our local user's own homeserver, or `None` if the client is not logged
|
||||
/// in.
|
||||
pub own_homeserver: Option<String>,
|
||||
}
|
||||
|
||||
impl From<SdkUnableToDecryptInfo> for UnableToDecryptInfo {
|
||||
fn from(value: SdkUnableToDecryptInfo) -> Self {
|
||||
Self {
|
||||
event_id: value.event_id.to_string(),
|
||||
time_to_decrypt_ms: value.time_to_decrypt.map(|ttd| ttd.as_millis() as u64),
|
||||
cause: value.cause,
|
||||
event_local_age_millis: value.event_local_age_millis,
|
||||
user_trusts_own_identity: value.user_trusts_own_identity,
|
||||
sender_homeserver: value.sender_homeserver.to_string(),
|
||||
own_homeserver: value.own_homeserver.map(String::from),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,11 @@
|
||||
|
||||
use std::{mem::ManuallyDrop, ops::Deref};
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use ruma::{MilliSecondsSinceUnixEpoch, UInt};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::runtime::get_runtime_handle;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Timestamp(u64);
|
||||
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use language_tags::LanguageTag;
|
||||
use matrix_sdk::{
|
||||
async_trait,
|
||||
widget::{MessageLikeEventFilter, StateEventFilter},
|
||||
};
|
||||
use matrix_sdk::widget::{MessageLikeEventFilter, StateEventFilter, ToDeviceEventFilter};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::events::MessageLikeEventType;
|
||||
use tracing::error;
|
||||
|
||||
use crate::room::Room;
|
||||
use crate::{room::Room, runtime::get_runtime_handle};
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct WidgetDriverAndHandle {
|
||||
@@ -95,7 +92,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]
|
||||
@@ -113,168 +110,6 @@ pub async fn generate_webview_url(
|
||||
.map(|url| url.to_string())?)
|
||||
}
|
||||
|
||||
/// Defines if a call is encrypted and which encryption system should be used.
|
||||
///
|
||||
/// This controls the url parameters: `perParticipantE2EE`, `password`.
|
||||
#[derive(uniffi::Enum, Clone)]
|
||||
pub enum EncryptionSystem {
|
||||
/// Equivalent to the element call url parameter: `enableE2EE=false`
|
||||
Unencrypted,
|
||||
/// Equivalent to the element call url parameter:
|
||||
/// `perParticipantE2EE=true`
|
||||
PerParticipantKeys,
|
||||
/// Equivalent to the element call url parameter:
|
||||
/// `password={secret}`
|
||||
SharedSecret {
|
||||
/// The secret/password which is used in the url.
|
||||
secret: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<EncryptionSystem> for matrix_sdk::widget::EncryptionSystem {
|
||||
fn from(value: EncryptionSystem) -> Self {
|
||||
match value {
|
||||
EncryptionSystem::Unencrypted => Self::Unencrypted,
|
||||
EncryptionSystem::PerParticipantKeys => Self::PerParticipantKeys,
|
||||
EncryptionSystem::SharedSecret { secret } => Self::SharedSecret { secret },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 Element Call app including any `/room` path if required.
|
||||
///
|
||||
/// E.g. <https://call.element.io>, <https://call.element.dev>, <https://call.element.dev/room>
|
||||
pub element_call_url: String,
|
||||
|
||||
/// The widget id.
|
||||
pub widget_id: String,
|
||||
|
||||
/// The url that is used as the target for the PostMessages sent
|
||||
/// by the widget (to the client).
|
||||
///
|
||||
/// For a web app client this is the client url. In case of using other
|
||||
/// platforms the client most likely is setup up to listen to
|
||||
/// postmessages in the same webview the widget is hosted. In this case
|
||||
/// the `parent_url` is set to the url of the webview with the widget. Be
|
||||
/// aware that this means that the widget will receive its own postmessage
|
||||
/// messages. The `matrix-widget-api` (js) ignores those so this works but
|
||||
/// it might break custom implementations.
|
||||
///
|
||||
/// Defaults to `element_call_url` for the non-iframe (dedicated webview)
|
||||
/// usecase.
|
||||
pub parent_url: Option<String>,
|
||||
|
||||
/// Whether the branding header of Element call should be hidden.
|
||||
///
|
||||
/// Default: `true`
|
||||
pub hide_header: Option<bool>,
|
||||
|
||||
/// If set, the lobby will be skipped and the widget will join the
|
||||
/// call on the `io.element.join` action.
|
||||
///
|
||||
/// Default: `false`
|
||||
pub preload: Option<bool>,
|
||||
|
||||
/// The font scale which will be used inside element call.
|
||||
///
|
||||
/// Default: `1`
|
||||
pub font_scale: Option<f64>,
|
||||
|
||||
/// Whether element call should prompt the user to open in the browser or
|
||||
/// the app.
|
||||
///
|
||||
/// Default: `false`
|
||||
pub app_prompt: Option<bool>,
|
||||
|
||||
/// Make it not possible to get to the calls list in the webview.
|
||||
///
|
||||
/// Default: `true`
|
||||
pub confine_to_room: Option<bool>,
|
||||
|
||||
/// The font to use, to adapt to the system font.
|
||||
pub font: 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>,
|
||||
}
|
||||
|
||||
impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElementCallWidgetOptions {
|
||||
fn from(value: VirtualElementCallWidgetOptions) -> Self {
|
||||
Self {
|
||||
element_call_url: value.element_call_url,
|
||||
widget_id: value.widget_id,
|
||||
parent_url: value.parent_url,
|
||||
hide_header: value.hide_header,
|
||||
preload: value.preload,
|
||||
font_scale: value.font_scale,
|
||||
app_prompt: value.app_prompt,
|
||||
confine_to_room: value.confine_to_room,
|
||||
font: value.font,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `WidgetSettings` are usually created from a state event.
|
||||
/// (currently unimplemented)
|
||||
///
|
||||
@@ -290,9 +125,9 @@ impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElemen
|
||||
/// call widget.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn new_virtual_element_call_widget(
|
||||
props: VirtualElementCallWidgetOptions,
|
||||
props: matrix_sdk::widget::VirtualElementCallWidgetOptions,
|
||||
) -> Result<WidgetSettings, ParseError> {
|
||||
Ok(matrix_sdk::widget::WidgetSettings::new_virtual_element_call_widget(props.into())
|
||||
Ok(matrix_sdk::widget::WidgetSettings::new_virtual_element_call_widget(props)
|
||||
.map(|w| w.into())?)
|
||||
}
|
||||
|
||||
@@ -321,7 +156,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(),
|
||||
},
|
||||
@@ -344,6 +181,8 @@ pub fn get_element_call_required_permissions(
|
||||
read: vec![
|
||||
// To compute the current state of the matrixRTC session.
|
||||
WidgetEventFilter::StateWithType { event_type: StateEventType::CallMember.to_string() },
|
||||
// To display the name of the room.
|
||||
WidgetEventFilter::StateWithType { event_type: StateEventType::RoomName.to_string() },
|
||||
// To detect leaving/kicked room members during a call.
|
||||
WidgetEventFilter::StateWithType { event_type: StateEventType::RoomMember.to_string() },
|
||||
// To decide whether to encrypt the call streams based on the room encryption setting.
|
||||
@@ -373,7 +212,7 @@ pub fn get_element_call_required_permissions(
|
||||
state_key: format!("{own_user_id}_{own_device_id}"),
|
||||
},
|
||||
// The same as above but with an underscore.
|
||||
// To work around the issue that state events starting with `@` have to be matrix id's
|
||||
// To work around the issue that state events starting with `@` have to be Matrix id's
|
||||
// but we use mxId+deviceId.
|
||||
WidgetEventFilter::StateWithTypeAndStateKey {
|
||||
event_type: StateEventType::CallMember.to_string(),
|
||||
@@ -442,7 +281,7 @@ pub struct WidgetCapabilities {
|
||||
/// Types of the messages that a widget wants to be able to send.
|
||||
pub send: Vec<WidgetEventFilter>,
|
||||
/// If this capability is requested by the widget, it can not operate
|
||||
/// separately from the matrix client.
|
||||
/// separately from the Matrix client.
|
||||
///
|
||||
/// This means clients should not offer to open the widget in a separate
|
||||
/// browser/tab/webview that is not connected to the postmessage widget-api.
|
||||
@@ -488,9 +327,11 @@ pub enum WidgetEventFilter {
|
||||
StateWithType { event_type: String },
|
||||
/// Matches state events with the given `type` and `state_key`.
|
||||
StateWithTypeAndStateKey { event_type: String, state_key: String },
|
||||
/// Matches to-device events with the given `event_type`.
|
||||
ToDevice { event_type: String },
|
||||
}
|
||||
|
||||
impl From<WidgetEventFilter> for matrix_sdk::widget::EventFilter {
|
||||
impl From<WidgetEventFilter> for matrix_sdk::widget::Filter {
|
||||
fn from(value: WidgetEventFilter) -> Self {
|
||||
match value {
|
||||
WidgetEventFilter::MessageLikeWithType { event_type } => {
|
||||
@@ -505,13 +346,16 @@ impl From<WidgetEventFilter> for matrix_sdk::widget::EventFilter {
|
||||
WidgetEventFilter::StateWithTypeAndStateKey { event_type, state_key } => {
|
||||
Self::State(StateEventFilter::WithTypeAndStateKey(event_type.into(), state_key))
|
||||
}
|
||||
WidgetEventFilter::ToDevice { event_type } => {
|
||||
Self::ToDevice(ToDeviceEventFilter { event_type: event_type.into() })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::widget::EventFilter> for WidgetEventFilter {
|
||||
fn from(value: matrix_sdk::widget::EventFilter) -> Self {
|
||||
use matrix_sdk::widget::EventFilter as F;
|
||||
impl From<matrix_sdk::widget::Filter> for WidgetEventFilter {
|
||||
fn from(value: matrix_sdk::widget::Filter) -> Self {
|
||||
use matrix_sdk::widget::Filter as F;
|
||||
|
||||
match value {
|
||||
F::MessageLike(MessageLikeEventFilter::WithType(event_type)) => {
|
||||
@@ -526,18 +370,20 @@ 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]
|
||||
impl matrix_sdk::widget::CapabilitiesProvider for CapabilitiesProviderWrap {
|
||||
async fn acquire_capabilities(
|
||||
&self,
|
||||
@@ -631,8 +477,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."
|
||||
);
|
||||
};
|
||||
|
||||
@@ -640,14 +485,21 @@ mod tests {
|
||||
cap_assert("org.matrix.msc4157.update_delayed_event");
|
||||
cap_assert("org.matrix.msc4157.send.delayed_event");
|
||||
cap_assert("org.matrix.msc2762.receive.state_event:org.matrix.msc3401.call.member");
|
||||
cap_assert("org.matrix.msc2762.receive.state_event:m.room.name");
|
||||
cap_assert("org.matrix.msc2762.receive.state_event:m.room.member");
|
||||
cap_assert("org.matrix.msc2762.receive.state_event:m.room.encryption");
|
||||
cap_assert("org.matrix.msc2762.receive.event:org.matrix.rageshake_request");
|
||||
cap_assert("org.matrix.msc2762.receive.event:io.element.call.encryption_keys");
|
||||
cap_assert("org.matrix.msc2762.receive.state_event:m.room.create");
|
||||
cap_assert("org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org");
|
||||
cap_assert("org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org_ABCDEFGHI");
|
||||
cap_assert("org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#_@my_user:my_domain.org_ABCDEFGHI");
|
||||
cap_assert(
|
||||
"org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org",
|
||||
);
|
||||
cap_assert(
|
||||
"org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org_ABCDEFGHI",
|
||||
);
|
||||
cap_assert(
|
||||
"org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#_@my_user:my_domain.org_ABCDEFGHI",
|
||||
);
|
||||
cap_assert("org.matrix.msc2762.send.event:org.matrix.rageshake_request");
|
||||
cap_assert("org.matrix.msc2762.send.event:io.element.call.encryption_keys");
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -6,6 +6,34 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
## [0.13.0] - 2025-07-10
|
||||
|
||||
### Features
|
||||
- The `RoomInfo` now remembers when an invite was explicitly accepted when the
|
||||
`BaseClient::room_joined()` method was called. A new getter for this
|
||||
timestamp exists, the `RoomInfo::invite_accepted_at()` method returns this
|
||||
timestamp.
|
||||
([#5333](https://github.com/matrix-org/matrix-rust-sdk/pull/5333))
|
||||
- [**breaking**] The `BaseClient::new()` method now takes an additional `ThreadingSupport`
|
||||
parameter controlling whether the client is supposed to do extra processing for threads. Right
|
||||
now, it controls whether to exclude in-thread events from the room unread counts, but it may be
|
||||
expanded in the future to support more threading-related features.
|
||||
([#5325](https://github.com/matrix-org/matrix-rust-sdk/pull/5325))
|
||||
|
||||
### Refactor
|
||||
|
||||
- The cached `ServerCapabilities` has been renamed to `ServerInfo` and
|
||||
additionally contains the well-known response alongside the existing server versions.
|
||||
Despite the old name, it does not contain the server capabilities.
|
||||
([#5167](https://github.com/matrix-org/matrix-rust-sdk/pull/5167))
|
||||
- `Room::join_rule` and `Room::is_public` now return an `Option` to reflect that the join rule
|
||||
state event might be missing, in which case they will return `None`.
|
||||
([#5278](https://github.com/matrix-org/matrix-rust-sdk/pull/5278))
|
||||
|
||||
## [0.12.0] - 2025-06-10
|
||||
|
||||
No notable changes in this release.
|
||||
|
||||
## [0.11.0] - 2025-04-11
|
||||
|
||||
### Features
|
||||
@@ -40,6 +68,14 @@ All notable changes to this project will be documented in this file.
|
||||
- [**breaking**] `BaseClient::set_session_metadata` is renamed
|
||||
`activate`, and `BaseClient::logged_in` is renamed `is_activated`
|
||||
([#4850](https://github.com/matrix-org/matrix-rust-sdk/pull/4850))
|
||||
- [**breaking] `DependentQueuedRequestKind::UploadFileWithThumbnail`
|
||||
was renamed to `DependentQueuedRequestKind::UploadFileOrThumbnail`.
|
||||
Under the `unstable-msc4274` feature, `DependentQueuedRequestKind::UploadFileOrThumbnail`
|
||||
and `SentMediaInfo` were generalized to allow chaining multiple dependent
|
||||
file / thumbnail uploads.
|
||||
([#4897](https://github.com/matrix-org/matrix-rust-sdk/pull/4897))
|
||||
- [**breaking**] `RoomInfo::prev_state` has been removed due to being useless.
|
||||
([#5054](https://github.com/matrix-org/matrix-rust-sdk/pull/5054))
|
||||
|
||||
## [0.10.0] - 2025-02-04
|
||||
|
||||
|
||||
@@ -8,19 +8,25 @@ license = "Apache-2.0"
|
||||
name = "matrix-sdk-base"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = { workspace = true }
|
||||
version = "0.11.0"
|
||||
rust-version.workspace = true
|
||||
version = "0.13.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
rustdoc-args = ["--cfg", "docsrs", "--generate-link-to-definition"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
e2e-encryption = ["dep:matrix-sdk-crypto"]
|
||||
js = ["matrix-sdk-common/js", "matrix-sdk-crypto?/js", "ruma/js", "matrix-sdk-store-encryption/js"]
|
||||
js = [
|
||||
"matrix-sdk-common/js",
|
||||
"matrix-sdk-crypto?/js",
|
||||
"ruma/js",
|
||||
"matrix-sdk-store-encryption/js",
|
||||
]
|
||||
qrcode = ["matrix-sdk-crypto?/qrcode"]
|
||||
automatic-room-key-forwarding = ["matrix-sdk-crypto?/automatic-room-key-forwarding"]
|
||||
experimental-send-custom-to-device = ["matrix-sdk-crypto?/experimental-send-custom-to-device"]
|
||||
uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"]
|
||||
|
||||
# Private feature, see
|
||||
@@ -43,23 +49,26 @@ testing = [
|
||||
"matrix-sdk-crypto?/testing",
|
||||
]
|
||||
|
||||
# Add support for inline media galleries via msgtypes
|
||||
unstable-msc4274 = []
|
||||
|
||||
[dependencies]
|
||||
as_variant = { workspace = true }
|
||||
as_variant.workspace = true
|
||||
assert_matches = { workspace = true, optional = true }
|
||||
assert_matches2 = { workspace = true, optional = true }
|
||||
async-trait = { workspace = true }
|
||||
bitflags = { version = "2.8.0", features = ["serde"] }
|
||||
decancer = "3.2.8"
|
||||
async-trait.workspace = true
|
||||
bitflags = { workspace = true, features = ["serde"] }
|
||||
decancer = "3.3.0"
|
||||
eyeball = { workspace = true, features = ["async-lock"] }
|
||||
eyeball-im = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
growable-bloom-filter = { workspace = true }
|
||||
eyeball-im.workspace = true
|
||||
futures-util.workspace = true
|
||||
growable-bloom-filter.workspace = true
|
||||
http = { workspace = true, optional = true }
|
||||
matrix-sdk-common = { workspace = true }
|
||||
matrix-sdk-common.workspace = true
|
||||
matrix-sdk-crypto = { workspace = true, optional = true }
|
||||
matrix-sdk-store-encryption = { workspace = true }
|
||||
matrix-sdk-store-encryption.workspace = true
|
||||
matrix-sdk-test = { workspace = true, optional = true }
|
||||
once_cell = { workspace = true }
|
||||
once_cell.workspace = true
|
||||
regex = "1.11.1"
|
||||
ruma = { workspace = true, features = [
|
||||
"canonical-json",
|
||||
@@ -68,29 +77,30 @@ ruma = { workspace = true, features = [
|
||||
"unstable-msc4186",
|
||||
"rand",
|
||||
] }
|
||||
unicode-normalization = { workspace = true }
|
||||
serde = { workspace = true, features = ["rc"] }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
unicode-normalization.workspace = true
|
||||
uniffi = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = { workspace = true }
|
||||
assert_matches2 = { workspace = true }
|
||||
anyhow.workspace = true
|
||||
assert_matches.workspace = true
|
||||
assert_matches2.workspace = true
|
||||
assign = "1.1.1"
|
||||
futures-executor = { workspace = true }
|
||||
http = { workspace = true }
|
||||
matrix-sdk-test = { workspace = true }
|
||||
stream_assert = { workspace = true }
|
||||
similar-asserts = { workspace = true }
|
||||
futures-executor.workspace = true
|
||||
http.workspace = true
|
||||
matrix-sdk-test.workspace = true
|
||||
similar-asserts.workspace = true
|
||||
stream_assert.workspace = true
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
[target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
wasm-bindgen-test = { workspace = true }
|
||||
[target.'cfg(target_family = "wasm")'.dev-dependencies]
|
||||
wasm-bindgen-test.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
fmt, iter,
|
||||
fmt,
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
@@ -26,8 +26,8 @@ use eyeball_im::{Vector, VectorDiff};
|
||||
use futures_util::Stream;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_crypto::{
|
||||
store::DynCryptoStore, types::requests::ToDeviceRequest, CollectStrategy, EncryptionSettings,
|
||||
OlmError, OlmMachine, TrustRequirement,
|
||||
store::DynCryptoStore, types::requests::ToDeviceRequest, CollectStrategy, DecryptionSettings,
|
||||
EncryptionSettings, OlmError, OlmMachine, TrustRequirement,
|
||||
};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::events::room::{history_visibility::HistoryVisibility, member::MembershipState};
|
||||
@@ -36,37 +36,36 @@ use ruma::DeviceId;
|
||||
use ruma::{
|
||||
api::client::{self as api, sync::sync_events::v5},
|
||||
events::{
|
||||
ignored_user_list::IgnoredUserListEventContent,
|
||||
push_rules::{PushRulesEvent, PushRulesEventContent},
|
||||
room::member::SyncRoomMemberEvent,
|
||||
AnyStrippedStateEvent, AnySyncEphemeralRoomEvent, StateEvent, StateEventType,
|
||||
StateEvent, StateEventType,
|
||||
},
|
||||
push::{Action, Ruleset},
|
||||
serde::Raw,
|
||||
push::Ruleset,
|
||||
time::Instant,
|
||||
OwnedRoomId, OwnedUserId, RoomId,
|
||||
OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
use tokio::sync::{broadcast, Mutex};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
use tracing::{debug, info, instrument};
|
||||
use tracing::{debug, enabled, info, instrument, warn, Level};
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use crate::RoomMemberships;
|
||||
use crate::{
|
||||
deserialized_responses::{DisplayName, RawAnySyncOrStrippedTimelineEvent},
|
||||
deserialized_responses::DisplayName,
|
||||
error::{Error, Result},
|
||||
event_cache::store::EventCacheStoreLock,
|
||||
response_processors::{self as processors, Context},
|
||||
rooms::{
|
||||
normal::{RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMembersUpdate},
|
||||
Room, RoomInfo, RoomState,
|
||||
room::{
|
||||
Room, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMembersUpdate, RoomState,
|
||||
},
|
||||
store::{
|
||||
ambiguity_map::AmbiguityCache, BaseStateStore, DynStateStore, MemoryStore,
|
||||
Result as StoreResult, RoomLoadSettings, StateChanges, StateStoreDataKey,
|
||||
StateStoreDataValue, StateStoreExt, StoreConfig,
|
||||
},
|
||||
sync::{JoinedRoomUpdate, LeftRoomUpdate, Notification, RoomUpdates, SyncResponse},
|
||||
sync::{RoomUpdates, SyncResponse},
|
||||
RoomStateFilter, SessionMeta,
|
||||
};
|
||||
|
||||
@@ -77,11 +76,12 @@ use crate::{
|
||||
/// rather through `matrix_sdk::Client`.
|
||||
///
|
||||
/// ```rust
|
||||
/// use matrix_sdk_base::{store::StoreConfig, BaseClient};
|
||||
/// use matrix_sdk_base::{store::StoreConfig, BaseClient, ThreadingSupport};
|
||||
///
|
||||
/// let client = BaseClient::new(StoreConfig::new(
|
||||
/// "cross-process-holder-name".to_owned(),
|
||||
/// ));
|
||||
/// let client = BaseClient::new(
|
||||
/// StoreConfig::new("cross-process-holder-name".to_owned()),
|
||||
/// ThreadingSupport::Disabled,
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct BaseClient {
|
||||
@@ -116,13 +116,16 @@ pub struct BaseClient {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub room_key_recipient_strategy: CollectStrategy,
|
||||
|
||||
/// The trust requirement to use for decrypting events.
|
||||
/// The settings to use for decrypting events.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub decryption_trust_requirement: TrustRequirement,
|
||||
pub decryption_settings: DecryptionSettings,
|
||||
|
||||
/// If the client should handle verification events received when syncing.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub handle_verification_events: bool,
|
||||
|
||||
/// Whether the client supports threads or not.
|
||||
pub threading_support: ThreadingSupport,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
@@ -135,6 +138,25 @@ impl fmt::Debug for BaseClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this client instance supports threading or not. Currently used to
|
||||
/// determine how the client handles read receipts and unread count computations
|
||||
/// on the base SDK level.
|
||||
///
|
||||
/// Timelines on the other hand have a separate `TimelineFocus`
|
||||
/// `hide_threaded_events` associated value that can be used to hide threaded
|
||||
/// events but also to enable threaded read receipt sending. This is because
|
||||
/// certain timeline instances should ignore threading no matter what's defined
|
||||
/// at the client level. One such example are media filtered timelines which
|
||||
/// should contain all the room's media no matter what thread its in (unless
|
||||
/// explicitly opted into).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ThreadingSupport {
|
||||
/// Threading enabled
|
||||
Enabled,
|
||||
/// Threading disabled
|
||||
Disabled,
|
||||
}
|
||||
|
||||
impl BaseClient {
|
||||
/// Create a new client.
|
||||
///
|
||||
@@ -142,7 +164,7 @@ impl BaseClient {
|
||||
///
|
||||
/// * `config` - the configuration for the stores (state store, event cache
|
||||
/// store and crypto store).
|
||||
pub fn new(config: StoreConfig) -> Self {
|
||||
pub fn new(config: StoreConfig, threading_support: ThreadingSupport) -> Self {
|
||||
let store = BaseStateStore::new(config.state_store);
|
||||
|
||||
// Create the channel to receive `RoomInfoNotableUpdate`.
|
||||
@@ -169,9 +191,12 @@ impl BaseClient {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
room_key_recipient_strategy: Default::default(),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
decryption_trust_requirement: TrustRequirement::Untrusted,
|
||||
decryption_settings: DecryptionSettings {
|
||||
sender_device_trust_requirement: TrustRequirement::Untrusted,
|
||||
},
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
handle_verification_events: true,
|
||||
threading_support,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,8 +226,9 @@ impl BaseClient {
|
||||
ignore_user_list_changes: Default::default(),
|
||||
room_info_notable_update_sender: self.room_info_notable_update_sender.clone(),
|
||||
room_key_recipient_strategy: self.room_key_recipient_strategy.clone(),
|
||||
decryption_trust_requirement: self.decryption_trust_requirement,
|
||||
decryption_settings: self.decryption_settings.clone(),
|
||||
handle_verification_events,
|
||||
threading_support: self.threading_support,
|
||||
};
|
||||
|
||||
copy.state_store
|
||||
@@ -223,7 +249,7 @@ impl BaseClient {
|
||||
) -> Result<Self> {
|
||||
let config = StoreConfig::new(cross_process_store_locks_holder.to_owned())
|
||||
.state_store(MemoryStore::new());
|
||||
Ok(Self::new(config))
|
||||
Ok(Self::new(config, ThreadingSupport::Disabled))
|
||||
}
|
||||
|
||||
/// Get the session meta information.
|
||||
@@ -367,65 +393,6 @@ impl BaseClient {
|
||||
self.state_store.sync_token.read().await.clone()
|
||||
}
|
||||
|
||||
/// Handles the stripped state events in `invite_state`, modifying the
|
||||
/// room's info and posting notifications as needed.
|
||||
///
|
||||
/// * `room` - The [`Room`] to modify.
|
||||
/// * `events` - The contents of `invite_state` in the form of list of pairs
|
||||
/// of raw stripped state events with their deserialized counterpart.
|
||||
/// * `push_rules` - The push rules for this room.
|
||||
/// * `room_info` - The current room's info.
|
||||
/// * `changes` - The accumulated list of changes to apply once the
|
||||
/// processing is finished.
|
||||
/// * `notifications` - Notifications to post for the current room.
|
||||
#[instrument(skip_all, fields(room_id = ?room_info.room_id))]
|
||||
pub(crate) async fn handle_invited_state(
|
||||
&self,
|
||||
context: &mut Context,
|
||||
room: &Room,
|
||||
events: (Vec<Raw<AnyStrippedStateEvent>>, Vec<AnyStrippedStateEvent>),
|
||||
push_rules: &Ruleset,
|
||||
room_info: &mut RoomInfo,
|
||||
notifications: &mut BTreeMap<OwnedRoomId, Vec<Notification>>,
|
||||
) -> Result<()> {
|
||||
let mut state_events = BTreeMap::new();
|
||||
|
||||
for (raw_event, event) in iter::zip(events.0, events.1) {
|
||||
room_info.handle_stripped_state_event(&event);
|
||||
state_events
|
||||
.entry(event.event_type())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(event.state_key().to_owned(), raw_event);
|
||||
}
|
||||
|
||||
context
|
||||
.state_changes
|
||||
.stripped_state
|
||||
.insert(room_info.room_id().to_owned(), state_events.clone());
|
||||
|
||||
// We need to check for notifications after we have handled all state
|
||||
// events, to make sure we have the full push context.
|
||||
if let Some(push_context) =
|
||||
processors::timeline::get_push_room_context(context, room, room_info, &self.state_store)
|
||||
.await?
|
||||
{
|
||||
// Check every event again for notification.
|
||||
for event in state_events.values().flat_map(|map| map.values()) {
|
||||
let actions = push_rules.get_actions(event, &push_context);
|
||||
if actions.iter().any(Action::should_notify) {
|
||||
notifications.entry(room.room_id().to_owned()).or_default().push(
|
||||
Notification {
|
||||
actions: actions.to_owned(),
|
||||
event: RawAnySyncOrStrippedTimelineEvent::Stripped(event.clone()),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// User has knocked on a room.
|
||||
///
|
||||
/// Update the internal and cached state accordingly. Return the final Room.
|
||||
@@ -452,9 +419,33 @@ impl BaseClient {
|
||||
Ok(room)
|
||||
}
|
||||
|
||||
/// User has joined a room.
|
||||
/// The user has joined a room using this specific client.
|
||||
///
|
||||
/// This method should be called if the user accepts an invite or if they
|
||||
/// join a public room.
|
||||
///
|
||||
/// The method will create a [`Room`] object if one does not exist yet and
|
||||
/// set the state of the [`Room`] to [`RoomState::Joined`]. The [`Room`]
|
||||
/// object will be persisted in the cache. Please note that the [`Room`]
|
||||
/// will be a stub until a sync has been received with the full room
|
||||
/// state using [`BaseClient::receive_sync_response`].
|
||||
///
|
||||
/// Update the internal and cached state accordingly. Return the final Room.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use matrix_sdk_base::{BaseClient, store::StoreConfig, RoomState, ThreadingSupport};
|
||||
/// # use ruma::OwnedRoomId;
|
||||
/// # async {
|
||||
/// # let client = BaseClient::new(StoreConfig::new("example".to_owned()), ThreadingSupport::Disabled);
|
||||
/// # async fn send_join_request() -> anyhow::Result<OwnedRoomId> { todo!() }
|
||||
/// let room_id = send_join_request().await?;
|
||||
/// let room = client.room_joined(&room_id).await?;
|
||||
///
|
||||
/// assert_eq!(room.state(), RoomState::Joined);
|
||||
/// # anyhow::Ok(()) };
|
||||
/// ```
|
||||
pub async fn room_joined(&self, room_id: &RoomId) -> Result<Room> {
|
||||
let room = self.state_store.get_or_create_room(
|
||||
room_id,
|
||||
@@ -462,16 +453,36 @@ impl BaseClient {
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
);
|
||||
|
||||
// If the state isn't `RoomState::Joined` then this means that we knew about
|
||||
// this room before. Let's modify the existing state now.
|
||||
if room.state() != RoomState::Joined {
|
||||
let _sync_lock = self.sync_lock().lock().await;
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
|
||||
// If our previous state was an invite and we're now in the joined state, this
|
||||
// means that the user has explicitly accepted the invite. Let's
|
||||
// remember when this has happened.
|
||||
//
|
||||
// This is somewhat of a workaround for our lack of cryptographic membership.
|
||||
// Later on we will decide if historic room keys should be accepted
|
||||
// based on this info. If a user has accepted an invite and we receive a room
|
||||
// key bundle shortly after, we might accept it. If we don't do
|
||||
// this, the homeserver could trick us into accepting any historic room key
|
||||
// bundle.
|
||||
if room.state() == RoomState::Invited {
|
||||
room_info.set_invite_accepted_now();
|
||||
}
|
||||
|
||||
room_info.mark_as_joined();
|
||||
room_info.mark_state_partially_synced();
|
||||
room_info.mark_members_missing(); // the own member event changed
|
||||
|
||||
let mut changes = StateChanges::default();
|
||||
changes.add_room(room_info.clone());
|
||||
|
||||
self.state_store.save_changes(&changes).await?; // Update the store
|
||||
|
||||
room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
|
||||
}
|
||||
|
||||
@@ -546,25 +557,19 @@ impl BaseClient {
|
||||
return Ok(SyncResponse::default());
|
||||
}
|
||||
|
||||
let now = Instant::now();
|
||||
let now = if enabled!(Level::INFO) { Some(Instant::now()) } else { None };
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
let olm_machine = self.olm_machine().await;
|
||||
|
||||
let mut context =
|
||||
Context::new(StateChanges::new(response.next_batch.clone()), Default::default());
|
||||
let mut context = Context::new(StateChanges::new(response.next_batch.clone()));
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
let to_device = {
|
||||
let processors::e2ee::to_device::Output {
|
||||
decrypted_to_device_events: to_device,
|
||||
processed_to_device_events: to_device,
|
||||
room_key_updates,
|
||||
} = processors::e2ee::to_device::from_sync_v2(
|
||||
&mut context,
|
||||
&response,
|
||||
olm_machine.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
} = processors::e2ee::to_device::from_sync_v2(&response, olm_machine.as_ref()).await?;
|
||||
|
||||
processors::latest_event::decrypt_from_rooms(
|
||||
&mut context,
|
||||
@@ -573,9 +578,11 @@ impl BaseClient {
|
||||
.flatten()
|
||||
.filter_map(|room_key_info| self.get_room(&room_key_info.room_id))
|
||||
.collect(),
|
||||
olm_machine.as_ref(),
|
||||
self.decryption_trust_requirement,
|
||||
self.handle_verification_events,
|
||||
processors::e2ee::E2EE::new(
|
||||
olm_machine.as_ref(),
|
||||
&self.decryption_settings,
|
||||
self.handle_verification_events,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -583,7 +590,22 @@ impl BaseClient {
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "e2e-encryption"))]
|
||||
let to_device = response.to_device.events;
|
||||
let to_device = response
|
||||
.to_device
|
||||
.events
|
||||
.into_iter()
|
||||
.map(|raw| {
|
||||
if let Ok(Some(event_type)) = raw.get_field::<String>("type") {
|
||||
if event_type == "m.room.encrypted" {
|
||||
matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent::UnableToDecrypt(raw)
|
||||
} else {
|
||||
matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent::PlainText(raw)
|
||||
}
|
||||
} else {
|
||||
matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent::Invalid(raw) // Exclude events with no type
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut ambiguity_cache = AmbiguityCache::new(self.state_store.inner.clone());
|
||||
|
||||
@@ -592,285 +614,99 @@ impl BaseClient {
|
||||
|
||||
let push_rules = self.get_push_rules(&global_account_data_processor).await?;
|
||||
|
||||
let mut new_rooms = RoomUpdates::default();
|
||||
let mut room_updates = RoomUpdates::default();
|
||||
let mut notifications = Default::default();
|
||||
|
||||
let mut updated_members_in_room: BTreeMap<OwnedRoomId, BTreeSet<OwnedUserId>> =
|
||||
BTreeMap::new();
|
||||
|
||||
for (room_id, new_info) in response.rooms.join {
|
||||
let room = self.state_store.get_or_create_room(
|
||||
&room_id,
|
||||
RoomState::Joined,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
);
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
|
||||
room_info.mark_as_joined();
|
||||
room_info.update_from_ruma_summary(&new_info.summary);
|
||||
room_info.set_prev_batch(new_info.timeline.prev_batch.as_deref());
|
||||
room_info.mark_state_fully_synced();
|
||||
room_info.handle_encryption_state(requested_required_states.for_room(&room_id));
|
||||
|
||||
let (raw_state_events, state_events) =
|
||||
processors::state_events::sync::collect(&mut context, &new_info.state.events);
|
||||
|
||||
let mut new_user_ids = processors::state_events::dispatch_and_get_new_users(
|
||||
for (room_id, joined_room) in response.rooms.join {
|
||||
let joined_room_update = processors::room::sync_v2::update_joined_room(
|
||||
&mut context,
|
||||
(&raw_state_events, &state_events),
|
||||
&mut room_info,
|
||||
&mut ambiguity_cache,
|
||||
)
|
||||
.await?;
|
||||
|
||||
for raw in &new_info.ephemeral.events {
|
||||
match raw.deserialize() {
|
||||
Ok(AnySyncEphemeralRoomEvent::Receipt(event)) => {
|
||||
context.state_changes.add_receipts(&room_id, event.content);
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let event_id: Option<String> = raw.get_field("event_id").ok().flatten();
|
||||
#[rustfmt::skip]
|
||||
info!(
|
||||
?room_id, event_id,
|
||||
"Failed to deserialize ephemeral room event: {e}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if new_info.timeline.limited {
|
||||
room_info.mark_members_missing();
|
||||
}
|
||||
|
||||
let (raw_state_events_from_timeline, state_events_from_timeline) =
|
||||
processors::state_events::sync::collect_from_timeline(
|
||||
&mut context,
|
||||
&new_info.timeline.events,
|
||||
);
|
||||
|
||||
let mut other_new_user_ids = processors::state_events::dispatch_and_get_new_users(
|
||||
&mut context,
|
||||
(&raw_state_events_from_timeline, &state_events_from_timeline),
|
||||
&mut room_info,
|
||||
&mut ambiguity_cache,
|
||||
)
|
||||
.await?;
|
||||
new_user_ids.append(&mut other_new_user_ids);
|
||||
updated_members_in_room.insert(room_id.to_owned(), new_user_ids.clone());
|
||||
|
||||
let timeline = processors::timeline::build(
|
||||
&mut context,
|
||||
&room,
|
||||
&mut room_info,
|
||||
processors::timeline::builder::Timeline::from(new_info.timeline),
|
||||
processors::timeline::builder::Notification::new(
|
||||
processors::room::RoomCreationData::new(
|
||||
&room_id,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
requested_required_states,
|
||||
&mut ambiguity_cache,
|
||||
),
|
||||
joined_room,
|
||||
&mut updated_members_in_room,
|
||||
processors::notification::Notification::new(
|
||||
&push_rules,
|
||||
&mut notifications,
|
||||
&self.state_store,
|
||||
),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
processors::timeline::builder::E2EE::new(
|
||||
processors::e2ee::E2EE::new(
|
||||
olm_machine.as_ref(),
|
||||
self.decryption_trust_requirement,
|
||||
&self.decryption_settings,
|
||||
self.handle_verification_events,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Save the new `RoomInfo`.
|
||||
context.state_changes.add_room(room_info);
|
||||
|
||||
processors::account_data::for_room(
|
||||
&mut context,
|
||||
&room_id,
|
||||
&new_info.account_data.events,
|
||||
&self.state_store,
|
||||
)
|
||||
.await;
|
||||
|
||||
// `Self::handle_room_account_data` might have updated the `RoomInfo`. Let's
|
||||
// fetch it again.
|
||||
//
|
||||
// SAFETY: `unwrap` is safe because the `RoomInfo` has been inserted 2 lines
|
||||
// above.
|
||||
let mut room_info = context.state_changes.room_infos.get(&room_id).unwrap().clone();
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
processors::e2ee::tracked_users::update_or_set_if_room_is_newly_encrypted(
|
||||
&mut context,
|
||||
olm_machine.as_ref(),
|
||||
&new_user_ids,
|
||||
room_info.encryption_state(),
|
||||
room.encryption_state(),
|
||||
&room_id,
|
||||
&self.state_store,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let notification_count = new_info.unread_notifications.into();
|
||||
room_info.update_notification_count(notification_count);
|
||||
|
||||
let ambiguity_changes = ambiguity_cache.changes.remove(&room_id).unwrap_or_default();
|
||||
|
||||
new_rooms.join.insert(
|
||||
room_id,
|
||||
JoinedRoomUpdate::new(
|
||||
timeline,
|
||||
new_info.state.events,
|
||||
new_info.account_data.events,
|
||||
new_info.ephemeral.events,
|
||||
notification_count,
|
||||
ambiguity_changes,
|
||||
),
|
||||
);
|
||||
|
||||
context.state_changes.add_room(room_info);
|
||||
room_updates.joined.insert(room_id, joined_room_update);
|
||||
}
|
||||
|
||||
for (room_id, new_info) in response.rooms.leave {
|
||||
let room = self.state_store.get_or_create_room(
|
||||
&room_id,
|
||||
RoomState::Left,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
);
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_as_left();
|
||||
room_info.mark_state_partially_synced();
|
||||
room_info.handle_encryption_state(requested_required_states.for_room(&room_id));
|
||||
|
||||
let (raw_state_events, state_events) =
|
||||
processors::state_events::sync::collect(&mut context, &new_info.state.events);
|
||||
|
||||
let mut new_user_ids = processors::state_events::dispatch_and_get_new_users(
|
||||
for (room_id, left_room) in response.rooms.leave {
|
||||
let left_room_update = processors::room::sync_v2::update_left_room(
|
||||
&mut context,
|
||||
(&raw_state_events, &state_events),
|
||||
&mut room_info,
|
||||
&mut ambiguity_cache,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (raw_state_events_from_timeline, state_events_from_timeline) =
|
||||
processors::state_events::sync::collect_from_timeline(
|
||||
&mut context,
|
||||
&new_info.timeline.events,
|
||||
);
|
||||
|
||||
let mut other_new_user_ids = processors::state_events::dispatch_and_get_new_users(
|
||||
&mut context,
|
||||
(&raw_state_events_from_timeline, &state_events_from_timeline),
|
||||
&mut room_info,
|
||||
&mut ambiguity_cache,
|
||||
)
|
||||
.await?;
|
||||
new_user_ids.append(&mut other_new_user_ids);
|
||||
|
||||
let timeline = processors::timeline::build(
|
||||
&mut context,
|
||||
&room,
|
||||
&mut room_info,
|
||||
processors::timeline::builder::Timeline::from(new_info.timeline),
|
||||
processors::timeline::builder::Notification::new(
|
||||
processors::room::RoomCreationData::new(
|
||||
&room_id,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
requested_required_states,
|
||||
&mut ambiguity_cache,
|
||||
),
|
||||
left_room,
|
||||
processors::notification::Notification::new(
|
||||
&push_rules,
|
||||
&mut notifications,
|
||||
&self.state_store,
|
||||
),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
processors::timeline::builder::E2EE::new(
|
||||
processors::e2ee::E2EE::new(
|
||||
olm_machine.as_ref(),
|
||||
self.decryption_trust_requirement,
|
||||
&self.decryption_settings,
|
||||
self.handle_verification_events,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Save the new `RoomInfo`.
|
||||
context.state_changes.add_room(room_info);
|
||||
room_updates.left.insert(room_id, left_room_update);
|
||||
}
|
||||
|
||||
processors::account_data::for_room(
|
||||
for (room_id, invited_room) in response.rooms.invite {
|
||||
let invited_room_update = processors::room::sync_v2::update_invited_room(
|
||||
&mut context,
|
||||
&room_id,
|
||||
&new_info.account_data.events,
|
||||
&self.state_store,
|
||||
)
|
||||
.await;
|
||||
|
||||
let ambiguity_changes = ambiguity_cache.changes.remove(&room_id).unwrap_or_default();
|
||||
|
||||
new_rooms.leave.insert(
|
||||
room_id,
|
||||
LeftRoomUpdate::new(
|
||||
timeline,
|
||||
new_info.state.events,
|
||||
new_info.account_data.events,
|
||||
ambiguity_changes,
|
||||
invited_room,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
processors::notification::Notification::new(
|
||||
&push_rules,
|
||||
&mut notifications,
|
||||
&self.state_store,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
for (room_id, new_info) in response.rooms.invite {
|
||||
let room = self.state_store.get_or_create_room(
|
||||
&room_id,
|
||||
RoomState::Invited,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
);
|
||||
|
||||
let invite_state = processors::state_events::stripped::collect(
|
||||
&mut context,
|
||||
&new_info.invite_state.events,
|
||||
);
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_as_invited();
|
||||
room_info.mark_state_fully_synced();
|
||||
|
||||
self.handle_invited_state(
|
||||
&mut context,
|
||||
&room,
|
||||
invite_state,
|
||||
&push_rules,
|
||||
&mut room_info,
|
||||
&mut notifications,
|
||||
)
|
||||
.await?;
|
||||
|
||||
context.state_changes.add_room(room_info);
|
||||
|
||||
new_rooms.invite.insert(room_id, new_info);
|
||||
room_updates.invited.insert(room_id, invited_room_update);
|
||||
}
|
||||
|
||||
for (room_id, new_info) in response.rooms.knock {
|
||||
let room = self.state_store.get_or_create_room(
|
||||
for (room_id, knocked_room) in response.rooms.knock {
|
||||
let knocked_room_update = processors::room::sync_v2::update_knocked_room(
|
||||
&mut context,
|
||||
&room_id,
|
||||
RoomState::Knocked,
|
||||
knocked_room,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
);
|
||||
|
||||
let knock_state = processors::state_events::stripped::collect(
|
||||
&mut context,
|
||||
&new_info.knock_state.events,
|
||||
);
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_as_knocked();
|
||||
room_info.mark_state_fully_synced();
|
||||
|
||||
self.handle_invited_state(
|
||||
&mut context,
|
||||
&room,
|
||||
knock_state,
|
||||
&push_rules,
|
||||
&mut room_info,
|
||||
&mut notifications,
|
||||
processors::notification::Notification::new(
|
||||
&push_rules,
|
||||
&mut notifications,
|
||||
&self.state_store,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
context.state_changes.add_room(room_info);
|
||||
|
||||
new_rooms.knocked.insert(room_id, new_info);
|
||||
room_updates.knocked.insert(room_id, knocked_room_update);
|
||||
}
|
||||
|
||||
global_account_data_processor.apply(&mut context, &self.state_store).await;
|
||||
@@ -899,12 +735,19 @@ impl BaseClient {
|
||||
.await?;
|
||||
}
|
||||
|
||||
let mut context = Context::default();
|
||||
|
||||
// Now that all the rooms information have been saved, update the display name
|
||||
// cache (which relies on information stored in the database). This will
|
||||
// live in memory, until the next sync which will saves the room info to
|
||||
// disk; we do this to avoid saving that would be redundant with the
|
||||
// above. Oh well.
|
||||
new_rooms.update_in_memory_caches(&self.state_store).await;
|
||||
// of the updated rooms (which relies on information stored in the database).
|
||||
processors::room::display_name::update_for_rooms(
|
||||
&mut context,
|
||||
&room_updates,
|
||||
&self.state_store,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Save the new display name updates if any.
|
||||
processors::changes::save_only(context, &self.state_store).await?;
|
||||
|
||||
for (room_id, member_ids) in updated_members_in_room {
|
||||
if let Some(room) = self.get_room(&room_id) {
|
||||
@@ -913,10 +756,12 @@ impl BaseClient {
|
||||
}
|
||||
}
|
||||
|
||||
info!("Processed a sync response in {:?}", now.elapsed());
|
||||
if enabled!(Level::INFO) {
|
||||
info!("Processed a sync response in {:?}", now.map(|now| now.elapsed()));
|
||||
}
|
||||
|
||||
let response = SyncResponse {
|
||||
rooms: new_rooms,
|
||||
rooms: room_updates,
|
||||
presence: response.presence.events,
|
||||
account_data: response.account_data.events,
|
||||
to_device,
|
||||
@@ -958,7 +803,7 @@ impl BaseClient {
|
||||
};
|
||||
|
||||
let mut chunk = Vec::with_capacity(response.chunk.len());
|
||||
let mut context = Context::new(StateChanges::default(), Default::default());
|
||||
let mut context = Context::default();
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
let mut user_ids = BTreeSet::new();
|
||||
@@ -1018,7 +863,6 @@ impl BaseClient {
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
processors::e2ee::tracked_users::update(
|
||||
&mut context,
|
||||
self.olm_machine().await.as_ref(),
|
||||
room.encryption_state(),
|
||||
&user_ids,
|
||||
@@ -1203,6 +1047,27 @@ impl BaseClient {
|
||||
pub fn room_info_notable_update_receiver(&self) -> broadcast::Receiver<RoomInfoNotableUpdate> {
|
||||
self.room_info_notable_update_sender.subscribe()
|
||||
}
|
||||
|
||||
/// Checks whether the provided `user_id` belongs to an ignored user.
|
||||
pub async fn is_user_ignored(&self, user_id: &UserId) -> bool {
|
||||
match self.state_store.get_account_data_event_static::<IgnoredUserListEventContent>().await
|
||||
{
|
||||
Ok(Some(raw_ignored_user_list)) => match raw_ignored_user_list.deserialize() {
|
||||
Ok(current_ignored_user_list) => {
|
||||
current_ignored_user_list.content.ignored_users.contains_key(user_id)
|
||||
}
|
||||
Err(error) => {
|
||||
warn!(?error, "Failed to deserialize the ignored user list event");
|
||||
false
|
||||
}
|
||||
},
|
||||
Ok(None) => false,
|
||||
Err(error) => {
|
||||
warn!(?error, "Could not get the ignored user list from the state store");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represent the `required_state` values sent by a sync request.
|
||||
@@ -1284,6 +1149,7 @@ mod tests {
|
||||
|
||||
use super::{BaseClient, RequestedRequiredStates};
|
||||
use crate::{
|
||||
client::ThreadingSupport,
|
||||
store::{RoomLoadSettings, StateStoreExt, StoreConfig},
|
||||
test_utils::logged_in_base_client,
|
||||
RoomDisplayName, RoomState, SessionMeta,
|
||||
@@ -1568,7 +1434,7 @@ mod tests {
|
||||
let room = client.get_room(room_id).expect("Room not found");
|
||||
assert_eq!(room.state(), RoomState::Invited);
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.expect("fetching display name failed"),
|
||||
room.compute_display_name().await.expect("fetching display name failed").into_inner(),
|
||||
RoomDisplayName::Calculated("Kyra".to_owned())
|
||||
);
|
||||
}
|
||||
@@ -1578,8 +1444,10 @@ mod tests {
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
|
||||
|
||||
let client =
|
||||
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
|
||||
let client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
client
|
||||
.activate(
|
||||
SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
|
||||
@@ -1638,8 +1506,10 @@ mod tests {
|
||||
let inviter_user_id = user_id!("@bob:example.org");
|
||||
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
|
||||
|
||||
let client =
|
||||
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
|
||||
let client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
client
|
||||
.activate(
|
||||
SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
|
||||
@@ -1700,8 +1570,10 @@ mod tests {
|
||||
let inviter_user_id = user_id!("@bob:example.org");
|
||||
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
|
||||
|
||||
let client =
|
||||
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
|
||||
let client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
client
|
||||
.activate(
|
||||
SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
|
||||
@@ -1772,8 +1644,10 @@ mod tests {
|
||||
#[async_test]
|
||||
async fn test_ignored_user_list_changes() {
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
let client =
|
||||
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
|
||||
let client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
client
|
||||
.activate(
|
||||
@@ -1840,4 +1714,70 @@ mod tests {
|
||||
assert_let!(Some(ignored) = subscriber.next().await);
|
||||
assert!(ignored.is_empty());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_is_user_ignored() {
|
||||
let ignored_user_id = user_id!("@alice:example.org");
|
||||
let client = logged_in_base_client(None).await;
|
||||
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
let response = sync_builder
|
||||
.add_global_account_data_event(matrix_sdk_test::GlobalAccountDataTestEvent::Custom(
|
||||
json!({
|
||||
"content": {
|
||||
"ignored_users": {
|
||||
ignored_user_id: {}
|
||||
}
|
||||
},
|
||||
"type": "m.ignored_user_list",
|
||||
}),
|
||||
))
|
||||
.build_sync_response();
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
assert!(client.is_user_ignored(ignored_user_id).await);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_joined_at_timestamp_is_set() {
|
||||
let client = logged_in_base_client(None).await;
|
||||
let invited_room_id = room_id!("!invited:localhost");
|
||||
let unknown_room_id = room_id!("!unknown:localhost");
|
||||
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
let response = sync_builder
|
||||
.add_invited_room(InvitedRoomBuilder::new(invited_room_id))
|
||||
.build_sync_response();
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
// Let us first check the initial state, we should have a room in the invite
|
||||
// state.
|
||||
let invited_room = client
|
||||
.get_room(invited_room_id)
|
||||
.expect("The sync should have created a room in the invited state");
|
||||
|
||||
assert_eq!(invited_room.state(), RoomState::Invited);
|
||||
assert!(invited_room.inner.get().invite_accepted_at().is_none());
|
||||
|
||||
// Now we join the room.
|
||||
let joined_room = client
|
||||
.room_joined(invited_room_id)
|
||||
.await
|
||||
.expect("We should be able to mark a room as joined");
|
||||
|
||||
// Yup, there's a timestamp now.
|
||||
assert_eq!(joined_room.state(), RoomState::Joined);
|
||||
assert!(joined_room.inner.get().invite_accepted_at().is_some());
|
||||
|
||||
// If we didn't know about the room before the join, we assume that there wasn't
|
||||
// an invite and we don't record the timestamp.
|
||||
assert!(client.get_room(unknown_room_id).is_none());
|
||||
let unknown_room = client
|
||||
.room_joined(unknown_room_id)
|
||||
.await
|
||||
.expect("We should be able to mark a room as joined");
|
||||
|
||||
assert_eq!(unknown_room.state(), RoomState::Joined);
|
||||
assert!(unknown_room.inner.get().invite_accepted_at().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
use std::fmt;
|
||||
|
||||
pub use matrix_sdk_common::debug::*;
|
||||
use matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent;
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::v3::{InvitedRoom, KnockedRoom},
|
||||
serde::Raw,
|
||||
@@ -35,6 +36,19 @@ impl<T> fmt::Debug for DebugListOfRawEventsNoId<'_, T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around a slice of `ProcessedToDeviceEvent` events that implements
|
||||
/// `Debug` in a way that only prints the event type of each item.
|
||||
pub struct DebugListOfProcessedToDeviceEvents<'a>(pub &'a [ProcessedToDeviceEvent]);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for DebugListOfProcessedToDeviceEvents<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut list = f.debug_list();
|
||||
list.entries(self.0.iter().map(|e| DebugRawEventNoId(e.as_raw())));
|
||||
list.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around an invited room as found in `/sync` responses that
|
||||
/// implements `Debug` in a way that only prints the event ID and event type for
|
||||
/// the raw events contained in `invite_state`.
|
||||
|
||||
@@ -263,6 +263,18 @@ pub enum RawAnySyncOrStrippedTimelineEvent {
|
||||
Stripped(Raw<AnyStrippedStateEvent>),
|
||||
}
|
||||
|
||||
impl From<Raw<AnySyncTimelineEvent>> for RawAnySyncOrStrippedTimelineEvent {
|
||||
fn from(event: Raw<AnySyncTimelineEvent>) -> Self {
|
||||
Self::Sync(event)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Raw<AnyStrippedStateEvent>> for RawAnySyncOrStrippedTimelineEvent {
|
||||
fn from(event: Raw<AnyStrippedStateEvent>) -> Self {
|
||||
Self::Stripped(event)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper around both versions of any raw state event.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
|
||||
@@ -14,14 +14,17 @@
|
||||
|
||||
//! Trait and macro of integration tests for `EventCacheStore` implementations.
|
||||
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
deserialized_responses::{
|
||||
AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, TimelineEvent, TimelineEventKind,
|
||||
VerificationState,
|
||||
},
|
||||
linked_chunk::{lazy_loader, ChunkContent, ChunkIdentifier as CId, Position, Update},
|
||||
linked_chunk::{
|
||||
lazy_loader, ChunkContent, ChunkIdentifier as CId, LinkedChunkId, Position, Update,
|
||||
},
|
||||
};
|
||||
use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID};
|
||||
use ruma::{
|
||||
@@ -56,16 +59,16 @@ pub fn make_test_event_with_event_id(
|
||||
content: &str,
|
||||
event_id: Option<&EventId>,
|
||||
) -> TimelineEvent {
|
||||
let encryption_info = EncryptionInfo {
|
||||
let encryption_info = Arc::new(EncryptionInfo {
|
||||
sender: (*ALICE).into(),
|
||||
sender_device: None,
|
||||
algorithm_info: AlgorithmInfo::MegolmV1AesSha2 {
|
||||
curve25519_key: "1337".to_owned(),
|
||||
sender_claimed_keys: Default::default(),
|
||||
session_id: Some("mysessionid9".to_owned()),
|
||||
},
|
||||
verification_state: VerificationState::Verified,
|
||||
session_id: Some("mysessionid9".to_owned()),
|
||||
};
|
||||
});
|
||||
|
||||
let mut builder = EventFactory::new().text_msg(content).room(room_id).sender(*ALICE);
|
||||
if let Some(event_id) = event_id {
|
||||
@@ -73,14 +76,10 @@ pub fn make_test_event_with_event_id(
|
||||
}
|
||||
let event = builder.into_raw_timeline().cast();
|
||||
|
||||
TimelineEvent {
|
||||
kind: TimelineEventKind::Decrypted(DecryptedRoomEvent {
|
||||
event,
|
||||
encryption_info,
|
||||
unsigned_encryption_info: None,
|
||||
}),
|
||||
push_actions: Some(vec![Action::Notify]),
|
||||
}
|
||||
TimelineEvent::from_decrypted(
|
||||
DecryptedRoomEvent { event, encryption_info, unsigned_encryption_info: None },
|
||||
Some(vec![Action::Notify]),
|
||||
)
|
||||
}
|
||||
|
||||
/// Check that an event created with [`make_test_event`] contains the expected
|
||||
@@ -90,7 +89,7 @@ pub fn make_test_event_with_event_id(
|
||||
#[track_caller]
|
||||
pub fn check_test_event(event: &TimelineEvent, text: &str) {
|
||||
// Check push actions.
|
||||
let actions = event.push_actions.as_ref().unwrap();
|
||||
let actions = event.push_actions().unwrap();
|
||||
assert_eq!(actions.len(), 1);
|
||||
assert_matches!(&actions[0], Action::Notify);
|
||||
|
||||
@@ -114,8 +113,7 @@ pub fn check_test_event(event: &TimelineEvent, text: &str) {
|
||||
///
|
||||
/// This trait is not meant to be used directly, but will be used with the
|
||||
/// `event_cache_store_integration_tests!` macro.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait EventCacheStoreIntegrationTests {
|
||||
/// Test media content storage.
|
||||
async fn test_media_content(&self);
|
||||
@@ -135,8 +133,11 @@ pub trait EventCacheStoreIntegrationTests {
|
||||
/// anything.
|
||||
async fn test_rebuild_empty_linked_chunk(&self);
|
||||
|
||||
/// Test that loading a linked chunk's metadata works as intended.
|
||||
async fn test_load_all_chunks_metadata(&self);
|
||||
|
||||
/// Test that clear all the rooms' linked chunks works.
|
||||
async fn test_clear_all_rooms_chunks(&self);
|
||||
async fn test_clear_all_linked_chunks(&self);
|
||||
|
||||
/// Test that removing a room from storage empties all associated data.
|
||||
async fn test_remove_room(&self);
|
||||
@@ -154,8 +155,6 @@ pub trait EventCacheStoreIntegrationTests {
|
||||
async fn test_save_event(&self);
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
async fn test_media_content(&self) {
|
||||
let uri = mxc_uri!("mxc://localhost/media");
|
||||
@@ -343,9 +342,10 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
|
||||
async fn test_handle_updates_and_rebuild_linked_chunk(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
room_id,
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
// new chunk
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
@@ -377,10 +377,11 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
.unwrap();
|
||||
|
||||
// The linked chunk is correctly reloaded.
|
||||
let lc =
|
||||
lazy_loader::from_all_chunks::<3, _, _>(self.load_all_chunks(room_id).await.unwrap())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let lc = lazy_loader::from_all_chunks::<3, _, _>(
|
||||
self.load_all_chunks(linked_chunk_id).await.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let mut chunks = lc.chunks();
|
||||
|
||||
@@ -419,21 +420,88 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
assert!(chunks.next().is_none());
|
||||
}
|
||||
|
||||
async fn test_load_all_chunks_metadata(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
// new chunk
|
||||
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![
|
||||
make_test_event(room_id, "hello"),
|
||||
make_test_event(room_id, "world"),
|
||||
],
|
||||
},
|
||||
// a gap chunk
|
||||
Update::NewGapChunk {
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "parmesan".to_owned() },
|
||||
},
|
||||
// another items chunk
|
||||
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![make_test_event(room_id, "sup")],
|
||||
},
|
||||
// and an empty items chunk to finish
|
||||
Update::NewItemsChunk { previous: Some(CId::new(2)), new: CId::new(3), next: None },
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let metas = self.load_all_chunks_metadata(linked_chunk_id).await.unwrap();
|
||||
assert_eq!(metas.len(), 4);
|
||||
|
||||
// The first chunk has two items.
|
||||
assert_eq!(metas[0].identifier, CId::new(0));
|
||||
assert_eq!(metas[0].previous, None);
|
||||
assert_eq!(metas[0].next, Some(CId::new(1)));
|
||||
assert_eq!(metas[0].num_items, 2);
|
||||
|
||||
// The second chunk is a gap, so it has 0 items.
|
||||
assert_eq!(metas[1].identifier, CId::new(1));
|
||||
assert_eq!(metas[1].previous, Some(CId::new(0)));
|
||||
assert_eq!(metas[1].next, Some(CId::new(2)));
|
||||
assert_eq!(metas[1].num_items, 0);
|
||||
|
||||
// The third event chunk has one item.
|
||||
assert_eq!(metas[2].identifier, CId::new(2));
|
||||
assert_eq!(metas[2].previous, Some(CId::new(1)));
|
||||
assert_eq!(metas[2].next, Some(CId::new(3)));
|
||||
assert_eq!(metas[2].num_items, 1);
|
||||
|
||||
// The final event chunk is empty.
|
||||
assert_eq!(metas[3].identifier, CId::new(3));
|
||||
assert_eq!(metas[3].previous, Some(CId::new(2)));
|
||||
assert_eq!(metas[3].next, None);
|
||||
assert_eq!(metas[3].num_items, 0);
|
||||
}
|
||||
|
||||
async fn test_linked_chunk_incremental_loading(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let event = |msg: &str| make_test_event(room_id, msg);
|
||||
|
||||
// Load the last chunk, but none exists yet.
|
||||
{
|
||||
let (last_chunk, chunk_identifier_generator) =
|
||||
self.load_last_chunk(room_id).await.unwrap();
|
||||
self.load_last_chunk(linked_chunk_id).await.unwrap();
|
||||
|
||||
assert!(last_chunk.is_none());
|
||||
assert_eq!(chunk_identifier_generator.current(), 0);
|
||||
}
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
room_id,
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
// new chunk for items
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
@@ -464,7 +532,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
// Load the last chunk.
|
||||
let mut linked_chunk = {
|
||||
let (last_chunk, chunk_identifier_generator) =
|
||||
self.load_last_chunk(room_id).await.unwrap();
|
||||
self.load_last_chunk(linked_chunk_id).await.unwrap();
|
||||
|
||||
assert_eq!(chunk_identifier_generator.current(), 2);
|
||||
|
||||
@@ -499,9 +567,9 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
{
|
||||
let first_chunk = linked_chunk.chunks().next().unwrap().identifier();
|
||||
let previous_chunk =
|
||||
self.load_previous_chunk(room_id, first_chunk).await.unwrap().unwrap();
|
||||
self.load_previous_chunk(linked_chunk_id, first_chunk).await.unwrap().unwrap();
|
||||
|
||||
let _ = lazy_loader::insert_new_first_chunk(&mut linked_chunk, previous_chunk).unwrap();
|
||||
lazy_loader::insert_new_first_chunk(&mut linked_chunk, previous_chunk).unwrap();
|
||||
|
||||
let mut rchunks = linked_chunk.rchunks();
|
||||
|
||||
@@ -536,9 +604,9 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
{
|
||||
let first_chunk = linked_chunk.chunks().next().unwrap().identifier();
|
||||
let previous_chunk =
|
||||
self.load_previous_chunk(room_id, first_chunk).await.unwrap().unwrap();
|
||||
self.load_previous_chunk(linked_chunk_id, first_chunk).await.unwrap().unwrap();
|
||||
|
||||
let _ = lazy_loader::insert_new_first_chunk(&mut linked_chunk, previous_chunk).unwrap();
|
||||
lazy_loader::insert_new_first_chunk(&mut linked_chunk, previous_chunk).unwrap();
|
||||
|
||||
let mut rchunks = linked_chunk.rchunks();
|
||||
|
||||
@@ -585,7 +653,8 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
// Load the previous chunk: there is none.
|
||||
{
|
||||
let first_chunk = linked_chunk.chunks().next().unwrap().identifier();
|
||||
let previous_chunk = self.load_previous_chunk(room_id, first_chunk).await.unwrap();
|
||||
let previous_chunk =
|
||||
self.load_previous_chunk(linked_chunk_id, first_chunk).await.unwrap();
|
||||
|
||||
assert!(previous_chunk.is_none());
|
||||
}
|
||||
@@ -637,19 +706,21 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
async fn test_rebuild_empty_linked_chunk(&self) {
|
||||
// When I rebuild a linked chunk from an empty store, it's empty.
|
||||
let linked_chunk = lazy_loader::from_all_chunks::<3, _, _>(
|
||||
self.load_all_chunks(&DEFAULT_TEST_ROOM_ID).await.unwrap(),
|
||||
self.load_all_chunks(LinkedChunkId::Room(&DEFAULT_TEST_ROOM_ID)).await.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(linked_chunk.is_none());
|
||||
}
|
||||
|
||||
async fn test_clear_all_rooms_chunks(&self) {
|
||||
async fn test_clear_all_linked_chunks(&self) {
|
||||
let r0 = room_id!("!r0:matrix.org");
|
||||
let linked_chunk_id0 = LinkedChunkId::Room(r0);
|
||||
let r1 = room_id!("!r1:matrix.org");
|
||||
let linked_chunk_id1 = LinkedChunkId::Room(r1);
|
||||
|
||||
// Add updates for the first room.
|
||||
self.handle_linked_chunk_updates(
|
||||
r0,
|
||||
linked_chunk_id0,
|
||||
vec![
|
||||
// new chunk
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
@@ -665,7 +736,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
|
||||
// Add updates for the second room.
|
||||
self.handle_linked_chunk_updates(
|
||||
r1,
|
||||
linked_chunk_id1,
|
||||
vec![
|
||||
// Empty items chunk.
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
@@ -689,32 +760,42 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
.unwrap();
|
||||
|
||||
// Sanity check: both linked chunks can be reloaded.
|
||||
assert!(lazy_loader::from_all_chunks::<3, _, _>(self.load_all_chunks(r0).await.unwrap())
|
||||
.unwrap()
|
||||
.is_some());
|
||||
assert!(lazy_loader::from_all_chunks::<3, _, _>(self.load_all_chunks(r1).await.unwrap())
|
||||
.unwrap()
|
||||
.is_some());
|
||||
assert!(lazy_loader::from_all_chunks::<3, _, _>(
|
||||
self.load_all_chunks(linked_chunk_id0).await.unwrap()
|
||||
)
|
||||
.unwrap()
|
||||
.is_some());
|
||||
assert!(lazy_loader::from_all_chunks::<3, _, _>(
|
||||
self.load_all_chunks(linked_chunk_id1).await.unwrap()
|
||||
)
|
||||
.unwrap()
|
||||
.is_some());
|
||||
|
||||
// Clear the chunks.
|
||||
self.clear_all_rooms_chunks().await.unwrap();
|
||||
self.clear_all_linked_chunks().await.unwrap();
|
||||
|
||||
// Both rooms now have no linked chunk.
|
||||
assert!(lazy_loader::from_all_chunks::<3, _, _>(self.load_all_chunks(r0).await.unwrap())
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert!(lazy_loader::from_all_chunks::<3, _, _>(self.load_all_chunks(r1).await.unwrap())
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert!(lazy_loader::from_all_chunks::<3, _, _>(
|
||||
self.load_all_chunks(linked_chunk_id0).await.unwrap()
|
||||
)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert!(lazy_loader::from_all_chunks::<3, _, _>(
|
||||
self.load_all_chunks(linked_chunk_id1).await.unwrap()
|
||||
)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
}
|
||||
|
||||
async fn test_remove_room(&self) {
|
||||
let r0 = room_id!("!r0:matrix.org");
|
||||
let linked_chunk_id0 = LinkedChunkId::Room(r0);
|
||||
let r1 = room_id!("!r1:matrix.org");
|
||||
let linked_chunk_id1 = LinkedChunkId::Room(r1);
|
||||
|
||||
// Add updates to the first room.
|
||||
self.handle_linked_chunk_updates(
|
||||
r0,
|
||||
linked_chunk_id0,
|
||||
vec![
|
||||
// new chunk
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
@@ -730,7 +811,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
|
||||
// Add updates to the second room.
|
||||
self.handle_linked_chunk_updates(
|
||||
r1,
|
||||
linked_chunk_id1,
|
||||
vec![
|
||||
// new chunk
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
@@ -748,17 +829,19 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
self.remove_room(r0).await.unwrap();
|
||||
|
||||
// Check that r0 doesn't have a linked chunk anymore.
|
||||
let r0_linked_chunk = self.load_all_chunks(r0).await.unwrap();
|
||||
let r0_linked_chunk = self.load_all_chunks(linked_chunk_id0).await.unwrap();
|
||||
assert!(r0_linked_chunk.is_empty());
|
||||
|
||||
// Check that r1 is unaffected.
|
||||
let r1_linked_chunk = self.load_all_chunks(r1).await.unwrap();
|
||||
let r1_linked_chunk = self.load_all_chunks(linked_chunk_id1).await.unwrap();
|
||||
assert!(!r1_linked_chunk.is_empty());
|
||||
}
|
||||
|
||||
async fn test_filter_duplicated_events(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let another_room_id = room_id!("!r1:matrix.org");
|
||||
let another_linked_chunk_id = LinkedChunkId::Room(another_room_id);
|
||||
let event = |msg: &str| make_test_event(room_id, msg);
|
||||
|
||||
let event_comte = event("comté");
|
||||
@@ -770,7 +853,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
let event_mont_dor = event("mont d'or");
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
room_id,
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
@@ -796,7 +879,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
// Add other events in another room, to ensure filtering take the `room_id` into
|
||||
// account.
|
||||
self.handle_linked_chunk_updates(
|
||||
another_room_id,
|
||||
another_linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
@@ -808,9 +891,9 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let duplicated_events = self
|
||||
.filter_duplicated_events(
|
||||
room_id,
|
||||
let duplicated_events = BTreeMap::from_iter(
|
||||
self.filter_duplicated_events(
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
event_comte.event_id().unwrap().to_owned(),
|
||||
event_raclette.event_id().unwrap().to_owned(),
|
||||
@@ -821,26 +904,29 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(duplicated_events.len(), 3);
|
||||
|
||||
assert_eq!(
|
||||
duplicated_events[0],
|
||||
(event_comte.event_id().unwrap(), Position::new(CId::new(0), 0))
|
||||
*duplicated_events.get(&event_comte.event_id().unwrap()).unwrap(),
|
||||
Position::new(CId::new(0), 0)
|
||||
);
|
||||
assert_eq!(
|
||||
duplicated_events[1],
|
||||
(event_morbier.event_id().unwrap(), Position::new(CId::new(2), 0))
|
||||
*duplicated_events.get(&event_morbier.event_id().unwrap()).unwrap(),
|
||||
Position::new(CId::new(2), 0)
|
||||
);
|
||||
assert_eq!(
|
||||
duplicated_events[2],
|
||||
(event_mont_dor.event_id().unwrap(), Position::new(CId::new(2), 1))
|
||||
*duplicated_events.get(&event_mont_dor.event_id().unwrap()).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é");
|
||||
@@ -848,7 +934,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
|
||||
// Add one event in one room.
|
||||
self.handle_linked_chunk_updates(
|
||||
room_id,
|
||||
LinkedChunkId::Room(room_id),
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
@@ -862,7 +948,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
|
||||
// Add another event in another room.
|
||||
self.handle_linked_chunk_updates(
|
||||
another_room_id,
|
||||
another_linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
@@ -891,7 +977,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
.is_none());
|
||||
|
||||
// Clearing the rooms also clears the event's storage.
|
||||
self.clear_all_rooms_chunks().await.expect("failed to clear all rooms chunks");
|
||||
self.clear_all_linked_chunks().await.expect("failed to clear all rooms chunks");
|
||||
assert!(self
|
||||
.find_event(room_id, event_comte.event_id().unwrap().as_ref())
|
||||
.await
|
||||
@@ -934,7 +1020,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
// 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, reaction_e1.clone()).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();
|
||||
@@ -942,8 +1028,13 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
// 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)));
|
||||
// The position is `None` for items outside the linked chunk.
|
||||
assert!(relations
|
||||
.iter()
|
||||
.any(|(ev, pos)| ev.event_id().as_deref() == Some(edit_eid1) && pos.is_none()));
|
||||
assert!(relations
|
||||
.iter()
|
||||
.any(|(ev, pos)| ev.event_id().as_deref() == Some(reaction_eid1) && pos.is_none()));
|
||||
|
||||
// Finding relations with a filter only returns a subset.
|
||||
let relations = self
|
||||
@@ -951,7 +1042,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(relations.len(), 1);
|
||||
assert_eq!(relations[0].event_id().as_deref(), Some(edit_eid1));
|
||||
assert_eq!(relations[0].0.event_id().as_deref(), Some(edit_eid1));
|
||||
|
||||
let relations = self
|
||||
.find_event_relations(
|
||||
@@ -962,8 +1053,8 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
.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)));
|
||||
assert!(relations.iter().any(|r| r.0.event_id().as_deref() == Some(edit_eid1)));
|
||||
assert!(relations.iter().any(|r| r.0.event_id().as_deref() == Some(reaction_eid1)));
|
||||
|
||||
// We can't find relations using the wrong room.
|
||||
let relations = self
|
||||
@@ -971,6 +1062,35 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(relations.is_empty());
|
||||
|
||||
// But if an event exists in the linked chunk, we may have its position when
|
||||
// it's found as a relationship.
|
||||
|
||||
// Add reaction_e1 to the room's linked chunk.
|
||||
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![reaction_e1] },
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// When looking for aggregations to e1, we should have the position for
|
||||
// reaction_e1.
|
||||
let relations = self.find_event_relations(room_id, eid1, None).await.unwrap();
|
||||
|
||||
// The position is set for `reaction_eid1` now.
|
||||
assert!(relations.iter().any(|(ev, pos)| {
|
||||
ev.event_id().as_deref() == Some(reaction_eid1)
|
||||
&& *pos == Some(Position::new(CId::new(0), 0))
|
||||
}));
|
||||
|
||||
// But it's still not set for the other related events.
|
||||
assert!(relations
|
||||
.iter()
|
||||
.any(|(ev, pos)| ev.event_id().as_deref() == Some(edit_eid1) && pos.is_none()));
|
||||
}
|
||||
|
||||
async fn test_save_event(&self) {
|
||||
@@ -1091,10 +1211,17 @@ macro_rules! event_cache_store_integration_tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_clear_all_rooms_chunks() {
|
||||
async fn test_load_all_chunks_metadata() {
|
||||
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_load_all_chunks_metadata().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
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_linked_chunks().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
@@ -1141,7 +1268,7 @@ macro_rules! event_cache_store_integration_tests {
|
||||
#[macro_export]
|
||||
macro_rules! event_cache_store_integration_tests_time {
|
||||
() => {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
mod event_cache_store_integration_tests_time {
|
||||
use std::time::Duration;
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
//! Trait and macro of integration tests for `EventCacheStoreMedia`
|
||||
//! implementations.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use ruma::{
|
||||
events::room::MediaSource,
|
||||
mxc_uri, owned_mxc_uri,
|
||||
@@ -31,8 +30,7 @@ use crate::media::{MediaFormat, MediaRequestParameters};
|
||||
///
|
||||
/// This trait is not meant to be used directly, but will be used with the
|
||||
/// `event_cache_store_media_integration_tests!` macro.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait EventCacheStoreMediaIntegrationTests {
|
||||
/// Test media retention policy storage.
|
||||
async fn test_store_media_retention_policy(&self);
|
||||
@@ -58,8 +56,6 @@ pub trait EventCacheStoreMediaIntegrationTests {
|
||||
async fn test_store_last_media_cleanup_time(&self);
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<Store> EventCacheStoreMediaIntegrationTests for Store
|
||||
where
|
||||
Store: EventCacheStoreMedia + std::fmt::Debug,
|
||||
|
||||
@@ -356,8 +356,8 @@ where
|
||||
/// [`MediaRetentionPolicy`] by wrapping this in a [`MediaService`], and to
|
||||
/// simplify the implementation of tests by being able to have complete control
|
||||
/// over the `SystemTime`s provided to the store.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
pub trait EventCacheStoreMedia: AsyncTraitDeps + Clone {
|
||||
/// The error type used by this media cache store.
|
||||
type Error: fmt::Debug + fmt::Display + Into<EventCacheStoreError>;
|
||||
@@ -630,8 +630,8 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
impl EventCacheStoreMedia for MockEventCacheStoreMedia {
|
||||
type Error = MockEventCacheStoreMediaError;
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ use std::{
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
linked_chunk::{
|
||||
relational::RelationalLinkedChunk, ChunkIdentifier, ChunkIdentifierGenerator, Position,
|
||||
RawChunk, Update,
|
||||
relational::RelationalLinkedChunk, ChunkIdentifier, ChunkIdentifierGenerator,
|
||||
ChunkMetadata, LinkedChunkId, OwnedLinkedChunkId, Position, RawChunk, Update,
|
||||
},
|
||||
ring_buffer::RingBuffer,
|
||||
store_locks::memory_store_helper::try_take_leased_lock,
|
||||
@@ -110,8 +110,8 @@ impl MemoryStore {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
impl EventCacheStore for MemoryStore {
|
||||
type Error = EventCacheStoreError;
|
||||
|
||||
@@ -128,70 +128,81 @@ impl EventCacheStore for MemoryStore {
|
||||
|
||||
async fn handle_linked_chunk_updates(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
updates: Vec<Update<Event, Gap>>,
|
||||
) -> Result<(), Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
inner.events.apply_updates(room_id, updates);
|
||||
inner.events.apply_updates(linked_chunk_id, updates);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_all_chunks(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
inner
|
||||
.events
|
||||
.load_all_chunks(room_id)
|
||||
.load_all_chunks(linked_chunk_id)
|
||||
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
|
||||
}
|
||||
|
||||
async fn load_all_chunks_metadata(
|
||||
&self,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<Vec<ChunkMetadata>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
inner
|
||||
.events
|
||||
.load_all_chunks_metadata(linked_chunk_id)
|
||||
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
|
||||
}
|
||||
|
||||
async fn load_last_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<(Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator), Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
inner
|
||||
.events
|
||||
.load_last_chunk(room_id)
|
||||
.load_last_chunk(linked_chunk_id)
|
||||
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
|
||||
}
|
||||
|
||||
async fn load_previous_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
before_chunk_identifier: ChunkIdentifier,
|
||||
) -> Result<Option<RawChunk<Event, Gap>>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
inner
|
||||
.events
|
||||
.load_previous_chunk(room_id, before_chunk_identifier)
|
||||
.load_previous_chunk(linked_chunk_id, before_chunk_identifier)
|
||||
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
|
||||
}
|
||||
|
||||
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error> {
|
||||
async fn clear_all_linked_chunks(&self) -> Result<(), Self::Error> {
|
||||
self.inner.write().unwrap().events.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn filter_duplicated_events(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
mut events: Vec<OwnedEventId>,
|
||||
) -> Result<Vec<(OwnedEventId, Position)>, Self::Error> {
|
||||
// Collect all duplicated events.
|
||||
if events.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
let mut duplicated_events = Vec::new();
|
||||
|
||||
for (event, position) in inner.events.unordered_room_items(room_id) {
|
||||
// If `events` is empty, we can short-circuit.
|
||||
if events.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
for (event, position) in
|
||||
inner.events.unordered_linked_chunk_items(&linked_chunk_id.to_owned())
|
||||
{
|
||||
if let Some(known_event_id) = event.event_id() {
|
||||
// This event is a duplicate!
|
||||
if let Some(index) =
|
||||
@@ -212,9 +223,12 @@ impl EventCacheStore for MemoryStore {
|
||||
) -> Result<Option<Event>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
let event = inner.events.items().find_map(|(event, this_room_id)| {
|
||||
(room_id == this_room_id && event.event_id()? == event_id).then_some(event.clone())
|
||||
});
|
||||
let target_linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
|
||||
|
||||
let event = inner
|
||||
.events
|
||||
.items(&target_linked_chunk_id)
|
||||
.find_map(|(event, _pos)| (event.event_id()? == event_id).then_some(event.clone()));
|
||||
|
||||
Ok(event)
|
||||
}
|
||||
@@ -224,20 +238,17 @@ impl EventCacheStore for MemoryStore {
|
||||
room_id: &RoomId,
|
||||
event_id: &EventId,
|
||||
filters: Option<&[RelationType]>,
|
||||
) -> Result<Vec<Event>, Self::Error> {
|
||||
) -> Result<Vec<(Event, Option<Position>)>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
let target_linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
|
||||
|
||||
let filters = compute_filters_string(filters);
|
||||
|
||||
let related_events = inner
|
||||
.events
|
||||
.items()
|
||||
.filter_map(|(event, this_room_id)| {
|
||||
// Must be in the same room.
|
||||
if room_id != this_room_id {
|
||||
return None;
|
||||
}
|
||||
|
||||
.items(&target_linked_chunk_id)
|
||||
.filter_map(|(event, pos)| {
|
||||
// Must have a relation.
|
||||
let (related_to, rel_type) = extract_event_relation(event.raw())?;
|
||||
|
||||
@@ -248,9 +259,9 @@ impl EventCacheStore for MemoryStore {
|
||||
|
||||
// Must not be filtered out.
|
||||
if let Some(filters) = &filters {
|
||||
filters.contains(&rel_type).then_some(event.clone())
|
||||
filters.contains(&rel_type).then_some((event.clone(), pos))
|
||||
} else {
|
||||
Some(event.clone())
|
||||
Some((event.clone(), pos))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@@ -364,8 +375,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;
|
||||
|
||||
@@ -399,7 +410,7 @@ impl EventCacheStoreMedia for MemoryStore {
|
||||
if !ignore_policy && policy.exceeds_max_file_size(data.len() as u64) {
|
||||
// Do not store it.
|
||||
return Ok(());
|
||||
};
|
||||
}
|
||||
|
||||
// Now, let's add it.
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
@@ -185,8 +185,6 @@ pub type Result<T, E = EventCacheStoreError> = std::result::Result<T, E>;
|
||||
#[derive(Clone, Debug)]
|
||||
struct LockableEventCacheStore(Arc<DynEventCacheStore>);
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
|
||||
impl BackingStore for LockableEventCacheStore {
|
||||
type LockError = EventCacheStoreError;
|
||||
|
||||
|
||||
@@ -16,7 +16,10 @@ use std::{fmt, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
linked_chunk::{ChunkIdentifier, ChunkIdentifierGenerator, Position, RawChunk, Update},
|
||||
linked_chunk::{
|
||||
ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId, Position,
|
||||
RawChunk, Update,
|
||||
},
|
||||
AsyncTraitDeps,
|
||||
};
|
||||
use ruma::{events::relation::RelationType, EventId, MxcUri, OwnedEventId, RoomId};
|
||||
@@ -37,8 +40,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 +59,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
/// in-memory. This method aims at forwarding this update inside this store.
|
||||
async fn handle_linked_chunk_updates(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
updates: Vec<Update<Event, Gap>>,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
@@ -64,7 +67,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
async fn remove_room(&self, room_id: &RoomId) -> Result<(), Self::Error> {
|
||||
// Right now, this means removing all the linked chunk. If implementations
|
||||
// override this behavior, they should *also* include this code.
|
||||
self.handle_linked_chunk_updates(room_id, vec![Update::Clear]).await
|
||||
self.handle_linked_chunk_updates(LinkedChunkId::Room(room_id), vec![Update::Clear]).await
|
||||
}
|
||||
|
||||
/// Return all the raw components of a linked chunk, so the caller may
|
||||
@@ -72,16 +75,25 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
#[doc(hidden)]
|
||||
async fn load_all_chunks(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error>;
|
||||
|
||||
/// Load all of the chunks' metadata for the given [`LinkedChunkId`].
|
||||
///
|
||||
/// Chunks are unordered, and there's no guarantee that the chunks would
|
||||
/// form a valid linked chunk after reconstruction.
|
||||
async fn load_all_chunks_metadata(
|
||||
&self,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<Vec<ChunkMetadata>, 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,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<(Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator), Self::Error>;
|
||||
|
||||
/// Load the chunk before the chunk identified by `before_chunk_identifier`
|
||||
@@ -91,7 +103,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
/// This is used to iteratively load events for the `EventCache`.
|
||||
async fn load_previous_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
before_chunk_identifier: ChunkIdentifier,
|
||||
) -> Result<Option<RawChunk<Event, Gap>>, Self::Error>;
|
||||
|
||||
@@ -105,24 +117,33 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
/// ⚠ This is meant only for super specific use cases, where there shouldn't
|
||||
/// be any live in-memory linked chunks. In general, prefer using
|
||||
/// `EventCache::clear_all_rooms()` from the common SDK crate.
|
||||
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error>;
|
||||
async fn clear_all_linked_chunks(&self) -> Result<(), Self::Error>;
|
||||
|
||||
/// Given a set of event IDs, return the duplicated events along with their
|
||||
/// position if there are any.
|
||||
async fn filter_duplicated_events(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
events: Vec<OwnedEventId>,
|
||||
) -> Result<Vec<(OwnedEventId, Position)>, Self::Error>;
|
||||
|
||||
/// Find an event by its ID.
|
||||
/// Find an event by its ID in a room.
|
||||
async fn find_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_id: &EventId,
|
||||
) -> Result<Option<Event>, Self::Error>;
|
||||
|
||||
/// Find all the events that relate to a given event.
|
||||
/// Find all the events (alongside their position in the room's linked
|
||||
/// chunk, if available) that relate to a given event.
|
||||
///
|
||||
/// The only events which don't have a position are those which have been
|
||||
/// saved out-of-band using [`Self::save_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.
|
||||
@@ -131,7 +152,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
room_id: &RoomId,
|
||||
event_id: &EventId,
|
||||
filter: Option<&[RelationType]>,
|
||||
) -> Result<Vec<Event>, Self::Error>;
|
||||
) -> Result<Vec<(Event, Option<Position>)>, Self::Error>;
|
||||
|
||||
/// Save an event, that might or might not be part of an existing linked
|
||||
/// chunk.
|
||||
@@ -277,8 +298,8 @@ impl<T: fmt::Debug> fmt::Debug for EraseEventCacheStoreError<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
|
||||
type Error = EventCacheStoreError;
|
||||
|
||||
@@ -293,44 +314,54 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
|
||||
|
||||
async fn handle_linked_chunk_updates(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
updates: Vec<Update<Event, Gap>>,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.handle_linked_chunk_updates(room_id, updates).await.map_err(Into::into)
|
||||
self.0.handle_linked_chunk_updates(linked_chunk_id, updates).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn load_all_chunks(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
|
||||
self.0.load_all_chunks(room_id).await.map_err(Into::into)
|
||||
self.0.load_all_chunks(linked_chunk_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn load_all_chunks_metadata(
|
||||
&self,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<Vec<ChunkMetadata>, Self::Error> {
|
||||
self.0.load_all_chunks_metadata(linked_chunk_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn load_last_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
) -> Result<(Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator), Self::Error> {
|
||||
self.0.load_last_chunk(room_id).await.map_err(Into::into)
|
||||
self.0.load_last_chunk(linked_chunk_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn load_previous_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
before_chunk_identifier: ChunkIdentifier,
|
||||
) -> Result<Option<RawChunk<Event, Gap>>, Self::Error> {
|
||||
self.0.load_previous_chunk(room_id, before_chunk_identifier).await.map_err(Into::into)
|
||||
self.0
|
||||
.load_previous_chunk(linked_chunk_id, before_chunk_identifier)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error> {
|
||||
self.0.clear_all_rooms_chunks().await.map_err(Into::into)
|
||||
async fn clear_all_linked_chunks(&self) -> Result<(), Self::Error> {
|
||||
self.0.clear_all_linked_chunks().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn filter_duplicated_events(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
events: Vec<OwnedEventId>,
|
||||
) -> Result<Vec<(OwnedEventId, Position)>, Self::Error> {
|
||||
self.0.filter_duplicated_events(room_id, events).await.map_err(Into::into)
|
||||
self.0.filter_duplicated_events(linked_chunk_id, events).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn find_event(
|
||||
@@ -346,7 +377,7 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
|
||||
room_id: &RoomId,
|
||||
event_id: &EventId,
|
||||
filter: Option<&[RelationType]>,
|
||||
) -> Result<Vec<Event>, Self::Error> {
|
||||
) -> Result<Vec<(Event, Option<Position>)>, Self::Error> {
|
||||
self.0.find_event_relations(room_id, event_id, filter).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
|
||||
@@ -227,9 +227,10 @@ impl<'de> Deserialize<'de> for LatestEvent {
|
||||
Err(err) => variant_errors.push(err),
|
||||
}
|
||||
|
||||
Err(serde::de::Error::custom(
|
||||
format!("data did not match any variant of serialized LatestEvent (using serde_json). Observed errors: {variant_errors:?}")
|
||||
))
|
||||
Err(serde::de::Error::custom(format!(
|
||||
"data did not match any variant of serialized LatestEvent (using serde_json). \
|
||||
Observed errors: {variant_errors:?}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -629,7 +630,7 @@ mod tests {
|
||||
latest_event: LatestEvent,
|
||||
}
|
||||
|
||||
let event = TimelineEvent::new(
|
||||
let event = TimelineEvent::from_plaintext(
|
||||
Raw::from_json_string(json!({ "event_id": "$1" }).to_string()).unwrap(),
|
||||
);
|
||||
|
||||
@@ -654,8 +655,9 @@ mod tests {
|
||||
"event_id": "$1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"thread_summary": "None",
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))]
|
||||
#![cfg_attr(target_family = "wasm", allow(clippy::arc_with_non_send_sync))]
|
||||
#![warn(missing_docs, missing_debug_implementations)]
|
||||
|
||||
pub use matrix_sdk_common::*;
|
||||
@@ -34,10 +34,9 @@ pub mod latest_event;
|
||||
pub mod media;
|
||||
pub mod notification_settings;
|
||||
mod response_processors;
|
||||
mod rooms;
|
||||
mod room;
|
||||
|
||||
pub mod read_receipts;
|
||||
pub use read_receipts::PreviousEventsProvider;
|
||||
pub mod sliding_sync;
|
||||
|
||||
pub mod store;
|
||||
@@ -49,16 +48,16 @@ mod utils;
|
||||
#[cfg(feature = "uniffi")]
|
||||
uniffi::setup_scaffolding!();
|
||||
|
||||
pub use client::BaseClient;
|
||||
pub use client::{BaseClient, ThreadingSupport};
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
pub use http;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub use matrix_sdk_crypto as crypto;
|
||||
pub use once_cell;
|
||||
pub use rooms::{
|
||||
apply_redaction, EncryptionState, Room, RoomCreateWithCreatorEventContent, RoomDisplayName,
|
||||
RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember,
|
||||
RoomMembersUpdate, RoomMemberships, RoomState, RoomStateFilter,
|
||||
pub use room::{
|
||||
apply_redaction, EncryptionState, PredecessorRoom, Room, RoomCreateWithCreatorEventContent,
|
||||
RoomDisplayName, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons,
|
||||
RoomMember, RoomMembersUpdate, RoomMemberships, RoomState, RoomStateFilter, SuccessorRoom,
|
||||
};
|
||||
pub use store::{
|
||||
ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey,
|
||||
|
||||
@@ -122,8 +122,10 @@ use std::{
|
||||
num::NonZeroUsize,
|
||||
};
|
||||
|
||||
use eyeball_im::Vector;
|
||||
use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer};
|
||||
use matrix_sdk_common::{
|
||||
deserialized_responses::TimelineEvent, ring_buffer::RingBuffer,
|
||||
serde_helpers::extract_thread_root,
|
||||
};
|
||||
use ruma::{
|
||||
events::{
|
||||
poll::{start::PollStartEventContent, unstable_start::UnstablePollStartEventContent},
|
||||
@@ -138,6 +140,8 @@ use ruma::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{debug, instrument, trace, warn};
|
||||
|
||||
use crate::ThreadingSupport;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
struct LatestReadReceipt {
|
||||
/// The id of the event the read receipt is referring to. (Not the read
|
||||
@@ -202,7 +206,18 @@ impl RoomReadReceipts {
|
||||
///
|
||||
/// Returns whether a new event triggered a new unread/notification/mention.
|
||||
#[inline(always)]
|
||||
fn process_event(&mut self, event: &TimelineEvent, user_id: &UserId) {
|
||||
fn process_event(
|
||||
&mut self,
|
||||
event: &TimelineEvent,
|
||||
user_id: &UserId,
|
||||
threading_support: ThreadingSupport,
|
||||
) {
|
||||
if matches!(threading_support, ThreadingSupport::Enabled)
|
||||
&& extract_thread_root(event.raw()).is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if marks_as_unread(event.raw(), user_id) {
|
||||
self.num_unread += 1;
|
||||
}
|
||||
@@ -210,7 +225,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;
|
||||
};
|
||||
|
||||
@@ -241,6 +256,7 @@ impl RoomReadReceipts {
|
||||
receipt_event_id: &EventId,
|
||||
user_id: &UserId,
|
||||
events: impl IntoIterator<Item = &'a TimelineEvent>,
|
||||
threading_support: ThreadingSupport,
|
||||
) -> bool {
|
||||
let mut counting_receipts = false;
|
||||
|
||||
@@ -260,7 +276,7 @@ impl RoomReadReceipts {
|
||||
}
|
||||
|
||||
if counting_receipts {
|
||||
self.process_event(event, user_id);
|
||||
self.process_event(event, user_id, threading_support);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,19 +284,6 @@ impl RoomReadReceipts {
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider for timeline events prior to the current sync.
|
||||
pub trait PreviousEventsProvider: Send + Sync {
|
||||
/// Returns the list of known timeline events, in sync order, for the given
|
||||
/// room.
|
||||
fn for_room(&self, room_id: &RoomId) -> Vector<TimelineEvent>;
|
||||
}
|
||||
|
||||
impl PreviousEventsProvider for () {
|
||||
fn for_room(&self, _: &RoomId) -> Vector<TimelineEvent> {
|
||||
Vector::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Small helper to select the "best" receipt (that with the biggest sync
|
||||
/// order).
|
||||
struct ReceiptSelector {
|
||||
@@ -294,10 +297,7 @@ struct ReceiptSelector {
|
||||
}
|
||||
|
||||
impl ReceiptSelector {
|
||||
fn new(
|
||||
all_events: &Vector<TimelineEvent>,
|
||||
latest_active_receipt_event: Option<&EventId>,
|
||||
) -> Self {
|
||||
fn new(all_events: &[TimelineEvent], latest_active_receipt_event: Option<&EventId>) -> Self {
|
||||
let event_id_to_pos = Self::create_sync_index(all_events.iter());
|
||||
|
||||
let best_pos =
|
||||
@@ -457,23 +457,23 @@ 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,
|
||||
threading_support: ThreadingSupport,
|
||||
) {
|
||||
debug!(?read_receipts, "Starting.");
|
||||
debug!(?read_receipts, "Starting");
|
||||
|
||||
let all_events = if events_intersects(previous_events.iter(), new_events) {
|
||||
// The previous and new events sets can intersect, for instance if we restored
|
||||
// previous events from the disk cache, or a timeline was limited. This
|
||||
// means the old events will be cleared, because we don't reconcile
|
||||
// timelines in sliding sync (yet). As a result, forget
|
||||
// timelines in the event cache (yet). As a result, forget
|
||||
// about the previous events.
|
||||
Vector::from_iter(new_events.iter().cloned())
|
||||
new_events.to_owned()
|
||||
} else {
|
||||
let mut all_events = previous_events;
|
||||
all_events.extend(new_events.iter().cloned());
|
||||
all_events
|
||||
previous_events.extend(new_events.iter().cloned());
|
||||
previous_events
|
||||
};
|
||||
|
||||
let new_receipt = {
|
||||
@@ -481,6 +481,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 {
|
||||
@@ -506,7 +507,12 @@ pub(crate) fn compute_unread_counts(
|
||||
|
||||
// The event for the receipt is in `all_events`, so we'll find it and can count
|
||||
// safely from here.
|
||||
read_receipts.find_and_process_events(&event_id, user_id, all_events.iter());
|
||||
read_receipts.find_and_process_events(
|
||||
&event_id,
|
||||
user_id,
|
||||
all_events.iter(),
|
||||
threading_support,
|
||||
);
|
||||
|
||||
debug!(?read_receipts, "after finding a better receipt");
|
||||
return;
|
||||
@@ -520,7 +526,7 @@ pub(crate) fn compute_unread_counts(
|
||||
// for the next receipt.
|
||||
|
||||
for event in new_events {
|
||||
read_receipts.process_event(event, user_id);
|
||||
read_receipts.process_event(event, user_id, threading_support);
|
||||
}
|
||||
|
||||
debug!(?read_receipts, "no better receipt, {} new events", new_events.len());
|
||||
@@ -622,7 +628,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::{
|
||||
@@ -637,7 +642,10 @@ mod tests {
|
||||
};
|
||||
|
||||
use super::compute_unread_counts;
|
||||
use crate::read_receipts::{marks_as_unread, ReceiptSelector, RoomReadReceipts};
|
||||
use crate::{
|
||||
read_receipts::{marks_as_unread, ReceiptSelector, RoomReadReceipts},
|
||||
ThreadingSupport,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_room_message_marks_as_unread() {
|
||||
@@ -729,7 +737,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
|
||||
}
|
||||
|
||||
@@ -738,7 +746,7 @@ mod tests {
|
||||
// An interesting event from oneself doesn't count as a new unread message.
|
||||
let event = make_event(user_id, Vec::new());
|
||||
let mut receipts = RoomReadReceipts::default();
|
||||
receipts.process_event(&event, user_id);
|
||||
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
|
||||
assert_eq!(receipts.num_unread, 0);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
@@ -746,7 +754,7 @@ mod tests {
|
||||
// An interesting event from someone else does count as a new unread message.
|
||||
let event = make_event(user_id!("@bob:example.org"), Vec::new());
|
||||
let mut receipts = RoomReadReceipts::default();
|
||||
receipts.process_event(&event, user_id);
|
||||
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
|
||||
assert_eq!(receipts.num_unread, 1);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
@@ -754,7 +762,7 @@ mod tests {
|
||||
// Push actions computed beforehand are respected.
|
||||
let event = make_event(user_id!("@bob:example.org"), vec![Action::Notify]);
|
||||
let mut receipts = RoomReadReceipts::default();
|
||||
receipts.process_event(&event, user_id);
|
||||
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
|
||||
assert_eq!(receipts.num_unread, 1);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
assert_eq!(receipts.num_notifications, 1);
|
||||
@@ -764,7 +772,7 @@ mod tests {
|
||||
vec![Action::SetTweak(ruma::push::Tweak::Highlight(true))],
|
||||
);
|
||||
let mut receipts = RoomReadReceipts::default();
|
||||
receipts.process_event(&event, user_id);
|
||||
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
|
||||
assert_eq!(receipts.num_unread, 1);
|
||||
assert_eq!(receipts.num_mentions, 1);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
@@ -774,7 +782,7 @@ mod tests {
|
||||
vec![Action::SetTweak(ruma::push::Tweak::Highlight(true)), Action::Notify],
|
||||
);
|
||||
let mut receipts = RoomReadReceipts::default();
|
||||
receipts.process_event(&event, user_id);
|
||||
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
|
||||
assert_eq!(receipts.num_unread, 1);
|
||||
assert_eq!(receipts.num_mentions, 1);
|
||||
assert_eq!(receipts.num_notifications, 1);
|
||||
@@ -783,7 +791,7 @@ mod tests {
|
||||
// make sure to resist against it.
|
||||
let event = make_event(user_id!("@bob:example.org"), vec![Action::Notify, Action::Notify]);
|
||||
let mut receipts = RoomReadReceipts::default();
|
||||
receipts.process_event(&event, user_id);
|
||||
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
|
||||
assert_eq!(receipts.num_unread, 1);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
assert_eq!(receipts.num_notifications, 1);
|
||||
@@ -797,7 +805,9 @@ mod tests {
|
||||
// When provided with no events, we report not finding the event to which the
|
||||
// receipt relates.
|
||||
let mut receipts = RoomReadReceipts::default();
|
||||
assert!(receipts.find_and_process_events(ev0, user_id, &[]).not());
|
||||
assert!(receipts
|
||||
.find_and_process_events(ev0, user_id, &[], ThreadingSupport::Disabled)
|
||||
.not());
|
||||
assert_eq!(receipts.num_unread, 0);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
@@ -819,7 +829,12 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
assert!(receipts
|
||||
.find_and_process_events(ev0, user_id, &[make_event(event_id!("$1"))],)
|
||||
.find_and_process_events(
|
||||
ev0,
|
||||
user_id,
|
||||
&[make_event(event_id!("$1"))],
|
||||
ThreadingSupport::Disabled
|
||||
)
|
||||
.not());
|
||||
assert_eq!(receipts.num_unread, 42);
|
||||
assert_eq!(receipts.num_notifications, 13);
|
||||
@@ -834,7 +849,12 @@ mod tests {
|
||||
num_mentions: 37,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(receipts.find_and_process_events(ev0, user_id, &[make_event(ev0)]));
|
||||
assert!(receipts.find_and_process_events(
|
||||
ev0,
|
||||
user_id,
|
||||
&[make_event(ev0)],
|
||||
ThreadingSupport::Disabled
|
||||
),);
|
||||
assert_eq!(receipts.num_unread, 0);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
@@ -856,6 +876,7 @@ mod tests {
|
||||
make_event(event_id!("$2")),
|
||||
make_event(event_id!("$3"))
|
||||
],
|
||||
ThreadingSupport::Disabled
|
||||
)
|
||||
.not());
|
||||
assert_eq!(receipts.num_unread, 42);
|
||||
@@ -879,6 +900,7 @@ mod tests {
|
||||
make_event(event_id!("$2")),
|
||||
make_event(event_id!("$3"))
|
||||
],
|
||||
ThreadingSupport::Disabled
|
||||
));
|
||||
assert_eq!(receipts.num_unread, 2);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
@@ -901,6 +923,7 @@ mod tests {
|
||||
make_event(event_id!("$2")),
|
||||
make_event(event_id!("$3"))
|
||||
],
|
||||
ThreadingSupport::Disabled
|
||||
));
|
||||
assert_eq!(receipts.num_unread, 2);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
@@ -915,7 +938,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();
|
||||
@@ -926,7 +949,7 @@ mod tests {
|
||||
.add(receipt_event_id, user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
|
||||
.into_content();
|
||||
|
||||
let mut read_receipts = Default::default();
|
||||
let mut read_receipts = RoomReadReceipts::default();
|
||||
compute_unread_counts(
|
||||
user_id,
|
||||
room_id,
|
||||
@@ -934,14 +957,15 @@ mod tests {
|
||||
previous_events.clone(),
|
||||
&[ev1.clone(), ev2.clone()],
|
||||
&mut read_receipts,
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
// It did find the receipt event (ev1).
|
||||
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();
|
||||
@@ -952,13 +976,14 @@ mod tests {
|
||||
previous_events,
|
||||
&[new_event],
|
||||
&mut read_receipts,
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
// Only the new event should be added.
|
||||
assert_eq!(read_receipts.num_unread, 2);
|
||||
}
|
||||
|
||||
fn make_test_events(user_id: &UserId) -> Vector<TimelineEvent> {
|
||||
fn make_test_events(user_id: &UserId) -> Vec<TimelineEvent> {
|
||||
let f = EventFactory::new().sender(user_id);
|
||||
let ev1 = f.text_msg("With the lights out, it's less dangerous").event_id(event_id!("$1"));
|
||||
let ev2 = f.text_msg("Here we are now, entertain us").event_id(event_id!("$2"));
|
||||
@@ -976,7 +1001,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
|
||||
@@ -1018,6 +1043,7 @@ mod tests {
|
||||
all_events.clone(),
|
||||
&[],
|
||||
&mut read_receipts,
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
assert!(
|
||||
@@ -1039,6 +1065,7 @@ mod tests {
|
||||
head_events.clone(),
|
||||
&tail_events,
|
||||
&mut read_receipts,
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
assert!(
|
||||
@@ -1083,6 +1110,7 @@ mod tests {
|
||||
events,
|
||||
&[], // no new events
|
||||
&mut read_receipts,
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
// Then there are no unread events,
|
||||
@@ -1120,6 +1148,7 @@ mod tests {
|
||||
events,
|
||||
&[ev0], // duplicate event!
|
||||
&mut read_receipts,
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
// All events are unread, and there's no pending receipt.
|
||||
@@ -1165,7 +1194,7 @@ mod tests {
|
||||
|
||||
{
|
||||
// No initial active receipt, so the first receipt we get *will* win.
|
||||
let mut selector = ReceiptSelector::new(&vec![].into(), None);
|
||||
let mut selector = ReceiptSelector::new(&[], None);
|
||||
selector.try_select_later(event_id!("$1"), 0);
|
||||
let best_receipt = selector.select();
|
||||
assert_eq!(best_receipt.unwrap().event_id, event_id!("$1"));
|
||||
@@ -1203,11 +1232,11 @@ mod tests {
|
||||
let f = EventFactory::new().sender(sender);
|
||||
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
|
||||
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
|
||||
let events: Vector<_> = vec![ev1, ev2].into();
|
||||
let events = &[ev1, ev2][..];
|
||||
|
||||
{
|
||||
// No pending receipt => no better receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, None);
|
||||
let mut selector = ReceiptSelector::new(events, None);
|
||||
|
||||
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
|
||||
selector.handle_pending_receipts(&mut pending);
|
||||
@@ -1221,7 +1250,7 @@ mod tests {
|
||||
{
|
||||
// No pending receipt, and there was an active last receipt => no better
|
||||
// receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$1")));
|
||||
let mut selector = ReceiptSelector::new(events, Some(event_id!("$1")));
|
||||
|
||||
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
|
||||
selector.handle_pending_receipts(&mut pending);
|
||||
@@ -1239,11 +1268,11 @@ mod tests {
|
||||
let f = EventFactory::new().sender(sender);
|
||||
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
|
||||
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
|
||||
let events: Vector<_> = vec![ev1, ev2].into();
|
||||
let events = &[ev1, ev2][..];
|
||||
|
||||
{
|
||||
// A pending receipt for an event that is still missing => no better receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, None);
|
||||
let mut selector = ReceiptSelector::new(events, None);
|
||||
|
||||
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
|
||||
pending.push(owned_event_id!("$3"));
|
||||
@@ -1257,7 +1286,7 @@ mod tests {
|
||||
|
||||
{
|
||||
// Ditto but there was an active receipt => no better receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$1")));
|
||||
let mut selector = ReceiptSelector::new(events, Some(event_id!("$1")));
|
||||
|
||||
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
|
||||
pending.push(owned_event_id!("$3"));
|
||||
@@ -1276,11 +1305,11 @@ mod tests {
|
||||
let f = EventFactory::new().sender(sender);
|
||||
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
|
||||
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
|
||||
let events: Vector<_> = vec![ev1, ev2].into();
|
||||
let events = &[ev1, ev2][..];
|
||||
|
||||
{
|
||||
// A pending receipt for an event that is present => better receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, None);
|
||||
let mut selector = ReceiptSelector::new(events, None);
|
||||
|
||||
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
|
||||
pending.push(owned_event_id!("$2"));
|
||||
@@ -1296,7 +1325,7 @@ mod tests {
|
||||
|
||||
{
|
||||
// Mixed found and not found receipt => better receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, None);
|
||||
let mut selector = ReceiptSelector::new(events, None);
|
||||
|
||||
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
|
||||
pending.push(owned_event_id!("$1"));
|
||||
@@ -1318,12 +1347,12 @@ mod tests {
|
||||
let f = EventFactory::new().sender(sender);
|
||||
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
|
||||
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
|
||||
let events: Vector<_> = vec![ev1, ev2].into();
|
||||
let events = &[ev1, ev2][..];
|
||||
|
||||
{
|
||||
// Same, and there was an initial receipt that was less good than the one we
|
||||
// selected => better receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$1")));
|
||||
let mut selector = ReceiptSelector::new(events, Some(event_id!("$1")));
|
||||
|
||||
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
|
||||
pending.push(owned_event_id!("$2"));
|
||||
@@ -1339,7 +1368,7 @@ mod tests {
|
||||
|
||||
{
|
||||
// Same, but the previous receipt was better => no better receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$2")));
|
||||
let mut selector = ReceiptSelector::new(events, Some(event_id!("$2")));
|
||||
|
||||
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
|
||||
pending.push(owned_event_id!("$1"));
|
||||
@@ -1484,26 +1513,26 @@ mod tests {
|
||||
// When the selector sees only other users' events,
|
||||
let mut selector = ReceiptSelector::new(&events, None);
|
||||
// And I search for my implicit read receipt,
|
||||
selector.try_match_implicit(&myself, &events.iter().cloned().collect::<Vec<_>>());
|
||||
selector.try_match_implicit(&myself, &events);
|
||||
// Then I don't find any.
|
||||
let best_receipt = selector.select();
|
||||
assert!(best_receipt.is_none());
|
||||
|
||||
// Now, if there are events I've written too...
|
||||
let f = EventFactory::new();
|
||||
events.push_back(
|
||||
events.push(
|
||||
f.text_msg("A mulatto, an albino")
|
||||
.sender(&myself)
|
||||
.event_id(event_id!("$6"))
|
||||
.into_event(),
|
||||
);
|
||||
events.push_back(
|
||||
events.push(
|
||||
f.text_msg("A mosquito, my libido").sender(bob).event_id(event_id!("$7")).into_event(),
|
||||
);
|
||||
|
||||
let mut selector = ReceiptSelector::new(&events, None);
|
||||
// And I search for my implicit read receipt,
|
||||
selector.try_match_implicit(&myself, &events.iter().cloned().collect::<Vec<_>>());
|
||||
selector.try_match_implicit(&myself, &events);
|
||||
// Then my last sent event counts as a read receipt.
|
||||
let best_receipt = selector.select();
|
||||
assert_eq!(best_receipt.unwrap().event_id, event_id!("$6"));
|
||||
@@ -1520,7 +1549,7 @@ mod tests {
|
||||
|
||||
// One by me,
|
||||
let f = EventFactory::new();
|
||||
events.push_back(
|
||||
events.push(
|
||||
f.text_msg("A mulatto, an albino")
|
||||
.sender(user_id)
|
||||
.event_id(event_id!("$6"))
|
||||
@@ -1528,10 +1557,10 @@ mod tests {
|
||||
);
|
||||
|
||||
// And others by Bob,
|
||||
events.push_back(
|
||||
events.push(
|
||||
f.text_msg("A mosquito, my libido").sender(bob).event_id(event_id!("$7")).into_event(),
|
||||
);
|
||||
events.push_back(
|
||||
events.push(
|
||||
f.text_msg("A denial, a denial").sender(bob).event_id(event_id!("$8")).into_event(),
|
||||
);
|
||||
|
||||
@@ -1551,9 +1580,10 @@ mod tests {
|
||||
user_id,
|
||||
room_id,
|
||||
Some(&receipt_event),
|
||||
Vector::new(),
|
||||
Vec::new(),
|
||||
&events,
|
||||
&mut read_receipts,
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
// Only the last two events sent by Bob count as unread.
|
||||
@@ -1565,4 +1595,60 @@ mod tests {
|
||||
// And the active receipt is the implicit one on my event.
|
||||
assert_eq!(read_receipts.latest_active.unwrap().event_id, event_id!("$6"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_unread_counts_with_threading_enabled() {
|
||||
fn make_event(user_id: &UserId, thread_root: &EventId) -> TimelineEvent {
|
||||
EventFactory::new()
|
||||
.text_msg("A")
|
||||
.sender(user_id)
|
||||
.event_id(event_id!("$ida"))
|
||||
.in_thread(thread_root, event_id!("$latest_event"))
|
||||
.into_event()
|
||||
}
|
||||
|
||||
let mut receipts = RoomReadReceipts::default();
|
||||
|
||||
let own_alice = user_id!("@alice:example.org");
|
||||
let bob = user_id!("@bob:example.org");
|
||||
|
||||
// Threaded messages from myself or other users shouldn't change the
|
||||
// unread counts.
|
||||
receipts.process_event(
|
||||
&make_event(own_alice, event_id!("$some_thread_root")),
|
||||
own_alice,
|
||||
ThreadingSupport::Enabled,
|
||||
);
|
||||
receipts.process_event(
|
||||
&make_event(own_alice, event_id!("$some_other_thread_root")),
|
||||
own_alice,
|
||||
ThreadingSupport::Enabled,
|
||||
);
|
||||
|
||||
receipts.process_event(
|
||||
&make_event(bob, event_id!("$some_thread_root")),
|
||||
own_alice,
|
||||
ThreadingSupport::Enabled,
|
||||
);
|
||||
receipts.process_event(
|
||||
&make_event(bob, event_id!("$some_other_thread_root")),
|
||||
own_alice,
|
||||
ThreadingSupport::Enabled,
|
||||
);
|
||||
|
||||
assert_eq!(receipts.num_unread, 0);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
|
||||
// Processing an unthreaded message should still count as unread.
|
||||
receipts.process_event(
|
||||
&EventFactory::new().text_msg("A").sender(bob).event_id(event_id!("$ida")).into_event(),
|
||||
own_alice,
|
||||
ThreadingSupport::Enabled,
|
||||
);
|
||||
|
||||
assert_eq!(receipts.num_unread, 1);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,10 @@ use ruma::{
|
||||
use tracing::{instrument, warn};
|
||||
|
||||
use super::super::{Context, RoomInfoNotableUpdates};
|
||||
use crate::{store::BaseStateStore, RoomInfo, RoomInfoNotableUpdateReasons, StateChanges};
|
||||
use crate::{
|
||||
room::AccountDataSource, store::BaseStateStore, RoomInfo, RoomInfoNotableUpdateReasons,
|
||||
StateChanges,
|
||||
};
|
||||
|
||||
#[instrument(skip_all, fields(?room_id))]
|
||||
pub async fn for_room(
|
||||
@@ -49,6 +52,7 @@ pub async fn for_room(
|
||||
on_unread_marker(
|
||||
room_id,
|
||||
&event.content,
|
||||
AccountDataSource::Stable,
|
||||
room_info,
|
||||
&mut context.room_info_notable_updates,
|
||||
);
|
||||
@@ -64,6 +68,7 @@ pub async fn for_room(
|
||||
on_unread_marker(
|
||||
room_id,
|
||||
&event.content.0,
|
||||
AccountDataSource::Unstable,
|
||||
room_info,
|
||||
&mut context.room_info_notable_updates,
|
||||
);
|
||||
@@ -127,9 +132,17 @@ fn on_room_info<F>(
|
||||
fn on_unread_marker(
|
||||
room_id: &RoomId,
|
||||
content: &MarkedUnreadEventContent,
|
||||
source: AccountDataSource,
|
||||
room_info: &mut RoomInfo,
|
||||
room_info_notable_updates: &mut RoomInfoNotableUpdates,
|
||||
) {
|
||||
if room_info.base_info.is_marked_unread_source == AccountDataSource::Stable
|
||||
&& source != AccountDataSource::Stable
|
||||
{
|
||||
// Ignore the unstable source if a stable source was used previously.
|
||||
return;
|
||||
}
|
||||
|
||||
if room_info.base_info.is_marked_unread != content.unread {
|
||||
// Notify the room list about a manual read marker change if the
|
||||
// value's changed.
|
||||
@@ -140,4 +153,5 @@ fn on_unread_marker(
|
||||
}
|
||||
|
||||
room_info.base_info.is_marked_unread = content.unread;
|
||||
room_info.base_info.is_marked_unread_source = source;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,16 @@ use crate::{
|
||||
Result,
|
||||
};
|
||||
|
||||
/// Save the [`StateChanges`] from the [`Context`] inside the [`BaseStateStore`]
|
||||
/// only! The changes aren't applied on the in-memory rooms.
|
||||
#[instrument(skip_all)]
|
||||
pub async fn save_only(context: Context, state_store: &BaseStateStore) -> Result<()> {
|
||||
save_changes(&context, state_store, None).await?;
|
||||
broadcast_room_info_notable_updates(&context, state_store);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save the [`StateChanges`] from the [`Context`] inside the
|
||||
/// [`BaseStateStore`], and apply them on the in-memory rooms.
|
||||
#[instrument(skip_all)]
|
||||
@@ -39,28 +49,36 @@ pub async fn save_and_apply(
|
||||
let previous_ignored_user_list =
|
||||
state_store.get_account_data_event_static().await.ok().flatten();
|
||||
|
||||
state_store.save_changes(&context.state_changes).await?;
|
||||
|
||||
if let Some(sync_token) = sync_token {
|
||||
*state_store.sync_token.write().await = Some(sync_token);
|
||||
}
|
||||
apply_changes(context, state_store, ignore_user_list_changes, previous_ignored_user_list);
|
||||
save_changes(&context, state_store, sync_token).await?;
|
||||
apply_changes(&context, ignore_user_list_changes, previous_ignored_user_list);
|
||||
broadcast_room_info_notable_updates(&context, state_store);
|
||||
|
||||
trace!("applied changes");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_changes(
|
||||
context: Context,
|
||||
async fn save_changes(
|
||||
context: &Context,
|
||||
state_store: &BaseStateStore,
|
||||
sync_token: Option<String>,
|
||||
) -> Result<()> {
|
||||
state_store.save_changes(&context.state_changes).await?;
|
||||
|
||||
if let Some(sync_token) = sync_token {
|
||||
*state_store.sync_token.write().await = Some(sync_token);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_changes(
|
||||
context: &Context,
|
||||
ignore_user_list_changes: &SharedObservable<Vec<String>>,
|
||||
previous_ignored_user_list: Option<Raw<IgnoredUserListEvent>>,
|
||||
) {
|
||||
let (state_changes, room_info_notable_updates) = context.into_parts();
|
||||
|
||||
if let Some(event) =
|
||||
state_changes.account_data.get(&GlobalAccountDataEventType::IgnoredUserList)
|
||||
context.state_changes.account_data.get(&GlobalAccountDataEventType::IgnoredUserList)
|
||||
{
|
||||
match event.deserialize_as::<IgnoredUserListEvent>() {
|
||||
Ok(event) => {
|
||||
@@ -93,11 +111,13 @@ fn apply_changes(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (room_id, room_info) in &state_changes.room_infos {
|
||||
fn broadcast_room_info_notable_updates(context: &Context, state_store: &BaseStateStore) {
|
||||
for (room_id, room_info) in &context.state_changes.room_infos {
|
||||
if let Some(room) = state_store.room(room_id) {
|
||||
let room_info_notable_update_reasons =
|
||||
room_info_notable_updates.get(room_id).copied().unwrap_or_default();
|
||||
context.room_info_notable_updates.get(room_id).copied().unwrap_or_default();
|
||||
|
||||
room.set_room_info(room_info.clone(), room_info_notable_update_reasons)
|
||||
}
|
||||
|
||||
@@ -13,12 +13,10 @@
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use matrix_sdk_crypto::{
|
||||
DecryptionSettings, OlmMachine, RoomEventDecryptionResult, TrustRequirement,
|
||||
};
|
||||
use matrix_sdk_crypto::RoomEventDecryptionResult;
|
||||
use ruma::{events::AnySyncTimelineEvent, serde::Raw, RoomId};
|
||||
|
||||
use super::super::{verification, Context};
|
||||
use super::{super::verification, E2EE};
|
||||
use crate::Result;
|
||||
|
||||
/// Attempt to decrypt the given raw event into a [`TimelineEvent`].
|
||||
@@ -29,38 +27,29 @@ use crate::Result;
|
||||
///
|
||||
/// Returns `Ok(None)` if encryption is not configured.
|
||||
pub async fn sync_timeline_event(
|
||||
context: &mut Context,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
e2ee: E2EE<'_>,
|
||||
event: &Raw<AnySyncTimelineEvent>,
|
||||
room_id: &RoomId,
|
||||
decryption_trust_requirement: TrustRequirement,
|
||||
verification_is_allowed: bool,
|
||||
) -> Result<Option<TimelineEvent>> {
|
||||
let Some(olm) = olm_machine else { return Ok(None) };
|
||||
|
||||
let decryption_settings =
|
||||
DecryptionSettings { sender_device_trust_requirement: decryption_trust_requirement };
|
||||
let Some(olm) = e2ee.olm_machine else { return Ok(None) };
|
||||
|
||||
Ok(Some(
|
||||
match olm.try_decrypt_room_event(event.cast_ref(), room_id, &decryption_settings).await? {
|
||||
match olm
|
||||
.try_decrypt_room_event(event.cast_ref(), room_id, e2ee.decryption_settings)
|
||||
.await?
|
||||
{
|
||||
RoomEventDecryptionResult::Decrypted(decrypted) => {
|
||||
let timeline_event = TimelineEvent::from(decrypted);
|
||||
// Note: the push actions are set by the caller.
|
||||
let timeline_event = TimelineEvent::from_decrypted(decrypted, None);
|
||||
|
||||
if let Ok(sync_timeline_event) = timeline_event.raw().deserialize() {
|
||||
verification::process_if_relevant(
|
||||
context,
|
||||
&sync_timeline_event,
|
||||
verification_is_allowed,
|
||||
olm_machine,
|
||||
room_id,
|
||||
)
|
||||
.await?;
|
||||
verification::process_if_relevant(&sync_timeline_event, e2ee, room_id).await?;
|
||||
}
|
||||
|
||||
timeline_event
|
||||
}
|
||||
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
|
||||
TimelineEvent::new_utd_event(event.clone(), utd_info)
|
||||
TimelineEvent::from_utd(event.clone(), utd_info)
|
||||
}
|
||||
},
|
||||
))
|
||||
|
||||
@@ -12,6 +12,26 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk_crypto::{DecryptionSettings, OlmMachine};
|
||||
|
||||
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_settings: &'a DecryptionSettings,
|
||||
pub verification_is_allowed: bool,
|
||||
}
|
||||
|
||||
impl<'a> E2EE<'a> {
|
||||
pub fn new(
|
||||
olm_machine: Option<&'a OlmMachine>,
|
||||
decryption_settings: &'a DecryptionSettings,
|
||||
verification_is_allowed: bool,
|
||||
) -> Self {
|
||||
Self { olm_machine, decryption_settings, verification_is_allowed }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use matrix_sdk_crypto::{store::RoomKeyInfo, EncryptionSyncChanges, OlmMachine};
|
||||
use matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent;
|
||||
use matrix_sdk_crypto::{store::types::RoomKeyInfo, EncryptionSyncChanges, OlmMachine};
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::{v3, v5, DeviceLists},
|
||||
events::AnyToDeviceEvent,
|
||||
@@ -22,7 +23,6 @@ use ruma::{
|
||||
OneTimeKeyAlgorithm, UInt,
|
||||
};
|
||||
|
||||
use super::super::Context;
|
||||
use crate::Result;
|
||||
|
||||
/// Process the to-device events and other related e2ee data based on a response
|
||||
@@ -31,13 +31,11 @@ use crate::Result;
|
||||
/// This returns a list of all the to-device events that were passed in but
|
||||
/// encrypted ones were replaced with their decrypted version.
|
||||
pub async fn from_msc4186(
|
||||
context: &mut Context,
|
||||
to_device: Option<&v5::response::ToDevice>,
|
||||
e2ee: &v5::response::E2EE,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
) -> Result<Output> {
|
||||
process(
|
||||
context,
|
||||
olm_machine,
|
||||
to_device.as_ref().map(|to_device| to_device.events.clone()).unwrap_or_default(),
|
||||
&e2ee.device_lists,
|
||||
@@ -54,12 +52,10 @@ pub async fn from_msc4186(
|
||||
/// This returns a list of all the to-device events that were passed in but
|
||||
/// encrypted ones were replaced with their decrypted version.
|
||||
pub async fn from_sync_v2(
|
||||
context: &mut Context,
|
||||
response: &v3::Response,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
) -> Result<Output> {
|
||||
process(
|
||||
context,
|
||||
olm_machine,
|
||||
response.to_device.events.clone(),
|
||||
&response.device_lists,
|
||||
@@ -75,7 +71,6 @@ pub async fn from_sync_v2(
|
||||
/// This returns a list of all the to-device events that were passed in but
|
||||
/// encrypted ones were replaced with their decrypted version.
|
||||
async fn process(
|
||||
_context: &mut Context,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
to_device_events: Vec<Raw<AnyToDeviceEvent>>,
|
||||
device_lists: &DeviceLists,
|
||||
@@ -99,19 +94,35 @@ async fn process(
|
||||
let (events, room_key_updates) =
|
||||
olm_machine.receive_sync_changes(encryption_sync_changes).await?;
|
||||
|
||||
Output { decrypted_to_device_events: events, room_key_updates: Some(room_key_updates) }
|
||||
Output { processed_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.
|
||||
// If we have no `OlmMachine`, just return the clear events that were passed in.
|
||||
// The encrypted ones are dropped as they are un-usable.
|
||||
// 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,
|
||||
processed_to_device_events: encryption_sync_changes
|
||||
.to_device_events
|
||||
.into_iter()
|
||||
.map(|raw| {
|
||||
if let Ok(Some(event_type)) = raw.get_field::<String>("type") {
|
||||
if event_type == "m.room.encrypted" {
|
||||
ProcessedToDeviceEvent::UnableToDecrypt(raw)
|
||||
} else {
|
||||
ProcessedToDeviceEvent::PlainText(raw)
|
||||
}
|
||||
} else {
|
||||
// Exclude events with no type
|
||||
ProcessedToDeviceEvent::Invalid(raw)
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
room_key_updates: None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub struct Output {
|
||||
pub decrypted_to_device_events: Vec<Raw<AnyToDeviceEvent>>,
|
||||
pub processed_to_device_events: Vec<ProcessedToDeviceEvent>,
|
||||
pub room_key_updates: Option<Vec<RoomKeyInfo>>,
|
||||
}
|
||||
|
||||
@@ -17,12 +17,10 @@ use std::collections::BTreeSet;
|
||||
use matrix_sdk_crypto::OlmMachine;
|
||||
use ruma::{OwnedUserId, RoomId};
|
||||
|
||||
use super::super::Context;
|
||||
use crate::{store::BaseStateStore, EncryptionState, Result, RoomMemberships};
|
||||
|
||||
/// Update tracked users, if the room is encrypted.
|
||||
pub async fn update(
|
||||
_context: &mut Context,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
room_encryption_state: EncryptionState,
|
||||
user_ids_to_track: &BTreeSet<OwnedUserId>,
|
||||
@@ -41,7 +39,6 @@ pub async fn update(
|
||||
/// Update tracked users, if the room is encrypted, or if the room has become
|
||||
/// encrypted.
|
||||
pub async fn update_or_set_if_room_is_newly_encrypted(
|
||||
_context: &mut Context,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
user_ids_to_track: &BTreeSet<OwnedUserId>,
|
||||
new_room_encryption_state: EncryptionState,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use ruma::{events::AnySyncEphemeralRoomEvent, serde::Raw, RoomId};
|
||||
use tracing::info;
|
||||
|
||||
use super::Context;
|
||||
|
||||
/// Dispatch [`AnySyncEphemeralRoomEvent`]s on the [`Context`].
|
||||
pub fn dispatch(
|
||||
context: &mut Context,
|
||||
raw_events: &[Raw<AnySyncEphemeralRoomEvent>],
|
||||
room_id: &RoomId,
|
||||
) {
|
||||
for raw_event in raw_events {
|
||||
dispatch_receipt(context, raw_event, room_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispatch the [`AnySyncEphemeralRoomEvent::Receipt`] on the [`Context`].
|
||||
pub(super) fn dispatch_receipt(
|
||||
context: &mut Context,
|
||||
raw_event: &Raw<AnySyncEphemeralRoomEvent>,
|
||||
room_id: &RoomId,
|
||||
) {
|
||||
match raw_event.deserialize() {
|
||||
Ok(AnySyncEphemeralRoomEvent::Receipt(event)) => {
|
||||
context.state_changes.add_receipts(room_id, event.content);
|
||||
}
|
||||
|
||||
Ok(_) => {}
|
||||
|
||||
Err(e) => {
|
||||
let event_id = raw_event.get_field::<String>("event_id").ok().flatten();
|
||||
|
||||
info!(?room_id, event_id, "Failed to deserialize ephemeral room event: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,10 @@
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use matrix_sdk_crypto::{
|
||||
DecryptionSettings, OlmMachine, RoomEventDecryptionResult, TrustRequirement,
|
||||
};
|
||||
use matrix_sdk_crypto::RoomEventDecryptionResult;
|
||||
use ruma::{events::AnySyncTimelineEvent, serde::Raw, RoomId};
|
||||
|
||||
use super::{verification, Context};
|
||||
use super::{e2ee::E2EE, verification, Context};
|
||||
use crate::{
|
||||
latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent},
|
||||
Result, Room,
|
||||
@@ -33,27 +31,19 @@ use crate::{
|
||||
pub async fn decrypt_from_rooms(
|
||||
context: &mut Context,
|
||||
rooms: Vec<Room>,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
decryption_trust_requirement: TrustRequirement,
|
||||
verification_is_allowed: bool,
|
||||
e2ee: E2EE<'_>,
|
||||
) -> Result<()> {
|
||||
let Some(olm_machine) = olm_machine else {
|
||||
// All functions used by this one expect an `OlmMachine`. Return if there is
|
||||
// none.
|
||||
if e2ee.olm_machine.is_none() {
|
||||
return Ok(());
|
||||
};
|
||||
}
|
||||
|
||||
for room in rooms {
|
||||
// Try to find a message we can decrypt and is suitable for using as the latest
|
||||
// event. If we found one, set it as the latest and delete any older
|
||||
// encrypted events
|
||||
if let Some((found, found_index)) = find_suitable_and_decrypt(
|
||||
context,
|
||||
olm_machine,
|
||||
&room,
|
||||
&decryption_trust_requirement,
|
||||
verification_is_allowed,
|
||||
)
|
||||
.await
|
||||
{
|
||||
if let Some((found, found_index)) = find_suitable_and_decrypt(&room, &e2ee).await {
|
||||
room.on_latest_event_decrypted(
|
||||
found,
|
||||
found_index,
|
||||
@@ -67,11 +57,8 @@ pub async fn decrypt_from_rooms(
|
||||
}
|
||||
|
||||
async fn find_suitable_and_decrypt(
|
||||
context: &mut Context,
|
||||
olm_machine: &OlmMachine,
|
||||
room: &Room,
|
||||
decryption_trust_requirement: &TrustRequirement,
|
||||
verification_is_allowed: bool,
|
||||
e2ee: &E2EE<'_>,
|
||||
) -> Option<(Box<LatestEvent>, usize)> {
|
||||
let enc_events = room.latest_encrypted_events();
|
||||
let power_levels = room.power_levels().await.ok();
|
||||
@@ -82,14 +69,8 @@ async fn find_suitable_and_decrypt(
|
||||
// Size of the `decrypt_sync_room_event` future should not impact this
|
||||
// async fn since it is likely that there aren't even any encrypted
|
||||
// events when calling it.
|
||||
let decrypt_sync_room_event = Box::pin(decrypt_sync_room_event(
|
||||
context,
|
||||
olm_machine,
|
||||
event,
|
||||
room.room_id(),
|
||||
decryption_trust_requirement,
|
||||
verification_is_allowed,
|
||||
));
|
||||
let decrypt_sync_room_event =
|
||||
Box::pin(decrypt_sync_room_event(event, e2ee, room.room_id()));
|
||||
|
||||
if let Ok(decrypted) = decrypt_sync_room_event.await {
|
||||
// We found an event we can decrypt
|
||||
@@ -119,41 +100,34 @@ async fn find_suitable_and_decrypt(
|
||||
/// representing the decryption error; in the case of problems with our
|
||||
/// application, returns `Err`.
|
||||
///
|
||||
/// Returns `Ok(None)` if encryption is not configured.
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if there is no [`OlmMachine`] in [`E2EE`].
|
||||
async fn decrypt_sync_room_event(
|
||||
context: &mut Context,
|
||||
olm_machine: &OlmMachine,
|
||||
event: &Raw<AnySyncTimelineEvent>,
|
||||
e2ee: &E2EE<'_>,
|
||||
room_id: &RoomId,
|
||||
decryption_trust_requirement: &TrustRequirement,
|
||||
verification_is_allowed: bool,
|
||||
) -> Result<TimelineEvent> {
|
||||
let decryption_settings =
|
||||
DecryptionSettings { sender_device_trust_requirement: *decryption_trust_requirement };
|
||||
|
||||
let event = match olm_machine
|
||||
.try_decrypt_room_event(event.cast_ref(), room_id, &decryption_settings)
|
||||
let event = match e2ee
|
||||
.olm_machine
|
||||
.expect("An `OlmMachine` is expected")
|
||||
.try_decrypt_room_event(event.cast_ref(), room_id, e2ee.decryption_settings)
|
||||
.await?
|
||||
{
|
||||
RoomEventDecryptionResult::Decrypted(decrypted) => {
|
||||
let event: TimelineEvent = decrypted.into();
|
||||
// We're fine not setting the push actions for the latest event.
|
||||
let event = TimelineEvent::from_decrypted(decrypted, None);
|
||||
|
||||
if let Ok(sync_timeline_event) = event.raw().deserialize() {
|
||||
verification::process_if_relevant(
|
||||
context,
|
||||
&sync_timeline_event,
|
||||
verification_is_allowed,
|
||||
Some(olm_machine),
|
||||
room_id,
|
||||
)
|
||||
.await?;
|
||||
verification::process_if_relevant(&sync_timeline_event, e2ee.clone(), room_id)
|
||||
.await?;
|
||||
}
|
||||
|
||||
event
|
||||
}
|
||||
|
||||
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
|
||||
TimelineEvent::new_utd_event(event.clone(), utd_info)
|
||||
TimelineEvent::from_utd(event.clone(), utd_info)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -167,11 +141,8 @@ mod tests {
|
||||
};
|
||||
use ruma::{event_id, events::room::member::MembershipState, room_id, user_id};
|
||||
|
||||
use super::{decrypt_from_rooms, Context};
|
||||
use crate::{
|
||||
rooms::normal::RoomInfoNotableUpdateReasons, test_utils::logged_in_base_client,
|
||||
StateChanges,
|
||||
};
|
||||
use super::{decrypt_from_rooms, Context, E2EE};
|
||||
use crate::{room::RoomInfoNotableUpdateReasons, test_utils::logged_in_base_client};
|
||||
|
||||
#[async_test]
|
||||
async fn test_when_there_are_no_latest_encrypted_events_decrypting_them_does_nothing() {
|
||||
@@ -203,14 +174,16 @@ mod tests {
|
||||
assert!(room.latest_event().is_none());
|
||||
|
||||
// When I tell it to do some decryption
|
||||
let mut context = Context::new(StateChanges::default(), Default::default());
|
||||
let mut context = Context::default();
|
||||
|
||||
decrypt_from_rooms(
|
||||
&mut context,
|
||||
vec![room.clone()],
|
||||
client.olm_machine().await.as_ref(),
|
||||
client.decryption_trust_requirement,
|
||||
client.handle_verification_events,
|
||||
E2EE::new(
|
||||
client.olm_machine().await.as_ref(),
|
||||
&client.decryption_settings,
|
||||
client.handle_verification_events,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -16,9 +16,12 @@ pub mod account_data;
|
||||
pub mod changes;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub mod e2ee;
|
||||
pub mod ephemeral_events;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub mod latest_event;
|
||||
pub mod notification;
|
||||
pub mod profiles;
|
||||
pub mod room;
|
||||
pub mod state_events;
|
||||
pub mod timeline;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
@@ -33,22 +36,14 @@ use crate::{RoomInfoNotableUpdateReasons, StateChanges};
|
||||
type RoomInfoNotableUpdates = BTreeMap<OwnedRoomId, RoomInfoNotableUpdateReasons>;
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Context {
|
||||
pub(super) state_changes: StateChanges,
|
||||
pub(super) room_info_notable_updates: RoomInfoNotableUpdates,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn new(
|
||||
state_changes: StateChanges,
|
||||
room_info_notable_updates: RoomInfoNotableUpdates,
|
||||
) -> Self {
|
||||
Self { state_changes, room_info_notable_updates }
|
||||
}
|
||||
|
||||
pub fn into_parts(self) -> (StateChanges, RoomInfoNotableUpdates) {
|
||||
let Self { state_changes, room_info_notable_updates } = self;
|
||||
|
||||
(state_changes, room_info_notable_updates)
|
||||
pub fn new(state_changes: StateChanges) -> Self {
|
||||
Self { state_changes, room_info_notable_updates: Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use ruma::{
|
||||
push::{Action, PushConditionRoomCtx, Ruleset},
|
||||
serde::Raw,
|
||||
OwnedRoomId, RoomId,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
deserialized_responses::RawAnySyncOrStrippedTimelineEvent, store::BaseStateStore, sync,
|
||||
};
|
||||
|
||||
/// A classical set of data used by some processors dealing with notifications
|
||||
/// and push rules.
|
||||
pub struct Notification<'a> {
|
||||
pub push_rules: &'a Ruleset,
|
||||
pub notifications: &'a mut BTreeMap<OwnedRoomId, Vec<sync::Notification>>,
|
||||
pub state_store: &'a BaseStateStore,
|
||||
}
|
||||
|
||||
impl<'a> Notification<'a> {
|
||||
pub fn new(
|
||||
push_rules: &'a Ruleset,
|
||||
notifications: &'a mut BTreeMap<OwnedRoomId, Vec<sync::Notification>>,
|
||||
state_store: &'a BaseStateStore,
|
||||
) -> Self {
|
||||
Self { push_rules, notifications, state_store }
|
||||
}
|
||||
|
||||
fn push_notification(
|
||||
&mut self,
|
||||
room_id: &RoomId,
|
||||
actions: Vec<Action>,
|
||||
event: RawAnySyncOrStrippedTimelineEvent,
|
||||
) {
|
||||
self.notifications
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.push(sync::Notification { actions, event });
|
||||
}
|
||||
|
||||
/// Push a new [`sync::Notification`] in [`Self::notifications`] from
|
||||
/// `event` if and only if `predicate` returns `true` for at least one of
|
||||
/// the [`Action`]s associated to this event and this
|
||||
/// `push_condition_room_ctx`. (based on `Self::push_rules`).
|
||||
///
|
||||
/// This method returns the fetched [`Action`]s.
|
||||
pub fn push_notification_from_event_if<E, P>(
|
||||
&mut self,
|
||||
room_id: &RoomId,
|
||||
push_condition_room_ctx: &PushConditionRoomCtx,
|
||||
event: &Raw<E>,
|
||||
predicate: P,
|
||||
) -> &[Action]
|
||||
where
|
||||
Raw<E>: Into<RawAnySyncOrStrippedTimelineEvent>,
|
||||
P: Fn(&Action) -> bool,
|
||||
{
|
||||
let actions = self.push_rules.get_actions(event, push_condition_room_ctx);
|
||||
|
||||
if actions.iter().any(predicate) {
|
||||
self.push_notification(room_id, actions.to_owned(), event.clone().into());
|
||||
}
|
||||
|
||||
actions
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::super::Context;
|
||||
use crate::{
|
||||
room::UpdatedRoomDisplayName, store::BaseStateStore, sync::RoomUpdates,
|
||||
RoomInfoNotableUpdateReasons,
|
||||
};
|
||||
|
||||
pub async fn update_for_rooms(
|
||||
context: &mut Context,
|
||||
room_updates: &RoomUpdates,
|
||||
state_store: &BaseStateStore,
|
||||
) {
|
||||
for room in room_updates.iter_all_room_ids().filter_map(|room_id| state_store.room(room_id)) {
|
||||
// Compute the display name. If it's different, let's register the `RoomInfo` in
|
||||
// the `StateChanges`.
|
||||
if let Ok(UpdatedRoomDisplayName::New(_)) = room.compute_display_name().await {
|
||||
let room_id = room.room_id().to_owned();
|
||||
|
||||
context.state_changes.room_infos.insert(room_id.clone(), room.clone_info());
|
||||
context
|
||||
.room_info_notable_updates
|
||||
.entry(room_id)
|
||||
.or_default()
|
||||
.insert(RoomInfoNotableUpdateReasons::DISPLAY_NAME);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use ruma::RoomId;
|
||||
use tokio::sync::broadcast::Sender;
|
||||
|
||||
use crate::{store::ambiguity_map::AmbiguityCache, RequestedRequiredStates, RoomInfoNotableUpdate};
|
||||
|
||||
pub mod display_name;
|
||||
pub mod msc4186;
|
||||
pub mod sync_v2;
|
||||
|
||||
/// A classical set of data used by some processors in this module.
|
||||
pub struct RoomCreationData<'a> {
|
||||
room_id: &'a RoomId,
|
||||
room_info_notable_update_sender: Sender<RoomInfoNotableUpdate>,
|
||||
requested_required_states: &'a RequestedRequiredStates,
|
||||
ambiguity_cache: &'a mut AmbiguityCache,
|
||||
}
|
||||
|
||||
impl<'a> RoomCreationData<'a> {
|
||||
pub fn new(
|
||||
room_id: &'a RoomId,
|
||||
room_info_notable_update_sender: Sender<RoomInfoNotableUpdate>,
|
||||
requested_required_states: &'a RequestedRequiredStates,
|
||||
ambiguity_cache: &'a mut AmbiguityCache,
|
||||
) -> Self {
|
||||
Self {
|
||||
room_id,
|
||||
room_info_notable_update_sender,
|
||||
requested_required_states,
|
||||
ambiguity_cache,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::v5 as http,
|
||||
events::{receipt::ReceiptEventContent, AnySyncEphemeralRoomEvent, SyncEphemeralRoomEvent},
|
||||
serde::Raw,
|
||||
OwnedRoomId, RoomId,
|
||||
};
|
||||
|
||||
use super::super::super::{
|
||||
account_data::for_room as account_data_for_room, ephemeral_events::dispatch_receipt, Context,
|
||||
};
|
||||
use crate::{
|
||||
store::BaseStateStore,
|
||||
sync::{JoinedRoomUpdate, RoomUpdates},
|
||||
RoomState,
|
||||
};
|
||||
|
||||
/// Dispatch the ephemeral events in the `extensions.typing` part of the
|
||||
/// response.
|
||||
pub fn dispatch_typing_ephemeral_events(
|
||||
typing: &http::response::Typing,
|
||||
joined_room_updates: &mut BTreeMap<OwnedRoomId, JoinedRoomUpdate>,
|
||||
) {
|
||||
for (room_id, raw) in &typing.rooms {
|
||||
joined_room_updates
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.ephemeral
|
||||
.push(raw.clone().cast());
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispatch the ephemeral event in the `extensions.receipts` part of the
|
||||
/// response for a particular room.
|
||||
pub fn dispatch_receipt_ephemeral_event_for_room(
|
||||
context: &mut Context,
|
||||
room_id: &RoomId,
|
||||
receipt: &Raw<SyncEphemeralRoomEvent<ReceiptEventContent>>,
|
||||
joined_room_update: &mut JoinedRoomUpdate,
|
||||
) {
|
||||
let receipt: Raw<AnySyncEphemeralRoomEvent> = receipt.cast_ref().clone();
|
||||
|
||||
dispatch_receipt(context, &receipt, room_id);
|
||||
joined_room_update.ephemeral.push(receipt);
|
||||
}
|
||||
|
||||
pub async fn room_account_data(
|
||||
context: &mut Context,
|
||||
account_data: &http::response::AccountData,
|
||||
room_updates: &mut RoomUpdates,
|
||||
state_store: &BaseStateStore,
|
||||
) {
|
||||
for (room_id, raw) in &account_data.rooms {
|
||||
account_data_for_room(context, room_id, raw, state_store).await;
|
||||
|
||||
if let Some(room) = state_store.room(room_id) {
|
||||
match room.state() {
|
||||
RoomState::Joined => room_updates
|
||||
.joined
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.account_data
|
||||
.append(&mut raw.to_vec()),
|
||||
RoomState::Left | RoomState::Banned => room_updates
|
||||
.left
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.account_data
|
||||
.append(&mut raw.to_vec()),
|
||||
RoomState::Invited | RoomState::Knocked => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,565 @@
|
||||
// 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,
|
||||
state_store,
|
||||
)
|
||||
.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,303 @@
|
||||
// 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,
|
||||
state_store,
|
||||
)
|
||||
.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,
|
||||
state_store,
|
||||
)
|
||||
.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 (),
|
||||
state_store,
|
||||
)
|
||||
.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 (),
|
||||
state_store,
|
||||
)
|
||||
.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)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,17 +24,16 @@ use ruma::{
|
||||
StateEventType,
|
||||
},
|
||||
push::{Action, PushConditionRoomCtx},
|
||||
RoomVersionId, UInt, UserId,
|
||||
UInt, UserId,
|
||||
};
|
||||
use tracing::{instrument, trace, warn};
|
||||
|
||||
use super::Context;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use super::{e2ee, verification};
|
||||
use super::{notification, Context};
|
||||
use crate::{
|
||||
deserialized_responses::RawAnySyncOrStrippedTimelineEvent,
|
||||
store::{BaseStateStore, StateStoreExt as _},
|
||||
sync::{Notification, Timeline},
|
||||
sync::Timeline,
|
||||
Result, Room, RoomInfo,
|
||||
};
|
||||
|
||||
@@ -51,18 +50,18 @@ pub async fn build<'notification, 'e2ee>(
|
||||
room: &Room,
|
||||
room_info: &mut RoomInfo,
|
||||
timeline_inputs: builder::Timeline,
|
||||
notification_inputs: builder::Notification<'notification>,
|
||||
#[cfg(feature = "e2e-encryption")] e2ee: builder::E2EE<'e2ee>,
|
||||
mut notification: notification::Notification<'notification>,
|
||||
#[cfg(feature = "e2e-encryption")] e2ee: e2ee::E2EE<'e2ee>,
|
||||
) -> Result<Timeline> {
|
||||
let mut timeline = Timeline::new(timeline_inputs.limited, timeline_inputs.prev_batch);
|
||||
let mut push_context =
|
||||
get_push_room_context(context, room, room_info, notification_inputs.state_store).await?;
|
||||
let mut push_condition_room_ctx =
|
||||
get_push_room_context(context, room, room_info, notification.state_store).await?;
|
||||
let room_id = room.room_id();
|
||||
|
||||
for raw_event in timeline_inputs.raw_events {
|
||||
// Start by assuming we have a plaintext event. We'll replace it with a
|
||||
// decrypted or UTD event below if necessary.
|
||||
let mut timeline_event = TimelineEvent::new(raw_event);
|
||||
let mut timeline_event = TimelineEvent::from_plaintext(raw_event);
|
||||
|
||||
// Do some special stuff on the `timeline_event` before collecting it.
|
||||
match timeline_event.raw().deserialize() {
|
||||
@@ -77,9 +76,9 @@ pub async fn build<'notification, 'e2ee>(
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomRedaction(
|
||||
redaction_event,
|
||||
)) => {
|
||||
let room_version = room_info.room_version().unwrap_or(&RoomVersionId::V1);
|
||||
let room_version = room_info.room_version_or_default();
|
||||
|
||||
if let Some(redacts) = redaction_event.redacts(room_version) {
|
||||
if let Some(redacts) = redaction_event.redacts(&room_version) {
|
||||
room_info
|
||||
.handle_redaction(redaction_event, timeline_event.raw().cast_ref());
|
||||
|
||||
@@ -100,12 +99,9 @@ pub async fn build<'notification, 'e2ee>(
|
||||
) => {
|
||||
if let Some(decrypted_timeline_event) =
|
||||
Box::pin(e2ee::decrypt::sync_timeline_event(
|
||||
context,
|
||||
e2ee.olm_machine,
|
||||
e2ee.clone(),
|
||||
timeline_event.raw(),
|
||||
room_id,
|
||||
e2ee.decryption_trust_requirement,
|
||||
e2ee.verification_is_allowed,
|
||||
))
|
||||
.await?
|
||||
{
|
||||
@@ -115,10 +111,8 @@ pub async fn build<'notification, 'e2ee>(
|
||||
|
||||
_ => {
|
||||
Box::pin(verification::process_if_relevant(
|
||||
context,
|
||||
&sync_timeline_event,
|
||||
e2ee.verification_is_allowed,
|
||||
e2ee.olm_machine,
|
||||
e2ee.clone(),
|
||||
room_id,
|
||||
))
|
||||
.await?;
|
||||
@@ -131,36 +125,28 @@ pub async fn build<'notification, 'e2ee>(
|
||||
AnySyncTimelineEvent::MessageLike(_) => (),
|
||||
}
|
||||
|
||||
if let Some(push_context) = &mut push_context {
|
||||
update_push_room_context(context, push_context, room.own_user_id(), room_info)
|
||||
} else {
|
||||
push_context = get_push_room_context(
|
||||
if let Some(push_condition_room_ctx) = &mut push_condition_room_ctx {
|
||||
update_push_room_context(
|
||||
context,
|
||||
room,
|
||||
push_condition_room_ctx,
|
||||
room.own_user_id(),
|
||||
room_info,
|
||||
notification_inputs.state_store,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
push_condition_room_ctx =
|
||||
get_push_room_context(context, room, room_info, notification.state_store)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(context) = &push_context {
|
||||
let actions =
|
||||
notification_inputs.push_rules.get_actions(timeline_event.raw(), context);
|
||||
if let Some(push_condition_room_ctx) = &push_condition_room_ctx {
|
||||
let actions = notification.push_notification_from_event_if(
|
||||
room_id,
|
||||
push_condition_room_ctx,
|
||||
timeline_event.raw(),
|
||||
Action::should_notify,
|
||||
);
|
||||
|
||||
if actions.iter().any(Action::should_notify) {
|
||||
notification_inputs
|
||||
.notifications
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.push(Notification {
|
||||
actions: actions.to_owned(),
|
||||
event: RawAnySyncOrStrippedTimelineEvent::Sync(
|
||||
timeline_event.raw().clone(),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
timeline_event.push_actions = Some(actions.to_owned());
|
||||
timeline_event.set_push_actions(actions.to_owned());
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
@@ -178,20 +164,12 @@ pub async fn build<'notification, 'e2ee>(
|
||||
/// Set of types used by [`build`] to reduce the number of arguments by grouping
|
||||
/// them by thematics.
|
||||
pub mod builder {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_crypto::{OlmMachine, TrustRequirement};
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::{v3, v5},
|
||||
events::AnySyncTimelineEvent,
|
||||
push::Ruleset,
|
||||
serde::Raw,
|
||||
OwnedRoomId,
|
||||
};
|
||||
|
||||
use crate::{store::BaseStateStore, sync};
|
||||
|
||||
pub struct Timeline {
|
||||
pub limited: bool,
|
||||
pub raw_events: Vec<Raw<AnySyncTimelineEvent>>,
|
||||
@@ -213,47 +191,13 @@ pub mod builder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Notification<'a> {
|
||||
pub push_rules: &'a Ruleset,
|
||||
pub notifications: &'a mut BTreeMap<OwnedRoomId, Vec<sync::Notification>>,
|
||||
pub state_store: &'a BaseStateStore,
|
||||
}
|
||||
|
||||
impl<'a> Notification<'a> {
|
||||
pub fn new(
|
||||
push_rules: &'a Ruleset,
|
||||
notifications: &'a mut BTreeMap<OwnedRoomId, Vec<sync::Notification>>,
|
||||
state_store: &'a BaseStateStore,
|
||||
) -> Self {
|
||||
Self { push_rules, notifications, state_store }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub struct E2EE<'a> {
|
||||
pub olm_machine: Option<&'a OlmMachine>,
|
||||
pub decryption_trust_requirement: TrustRequirement,
|
||||
pub verification_is_allowed: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
impl<'a> E2EE<'a> {
|
||||
pub fn new(
|
||||
olm_machine: Option<&'a OlmMachine>,
|
||||
decryption_trust_requirement: TrustRequirement,
|
||||
verification_is_allowed: bool,
|
||||
) -> Self {
|
||||
Self { olm_machine, decryption_trust_requirement, verification_is_allowed }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the push context for the given room.
|
||||
///
|
||||
/// Updates the context data from `context.state_changes` or `room_info`.
|
||||
fn update_push_room_context(
|
||||
context: &mut Context,
|
||||
context: &Context,
|
||||
push_rules: &mut PushConditionRoomCtx,
|
||||
user_id: &UserId,
|
||||
room_info: &RoomInfo,
|
||||
@@ -291,7 +235,7 @@ fn update_push_room_context(
|
||||
/// Returns `None` if some data couldn't be found. This should only happen
|
||||
/// in brand new rooms, while we process its state.
|
||||
pub async fn get_push_room_context(
|
||||
context: &mut Context,
|
||||
context: &Context,
|
||||
room: &Room,
|
||||
room_info: &RoomInfo,
|
||||
state_store: &BaseStateStore,
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk_crypto::OlmMachine;
|
||||
use ruma::{
|
||||
events::{
|
||||
room::message::MessageType, AnySyncMessageLikeEvent, AnySyncTimelineEvent,
|
||||
@@ -21,64 +20,48 @@ use ruma::{
|
||||
RoomId,
|
||||
};
|
||||
|
||||
use super::Context;
|
||||
use super::e2ee::E2EE;
|
||||
use crate::Result;
|
||||
|
||||
/// Process the given event as a verification event if it is a candidate. The
|
||||
/// event must be decrypted.
|
||||
pub async fn process_if_relevant(
|
||||
context: &mut Context,
|
||||
event: &AnySyncTimelineEvent,
|
||||
verification_is_allowed: bool,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
e2ee: E2EE<'_>,
|
||||
room_id: &RoomId,
|
||||
) -> Result<()> {
|
||||
if let AnySyncTimelineEvent::MessageLike(event) = event {
|
||||
// That's it, we are good, the event has been decrypted successfully.
|
||||
|
||||
// However, let's run an additional action. Check if this is a verification
|
||||
// event (`m.key.verification.*`), and call `verification` accordingly.
|
||||
if match &event {
|
||||
// This is an original (i.e. non-redacted) `m.room.message` event and its
|
||||
// content is a verification request…
|
||||
AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(
|
||||
original_event,
|
||||
)) => {
|
||||
matches!(&original_event.content.msgtype, MessageType::VerificationRequest(_))
|
||||
}
|
||||
|
||||
// … or this is verification request event
|
||||
AnySyncMessageLikeEvent::KeyVerificationReady(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationStart(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationCancel(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationAccept(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationKey(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationMac(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationDone(_) => true,
|
||||
|
||||
_ => false,
|
||||
} {
|
||||
verification(context, verification_is_allowed, olm_machine, event, room_id).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn verification(
|
||||
_context: &mut Context,
|
||||
verification_is_allowed: bool,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
event: &AnySyncMessageLikeEvent,
|
||||
room_id: &RoomId,
|
||||
) -> Result<()> {
|
||||
if !verification_is_allowed {
|
||||
if !e2ee.verification_is_allowed {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(olm) = olm_machine {
|
||||
olm.receive_verification_event(&event.clone().into_full_event(room_id.to_owned())).await?;
|
||||
let Some(olm) = e2ee.olm_machine else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let AnySyncTimelineEvent::MessageLike(event) = event else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
match event {
|
||||
// This is an original (i.e. non-redacted) `m.room.message` event and its
|
||||
// content is a verification request…
|
||||
AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(original_event))
|
||||
if matches!(&original_event.content.msgtype, MessageType::VerificationRequest(_)) => {}
|
||||
|
||||
// … or this is verification request event.
|
||||
AnySyncMessageLikeEvent::KeyVerificationReady(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationStart(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationCancel(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationAccept(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationKey(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationMac(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationDone(_) => {}
|
||||
|
||||
_ => {
|
||||
// No need to handle those other event types.
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(olm.receive_verification_event(&event.clone().into_full_event(room_id.to_owned())).await?)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use ruma::OwnedUserId;
|
||||
|
||||
use super::Room;
|
||||
|
||||
impl Room {
|
||||
/// Is there a non expired membership with application `m.call` and scope
|
||||
/// `m.room` in this room.
|
||||
pub fn has_active_room_call(&self) -> bool {
|
||||
self.inner.read().has_active_room_call()
|
||||
}
|
||||
|
||||
/// Returns a `Vec` of `OwnedUserId`'s that participate in the room call.
|
||||
///
|
||||
/// MatrixRTC memberships with application `m.call` and scope `m.room` are
|
||||
/// considered. A user can occur twice if they join with two devices.
|
||||
/// Convert to a set depending if the different users are required or the
|
||||
/// amount of sessions.
|
||||
///
|
||||
/// The vector is ordered by oldest membership user to newest.
|
||||
pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
|
||||
self.inner.read().active_room_call_participants()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{ops::Sub, sync::Arc, time::Duration};
|
||||
|
||||
use assign::assign;
|
||||
use matrix_sdk_test::{ALICE, BOB, CAROL};
|
||||
use ruma::{
|
||||
device_id, event_id,
|
||||
events::{
|
||||
call::member::{
|
||||
ActiveFocus, ActiveLivekitFocus, Application, CallApplicationContent,
|
||||
CallMemberEventContent, CallMemberStateKey, Focus, LegacyMembershipData,
|
||||
LegacyMembershipDataInit, LivekitFocus, OriginalSyncCallMemberEvent,
|
||||
},
|
||||
AnySyncStateEvent, StateUnsigned, SyncStateEvent,
|
||||
},
|
||||
room_id,
|
||||
time::SystemTime,
|
||||
user_id, DeviceId, EventId, MilliSecondsSinceUnixEpoch, OwnedUserId, UserId,
|
||||
};
|
||||
use similar_asserts::assert_eq;
|
||||
|
||||
use super::super::{Room, RoomState};
|
||||
use crate::store::MemoryStore;
|
||||
|
||||
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
|
||||
let store = Arc::new(MemoryStore::new());
|
||||
let user_id = user_id!("@me:example.org");
|
||||
let room_id = room_id!("!test:localhost");
|
||||
let (sender, _receiver) = tokio::sync::broadcast::channel(1);
|
||||
|
||||
(store.clone(), Room::new(user_id, store, room_id, room_type, sender))
|
||||
}
|
||||
|
||||
fn timestamp(minutes_ago: u32) -> MilliSecondsSinceUnixEpoch {
|
||||
MilliSecondsSinceUnixEpoch::from_system_time(
|
||||
SystemTime::now().sub(Duration::from_secs((60 * minutes_ago).into())),
|
||||
)
|
||||
.expect("date out of range")
|
||||
}
|
||||
|
||||
fn legacy_membership_for_my_call(
|
||||
device_id: &DeviceId,
|
||||
membership_id: &str,
|
||||
minutes_ago: u32,
|
||||
) -> LegacyMembershipData {
|
||||
let (application, foci) = foci_and_application();
|
||||
assign!(
|
||||
LegacyMembershipData::from(LegacyMembershipDataInit {
|
||||
application,
|
||||
device_id: device_id.to_owned(),
|
||||
expires: Duration::from_millis(3_600_000),
|
||||
foci_active: foci,
|
||||
membership_id: membership_id.to_owned(),
|
||||
}),
|
||||
{ created_ts: Some(timestamp(minutes_ago)) }
|
||||
)
|
||||
}
|
||||
|
||||
fn legacy_member_state_event(
|
||||
memberships: Vec<LegacyMembershipData>,
|
||||
ev_id: &EventId,
|
||||
user_id: &UserId,
|
||||
) -> AnySyncStateEvent {
|
||||
let content = CallMemberEventContent::new_legacy(memberships);
|
||||
|
||||
AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent {
|
||||
content,
|
||||
event_id: ev_id.to_owned(),
|
||||
sender: user_id.to_owned(),
|
||||
// we can simply use now here since this will be dropped when using a MinimalStateEvent
|
||||
// in the roomInfo
|
||||
origin_server_ts: timestamp(0),
|
||||
state_key: CallMemberStateKey::new(user_id.to_owned(), None, false),
|
||||
unsigned: StateUnsigned::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
struct InitData<'a> {
|
||||
device_id: &'a DeviceId,
|
||||
minutes_ago: u32,
|
||||
}
|
||||
|
||||
fn session_member_state_event(
|
||||
ev_id: &EventId,
|
||||
user_id: &UserId,
|
||||
init_data: Option<InitData<'_>>,
|
||||
) -> AnySyncStateEvent {
|
||||
let application = Application::Call(CallApplicationContent::new(
|
||||
"my_call_id_1".to_owned(),
|
||||
ruma::events::call::member::CallScope::Room,
|
||||
));
|
||||
let foci_preferred = vec![Focus::Livekit(LivekitFocus::new(
|
||||
"my_call_foci_alias".to_owned(),
|
||||
"https://lk.org".to_owned(),
|
||||
))];
|
||||
let focus_active = ActiveFocus::Livekit(ActiveLivekitFocus::new());
|
||||
let (content, state_key) = match init_data {
|
||||
Some(InitData { device_id, minutes_ago }) => (
|
||||
CallMemberEventContent::new(
|
||||
application,
|
||||
device_id.to_owned(),
|
||||
focus_active,
|
||||
foci_preferred,
|
||||
Some(timestamp(minutes_ago)),
|
||||
),
|
||||
CallMemberStateKey::new(user_id.to_owned(), Some(device_id.to_owned()), false),
|
||||
),
|
||||
None => (
|
||||
CallMemberEventContent::new_empty(None),
|
||||
CallMemberStateKey::new(user_id.to_owned(), None, false),
|
||||
),
|
||||
};
|
||||
|
||||
AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent {
|
||||
content,
|
||||
event_id: ev_id.to_owned(),
|
||||
sender: user_id.to_owned(),
|
||||
// we can simply use now here since this will be dropped when using a MinimalStateEvent
|
||||
// in the roomInfo
|
||||
origin_server_ts: timestamp(0),
|
||||
state_key,
|
||||
unsigned: StateUnsigned::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn foci_and_application() -> (Application, Vec<Focus>) {
|
||||
(
|
||||
Application::Call(CallApplicationContent::new(
|
||||
"my_call_id_1".to_owned(),
|
||||
ruma::events::call::member::CallScope::Room,
|
||||
)),
|
||||
vec![Focus::Livekit(LivekitFocus::new(
|
||||
"my_call_foci_alias".to_owned(),
|
||||
"https://lk.org".to_owned(),
|
||||
))],
|
||||
)
|
||||
}
|
||||
|
||||
fn receive_state_events(room: &Room, events: Vec<&AnySyncStateEvent>) {
|
||||
room.inner.update_if(|info| {
|
||||
let mut res = false;
|
||||
for ev in events {
|
||||
res |= info.handle_state_event(ev);
|
||||
}
|
||||
res
|
||||
});
|
||||
}
|
||||
|
||||
/// `user_a`: empty memberships
|
||||
/// `user_b`: one membership
|
||||
/// `user_c`: two memberships (two devices)
|
||||
fn legacy_create_call_with_member_events_for_user(a: &UserId, b: &UserId, c: &UserId) -> Room {
|
||||
let (_, room) = make_room_test_helper(RoomState::Joined);
|
||||
|
||||
let a_empty = legacy_member_state_event(Vec::new(), event_id!("$1234"), a);
|
||||
|
||||
// make b 10min old
|
||||
let m_init_b = legacy_membership_for_my_call(device_id!("DEVICE_0"), "0", 1);
|
||||
let b_one = legacy_member_state_event(vec![m_init_b], event_id!("$12345"), b);
|
||||
|
||||
// c1 1min old
|
||||
let m_init_c1 = legacy_membership_for_my_call(device_id!("DEVICE_0"), "0", 10);
|
||||
// c2 20min old
|
||||
let m_init_c2 = legacy_membership_for_my_call(device_id!("DEVICE_1"), "0", 20);
|
||||
let c_two = legacy_member_state_event(vec![m_init_c1, m_init_c2], event_id!("$123456"), c);
|
||||
|
||||
// Intentionally use a non time sorted receive order.
|
||||
receive_state_events(&room, vec![&c_two, &a_empty, &b_one]);
|
||||
|
||||
room
|
||||
}
|
||||
|
||||
/// `user_a`: empty memberships
|
||||
/// `user_b`: one membership
|
||||
/// `user_c`: two memberships (two devices)
|
||||
fn session_create_call_with_member_events_for_user(a: &UserId, b: &UserId, c: &UserId) -> Room {
|
||||
let (_, room) = make_room_test_helper(RoomState::Joined);
|
||||
|
||||
let a_empty = session_member_state_event(event_id!("$1234"), a, None);
|
||||
|
||||
// make b 10min old
|
||||
let b_one = session_member_state_event(
|
||||
event_id!("$12345"),
|
||||
b,
|
||||
Some(InitData { device_id: "DEVICE_0".into(), minutes_ago: 1 }),
|
||||
);
|
||||
|
||||
let m_c1 = session_member_state_event(
|
||||
event_id!("$123456_0"),
|
||||
c,
|
||||
Some(InitData { device_id: "DEVICE_0".into(), minutes_ago: 10 }),
|
||||
);
|
||||
let m_c2 = session_member_state_event(
|
||||
event_id!("$123456_1"),
|
||||
c,
|
||||
Some(InitData { device_id: "DEVICE_1".into(), minutes_ago: 20 }),
|
||||
);
|
||||
// Intentionally use a non time sorted receive order1
|
||||
receive_state_events(&room, vec![&m_c1, &m_c2, &a_empty, &b_one]);
|
||||
|
||||
room
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_show_correct_active_call_state() {
|
||||
let room_legacy = legacy_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
|
||||
|
||||
// This check also tests the ordering.
|
||||
// We want older events to be in the front.
|
||||
// user_b (Bob) is 1min old, c1 (CAROL) 10min old, c2 (CAROL) 20min old
|
||||
assert_eq!(
|
||||
vec![CAROL.to_owned(), CAROL.to_owned(), BOB.to_owned()],
|
||||
room_legacy.active_room_call_participants()
|
||||
);
|
||||
assert!(room_legacy.has_active_room_call());
|
||||
|
||||
let room_session = session_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
|
||||
assert_eq!(
|
||||
vec![CAROL.to_owned(), CAROL.to_owned(), BOB.to_owned()],
|
||||
room_session.active_room_call_participants()
|
||||
);
|
||||
assert!(room_session.has_active_room_call());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_active_call_is_false_when_everyone_left() {
|
||||
let room = legacy_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
|
||||
|
||||
let b_empty_membership = legacy_member_state_event(Vec::new(), event_id!("$1234_1"), &BOB);
|
||||
let c_empty_membership =
|
||||
legacy_member_state_event(Vec::new(), event_id!("$12345_1"), &CAROL);
|
||||
|
||||
receive_state_events(&room, vec![&b_empty_membership, &c_empty_membership]);
|
||||
|
||||
// We have no active call anymore after emptying the memberships
|
||||
assert_eq!(Vec::<OwnedUserId>::new(), room.active_room_call_participants());
|
||||
assert!(!room.has_active_room_call());
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user