Compare commits
912 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ffe5aa6ca | |||
| c604e4acd2 | |||
| f8b343bece | |||
| 94f8f8c44c | |||
| 4c1f80faf7 | |||
| f9bf492fdb | |||
| 824fc0b62e | |||
| 359db7f28b | |||
| 30672e6feb | |||
| f9b419077d | |||
| d46f934d57 | |||
| 0bed6afc29 | |||
| 412d4b80ee | |||
| bcabf1bda4 | |||
| 7767ef6ca3 | |||
| 6765ca0c39 | |||
| 17abab0d53 | |||
| cc0bf91a06 | |||
| 0b3345f592 | |||
| 472b934816 | |||
| 27a28e55d1 | |||
| d6a418f46a | |||
| 268e14e4f5 | |||
| f1190deef9 | |||
| ee62cd749f | |||
| cea5c190d8 | |||
| ad4cb4f6c9 | |||
| 949e7a6cac | |||
| 8e66963a1e | |||
| aa02e31cf6 | |||
| 57c7972c63 | |||
| e89ac3d7df | |||
| a3704c3563 | |||
| 5fb728e8f0 | |||
| eab62ec0b5 | |||
| 2fae949a42 | |||
| 4adbb4aa88 | |||
| 18affe3edd | |||
| ea59bc8955 | |||
| 68f6d927f1 | |||
| c3766789cc | |||
| 7c31525f68 | |||
| b2dd5ce02d | |||
| 1f2b4f87bc | |||
| 893c45af74 | |||
| a161dfa9a0 | |||
| 20cd0bedfa | |||
| d4a1ce06e4 | |||
| e53906a920 | |||
| 1e30916754 | |||
| 15c46b503c | |||
| 5cd3818841 | |||
| 79b7d6d235 | |||
| 05d0f9e077 | |||
| 4c1e2d6d51 | |||
| c9162373a1 | |||
| 9f22f550bf | |||
| 7a762035f1 | |||
| 8c0a918e6e | |||
| 33c317e6d2 | |||
| 371ed49670 | |||
| f0c1c65308 | |||
| ea5063ca84 | |||
| dcc07e1049 | |||
| 76a0eb1599 | |||
| a9c16c96e0 | |||
| 93f8ebba27 | |||
| 95bb153269 | |||
| ff72a09870 | |||
| 06de58dee9 | |||
| fbb49e9b65 | |||
| 04728cc1a6 | |||
| d43d141dc8 | |||
| 3c069f0c5c | |||
| a2200b6324 | |||
| 9caa0817ae | |||
| b3229041fb | |||
| 50446377de | |||
| 4f02a6d3be | |||
| 87fdd3c3bf | |||
| 990fe86fdc | |||
| be2ba26974 | |||
| 1392a0e637 | |||
| 03ffa4c9a4 | |||
| f8b5992101 | |||
| 97a5fbebfb | |||
| 74c2032974 | |||
| 274aaf5ba3 | |||
| 3e72cce7a0 | |||
| aa4b176ab3 | |||
| ad2f4c731a | |||
| f78015fae1 | |||
| 211a1f5a40 | |||
| b8be1fdb26 | |||
| a43e42c170 | |||
| f031eaf96b | |||
| 3ba31d1e97 | |||
| bbf8f9f900 | |||
| 49dc2bb640 | |||
| 99af951d7a | |||
| a17bf18ff2 | |||
| 5d87570a33 | |||
| 80806303b5 | |||
| 6155772bb1 | |||
| 33db267a89 | |||
| 6fc68dac83 | |||
| f3eeb82b0b | |||
| 951d22ac24 | |||
| 527d001010 | |||
| 759eeeb27f | |||
| 97d6f57aee | |||
| 6622a3ac93 | |||
| 39730173d4 | |||
| 763314645b | |||
| b2fee72d79 | |||
| 9803d2bcca | |||
| 296867d2ac | |||
| 710b57e035 | |||
| baa75368d6 | |||
| feb264e899 | |||
| 8e5075569e | |||
| 33c11d08f0 | |||
| 9714ac8e10 | |||
| e1d136aa6e | |||
| 8018753332 | |||
| 61824f866c | |||
| 6919444e98 | |||
| c5097cf07e | |||
| b77c6c65cc | |||
| f4ce4356ab | |||
| d66733052a | |||
| 001dadffe1 | |||
| b2387bf3a9 | |||
| d43858ecb2 | |||
| 8804966094 | |||
| a3ee011b61 | |||
| f586172f3e | |||
| 85d52586b6 | |||
| 40d3dd57db | |||
| 4e2655aa1b | |||
| 41fcebbcb0 | |||
| 13b86a3f5d | |||
| dba23b66fa | |||
| 132d0eb34a | |||
| e2e70448ca | |||
| adaccbab2c | |||
| 7a09ca0bbd | |||
| fffdd34ebd | |||
| 24502d2706 | |||
| b6433dea27 | |||
| 385f1a824f | |||
| dfc0ef8b35 | |||
| d2feeaac30 | |||
| 25a81876a0 | |||
| e388fe6522 | |||
| ef20342ddf | |||
| e6b1ffba99 | |||
| 9a3ceb8be6 | |||
| faee647c3a | |||
| 1866143456 | |||
| be8e322ad6 | |||
| 838f607b32 | |||
| 6965004812 | |||
| 8ec23a95d5 | |||
| 7880ec5b01 | |||
| 36428564fc | |||
| 7adaf7be73 | |||
| 7920723bb4 | |||
| b73163aa45 | |||
| feeeb53f19 | |||
| 1ac876db98 | |||
| afaf2cc036 | |||
| 5a3bb0a86d | |||
| 2640aa1e23 | |||
| d1a8392ce7 | |||
| 4d14dd3692 | |||
| 63defca8af | |||
| fd83904b4d | |||
| 9254c38a8d | |||
| 63d9dd5c6e | |||
| 23dacc329e | |||
| 5896a438f5 | |||
| 31a3d76436 | |||
| dfaaf323ad | |||
| 94439d8913 | |||
| 9cc29d7c65 | |||
| f2fbdfbac2 | |||
| fd34927f61 | |||
| 0190d3556d | |||
| 2d657fe908 | |||
| 5e43177d3a | |||
| e44b01cbe5 | |||
| 4882c98f99 | |||
| 4bf0187310 | |||
| 10ca400d4d | |||
| cc61e123b7 | |||
| 6f23981268 | |||
| 859044285a | |||
| 6c1134006e | |||
| 6d1cdbc613 | |||
| 6160c15103 | |||
| 100cbde526 | |||
| 6ff8a26cca | |||
| a1c484fb6e | |||
| d2ecc77014 | |||
| 2e86fbc234 | |||
| 64698eaf1a | |||
| f180a14c88 | |||
| bb0d480f24 | |||
| 7af1d3ab0e | |||
| 13ee4c8098 | |||
| 7bbd02ca73 | |||
| e22a7a2ed5 | |||
| 4aad2c6b07 | |||
| 960162453c | |||
| 1ea2162012 | |||
| 1f0151705a | |||
| 84ebbd913c | |||
| 756d50737e | |||
| c32877284c | |||
| 6260811ea5 | |||
| 4f0415d4d2 | |||
| b43aac129b | |||
| c019009d00 | |||
| a88d6b37dc | |||
| 705d6f870e | |||
| 4fc28c4701 | |||
| 64eecd0aee | |||
| c25be8b070 | |||
| 1554c9d8fa | |||
| d568c07489 | |||
| 13c30f6691 | |||
| 0e70a2fdfb | |||
| d70d758861 | |||
| eefa9ff556 | |||
| 28a8603f42 | |||
| ae7f0fe022 | |||
| d9f4e7c426 | |||
| 247ec1dcd2 | |||
| 558d7b56f9 | |||
| 1201be484a | |||
| 1ffc014621 | |||
| 9491757cad | |||
| 33df0422e8 | |||
| a3a239f999 | |||
| ca8b64e041 | |||
| 140e751af0 | |||
| a66b2c5123 | |||
| 69bef9a76a | |||
| b3c53dd08f | |||
| c8bffa26a4 | |||
| b4ef6cef55 | |||
| c6854a5c22 | |||
| fb563953c9 | |||
| bc0018aecb | |||
| 12292c5375 | |||
| cf9d058265 | |||
| 4da13e1096 | |||
| 333d4563ce | |||
| 01059ef26c | |||
| 7724271508 | |||
| 8dfe732cce | |||
| 1cf3477ada | |||
| 0a2205f540 | |||
| c586812159 | |||
| c6210cad21 | |||
| a9ce1c6e58 | |||
| 1eb8f6ac16 | |||
| e0feebdb2b | |||
| 0fee716c1e | |||
| c41ed8a78a | |||
| 53f02c9f2d | |||
| e2f0b4f3fd | |||
| 0a796cb468 | |||
| e3390c17ec | |||
| c6dc070c31 | |||
| 486befc7fb | |||
| 9848d1472e | |||
| 6c944a9b39 | |||
| b4b010f9fe | |||
| 536ba518bb | |||
| 917c46b570 | |||
| b29886c0df | |||
| 360c2d7f32 | |||
| 683f0f4027 | |||
| c783ed8a6f | |||
| 139673810f | |||
| 669ebf2408 | |||
| 992774b8b5 | |||
| 9d90a92b4c | |||
| d79975e0e3 | |||
| 2e598c0532 | |||
| 5a3ef30fdc | |||
| 05178ccaf9 | |||
| 65b9bd20a8 | |||
| 35505f9130 | |||
| a6d630216d | |||
| 159c9b4547 | |||
| aead1a4489 | |||
| 7fee1c7fd7 | |||
| ab9bfb2d61 | |||
| de5f00fd33 | |||
| 33c16b2979 | |||
| e9dcdb7176 | |||
| 0a3fe939c5 | |||
| 37e07ea331 | |||
| e4e3ff63f5 | |||
| 8409e52654 | |||
| e8096ee518 | |||
| 6814e70aa4 | |||
| efa4539a91 | |||
| 42d2b93489 | |||
| 872713c4bc | |||
| feb22d4370 | |||
| 6520c9b16e | |||
| cd6fe271ba | |||
| 5f447bbb17 | |||
| 94e7ddd1ab | |||
| 6ac4a8431d | |||
| b585963abb | |||
| 5719fde701 | |||
| 2914d7a727 | |||
| 0cdec9d912 | |||
| d180d49c07 | |||
| bcee5badae | |||
| ebb7059d55 | |||
| 8d3b1d3c7e | |||
| 056e90db25 | |||
| 787861eb35 | |||
| f081416baa | |||
| 8a6cc7bc22 | |||
| 61258e823f | |||
| e86aab68b4 | |||
| 48f1bc0780 | |||
| 1fe71acbcb | |||
| 0e054deb19 | |||
| d2b7dc6116 | |||
| 1089a25588 | |||
| 3276bc87ad | |||
| a4da6ba7c8 | |||
| 033c6bd6a4 | |||
| b02e1da471 | |||
| 5ad477ac96 | |||
| 975432565d | |||
| 46b0113765 | |||
| 09eff8c6bd | |||
| 7ee546a3d9 | |||
| b164cd6a51 | |||
| 6d95abfb36 | |||
| 33f09d6d26 | |||
| 8c01e99144 | |||
| 277cb7ac49 | |||
| fc7124fd1a | |||
| 30c0420f83 | |||
| cb13c345ad | |||
| cd26973082 | |||
| 0edcdd33b2 | |||
| c191eb7cd1 | |||
| fa6f270812 | |||
| d4e96595d9 | |||
| 540a11e7a8 | |||
| 92192c549b | |||
| 88360040fb | |||
| 4184e245a4 | |||
| f37bf2f5d1 | |||
| d57d3c4124 | |||
| 1a5cb2beb8 | |||
| b645c1101f | |||
| 8091094bbc | |||
| feadfde1b5 | |||
| e2ad07881c | |||
| 1be8b42d03 | |||
| 7a5f83f6ec | |||
| 88bb7a366f | |||
| 7d9bf56581 | |||
| 770f65ede0 | |||
| 1f33e0f4d1 | |||
| 117f76102d | |||
| fd04ebfaba | |||
| afe9f7a979 | |||
| 27a002c8e2 | |||
| b8ab0972b3 | |||
| d3419ea4ac | |||
| 019adb9a56 | |||
| ca89700dfe | |||
| a0c87cfe4f | |||
| 9ddc892aa0 | |||
| 0a7ac18d9f | |||
| d8b6966c0a | |||
| d40f04e32c | |||
| d8294a0788 | |||
| 06a4476e7f | |||
| def1fedea3 | |||
| d061e7a5b2 | |||
| f4619c91d3 | |||
| 227f6eab85 | |||
| 16d7c3c094 | |||
| c238a0edb8 | |||
| 06bf487512 | |||
| c636ec63f4 | |||
| ffe239d620 | |||
| 822b709107 | |||
| d75d7973b2 | |||
| cfe3adce48 | |||
| b478ae65f7 | |||
| ada68e1114 | |||
| c9137f0cad | |||
| 4e0dab959a | |||
| e862ded147 | |||
| 5cb033ad91 | |||
| 0e622cc5a1 | |||
| 6d562eff2f | |||
| af2e15e02f | |||
| 79aa5aaf16 | |||
| 0833ffdef2 | |||
| 16f7239215 | |||
| da946e51dd | |||
| cba711dbdf | |||
| e87e9331c1 | |||
| 6e9fc70d13 | |||
| e2148e46bc | |||
| d1163b75bf | |||
| 5ae7d0f60f | |||
| 6f067d5510 | |||
| d6fe654814 | |||
| 2710510786 | |||
| 35a8528712 | |||
| f9735c75d3 | |||
| 15e6b81835 | |||
| bcb4ab4b10 | |||
| 4931c0749e | |||
| 37626b5ad9 | |||
| d19616da03 | |||
| 7c8f870d16 | |||
| b482ccd318 | |||
| 165ec9db1b | |||
| 6f42210d6a | |||
| 6125580275 | |||
| 8b3e295429 | |||
| 1e568efbb5 | |||
| f89ced3ded | |||
| a5fbcf1491 | |||
| 8923e58ee3 | |||
| f14994baa9 | |||
| d42d0f3e17 | |||
| e4849d5cab | |||
| 65aec7ee7f | |||
| a6868386d0 | |||
| b3c1ca1577 | |||
| ce66ee4a16 | |||
| 2bbf6fc711 | |||
| 1a9e5b904b | |||
| 4c43b06445 | |||
| 49a0765880 | |||
| 39cf8b325d | |||
| bb67150d6b | |||
| 471e3c340c | |||
| 74972d8db7 | |||
| 03a76fbaf5 | |||
| 392a1ef47b | |||
| c5f2460e02 | |||
| 8ad785a117 | |||
| a5b936d0b6 | |||
| 1a0544c8eb | |||
| 232c23e8df | |||
| 7d9d5bf3b4 | |||
| ea076b3d76 | |||
| 8aa6f97f7c | |||
| 679aa07115 | |||
| 0c66d8a53f | |||
| 25ed7eef2b | |||
| a399840dff | |||
| 8d4e7f0478 | |||
| 3bd93130c5 | |||
| 153618b77c | |||
| dd871ef9ac | |||
| 8a29f17d1d | |||
| fab520ab33 | |||
| 435553c3d1 | |||
| 610ecd218c | |||
| fa300d1f33 | |||
| af2a483158 | |||
| 753b0d8584 | |||
| 36713adbdb | |||
| d73a02c608 | |||
| f73199b472 | |||
| 420d373144 | |||
| a79e9130e6 | |||
| 355b5327f8 | |||
| fa77852001 | |||
| 7b73311de5 | |||
| f03934bc4f | |||
| 014ee98fb7 | |||
| fbcf9fce7c | |||
| 6de403276a | |||
| 6209bc942c | |||
| edd371b570 | |||
| 817f32e15b | |||
| 30eb12ed2d | |||
| 900697bc3b | |||
| 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 |
@@ -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
|
||||
|
||||
+2
-12
@@ -17,6 +17,7 @@ version = 2
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"CDLA-Permissive-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"BSL-1.0",
|
||||
@@ -28,15 +29,6 @@ allow = [
|
||||
]
|
||||
exceptions = [
|
||||
{ allow = ["Unicode-DFS-2016"], crate = "unicode-ident" },
|
||||
{ allow = ["CDDL-1.0"], crate = "inferno" },
|
||||
{ allow = ["LicenseRef-ring"], crate = "ring" },
|
||||
]
|
||||
|
||||
[[licenses.clarify]]
|
||||
name = "ring"
|
||||
expression = "LicenseRef-ring"
|
||||
license-files = [
|
||||
{ path = "LICENSE", hash = 0xbd0eed23 },
|
||||
]
|
||||
|
||||
[bans]
|
||||
@@ -51,9 +43,7 @@ unknown-git = "deny"
|
||||
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",
|
||||
# Same as for the tracing dependency.
|
||||
"https://github.com/element-hq/paranoid-android.git",
|
||||
"https://github.com/tokio-rs/tracing.git",
|
||||
# Well, it's Ruma.
|
||||
"https://github.com/ruma/ruma",
|
||||
# A patch override for the bindings: https://github.com/rodrimati1992/const_panic/pull/10
|
||||
|
||||
@@ -1,54 +1,40 @@
|
||||
name: Benchmarks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
benchmarks:
|
||||
name: Run Benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
environment: matrix-rust-bot
|
||||
if: github.event_name == 'push'
|
||||
strategy:
|
||||
matrix:
|
||||
benchmark:
|
||||
- crypto_bench
|
||||
- event_cache
|
||||
- linked_chunk
|
||||
- store_bench
|
||||
- timeline
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2025-02-20
|
||||
components: rustfmt
|
||||
- name: Setup rust toolchain, cache and cargo-codspeed binary
|
||||
uses: moonrepo/setup-rust@ede6de059f8046a5e236c94046823e2af11ca670
|
||||
with:
|
||||
channel: stable
|
||||
cache-target: release
|
||||
bins: cargo-codspeed
|
||||
|
||||
- name: Run Benchmarks
|
||||
run: cargo bench | tee benchmark-output.txt
|
||||
- name: Build the benchmark target(s)
|
||||
run: cargo codspeed build -p benchmarks ${{ matrix.benchmark }} --features codspeed
|
||||
|
||||
- name: Check benchmark result for PR
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: benchmark-action/github-action-benchmark@v1
|
||||
with:
|
||||
name: Rust Benchmark
|
||||
tool: 'cargo'
|
||||
output-file-path: benchmark-output.txt
|
||||
auto-push: false
|
||||
# comment to alert the user this has gone bad
|
||||
github-token: ${{ secrets.MRB_ACCESS_TOKEN }}
|
||||
alert-threshold: '120%'
|
||||
comment-on-alert: true
|
||||
fail-threshold: '150%'
|
||||
fail-on-alert: true
|
||||
|
||||
- name: Store benchmark result
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: benchmark-action/github-action-benchmark@v1
|
||||
with:
|
||||
name: Rust Benchmark
|
||||
tool: 'cargo'
|
||||
output-file-path: benchmark-output.txt
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
auto-push: true
|
||||
# Show alert with commit comment on detecting possible performance regression
|
||||
alert-threshold: '150%'
|
||||
comment-on-alert: true
|
||||
fail-on-alert: true
|
||||
alert-comment-cc-users: '@gnunicornBen,@jplatte,@poljar'
|
||||
- name: Run the benchmarks
|
||||
uses: CodSpeedHQ/action@76578c2a7ddd928664caa737f0e962e3085d4e7c
|
||||
with:
|
||||
run: cargo codspeed run
|
||||
token: ${{ secrets.CODSPEED_TOKEN }}
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
@@ -69,17 +69,17 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Rust SDK
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Checkout Kotlin Rust Components project
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: matrix-org/matrix-rust-components-kotlin
|
||||
path: rust-components-kotlin
|
||||
ref: main
|
||||
|
||||
- name: Use JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
@@ -136,7 +136,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# install protoc in case we end up rebuilding opentelemetry-proto
|
||||
- name: Install protoc
|
||||
@@ -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"
|
||||
@@ -191,7 +191,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# install protoc in case we end up rebuilding opentelemetry-proto
|
||||
- name: Install protoc
|
||||
|
||||
+24
-17
@@ -34,14 +34,16 @@ jobs:
|
||||
- no-sqlite
|
||||
- no-encryption-and-sqlite
|
||||
- sqlite-cryptostore
|
||||
- experimental-encrypted-state-events
|
||||
- rustls-tls
|
||||
- markdown
|
||||
- socks
|
||||
- sso-login
|
||||
- search
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -83,7 +85,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -114,7 +116,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
@@ -165,7 +167,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
@@ -237,7 +239,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -246,10 +248,10 @@ 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
|
||||
version: v0.13.1
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
@@ -287,10 +289,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Actions Repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Check the spelling of the files in our repo
|
||||
uses: crate-ci/typos@v1.33.1
|
||||
uses: crate-ci/typos@v1.35.7
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
@@ -299,7 +301,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
@@ -309,7 +311,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2025-02-20
|
||||
toolchain: nightly-2025-08-08
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Load cache
|
||||
@@ -333,8 +335,7 @@ jobs:
|
||||
target/debug/xtask ci clippy
|
||||
|
||||
integration-tests:
|
||||
name: Integration test
|
||||
|
||||
name: 'Integration test (features: ${{ matrix.feature }})'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# run several docker containers with the same networking stack so the hostname 'synapse'
|
||||
@@ -350,9 +351,16 @@ jobs:
|
||||
ports:
|
||||
- 8008:8008
|
||||
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
feature:
|
||||
- "default"
|
||||
- "experimental-encrypted-state-events"
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
@@ -376,7 +384,7 @@ jobs:
|
||||
HOMESERVER_URL: "http://localhost:8008"
|
||||
HOMESERVER_DOMAIN: "synapse"
|
||||
run: |
|
||||
cargo nextest run -p matrix-sdk-integration-testing
|
||||
cargo nextest run -p matrix-sdk-integration-testing --features "${{ matrix.feature }}"
|
||||
|
||||
compile-bench:
|
||||
name: 🚄 Compile benchmarks
|
||||
@@ -384,7 +392,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -397,4 +405,3 @@ jobs:
|
||||
- name: Compile benchmarks (no run)
|
||||
run: |
|
||||
cargo bench --profile dev --no-run
|
||||
|
||||
|
||||
@@ -17,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'
|
||||
@@ -35,8 +39,65 @@ 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: |
|
||||
echo "Disk space before cleanup"
|
||||
df -h
|
||||
|
||||
cd /opt
|
||||
find . -maxdepth 1 -mindepth 1 '!' -path ./containerd '!' -path ./actionarchivecache '!' -path ./runner '!' -path ./runner-cache -exec rm -rf '{}' ';'
|
||||
rm -rf /opt/hostedtoolcache
|
||||
|
||||
# Get rid of binaries and libs we're not interested in.
|
||||
sudo rm -rf \
|
||||
/usr/local/julia* \
|
||||
/usr/local/aws*
|
||||
|
||||
sudo rm -rf \
|
||||
/usr/local/bin/minikube \
|
||||
/usr/local/bin/node \
|
||||
/usr/local/bin/stack \
|
||||
/usr/local/bin/bicep \
|
||||
/usr/local/bin/pulumi* \
|
||||
/usr/local/bin/helm \
|
||||
/usr/local/bin/azcopy \
|
||||
/usr/local/bin/packer \
|
||||
/usr/local/bin/cmake-gui \
|
||||
/usr/local/bin/cpack
|
||||
|
||||
sudo rm -rf \
|
||||
/usr/local/share/powershell \
|
||||
/usr/local/share/chromium
|
||||
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
|
||||
echo "::group::/usr/local/bin/*"
|
||||
du -hsc /usr/local/bin/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "::group::/usr/local/share/*"
|
||||
du -hsc /usr/local/share/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "::group::/usr/local/*"
|
||||
du -hsc /usr/local/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "::group::/usr/local/lib/*"
|
||||
du -hsc /usr/local/lib/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "::group::/opt/*"
|
||||
du -hsc /opt/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "Disk space after cleanup"
|
||||
df -h
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
@@ -44,6 +105,8 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsqlite3-dev
|
||||
sudo apt-get clean
|
||||
sudo rm -rf /var/lib/apt/lists/*
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -56,23 +119,29 @@ 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: Check total disk space before running
|
||||
run: |
|
||||
rustup run stable cargo tarpaulin \
|
||||
--skip-clean --profile cov --out xml \
|
||||
--features experimental-widgets,testing
|
||||
df -h
|
||||
|
||||
- name: Create the coverage report
|
||||
run: |
|
||||
target/debug/xtask ci coverage -o codecov
|
||||
env:
|
||||
CARGO_PROFILE_COV_INHERITS: 'dev'
|
||||
CARGO_PROFILE_COV_DEBUG: 1
|
||||
@@ -89,6 +158,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.
|
||||
@@ -97,7 +171,8 @@ jobs:
|
||||
with:
|
||||
name: codecov_report
|
||||
path: |
|
||||
cobertura.xml
|
||||
coverage.xml
|
||||
junit.xml
|
||||
pr_number.txt
|
||||
commit_sha.txt
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -10,5 +10,5 @@ jobs:
|
||||
cargo-deny:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
|
||||
@@ -17,10 +17,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@115870536a85eaf050e369291c7895748ff12aea
|
||||
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
|
||||
|
||||
@@ -7,6 +7,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Machete
|
||||
uses: bnjbvr/cargo-machete@v0.8.0
|
||||
uses: bnjbvr/cargo-machete@v0.9.1
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2025-02-20
|
||||
toolchain: nightly-2025-08-08
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
|
||||
- name: Upload artifact
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
path: './target/doc/'
|
||||
|
||||
|
||||
@@ -7,6 +7,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Block Fixup Commit Merge
|
||||
uses: 13rac1/block-fixup-merge-action@v2.0.0
|
||||
|
||||
@@ -11,6 +11,6 @@ jobs:
|
||||
msrv:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: taiki-e/install-action@cargo-hack
|
||||
- run: cargo hack check --rust-version --workspace --all-targets --ignore-private
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
echo "override_commit=$(<commit_sha.txt)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
|
||||
path: repo_root
|
||||
@@ -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 }}
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Calculate cache key
|
||||
id: cachekey
|
||||
|
||||
@@ -4,6 +4,7 @@ master.zip
|
||||
emsdk-*
|
||||
.idea/
|
||||
.env
|
||||
.envrc
|
||||
.build
|
||||
.swiftpm
|
||||
/Package.swift
|
||||
|
||||
Generated
+1275
-655
File diff suppressed because it is too large
Load Diff
+66
-50
@@ -16,23 +16,28 @@ default-members = ["benchmarks", "crates/*", "labs/*"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
rust-version = "1.85"
|
||||
rust-version = "1.88"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.95"
|
||||
anyhow = "1.0.99"
|
||||
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-compat = "0.2.5"
|
||||
async-rx = "0.1.3"
|
||||
# Bumping this to 0.3.6 produces a test failure because the semantic between the
|
||||
# versions changed subtly.
|
||||
async-stream = "0.3.5"
|
||||
async-trait = "0.1.85"
|
||||
async-trait = "0.1.89"
|
||||
base64 = "0.22.1"
|
||||
bitflags = "2.8.0"
|
||||
bitflags = "2.9.3"
|
||||
byteorder = "1.5.0"
|
||||
chrono = "0.4.39"
|
||||
cfg-if = "1.0.3"
|
||||
clap = "4.5.46"
|
||||
chrono = "0.4.41"
|
||||
dirs = "6.0.0"
|
||||
eyeball = { version = "0.8.8", features = ["tracing"] }
|
||||
eyeball-im = { version = "0.7.0", features = ["tracing"] }
|
||||
eyeball-im-util = "0.9.0"
|
||||
@@ -44,78 +49,84 @@ gloo-timers = "0.3.0"
|
||||
growable-bloom-filter = "2.1.1"
|
||||
hkdf = "0.12.4"
|
||||
hmac = "0.12.1"
|
||||
http = "1.2.0"
|
||||
http = "1.3.1"
|
||||
imbl = "5.0.0"
|
||||
indexmap = "2.7.1"
|
||||
insta = { version = "1.42.1", features = ["json", "redactions"] }
|
||||
indexmap = "2.11.0"
|
||||
insta = { version = "1.43.1", features = ["json", "redactions"] }
|
||||
itertools = "0.14.0"
|
||||
js-sys = "0.3.69"
|
||||
js-sys = "0.3.77"
|
||||
mime = "0.3.17"
|
||||
once_cell = "1.20.2"
|
||||
oauth2 = { version = "5.0.0", default-features = false, features = ["reqwest", "timing-resistant-secret-traits"] }
|
||||
once_cell = "1.21.3"
|
||||
pbkdf2 = { version = "0.12.2" }
|
||||
pin-project-lite = "0.2.16"
|
||||
proptest = { version = "1.6.0", default-features = false, features = ["std"] }
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.12.12", default-features = false }
|
||||
reqwest = { version = "0.12.23", 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.3", features = [
|
||||
ruma = { version = "0.13.0", 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-msc4222",
|
||||
"unstable-msc4278",
|
||||
"unstable-msc4286",
|
||||
] }
|
||||
ruma-common = "0.15.2"
|
||||
sentry = "0.36.0"
|
||||
sentry-tracing = "0.36.0"
|
||||
serde = { version = "1.0.217", features = ["rc"] }
|
||||
"unstable-msc4306",
|
||||
"unstable-msc4308"
|
||||
] }
|
||||
sentry = { version = "0.42.0", default-features = false }
|
||||
sentry-tracing = "0.42.0"
|
||||
serde = { version = "1.0.219", features = ["rc"] }
|
||||
serde_html_form = "0.2.7"
|
||||
serde_json = "1.0.138"
|
||||
sha2 = "0.10.8"
|
||||
similar-asserts = "1.6.1"
|
||||
serde_json = "1.0.143"
|
||||
sha2 = "0.10.9"
|
||||
similar-asserts = "1.7.0"
|
||||
stream_assert = "0.1.1"
|
||||
tempfile = "3.16.0"
|
||||
thiserror = "2.0.11"
|
||||
tokio = { version = "1.43.1", default-features = false, features = ["sync"] }
|
||||
tempfile = "3.21.0"
|
||||
thiserror = "2.0.16"
|
||||
tokio = { version = "1.47.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"
|
||||
tracing-subscriber = "0.3.18"
|
||||
tracing = { version = "0.1.41", default-features = false, features = ["std"] }
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-core = "0.1.34"
|
||||
tracing-subscriber = "0.3.20"
|
||||
unicode-normalization = "0.1.24"
|
||||
uniffi = { version = "0.28.0" }
|
||||
uniffi_bindgen = { version = "0.28.0" }
|
||||
url = "2.5.4"
|
||||
uuid = "1.12.1"
|
||||
url = "2.5.7"
|
||||
uuid = "1.18.0"
|
||||
vergen-gitcl = "1.0.8"
|
||||
vodozemac = { version = "0.9.0", features = ["insecure-pk-encryption"] }
|
||||
wasm-bindgen = "0.2.84"
|
||||
wasm-bindgen-test = "0.3.50"
|
||||
web-sys = "0.3.69"
|
||||
wiremock = "0.6.2"
|
||||
wiremock = "0.6.5"
|
||||
zeroize = "1.8.1"
|
||||
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.12.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.12.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.12.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.12.0" }
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.14.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.14.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.14.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.14.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.12.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.12.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.12.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.12.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.12.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.12.0", default-features = false }
|
||||
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.14.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.14.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.14.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.14.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.14.0" }
|
||||
matrix-sdk-test-utils = { path = "testing/matrix-sdk-test-utils", version = "0.14.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.14.0", default-features = false }
|
||||
matrix-sdk-search = { path = "crates/matrix-sdk-search", version = "0.14.0" }
|
||||
|
||||
[workspace.lints.rust]
|
||||
rust_2018_idioms = "warn"
|
||||
@@ -137,13 +148,15 @@ 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"
|
||||
redundant_clone = "warn"
|
||||
|
||||
# Default development profile; default for most Cargo commands, otherwise
|
||||
# selected with `--debug`
|
||||
@@ -178,12 +191,15 @@ lto = false
|
||||
# Get symbol names for profiling purposes.
|
||||
debug = true
|
||||
|
||||
[profile.bench]
|
||||
inherits = "release"
|
||||
lto = false
|
||||
|
||||
[patch.crates-io]
|
||||
async-compat = { git = "https://github.com/element-hq/async-compat", rev = "5a27c8b290f1f1dcfc0c4ec22c464e38528aa591" }
|
||||
const_panic = { git = "https://github.com/jplatte/const_panic", rev = "9024a4cb3eac45c1d2d980f17aaee287b17be498" }
|
||||
# Needed to fix rotation log issue on Android (https://github.com/tokio-rs/tracing/issues/2937)
|
||||
tracing = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
tracing-core = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
tracing-subscriber = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
tracing-appender = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
paranoid-android = { git = "https://github.com/element-hq/paranoid-android.git", rev = "69388ac5b4afeed7be4401c70ce17f6d9a2cf19b" }
|
||||
tracing = { git = "https://github.com/tokio-rs/tracing.git", rev = "20f5b3d8ba057ca9c4ae00ad30dda3dce8a71c05" }
|
||||
tracing-core = { git = "https://github.com/tokio-rs/tracing.git", rev = "20f5b3d8ba057ca9c4ae00ad30dda3dce8a71c05" }
|
||||
tracing-subscriber = { git = "https://github.com/tokio-rs/tracing.git", rev = "20f5b3d8ba057ca9c4ae00ad30dda3dce8a71c05" }
|
||||
tracing-appender = { git = "https://github.com/tokio-rs/tracing.git", rev = "20f5b3d8ba057ca9c4ae00ad30dda3dce8a71c05" }
|
||||
|
||||
@@ -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/
|
||||
|
||||
+10
-6
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "benchmarks"
|
||||
description = "Matrix SDK benchmarks"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
rust-version.workspace = true
|
||||
version = "1.0.0"
|
||||
@@ -10,8 +10,11 @@ publish = false
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
[features]
|
||||
codspeed = []
|
||||
|
||||
[dependencies]
|
||||
criterion = { version = "0.5.1", features = ["async", "async_tokio", "html_reports"] }
|
||||
criterion = { version = "3.0.5", features = ["async", "async_tokio", "html_reports"], package = "codspeed-criterion-compat" }
|
||||
matrix-sdk = { workspace = true, features = ["native-tls", "e2e-encryption", "sqlite", "testing"] }
|
||||
matrix-sdk-base.workspace = true
|
||||
matrix-sdk-crypto.workspace = true
|
||||
@@ -21,13 +24,10 @@ matrix-sdk-ui.workspace = true
|
||||
ruma.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tempfile = "3.3.0"
|
||||
tempfile.workspace = true
|
||||
tokio = { workspace = true, default-features = false, features = ["rt-multi-thread"] }
|
||||
wiremock.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
pprof = { version = "0.14.0", features = ["flamegraph", "criterion"] }
|
||||
|
||||
[[bench]]
|
||||
name = "crypto_bench"
|
||||
harness = false
|
||||
@@ -47,3 +47,7 @@ harness = false
|
||||
[[bench]]
|
||||
name = "timeline"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "event_cache"
|
||||
harness = false
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput};
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use matrix_sdk_crypto::{EncryptionSettings, OlmMachine};
|
||||
use matrix_sdk_sqlite::SqliteCryptoStore;
|
||||
use matrix_sdk_test::ruma_response_from_json;
|
||||
use ruma::{
|
||||
DeviceId, OwnedUserId, TransactionId, UserId,
|
||||
api::client::{
|
||||
keys::{claim_keys, get_keys},
|
||||
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
|
||||
},
|
||||
device_id, room_id, user_id, DeviceId, OwnedUserId, TransactionId, UserId,
|
||||
device_id, room_id, user_id,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use tokio::runtime::Builder;
|
||||
@@ -58,10 +59,14 @@ pub fn keys_query(c: &mut Criterion) {
|
||||
|
||||
// Benchmark memory store.
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("memory store", &name), &response, |b, response| {
|
||||
b.to_async(&runtime)
|
||||
.iter(|| async { machine.mark_request_as_sent(&txn_id, response).await.unwrap() })
|
||||
});
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("Device keys query [memory]", &name),
|
||||
&response,
|
||||
|b, response| {
|
||||
b.to_async(&runtime)
|
||||
.iter(|| async { machine.mark_request_as_sent(&txn_id, response).await.unwrap() })
|
||||
},
|
||||
);
|
||||
|
||||
// Benchmark sqlite store.
|
||||
|
||||
@@ -71,10 +76,14 @@ pub fn keys_query(c: &mut Criterion) {
|
||||
.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store, None))
|
||||
.unwrap();
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("sqlite store", &name), &response, |b, response| {
|
||||
b.to_async(&runtime)
|
||||
.iter(|| async { machine.mark_request_as_sent(&txn_id, response).await.unwrap() })
|
||||
});
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("Device keys query [SQLite]", &name),
|
||||
&response,
|
||||
|b, response| {
|
||||
b.to_async(&runtime)
|
||||
.iter(|| async { machine.mark_request_as_sent(&txn_id, response).await.unwrap() })
|
||||
},
|
||||
);
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
@@ -84,6 +93,8 @@ pub fn keys_query(c: &mut Criterion) {
|
||||
group.finish()
|
||||
}
|
||||
|
||||
/// This test panics on the CI, not sure why so we're disabling it for now.
|
||||
#[cfg(not(feature = "codspeed"))]
|
||||
pub fn keys_claiming(c: &mut Criterion) {
|
||||
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
|
||||
|
||||
@@ -99,49 +110,65 @@ pub fn keys_claiming(c: &mut Criterion) {
|
||||
|
||||
let name = format!("{count} one-time keys");
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("memory store", &name), &response, |b, response| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let machine = runtime.block_on(OlmMachine::new(alice_id(), alice_device_id()));
|
||||
runtime
|
||||
.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response))
|
||||
.unwrap();
|
||||
(machine, &runtime, &txn_id)
|
||||
},
|
||||
move |(machine, runtime, txn_id)| {
|
||||
runtime.block_on(async {
|
||||
machine.mark_request_as_sent(txn_id, response).await.unwrap();
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("One-time keys claiming [memory]", &name),
|
||||
&response,
|
||||
|b, response| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let machine = runtime.block_on(OlmMachine::new(alice_id(), alice_device_id()));
|
||||
runtime
|
||||
.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response))
|
||||
.unwrap();
|
||||
(machine, &runtime, &txn_id)
|
||||
},
|
||||
move |(machine, runtime, txn_id)| {
|
||||
runtime.block_on(async {
|
||||
machine.mark_request_as_sent(txn_id, response).await.unwrap();
|
||||
drop(machine);
|
||||
})
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("One-time keys claiming [SQLite]", &name),
|
||||
&response,
|
||||
|b, response| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = Arc::new(
|
||||
runtime.block_on(SqliteCryptoStore::open(dir.path(), None)).unwrap(),
|
||||
);
|
||||
|
||||
let machine = runtime
|
||||
.block_on(OlmMachine::with_store(
|
||||
alice_id(),
|
||||
alice_device_id(),
|
||||
store,
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
runtime
|
||||
.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response))
|
||||
.unwrap();
|
||||
(machine, &runtime, &txn_id)
|
||||
},
|
||||
move |(machine, runtime, txn_id)| {
|
||||
runtime.block_on(async {
|
||||
machine.mark_request_as_sent(txn_id, response).await.unwrap();
|
||||
});
|
||||
|
||||
let _ = runtime.enter();
|
||||
drop(machine);
|
||||
})
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("sqlite store", &name), &response, |b, response| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store =
|
||||
Arc::new(runtime.block_on(SqliteCryptoStore::open(dir.path(), None)).unwrap());
|
||||
|
||||
let machine = runtime
|
||||
.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store, None))
|
||||
.unwrap();
|
||||
runtime
|
||||
.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response))
|
||||
.unwrap();
|
||||
(machine, &runtime, &txn_id)
|
||||
},
|
||||
move |(machine, runtime, txn_id)| {
|
||||
runtime.block_on(async {
|
||||
machine.mark_request_as_sent(txn_id, response).await.unwrap();
|
||||
drop(machine)
|
||||
})
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
group.finish()
|
||||
}
|
||||
@@ -169,7 +196,7 @@ pub fn room_key_sharing(c: &mut Criterion) {
|
||||
|
||||
// Benchmark memory store.
|
||||
|
||||
group.bench_function(BenchmarkId::new("memory store", &name), |b| {
|
||||
group.bench_function(BenchmarkId::new("Room key sharing [memory]", &name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let requests = machine
|
||||
.share_room_key(
|
||||
@@ -201,7 +228,7 @@ pub fn room_key_sharing(c: &mut Criterion) {
|
||||
runtime.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response)).unwrap();
|
||||
runtime.block_on(machine.mark_request_as_sent(&txn_id, &response)).unwrap();
|
||||
|
||||
group.bench_function(BenchmarkId::new("sqlite store", &name), |b| {
|
||||
group.bench_function(BenchmarkId::new("Room key sharing [SQLite]", &name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let requests = machine
|
||||
.share_room_key(
|
||||
@@ -249,7 +276,7 @@ pub fn devices_missing_sessions_collecting(c: &mut Criterion) {
|
||||
|
||||
// Benchmark memory store.
|
||||
|
||||
group.bench_function(BenchmarkId::new("memory store", &name), |b| {
|
||||
group.bench_function(BenchmarkId::new("Devices collecting [memory]", &name), |b| {
|
||||
b.to_async(&runtime).iter_with_large_drop(|| async {
|
||||
machine.get_missing_sessions(users.iter().map(Deref::deref)).await.unwrap()
|
||||
})
|
||||
@@ -266,7 +293,7 @@ pub fn devices_missing_sessions_collecting(c: &mut Criterion) {
|
||||
|
||||
runtime.block_on(machine.mark_request_as_sent(&txn_id, &response)).unwrap();
|
||||
|
||||
group.bench_function(BenchmarkId::new("sqlite store", &name), |b| {
|
||||
group.bench_function(BenchmarkId::new("Devices collecting [SQLite]", &name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
machine.get_missing_sessions(users.iter().map(Deref::deref)).await.unwrap()
|
||||
})
|
||||
@@ -280,21 +307,18 @@ pub fn devices_missing_sessions_collecting(c: &mut Criterion) {
|
||||
group.finish()
|
||||
}
|
||||
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
let criterion = Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
100,
|
||||
pprof::criterion::Output::Flamegraph(None),
|
||||
));
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let criterion = Criterion::default();
|
||||
|
||||
criterion
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "codspeed"))]
|
||||
criterion_group! {
|
||||
name = benches;
|
||||
config = criterion();
|
||||
config = Criterion::default();
|
||||
targets = keys_query, keys_claiming, room_key_sharing, devices_missing_sessions_collecting,
|
||||
}
|
||||
|
||||
#[cfg(feature = "codspeed")]
|
||||
criterion_group! {
|
||||
name = benches;
|
||||
config = Criterion::default();
|
||||
targets = keys_query, room_key_sharing, devices_missing_sessions_collecting,
|
||||
}
|
||||
|
||||
criterion_main!(benches);
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
use std::{pin::Pin, sync::Arc};
|
||||
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use matrix_sdk::{
|
||||
RoomInfo, RoomState, SqliteEventCacheStore, StateStore,
|
||||
store::StoreConfig,
|
||||
sync::{JoinedRoomUpdate, RoomUpdates},
|
||||
test_utils::client::MockClientBuilder,
|
||||
};
|
||||
use matrix_sdk_base::event_cache::store::{DynEventCacheStore, IntoEventCacheStore, MemoryStore};
|
||||
use matrix_sdk_test::{ALICE, event_factory::EventFactory};
|
||||
use ruma::{
|
||||
EventId, RoomId, event_id,
|
||||
events::{relation::RelationType, room::message::RoomMessageEventContentWithoutRelation},
|
||||
room_id,
|
||||
};
|
||||
use tempfile::tempdir;
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
type StoreBuilder = Box<dyn Fn() -> Pin<Box<dyn Future<Output = Arc<DynEventCacheStore>>>>>;
|
||||
|
||||
fn handle_room_updates(c: &mut Criterion) {
|
||||
// Create a new asynchronous runtime.
|
||||
let runtime = Builder::new_multi_thread()
|
||||
.enable_time()
|
||||
.enable_io()
|
||||
.build()
|
||||
.expect("Failed to create an asynchronous runtime");
|
||||
|
||||
let mut group = c.benchmark_group("Event cache room updates");
|
||||
group.sample_size(10);
|
||||
|
||||
const NUM_EVENTS: usize = 1000;
|
||||
|
||||
for num_rooms in [1, 10, 100] {
|
||||
// Add some joined rooms, each with NUM_EVENTS in it, to the sync response.
|
||||
let mut room_updates = RoomUpdates::default();
|
||||
|
||||
let mut changes = matrix_sdk::StateChanges::default();
|
||||
|
||||
for i in 0..num_rooms {
|
||||
let room_id = RoomId::parse(format!("!room{i}:example.com")).unwrap();
|
||||
let event_factory = EventFactory::new().room(&room_id).sender(&ALICE);
|
||||
|
||||
let mut joined_room_update = JoinedRoomUpdate::default();
|
||||
for j in 0..NUM_EVENTS {
|
||||
let event_id = EventId::parse(format!("$ev{i}_{j}")).unwrap();
|
||||
let event =
|
||||
event_factory.text_msg(format!("Message {j}")).event_id(&event_id).into();
|
||||
joined_room_update.timeline.events.push(event);
|
||||
}
|
||||
room_updates.joined.insert(room_id.clone(), joined_room_update);
|
||||
|
||||
changes.add_room(RoomInfo::new(&room_id, RoomState::Joined));
|
||||
}
|
||||
|
||||
// Declare new stores for this set of events.
|
||||
let temp_dir = Arc::new(tempdir().unwrap());
|
||||
|
||||
let store_builders: Vec<(_, StoreBuilder)> = vec![
|
||||
(
|
||||
"memory",
|
||||
Box::new(|| Box::pin(async { MemoryStore::default().into_event_cache_store() })),
|
||||
),
|
||||
(
|
||||
"SQLite",
|
||||
Box::new(move || {
|
||||
let temp_dir = temp_dir.clone();
|
||||
Box::pin(async move {
|
||||
// Remove all the files in the temp_dir, to reset the event cache state.
|
||||
for entry in temp_dir.path().read_dir().unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
// If it's a directory, remove it recursively.
|
||||
std::fs::remove_dir_all(path).unwrap();
|
||||
} else {
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Recreate a new store.
|
||||
SqliteEventCacheStore::open(temp_dir.path().join("bench"), None)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_event_cache_store()
|
||||
})
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
let state_store = runtime.block_on(async {
|
||||
let state_store = matrix_sdk::MemoryStore::new();
|
||||
state_store.save_changes(&changes).await.unwrap();
|
||||
Arc::new(state_store)
|
||||
});
|
||||
|
||||
for (store_name, store_builder) in &store_builders {
|
||||
let client = runtime.block_on(async {
|
||||
let event_cache_store = store_builder().await;
|
||||
|
||||
let client = MockClientBuilder::new(None)
|
||||
.on_builder(|builder| {
|
||||
builder.store_config(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
|
||||
.state_store(state_store.clone())
|
||||
.event_cache_store(event_cache_store.clone()),
|
||||
)
|
||||
})
|
||||
.build()
|
||||
.await;
|
||||
|
||||
client.event_cache().subscribe().unwrap();
|
||||
|
||||
client
|
||||
});
|
||||
|
||||
// Define a state store with all rooms known in it.
|
||||
// Define the throughput.
|
||||
group.throughput(Throughput::Elements(num_rooms));
|
||||
|
||||
// Bench the handling of room updates.
|
||||
group.bench_function(
|
||||
BenchmarkId::new(
|
||||
format!("Event cache room updates[{store_name}]"),
|
||||
format!("room count: {num_rooms}"),
|
||||
),
|
||||
|bencher| {
|
||||
bencher.to_async(&runtime).iter(
|
||||
// The routine itself.
|
||||
|| {
|
||||
let room_updates = room_updates.clone();
|
||||
let client = client.clone();
|
||||
|
||||
async move {
|
||||
client.event_cache().clear_all_rooms().await.unwrap();
|
||||
|
||||
client
|
||||
.event_cache()
|
||||
.handle_room_updates(room_updates.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
group.finish()
|
||||
}
|
||||
|
||||
fn find_event_relations(c: &mut Criterion) {
|
||||
// Number of other events to saturate the DB, but that will not be affected by
|
||||
// the benchmark. A small multiple of this number will be added.
|
||||
// When running locally, run with more events than in Codespeed CI.
|
||||
#[cfg(feature = "codspeed")]
|
||||
const NUM_OTHER_EVENTS: usize = 100;
|
||||
#[cfg(not(feature = "codspeed"))]
|
||||
const NUM_OTHER_EVENTS: usize = 1000;
|
||||
|
||||
// Create a new asynchronous runtime.
|
||||
let runtime = Builder::new_multi_thread()
|
||||
.enable_time()
|
||||
.enable_io()
|
||||
.build()
|
||||
.expect("Failed to create an asynchronous runtime");
|
||||
|
||||
let mut group = c.benchmark_group("Event cache room updates");
|
||||
group.sample_size(10);
|
||||
|
||||
let room_id = room_id!("!room:ben.ch");
|
||||
let other_room_id = room_id!("!other-room:ben.ch");
|
||||
|
||||
// Make the state store aware of the room, so that `client.get_room()` works
|
||||
// with it.
|
||||
let mut changes = matrix_sdk::StateChanges::default();
|
||||
changes.add_room(RoomInfo::new(room_id, RoomState::Joined));
|
||||
changes.add_room(RoomInfo::new(other_room_id, RoomState::Joined));
|
||||
let state_store = runtime.block_on(async {
|
||||
let state_store = matrix_sdk::MemoryStore::new();
|
||||
state_store.save_changes(&changes).await.unwrap();
|
||||
Arc::new(state_store)
|
||||
});
|
||||
|
||||
for num_related_events in [10, 100, 1000] {
|
||||
// Prefill the event cache store with one event and N related events.
|
||||
let mut room_updates = RoomUpdates::default();
|
||||
|
||||
let event_factory = EventFactory::new().room(room_id).sender(&ALICE);
|
||||
|
||||
let mut joined_room_update = JoinedRoomUpdate::default();
|
||||
|
||||
// Add the target event.
|
||||
let target_event_id = event_id!("$target");
|
||||
let target_event =
|
||||
event_factory.text_msg("hello world").event_id(target_event_id).into_event();
|
||||
joined_room_update.timeline.events.push(target_event);
|
||||
|
||||
// Add the numerous edits.
|
||||
for i in 0..num_related_events {
|
||||
let event_id = EventId::parse(format!("$edit{i}")).unwrap();
|
||||
let event = event_factory
|
||||
.text_msg(format!("* edit {i}"))
|
||||
.edit(
|
||||
target_event_id,
|
||||
RoomMessageEventContentWithoutRelation::text_plain(format!("edit {i}")),
|
||||
)
|
||||
.event_id(&event_id)
|
||||
.into();
|
||||
joined_room_update.timeline.events.push(event);
|
||||
}
|
||||
|
||||
// Add other events, in the same room, without a relation.
|
||||
for i in 0..NUM_OTHER_EVENTS {
|
||||
let event_id = EventId::parse(format!("$msg{i}")).unwrap();
|
||||
let event =
|
||||
event_factory.text_msg(format!("unrelated message {i}")).event_id(&event_id).into();
|
||||
joined_room_update.timeline.events.push(event);
|
||||
}
|
||||
|
||||
// Add other events, in the same room, related to other events.
|
||||
let other_target_event_id = event_id!("$other_target");
|
||||
let other_target_event =
|
||||
event_factory.text_msg("hello world").event_id(other_target_event_id).into_event();
|
||||
joined_room_update.timeline.events.push(other_target_event);
|
||||
|
||||
for i in 0..NUM_OTHER_EVENTS {
|
||||
let event_id = EventId::parse(format!("$unrelated{i}")).unwrap();
|
||||
let event =
|
||||
event_factory.reaction(other_target_event_id, "👍").event_id(&event_id).into();
|
||||
joined_room_update.timeline.events.push(event);
|
||||
}
|
||||
|
||||
room_updates.joined.insert(room_id.to_owned(), joined_room_update);
|
||||
|
||||
// Add other events, in another room.
|
||||
let mut other_joined_room_update = JoinedRoomUpdate::default();
|
||||
let event_factory = event_factory.room(other_room_id);
|
||||
for i in 0..NUM_OTHER_EVENTS {
|
||||
let event_id = EventId::parse(format!("$other_room{i}")).unwrap();
|
||||
let event = event_factory.text_msg(format!("hi {i}")).event_id(&event_id).into();
|
||||
other_joined_room_update.timeline.events.push(event);
|
||||
}
|
||||
room_updates.joined.insert(other_room_id.to_owned(), other_joined_room_update);
|
||||
|
||||
changes.add_room(RoomInfo::new(room_id, RoomState::Joined));
|
||||
|
||||
// Declare new stores for this set of events.
|
||||
let temp_dir = Arc::new(tempdir().unwrap());
|
||||
|
||||
let stores = vec![
|
||||
("memory", MemoryStore::default().into_event_cache_store()),
|
||||
(
|
||||
"SQLite",
|
||||
runtime.block_on(async {
|
||||
SqliteEventCacheStore::open(temp_dir.path().join("bench"), None)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_event_cache_store()
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
for (store_name, event_cache_store) in stores {
|
||||
let (client, room_event_cache, _drop_handles) = runtime.block_on(async {
|
||||
let client = MockClientBuilder::new(None)
|
||||
.on_builder(|builder| {
|
||||
builder.store_config(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
|
||||
.state_store(state_store.clone())
|
||||
.event_cache_store(event_cache_store),
|
||||
)
|
||||
})
|
||||
.build()
|
||||
.await;
|
||||
|
||||
client.event_cache().subscribe().unwrap();
|
||||
|
||||
// Sync the updates before starting the benchmark.
|
||||
let mut update_recv = client.event_cache().subscribe_to_room_generic_updates();
|
||||
|
||||
client.event_cache().handle_room_updates(room_updates.clone()).await.unwrap();
|
||||
|
||||
// Wait for the event cache to notify us of the room updates.
|
||||
let update = update_recv.recv().await.unwrap();
|
||||
assert!(update.room_id == room_id || update.room_id == other_room_id);
|
||||
|
||||
let update = update_recv.recv().await.unwrap();
|
||||
assert!(update.room_id == room_id || update.room_id == other_room_id);
|
||||
|
||||
let room = client.get_room(room_id).unwrap();
|
||||
let room_event_cache = room.event_cache().await.unwrap();
|
||||
|
||||
(client, room_event_cache.0, room_event_cache.1)
|
||||
});
|
||||
|
||||
// Define the throughput.
|
||||
group.throughput(Throughput::Elements(num_related_events));
|
||||
|
||||
for filter in [None, Some(vec![RelationType::Replacement])] {
|
||||
group.bench_function(
|
||||
BenchmarkId::new(
|
||||
format!("Event cache find_event_relations[{store_name}]"),
|
||||
format!(
|
||||
"{num_related_events} events, {} filter",
|
||||
if filter.is_some() { "edits" } else { "#no" },
|
||||
),
|
||||
),
|
||||
|bencher| {
|
||||
bencher.to_async(&runtime).iter_batched(
|
||||
// The setup.
|
||||
|| (room_event_cache.clone(), filter.clone()),
|
||||
// The routine itself.
|
||||
|(room_event_cache, filter)| async move {
|
||||
let (target, relations) = room_event_cache
|
||||
.find_event_with_relations(target_event_id, filter)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(target.event_id().as_deref().unwrap(), target_event_id);
|
||||
assert_eq!(relations.len(), num_related_events as usize);
|
||||
},
|
||||
criterion::BatchSize::PerIteration,
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(room_event_cache);
|
||||
drop(client);
|
||||
drop(_drop_handles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(state_store);
|
||||
}
|
||||
|
||||
group.finish()
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = event_cache;
|
||||
config = Criterion::default();
|
||||
targets = handle_room_updates, find_event_relations,
|
||||
}
|
||||
|
||||
criterion_main!(event_cache);
|
||||
@@ -1,16 +1,16 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput};
|
||||
use criterion::{BatchSize, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use matrix_sdk::{
|
||||
linked_chunk::{lazy_loader, LinkedChunk, LinkedChunkId, Update},
|
||||
SqliteEventCacheStore,
|
||||
linked_chunk::{LinkedChunk, LinkedChunkId, Update, lazy_loader},
|
||||
};
|
||||
use matrix_sdk_base::event_cache::{
|
||||
store::{DynEventCacheStore, IntoEventCacheStore, MemoryStore, DEFAULT_CHUNK_CAPACITY},
|
||||
Event, Gap,
|
||||
store::{DEFAULT_CHUNK_CAPACITY, DynEventCacheStore, IntoEventCacheStore, MemoryStore},
|
||||
};
|
||||
use matrix_sdk_test::{event_factory::EventFactory, ALICE};
|
||||
use ruma::{room_id, EventId};
|
||||
use matrix_sdk_test::{ALICE, event_factory::EventFactory};
|
||||
use ruma::{EventId, room_id};
|
||||
use tempfile::tempdir;
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
@@ -20,6 +20,11 @@ enum Operation {
|
||||
PushGapBack(Gap),
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "codspeed"))]
|
||||
const NUMBER_OF_EVENTS: &[u64] = &[10, 100, 1000, 10_000, 100_000];
|
||||
#[cfg(feature = "codspeed")]
|
||||
const NUMBER_OF_EVENTS: &[u64] = &[10, 100, 1000];
|
||||
|
||||
fn writing(c: &mut Criterion) {
|
||||
// Create a new asynchronous runtime.
|
||||
let runtime = Builder::new_multi_thread()
|
||||
@@ -32,10 +37,10 @@ fn writing(c: &mut Criterion) {
|
||||
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");
|
||||
let mut group = c.benchmark_group("Linked chunk writing");
|
||||
group.sample_size(10).measurement_time(Duration::from_secs(30));
|
||||
|
||||
for number_of_events in [10, 100, 1000, 10_000, 100_000] {
|
||||
for &number_of_events in NUMBER_OF_EVENTS {
|
||||
let sqlite_temp_dir = tempdir().unwrap();
|
||||
|
||||
// Declare new stores for this set of events.
|
||||
@@ -96,7 +101,7 @@ fn writing(c: &mut Criterion) {
|
||||
|
||||
// Get a bencher.
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new(store_name, number_of_events),
|
||||
BenchmarkId::new(format!("Linked chunk writing [{store_name}]"), number_of_events),
|
||||
&operations,
|
||||
|bencher, operations| {
|
||||
// Bench the routine.
|
||||
@@ -149,10 +154,10 @@ fn reading(c: &mut Criterion) {
|
||||
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");
|
||||
let mut group = c.benchmark_group("Linked chunk reading");
|
||||
group.sample_size(10);
|
||||
|
||||
for num_events in [10, 100, 1000, 10_000, 100_000] {
|
||||
for &num_events in NUMBER_OF_EVENTS {
|
||||
let sqlite_temp_dir = tempdir().unwrap();
|
||||
|
||||
// Declare new stores for this set of events.
|
||||
@@ -187,11 +192,14 @@ fn reading(c: &mut Criterion) {
|
||||
|
||||
while events.peek().is_some() {
|
||||
let events_chunk = events.by_ref().take(80).collect::<Vec<_>>();
|
||||
|
||||
if events_chunk.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
lc.push_items_back(events_chunk);
|
||||
lc.push_gap_back(Gap { prev_token: format!("gap{num_gaps}") });
|
||||
|
||||
num_gaps += 1;
|
||||
}
|
||||
|
||||
@@ -205,30 +213,47 @@ fn reading(c: &mut Criterion) {
|
||||
// Define the throughput.
|
||||
group.throughput(Throughput::Elements(num_events));
|
||||
|
||||
// Get a bencher.
|
||||
group.bench_function(BenchmarkId::new(store_name, num_events), |bencher| {
|
||||
// Bench the routine.
|
||||
bencher.to_async(&runtime).iter(|| async {
|
||||
// Load the last chunk first,
|
||||
let (last_chunk, chunk_id_gen) =
|
||||
store.load_last_chunk(linked_chunk_id).await.unwrap();
|
||||
// Bench the lazy loader.
|
||||
group.bench_function(
|
||||
BenchmarkId::new(format!("Linked chunk lazy loader[{store_name}]"), num_events),
|
||||
|bencher| {
|
||||
// Bench the routine.
|
||||
bencher.to_async(&runtime).iter(|| async {
|
||||
// Load the last chunk first,
|
||||
let (last_chunk, chunk_id_gen) =
|
||||
store.load_last_chunk(linked_chunk_id).await.unwrap();
|
||||
|
||||
let mut lc =
|
||||
lazy_loader::from_last_chunk::<128, _, _>(last_chunk, chunk_id_gen)
|
||||
.expect("no error when reconstructing the linked chunk")
|
||||
.expect("there is a linked chunk in the store");
|
||||
let mut lc =
|
||||
lazy_loader::from_last_chunk::<128, _, _>(last_chunk, chunk_id_gen)
|
||||
.expect("no error when reconstructing the linked chunk")
|
||||
.expect("there is a linked chunk in the store");
|
||||
|
||||
// Then load until the start of the linked chunk.
|
||||
let mut cur_chunk_id = lc.chunks().next().unwrap().identifier();
|
||||
while let Some(prev) =
|
||||
store.load_previous_chunk(linked_chunk_id, cur_chunk_id).await.unwrap()
|
||||
{
|
||||
cur_chunk_id = prev.identifier;
|
||||
lazy_loader::insert_new_first_chunk(&mut lc, prev)
|
||||
.expect("no error when linking the previous lazy-loaded chunk");
|
||||
}
|
||||
})
|
||||
});
|
||||
// Then load until the start of the linked chunk.
|
||||
let mut cur_chunk_id = lc.chunks().next().unwrap().identifier();
|
||||
while let Some(prev) =
|
||||
store.load_previous_chunk(linked_chunk_id, cur_chunk_id).await.unwrap()
|
||||
{
|
||||
cur_chunk_id = prev.identifier;
|
||||
lazy_loader::insert_new_first_chunk(&mut lc, prev)
|
||||
.expect("no error when linking the previous lazy-loaded chunk");
|
||||
}
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
// Bench the metadata loader.
|
||||
group.bench_function(
|
||||
BenchmarkId::new(format!("Linked chunk metadata loader[{store_name}]"), num_events),
|
||||
|bencher| {
|
||||
// Bench the routine.
|
||||
bencher.to_async(&runtime).iter(|| async {
|
||||
let _metadata = store
|
||||
.load_all_chunks_metadata(linked_chunk_id)
|
||||
.await
|
||||
.expect("metadata must load");
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
@@ -240,21 +265,9 @@ fn reading(c: &mut Criterion) {
|
||||
group.finish()
|
||||
}
|
||||
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
let criterion = Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
100,
|
||||
pprof::criterion::Output::Flamegraph(None),
|
||||
));
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let criterion = Criterion::default();
|
||||
|
||||
criterion
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = event_cache;
|
||||
config = criterion();
|
||||
config = Criterion::default();
|
||||
targets = writing, reading,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use matrix_sdk::{store::RoomLoadSettings, test_utils::mocks::MatrixMockServer};
|
||||
use matrix_sdk_base::{
|
||||
store::StoreConfig, BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore,
|
||||
BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore, ThreadingSupport,
|
||||
store::StoreConfig,
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteStateStore;
|
||||
use matrix_sdk_test::{event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent};
|
||||
use matrix_sdk_test::{JoinedRoomBuilder, StateTestEvent, event_factory::EventFactory};
|
||||
use matrix_sdk_ui::timeline::{TimelineBuilder, TimelineFocus};
|
||||
use ruma::{
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId,
|
||||
api::client::membership::get_member_events,
|
||||
device_id,
|
||||
events::room::member::{MembershipState, RoomMemberEvent},
|
||||
mxc_uri, owned_room_id, owned_user_id,
|
||||
serde::Raw,
|
||||
user_id, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId,
|
||||
user_id,
|
||||
};
|
||||
use serde_json::json;
|
||||
use tokio::runtime::Builder;
|
||||
@@ -58,6 +60,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
|
||||
@@ -82,7 +85,7 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
|
||||
group.throughput(Throughput::Elements(count as u64));
|
||||
group.sample_size(50);
|
||||
|
||||
group.bench_function(BenchmarkId::new("receive_members", name), |b| {
|
||||
group.bench_function(BenchmarkId::new("Handle /members request [SQLite]", name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
base_client.receive_all_members(&room_id, &request, &response).await.unwrap();
|
||||
});
|
||||
@@ -162,11 +165,11 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
|
||||
let count = PINNED_EVENTS_COUNT;
|
||||
let name = format!("{count} pinned events");
|
||||
let mut group = c.benchmark_group("Test");
|
||||
let mut group = c.benchmark_group("Load pinned events");
|
||||
group.throughput(Throughput::Elements(count as u64));
|
||||
group.sample_size(10);
|
||||
|
||||
group.bench_function(BenchmarkId::new("load_pinned_events", name), |b| {
|
||||
group.bench_function(BenchmarkId::new("Load pinned events [memory]", name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
|
||||
assert!(!pinned_event_ids.is_empty());
|
||||
@@ -205,24 +208,9 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
100,
|
||||
pprof::criterion::Output::Flamegraph(None),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
Criterion::default()
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = room;
|
||||
config = criterion();
|
||||
config = Criterion::default();
|
||||
targets = receive_all_members_benchmark, load_pinned_events_benchmark,
|
||||
}
|
||||
criterion_main!(room);
|
||||
|
||||
@@ -1,28 +1,15 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use matrix_sdk::{
|
||||
authentication::matrix::MatrixSession, config::StoreConfig, Client, RoomInfo, RoomState,
|
||||
SessionTokens, StateChanges,
|
||||
Client, RoomInfo, RoomState, SessionTokens, StateChanges,
|
||||
authentication::matrix::MatrixSession, config::StoreConfig,
|
||||
};
|
||||
use matrix_sdk_base::{store::MemoryStore, SessionMeta, StateStore as _};
|
||||
use matrix_sdk_base::{SessionMeta, StateStore as _, store::MemoryStore};
|
||||
use matrix_sdk_sqlite::SqliteStateStore;
|
||||
use ruma::{device_id, user_id, RoomId};
|
||||
use ruma::{RoomId, device_id, user_id};
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
let criterion = Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
100,
|
||||
pprof::criterion::Output::Flamegraph(None),
|
||||
));
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let criterion = Criterion::default();
|
||||
|
||||
criterion
|
||||
}
|
||||
|
||||
/// Number of joined rooms in the benchmark.
|
||||
const NUM_JOINED_ROOMS: usize = 10000;
|
||||
|
||||
@@ -30,7 +17,7 @@ const NUM_JOINED_ROOMS: usize = 10000;
|
||||
const NUM_STRIPPED_JOINED_ROOMS: usize = 10000;
|
||||
|
||||
pub fn restore_session(c: &mut Criterion) {
|
||||
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
|
||||
let runtime = Builder::new_multi_thread().enable_time().build().expect("Can't create runtime");
|
||||
|
||||
// Create a fake list of changes, and a session to recover from.
|
||||
let mut changes = StateChanges::default();
|
||||
@@ -58,13 +45,11 @@ pub fn restore_session(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("Client reload");
|
||||
group.throughput(Throughput::Elements(100));
|
||||
|
||||
const NAME: &str = "restore a session";
|
||||
|
||||
// Memory
|
||||
let mem_store = Arc::new(MemoryStore::new());
|
||||
runtime.block_on(mem_store.save_changes(&changes)).expect("initial filling of mem failed");
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("memory store", NAME), &mem_store, |b, store| {
|
||||
group.bench_with_input("Restore session [memory store]", &mem_store, |b, store| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let client = Client::builder()
|
||||
.homeserver_url("https://matrix.example.com")
|
||||
@@ -92,7 +77,7 @@ pub fn restore_session(c: &mut Criterion) {
|
||||
.expect("initial filling of sqlite failed");
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new(format!("sqlite store {encrypted_suffix}"), NAME),
|
||||
BenchmarkId::new("Restore session [SQLite]", encrypted_suffix),
|
||||
&sqlite_store,
|
||||
|b, store| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
@@ -124,7 +109,7 @@ pub fn restore_session(c: &mut Criterion) {
|
||||
|
||||
criterion_group! {
|
||||
name = benches;
|
||||
config = criterion();
|
||||
config = Criterion::default();
|
||||
targets = restore_session
|
||||
}
|
||||
criterion_main!(benches);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use matrix_sdk::test_utils::mocks::MatrixMockServer;
|
||||
use matrix_sdk_test::{event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent};
|
||||
use matrix_sdk_test::{JoinedRoomBuilder, StateTestEvent, event_factory::EventFactory};
|
||||
use matrix_sdk_ui::timeline::TimelineBuilder;
|
||||
use ruma::{
|
||||
events::room::message::RoomMessageEventContentWithoutRelation, owned_room_id, owned_user_id,
|
||||
EventId,
|
||||
EventId, events::room::message::RoomMessageEventContentWithoutRelation, owned_room_id,
|
||||
owned_user_id,
|
||||
};
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
@@ -94,12 +94,12 @@ pub fn create_timeline_with_initial_events(c: &mut Criterion) {
|
||||
room
|
||||
});
|
||||
|
||||
let mut group = c.benchmark_group("Test");
|
||||
let mut group = c.benchmark_group("Create a timeline");
|
||||
group.throughput(Throughput::Elements(NUM_EVENTS as _));
|
||||
group.sample_size(10);
|
||||
|
||||
group.bench_function(
|
||||
BenchmarkId::new("create_timeline_with_initial_events", format!("{NUM_EVENTS} events")),
|
||||
BenchmarkId::new("Create a timeline with initial events", format!("{NUM_EVENTS} events")),
|
||||
|b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let timeline = TimelineBuilder::new(&room)
|
||||
@@ -117,24 +117,9 @@ pub fn create_timeline_with_initial_events(c: &mut Criterion) {
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
100,
|
||||
pprof::criterion::Output::Flamegraph(None),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
Criterion::default()
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = room;
|
||||
config = criterion();
|
||||
config = Criterion::default();
|
||||
targets = create_timeline_with_initial_events
|
||||
}
|
||||
criterion_main!(room);
|
||||
|
||||
@@ -23,14 +23,20 @@ path = "uniffi-bindgen.rs"
|
||||
default = ["bundled-sqlite"]
|
||||
bundled-sqlite = ["matrix-sdk-sqlite/bundled"]
|
||||
|
||||
# Enable experimental support for encrypting state events; see
|
||||
# https://github.com/matrix-org/matrix-rust-sdk/issues/5397.
|
||||
experimental-encrypted-state-events = [
|
||||
"matrix-sdk-crypto/experimental-encrypted-state-events",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
futures-util.workspace = true
|
||||
hmac = "0.12.1"
|
||||
hmac.workspace = true
|
||||
http.workspace = true
|
||||
matrix-sdk-common = { workspace = true, features = ["uniffi"] }
|
||||
matrix-sdk-ffi-macros.workspace = true
|
||||
pbkdf2 = "0.12.2"
|
||||
pbkdf2.workspace = true
|
||||
rand.workspace = true
|
||||
ruma.workspace = true
|
||||
serde.workspace = true
|
||||
@@ -56,17 +62,17 @@ workspace = true
|
||||
features = ["crypto-store"]
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.43.1"
|
||||
workspace = true
|
||||
default-features = false
|
||||
features = ["rt-multi-thread"]
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
vergen = { version = "8.2.5", features = ["build", "git", "gitcl"] }
|
||||
vergen-gitcl = { workspace = true, features = ["build"] }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches2.workspace = true
|
||||
tempfile = "3.8.0"
|
||||
tempfile.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use vergen::EmitBuilder;
|
||||
use vergen_gitcl::{Emitter, GitclBuilder};
|
||||
|
||||
/// Adds a temporary workaround for an issue with the Rust compiler and Android
|
||||
/// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717.
|
||||
@@ -59,7 +59,8 @@ fn get_clang_major_version(clang_path: &Path) -> String {
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
setup_x86_64_android_workaround();
|
||||
|
||||
EmitBuilder::builder().git_sha(true).git_describe(true, false, None).emit()?;
|
||||
let git_config = GitclBuilder::default().sha(true).describe(true, false, None).build()?;
|
||||
Emitter::default().add_instructions(&git_config)?.emit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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,16 @@
|
||||
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,
|
||||
DecryptionSettings,
|
||||
};
|
||||
use ruma::{api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw, OwnedDeviceId};
|
||||
use serde_json::json;
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
use crate::{CryptoStoreError, DehydratedDeviceKey};
|
||||
|
||||
@@ -154,9 +155,13 @@ impl Drop for RehydratedDevice {
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RehydratedDevice {
|
||||
pub fn receive_events(&self, events: String) -> Result<(), crate::CryptoStoreError> {
|
||||
pub fn receive_events(
|
||||
&self,
|
||||
events: String,
|
||||
decryption_settings: &DecryptionSettings,
|
||||
) -> Result<(), crate::CryptoStoreError> {
|
||||
let events: Vec<Raw<AnyToDeviceEvent>> = serde_json::from_str(&events)?;
|
||||
self.runtime.block_on(self.inner.receive_events(events))?;
|
||||
self.runtime.block_on(self.inner.receive_events(events, decryption_settings))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -662,6 +665,9 @@ impl From<HistoryVisibility> for RustHistoryVisibility {
|
||||
pub struct EncryptionSettings {
|
||||
/// The encryption algorithm that should be used in the room.
|
||||
pub algorithm: EventEncryptionAlgorithm,
|
||||
/// Whether state event encryption is enabled.
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
pub encrypt_state_events: bool,
|
||||
/// How long can the room key be used before it should be rotated. Time in
|
||||
/// seconds.
|
||||
pub rotation_period: u64,
|
||||
@@ -691,6 +697,8 @@ impl From<EncryptionSettings> for RustEncryptionSettings {
|
||||
|
||||
RustEncryptionSettings {
|
||||
algorithm: v.algorithm.into(),
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
encrypt_state_events: false,
|
||||
rotation_period: Duration::from_secs(v.rotation_period),
|
||||
rotation_period_msgs: v.rotation_period_msgs,
|
||||
history_visibility: v.history_visibility.into(),
|
||||
@@ -818,10 +826,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 +874,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 }
|
||||
}
|
||||
}
|
||||
@@ -907,6 +915,10 @@ impl From<matrix_sdk_crypto::CrossSigningStatus> for CrossSigningStatus {
|
||||
pub struct RoomSettings {
|
||||
/// The encryption algorithm that should be used in the room.
|
||||
pub algorithm: EventEncryptionAlgorithm,
|
||||
/// Whether state event encryption is enabled.
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
#[serde(default)]
|
||||
pub encrypt_state_events: bool,
|
||||
/// Should untrusted devices receive the room key, or should they be
|
||||
/// excluded from the conversation.
|
||||
pub only_allow_trusted_devices: bool,
|
||||
@@ -917,7 +929,12 @@ impl TryFrom<RustRoomSettings> for RoomSettings {
|
||||
|
||||
fn try_from(value: RustRoomSettings) -> Result<Self, Self::Error> {
|
||||
let algorithm = value.algorithm.try_into()?;
|
||||
Ok(Self { algorithm, only_allow_trusted_devices: value.only_allow_trusted_devices })
|
||||
Ok(Self {
|
||||
algorithm,
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
encrypt_state_events: value.encrypt_state_events,
|
||||
only_allow_trusted_devices: value.only_allow_trusted_devices,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1170,6 +1187,8 @@ mod tests {
|
||||
assert_eq!(
|
||||
Some(RoomSettings {
|
||||
algorithm: EventEncryptionAlgorithm::OlmV1Curve25519AesSha2,
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
encrypt_state_events: false,
|
||||
only_allow_trusted_devices: true
|
||||
}),
|
||||
settings1
|
||||
@@ -1179,6 +1198,8 @@ mod tests {
|
||||
assert_eq!(
|
||||
Some(RoomSettings {
|
||||
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2,
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
encrypt_state_events: false,
|
||||
only_allow_trusted_devices: false
|
||||
}),
|
||||
settings2
|
||||
|
||||
@@ -16,9 +16,10 @@ 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,
|
||||
CollectStrategy, DecryptionSettings, LocalTrust, OlmMachine as InnerMachine,
|
||||
UserIdentity as SdkUserIdentity,
|
||||
};
|
||||
use ruma::{
|
||||
api::{
|
||||
@@ -38,7 +39,7 @@ use ruma::{
|
||||
},
|
||||
events::{
|
||||
key::verification::VerificationMethod, room::message::MessageType, AnyMessageLikeEvent,
|
||||
AnySyncMessageLikeEvent, MessageLikeEvent,
|
||||
AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
to_device::DeviceIdOrAllDevices,
|
||||
@@ -96,8 +97,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(),
|
||||
@@ -526,6 +527,7 @@ impl OlmMachine {
|
||||
key_counts: HashMap<String, i32>,
|
||||
unused_fallback_keys: Option<Vec<String>>,
|
||||
next_batch_token: String,
|
||||
decryption_settings: &DecryptionSettings,
|
||||
) -> Result<SyncChangesResult, CryptoStoreError> {
|
||||
let to_device: ToDevice = serde_json::from_str(&events)?;
|
||||
let device_changes: RumaDeviceLists = device_changes.into();
|
||||
@@ -544,15 +546,17 @@ impl OlmMachine {
|
||||
let unused_fallback_keys: Option<Vec<OneTimeKeyAlgorithm>> =
|
||||
unused_fallback_keys.map(|u| u.into_iter().map(OneTimeKeyAlgorithm::from).collect());
|
||||
|
||||
let (to_device_events, room_key_infos) = self.runtime.block_on(
|
||||
self.inner.receive_sync_changes(matrix_sdk_crypto::EncryptionSyncChanges {
|
||||
to_device_events: to_device.events,
|
||||
changed_devices: &device_changes,
|
||||
one_time_keys_counts: &key_counts,
|
||||
unused_fallback_keys: unused_fallback_keys.as_deref(),
|
||||
next_batch_token: Some(next_batch_token),
|
||||
}),
|
||||
)?;
|
||||
let (to_device_events, room_key_infos) =
|
||||
self.runtime.block_on(self.inner.receive_sync_changes(
|
||||
matrix_sdk_crypto::EncryptionSyncChanges {
|
||||
to_device_events: to_device.events,
|
||||
changed_devices: &device_changes,
|
||||
one_time_keys_counts: &key_counts,
|
||||
unused_fallback_keys: unused_fallback_keys.as_deref(),
|
||||
next_batch_token: Some(next_batch_token),
|
||||
},
|
||||
decryption_settings,
|
||||
))?;
|
||||
|
||||
let to_device_events = to_device_events
|
||||
.into_iter()
|
||||
@@ -829,6 +833,7 @@ impl OlmMachine {
|
||||
device_id: String,
|
||||
event_type: String,
|
||||
content: String,
|
||||
share_strategy: CollectStrategy,
|
||||
) -> Result<Option<Request>, CryptoStoreError> {
|
||||
let user_id = parse_user_id(&user_id)?;
|
||||
let device_id = device_id.as_str().into();
|
||||
@@ -837,8 +842,11 @@ impl OlmMachine {
|
||||
let device = self.runtime.block_on(self.inner.get_device(&user_id, device_id, None))?;
|
||||
|
||||
if let Some(device) = device {
|
||||
let encrypted_content =
|
||||
self.runtime.block_on(device.encrypt_event_raw(&event_type, &content))?;
|
||||
let encrypted_content = self.runtime.block_on(device.encrypt_event_raw(
|
||||
&event_type,
|
||||
&content,
|
||||
share_strategy,
|
||||
))?;
|
||||
|
||||
let request = ToDeviceRequest::new(
|
||||
user_id.as_ref(),
|
||||
@@ -894,7 +902,7 @@ impl OlmMachine {
|
||||
))?;
|
||||
|
||||
if handle_verification_events {
|
||||
if let Ok(e) = decrypted.event.deserialize() {
|
||||
if let Ok(AnyTimelineEvent::MessageLike(e)) = decrypted.event.deserialize() {
|
||||
match &e {
|
||||
AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original(
|
||||
original_event,
|
||||
|
||||
@@ -27,7 +27,7 @@ use ruma::{
|
||||
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
|
||||
},
|
||||
assign,
|
||||
events::EventContent,
|
||||
events::MessageLikeEventContent,
|
||||
OwnedTransactionId, UserId,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -6,11 +6,135 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
## [0.14.0] - 2025-09-04
|
||||
|
||||
### Features:
|
||||
|
||||
- Add `LowPriority` and `NonLowPriority` variants to `RoomListEntriesDynamicFilterKind` for filtering
|
||||
rooms based on their low priority status. These filters allow clients to show only low priority rooms
|
||||
or exclude low priority rooms from the room list.
|
||||
([#5508](https://github.com/matrix-org/matrix-rust-sdk/pull/5508))
|
||||
- Add `room_version` and `privileged_creators_role` to `RoomInfo` ([#5449](https://github.com/matrix-org/matrix-rust-sdk/pull/5449)).
|
||||
- The [`unstable-hydra`] feature has been enabled, which enables room v12 changes in the SDK.
|
||||
([#5450](https://github.com/matrix-org/matrix-rust-sdk/pull/5450)).
|
||||
- Add experimental support for
|
||||
[MSC4306](https://github.com/matrix-org/matrix-spec-proposals/pull/4306), with the
|
||||
`Room::fetch_thread_subscription()` and `Room::set_thread_subscription()` methods.
|
||||
([#5442](https://github.com/matrix-org/matrix-rust-sdk/pull/5442))
|
||||
- [**breaking**] [`GalleryUploadParameters::reply`] and [`UploadParameters::reply`] have been both
|
||||
replaced with a new optional `in_reply_to` field, that's a string which will be parsed into an
|
||||
`OwnedEventId` when sending the event. The thread relationship will be automatically filled in,
|
||||
based on the timeline focus.
|
||||
([5427](https://github.com/matrix-org/matrix-rust-sdk/pull/5427))
|
||||
- [**breaking**] [`Timeline::send_reply()`] now automatically fills in the thread relationship,
|
||||
based on the timeline focus. As a result, it only takes an `OwnedEventId` parameter, instead of
|
||||
the `Reply` type. The proper way to start a thread is now thus to create a threaded-focused
|
||||
timeline, and then use `Timeline::send()`.
|
||||
([5427](https://github.com/matrix-org/matrix-rust-sdk/pull/5427))
|
||||
- Add `HomeserverLoginDetails::supports_sso_login` for legacy SSO support information.
|
||||
This is primarily for Element X to give a dedicated error message in case
|
||||
it connects a homeserver with only this method available.
|
||||
([#5222](https://github.com/matrix-org/matrix-rust-sdk/pull/5222))
|
||||
|
||||
### Breaking changes:
|
||||
|
||||
- The timeline will now always use the send queue to upload medias, so the
|
||||
`UploadParameters::use_send_queue` bool has been removed. Make sure to listen to the send queue's
|
||||
error updates, and to handle send queue restarts.
|
||||
([#5525](https://github.com/matrix-org/matrix-rust-sdk/pull/5525))
|
||||
- Support for the legacy media upload progress has been disabled. Media upload progress is
|
||||
available through the send queue, and can be enabled thanks to
|
||||
`Client::enable_send_queue_upload_progress()`.
|
||||
([#5525](https://github.com/matrix-org/matrix-rust-sdk/pull/5525))
|
||||
- `TimelineDiff` is now exported as a true `uniffi::Enum` instead of the weird `uniffi::Object` hybrid. This matches
|
||||
both `RoomDirectorySearchEntryUpdate` and `RoomListEntriesUpdate` and can be used in the same way.
|
||||
([#5474](https://github.com/matrix-org/matrix-rust-sdk/pull/5474))
|
||||
- The `creator` field of `RoomInfo` has been renamed to `creators` and can now contain a list of
|
||||
user IDs, to reflect that a room can now have several creators, as introduced in room version 12.
|
||||
([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436))
|
||||
- The `PowerLevel` type was introduced to represent power levels instead of `i64` to differentiate
|
||||
the infinite power level of creators, as introduced in room version 12. It is used in
|
||||
`suggested_role_for_power_level`, `suggested_power_level_for_role` and `RoomMember`.
|
||||
([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436))
|
||||
- `Client::get_url` now returns a `Vec<u8>` instead of a `String`. It also throws an error when the
|
||||
response isn't status code 200 OK, instead of providing the error in the response body.
|
||||
([#5438](https://github.com/matrix-org/matrix-rust-sdk/pull/5438))
|
||||
- `RoomPreview::info()` doesn't return a result anymore. All unknown join rules are handled in the
|
||||
`JoinRule::Custom` variant.
|
||||
([#5337](https://github.com/matrix-org/matrix-rust-sdk/pull/5337))
|
||||
- The `reason` argument of `Room::report_room` is now required, do to a clarification in the spec.
|
||||
([#5337](https://github.com/matrix-org/matrix-rust-sdk/pull/5337))
|
||||
- `PublicRoomJoinRule` has more variants, supporting all the known values from the spec.
|
||||
([#5337](https://github.com/matrix-org/matrix-rust-sdk/pull/5337))
|
||||
- The fields of `MediaPreviewConfig` are both optional, allowing to use the type for room account
|
||||
data as well as global account data.
|
||||
([#5337](https://github.com/matrix-org/matrix-rust-sdk/pull/5337))
|
||||
- The `event_id` field of `PredecessorRoom` was removed, due to its removal in the Matrix
|
||||
specification with MSC4291.
|
||||
([#5419](https://github.com/matrix-org/matrix-rust-sdk/pull/5419))
|
||||
- `Client::url_for_oidc` now allows requesting additional scopes for the OAuth2 authorization code grant.
|
||||
([#5395](https://github.com/matrix-org/matrix-rust-sdk/pull/5395))
|
||||
- `Client::url_for_oidc` now allows passing an optional existing device id from a previous login call.
|
||||
([#5394](https://github.com/matrix-org/matrix-rust-sdk/pull/5394))
|
||||
- `ClientBuilder::build_with_qr_code` has been removed. Instead, the Client should be built by passing
|
||||
`QrCodeData::server_name` to `ClientBuilder::server_name_or_homeserver_url`, after which QR login can be performed by
|
||||
calling `Client::login_with_qr_code`. ([#5388](https://github.com/matrix-org/matrix-rust-sdk/pull/5388))
|
||||
- The MSRV has been bumped to Rust 1.88.
|
||||
([#5431](https://github.com/matrix-org/matrix-rust-sdk/pull/5431))
|
||||
- `Room::send_call_notification` and `Room::send_call_notification_if_needed` have been removed, since the event type they send is outdated, and `Client` is not actually supposed to be able to join MatrixRTC sessions (yet). In practice, users of these methods probably already rely on another MatrixRTC implementation to participate in sessions, and such an implementation should be capable of sending notifications itself.
|
||||
- The `GalleryItemInfo` variants now take an `UploadSource` rather than a `String` path to enable uploading
|
||||
from bytes directly.
|
||||
([#5529](https://github.com/matrix-org/matrix-rust-sdk/pull/5529))
|
||||
- Media and gallery uploads now use `UploadSource` to specify the thumbnail.
|
||||
([#5530](https://github.com/matrix-org/matrix-rust-sdk/pull/5530))
|
||||
|
||||
## [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
|
||||
- `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.
|
||||
@@ -25,12 +149,13 @@ Breaking changes:
|
||||
|
||||
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,
|
||||
- `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).
|
||||
- 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.
|
||||
@@ -46,7 +171,7 @@ Additions:
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- `contacts` has been removed from `OidcConfiguration` (it was unused since the switch to OAuth).
|
||||
- `contacts` has been removed from `OidcConfiguration` (it was unused since the switch to OAuth).
|
||||
|
||||
## [0.11.0] - 2025-04-11
|
||||
|
||||
@@ -123,7 +248,8 @@ Breaking changes:
|
||||
- The `dynamic_registrations_file` field of `OidcConfiguration` was removed.
|
||||
Clients are supposed to re-register with the homeserver for every login.
|
||||
|
||||
- `RoomPreview::own_membership_details` is now `RoomPreview::member_with_sender_info`, takes any user id and returns an `Option<RoomMemberWithSenderInfo>`.
|
||||
- `RoomPreview::own_membership_details` is now `RoomPreview::member_with_sender_info`, takes any user id and returns an
|
||||
`Option<RoomMemberWithSenderInfo>`.
|
||||
|
||||
Additions:
|
||||
|
||||
@@ -138,9 +264,11 @@ Additions:
|
||||
- Add `Timeline::send_thread_reply` for clients that need to start threads
|
||||
themselves.
|
||||
([4819](https://github.com/matrix-org/matrix-rust-sdk/pull/4819))
|
||||
- Add `ClientBuilder::session_pool_max_size`, `::session_cache_size` and `::session_journal_size_limit` to control the stores configuration, especially their memory consumption
|
||||
- Add `ClientBuilder::session_pool_max_size`, `::session_cache_size` and `::session_journal_size_limit` to control the
|
||||
stores configuration, especially their memory consumption
|
||||
([#4870](https://github.com/matrix-org/matrix-rust-sdk/pull/4870/))
|
||||
- Add `ClientBuilder::system_is_memory_constrained` to indicate that the system
|
||||
has less memory available than the current standard
|
||||
([#4894](https://github.com/matrix-org/matrix-rust-sdk/pull/4894))
|
||||
- Add `Room::member_with_sender_info` to get both a room member's info and for the user who sent the `m.room.member` event the `RoomMember` is based on.
|
||||
- Add `Room::member_with_sender_info` to get both a room member's info and for the user who sent the `m.room.member`
|
||||
event the `RoomMember` is based on.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "matrix-sdk-ffi"
|
||||
version = "0.12.0"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ffi"]
|
||||
@@ -14,102 +14,90 @@ publish = false
|
||||
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", "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
|
||||
async-compat = "0.2.4"
|
||||
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"] }
|
||||
sentry-tracing = "0.36.0"
|
||||
ruma = { workspace = true, features = ["html", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat", "unstable-msc4278", "unstable-hydra"] }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sentry = { workspace = true, optional = true, default-features = false, features = [
|
||||
# Most default features enabled otherwise.
|
||||
"backtrace",
|
||||
"contexts",
|
||||
"panic",
|
||||
"reqwest",
|
||||
"sentry-debug-images",
|
||||
] }
|
||||
sentry-tracing = { workspace = true, optional = true }
|
||||
thiserror.workspace = true
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
tracing.workspace = true
|
||||
tracing-appender = { version = "0.2.2" }
|
||||
tracing-appender.workspace = true
|
||||
tracing-core.workspace = true
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
uniffi = { workspace = true, features = ["tokio"] }
|
||||
url.workspace = true
|
||||
uuid = { version = "1.4.1", features = ["v4"] }
|
||||
zeroize.workspace = true
|
||||
oauth2.workspace = true
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies.matrix-sdk]
|
||||
workspace = true
|
||||
features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
"experimental-widgets",
|
||||
"markdown",
|
||||
# note: differ from block below
|
||||
"native-tls",
|
||||
"socks",
|
||||
"sqlite",
|
||||
"uniffi",
|
||||
]
|
||||
[target.'cfg(target_family = "wasm")'.dependencies]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
tokio = { workspace = true, features = ["sync", "macros"] }
|
||||
uniffi.workspace = true
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies.sentry]
|
||||
version = "0.36.0"
|
||||
default-features = false
|
||||
features = [
|
||||
# TLS lib used on non-Android platforms.
|
||||
"native-tls",
|
||||
# Most default features enabled otherwise.
|
||||
"backtrace",
|
||||
"contexts",
|
||||
"panic",
|
||||
"reqwest",
|
||||
]
|
||||
[target.'cfg(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"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies.matrix-sdk]
|
||||
workspace = true
|
||||
features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
"experimental-widgets",
|
||||
"markdown",
|
||||
# note: differ from block above
|
||||
"rustls-tls",
|
||||
"socks",
|
||||
"sqlite",
|
||||
"uniffi",
|
||||
]
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies.sentry]
|
||||
version = "0.36.0"
|
||||
default-features = false
|
||||
features = [
|
||||
# TLS lib specific for Android.
|
||||
"rustls",
|
||||
# Most default features enabled otherwise.
|
||||
"backtrace",
|
||||
"contexts",
|
||||
"panic",
|
||||
"reqwest",
|
||||
]
|
||||
[dev-dependencies]
|
||||
similar-asserts.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
vergen = { version = "8.1.3", features = ["build", "git", "gitcl"] }
|
||||
vergen-gitcl = { workspace = true, features = ["build"] }
|
||||
|
||||
[lints]
|
||||
workspace = 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
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use vergen::EmitBuilder;
|
||||
use vergen_gitcl::{Emitter, GitclBuilder};
|
||||
|
||||
/// Adds a temporary workaround for an issue with the Rust compiler and Android
|
||||
/// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717.
|
||||
@@ -59,6 +59,9 @@ fn get_clang_major_version(clang_path: &Path) -> String {
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
setup_x86_64_android_workaround();
|
||||
uniffi::generate_scaffolding("./src/api.udl").expect("Building the UDL file failed");
|
||||
EmitBuilder::builder().git_sha(true).emit()?;
|
||||
|
||||
let git_config = GitclBuilder::default().sha(true).build()?;
|
||||
Emitter::default().add_instructions(&git_config)?.emit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ pub struct HomeserverLoginDetails {
|
||||
pub(crate) sliding_sync_version: SlidingSyncVersion,
|
||||
pub(crate) supports_oidc_login: bool,
|
||||
pub(crate) supported_oidc_prompts: Vec<OidcPrompt>,
|
||||
pub(crate) supports_sso_login: bool,
|
||||
pub(crate) supports_password_login: bool,
|
||||
}
|
||||
|
||||
@@ -43,6 +44,11 @@ impl HomeserverLoginDetails {
|
||||
self.supports_oidc_login
|
||||
}
|
||||
|
||||
/// Whether the current homeserver supports login using legacy SSO.
|
||||
pub fn supports_sso_login(&self) -> bool {
|
||||
self.supports_sso_login
|
||||
}
|
||||
|
||||
/// The prompts advertised by the authentication issuer for use in the login
|
||||
/// URL.
|
||||
pub fn supported_oidc_prompts(&self) -> Vec<OidcPrompt> {
|
||||
|
||||
@@ -2,24 +2,26 @@ use std::{
|
||||
collections::HashMap,
|
||||
fmt::Debug,
|
||||
path::PathBuf,
|
||||
sync::{Arc, OnceLock, RwLock},
|
||||
sync::{Arc, OnceLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context as _};
|
||||
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,
|
||||
@@ -37,7 +39,7 @@ use matrix_sdk::{
|
||||
},
|
||||
sliding_sync::Version as SdkSlidingSyncVersion,
|
||||
store::RoomLoadSettings as SdkRoomLoadSettings,
|
||||
AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens,
|
||||
Account, AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens,
|
||||
STATE_STORE_DATABASE_NAME,
|
||||
};
|
||||
use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm};
|
||||
@@ -46,11 +48,18 @@ use matrix_sdk_ui::{
|
||||
NotificationClient as MatrixNotificationClient,
|
||||
NotificationProcessSetup as MatrixNotificationProcessSetup,
|
||||
},
|
||||
spaces::SpaceService as UISpaceService,
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
use mime::Mime;
|
||||
use oauth2::Scope;
|
||||
use ruma::{
|
||||
api::client::{alias::get_alias, error::ErrorKind, uiaa::UserIdentifier},
|
||||
api::client::{
|
||||
alias::get_alias,
|
||||
error::ErrorKind,
|
||||
profile::{AvatarUrl, DisplayName},
|
||||
uiaa::UserIdentifier,
|
||||
},
|
||||
events::{
|
||||
direct::DirectEventContent,
|
||||
fully_read::FullyReadEventContent,
|
||||
@@ -72,11 +81,11 @@ use ruma::{
|
||||
},
|
||||
tag::TagEventContent,
|
||||
GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
|
||||
GlobalAccountDataEventType as RumaGlobalAccountDataEventType,
|
||||
RoomAccountDataEvent as RumaRoomAccountDataEvent,
|
||||
},
|
||||
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
|
||||
OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
|
||||
room_version_rules::AuthorizationRules,
|
||||
OwnedDeviceId, OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
@@ -84,22 +93,26 @@ 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,
|
||||
qr_code::{HumanQrLoginError, QrCodeData, QrLoginProgressListener},
|
||||
room::{RoomHistoryVisibility, RoomInfoListener},
|
||||
room_directory_search::RoomDirectorySearch,
|
||||
room_info::RoomInfo,
|
||||
room_preview::RoomPreview,
|
||||
ruma::{
|
||||
AccountDataEvent, AccountDataEventType, AuthData, InviteAvatars, MediaPreviewConfig,
|
||||
MediaPreviews, MediaSource, RoomAccountDataEvent, RoomAccountDataEventType,
|
||||
},
|
||||
runtime::get_runtime_handle,
|
||||
spaces::SpaceService,
|
||||
sync_service::{SyncService, SyncServiceBuilder},
|
||||
task_handle::TaskHandle,
|
||||
utd::{UnableToDecryptDelegate, UtdHook},
|
||||
@@ -335,7 +348,24 @@ impl Client {
|
||||
}
|
||||
};
|
||||
|
||||
let supports_password_login = self.supports_password_login().await.ok().unwrap_or(false);
|
||||
let login_types = self.inner.matrix_auth().get_login_types().await.ok();
|
||||
let supports_password_login = login_types
|
||||
.as_ref()
|
||||
.map(|login_types| {
|
||||
login_types.flows.iter().any(|login_type| {
|
||||
matches!(login_type, get_login_types::v3::LoginType::Password(_))
|
||||
})
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let supports_sso_login = login_types
|
||||
.as_ref()
|
||||
.map(|login_types| {
|
||||
login_types
|
||||
.flows
|
||||
.iter()
|
||||
.any(|login_type| matches!(login_type, get_login_types::v3::LoginType::Sso(_)))
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let sliding_sync_version = self.sliding_sync_version();
|
||||
|
||||
Arc::new(HomeserverLoginDetails {
|
||||
@@ -343,6 +373,7 @@ impl Client {
|
||||
sliding_sync_version,
|
||||
supports_oidc_login,
|
||||
supported_oidc_prompts,
|
||||
supports_sso_login,
|
||||
supports_password_login,
|
||||
})
|
||||
}
|
||||
@@ -452,16 +483,39 @@ impl Client {
|
||||
/// 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
|
||||
///
|
||||
/// * `device_id` - The unique ID that will be associated with the session.
|
||||
/// If not set, a random one will be generated. It can be an existing
|
||||
/// device ID from a previous login call. Note that this should be done
|
||||
/// only if the client also holds the corresponding encryption keys.
|
||||
///
|
||||
/// * `additional_scopes` - Additional scopes to request from the
|
||||
/// authorization server, e.g. "urn:matrix:client:com.example.msc9999.foo".
|
||||
/// The scopes for API access and the device ID according to the
|
||||
/// [specification](https://spec.matrix.org/v1.15/client-server-api/#allocated-scope-tokens)
|
||||
/// are always requested.
|
||||
pub async fn url_for_oidc(
|
||||
&self,
|
||||
oidc_configuration: &OidcConfiguration,
|
||||
prompt: Option<OidcPrompt>,
|
||||
login_hint: Option<String>,
|
||||
device_id: Option<String>,
|
||||
additional_scopes: Option<Vec<String>>,
|
||||
) -> Result<Arc<OAuthAuthorizationData>, OidcError> {
|
||||
let registration_data = oidc_configuration.registration_data()?;
|
||||
let redirect_uri = oidc_configuration.redirect_uri()?;
|
||||
|
||||
let mut url_builder = self.inner.oauth().login(redirect_uri, None, Some(registration_data));
|
||||
let device_id = device_id.map(OwnedDeviceId::from);
|
||||
|
||||
let additional_scopes =
|
||||
additional_scopes.map(|scopes| scopes.into_iter().map(Scope::new).collect::<Vec<_>>());
|
||||
|
||||
let mut url_builder = self.inner.oauth().login(
|
||||
redirect_uri,
|
||||
device_id,
|
||||
Some(registration_data),
|
||||
additional_scopes,
|
||||
);
|
||||
|
||||
if let Some(prompt) = prompt {
|
||||
url_builder = url_builder.prompt(vec![prompt.into()]);
|
||||
@@ -490,30 +544,43 @@ 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()?;
|
||||
/// Log in using the provided [`QrCodeData`]. The `Client` must be built
|
||||
/// by providing [`QrCodeData::server_name`] as the server name for this
|
||||
/// login to succeed.
|
||||
///
|
||||
/// This method uses the login mechanism described in [MSC4108]. As such
|
||||
/// this method requires OAuth 2.0 support as well as sliding sync support.
|
||||
///
|
||||
/// The usage of the progress_listener is required to transfer the
|
||||
/// [`CheckCode`] to the existing client.
|
||||
///
|
||||
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
|
||||
pub async fn login_with_qr_code(
|
||||
self: Arc<Self>,
|
||||
qr_code_data: &QrCodeData,
|
||||
oidc_configuration: &OidcConfiguration,
|
||||
progress_listener: Box<dyn QrLoginProgressListener>,
|
||||
) -> Result<(), HumanQrLoginError> {
|
||||
let registration_data = oidc_configuration
|
||||
.registration_data()
|
||||
.map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
|
||||
|
||||
let handle = self
|
||||
.inner
|
||||
.media()
|
||||
.get_media_file(
|
||||
&MediaRequestParameters { source: source.media_source, format: MediaFormat::File },
|
||||
filename,
|
||||
&mime_type,
|
||||
use_cache,
|
||||
temp_dir,
|
||||
)
|
||||
.await?;
|
||||
let oauth = self.inner.oauth();
|
||||
let login = oauth.login_with_qr_code(&qr_code_data.inner, Some(®istration_data));
|
||||
|
||||
Ok(Arc::new(MediaFileHandle::new(handle)))
|
||||
let mut progress = login.subscribe_to_progress();
|
||||
|
||||
// We create this task, which will get cancelled once it's dropped, just in case
|
||||
// the progress stream doesn't end.
|
||||
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(state) = progress.next().await {
|
||||
progress_listener.on_update(state.into());
|
||||
}
|
||||
}));
|
||||
|
||||
login.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Restores the client from a `Session`.
|
||||
@@ -561,6 +628,12 @@ impl Client {
|
||||
self.inner.send_queue().set_enabled(enable).await;
|
||||
}
|
||||
|
||||
/// Enables or disables progress reporting for media uploads in the send
|
||||
/// queue.
|
||||
pub fn enable_send_queue_upload_progress(&self, enable: bool) {
|
||||
self.inner.send_queue().enable_upload_progress(enable);
|
||||
}
|
||||
|
||||
/// Subscribe to the global enablement status of the send queue, at the
|
||||
/// client-wide level.
|
||||
///
|
||||
@@ -716,32 +789,66 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows generic GET requests to be made through the SDKs internal HTTP
|
||||
/// client
|
||||
pub async fn get_url(&self, url: String) -> Result<String, ClientError> {
|
||||
let http_client = self.inner.http_client();
|
||||
Ok(http_client.get(url).send().await?.text().await?)
|
||||
/// Allows generic GET requests to be made through the SDK's internal HTTP
|
||||
/// client. This is useful when the caller's native HTTP client wouldn't
|
||||
/// have the same configuration (such as certificates, proxies, etc.) This
|
||||
/// method returns the raw bytes of the response, so that any kind of
|
||||
/// resource can be fetched including images, files, etc.
|
||||
///
|
||||
/// Note: When an HTTP error occurs, the error response can be found in the
|
||||
/// `ClientError::Generic`'s `details` field.
|
||||
pub async fn get_url(&self, url: String) -> Result<Vec<u8>, ClientError> {
|
||||
let response = self.inner.http_client().get(url).send().await?;
|
||||
if response.status().is_success() {
|
||||
Ok(response.bytes().await?.into())
|
||||
} else {
|
||||
Err(ClientError::Generic {
|
||||
msg: response.status().to_string(),
|
||||
details: response.text().await.ok(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// Whether or not the client's homeserver supports the password login flow.
|
||||
pub(crate) async fn supports_password_login(&self) -> anyhow::Result<bool> {
|
||||
let login_types = self.inner.matrix_auth().get_login_types().await?;
|
||||
let supports_password = login_types
|
||||
.flows
|
||||
.iter()
|
||||
.any(|login_type| matches!(login_type, get_login_types::v3::LoginType::Password(_)));
|
||||
Ok(supports_password)
|
||||
/// 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)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1133,15 +1240,8 @@ impl Client {
|
||||
}
|
||||
|
||||
pub async fn get_profile(&self, user_id: String) -> Result<UserProfile, ClientError> {
|
||||
let owned_user_id = UserId::parse(user_id.clone())?;
|
||||
|
||||
let response = self.inner.account().fetch_user_profile_of(&owned_user_id).await?;
|
||||
|
||||
Ok(UserProfile {
|
||||
user_id,
|
||||
display_name: response.displayname.clone(),
|
||||
avatar_url: response.avatar_url.as_ref().map(|url| url.to_string()),
|
||||
})
|
||||
let user_id = <&UserId>::try_from(user_id.as_str())?;
|
||||
UserProfile::fetch(&self.inner.account(), user_id).await
|
||||
}
|
||||
|
||||
pub async fn notification_client(
|
||||
@@ -1159,6 +1259,11 @@ impl Client {
|
||||
SyncServiceBuilder::new((*self.inner).clone(), self.utd_hook_manager.get().cloned())
|
||||
}
|
||||
|
||||
pub fn space_service(&self) -> Arc<SpaceService> {
|
||||
let inner = UISpaceService::new((*self.inner).clone());
|
||||
Arc::new(SpaceService::new(inner))
|
||||
}
|
||||
|
||||
pub async fn get_notification_settings(&self) -> Arc<NotificationSettings> {
|
||||
let inner = self.inner.notification_settings().await;
|
||||
|
||||
@@ -1172,13 +1277,10 @@ impl Client {
|
||||
// Ignored users
|
||||
|
||||
pub async fn ignored_users(&self) -> Result<Vec<String>, ClientError> {
|
||||
if let Some(raw_content) = self
|
||||
.inner
|
||||
.account()
|
||||
.fetch_account_data(RumaGlobalAccountDataEventType::IgnoredUserList)
|
||||
.await?
|
||||
if let Some(raw_content) =
|
||||
self.inner.account().fetch_account_data_static::<IgnoredUserListEventContent>().await?
|
||||
{
|
||||
let content = raw_content.deserialize_as::<IgnoredUserListEventContent>()?;
|
||||
let content = raw_content.deserialize()?;
|
||||
let user_ids: Vec<String> =
|
||||
content.ignored_users.keys().map(|id| id.to_string()).collect();
|
||||
|
||||
@@ -1419,9 +1521,16 @@ impl Client {
|
||||
/// Clear all the non-critical caches for this Client instance.
|
||||
///
|
||||
/// WARNING: This will clear all the caches, including the base store (state
|
||||
/// store), so callers must make sure that any sync is inactive before
|
||||
/// calling this method. In particular, the `SyncService` must not be
|
||||
/// running. After the method returns, the Client will be in an unstable
|
||||
/// 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.
|
||||
@@ -1431,8 +1540,23 @@ impl Client {
|
||||
/// 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> {
|
||||
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()
|
||||
@@ -1489,6 +1613,24 @@ impl Client {
|
||||
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(_))))
|
||||
}
|
||||
|
||||
/// Get server vendor information from the federation API.
|
||||
///
|
||||
/// This method retrieves information about the server's name and version
|
||||
/// by calling the `/_matrix/federation/v1/version` endpoint.
|
||||
pub async fn server_vendor_info(&self) -> Result<matrix_sdk::ServerVendorInfo, ClientError> {
|
||||
Ok(self.inner.server_vendor_info(None).await?)
|
||||
}
|
||||
|
||||
/// Subscribe to changes in the media preview configuration.
|
||||
pub async fn subscribe_to_media_preview_config(
|
||||
&self,
|
||||
@@ -1522,7 +1664,7 @@ impl Client {
|
||||
) -> 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())),
|
||||
Some(configuration) => Ok(configuration.media_previews.map(Into::into)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
@@ -1543,7 +1685,7 @@ impl Client {
|
||||
) -> 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())),
|
||||
Some(configuration) => Ok(configuration.invite_avatars.map(Into::into)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
@@ -1675,6 +1817,18 @@ pub struct UserProfile {
|
||||
pub avatar_url: Option<String>,
|
||||
}
|
||||
|
||||
impl UserProfile {
|
||||
/// Fetch the profile for the given user ID, using the given [`Account`]
|
||||
/// API.
|
||||
pub(crate) async fn fetch(account: &Account, user_id: &UserId) -> Result<Self, ClientError> {
|
||||
let response = account.fetch_user_profile_of(user_id).await?;
|
||||
let display_name = response.get_static::<DisplayName>()?;
|
||||
let avatar_url = response.get_static::<AvatarUrl>()?.map(|url| url.to_string());
|
||||
|
||||
Ok(UserProfile { user_id: user_id.to_string(), display_name, avatar_url })
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&search_users::v3::User> for UserProfile {
|
||||
fn from(value: &search_users::v3::User) -> Self {
|
||||
UserProfile {
|
||||
@@ -1789,7 +1943,7 @@ pub struct PowerLevels {
|
||||
|
||||
impl From<PowerLevels> for RoomPowerLevelsEventContent {
|
||||
fn from(value: PowerLevels) -> Self {
|
||||
let mut power_levels = RoomPowerLevelsEventContent::new();
|
||||
let mut power_levels = RoomPowerLevelsEventContent::new(&AuthorizationRules::V1);
|
||||
|
||||
if let Some(users_default) = value.users_default {
|
||||
power_levels.users_default = users_default.into();
|
||||
@@ -2153,17 +2307,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.
|
||||
@@ -2356,9 +2513,7 @@ impl TryFrom<AllowRule> for RumaAllowRule {
|
||||
match value {
|
||||
AllowRule::RoomMembership { room_id } => {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
Ok(Self::RoomMembership(ruma::events::room::join_rules::RoomMembership::new(
|
||||
room_id,
|
||||
)))
|
||||
Ok(Self::RoomMembership(ruma::room::RoomMembership::new(room_id)))
|
||||
}
|
||||
AllowRule::Custom { json } => Ok(Self::_Custom(Box::new(serde_json::from_str(&json)?))),
|
||||
}
|
||||
|
||||
@@ -1,33 +1,25 @@
|
||||
use std::{fs, num::NonZeroUsize, path::Path, sync::Arc, time::Duration};
|
||||
|
||||
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,
|
||||
},
|
||||
crypto::{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 matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::api::error::{DeserializationError, FromHttpResponseError};
|
||||
use tracing::{debug, error};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use super::client::Client;
|
||||
use crate::{
|
||||
authentication::OidcConfiguration, client::ClientSessionDelegate, error::ClientError,
|
||||
helpers::unwrap_or_clone_arc, runtime::get_runtime_handle, task_handle::TaskHandle,
|
||||
};
|
||||
use crate::{client::ClientSessionDelegate, error::ClientError, helpers::unwrap_or_clone_arc};
|
||||
|
||||
/// A list of bytes containing a certificate in DER or PEM form.
|
||||
pub type CertificateBytes = Vec<u8>;
|
||||
@@ -39,164 +31,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())
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum ClientBuildError {
|
||||
@@ -274,23 +108,36 @@ 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>,
|
||||
|
||||
#[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>>,
|
||||
|
||||
threading_support: ThreadingSupport,
|
||||
}
|
||||
|
||||
/// The timeout applies to each read operation, and resets after a successful
|
||||
/// read. This is more appropriate for detecting stalled connections when the
|
||||
/// size isn’t known beforehand.
|
||||
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl ClientBuilder {
|
||||
#[uniffi::constructor]
|
||||
@@ -321,9 +168,12 @@ 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(),
|
||||
threading_support: ThreadingSupport::Disabled,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -455,12 +305,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,
|
||||
@@ -470,43 +314,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,
|
||||
@@ -545,12 +358,12 @@ 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)
|
||||
}
|
||||
|
||||
@@ -574,6 +387,23 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Whether the client should support threads client-side or not, and enable
|
||||
/// experimental support for MSC4306 (threads subscriptions) or not.
|
||||
pub fn threads_enabled(
|
||||
self: Arc<Self>,
|
||||
enabled: bool,
|
||||
thread_subscriptions: bool,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
let support = if enabled {
|
||||
ThreadingSupport::Enabled { with_subscriptions: thread_subscriptions }
|
||||
} else {
|
||||
ThreadingSupport::Disabled
|
||||
};
|
||||
builder.threading_support = support;
|
||||
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();
|
||||
@@ -650,52 +480,55 @@ 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 {
|
||||
@@ -722,6 +555,7 @@ impl ClientBuilder {
|
||||
if let Some(timeout) = config.timeout {
|
||||
updated_config = updated_config.timeout(Duration::from_millis(timeout));
|
||||
}
|
||||
updated_config = updated_config.read_timeout(DEFAULT_READ_TIMEOUT);
|
||||
if let Some(max_concurrent_requests) = config.max_concurrent_requests {
|
||||
if max_concurrent_requests > 0 {
|
||||
updated_config = updated_config.max_concurrent_requests(NonZeroUsize::new(
|
||||
@@ -736,8 +570,25 @@ impl ClientBuilder {
|
||||
inner_builder = inner_builder.request_config(updated_config);
|
||||
}
|
||||
|
||||
inner_builder = inner_builder.with_threading_support(builder.threading_support);
|
||||
|
||||
let sdk_client = inner_builder.build().await?;
|
||||
|
||||
// Disable retries for this request to prevent it from being retried
|
||||
// indefinitely
|
||||
let config = sdk_client.request_config().disable_retry();
|
||||
|
||||
// Log server version information at info level.
|
||||
if let Ok(server_info) = sdk_client.server_vendor_info(Some(config)).await {
|
||||
tracing::info!(
|
||||
server_name = %server_info.server_name,
|
||||
version = %server_info.version,
|
||||
"Connected to Matrix server"
|
||||
);
|
||||
} else {
|
||||
tracing::warn!("Could not retrieve server version information");
|
||||
}
|
||||
|
||||
Ok(Arc::new(
|
||||
Client::new(
|
||||
sdk_client,
|
||||
@@ -748,59 +599,46 @@ impl ClientBuilder {
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Finish the building of the client and attempt to log in using the
|
||||
/// provided [`QrCodeData`].
|
||||
///
|
||||
/// This method will build the client and immediately attempt to log the
|
||||
/// client in using the provided [`QrCodeData`] using the login
|
||||
/// mechanism described in [MSC4108]. As such this methods requires OAuth
|
||||
/// 2.0 support as well as sliding sync support.
|
||||
///
|
||||
/// The usage of the progress_listener is required to transfer the
|
||||
/// [`CheckCode`] to the existing client.
|
||||
///
|
||||
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
|
||||
pub async fn build_with_qr_code(
|
||||
#[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>,
|
||||
qr_code_data: &QrCodeData,
|
||||
oidc_configuration: &OidcConfiguration,
|
||||
progress_listener: Box<dyn QrLoginProgressListener>,
|
||||
) -> Result<Arc<Client>, HumanQrLoginError> {
|
||||
let QrCodeModeData::Reciprocate { server_name } = &qr_code_data.inner.mode_data else {
|
||||
return Err(HumanQrLoginError::OtherDeviceNotSignedIn);
|
||||
};
|
||||
certificates: Vec<CertificateBytes>,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.additional_root_certificates = certificates;
|
||||
|
||||
let builder = self.server_name_or_homeserver_url(server_name.to_owned());
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
let client = builder.build().await.map_err(|e| match e {
|
||||
ClientBuildError::SlidingSync(_) => HumanQrLoginError::SlidingSyncNotAvailable,
|
||||
_ => {
|
||||
error!("Couldn't build the client {e:?}");
|
||||
HumanQrLoginError::Unknown
|
||||
}
|
||||
})?;
|
||||
/// 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)
|
||||
}
|
||||
|
||||
let registration_data = oidc_configuration
|
||||
.registration_data()
|
||||
.map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
|
||||
|
||||
let oauth = client.inner.oauth();
|
||||
let login = oauth.login_with_qr_code(&qr_code_data.inner, Some(®istration_data));
|
||||
|
||||
let mut progress = login.subscribe_to_progress();
|
||||
|
||||
// We create this task, which will get cancelled once it's dropped, just in case
|
||||
// the progress stream doesn't end.
|
||||
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(state) = progress.next().await {
|
||||
progress_listener.on_update(state.into());
|
||||
}
|
||||
}));
|
||||
|
||||
login.await?;
|
||||
|
||||
Ok(client)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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::from_err)
|
||||
}
|
||||
@@ -442,7 +442,7 @@ impl Encryption {
|
||||
Err(error) => {
|
||||
error!("Failed fetching identity from the store: {error}");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
info!("Requesting identity from the server.");
|
||||
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
use std::{collections::HashMap, error::Error, 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;
|
||||
|
||||
@@ -198,6 +205,12 @@ impl From<FocusEventError> for ClientError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RequestVerificationError> for ClientError {
|
||||
fn from(e: RequestVerificationError) -> Self {
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// Bindings version of the sdk type replacing OwnedUserId/DeviceIds with simple
|
||||
/// String.
|
||||
///
|
||||
@@ -749,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,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,16 +15,17 @@ 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 spaces;
|
||||
mod sync_service;
|
||||
mod task_handle;
|
||||
mod timeline;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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, OwnedEventId, OwnedRoomId, RoomId};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
client::{Client, JoinRule},
|
||||
@@ -31,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)]
|
||||
@@ -54,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() }
|
||||
}
|
||||
};
|
||||
@@ -74,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,
|
||||
@@ -87,9 +87,46 @@ 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.
|
||||
///
|
||||
@@ -113,55 +150,54 @@ impl NotificationClient {
|
||||
Ok(room)
|
||||
}
|
||||
|
||||
/// See also documentation of
|
||||
/// `MatrixNotificationClient::get_notification`.
|
||||
/// 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
|
||||
/// [`NotificationItem`] or no entry for it if it failed to fetch a
|
||||
/// notification for the provided [`EventId`].
|
||||
/// [`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, NotificationItem>, ClientError> {
|
||||
) -> 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 result = HashMap::new();
|
||||
|
||||
let mut batch_result = HashMap::new();
|
||||
for (key, value) in items.into_iter() {
|
||||
match value {
|
||||
Ok(item) => {
|
||||
result.insert(key.to_string(), NotificationItem::from_inner(item));
|
||||
}
|
||||
Err(error) => {
|
||||
// TODO This error should actually be returned so the clients can handle the
|
||||
// error as they see fit, but it's failing when creating
|
||||
// bindings for Go, i.e.
|
||||
// (https://github.com/NordSecurity/uniffi-bindgen-go/issues/62)
|
||||
error!("Could not fetch notification {key}, an error happened: {error}");
|
||||
}
|
||||
}
|
||||
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(result)
|
||||
|
||||
Ok(batch_result)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ impl TryFrom<SdkPushCondition> for PushCondition {
|
||||
Self::RoomMemberCount { prefix: is.prefix.into(), count: is.count.into() }
|
||||
}
|
||||
SdkPushCondition::SenderNotificationPermission { key } => {
|
||||
Self::SenderNotificationPermission { key }
|
||||
Self::SenderNotificationPermission { key: key.to_string() }
|
||||
}
|
||||
SdkPushCondition::EventPropertyIs { key, value } => {
|
||||
Self::EventPropertyIs { key, value: value.into() }
|
||||
@@ -197,7 +197,7 @@ impl From<PushCondition> for SdkPushCondition {
|
||||
},
|
||||
},
|
||||
PushCondition::SenderNotificationPermission { key } => {
|
||||
Self::SenderNotificationPermission { key }
|
||||
Self::SenderNotificationPermission { key: key.into() }
|
||||
}
|
||||
PushCondition::EventPropertyIs { key, value } => {
|
||||
Self::EventPropertyIs { key, value: value.into() }
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use std::sync::{atomic::AtomicBool, Arc, OnceLock};
|
||||
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;
|
||||
@@ -11,164 +14,158 @@ use tracing_subscriber::{
|
||||
time::FormatTime,
|
||||
FormatEvent, FormatFields, FormattedFields,
|
||||
},
|
||||
layer::SubscriberExt as _,
|
||||
layer::{Layered, SubscriberExt as _},
|
||||
registry::LookupSpan,
|
||||
reload::{self, Handle},
|
||||
util::SubscriberInitExt as _,
|
||||
Layer,
|
||||
EnvFilter, Layer, Registry,
|
||||
};
|
||||
|
||||
use crate::{error::ClientError, tracing::LogLevel};
|
||||
|
||||
fn text_layers<S>(config: TracingConfiguration) -> impl Layer<S>
|
||||
// Adjusted version of tracing_subscriber::fmt::Format
|
||||
struct EventFormatter {
|
||||
display_timestamp: bool,
|
||||
display_level: bool,
|
||||
}
|
||||
|
||||
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
|
||||
@@ -206,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.
|
||||
@@ -240,6 +271,7 @@ enum LogTarget {
|
||||
MatrixSdkBaseEventCache,
|
||||
MatrixSdkBaseSlidingSync,
|
||||
MatrixSdkBaseStoreAmbiguityMap,
|
||||
MatrixSdkBaseResponseProcessors,
|
||||
|
||||
// SDK common modules.
|
||||
MatrixSdkCommonStoreLocks,
|
||||
@@ -258,6 +290,7 @@ enum LogTarget {
|
||||
|
||||
// SDK UI modules.
|
||||
MatrixSdkUiTimeline,
|
||||
MatrixSdkUiNotificationClient,
|
||||
}
|
||||
|
||||
impl LogTarget {
|
||||
@@ -268,6 +301,7 @@ impl LogTarget {
|
||||
LogTarget::MatrixSdkBaseEventCache => "matrix_sdk_base::event_cache",
|
||||
LogTarget::MatrixSdkBaseSlidingSync => "matrix_sdk_base::sliding_sync",
|
||||
LogTarget::MatrixSdkBaseStoreAmbiguityMap => "matrix_sdk_base::store::ambiguity_map",
|
||||
LogTarget::MatrixSdkBaseResponseProcessors => "matrix_sdk_base::response_processors",
|
||||
LogTarget::MatrixSdkCommonStoreLocks => "matrix_sdk_common::store_locks",
|
||||
LogTarget::MatrixSdk => "matrix_sdk",
|
||||
LogTarget::MatrixSdkClient => "matrix_sdk::client",
|
||||
@@ -280,6 +314,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",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -302,6 +337,8 @@ const DEFAULT_TARGET_LOG_LEVELS: &[(LogTarget, LogLevel)] = &[
|
||||
(LogTarget::MatrixSdkEventCacheStore, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkCommonStoreLocks, LogLevel::Warn),
|
||||
(LogTarget::MatrixSdkBaseStoreAmbiguityMap, LogLevel::Warn),
|
||||
(LogTarget::MatrixSdkUiNotificationClient, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkBaseResponseProcessors, LogLevel::Debug),
|
||||
];
|
||||
|
||||
const IMMUTABLE_LOG_TARGETS: &[LogTarget] = &[
|
||||
@@ -322,6 +359,10 @@ pub enum TraceLogPacks {
|
||||
SendQueue,
|
||||
/// Enables all the logs relevant to the timeline.
|
||||
Timeline,
|
||||
/// Enables all the logs relevant to the notification client.
|
||||
NotificationClient,
|
||||
/// Enables all the logs relevant to sync profiling.
|
||||
SyncProfiling,
|
||||
}
|
||||
|
||||
impl TraceLogPacks {
|
||||
@@ -336,10 +377,18 @@ impl TraceLogPacks {
|
||||
],
|
||||
TraceLogPacks::SendQueue => &[LogTarget::MatrixSdkSendQueue],
|
||||
TraceLogPacks::Timeline => &[LogTarget::MatrixSdkUiTimeline],
|
||||
TraceLogPacks::NotificationClient => &[LogTarget::MatrixSdkUiNotificationClient],
|
||||
TraceLogPacks::SyncProfiling => &[
|
||||
LogTarget::MatrixSdkSlidingSync,
|
||||
LogTarget::MatrixSdkBaseSlidingSync,
|
||||
LogTarget::MatrixSdkBaseResponseProcessors,
|
||||
LogTarget::MatrixSdkCrypto,
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sentry")]
|
||||
struct SentryLoggingCtx {
|
||||
/// The Sentry client guard, which keeps the Sentry context alive.
|
||||
_guard: sentry::ClientInitGuard,
|
||||
@@ -349,6 +398,8 @@ struct SentryLoggingCtx {
|
||||
}
|
||||
|
||||
struct LoggingCtx {
|
||||
reload_handle: Option<ReloadHandle>,
|
||||
#[cfg(feature = "sentry")]
|
||||
sentry: Option<SentryLoggingCtx>,
|
||||
}
|
||||
|
||||
@@ -376,12 +427,14 @@ pub struct TracingConfiguration {
|
||||
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");
|
||||
@@ -389,73 +442,90 @@ impl TracingConfiguration {
|
||||
// Log panics.
|
||||
log_panics::init();
|
||||
|
||||
// Prepare the Sentry layer, if a DSN is provided.
|
||||
let (sentry_layer, sentry_logging_ctx) = if let Some(sentry_dsn) = self.sentry_dsn.take() {
|
||||
// Initialize the Sentry client with the given options.
|
||||
let sentry_guard = sentry::init((
|
||||
sentry_dsn,
|
||||
sentry::ClientOptions {
|
||||
traces_sample_rate: 0.0,
|
||||
attach_stacktrace: true,
|
||||
..sentry::ClientOptions::default()
|
||||
},
|
||||
));
|
||||
|
||||
let sentry_enabled = Arc::new(AtomicBool::new(true));
|
||||
|
||||
// Add a Sentry layer to the tracing subscriber.
|
||||
//
|
||||
// Pass custom event and span filters, which will ignore anything, if the Sentry
|
||||
// support has been globally disabled, or if the statement doesn't include a
|
||||
// `sentry` field set to `true`.
|
||||
let sentry_layer = sentry_tracing::layer()
|
||||
.event_filter({
|
||||
let enabled = sentry_enabled.clone();
|
||||
|
||||
move |metadata| {
|
||||
if enabled.load(std::sync::atomic::Ordering::SeqCst)
|
||||
&& metadata.fields().field("sentry").is_some()
|
||||
{
|
||||
sentry_tracing::default_event_filter(metadata)
|
||||
} else {
|
||||
// Ignore the event.
|
||||
sentry_tracing::EventFilter::Ignore
|
||||
}
|
||||
}
|
||||
})
|
||||
.span_filter({
|
||||
let enabled = sentry_enabled.clone();
|
||||
|
||||
move |metadata| {
|
||||
if enabled.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
sentry_tracing::default_span_filter(metadata)
|
||||
} else {
|
||||
// Ignore, if sentry is globally disabled.
|
||||
false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
(
|
||||
Some(sentry_layer),
|
||||
Some(SentryLoggingCtx { _guard: sentry_guard, enabled: sentry_enabled }),
|
||||
)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let env_filter = build_tracing_filter(&self);
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::EnvFilter::new(&env_filter))
|
||||
.with(crate::platform::text_layers(self))
|
||||
.with(sentry_layer)
|
||||
.init();
|
||||
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");
|
||||
|
||||
LoggingCtx { sentry: sentry_logging_ctx }
|
||||
logging_ctx
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,15 +575,22 @@ pub fn init_platform(
|
||||
config: TracingConfiguration,
|
||||
use_lightweight_tokio_runtime: bool,
|
||||
) -> Result<(), ClientError> {
|
||||
LOGGING.set(config.build()).map_err(|_| ClientError::Generic {
|
||||
msg: "logger already initialized".to_owned(),
|
||||
details: None,
|
||||
})?;
|
||||
#[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();
|
||||
if use_lightweight_tokio_runtime {
|
||||
setup_lightweight_tokio_runtime();
|
||||
} else {
|
||||
setup_multithreaded_tokio_runtime();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -522,6 +599,7 @@ pub fn init_platform(
|
||||
/// 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 {
|
||||
@@ -535,6 +613,37 @@ pub fn enable_sentry_logging(enabled: bool) {
|
||||
};
|
||||
}
|
||||
|
||||
/// 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");
|
||||
@@ -545,6 +654,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");
|
||||
@@ -576,6 +686,8 @@ fn setup_lightweight_tokio_runtime() {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use similar_asserts::assert_eq;
|
||||
|
||||
use super::build_tracing_filter;
|
||||
use crate::platform::TraceLogPacks;
|
||||
|
||||
@@ -587,6 +699,7 @@ 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,
|
||||
};
|
||||
|
||||
@@ -594,25 +707,31 @@ mod tests {
|
||||
|
||||
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,
|
||||
matrix_sdk_base::response_processors=debug,
|
||||
super_duper_app=error"#
|
||||
.split('\n')
|
||||
.map(|s| s.trim())
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -624,6 +743,7 @@ mod tests {
|
||||
extra_targets: vec!["super_duper_app".to_owned(), "some_other_span".to_owned()],
|
||||
write_to_stdout_or_system: true,
|
||||
write_to_files: None,
|
||||
#[cfg(feature = "sentry")]
|
||||
sentry_dsn: None,
|
||||
};
|
||||
|
||||
@@ -631,26 +751,32 @@ mod tests {
|
||||
|
||||
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,
|
||||
matrix_sdk_base::response_processors=trace,
|
||||
super_duper_app=trace,
|
||||
some_other_span=trace"#
|
||||
.split('\n')
|
||||
.map(|s| s.trim())
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -662,6 +788,7 @@ 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,
|
||||
};
|
||||
|
||||
@@ -687,6 +814,8 @@ 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,
|
||||
matrix_sdk_base::response_processors=debug,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,35 +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, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomAliasId,
|
||||
ServerName, UserId,
|
||||
};
|
||||
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},
|
||||
ruma::{ImageInfo, LocationContent},
|
||||
runtime::get_runtime_handle,
|
||||
timeline::{
|
||||
configuration::{TimelineConfiguration, TimelineFilter},
|
||||
EventTimelineItem, ReceiptType, SendHandle, Timeline,
|
||||
EventTimelineItem, LatestEventValue, ReceiptType, SendHandle, Timeline,
|
||||
},
|
||||
utils::{u64_to_uint, AsyncRuntimeDropped},
|
||||
TaskHandle,
|
||||
};
|
||||
|
||||
mod power_levels;
|
||||
pub mod room_info;
|
||||
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum Membership {
|
||||
Invited,
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -299,6 +304,10 @@ impl Room {
|
||||
self.inner.latest_event_item().await.map(Into::into)
|
||||
}
|
||||
|
||||
async fn new_latest_event(&self) -> LatestEventValue {
|
||||
self.inner.new_latest_event().await.into()
|
||||
}
|
||||
|
||||
pub async fn latest_encryption_state(&self) -> Result<EncryptionState, ClientError> {
|
||||
Ok(self.inner.latest_encryption_state().await?)
|
||||
}
|
||||
@@ -479,7 +488,7 @@ impl Room {
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the room is not found or on rate limit
|
||||
pub async fn report_room(&self, reason: Option<String>) -> Result<(), ClientError> {
|
||||
pub async fn report_room(&self, reason: String) -> Result<(), ClientError> {
|
||||
self.inner.report_room(reason).await?;
|
||||
|
||||
Ok(())
|
||||
@@ -570,21 +579,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,
|
||||
@@ -603,16 +597,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,
|
||||
@@ -622,37 +606,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()
|
||||
}
|
||||
@@ -717,9 +670,9 @@ impl Room {
|
||||
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(
|
||||
@@ -755,8 +708,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> {
|
||||
@@ -768,53 +724,6 @@ impl Room {
|
||||
Ok(self.inner.matrix_to_event_permalink(event_id).await?.to_string())
|
||||
}
|
||||
|
||||
/// This will only send a call notification event if appropriate.
|
||||
///
|
||||
/// This function is supposed to be called whenever the user creates a room
|
||||
/// call. It will send a `m.call.notify` event if:
|
||||
/// - there is not yet a running call.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// This is only supposed to be used in **custom** situations where the user
|
||||
/// explicitly chooses to send a `m.call.notify` event to invite/notify
|
||||
/// someone explicitly in unusual conditions. The default should be to
|
||||
/// use `send_call_notification_if_necessary` just before a new room call is
|
||||
/// created/joined.
|
||||
///
|
||||
/// One example could be that the UI allows to start a call with a subset of
|
||||
/// users of the room members first. And then later on the user can
|
||||
/// invite more users to the call.
|
||||
pub async fn send_call_notification(
|
||||
&self,
|
||||
call_id: String,
|
||||
application: RtcApplicationType,
|
||||
notify_type: NotifyType,
|
||||
mentions: Mentions,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner
|
||||
.send_call_notification(
|
||||
call_id,
|
||||
application.into(),
|
||||
notify_type.into(),
|
||||
mentions.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns whether the send queue for that particular room is enabled or
|
||||
/// not.
|
||||
pub fn is_send_queue_enabled(&self) -> bool {
|
||||
@@ -828,18 +737,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.
|
||||
@@ -1174,6 +1096,59 @@ impl Room {
|
||||
|
||||
Ok(Arc::new(RoomPreview::new(AsyncRuntimeDropped::new(client), room_preview)))
|
||||
}
|
||||
|
||||
/// Set a MSC4306 subscription to a thread in this room, based on the thread
|
||||
/// root event id.
|
||||
///
|
||||
/// If `subscribed` is `true`, it will subscribe to the thread, with a
|
||||
/// precision that the subscription was manually requested by the user
|
||||
/// (i.e. not automatic).
|
||||
///
|
||||
/// If the thread was already subscribed to (resp. unsubscribed from), while
|
||||
/// trying to subscribe to it (resp. unsubscribe from it), it will do
|
||||
/// nothing, i.e. subscribing (resp. unsubscribing) to a thread is an
|
||||
/// idempotent operation.
|
||||
pub async fn set_thread_subscription(
|
||||
&self,
|
||||
thread_root_event_id: String,
|
||||
subscribed: bool,
|
||||
) -> Result<(), ClientError> {
|
||||
let thread_root = EventId::parse(thread_root_event_id)?;
|
||||
if subscribed {
|
||||
// This is a manual subscription.
|
||||
let automatic = None;
|
||||
self.inner.subscribe_thread(thread_root, automatic).await?;
|
||||
} else {
|
||||
self.inner.unsubscribe_thread(thread_root).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return the current MSC4306 thread subscription for the given thread root
|
||||
/// in this room.
|
||||
///
|
||||
/// Returns `None` if the thread doesn't exist, or isn't subscribed to, or
|
||||
/// the server can't handle MSC4306; otherwise, returns the thread
|
||||
/// subscription status.
|
||||
pub async fn fetch_thread_subscription(
|
||||
&self,
|
||||
thread_root_event_id: String,
|
||||
) -> Result<Option<ThreadSubscription>, ClientError> {
|
||||
let thread_root = EventId::parse(thread_root_event_id)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.fetch_thread_subscription(thread_root)
|
||||
.await?
|
||||
.map(|sub| ThreadSubscription { automatic: sub.automatic }))
|
||||
}
|
||||
}
|
||||
|
||||
/// A thread subscription (MSC4306).
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct ThreadSubscription {
|
||||
/// Whether the thread subscription happened automatically (e.g. after a
|
||||
/// mention) or if it was manually requested by the user.
|
||||
automatic: bool,
|
||||
}
|
||||
|
||||
/// A listener for receiving new live location shares in a room.
|
||||
@@ -1271,54 +1246,6 @@ 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: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn call(&self, room_info: RoomInfo);
|
||||
@@ -1587,13 +1514,10 @@ impl From<SdkSuccessorRoom> for SuccessorRoom {
|
||||
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() }
|
||||
Self { room_id: value.room_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),
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
-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, SuccessorRoom},
|
||||
room::{
|
||||
power_levels::RoomPowerLevels, Membership, RoomHero, RoomHistoryVisibility, SuccessorRoom,
|
||||
},
|
||||
room_member::RoomMember,
|
||||
};
|
||||
|
||||
@@ -15,7 +17,7 @@ use crate::{
|
||||
pub struct RoomInfo {
|
||||
id: String,
|
||||
encryption_state: EncryptionState,
|
||||
creator: Option<String>,
|
||||
creators: Option<Vec<String>>,
|
||||
/// The room's name from the room state event if received from sync, or one
|
||||
/// that's been computed otherwise.
|
||||
display_name: Option<String>,
|
||||
@@ -24,7 +26,11 @@ 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,
|
||||
/// If present, it means the room has been archived/upgraded.
|
||||
successor_room: Option<SuccessorRoom>,
|
||||
@@ -42,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>,
|
||||
@@ -65,29 +70,46 @@ 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>>,
|
||||
/// This room's version.
|
||||
room_version: Option<String>,
|
||||
/// Whether creators are privileged over every other user (have infinite
|
||||
/// power level).
|
||||
privileged_creators_role: bool,
|
||||
}
|
||||
|
||||
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(),
|
||||
encryption_state: room.encryption_state(),
|
||||
creator: room.creator().as_ref().map(ToString::to_string),
|
||||
creators: room
|
||||
.creators()
|
||||
.map(|creators| creators.into_iter().map(Into::into).collect()),
|
||||
display_name: room.cached_display_name().map(|name| name.to_string()),
|
||||
raw_name: room.name(),
|
||||
topic: room.topic(),
|
||||
@@ -116,7 +138,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
|
||||
@@ -133,8 +154,15 @@ 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),
|
||||
room_version: room.version().map(|version| version.to_string()),
|
||||
privileged_creators_role: room
|
||||
.version()
|
||||
.and_then(|version| version.rules())
|
||||
.map(|rules| rules.authorization.explicitly_privilege_room_creators)
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -28,15 +28,21 @@ use crate::{error::ClientError, runtime::get_runtime_handle, task_handle::TaskHa
|
||||
pub enum PublicRoomJoinRule {
|
||||
Public,
|
||||
Knock,
|
||||
Restricted,
|
||||
KnockRestricted,
|
||||
Invite,
|
||||
}
|
||||
|
||||
impl TryFrom<ruma::directory::PublicRoomJoinRule> for PublicRoomJoinRule {
|
||||
impl TryFrom<ruma::room::JoinRuleKind> for PublicRoomJoinRule {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: ruma::directory::PublicRoomJoinRule) -> Result<Self, Self::Error> {
|
||||
fn try_from(value: ruma::room::JoinRuleKind) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
ruma::directory::PublicRoomJoinRule::Public => Ok(Self::Public),
|
||||
ruma::directory::PublicRoomJoinRule::Knock => Ok(Self::Knock),
|
||||
ruma::room::JoinRuleKind::Public => Ok(Self::Public),
|
||||
ruma::room::JoinRuleKind::Knock => Ok(Self::Knock),
|
||||
ruma::room::JoinRuleKind::Restricted => Ok(Self::Restricted),
|
||||
ruma::room::JoinRuleKind::KnockRestricted => Ok(Self::KnockRestricted),
|
||||
ruma::room::JoinRuleKind::Invite => Ok(Self::Invite),
|
||||
rule => Err(format!("unsupported join rule: {rule:?}")),
|
||||
}
|
||||
}
|
||||
@@ -149,11 +155,6 @@ impl RoomDirectorySearch {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomDirectorySearchEntriesResult {
|
||||
pub entries_stream: Arc<TaskHandle>,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RoomDirectorySearchEntryUpdate {
|
||||
Append { values: Vec<RoomDescription> },
|
||||
|
||||
@@ -16,8 +16,9 @@ use matrix_sdk_ui::{
|
||||
room_list_service::filters::{
|
||||
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,
|
||||
new_filter_joined, new_filter_low_priority, new_filter_non_left, new_filter_none,
|
||||
new_filter_normalized_match_room_name, new_filter_not, new_filter_space, new_filter_unread,
|
||||
BoxedFilterFn, RoomCategory,
|
||||
},
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
@@ -42,7 +43,10 @@ pub enum RoomListError {
|
||||
InvalidRoomId { 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 },
|
||||
}
|
||||
|
||||
@@ -118,7 +122,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| {
|
||||
@@ -126,7 +130,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(())
|
||||
}
|
||||
@@ -449,10 +455,15 @@ impl RoomListDynamicEntriesController {
|
||||
pub enum RoomListEntriesDynamicFilterKind {
|
||||
All { filters: Vec<RoomListEntriesDynamicFilterKind> },
|
||||
Any { filters: Vec<RoomListEntriesDynamicFilterKind> },
|
||||
NonSpace,
|
||||
NonLeft,
|
||||
// Not { filter: RoomListEntriesDynamicFilterKind } - requires recursive enum
|
||||
// support in uniffi https://github.com/mozilla/uniffi-rs/issues/396
|
||||
Joined,
|
||||
Unread,
|
||||
Favourite,
|
||||
LowPriority,
|
||||
NonLowPriority,
|
||||
Invite,
|
||||
Category { expect: RoomListFilterCategory },
|
||||
None,
|
||||
@@ -488,9 +499,12 @@ impl From<RoomListEntriesDynamicFilterKind> for BoxedFilterFn {
|
||||
filters.into_iter().map(|filter| BoxedFilterFn::from(filter)).collect(),
|
||||
)),
|
||||
Kind::NonLeft => Box::new(new_filter_non_left()),
|
||||
Kind::NonSpace => Box::new(new_filter_not(Box::new(new_filter_space()))),
|
||||
Kind::Joined => Box::new(new_filter_joined()),
|
||||
Kind::Unread => Box::new(new_filter_unread()),
|
||||
Kind::Favourite => Box::new(new_filter_favourite()),
|
||||
Kind::LowPriority => Box::new(new_filter_low_priority()),
|
||||
Kind::NonLowPriority => Box::new(new_filter_not(Box::new(new_filter_low_priority()))),
|
||||
Kind::Invite => Box::new(new_filter_invite()),
|
||||
Kind::Category { expect } => Box::new(new_filter_category(expect.into())),
|
||||
Kind::None => Box::new(new_filter_none()),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use matrix_sdk::room::{RoomMember as SdkRoomMember, RoomMemberRole};
|
||||
use ruma::UserId;
|
||||
use ruma::{events::room::power_levels::UserPowerLevel, UserId};
|
||||
|
||||
use crate::error::{ClientError, NotYetImplemented};
|
||||
|
||||
@@ -57,16 +57,25 @@ impl TryFrom<matrix_sdk::ruma::events::room::member::MembershipState> for Member
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the suggested role for the given power level.
|
||||
///
|
||||
/// Returns an error if the value of the power level is out of range for numbers
|
||||
/// accepted in canonical JSON.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn suggested_role_for_power_level(power_level: i64) -> RoomMemberRole {
|
||||
pub fn suggested_role_for_power_level(
|
||||
power_level: PowerLevel,
|
||||
) -> Result<RoomMemberRole, ClientError> {
|
||||
// It's not possible to expose the constructor on the Enum through Uniffi ☹️
|
||||
RoomMemberRole::suggested_role_for_power_level(power_level)
|
||||
Ok(RoomMemberRole::suggested_role_for_power_level(power_level.try_into()?))
|
||||
}
|
||||
|
||||
/// Get the suggested power level for the given role.
|
||||
///
|
||||
/// Returns an error if the value of the power level is unsupported.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn suggested_power_level_for_role(role: RoomMemberRole) -> i64 {
|
||||
pub fn suggested_power_level_for_role(role: RoomMemberRole) -> Result<PowerLevel, ClientError> {
|
||||
// It's not possible to expose methods on an Enum through Uniffi ☹️
|
||||
role.suggested_power_level()
|
||||
Ok(role.suggested_power_level().try_into()?)
|
||||
}
|
||||
|
||||
/// Generates a `matrix.to` permalink to the given userID.
|
||||
@@ -83,8 +92,8 @@ pub struct RoomMember {
|
||||
pub avatar_url: Option<String>,
|
||||
pub membership: MembershipState,
|
||||
pub is_name_ambiguous: bool,
|
||||
pub power_level: i64,
|
||||
pub normalized_power_level: i64,
|
||||
pub power_level: PowerLevel,
|
||||
pub normalized_power_level: PowerLevel,
|
||||
pub is_ignored: bool,
|
||||
pub suggested_role_for_power_level: RoomMemberRole,
|
||||
pub membership_change_reason: Option<String>,
|
||||
@@ -100,8 +109,8 @@ impl TryFrom<SdkRoomMember> for RoomMember {
|
||||
avatar_url: m.avatar_url().map(|a| a.to_string()),
|
||||
membership: m.membership().clone().try_into()?,
|
||||
is_name_ambiguous: m.name_ambiguous(),
|
||||
power_level: m.power_level(),
|
||||
normalized_power_level: m.normalized_power_level(),
|
||||
power_level: m.power_level().try_into()?,
|
||||
normalized_power_level: m.normalized_power_level().try_into()?,
|
||||
is_ignored: m.is_ignored(),
|
||||
suggested_role_for_power_level: m.suggested_role_for_power_level(),
|
||||
membership_change_reason: m.event().reason().map(|s| s.to_owned()),
|
||||
@@ -130,3 +139,42 @@ impl TryFrom<matrix_sdk::room::RoomMemberWithSenderInfo> for RoomMemberWithSende
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum PowerLevel {
|
||||
/// The user is a room creator and has infinite power level.
|
||||
///
|
||||
/// This power level was introduced in room version 12.
|
||||
Infinite,
|
||||
|
||||
/// The user has the given power level.
|
||||
Value { value: i64 },
|
||||
}
|
||||
|
||||
impl TryFrom<UserPowerLevel> for PowerLevel {
|
||||
type Error = NotYetImplemented;
|
||||
|
||||
fn try_from(value: UserPowerLevel) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
UserPowerLevel::Infinite => Ok(Self::Infinite),
|
||||
UserPowerLevel::Int(value) => Ok(Self::Value { value: value.into() }),
|
||||
_ => Err(NotYetImplemented),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<PowerLevel> for UserPowerLevel {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: PowerLevel) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
PowerLevel::Infinite => Self::Infinite,
|
||||
PowerLevel::Value { value } => {
|
||||
Self::Int(value.try_into().map_err(|err| ClientError::Generic {
|
||||
msg: "Power level is out of range".to_owned(),
|
||||
details: Some(format!("{err:?}")),
|
||||
})?)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use anyhow::Context as _;
|
||||
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, Client};
|
||||
use ruma::{room::RoomType as RumaRoomType, space::SpaceRoomJoinRule};
|
||||
use tracing::warn;
|
||||
use ruma::room::{JoinRuleSummary, RoomType as RumaRoomType};
|
||||
|
||||
use crate::{
|
||||
client::JoinRule,
|
||||
client::{AllowRule, JoinRule},
|
||||
error::ClientError,
|
||||
room::{Membership, RoomHero},
|
||||
room_member::{RoomMember, RoomMemberWithSenderInfo},
|
||||
@@ -22,9 +21,9 @@ pub struct RoomPreview {
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomPreview {
|
||||
/// Returns the room info the preview contains.
|
||||
pub fn info(&self) -> Result<RoomPreviewInfo, ClientError> {
|
||||
pub fn info(&self) -> RoomPreviewInfo {
|
||||
let info = &self.inner;
|
||||
Ok(RoomPreviewInfo {
|
||||
RoomPreviewInfo {
|
||||
room_id: info.room_id.to_string(),
|
||||
canonical_alias: info.canonical_alias.as_ref().map(|alias| alias.to_string()),
|
||||
name: info.name.clone(),
|
||||
@@ -32,20 +31,16 @@ impl RoomPreview {
|
||||
avatar_url: info.avatar_url.as_ref().map(|url| url.to_string()),
|
||||
num_joined_members: info.num_joined_members,
|
||||
num_active_members: info.num_active_members,
|
||||
room_type: info.room_type.as_ref().into(),
|
||||
room_type: info.room_type.clone().into(),
|
||||
is_history_world_readable: info.is_world_readable,
|
||||
membership: info.state.map(|state| state.into()),
|
||||
join_rule: info
|
||||
.join_rule
|
||||
.clone()
|
||||
.try_into()
|
||||
.map_err(|_| anyhow::anyhow!("unhandled SpaceRoomJoinRule kind"))?,
|
||||
join_rule: info.join_rule.clone().map(Into::into),
|
||||
is_direct: info.is_direct,
|
||||
heroes: info
|
||||
.heroes
|
||||
.as_ref()
|
||||
.map(|heroes| heroes.iter().map(|h| h.to_owned().into()).collect()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Leave the room if the room preview state is either joined, invited or
|
||||
@@ -114,30 +109,36 @@ 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 {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(join_rule: SpaceRoomJoinRule) -> Result<Self, ()> {
|
||||
Ok(match join_rule {
|
||||
SpaceRoomJoinRule::Invite => JoinRule::Invite,
|
||||
SpaceRoomJoinRule::Knock => JoinRule::Knock,
|
||||
SpaceRoomJoinRule::Private => JoinRule::Private,
|
||||
SpaceRoomJoinRule::Restricted => JoinRule::Restricted { rules: Vec::new() },
|
||||
SpaceRoomJoinRule::KnockRestricted => JoinRule::KnockRestricted { rules: Vec::new() },
|
||||
SpaceRoomJoinRule::Public => JoinRule::Public,
|
||||
SpaceRoomJoinRule::_Custom(_) => JoinRule::Custom { repr: join_rule.to_string() },
|
||||
_ => {
|
||||
warn!("unhandled SpaceRoomJoinRule: {join_rule}");
|
||||
return Err(());
|
||||
}
|
||||
})
|
||||
impl From<JoinRuleSummary> for JoinRule {
|
||||
fn from(join_rule: JoinRuleSummary) -> Self {
|
||||
match join_rule {
|
||||
JoinRuleSummary::Invite => JoinRule::Invite,
|
||||
JoinRuleSummary::Knock => JoinRule::Knock,
|
||||
JoinRuleSummary::Private => JoinRule::Private,
|
||||
JoinRuleSummary::Restricted(summary) => JoinRule::Restricted {
|
||||
rules: summary
|
||||
.allowed_room_ids
|
||||
.iter()
|
||||
.map(|room_id| AllowRule::RoomMembership { room_id: room_id.to_string() })
|
||||
.collect(),
|
||||
},
|
||||
JoinRuleSummary::KnockRestricted(summary) => JoinRule::KnockRestricted {
|
||||
rules: summary
|
||||
.allowed_room_ids
|
||||
.iter()
|
||||
.map(|room_id| AllowRule::RoomMembership { room_id: room_id.to_string() })
|
||||
.collect(),
|
||||
},
|
||||
JoinRuleSummary::Public => JoinRule::Public,
|
||||
_ => JoinRule::Custom { repr: join_rule.as_str().to_owned() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,8 +153,8 @@ pub enum RoomType {
|
||||
Custom { value: String },
|
||||
}
|
||||
|
||||
impl From<Option<&RumaRoomType>> for RoomType {
|
||||
fn from(value: Option<&RumaRoomType>) -> Self {
|
||||
impl From<Option<RumaRoomType>> for RoomType {
|
||||
fn from(value: Option<RumaRoomType>) -> Self {
|
||||
match value {
|
||||
Some(RumaRoomType::Space) => RoomType::Space,
|
||||
Some(RumaRoomType::_Custom(_)) => RoomType::Custom {
|
||||
|
||||
@@ -1486,17 +1486,17 @@ impl From<RumaSecretStorageV1AesHmacSha2Properties> for SecretStorageV1AesHmacSh
|
||||
#[derive(Clone, uniffi::Record, Default)]
|
||||
pub struct MediaPreviewConfig {
|
||||
/// The media previews setting for the user.
|
||||
pub media_previews: MediaPreviews,
|
||||
pub media_previews: Option<MediaPreviews>,
|
||||
|
||||
/// The invite avatars setting for the user.
|
||||
pub invite_avatars: InviteAvatars,
|
||||
pub invite_avatars: Option<InviteAvatars>,
|
||||
}
|
||||
|
||||
impl From<MediaPreviewConfigEventContent> for MediaPreviewConfig {
|
||||
fn from(value: MediaPreviewConfigEventContent) -> Self {
|
||||
Self {
|
||||
media_previews: value.media_previews.into(),
|
||||
invite_avatars: value.invite_avatars.into(),
|
||||
media_previews: value.media_previews.map(Into::into),
|
||||
invite_avatars: value.invite_avatars.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ mod sys {
|
||||
mod sys {
|
||||
use std::future::Future;
|
||||
|
||||
use crate::executor::{spawn, JoinHandle};
|
||||
use matrix_sdk_common::executor::{spawn, JoinHandle};
|
||||
|
||||
/// A dummy guard that does nothing when dropped.
|
||||
/// This is used for the Wasm implementation to match
|
||||
|
||||
@@ -116,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)
|
||||
}
|
||||
@@ -141,10 +138,7 @@ impl SessionVerificationController {
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -241,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;
|
||||
}
|
||||
}
|
||||
@@ -257,18 +254,14 @@ impl SessionVerificationController {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(sender_profile) = self.account.fetch_user_profile_of(sender).await else {
|
||||
let Ok(sender_profile) = UserProfile::fetch(&self.account, sender).await else {
|
||||
error!("Failed fetching user profile for verification request");
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(delegate) = &*self.delegate.read().unwrap() {
|
||||
delegate.did_receive_verification_request(SessionVerificationRequestDetails {
|
||||
sender_profile: UserProfile {
|
||||
user_id: request.other_user_id().to_string(),
|
||||
display_name: sender_profile.displayname,
|
||||
avatar_url: sender_profile.avatar_url.as_ref().map(|url| url.to_string()),
|
||||
},
|
||||
sender_profile,
|
||||
flow_id: request.flow_id().into(),
|
||||
device_id: other_device_data.device_id().into(),
|
||||
device_display_name: other_device_data.display_name().map(str::to_string),
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
// 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};
|
||||
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::spaces::{
|
||||
room_list::SpaceRoomListPaginationState, SpaceRoom as UISpaceRoom,
|
||||
SpaceRoomList as UISpaceRoomList, SpaceService as UISpaceService,
|
||||
};
|
||||
use ruma::RoomId;
|
||||
|
||||
use crate::{
|
||||
client::JoinRule,
|
||||
error::ClientError,
|
||||
room::{Membership, RoomHero},
|
||||
room_preview::RoomType,
|
||||
runtime::get_runtime_handle,
|
||||
TaskHandle,
|
||||
};
|
||||
|
||||
/// The main entry point into the Spaces facilities.
|
||||
///
|
||||
/// The spaces service is responsible for retrieving one's joined rooms,
|
||||
/// building a graph out of their `m.space.parent` and `m.space.child` state
|
||||
/// events, and providing access to the top-level spaces and their children.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SpaceService {
|
||||
inner: UISpaceService,
|
||||
}
|
||||
|
||||
impl SpaceService {
|
||||
/// Creates a new `SpaceService` instance.
|
||||
pub(crate) fn new(inner: UISpaceService) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SpaceService {
|
||||
/// Returns a list of all the top-level joined spaces. It will eagerly
|
||||
/// compute the latest version and also notify subscribers if there were
|
||||
/// any changes.
|
||||
pub async fn joined_spaces(&self) -> Vec<SpaceRoom> {
|
||||
self.inner.joined_spaces().await.into_iter().map(Into::into).collect()
|
||||
}
|
||||
|
||||
/// Subscribes to updates on the joined spaces list. If space rooms are
|
||||
/// joined or left, the stream will yield diffs that reflect the changes.
|
||||
pub async fn subscribe_to_joined_spaces(
|
||||
&self,
|
||||
listener: Box<dyn SpaceServiceJoinedSpacesListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let (initial_values, mut stream) = self.inner.subscribe_to_joined_spaces().await;
|
||||
|
||||
listener.on_update(vec![SpaceListUpdate::Reset {
|
||||
values: initial_values.into_iter().map(Into::into).collect(),
|
||||
}]);
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(diffs) = stream.next().await {
|
||||
listener.on_update(diffs.into_iter().map(Into::into).collect());
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Returns a `SpaceRoomList` for the given space ID.
|
||||
#[allow(clippy::unused_async)]
|
||||
// This method doesn't need to be async but if its not the FFI layer panics
|
||||
// with "there is no no reactor running, must be called from the context
|
||||
// of a Tokio 1.x runtime" error because the underlying constructor spawns
|
||||
// an async task.
|
||||
pub async fn space_room_list(
|
||||
&self,
|
||||
space_id: String,
|
||||
) -> Result<Arc<SpaceRoomList>, ClientError> {
|
||||
let space_id = RoomId::parse(space_id)?;
|
||||
Ok(Arc::new(SpaceRoomList::new(self.inner.space_room_list(space_id))))
|
||||
}
|
||||
}
|
||||
|
||||
/// The `SpaceRoomList`represents a paginated list of direct rooms
|
||||
/// that belong to a particular space.
|
||||
///
|
||||
/// It can be used to paginate through the list (and have live updates on the
|
||||
/// pagination state) as well as subscribe to changes as rooms are joined or
|
||||
/// left.
|
||||
///
|
||||
/// The `SpaceRoomList` also automatically subscribes to client room changes
|
||||
/// and updates the list accordingly as rooms are joined or left.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SpaceRoomList {
|
||||
inner: UISpaceRoomList,
|
||||
}
|
||||
|
||||
impl SpaceRoomList {
|
||||
/// Creates a new `SpaceRoomList` for the underlying UI crate room list.
|
||||
fn new(inner: UISpaceRoomList) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SpaceRoomList {
|
||||
/// Returns if the room list is currently paginating or not.
|
||||
pub fn pagination_state(&self) -> SpaceRoomListPaginationState {
|
||||
self.inner.pagination_state()
|
||||
}
|
||||
|
||||
/// Subscribe to pagination updates.
|
||||
pub fn subscribe_to_pagination_state_updates(
|
||||
&self,
|
||||
listener: Box<dyn SpaceRoomListPaginationStateListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let pagination_state = self.inner.subscribe_to_pagination_state_updates();
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(pagination_state);
|
||||
|
||||
while let Some(state) = pagination_state.next().await {
|
||||
listener.on_update(state);
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Return the current list of rooms.
|
||||
pub fn rooms(&self) -> Vec<SpaceRoom> {
|
||||
self.inner.rooms().into_iter().map(Into::into).collect()
|
||||
}
|
||||
|
||||
/// Subscribes to room list updates.
|
||||
pub fn subscribe_to_room_update(
|
||||
&self,
|
||||
listener: Box<dyn SpaceRoomListEntriesListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let (initial_values, mut stream) = self.inner.subscribe_to_room_updates();
|
||||
|
||||
listener.on_update(vec![SpaceListUpdate::Reset {
|
||||
values: initial_values.into_iter().map(Into::into).collect(),
|
||||
}]);
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(diffs) = stream.next().await {
|
||||
listener.on_update(diffs.into_iter().map(Into::into).collect());
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Ask the list to retrieve the next page if the end hasn't been reached
|
||||
/// yet. Otherwise it no-ops.
|
||||
pub async fn paginate(&self) -> Result<(), ClientError> {
|
||||
self.inner.paginate().await.map_err(ClientError::from)
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SpaceRoomListPaginationStateListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
fn on_update(&self, pagination_state: SpaceRoomListPaginationState);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SpaceRoomListEntriesListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
fn on_update(&self, rooms: Vec<SpaceListUpdate>);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SpaceServiceJoinedSpacesListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
fn on_update(&self, room_updates: Vec<SpaceListUpdate>);
|
||||
}
|
||||
|
||||
/// Structure representing a room in a space and aggregated information
|
||||
/// relevant to the UI layer.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct SpaceRoom {
|
||||
/// The ID of the room.
|
||||
pub room_id: String,
|
||||
/// The canonical alias of the room, if any.
|
||||
pub canonical_alias: Option<String>,
|
||||
/// The name of the room, if any.
|
||||
pub name: Option<String>,
|
||||
/// The topic of the room, if any.
|
||||
pub topic: Option<String>,
|
||||
/// The URL for the room's avatar, if one is set.
|
||||
pub avatar_url: Option<String>,
|
||||
/// The type of room from `m.room.create`, if any.
|
||||
pub room_type: RoomType,
|
||||
/// The number of members joined to the room.
|
||||
pub num_joined_members: u64,
|
||||
/// The join rule of the room.
|
||||
pub join_rule: Option<JoinRule>,
|
||||
/// Whether the room may be viewed by users without joining.
|
||||
pub world_readable: Option<bool>,
|
||||
/// Whether guest users may join the room and participate in it.
|
||||
pub guest_can_join: bool,
|
||||
|
||||
/// The number of children room this has, if a space.
|
||||
pub children_count: u64,
|
||||
/// Whether this room is joined, left etc.
|
||||
pub state: Option<Membership>,
|
||||
/// A list of room members considered to be heroes.
|
||||
pub heroes: Option<Vec<RoomHero>>,
|
||||
}
|
||||
|
||||
impl From<UISpaceRoom> for SpaceRoom {
|
||||
fn from(room: UISpaceRoom) -> Self {
|
||||
Self {
|
||||
room_id: room.room_id.into(),
|
||||
canonical_alias: room.canonical_alias.map(|alias| alias.into()),
|
||||
name: room.name,
|
||||
topic: room.topic,
|
||||
avatar_url: room.avatar_url.map(|url| url.into()),
|
||||
room_type: room.room_type.into(),
|
||||
num_joined_members: room.num_joined_members,
|
||||
join_rule: room.join_rule.map(Into::into),
|
||||
world_readable: room.world_readable,
|
||||
guest_can_join: room.guest_can_join,
|
||||
children_count: room.children_count,
|
||||
state: room.state.map(Into::into),
|
||||
heroes: room.heroes.map(|heroes| heroes.into_iter().map(Into::into).collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum SpaceListUpdate {
|
||||
Append { values: Vec<SpaceRoom> },
|
||||
Clear,
|
||||
PushFront { value: SpaceRoom },
|
||||
PushBack { value: SpaceRoom },
|
||||
PopFront,
|
||||
PopBack,
|
||||
Insert { index: u32, value: SpaceRoom },
|
||||
Set { index: u32, value: SpaceRoom },
|
||||
Remove { index: u32 },
|
||||
Truncate { length: u32 },
|
||||
Reset { values: Vec<SpaceRoom> },
|
||||
}
|
||||
|
||||
impl From<VectorDiff<UISpaceRoom>> for SpaceListUpdate {
|
||||
fn from(diff: VectorDiff<UISpaceRoom>) -> Self {
|
||||
match diff {
|
||||
VectorDiff::Append { values } => {
|
||||
Self::Append { values: values.into_iter().map(|v| v.into()).collect() }
|
||||
}
|
||||
VectorDiff::Clear => Self::Clear,
|
||||
VectorDiff::PushFront { value } => Self::PushFront { value: value.into() },
|
||||
VectorDiff::PushBack { value } => Self::PushBack { value: value.into() },
|
||||
VectorDiff::PopFront => Self::PopFront,
|
||||
VectorDiff::PopBack => Self::PopBack,
|
||||
VectorDiff::Insert { index, value } => {
|
||||
Self::Insert { index: index as u32, value: value.into() }
|
||||
}
|
||||
VectorDiff::Set { index, value } => {
|
||||
Self::Set { index: index as u32, value: value.into() }
|
||||
}
|
||||
VectorDiff::Remove { index } => Self::Remove { index: index as u32 },
|
||||
VectorDiff::Truncate { length } => Self::Truncate { length: length as u32 },
|
||||
VectorDiff::Reset { values } => {
|
||||
Self::Reset { values: values.into_iter().map(|v| v.into()).collect() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,15 @@ impl SyncService {
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Force expiring both sliding sync sessions.
|
||||
///
|
||||
/// This ensures that the sync service is stopped before expiring both
|
||||
/// sessions. It should be used sparingly, as it will cause a restart of
|
||||
/// the sessions on the server as well.
|
||||
pub async fn expire_sessions(&self) {
|
||||
self.inner.expire_sessions().await;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
@@ -119,6 +128,12 @@ impl SyncServiceBuilder {
|
||||
Arc::new(Self { builder, ..this })
|
||||
}
|
||||
|
||||
pub fn with_share_pos(self: Arc<Self>, enable: bool) -> Arc<Self> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
let builder = this.builder.with_share_pos(enable);
|
||||
Arc::new(Self { builder, ..this })
|
||||
}
|
||||
|
||||
pub async fn finish(self: Arc<Self>) -> Result<Arc<SyncService>, ClientError> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
Ok(Arc::new(SyncService {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -79,7 +79,6 @@ pub enum TimelineFocus {
|
||||
Thread {
|
||||
/// The thread root event ID to focus on.
|
||||
root_event_id: String,
|
||||
num_events: u16,
|
||||
},
|
||||
PinnedEvents {
|
||||
max_events_to_load: u16,
|
||||
@@ -108,7 +107,7 @@ impl TryFrom<TimelineFocus> for matrix_sdk_ui::timeline::TimelineFocus {
|
||||
hide_threaded_events,
|
||||
})
|
||||
}
|
||||
TimelineFocus::Thread { root_event_id, num_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(),
|
||||
@@ -116,7 +115,7 @@ impl TryFrom<TimelineFocus> for matrix_sdk_ui::timeline::TimelineFocus {
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(Self::Thread { root_event_id: parsed_root_event_id, num_events })
|
||||
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 })
|
||||
|
||||
@@ -15,24 +15,24 @@
|
||||
use std::{collections::HashMap, fmt::Write as _, fs, panic, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use as_variant::as_variant;
|
||||
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,
|
||||
BaseVideoInfo, Thumbnail,
|
||||
AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo, Thumbnail,
|
||||
},
|
||||
deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode},
|
||||
event_cache::RoomPaginationStatus,
|
||||
room::{
|
||||
edit::EditedContent as SdkEditedContent,
|
||||
reply::{EnforceThread, Reply},
|
||||
},
|
||||
room::edit::EditedContent as SdkEditedContent,
|
||||
};
|
||||
use matrix_sdk_common::{
|
||||
executor::{AbortHandle, JoinHandle},
|
||||
stream::StreamExt,
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{
|
||||
self, AttachmentSource, EventItemOrigin, Profile, TimelineDetails,
|
||||
TimelineUniqueId as SdkTimelineUniqueId,
|
||||
self, AttachmentConfig, AttachmentSource, EventItemOrigin,
|
||||
LatestEventValue as UiLatestEventValue, MediaUploadProgress as SdkMediaUploadProgress, Profile,
|
||||
TimelineDetails, TimelineUniqueId as SdkTimelineUniqueId,
|
||||
};
|
||||
use mime::Mime;
|
||||
use reply::{EmbeddedEventDetails, InReplyToDetails};
|
||||
@@ -47,29 +47,22 @@ use ruma::{
|
||||
UnstablePollStartContentBlock,
|
||||
},
|
||||
},
|
||||
receipt::ReceiptThread,
|
||||
room::message::{
|
||||
LocationMessageEventContent, MessageType, ReplyWithinThread,
|
||||
RoomMessageEventContentWithoutRelation,
|
||||
LocationMessageEventContent, MessageType, RoomMessageEventContentWithoutRelation,
|
||||
},
|
||||
AnyMessageLikeEventContent,
|
||||
},
|
||||
EventId, UInt,
|
||||
};
|
||||
use tokio::{
|
||||
sync::Mutex,
|
||||
task::{AbortHandle, JoinHandle},
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{error, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use self::content::TimelineItemContent;
|
||||
pub use self::msg_like::MessageContent;
|
||||
use crate::{
|
||||
client::ProgressWatcher,
|
||||
error::{ClientError, RoomError},
|
||||
event::EventOrTransactionId,
|
||||
helpers::unwrap_or_clone_arc,
|
||||
ruma::{
|
||||
AssetType, AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, PollKind,
|
||||
ThumbnailInfo, VideoInfo,
|
||||
@@ -105,45 +98,40 @@ impl Timeline {
|
||||
params: UploadParameters,
|
||||
attachment_info: AttachmentInfo,
|
||||
mime_type: Option<String>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
thumbnail: Option<Thumbnail>,
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let mime_str = mime_type.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let in_reply_to_event_id = params
|
||||
.in_reply_to
|
||||
.map(EventId::parse)
|
||||
.transpose()
|
||||
.map_err(|_| RoomError::InvalidRepliedToEventId)?;
|
||||
|
||||
let formatted_caption = formatted_body_from(
|
||||
params.caption.as_deref(),
|
||||
params.formatted_caption.map(Into::into),
|
||||
);
|
||||
|
||||
let attachment_config = AttachmentConfig::new()
|
||||
.thumbnail(thumbnail)
|
||||
.info(attachment_info)
|
||||
.caption(params.caption)
|
||||
.formatted_caption(formatted_caption)
|
||||
.mentions(params.mentions.map(Into::into))
|
||||
.reply(params.reply_params.map(|p| p.try_into()).transpose()?);
|
||||
let attachment_config = AttachmentConfig {
|
||||
info: Some(attachment_info),
|
||||
thumbnail,
|
||||
caption: params.caption,
|
||||
formatted_caption,
|
||||
mentions: params.mentions.map(Into::into),
|
||||
in_reply_to: in_reply_to_event_id,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let handle = SendAttachmentJoinHandle::new(get_runtime_handle().spawn(async move {
|
||||
let mut request =
|
||||
self.inner.send_attachment(params.source, mime_type, attachment_config);
|
||||
|
||||
if params.use_send_queue {
|
||||
request = request.use_send_queue();
|
||||
}
|
||||
|
||||
if let Some(progress_watcher) = progress_watcher {
|
||||
let mut subscriber = request.subscribe_to_send_progress();
|
||||
get_runtime_handle().spawn(async move {
|
||||
while let Some(progress) = subscriber.next().await {
|
||||
progress_watcher.transmission_progress(progress.into());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
request.await.map_err(|_| RoomError::FailedSendingAttachment)?;
|
||||
Ok(())
|
||||
self.inner
|
||||
.send_attachment(params.source, mime_type, attachment_config)
|
||||
.use_send_queue()
|
||||
.await
|
||||
.map_err(|_| RoomError::FailedSendingAttachment)
|
||||
}));
|
||||
|
||||
Ok(handle)
|
||||
@@ -151,15 +139,19 @@ impl Timeline {
|
||||
}
|
||||
|
||||
fn build_thumbnail_info(
|
||||
thumbnail_path: Option<String>,
|
||||
thumbnail_source: Option<UploadSource>,
|
||||
thumbnail_info: Option<ThumbnailInfo>,
|
||||
) -> Result<Option<Thumbnail>, RoomError> {
|
||||
match (thumbnail_path, thumbnail_info) {
|
||||
match (thumbnail_source, thumbnail_info) {
|
||||
(None, None) => Ok(None),
|
||||
|
||||
(Some(thumbnail_path), Some(thumbnail_info)) => {
|
||||
let thumbnail_data =
|
||||
fs::read(thumbnail_path).map_err(|_| RoomError::InvalidThumbnailData)?;
|
||||
(Some(thumbnail_source), Some(thumbnail_info)) => {
|
||||
let thumbnail_data = match thumbnail_source {
|
||||
UploadSource::File { filename } => {
|
||||
fs::read(filename).map_err(|_| RoomError::InvalidThumbnailData)?
|
||||
}
|
||||
UploadSource::Data { bytes, .. } => bytes,
|
||||
};
|
||||
|
||||
let height = thumbnail_info
|
||||
.height
|
||||
@@ -189,7 +181,7 @@ fn build_thumbnail_info(
|
||||
}
|
||||
|
||||
_ => {
|
||||
warn!("Ignoring thumbnail because either the thumbnail path or info isn't defined");
|
||||
warn!("Ignoring thumbnail because either the thumbnail source or info isn't defined");
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
@@ -205,16 +197,12 @@ pub struct UploadParameters {
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
/// Optional intentional mentions to be sent with the media.
|
||||
mentions: Option<Mentions>,
|
||||
/// Optional parameters for sending the media as (threaded) reply.
|
||||
reply_params: Option<ReplyParameters>,
|
||||
/// Should the media be sent with the send queue, or synchronously?
|
||||
///
|
||||
/// Watching progress only works with the synchronous method, at the moment.
|
||||
use_send_queue: bool,
|
||||
/// Optional Event ID to reply to.
|
||||
in_reply_to: Option<String>,
|
||||
}
|
||||
|
||||
/// A source for uploading a file
|
||||
#[derive(uniffi::Enum)]
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum UploadSource {
|
||||
/// Upload source is a file on disk
|
||||
File {
|
||||
@@ -239,34 +227,47 @@ impl From<UploadSource> for AttachmentSource {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct ReplyParameters {
|
||||
/// The ID of the event to reply to.
|
||||
event_id: String,
|
||||
/// Whether to enforce a thread relation.
|
||||
enforce_thread: bool,
|
||||
/// If enforcing a threaded relation, whether the message is a reply on a
|
||||
/// thread.
|
||||
reply_within_thread: bool,
|
||||
/// This type represents the progress of a media (consisting of a file and
|
||||
/// possibly a thumbnail) being uploaded.
|
||||
#[derive(Clone, Copy, uniffi::Record)]
|
||||
pub struct MediaUploadProgress {
|
||||
/// The index of the media within the transaction. A file and its
|
||||
/// thumbnail share the same index. Will always be 0 for non-gallery
|
||||
/// media uploads.
|
||||
pub index: u64,
|
||||
|
||||
/// The current combined upload progress for both the file and,
|
||||
/// if it exists, its thumbnail.
|
||||
pub progress: AbstractProgress,
|
||||
}
|
||||
|
||||
impl TryInto<Reply> for ReplyParameters {
|
||||
type Error = RoomError;
|
||||
impl From<SdkMediaUploadProgress> for MediaUploadProgress {
|
||||
fn from(value: SdkMediaUploadProgress) -> Self {
|
||||
Self { index: value.index, progress: value.progress.into() }
|
||||
}
|
||||
}
|
||||
|
||||
fn try_into(self) -> Result<Reply, Self::Error> {
|
||||
let event_id =
|
||||
EventId::parse(&self.event_id).map_err(|_| RoomError::InvalidRepliedToEventId)?;
|
||||
let enforce_thread = if self.enforce_thread {
|
||||
EnforceThread::Threaded(if self.reply_within_thread {
|
||||
ReplyWithinThread::Yes
|
||||
} else {
|
||||
ReplyWithinThread::No
|
||||
})
|
||||
} else {
|
||||
EnforceThread::MaybeThreaded
|
||||
};
|
||||
/// Progress of an operation in abstract units.
|
||||
///
|
||||
/// Contrary to [`TransmissionProgress`], this allows tracking the progress
|
||||
/// of sending or receiving a payload in estimated pseudo units representing a
|
||||
/// percentage. This is helpful in cases where the exact progress in bytes isn't
|
||||
/// known, for instance, because encryption (which changes the size) happens on
|
||||
/// the fly.
|
||||
#[derive(Clone, Copy, uniffi::Record)]
|
||||
pub struct AbstractProgress {
|
||||
/// How many units were already transferred.
|
||||
pub current: u64,
|
||||
/// How many units there are in total.
|
||||
pub total: u64,
|
||||
}
|
||||
|
||||
Ok(Reply { event_id, enforce_thread })
|
||||
impl From<matrix_sdk::send_queue::AbstractProgress> for AbstractProgress {
|
||||
fn from(value: matrix_sdk::send_queue::AbstractProgress) -> Self {
|
||||
Self {
|
||||
current: value.current.try_into().unwrap_or(u64::MAX),
|
||||
total: value.total.try_into().unwrap_or(u64::MAX),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,17 +282,14 @@ impl Timeline {
|
||||
// handled by the caller. See #3535 for details.
|
||||
|
||||
// First, pass all the items as a reset update.
|
||||
listener.on_update(vec![Arc::new(TimelineDiff::new(VectorDiff::Reset {
|
||||
values: timeline_items,
|
||||
}))]);
|
||||
listener.on_update(vec![TimelineDiff::new(VectorDiff::Reset { values: timeline_items })]);
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(timeline_stream);
|
||||
|
||||
// Then forward new items.
|
||||
while let Some(diffs) = timeline_stream.next().await {
|
||||
listener
|
||||
.on_update(diffs.into_iter().map(|d| Arc::new(TimelineDiff::new(d))).collect());
|
||||
listener.on_update(diffs.into_iter().map(TimelineDiff::new).collect());
|
||||
}
|
||||
})))
|
||||
}
|
||||
@@ -350,9 +348,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(())
|
||||
}
|
||||
|
||||
@@ -380,7 +376,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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -388,53 +384,38 @@ impl Timeline {
|
||||
pub fn send_image(
|
||||
self: Arc<Self>,
|
||||
params: UploadParameters,
|
||||
thumbnail_path: Option<String>,
|
||||
thumbnail_source: Option<UploadSource>,
|
||||
image_info: ImageInfo,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let attachment_info = AttachmentInfo::Image(
|
||||
BaseImageInfo::try_from(&image_info).map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
);
|
||||
let thumbnail = build_thumbnail_info(thumbnail_path, image_info.thumbnail_info)?;
|
||||
self.send_attachment(
|
||||
params,
|
||||
attachment_info,
|
||||
image_info.mimetype,
|
||||
progress_watcher,
|
||||
thumbnail,
|
||||
)
|
||||
let thumbnail = build_thumbnail_info(thumbnail_source, image_info.thumbnail_info)?;
|
||||
self.send_attachment(params, attachment_info, image_info.mimetype, thumbnail)
|
||||
}
|
||||
|
||||
pub fn send_video(
|
||||
self: Arc<Self>,
|
||||
params: UploadParameters,
|
||||
thumbnail_path: Option<String>,
|
||||
thumbnail_source: Option<UploadSource>,
|
||||
video_info: VideoInfo,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let attachment_info = AttachmentInfo::Video(
|
||||
BaseVideoInfo::try_from(&video_info).map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
);
|
||||
let thumbnail = build_thumbnail_info(thumbnail_path, video_info.thumbnail_info)?;
|
||||
self.send_attachment(
|
||||
params,
|
||||
attachment_info,
|
||||
video_info.mimetype,
|
||||
progress_watcher,
|
||||
thumbnail,
|
||||
)
|
||||
let thumbnail = build_thumbnail_info(thumbnail_source, video_info.thumbnail_info)?;
|
||||
self.send_attachment(params, attachment_info, video_info.mimetype, thumbnail)
|
||||
}
|
||||
|
||||
pub fn send_audio(
|
||||
self: Arc<Self>,
|
||||
params: UploadParameters,
|
||||
audio_info: AudioInfo,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let attachment_info = AttachmentInfo::Audio(
|
||||
BaseAudioInfo::try_from(&audio_info).map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
);
|
||||
self.send_attachment(params, attachment_info, audio_info.mimetype, progress_watcher, None)
|
||||
self.send_attachment(params, attachment_info, audio_info.mimetype, None)
|
||||
}
|
||||
|
||||
pub fn send_voice_message(
|
||||
@@ -442,26 +423,24 @@ impl Timeline {
|
||||
params: UploadParameters,
|
||||
audio_info: AudioInfo,
|
||||
waveform: Vec<u16>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let attachment_info = AttachmentInfo::Voice {
|
||||
audio_info: BaseAudioInfo::try_from(&audio_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
waveform: Some(waveform),
|
||||
};
|
||||
self.send_attachment(params, attachment_info, audio_info.mimetype, progress_watcher, None)
|
||||
self.send_attachment(params, attachment_info, audio_info.mimetype, None)
|
||||
}
|
||||
|
||||
pub fn send_file(
|
||||
self: Arc<Self>,
|
||||
params: UploadParameters,
|
||||
file_info: FileInfo,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let attachment_info = AttachmentInfo::File(
|
||||
BaseFileInfo::try_from(&file_info).map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
);
|
||||
self.send_attachment(params, attachment_info, file_info.mimetype, progress_watcher, None)
|
||||
self.send_attachment(params, attachment_info, file_info.mimetype, None)
|
||||
}
|
||||
|
||||
pub async fn create_poll(
|
||||
@@ -531,12 +510,10 @@ impl Timeline {
|
||||
pub async fn send_reply(
|
||||
&self,
|
||||
msg: Arc<RoomMessageEventContentWithoutRelation>,
|
||||
reply_params: ReplyParameters,
|
||||
event_id: String,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner
|
||||
.send_reply((*msg).clone(), reply_params.try_into()?)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!(err))?;
|
||||
let event_id = EventId::parse(&event_id).map_err(|_| RoomError::InvalidRepliedToEventId)?;
|
||||
self.inner.send_reply((*msg).clone(), event_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -566,7 +543,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(());
|
||||
}
|
||||
};
|
||||
@@ -587,7 +567,8 @@ impl Timeline {
|
||||
description: Option<String>,
|
||||
zoom_level: Option<u8>,
|
||||
asset_type: Option<AssetType>,
|
||||
) {
|
||||
replied_to_event_id: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let mut location_event_message_content =
|
||||
LocationMessageEventContent::new(body, geo_uri.clone());
|
||||
|
||||
@@ -604,8 +585,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(replied_to_event_id) = replied_to_event_id {
|
||||
self.send_reply(Arc::new(room_message_event_content), replied_to_event_id).await
|
||||
} else {
|
||||
self.send(Arc::new(room_message_event_content)).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle a reaction on an event.
|
||||
@@ -630,7 +616,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(())
|
||||
}
|
||||
|
||||
@@ -694,6 +683,8 @@ impl Timeline {
|
||||
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(),
|
||||
},
|
||||
))),
|
||||
|
||||
@@ -809,7 +800,7 @@ pub enum FocusEventError {
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait TimelineListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, diff: Vec<Arc<TimelineDiff>>);
|
||||
fn on_update(&self, diff: Vec<TimelineDiff>);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
@@ -817,7 +808,7 @@ pub trait PaginationStatusListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, status: RoomPaginationStatus);
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum TimelineDiff {
|
||||
Append { values: Vec<Arc<TimelineItem>> },
|
||||
Clear,
|
||||
@@ -825,10 +816,10 @@ pub enum TimelineDiff {
|
||||
PushBack { value: Arc<TimelineItem> },
|
||||
PopFront,
|
||||
PopBack,
|
||||
Insert { index: usize, value: Arc<TimelineItem> },
|
||||
Set { index: usize, value: Arc<TimelineItem> },
|
||||
Remove { index: usize },
|
||||
Truncate { length: usize },
|
||||
Insert { index: u32, value: Arc<TimelineItem> },
|
||||
Set { index: u32, value: Arc<TimelineItem> },
|
||||
Remove { index: u32 },
|
||||
Truncate { length: u32 },
|
||||
Reset { values: Vec<Arc<TimelineItem>> },
|
||||
}
|
||||
|
||||
@@ -839,14 +830,18 @@ impl TimelineDiff {
|
||||
Self::Append { values: values.into_iter().map(TimelineItem::from_arc).collect() }
|
||||
}
|
||||
VectorDiff::Clear => Self::Clear,
|
||||
VectorDiff::Insert { index, value } => {
|
||||
Self::Insert { index, value: TimelineItem::from_arc(value) }
|
||||
VectorDiff::Insert { index, value } => Self::Insert {
|
||||
index: u32::try_from(index).unwrap(),
|
||||
value: TimelineItem::from_arc(value),
|
||||
},
|
||||
VectorDiff::Set { index, value } => Self::Set {
|
||||
index: u32::try_from(index).unwrap(),
|
||||
value: TimelineItem::from_arc(value),
|
||||
},
|
||||
VectorDiff::Truncate { length } => {
|
||||
Self::Truncate { length: u32::try_from(length).unwrap() }
|
||||
}
|
||||
VectorDiff::Set { index, value } => {
|
||||
Self::Set { index, value: TimelineItem::from_arc(value) }
|
||||
}
|
||||
VectorDiff::Truncate { length } => Self::Truncate { length },
|
||||
VectorDiff::Remove { index } => Self::Remove { index },
|
||||
VectorDiff::Remove { index } => Self::Remove { index: u32::try_from(index).unwrap() },
|
||||
VectorDiff::PushBack { value } => {
|
||||
Self::PushBack { value: TimelineItem::from_arc(value) }
|
||||
}
|
||||
@@ -862,94 +857,6 @@ impl TimelineDiff {
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl TimelineDiff {
|
||||
pub fn change(&self) -> TimelineChange {
|
||||
match self {
|
||||
Self::Append { .. } => TimelineChange::Append,
|
||||
Self::Insert { .. } => TimelineChange::Insert,
|
||||
Self::Set { .. } => TimelineChange::Set,
|
||||
Self::Remove { .. } => TimelineChange::Remove,
|
||||
Self::PushBack { .. } => TimelineChange::PushBack,
|
||||
Self::PushFront { .. } => TimelineChange::PushFront,
|
||||
Self::PopBack => TimelineChange::PopBack,
|
||||
Self::PopFront => TimelineChange::PopFront,
|
||||
Self::Clear => TimelineChange::Clear,
|
||||
Self::Truncate { .. } => TimelineChange::Truncate,
|
||||
Self::Reset { .. } => TimelineChange::Reset,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append(self: Arc<Self>) -> Option<Vec<Arc<TimelineItem>>> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
as_variant!(this, Self::Append { values } => values)
|
||||
}
|
||||
|
||||
pub fn insert(self: Arc<Self>) -> Option<InsertData> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
as_variant!(this, Self::Insert { index, value } => {
|
||||
InsertData { index: index.try_into().unwrap(), item: value }
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set(self: Arc<Self>) -> Option<SetData> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
as_variant!(this, Self::Set { index, value } => {
|
||||
SetData { index: index.try_into().unwrap(), item: value }
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remove(&self) -> Option<u32> {
|
||||
as_variant!(self, Self::Remove { index } => (*index).try_into().unwrap())
|
||||
}
|
||||
|
||||
pub fn push_back(self: Arc<Self>) -> Option<Arc<TimelineItem>> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
as_variant!(this, Self::PushBack { value } => value)
|
||||
}
|
||||
|
||||
pub fn push_front(self: Arc<Self>) -> Option<Arc<TimelineItem>> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
as_variant!(this, Self::PushFront { value } => value)
|
||||
}
|
||||
|
||||
pub fn reset(self: Arc<Self>) -> Option<Vec<Arc<TimelineItem>>> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
as_variant!(this, Self::Reset { values } => values)
|
||||
}
|
||||
|
||||
pub fn truncate(&self) -> Option<u32> {
|
||||
as_variant!(self, Self::Truncate { length } => (*length).try_into().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct InsertData {
|
||||
pub index: u32,
|
||||
pub item: Arc<TimelineItem>,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct SetData {
|
||||
pub index: u32,
|
||||
pub item: Arc<TimelineItem>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, uniffi::Enum)]
|
||||
pub enum TimelineChange {
|
||||
Append,
|
||||
Clear,
|
||||
Insert,
|
||||
Set,
|
||||
Remove,
|
||||
PushBack,
|
||||
PushFront,
|
||||
PopBack,
|
||||
PopFront,
|
||||
Truncate,
|
||||
Reset,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct TimelineUniqueId {
|
||||
id: String,
|
||||
@@ -1009,7 +916,11 @@ impl TimelineItem {
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum EventSendState {
|
||||
/// The local event has not been sent yet.
|
||||
NotSentYet,
|
||||
NotSentYet {
|
||||
/// The progress of the sending operation, if the event involves a media
|
||||
/// upload.
|
||||
progress: Option<MediaUploadProgress>,
|
||||
},
|
||||
|
||||
/// The local event has been sent to the server, but unsuccessfully: The
|
||||
/// sending has failed.
|
||||
@@ -1034,7 +945,9 @@ impl From<&matrix_sdk_ui::timeline::EventSendState> for EventSendState {
|
||||
use matrix_sdk_ui::timeline::EventSendState::*;
|
||||
|
||||
match value {
|
||||
NotSentYet => Self::NotSentYet,
|
||||
NotSentYet { progress } => {
|
||||
Self::NotSentYet { progress: progress.clone().map(|p| p.into()) }
|
||||
}
|
||||
SendingFailed { error, is_recoverable } => {
|
||||
let as_queue_wedge_error: matrix_sdk::QueueWedgeError = (&**error).into();
|
||||
Self::SendingFailed {
|
||||
@@ -1229,7 +1142,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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1369,29 +1285,66 @@ impl LazyTimelineItemProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/// Mimic the [`UiLatestEventValue`] type.
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum LatestEventValue {
|
||||
None,
|
||||
Remote {
|
||||
timestamp: Timestamp,
|
||||
sender: String,
|
||||
is_own: bool,
|
||||
profile: ProfileDetails,
|
||||
content: TimelineItemContent,
|
||||
},
|
||||
Local {
|
||||
timestamp: Timestamp,
|
||||
content: TimelineItemContent,
|
||||
is_sending: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<UiLatestEventValue> for LatestEventValue {
|
||||
fn from(value: UiLatestEventValue) -> Self {
|
||||
match value {
|
||||
UiLatestEventValue::None => Self::None,
|
||||
UiLatestEventValue::Remote { timestamp, sender, is_own, profile, content } => {
|
||||
Self::Remote {
|
||||
timestamp: timestamp.into(),
|
||||
sender: sender.to_string(),
|
||||
is_own,
|
||||
profile: profile.into(),
|
||||
content: content.into(),
|
||||
}
|
||||
}
|
||||
UiLatestEventValue::Local { timestamp, content, is_sending } => {
|
||||
Self::Local { timestamp: timestamp.into(), content: content.into(), is_sending }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "unstable-msc4274")]
|
||||
mod galleries {
|
||||
use std::{panic, sync::Arc};
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use matrix_sdk::{
|
||||
attachment::{
|
||||
AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo, Thumbnail,
|
||||
},
|
||||
utils::formatted_body_from,
|
||||
};
|
||||
use matrix_sdk_common::executor::{AbortHandle, JoinHandle};
|
||||
use matrix_sdk_ui::timeline::GalleryConfig;
|
||||
use mime::Mime;
|
||||
use tokio::{
|
||||
sync::Mutex,
|
||||
task::{AbortHandle, JoinHandle},
|
||||
};
|
||||
use ruma::EventId;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
error::RoomError,
|
||||
ruma::{AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, VideoInfo},
|
||||
timeline::{build_thumbnail_info, ReplyParameters, Timeline},
|
||||
runtime::get_runtime_handle,
|
||||
timeline::{build_thumbnail_info, Timeline, UploadSource},
|
||||
};
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
@@ -1402,37 +1355,37 @@ mod galleries {
|
||||
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>,
|
||||
/// Optional Event ID to reply to.
|
||||
in_reply_to: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum GalleryItemInfo {
|
||||
Audio {
|
||||
audio_info: AudioInfo,
|
||||
filename: String,
|
||||
source: UploadSource,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
},
|
||||
File {
|
||||
file_info: FileInfo,
|
||||
filename: String,
|
||||
source: UploadSource,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
},
|
||||
Image {
|
||||
image_info: ImageInfo,
|
||||
filename: String,
|
||||
source: UploadSource,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
thumbnail_path: Option<String>,
|
||||
thumbnail_source: Option<UploadSource>,
|
||||
},
|
||||
Video {
|
||||
video_info: VideoInfo,
|
||||
filename: String,
|
||||
source: UploadSource,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
thumbnail_path: Option<String>,
|
||||
thumbnail_source: Option<UploadSource>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1446,12 +1399,12 @@ mod galleries {
|
||||
}
|
||||
}
|
||||
|
||||
fn filename(&self) -> &String {
|
||||
fn source(&self) -> &UploadSource {
|
||||
match self {
|
||||
GalleryItemInfo::Audio { filename, .. } => filename,
|
||||
GalleryItemInfo::File { filename, .. } => filename,
|
||||
GalleryItemInfo::Image { filename, .. } => filename,
|
||||
GalleryItemInfo::Video { filename, .. } => filename,
|
||||
GalleryItemInfo::File { source, .. } => source,
|
||||
GalleryItemInfo::Audio { source, .. } => source,
|
||||
GalleryItemInfo::Image { source, .. } => source,
|
||||
GalleryItemInfo::Video { source, .. } => source,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1497,11 +1450,17 @@ mod galleries {
|
||||
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::Image { image_info, thumbnail_source, .. } => {
|
||||
build_thumbnail_info(
|
||||
thumbnail_source.as_ref().cloned(),
|
||||
image_info.thumbnail_info.clone(),
|
||||
)
|
||||
}
|
||||
GalleryItemInfo::Video { video_info, thumbnail_path, .. } => {
|
||||
build_thumbnail_info(thumbnail_path.clone(), video_info.thumbnail_info.clone())
|
||||
GalleryItemInfo::Video { video_info, thumbnail_source, .. } => {
|
||||
build_thumbnail_info(
|
||||
thumbnail_source.as_ref().cloned(),
|
||||
video_info.thumbnail_info.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1517,7 +1476,7 @@ mod galleries {
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
Ok(matrix_sdk_ui::timeline::GalleryItemInfo {
|
||||
source: self.filename().into(),
|
||||
source: self.source().clone().into(),
|
||||
content_type: mime_type,
|
||||
attachment_info: self.attachment_info()?,
|
||||
caption: self.caption().clone(),
|
||||
@@ -1560,7 +1519,10 @@ mod galleries {
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1585,11 +1547,18 @@ mod galleries {
|
||||
params.formatted_caption.map(Into::into),
|
||||
);
|
||||
|
||||
let in_reply_to = params
|
||||
.in_reply_to
|
||||
.as_ref()
|
||||
.map(EventId::parse)
|
||||
.transpose()
|
||||
.map_err(|_| RoomError::InvalidRepliedToEventId)?;
|
||||
|
||||
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()?);
|
||||
.in_reply_to(in_reply_to);
|
||||
|
||||
for item_info in item_infos {
|
||||
gallery_config = gallery_config.add_item(item_info.try_into()?);
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use matrix_sdk::crypto::types::events::UtdCause;
|
||||
use ruma::events::{room::MediaSource as RumaMediaSource, EventContent};
|
||||
use ruma::events::{room::MediaSource as RumaMediaSource, MessageLikeEventContent};
|
||||
|
||||
use super::{
|
||||
content::Reaction,
|
||||
@@ -241,7 +241,7 @@ pub struct PollAnswer {
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct ThreadSummary {
|
||||
pub latest_event: EmbeddedEventDetails,
|
||||
pub num_replies: usize,
|
||||
pub num_replies: u32,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
@@ -249,6 +249,10 @@ 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 {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
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 {
|
||||
@@ -50,8 +51,16 @@ impl From<matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
|
||||
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 {
|
||||
@@ -63,6 +72,8 @@ impl From<TimelineDetails<Box<EmbeddedEvent>>> for EmbeddedEventDetails {
|
||||
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.
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use language_tags::LanguageTag;
|
||||
use matrix_sdk::{
|
||||
async_trait,
|
||||
widget::{MessageLikeEventFilter, StateEventFilter, ToDeviceEventFilter},
|
||||
};
|
||||
use matrix_sdk::widget::{MessageLikeEventFilter, StateEventFilter, ToDeviceEventFilter};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::events::MessageLikeEventType;
|
||||
use tracing::error;
|
||||
@@ -113,174 +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>,
|
||||
//// - `true`: The webview should show the list of media devices it detects using
|
||||
//// `enumerateDevices`.
|
||||
/// - `false`: the webview shows a a list of devices injected by the
|
||||
/// client. (used on ios & android)
|
||||
pub controlled_media_devices: bool,
|
||||
}
|
||||
|
||||
impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElementCallWidgetOptions {
|
||||
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,
|
||||
controlled_media_devices: value.controlled_media_devices,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `WidgetSettings` are usually created from a state event.
|
||||
/// (currently unimplemented)
|
||||
///
|
||||
@@ -296,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())?)
|
||||
}
|
||||
|
||||
@@ -352,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.
|
||||
@@ -366,6 +197,15 @@ pub fn get_element_call_required_permissions(
|
||||
.chain(read_send.clone())
|
||||
.collect(),
|
||||
send: vec![
|
||||
// To notify other users that a call has started.
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "org.matrix.msc4075.rtc.notification".to_owned(),
|
||||
},
|
||||
// Also for call notifications, except this is the deprecated fallback type which
|
||||
// Element Call still sends.
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: MessageLikeEventType::CallNotify.to_string(),
|
||||
},
|
||||
// To send the call participation state event (main MatrixRTC event).
|
||||
// This is required for legacy state events (using only one event for all devices with
|
||||
// a membership array). TODO: remove once legacy call member events are
|
||||
@@ -380,6 +220,12 @@ pub fn get_element_call_required_permissions(
|
||||
event_type: StateEventType::CallMember.to_string(),
|
||||
state_key: format!("{own_user_id}_{own_device_id}"),
|
||||
},
|
||||
// Same as above for [MSC3779] and [MSC4143](https://github.com/matrix-org/matrix-spec-proposals/pull/4143),
|
||||
// with application suffix
|
||||
WidgetEventFilter::StateWithTypeAndStateKey {
|
||||
event_type: StateEventType::CallMember.to_string(),
|
||||
state_key: format!("{own_user_id}_{own_device_id}_m.call"),
|
||||
},
|
||||
// The same as above but with an underscore.
|
||||
// To work around the issue that state events starting with `@` have to be Matrix id's
|
||||
// but we use mxId+deviceId.
|
||||
@@ -387,6 +233,11 @@ pub fn get_element_call_required_permissions(
|
||||
event_type: StateEventType::CallMember.to_string(),
|
||||
state_key: format!("_{own_user_id}_{own_device_id}"),
|
||||
},
|
||||
// Same as above for [MSC4143], with application suffix
|
||||
WidgetEventFilter::StateWithTypeAndStateKey {
|
||||
event_type: StateEventType::CallMember.to_string(),
|
||||
state_key: format!("_{own_user_id}_{own_device_id}_m.call"),
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.chain(read_send)
|
||||
@@ -553,8 +404,6 @@ pub trait WidgetCapabilitiesProvider: SendOutsideWasm + SyncOutsideWasm {
|
||||
|
||||
struct CapabilitiesProviderWrap(Arc<dyn WidgetCapabilitiesProvider>);
|
||||
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
impl matrix_sdk::widget::CapabilitiesProvider for CapabilitiesProviderWrap {
|
||||
async fn acquire_capabilities(
|
||||
&self,
|
||||
@@ -656,14 +505,27 @@ 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_m.call",
|
||||
);
|
||||
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_m.call",
|
||||
);
|
||||
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 |
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"rust-analyzer.checkOnSave.command": "clippy",
|
||||
"rust-analyzer.checkOnSave.command": "check",
|
||||
"rust-analyzer.rustfmt.extraArgs": ["+nightly"]
|
||||
}
|
||||
|
||||
@@ -6,6 +6,98 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
## [0.14.0] - 2025-09-04
|
||||
|
||||
### Features
|
||||
- Add `SyncResponse::RoomUpdates::is_empty` to check if there were any room updates.
|
||||
([#5593](https://github.com/matrix-org/matrix-rust-sdk/pull/5593))
|
||||
- Add `EncryptionState::StateEncrypted` to represent rooms supporting encrypted
|
||||
state events. Feature-gated behind `experimental-encrypted-state-events`.
|
||||
([#5523](https://github.com/matrix-org/matrix-rust-sdk/pull/5523))
|
||||
- [**breaking**] The `state` field of `JoinedRoomUpdate` and `LeftRoomUpdate`
|
||||
now uses the `State` enum, depending on whether the state changes were
|
||||
received in the `state` field or the `state_after` field.
|
||||
([#5488](https://github.com/matrix-org/matrix-rust-sdk/pull/5488))
|
||||
- [**breaking**] `RoomCreateWithCreatorEventContent` has a new field
|
||||
`additional_creators` that allows to specify additional room creators beside
|
||||
the user sending the `m.room.create` event, introduced with room version 12.
|
||||
([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436))
|
||||
- [**breaking**] The `RoomInfo` method now remembers the inviter at the time
|
||||
when the `BaseClient::room_joined()` method was called. The caller is
|
||||
responsible to remember the inviter before a server request to join the room
|
||||
is made. The `RoomInfo::invite_accepted_at` method was removed, the
|
||||
`RoomInfo::invite_details` method returns both the timestamp and the
|
||||
inviter.
|
||||
([#5390](https://github.com/matrix-org/matrix-rust-sdk/pull/5390))
|
||||
|
||||
### Refactor
|
||||
- [**breaking**] The `Stripped` variants of `RawAnySyncOrStrippedTimelineEvent`,
|
||||
`RawAnySyncOrStrippedState` and `AnySyncOrStrippedState` use `StrippedState`
|
||||
instead of `AnyStrippedStateEvent`.
|
||||
([#5473](https://github.com/matrix-org/matrix-rust-sdk/pull/5473))
|
||||
- [**breaking**] The `stripped_state` field of `StateChanges` uses
|
||||
`StrippedState` instead of `AnyStrippedStateEvent`.
|
||||
([#5473](https://github.com/matrix-org/matrix-rust-sdk/pull/5473))
|
||||
- [**breaking**] `RelationalLinkedChunk::items` now takes a `RoomId` instead of an
|
||||
`&OwnedLinkedChunkId` parameter.
|
||||
([#5445](https://github.com/matrix-org/matrix-rust-sdk/pull/5445))
|
||||
- [**breaking**] Add an `IsPrefix = False` bound to the
|
||||
`get_state_event_static()`, `get_state_event_static_for_key()` and
|
||||
`get_state_events_static()`, `get_account_data_event_static()` and
|
||||
`get_room_account_data_event_static` methods of `StateStoreExt`. These methods
|
||||
only worked for events where the full event type is statically-known, and this
|
||||
is now enforced at compile-time. The matching non-`static` methods of
|
||||
`StateStore` can be used instead for event types with a variable suffix.
|
||||
([#5444](https://github.com/matrix-org/matrix-rust-sdk/pull/5444))
|
||||
- [**breaking**] `SyncOrStrippedState<RoomPowerLevelsEventContent>::power_levels()`
|
||||
takes `AuthorizationRules` and a list of creators, because creators can have
|
||||
infinite power levels, as introduced in room version 12.
|
||||
([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436))
|
||||
- [**breaking**] `RoomMember::power_level()` and
|
||||
`RoomMember::normalized_power_level()` now use `UserPowerLevel` to represent
|
||||
power levels instead of `i64` to differentiate the infinite power level of
|
||||
creators, as introduced in room version 12.
|
||||
([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436))
|
||||
- [**breaking**] The `creator()` methods of `Room` and `RoomInfo` have been
|
||||
renamed to `creators()` and can now return a list of user IDs, to reflect that
|
||||
a room can have several creators, as introduced in room version 12.
|
||||
([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436))
|
||||
- [**breaking**] `RoomInfo::room_version_or_default()` was replaced with
|
||||
`room_version_rules_or_default()`. The room version should only be used for
|
||||
display purposes. The rules contain flags for all the differences in behavior
|
||||
between all known room versions.
|
||||
([#5337](https://github.com/matrix-org/matrix-rust-sdk/pull/5337))
|
||||
- [**breaking**] `MinimalStateEvent::redact()` takes `RedactionRules` instead of
|
||||
a `RoomVersionId`.
|
||||
([#5337](https://github.com/matrix-org/matrix-rust-sdk/pull/5337))
|
||||
- [**breaking**] The `event_id` field of `PredecessorRoom` was removed, due to
|
||||
its removal in the Matrix specification with MSC4291.
|
||||
([#5419](https://github.com/matrix-org/matrix-rust-sdk/pull/5419))
|
||||
|
||||
## [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.
|
||||
@@ -26,8 +118,8 @@ No notable changes in this release.
|
||||
- `EventCacheStoreMedia` has a new method `last_media_cleanup_time_inner`
|
||||
- There are new `'static` bounds in `MediaService` for the media cache stores
|
||||
- `event_cache::store::MemoryStore` implements `Clone`.
|
||||
- `BaseClient` now has a `handle_verification_events` field which is `true` by
|
||||
default and can be negated so the `NotificationClient` won't handle received
|
||||
- `BaseClient` now has a `handle_verification_events` field which is `true` by
|
||||
default and can be negated so the `NotificationClient` won't handle received
|
||||
verification events too, causing errors in the `VerificationMachine`.
|
||||
- [**breaking**] `Room::is_encryption_state_synced` has been removed
|
||||
([#4777](https://github.com/matrix-org/matrix-rust-sdk/pull/4777))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk>"]
|
||||
description = "The base component to build a Matrix client library."
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ruma", "nio"]
|
||||
license = "Apache-2.0"
|
||||
@@ -9,7 +9,7 @@ name = "matrix-sdk-base"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version.workspace = true
|
||||
version = "0.12.0"
|
||||
version = "0.14.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
@@ -25,8 +25,21 @@ 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"]
|
||||
automatic-room-key-forwarding = [
|
||||
"matrix-sdk-crypto?/automatic-room-key-forwarding",
|
||||
]
|
||||
experimental-send-custom-to-device = [
|
||||
"matrix-sdk-crypto?/experimental-send-custom-to-device",
|
||||
]
|
||||
|
||||
# Enable experimental support for encrypting state events; see
|
||||
# https://github.com/matrix-org/matrix-rust-sdk/issues/5397.
|
||||
experimental-encrypted-state-events = [
|
||||
"e2e-encryption",
|
||||
"ruma/unstable-msc3414",
|
||||
"matrix-sdk-crypto?/experimental-encrypted-state-events"
|
||||
]
|
||||
|
||||
uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"]
|
||||
|
||||
# Private feature, see
|
||||
@@ -58,7 +71,7 @@ assert_matches = { workspace = true, optional = true }
|
||||
assert_matches2 = { workspace = true, optional = true }
|
||||
async-trait.workspace = true
|
||||
bitflags = { workspace = true, features = ["serde"] }
|
||||
decancer = "3.3.0"
|
||||
decancer = "3.3.3"
|
||||
eyeball = { workspace = true, features = ["async-lock"] }
|
||||
eyeball-im.workspace = true
|
||||
futures-util.workspace = true
|
||||
@@ -69,7 +82,7 @@ matrix-sdk-crypto = { workspace = true, optional = true }
|
||||
matrix-sdk-store-encryption.workspace = true
|
||||
matrix-sdk-test = { workspace = true, optional = true }
|
||||
once_cell.workspace = true
|
||||
regex = "1.11.1"
|
||||
regex = "1.11.2"
|
||||
ruma = { workspace = true, features = [
|
||||
"canonical-json",
|
||||
"unstable-msc2867",
|
||||
@@ -86,12 +99,14 @@ unicode-normalization.workspace = true
|
||||
uniffi = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
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
|
||||
matrix-sdk-test-utils.workspace = true
|
||||
similar-asserts.workspace = true
|
||||
stream_assert.workspace = true
|
||||
|
||||
@@ -100,6 +115,7 @@ tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
[target.'cfg(target_family = "wasm")'.dev-dependencies]
|
||||
wasm-bindgen-test.workspace = true
|
||||
gloo-timers = { workspace = true, features = ["futures"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -24,35 +24,37 @@ use std::{
|
||||
use eyeball::{SharedObservable, Subscriber};
|
||||
use eyeball_im::{Vector, VectorDiff};
|
||||
use futures_util::Stream;
|
||||
use matrix_sdk_common::timer;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_crypto::{
|
||||
store::DynCryptoStore, types::requests::ToDeviceRequest, CollectStrategy, EncryptionSettings,
|
||||
OlmError, OlmMachine, TrustRequirement,
|
||||
CollectStrategy, DecryptionSettings, EncryptionSettings, OlmError, OlmMachine,
|
||||
TrustRequirement, store::DynCryptoStore, types::requests::ToDeviceRequest,
|
||||
};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::events::room::{history_visibility::HistoryVisibility, member::MembershipState};
|
||||
#[cfg(doc)]
|
||||
use ruma::DeviceId;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::events::room::{history_visibility::HistoryVisibility, member::MembershipState};
|
||||
use ruma::{
|
||||
MilliSecondsSinceUnixEpoch, OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||
api::client::{self as api, sync::sync_events::v5},
|
||||
events::{
|
||||
StateEvent, StateEventType,
|
||||
ignored_user_list::IgnoredUserListEventContent,
|
||||
push_rules::{PushRulesEvent, PushRulesEventContent},
|
||||
room::member::SyncRoomMemberEvent,
|
||||
StateEvent, StateEventType,
|
||||
},
|
||||
push::Ruleset,
|
||||
time::Instant,
|
||||
OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
use tokio::sync::{broadcast, Mutex};
|
||||
use tokio::sync::{Mutex, broadcast};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
use tracing::{debug, enabled, info, instrument, warn, Level};
|
||||
use tracing::{Level, debug, enabled, info, instrument, warn};
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use crate::RoomMemberships;
|
||||
use crate::{
|
||||
InviteAcceptanceDetails, RoomStateFilter, SessionMeta,
|
||||
deserialized_responses::DisplayName,
|
||||
error::{Error, Result},
|
||||
event_cache::store::EventCacheStoreLock,
|
||||
@@ -61,12 +63,11 @@ use crate::{
|
||||
Room, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMembersUpdate, RoomState,
|
||||
},
|
||||
store::{
|
||||
ambiguity_map::AmbiguityCache, BaseStateStore, DynStateStore, MemoryStore,
|
||||
Result as StoreResult, RoomLoadSettings, StateChanges, StateStoreDataKey,
|
||||
StateStoreDataValue, StateStoreExt, StoreConfig,
|
||||
BaseStateStore, DynStateStore, MemoryStore, Result as StoreResult, RoomLoadSettings,
|
||||
StateChanges, StateStoreDataKey, StateStoreDataValue, StateStoreExt, StoreConfig,
|
||||
ambiguity_map::AmbiguityCache,
|
||||
},
|
||||
sync::{RoomUpdates, SyncResponse},
|
||||
RoomStateFilter, SessionMeta,
|
||||
};
|
||||
|
||||
/// A no (network) IO client implementation.
|
||||
@@ -76,11 +77,12 @@ use crate::{
|
||||
/// rather through `matrix_sdk::Client`.
|
||||
///
|
||||
/// ```rust
|
||||
/// use matrix_sdk_base::{store::StoreConfig, BaseClient};
|
||||
/// use matrix_sdk_base::{BaseClient, ThreadingSupport, store::StoreConfig};
|
||||
///
|
||||
/// 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 {
|
||||
@@ -115,13 +117,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))]
|
||||
@@ -134,6 +139,32 @@ 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 {
|
||||
/// Enable client-wide thread subscriptions support (MSC4306 / MSC4308).
|
||||
///
|
||||
/// This may cause filtering out of thread subscriptions, and loading
|
||||
/// the thread subscriptions via the sliding sync extension,
|
||||
/// when the room list service is being used.
|
||||
with_subscriptions: bool,
|
||||
},
|
||||
/// Threading disabled.
|
||||
Disabled,
|
||||
}
|
||||
|
||||
impl BaseClient {
|
||||
/// Create a new client.
|
||||
///
|
||||
@@ -141,7 +172,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`.
|
||||
@@ -168,9 +199,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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,8 +234,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
|
||||
@@ -222,7 +257,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.
|
||||
@@ -246,7 +281,9 @@ impl BaseClient {
|
||||
|
||||
/// Get a stream of all the rooms changes, in addition to the existing
|
||||
/// rooms.
|
||||
pub fn rooms_stream(&self) -> (Vector<Room>, impl Stream<Item = Vec<VectorDiff<Room>>>) {
|
||||
pub fn rooms_stream(
|
||||
&self,
|
||||
) -> (Vector<Room>, impl Stream<Item = Vec<VectorDiff<Room>>> + use<>) {
|
||||
self.state_store.rooms_stream()
|
||||
}
|
||||
|
||||
@@ -392,26 +429,92 @@ 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.
|
||||
pub async fn room_joined(&self, room_id: &RoomId) -> Result<Room> {
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The unique ID identifying the joined room.
|
||||
/// * `inviter` - When joining this room in response to an invitation, the
|
||||
/// inviter should be recorded before sending the join request to the
|
||||
/// server. Providing the inviter here ensures that the
|
||||
/// [`InviteAcceptanceDetails`] are stored for this room.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use matrix_sdk_base::{BaseClient, store::StoreConfig, RoomState, ThreadingSupport};
|
||||
/// # use ruma::{OwnedRoomId, OwnedUserId, RoomId};
|
||||
/// # async {
|
||||
/// # let client = BaseClient::new(StoreConfig::new("example".to_owned()), ThreadingSupport::Disabled);
|
||||
/// # async fn send_join_request() -> anyhow::Result<OwnedRoomId> { todo!() }
|
||||
/// # async fn maybe_get_inviter(room_id: &RoomId) -> anyhow::Result<Option<OwnedUserId>> { todo!() }
|
||||
/// # let room_id: &RoomId = todo!();
|
||||
/// let maybe_inviter = maybe_get_inviter(room_id).await?;
|
||||
/// let room_id = send_join_request().await?;
|
||||
/// let room = client.room_joined(&room_id, maybe_inviter).await?;
|
||||
///
|
||||
/// assert_eq!(room.state(), RoomState::Joined);
|
||||
/// # matrix_sdk_test::TestResult::Ok(()) };
|
||||
/// ```
|
||||
pub async fn room_joined(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
inviter: Option<OwnedUserId>,
|
||||
) -> Result<Room> {
|
||||
let room = self.state_store.get_or_create_room(
|
||||
room_id,
|
||||
RoomState::Joined,
|
||||
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();
|
||||
let previous_state = room.state();
|
||||
|
||||
room_info.mark_as_joined();
|
||||
room_info.mark_state_partially_synced();
|
||||
room_info.mark_members_missing(); // the own member event changed
|
||||
|
||||
// If our previous state was an invite and we're now in the joined state, this
|
||||
// means that the user has explicitly accepted an invite. Let's
|
||||
// remember some details about the invite.
|
||||
//
|
||||
// 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 previous_state == RoomState::Invited
|
||||
&& let Some(inviter) = inviter
|
||||
{
|
||||
let details = InviteAcceptanceDetails {
|
||||
invite_accepted_at: MilliSecondsSinceUnixEpoch::now(),
|
||||
inviter,
|
||||
};
|
||||
room_info.set_invite_acceptance_details(details);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -496,9 +599,14 @@ impl BaseClient {
|
||||
#[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(&response, olm_machine.as_ref()).await?;
|
||||
} = processors::e2ee::to_device::from_sync_v2(
|
||||
&response,
|
||||
olm_machine.as_ref(),
|
||||
&self.decryption_settings,
|
||||
)
|
||||
.await?;
|
||||
|
||||
processors::latest_event::decrypt_from_rooms(
|
||||
&mut context,
|
||||
@@ -509,7 +617,7 @@ impl BaseClient {
|
||||
.collect(),
|
||||
processors::e2ee::E2EE::new(
|
||||
olm_machine.as_ref(),
|
||||
self.decryption_trust_requirement,
|
||||
&self.decryption_settings,
|
||||
self.handle_verification_events,
|
||||
),
|
||||
)
|
||||
@@ -519,7 +627,33 @@ 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| {
|
||||
use matrix_sdk_common::deserialized_responses::{
|
||||
ProcessedToDeviceEvent, ToDeviceUnableToDecryptInfo,
|
||||
ToDeviceUnableToDecryptReason,
|
||||
};
|
||||
|
||||
if let Ok(Some(event_type)) = raw.get_field::<String>("type") {
|
||||
if event_type == "m.room.encrypted" {
|
||||
ProcessedToDeviceEvent::UnableToDecrypt {
|
||||
encrypted_event: raw,
|
||||
utd_info: ToDeviceUnableToDecryptInfo {
|
||||
reason: ToDeviceUnableToDecryptReason::EncryptionIsDisabled,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
ProcessedToDeviceEvent::PlainText(raw)
|
||||
}
|
||||
} else {
|
||||
// Exclude events with no type
|
||||
ProcessedToDeviceEvent::Invalid(raw)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut ambiguity_cache = AmbiguityCache::new(self.state_store.inner.clone());
|
||||
|
||||
@@ -553,7 +687,7 @@ impl BaseClient {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
processors::e2ee::E2EE::new(
|
||||
olm_machine.as_ref(),
|
||||
self.decryption_trust_requirement,
|
||||
&self.decryption_settings,
|
||||
self.handle_verification_events,
|
||||
),
|
||||
)
|
||||
@@ -580,7 +714,7 @@ impl BaseClient {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
processors::e2ee::E2EE::new(
|
||||
olm_machine.as_ref(),
|
||||
self.decryption_trust_requirement,
|
||||
&self.decryption_settings,
|
||||
self.handle_verification_events,
|
||||
),
|
||||
)
|
||||
@@ -751,14 +885,11 @@ impl BaseClient {
|
||||
_ => (),
|
||||
}
|
||||
|
||||
if let StateEvent::Original(e) = &member {
|
||||
if let Some(d) = &e.content.displayname {
|
||||
let display_name = DisplayName::new(d);
|
||||
ambiguity_map
|
||||
.entry(display_name)
|
||||
.or_default()
|
||||
.insert(member.state_key().clone());
|
||||
}
|
||||
if let StateEvent::Original(e) = &member
|
||||
&& let Some(d) = &e.content.displayname
|
||||
{
|
||||
let display_name = DisplayName::new(d);
|
||||
ambiguity_map.entry(display_name).or_default().insert(member.state_key().clone());
|
||||
}
|
||||
|
||||
let sync_member: SyncRoomMemberEvent = member.clone().into();
|
||||
@@ -930,9 +1061,10 @@ impl BaseClient {
|
||||
&self,
|
||||
global_account_data_processor: &processors::account_data::Global,
|
||||
) -> Result<Ruleset> {
|
||||
let _timer = timer!(Level::TRACE, "get_push_rules");
|
||||
if let Some(event) = global_account_data_processor
|
||||
.push_rules()
|
||||
.and_then(|ev| ev.deserialize_as::<PushRulesEvent>().ok())
|
||||
.and_then(|ev| ev.deserialize_as_unchecked::<PushRulesEvent>().ok())
|
||||
{
|
||||
Ok(event.content.global)
|
||||
} else if let Some(event) = self
|
||||
@@ -1045,16 +1177,16 @@ impl From<&v5::Request> for RequestedRequiredStates {
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use assert_matches2::assert_let;
|
||||
use assert_matches2::{assert_let, assert_matches};
|
||||
use futures_util::FutureExt as _;
|
||||
use matrix_sdk_test::{
|
||||
async_test, event_factory::EventFactory, ruma_response_from_json, InvitedRoomBuilder,
|
||||
LeftRoomBuilder, StateTestEvent, StrippedStateTestEvent, SyncResponseBuilder, BOB,
|
||||
BOB, InvitedRoomBuilder, LeftRoomBuilder, StateTestEvent, StrippedStateTestEvent,
|
||||
SyncResponseBuilder, async_test, event_factory::EventFactory, ruma_response_from_json,
|
||||
};
|
||||
use ruma::{
|
||||
api::client::{self as api, sync::sync_events::v5},
|
||||
event_id,
|
||||
events::{room::member::MembershipState, StateEventType},
|
||||
events::{StateEventType, room::member::MembershipState},
|
||||
room_id,
|
||||
serde::Raw,
|
||||
user_id,
|
||||
@@ -1063,9 +1195,10 @@ mod tests {
|
||||
|
||||
use super::{BaseClient, RequestedRequiredStates};
|
||||
use crate::{
|
||||
RoomDisplayName, RoomState, SessionMeta,
|
||||
client::ThreadingSupport,
|
||||
store::{RoomLoadSettings, StateStoreExt, StoreConfig},
|
||||
test_utils::logged_in_base_client,
|
||||
RoomDisplayName, RoomState, SessionMeta,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -1357,8 +1490,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() },
|
||||
@@ -1417,8 +1552,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() },
|
||||
@@ -1479,8 +1616,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() },
|
||||
@@ -1551,8 +1690,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(
|
||||
@@ -1567,18 +1708,10 @@ mod tests {
|
||||
let mut subscriber = client.subscribe_to_ignore_user_list_changes();
|
||||
assert!(subscriber.next().now_or_never().is_none());
|
||||
|
||||
let f = EventFactory::new();
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
let response = sync_builder
|
||||
.add_global_account_data_event(matrix_sdk_test::GlobalAccountDataTestEvent::Custom(
|
||||
json!({
|
||||
"content": {
|
||||
"ignored_users": {
|
||||
*BOB: {}
|
||||
}
|
||||
},
|
||||
"type": "m.ignored_user_list",
|
||||
}),
|
||||
))
|
||||
.add_global_account_data(f.ignored_user_list([(*BOB).into()]))
|
||||
.build_sync_response();
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
@@ -1587,16 +1720,7 @@ mod tests {
|
||||
|
||||
// Receive the same response.
|
||||
let response = sync_builder
|
||||
.add_global_account_data_event(matrix_sdk_test::GlobalAccountDataTestEvent::Custom(
|
||||
json!({
|
||||
"content": {
|
||||
"ignored_users": {
|
||||
*BOB: {}
|
||||
}
|
||||
},
|
||||
"type": "m.ignored_user_list",
|
||||
}),
|
||||
))
|
||||
.add_global_account_data(f.ignored_user_list([(*BOB).into()]))
|
||||
.build_sync_response();
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
@@ -1604,16 +1728,8 @@ mod tests {
|
||||
assert!(subscriber.next().now_or_never().is_none());
|
||||
|
||||
// Now remove Bob from the ignored list.
|
||||
let response = sync_builder
|
||||
.add_global_account_data_event(matrix_sdk_test::GlobalAccountDataTestEvent::Custom(
|
||||
json!({
|
||||
"content": {
|
||||
"ignored_users": {}
|
||||
},
|
||||
"type": "m.ignored_user_list",
|
||||
}),
|
||||
))
|
||||
.build_sync_response();
|
||||
let response =
|
||||
sync_builder.add_global_account_data(f.ignored_user_list([])).build_sync_response();
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
assert_let!(Some(ignored) = subscriber.next().await);
|
||||
@@ -1626,20 +1742,70 @@ mod tests {
|
||||
let client = logged_in_base_client(None).await;
|
||||
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
let f = EventFactory::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",
|
||||
}),
|
||||
))
|
||||
.add_global_account_data(f.ignored_user_list([ignored_user_id.to_owned()]))
|
||||
.build_sync_response();
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
assert!(client.is_user_ignored(ignored_user_id).await);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_invite_details_are_set() {
|
||||
let user_id = user_id!("@alice:localhost");
|
||||
let client = logged_in_base_client(Some(user_id)).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.invite_acceptance_details().is_none());
|
||||
|
||||
// Now we join the room.
|
||||
let joined_room = client
|
||||
.room_joined(invited_room_id, Some(user_id.to_owned()))
|
||||
.await
|
||||
.expect("We should be able to mark a room as joined");
|
||||
|
||||
// Yup, we now have some invite details.
|
||||
assert_eq!(joined_room.state(), RoomState::Joined);
|
||||
assert_matches!(joined_room.invite_acceptance_details(), Some(details));
|
||||
assert_eq!(details.inviter, user_id);
|
||||
|
||||
// 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, Some(user_id.to_owned()))
|
||||
.await
|
||||
.expect("We should be able to mark a room as joined");
|
||||
|
||||
assert_eq!(unknown_room.state(), RoomState::Joined);
|
||||
assert!(unknown_room.invite_acceptance_details().is_none());
|
||||
|
||||
sync_builder.clear();
|
||||
let response =
|
||||
sync_builder.add_left_room(LeftRoomBuilder::new(invited_room_id)).build_sync_response();
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
// Now that we left the room, we shouldn't have any details anymore.
|
||||
let left_room = client
|
||||
.get_room(invited_room_id)
|
||||
.expect("The sync should have created a room in the invited state");
|
||||
|
||||
assert_eq!(left_room.state(), RoomState::Left);
|
||||
assert!(left_room.invite_acceptance_details().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`.
|
||||
|
||||
@@ -20,17 +20,18 @@ pub use matrix_sdk_common::deserialized_responses::*;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use ruma::{
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UInt, UserId,
|
||||
events::{
|
||||
AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, EventContentFromType,
|
||||
PossiblyRedactedStateEventContent, RedactContent, RedactedStateEventContent,
|
||||
StateEventContent, StaticStateEventContent, StrippedStateEvent, SyncStateEvent,
|
||||
room::{
|
||||
member::{MembershipState, RoomMemberEvent, RoomMemberEventContent},
|
||||
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
||||
},
|
||||
AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, EventContentFromType,
|
||||
PossiblyRedactedStateEventContent, RedactContent, RedactedStateEventContent,
|
||||
StateEventContent, StaticStateEventContent, StrippedStateEvent, SyncStateEvent,
|
||||
},
|
||||
room_version_rules::AuthorizationRules,
|
||||
serde::Raw,
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UInt, UserId,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use unicode_normalization::UnicodeNormalization;
|
||||
@@ -304,8 +305,8 @@ impl RawAnySyncOrStrippedState {
|
||||
C::Redacted: RedactedStateEventContent,
|
||||
{
|
||||
match self {
|
||||
Self::Sync(raw) => RawSyncOrStrippedState::Sync(raw.cast()),
|
||||
Self::Stripped(raw) => RawSyncOrStrippedState::Stripped(raw.cast()),
|
||||
Self::Sync(raw) => RawSyncOrStrippedState::Sync(raw.cast_unchecked()),
|
||||
Self::Stripped(raw) => RawSyncOrStrippedState::Stripped(raw.cast_unchecked()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -517,10 +518,14 @@ impl MemberEvent {
|
||||
|
||||
impl SyncOrStrippedState<RoomPowerLevelsEventContent> {
|
||||
/// The power levels of the event.
|
||||
pub fn power_levels(&self) -> RoomPowerLevels {
|
||||
pub fn power_levels(
|
||||
&self,
|
||||
rules: &AuthorizationRules,
|
||||
creators: Vec<OwnedUserId>,
|
||||
) -> RoomPowerLevels {
|
||||
match self {
|
||||
Self::Sync(e) => e.power_levels(),
|
||||
Self::Stripped(e) => e.power_levels(),
|
||||
Self::Sync(e) => e.power_levels(rules, creators),
|
||||
Self::Stripped(e) => e.power_levels(rules, creators),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,34 +14,37 @@
|
||||
|
||||
//! Trait and macro of integration tests for `EventCacheStore` implementations.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use assert_matches2::assert_let;
|
||||
use matrix_sdk_common::{
|
||||
deserialized_responses::{
|
||||
AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, TimelineEvent, TimelineEventKind,
|
||||
VerificationState,
|
||||
},
|
||||
linked_chunk::{
|
||||
lazy_loader, ChunkContent, ChunkIdentifier as CId, LinkedChunkId, Position, Update,
|
||||
ChunkContent, ChunkIdentifier as CId, LinkedChunkId, Position, Update, lazy_loader,
|
||||
},
|
||||
};
|
||||
use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID};
|
||||
use matrix_sdk_test::{ALICE, DEFAULT_TEST_ROOM_ID, event_factory::EventFactory};
|
||||
use ruma::{
|
||||
EventId, RoomId,
|
||||
api::client::media::get_content_thumbnail::v3::Method,
|
||||
event_id,
|
||||
events::{
|
||||
AnyMessageLikeEvent, AnyTimelineEvent,
|
||||
relation::RelationType,
|
||||
room::{message::RoomMessageEventContentWithoutRelation, MediaSource},
|
||||
room::{MediaSource, message::RoomMessageEventContentWithoutRelation},
|
||||
},
|
||||
mxc_uri,
|
||||
push::Action,
|
||||
room_id, uint, EventId, RoomId,
|
||||
room_id, uint,
|
||||
};
|
||||
|
||||
use super::{media::IgnoreMediaRetentionPolicy, DynEventCacheStore};
|
||||
use super::{DynEventCacheStore, media::IgnoreMediaRetentionPolicy};
|
||||
use crate::{
|
||||
event_cache::{store::DEFAULT_CHUNK_CAPACITY, Gap},
|
||||
event_cache::{Gap, store::DEFAULT_CHUNK_CAPACITY},
|
||||
media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},
|
||||
};
|
||||
|
||||
@@ -74,7 +77,7 @@ pub fn make_test_event_with_event_id(
|
||||
if let Some(event_id) = event_id {
|
||||
builder = builder.event_id(event_id);
|
||||
}
|
||||
let event = builder.into_raw_timeline().cast();
|
||||
let event = builder.into_raw();
|
||||
|
||||
TimelineEvent::from_decrypted(
|
||||
DecryptedRoomEvent { event, encryption_info, unsigned_encryption_info: None },
|
||||
@@ -103,7 +106,7 @@ pub fn check_test_event(event: &TimelineEvent, text: &str) {
|
||||
|
||||
// Check event.
|
||||
let deserialized = d.event.deserialize().unwrap();
|
||||
assert_matches!(deserialized, ruma::events::AnyMessageLikeEvent::RoomMessage(msg) => {
|
||||
assert_matches!(deserialized, AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::RoomMessage(msg)) => {
|
||||
assert_eq!(msg.as_original().unwrap().content.body(), text);
|
||||
});
|
||||
});
|
||||
@@ -133,6 +136,9 @@ 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_linked_chunks(&self);
|
||||
|
||||
@@ -150,6 +156,10 @@ pub trait EventCacheStoreIntegrationTests {
|
||||
|
||||
/// Test that saving an event works as expected.
|
||||
async fn test_save_event(&self);
|
||||
|
||||
/// Test multiple things related to distinguishing a thread linked chunk
|
||||
/// from a room linked chunk.
|
||||
async fn test_thread_vs_room_linked_chunk(&self);
|
||||
}
|
||||
|
||||
impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
@@ -417,6 +427,72 @@ 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);
|
||||
@@ -691,31 +767,39 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
.unwrap();
|
||||
|
||||
// Sanity check: both linked chunks can be reloaded.
|
||||
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());
|
||||
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_linked_chunks().await.unwrap();
|
||||
|
||||
// Both rooms now have no linked chunk.
|
||||
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());
|
||||
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) {
|
||||
@@ -822,8 +906,8 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let duplicated_events = self
|
||||
.filter_duplicated_events(
|
||||
let duplicated_events = BTreeMap::from_iter(
|
||||
self.filter_duplicated_events(
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
event_comte.event_id().unwrap().to_owned(),
|
||||
@@ -835,20 +919,22 @@ 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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -899,19 +985,21 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
assert_eq!(event.event_id(), event_comte.event_id());
|
||||
|
||||
// Now let's try to find an event that exists, but not in the expected room.
|
||||
assert!(self
|
||||
.find_event(room_id, event_gruyere.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.is_none());
|
||||
assert!(
|
||||
self.find_event(room_id, event_gruyere.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.is_none()
|
||||
);
|
||||
|
||||
// Clearing the rooms also clears the event's storage.
|
||||
self.clear_all_linked_chunks().await.expect("failed to clear all rooms chunks");
|
||||
assert!(self
|
||||
.find_event(room_id, event_comte.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.is_none());
|
||||
assert!(
|
||||
self.find_event(room_id, event_comte.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_find_event_relations(&self) {
|
||||
@@ -949,7 +1037,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();
|
||||
@@ -957,8 +1045,17 @@ 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
|
||||
@@ -966,7 +1063,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(
|
||||
@@ -977,8 +1074,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
|
||||
@@ -986,6 +1083,37 @@ 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) {
|
||||
@@ -1018,16 +1146,151 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
assert_eq!(event.event_id(), event_gruyere.event_id());
|
||||
|
||||
// But they won't be returned when searching in the wrong room.
|
||||
assert!(self
|
||||
.find_event(another_room_id, event_comte.event_id().unwrap().as_ref())
|
||||
assert!(
|
||||
self.find_event(another_room_id, event_comte.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
self.find_event(room_id, event_gruyere.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_thread_vs_room_linked_chunk(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
|
||||
let event = |msg: &str| make_test_event(room_id, msg);
|
||||
|
||||
let thread1_ev = event("comté");
|
||||
let thread2_ev = event("gruyère");
|
||||
let thread2_ev2 = event("beaufort");
|
||||
let room_ev = event("brillat savarin triple crème");
|
||||
|
||||
let thread_root1 = event("thread1");
|
||||
let thread_root2 = event("thread2");
|
||||
|
||||
// Add one event in a thread linked chunk.
|
||||
self.handle_linked_chunk_updates(
|
||||
LinkedChunkId::Thread(room_id, thread_root1.event_id().unwrap().as_ref()),
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![thread1_ev.clone()],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Add one event in another thread linked chunk (same room).
|
||||
self.handle_linked_chunk_updates(
|
||||
LinkedChunkId::Thread(room_id, thread_root2.event_id().unwrap().as_ref()),
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![thread2_ev.clone(), thread2_ev2.clone()],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Add another event to the room 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![room_ev.clone()],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// All the events can be found with `find_event()` for the room.
|
||||
self.find_event(room_id, thread2_ev.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.is_none());
|
||||
assert!(self
|
||||
.find_event(room_id, event_gruyere.event_id().unwrap().as_ref())
|
||||
.expect("failed to find thread1_ev");
|
||||
|
||||
self.find_event(room_id, thread2_ev.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.is_none());
|
||||
.expect("failed to find thread2_ev");
|
||||
|
||||
self.find_event(room_id, thread2_ev2.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.expect("failed to find thread2_ev2");
|
||||
|
||||
self.find_event(room_id, room_ev.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.expect("failed to find room_ev");
|
||||
|
||||
// Finding duplicates operates based on the linked chunk id.
|
||||
let dups = self
|
||||
.filter_duplicated_events(
|
||||
LinkedChunkId::Thread(room_id, thread_root1.event_id().unwrap().as_ref()),
|
||||
vec![
|
||||
thread1_ev.event_id().unwrap().to_owned(),
|
||||
room_ev.event_id().unwrap().to_owned(),
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dups.len(), 1);
|
||||
assert_eq!(dups[0].0, thread1_ev.event_id().unwrap());
|
||||
|
||||
// Loading all chunks operates based on the linked chunk id.
|
||||
let all_chunks = self
|
||||
.load_all_chunks(LinkedChunkId::Thread(
|
||||
room_id,
|
||||
thread_root2.event_id().unwrap().as_ref(),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(all_chunks.len(), 1);
|
||||
assert_eq!(all_chunks[0].identifier, CId::new(0));
|
||||
assert_let!(ChunkContent::Items(observed_items) = all_chunks[0].content.clone());
|
||||
assert_eq!(observed_items.len(), 2);
|
||||
assert_eq!(observed_items[0].event_id(), thread2_ev.event_id());
|
||||
assert_eq!(observed_items[1].event_id(), thread2_ev2.event_id());
|
||||
|
||||
// Loading the metadata of all chunks operates based on the linked chunk
|
||||
// id.
|
||||
let metas = self
|
||||
.load_all_chunks_metadata(LinkedChunkId::Thread(
|
||||
room_id,
|
||||
thread_root2.event_id().unwrap().as_ref(),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(metas.len(), 1);
|
||||
assert_eq!(metas[0].identifier, CId::new(0));
|
||||
assert_eq!(metas[0].num_items, 2);
|
||||
|
||||
// Loading the last chunk operates based on the linked chunk id.
|
||||
let (last_chunk, _chunk_identifier_generator) = self
|
||||
.load_last_chunk(LinkedChunkId::Thread(
|
||||
room_id,
|
||||
thread_root1.event_id().unwrap().as_ref(),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
let last_chunk = last_chunk.unwrap();
|
||||
assert_eq!(last_chunk.identifier, CId::new(0));
|
||||
assert_let!(ChunkContent::Items(observed_items) = last_chunk.content);
|
||||
assert_eq!(observed_items.len(), 1);
|
||||
assert_eq!(observed_items[0].event_id(), thread1_ev.event_id());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1050,8 +1313,8 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
/// mod tests {
|
||||
/// use super::{EventCacheStore, EventCacheStoreResult, MyStore};
|
||||
///
|
||||
/// async fn get_event_cache_store(
|
||||
/// ) -> EventCacheStoreResult<impl EventCacheStore> {
|
||||
/// async fn get_event_cache_store()
|
||||
/// -> EventCacheStoreResult<impl EventCacheStore> {
|
||||
/// Ok(MyStore::new())
|
||||
/// }
|
||||
///
|
||||
@@ -1105,6 +1368,13 @@ macro_rules! event_cache_store_integration_tests {
|
||||
event_cache_store.test_rebuild_empty_linked_chunk().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
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_load_all_chunks_metadata().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_clear_all_linked_chunks() {
|
||||
let event_cache_store =
|
||||
@@ -1146,6 +1416,13 @@ macro_rules! event_cache_store_integration_tests {
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_save_event().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_thread_vs_room_linked_chunk() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_thread_vs_room_linked_chunk().await;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1156,11 +1433,14 @@ macro_rules! event_cache_store_integration_tests {
|
||||
#[macro_export]
|
||||
macro_rules! event_cache_store_integration_tests_time {
|
||||
() => {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
mod event_cache_store_integration_tests_time {
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
|
||||
use gloo_timers::future::sleep;
|
||||
use matrix_sdk_test::async_test;
|
||||
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
|
||||
use tokio::time::sleep;
|
||||
use $crate::event_cache::store::IntoEventCacheStore;
|
||||
|
||||
use super::get_event_cache_store;
|
||||
@@ -1189,26 +1469,26 @@ macro_rules! event_cache_store_integration_tests_time {
|
||||
assert!(!acquired5);
|
||||
|
||||
// That's a nice test we got here, go take a little nap.
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
// Still too early.
|
||||
let acquired55 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(!acquired55);
|
||||
|
||||
// Ok you can take another nap then.
|
||||
tokio::time::sleep(Duration::from_millis(250)).await;
|
||||
sleep(Duration::from_millis(250)).await;
|
||||
|
||||
// At some point, we do get the lock.
|
||||
let acquired6 = store.try_take_leased_lock(0, "key", "bob").await.unwrap();
|
||||
assert!(acquired6);
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(1)).await;
|
||||
sleep(Duration::from_millis(1)).await;
|
||||
|
||||
// The other gets it almost immediately too.
|
||||
let acquired7 = store.try_take_leased_lock(0, "key", "alice").await.unwrap();
|
||||
assert!(acquired7);
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(1)).await;
|
||||
sleep(Duration::from_millis(1)).await;
|
||||
|
||||
// But when we take a longer lease...
|
||||
let acquired8 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
|
||||
@@ -22,7 +22,7 @@ use ruma::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
media_service::IgnoreMediaRetentionPolicy, EventCacheStoreMedia, MediaRetentionPolicy,
|
||||
EventCacheStoreMedia, MediaRetentionPolicy, media_service::IgnoreMediaRetentionPolicy,
|
||||
};
|
||||
use crate::media::{MediaFormat, MediaRequestParameters};
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@ use std::{fmt, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
executor::{spawn, JoinHandle},
|
||||
locks::Mutex,
|
||||
AsyncTraitDeps, SendOutsideWasm, SyncOutsideWasm,
|
||||
executor::{JoinHandle, spawn},
|
||||
locks::Mutex,
|
||||
};
|
||||
use ruma::{time::SystemTime, MxcUri};
|
||||
use ruma::{MxcUri, time::SystemTime};
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
use tracing::error;
|
||||
|
||||
@@ -538,15 +538,15 @@ mod tests {
|
||||
use matrix_sdk_common::locks::Mutex;
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{
|
||||
MxcUri, OwnedMxcUri,
|
||||
events::room::MediaSource,
|
||||
mxc_uri,
|
||||
time::{Duration, SystemTime},
|
||||
MxcUri, OwnedMxcUri,
|
||||
};
|
||||
|
||||
use super::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaService, TimeProvider};
|
||||
use crate::{
|
||||
event_cache::store::{media::MediaRetentionPolicy, EventCacheStoreError},
|
||||
event_cache::store::{EventCacheStoreError, media::MediaRetentionPolicy},
|
||||
media::{MediaFormat, MediaRequestParameters, UniqueKey},
|
||||
};
|
||||
|
||||
|
||||
@@ -21,23 +21,22 @@ use std::{
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
linked_chunk::{
|
||||
relational::RelationalLinkedChunk, ChunkIdentifier, ChunkIdentifierGenerator,
|
||||
LinkedChunkId, Position, RawChunk, Update,
|
||||
ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId, Position,
|
||||
RawChunk, Update, relational::RelationalLinkedChunk,
|
||||
},
|
||||
ring_buffer::RingBuffer,
|
||||
store_locks::memory_store_helper::try_take_leased_lock,
|
||||
};
|
||||
use ruma::{
|
||||
EventId, MxcUri, OwnedEventId, OwnedMxcUri, RoomId,
|
||||
events::relation::RelationType,
|
||||
time::{Instant, SystemTime},
|
||||
EventId, MxcUri, OwnedEventId, OwnedMxcUri, RoomId,
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
use super::{
|
||||
compute_filters_string, extract_event_relation,
|
||||
EventCacheStore, EventCacheStoreError, Result, compute_filters_string, extract_event_relation,
|
||||
media::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService},
|
||||
EventCacheStore, EventCacheStoreError, Result,
|
||||
};
|
||||
use crate::{
|
||||
event_cache::{Event, Gap},
|
||||
@@ -148,6 +147,17 @@ impl EventCacheStore for MemoryStore {
|
||||
.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,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
@@ -181,17 +191,17 @@ impl EventCacheStore for MemoryStore {
|
||||
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_linked_chunk_items(linked_chunk_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,10 +222,10 @@ impl EventCacheStore for MemoryStore {
|
||||
) -> Result<Option<Event>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
let event = inner.events.items().find_map(|(event, this_linked_chunk_id)| {
|
||||
(room_id == this_linked_chunk_id.room_id() && event.event_id()? == event_id)
|
||||
.then_some(event.clone())
|
||||
});
|
||||
let event = inner
|
||||
.events
|
||||
.items(room_id)
|
||||
.find_map(|(event, _pos)| (event.event_id()? == event_id).then_some(event.clone()));
|
||||
|
||||
Ok(event)
|
||||
}
|
||||
@@ -225,20 +235,15 @@ 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 filters = compute_filters_string(filters);
|
||||
|
||||
let related_events = inner
|
||||
.events
|
||||
.items()
|
||||
.filter_map(|(event, this_linked_chunk_id)| {
|
||||
// Must be in the same room.
|
||||
if room_id != this_linked_chunk_id.room_id() {
|
||||
return None;
|
||||
}
|
||||
|
||||
.items(room_id)
|
||||
.filter_map(|(event, pos)| {
|
||||
// Must have a relation.
|
||||
let (related_to, rel_type) = extract_event_relation(event.raw())?;
|
||||
|
||||
@@ -249,9 +254,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();
|
||||
@@ -400,7 +405,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();
|
||||
|
||||
@@ -33,9 +33,9 @@ use matrix_sdk_common::store_locks::{
|
||||
};
|
||||
pub use matrix_sdk_store_encryption::Error as StoreEncryptionError;
|
||||
use ruma::{
|
||||
events::{relation::RelationType, AnySyncTimelineEvent},
|
||||
serde::Raw,
|
||||
OwnedEventId,
|
||||
events::{AnySyncTimelineEvent, relation::RelationType},
|
||||
serde::Raw,
|
||||
};
|
||||
use tracing::trace;
|
||||
|
||||
@@ -43,7 +43,7 @@ use tracing::trace;
|
||||
pub use self::integration_tests::EventCacheStoreIntegrationTests;
|
||||
pub use self::{
|
||||
memory_store::MemoryStore,
|
||||
traits::{DynEventCacheStore, EventCacheStore, IntoEventCacheStore, DEFAULT_CHUNK_CAPACITY},
|
||||
traits::{DEFAULT_CHUNK_CAPACITY, DynEventCacheStore, EventCacheStore, IntoEventCacheStore},
|
||||
};
|
||||
|
||||
/// The high-level public type to represent an `EventCacheStore` lock.
|
||||
@@ -126,14 +126,8 @@ impl Deref for EventCacheStoreLockGuard<'_> {
|
||||
pub enum EventCacheStoreError {
|
||||
/// An error happened in the underlying database backend.
|
||||
#[error(transparent)]
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
Backend(Box<dyn std::error::Error + Send + Sync>),
|
||||
|
||||
/// An error happened in the underlying database backend.
|
||||
#[error(transparent)]
|
||||
#[cfg(target_family = "wasm")]
|
||||
Backend(Box<dyn std::error::Error>),
|
||||
|
||||
/// The store is locked with a passphrase and an incorrect passphrase
|
||||
/// was given.
|
||||
#[error("The event cache store failed to be unlocked")]
|
||||
@@ -175,25 +169,12 @@ impl EventCacheStoreError {
|
||||
///
|
||||
/// Shorthand for `EventCacheStoreError::Backend(Box::new(error))`.
|
||||
#[inline]
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub fn backend<E>(error: E) -> Self
|
||||
where
|
||||
E: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
Self::Backend(Box::new(error))
|
||||
}
|
||||
|
||||
/// Create a new [`Backend`][Self::Backend] error.
|
||||
///
|
||||
/// Shorthand for `EventCacheStoreError::Backend(Box::new(error))`.
|
||||
#[inline]
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub fn backend<E>(error: E) -> Self
|
||||
where
|
||||
E: std::error::Error + 'static,
|
||||
{
|
||||
Self::Backend(Box::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
/// An `EventCacheStore` specific result type.
|
||||
@@ -255,11 +236,7 @@ pub fn compute_filters_string(filters: Option<&[RelationType]>) -> Option<Vec<St
|
||||
filter
|
||||
.iter()
|
||||
.map(|f| {
|
||||
if *f == RelationType::Replacement {
|
||||
"m.replace".to_owned()
|
||||
} else {
|
||||
f.to_string()
|
||||
}
|
||||
if *f == RelationType::Replacement { "m.replace".to_owned() } else { f.to_string() }
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
|
||||
@@ -16,16 +16,17 @@ use std::{fmt, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
linked_chunk::{
|
||||
ChunkIdentifier, ChunkIdentifierGenerator, LinkedChunkId, Position, RawChunk, Update,
|
||||
},
|
||||
AsyncTraitDeps,
|
||||
linked_chunk::{
|
||||
ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId, Position,
|
||||
RawChunk, Update,
|
||||
},
|
||||
};
|
||||
use ruma::{events::relation::RelationType, EventId, MxcUri, OwnedEventId, RoomId};
|
||||
use ruma::{EventId, MxcUri, OwnedEventId, RoomId, events::relation::RelationType};
|
||||
|
||||
use super::{
|
||||
media::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy},
|
||||
EventCacheStoreError,
|
||||
media::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy},
|
||||
};
|
||||
use crate::{
|
||||
event_cache::{Event, Gap},
|
||||
@@ -77,6 +78,15 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
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`.
|
||||
///
|
||||
@@ -118,13 +128,20 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
) -> Result<Vec<(OwnedEventId, Position)>, Self::Error>;
|
||||
|
||||
/// Find an event by its ID in a room.
|
||||
///
|
||||
/// This method must return events saved either in any linked chunks, *or*
|
||||
/// events saved "out-of-band" with the [`Self::save_event`] method.
|
||||
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
|
||||
@@ -133,12 +150,15 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
///
|
||||
/// An additional filter can be provided to only retrieve related events for
|
||||
/// a certain relationship.
|
||||
///
|
||||
/// This method must return events saved either in any linked chunks, *or*
|
||||
/// events saved "out-of-band" with the [`Self::save_event`] method.
|
||||
async fn find_event_relations(
|
||||
&self,
|
||||
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.
|
||||
@@ -224,7 +244,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
///
|
||||
/// * `uri` - The `MxcUri` of the media file.
|
||||
async fn get_media_content_for_uri(&self, uri: &MxcUri)
|
||||
-> Result<Option<Vec<u8>>, Self::Error>;
|
||||
-> Result<Option<Vec<u8>>, Self::Error>;
|
||||
|
||||
/// Remove all the media files' content associated to an `MxcUri` from the
|
||||
/// media store.
|
||||
@@ -313,6 +333,13 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
|
||||
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,
|
||||
linked_chunk_id: LinkedChunkId<'_>,
|
||||
@@ -356,7 +383,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)
|
||||
}
|
||||
|
||||
@@ -443,6 +470,12 @@ pub trait IntoEventCacheStore {
|
||||
fn into_event_cache_store(self) -> Arc<DynEventCacheStore>;
|
||||
}
|
||||
|
||||
impl IntoEventCacheStore for Arc<DynEventCacheStore> {
|
||||
fn into_event_cache_store(self) -> Arc<DynEventCacheStore> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoEventCacheStore for T
|
||||
where
|
||||
T: EventCacheStore + Sized + 'static,
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
//! use as a [crate::Room::latest_event].
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use ruma::{MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::{
|
||||
UserId,
|
||||
events::{
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
|
||||
call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent},
|
||||
poll::unstable_start::SyncUnstablePollStartEvent,
|
||||
relation::RelationType,
|
||||
@@ -14,14 +17,43 @@ use ruma::{
|
||||
power_levels::RoomPowerLevels,
|
||||
},
|
||||
sticker::SyncStickerEvent,
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
|
||||
},
|
||||
UserId,
|
||||
};
|
||||
use ruma::{MxcUri, OwnedEventId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::MinimalRoomMemberEvent;
|
||||
use crate::{MinimalRoomMemberEvent, store::SerializableEventContent};
|
||||
|
||||
/// A latest event value!
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
pub enum LatestEventValue {
|
||||
/// No value has been computed yet, or no candidate value was found.
|
||||
#[default]
|
||||
None,
|
||||
|
||||
/// The latest event represents a remote event.
|
||||
Remote(RemoteLatestEventValue),
|
||||
|
||||
/// The latest event represents a local event that is sending.
|
||||
LocalIsSending(LocalLatestEventValue),
|
||||
|
||||
/// The latest event represents a local event that cannot be sent, either
|
||||
/// because a previous local event, or this local event cannot be sent.
|
||||
LocalCannotBeSent(LocalLatestEventValue),
|
||||
}
|
||||
|
||||
/// Represents the value for [`LatestEventValue::Remote`].
|
||||
pub type RemoteLatestEventValue = TimelineEvent;
|
||||
|
||||
/// Represents the value for [`LatestEventValue::LocalIsSending`] and
|
||||
/// [`LatestEventValue::LocalCannotBeSent`].
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LocalLatestEventValue {
|
||||
/// The time where the event has been created (by this module).
|
||||
pub timestamp: MilliSecondsSinceUnixEpoch,
|
||||
|
||||
/// The content of the local event.
|
||||
pub content: SerializableEventContent,
|
||||
}
|
||||
|
||||
/// Represents a decision about whether an event could be stored as the latest
|
||||
/// event in a room. Variants starting with Yes indicate that this message could
|
||||
@@ -125,21 +157,21 @@ pub fn is_suitable_for_latest_event<'a>(
|
||||
AnySyncTimelineEvent::State(state) => {
|
||||
// But we make an exception for knocked state events *if* the current user
|
||||
// can either accept or decline them
|
||||
if let AnySyncStateEvent::RoomMember(member) = state {
|
||||
if matches!(member.membership(), MembershipState::Knock) {
|
||||
let can_accept_or_decline_knocks = match power_levels_info {
|
||||
Some((own_user_id, room_power_levels)) => {
|
||||
room_power_levels.user_can_invite(own_user_id)
|
||||
|| room_power_levels.user_can_kick(own_user_id)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
// The current user can act on the knock changes, so they should be
|
||||
// displayed
|
||||
if can_accept_or_decline_knocks {
|
||||
return PossibleLatestEvent::YesKnockedStateEvent(member);
|
||||
if let AnySyncStateEvent::RoomMember(member) = state
|
||||
&& matches!(member.membership(), MembershipState::Knock)
|
||||
{
|
||||
let can_accept_or_decline_knocks = match power_levels_info {
|
||||
Some((own_user_id, room_power_levels)) => {
|
||||
room_power_levels.user_can_invite(own_user_id)
|
||||
|| room_power_levels.user_can_kick(own_user_id)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
// The current user can act on the knock changes, so they should be
|
||||
// displayed
|
||||
if can_accept_or_decline_knocks {
|
||||
return PossibleLatestEvent::YesKnockedStateEvent(member);
|
||||
}
|
||||
}
|
||||
PossibleLatestEvent::NoUnsupportedEventType
|
||||
@@ -227,9 +259,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:?}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,13 +343,17 @@ mod tests {
|
||||
use ruma::serde::Raw;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::{
|
||||
MilliSecondsSinceUnixEpoch, UInt, VoipVersionId,
|
||||
events::{
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, EmptyStateKey,
|
||||
Mentions, MessageLikeUnsigned, OriginalSyncMessageLikeEvent, OriginalSyncStateEvent,
|
||||
RedactedSyncMessageLikeEvent, RedactedUnsigned, StateUnsigned, SyncMessageLikeEvent,
|
||||
call::{
|
||||
SessionDescription,
|
||||
invite::{CallInviteEventContent, SyncCallInviteEvent},
|
||||
notify::{
|
||||
ApplicationType, CallNotifyEventContent, NotifyType, SyncCallNotifyEvent,
|
||||
},
|
||||
SessionDescription,
|
||||
},
|
||||
poll::{
|
||||
unstable_response::{
|
||||
@@ -329,6 +366,7 @@ mod tests {
|
||||
},
|
||||
relation::Replacement,
|
||||
room::{
|
||||
ImageInfo, MediaSource,
|
||||
encrypted::{
|
||||
EncryptedEventScheme, OlmV1Curve25519AesSha2Content, RoomEncryptedEventContent,
|
||||
SyncRoomEncryptedEvent,
|
||||
@@ -338,22 +376,16 @@ mod tests {
|
||||
Relation, RoomMessageEventContent, SyncRoomMessageEvent,
|
||||
},
|
||||
topic::{RoomTopicEventContent, SyncRoomTopicEvent},
|
||||
ImageInfo, MediaSource,
|
||||
},
|
||||
sticker::{StickerEventContent, SyncStickerEvent},
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, EmptyStateKey,
|
||||
Mentions, MessageLikeUnsigned, OriginalSyncMessageLikeEvent, OriginalSyncStateEvent,
|
||||
RedactedSyncMessageLikeEvent, RedactedUnsigned, StateUnsigned, SyncMessageLikeEvent,
|
||||
UnsignedRoomRedactionEvent,
|
||||
},
|
||||
owned_event_id, owned_mxc_uri, owned_user_id, MilliSecondsSinceUnixEpoch, UInt,
|
||||
VoipVersionId,
|
||||
owned_event_id, owned_mxc_uri, owned_user_id,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use super::LatestEvent;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use super::{is_suitable_for_latest_event, PossibleLatestEvent};
|
||||
use super::{PossibleLatestEvent, is_suitable_for_latest_event};
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
@@ -500,7 +532,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_redacted_messages_are_suitable() {
|
||||
// Ruma does not allow constructing UnsignedRoomRedactionEvent instances.
|
||||
let room_redaction_event: UnsignedRoomRedactionEvent = serde_json::from_value(json!({
|
||||
let room_redaction_event = serde_json::from_value(json!({
|
||||
"content": {},
|
||||
"event_id": "$redaction",
|
||||
"sender": "@x:y.za",
|
||||
|
||||
@@ -48,27 +48,28 @@ 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 room::{
|
||||
apply_redaction, EncryptionState, PredecessorRoom, Room, RoomCreateWithCreatorEventContent,
|
||||
RoomDisplayName, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons,
|
||||
RoomMember, RoomMembersUpdate, RoomMemberships, RoomState, RoomStateFilter, SuccessorRoom,
|
||||
EncryptionState, InviteAcceptanceDetails, PredecessorRoom, Room,
|
||||
RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo, RoomInfoNotableUpdate,
|
||||
RoomInfoNotableUpdateReasons, RoomMember, RoomMembersUpdate, RoomMemberships, RoomState,
|
||||
RoomStateFilter, SuccessorRoom, apply_redaction,
|
||||
};
|
||||
pub use store::{
|
||||
ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey,
|
||||
StateStoreDataValue, StoreError,
|
||||
StateStoreDataValue, StoreError, ThreadSubscriptionCatchupToken,
|
||||
};
|
||||
pub use utils::{
|
||||
MinimalRoomMemberEvent, MinimalStateEvent, OriginalMinimalStateEvent, RedactedMinimalStateEvent,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
matrix_sdk_test::init_tracing_for_tests!();
|
||||
matrix_sdk_test_utils::init_tracing_for_tests!();
|
||||
|
||||
/// The Matrix user session info.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
//! Common types for [media content](https://matrix.org/docs/spec/client_server/r0.6.1#id66).
|
||||
|
||||
use ruma::{
|
||||
MxcUri, UInt,
|
||||
api::client::media::get_content_thumbnail::v3::Method,
|
||||
events::{
|
||||
room::{
|
||||
MediaSource,
|
||||
message::{
|
||||
AudioMessageEventContent, FileMessageEventContent, ImageMessageEventContent,
|
||||
LocationMessageEventContent, VideoMessageEventContent,
|
||||
},
|
||||
MediaSource,
|
||||
},
|
||||
sticker::StickerEventContent,
|
||||
},
|
||||
MxcUri, UInt,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
||||
@@ -122,21 +122,26 @@ use std::{
|
||||
num::NonZeroUsize,
|
||||
};
|
||||
|
||||
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::{
|
||||
EventId, OwnedEventId, OwnedUserId, RoomId, UserId,
|
||||
events::{
|
||||
AnySyncMessageLikeEvent, AnySyncTimelineEvent, OriginalSyncMessageLikeEvent,
|
||||
SyncMessageLikeEvent,
|
||||
poll::{start::PollStartEventContent, unstable_start::UnstablePollStartEventContent},
|
||||
receipt::{ReceiptEventContent, ReceiptThread, ReceiptType},
|
||||
room::message::Relation,
|
||||
AnySyncMessageLikeEvent, AnySyncTimelineEvent, OriginalSyncMessageLikeEvent,
|
||||
SyncMessageLikeEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
EventId, OwnedEventId, OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
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
|
||||
@@ -201,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;
|
||||
}
|
||||
@@ -240,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;
|
||||
|
||||
@@ -247,19 +264,19 @@ impl RoomReadReceipts {
|
||||
// Sliding sync sometimes sends the same event multiple times, so it can be at
|
||||
// the beginning and end of a batch, for instance. In that case, just reset
|
||||
// every time we see the event matching the receipt.
|
||||
if let Some(event_id) = event.event_id() {
|
||||
if event_id == receipt_event_id {
|
||||
// Bingo! Switch over to the counting state, after resetting the
|
||||
// previous counts.
|
||||
trace!("Found the event the receipt was referring to! Starting to count.");
|
||||
self.reset();
|
||||
counting_receipts = true;
|
||||
continue;
|
||||
}
|
||||
if let Some(event_id) = event.event_id()
|
||||
&& event_id == receipt_event_id
|
||||
{
|
||||
// Bingo! Switch over to the counting state, after resetting the
|
||||
// previous counts.
|
||||
trace!("Found the event the receipt was referring to! Starting to count.");
|
||||
self.reset();
|
||||
counting_receipts = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if counting_receipts {
|
||||
self.process_event(event, user_id);
|
||||
self.process_event(event, user_id, threading_support);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,17 +387,17 @@ impl ReceiptSelector {
|
||||
// Now consider new receipts.
|
||||
for (event_id, receipts) in &receipt_event.0 {
|
||||
for ty in [ReceiptType::Read, ReceiptType::ReadPrivate] {
|
||||
if let Some(receipt) = receipts.get(&ty).and_then(|receipts| receipts.get(user_id))
|
||||
if let Some(receipts) = receipts.get(&ty)
|
||||
&& let Some(receipt) = receipts.get(user_id)
|
||||
&& matches!(receipt.thread, ReceiptThread::Main | ReceiptThread::Unthreaded)
|
||||
{
|
||||
if matches!(receipt.thread, ReceiptThread::Main | ReceiptThread::Unthreaded) {
|
||||
trace!(%event_id, "found new candidate");
|
||||
if let Some(event_pos) = self.event_id_to_pos.get(event_id) {
|
||||
self.try_select_later(event_id, *event_pos);
|
||||
} else {
|
||||
// It's a new pending receipt.
|
||||
trace!(%event_id, "stashed as pending");
|
||||
pending.push(event_id.clone());
|
||||
}
|
||||
trace!(%event_id, "found new candidate");
|
||||
if let Some(event_pos) = self.event_id_to_pos.get(event_id) {
|
||||
self.try_select_later(event_id, *event_pos);
|
||||
} else {
|
||||
// It's a new pending receipt.
|
||||
trace!(%event_id, "stashed as pending");
|
||||
pending.push(event_id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -443,6 +460,7 @@ pub(crate) fn compute_unread_counts(
|
||||
mut previous_events: Vec<TimelineEvent>,
|
||||
new_events: &[TimelineEvent],
|
||||
read_receipts: &mut RoomReadReceipts,
|
||||
threading_support: ThreadingSupport,
|
||||
) {
|
||||
debug!(?read_receipts, "Starting");
|
||||
|
||||
@@ -489,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;
|
||||
@@ -503,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());
|
||||
@@ -608,18 +631,21 @@ mod tests {
|
||||
use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer};
|
||||
use matrix_sdk_test::event_factory::EventFactory;
|
||||
use ruma::{
|
||||
event_id,
|
||||
EventId, UserId, event_id,
|
||||
events::{
|
||||
receipt::{ReceiptThread, ReceiptType},
|
||||
room::{member::MembershipState, message::MessageType},
|
||||
},
|
||||
owned_event_id, owned_user_id,
|
||||
push::Action,
|
||||
room_id, user_id, EventId, UserId,
|
||||
room_id, user_id,
|
||||
};
|
||||
|
||||
use super::compute_unread_counts;
|
||||
use crate::read_receipts::{marks_as_unread, ReceiptSelector, RoomReadReceipts};
|
||||
use crate::{
|
||||
ThreadingSupport,
|
||||
read_receipts::{ReceiptSelector, RoomReadReceipts, marks_as_unread},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_room_message_marks_as_unread() {
|
||||
@@ -720,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);
|
||||
@@ -728,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);
|
||||
@@ -736,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);
|
||||
@@ -746,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);
|
||||
@@ -756,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);
|
||||
@@ -765,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);
|
||||
@@ -779,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);
|
||||
@@ -800,9 +828,16 @@ mod tests {
|
||||
num_mentions: 37,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(receipts
|
||||
.find_and_process_events(ev0, user_id, &[make_event(event_id!("$1"))],)
|
||||
.not());
|
||||
assert!(
|
||||
receipts
|
||||
.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);
|
||||
assert_eq!(receipts.num_mentions, 37);
|
||||
@@ -816,7 +851,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);
|
||||
@@ -829,17 +869,20 @@ mod tests {
|
||||
num_mentions: 37,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(receipts
|
||||
.find_and_process_events(
|
||||
ev0,
|
||||
user_id,
|
||||
&[
|
||||
make_event(event_id!("$1")),
|
||||
make_event(event_id!("$2")),
|
||||
make_event(event_id!("$3"))
|
||||
],
|
||||
)
|
||||
.not());
|
||||
assert!(
|
||||
receipts
|
||||
.find_and_process_events(
|
||||
ev0,
|
||||
user_id,
|
||||
&[
|
||||
make_event(event_id!("$1")),
|
||||
make_event(event_id!("$2")),
|
||||
make_event(event_id!("$3"))
|
||||
],
|
||||
ThreadingSupport::Disabled
|
||||
)
|
||||
.not()
|
||||
);
|
||||
assert_eq!(receipts.num_unread, 42);
|
||||
assert_eq!(receipts.num_notifications, 13);
|
||||
assert_eq!(receipts.num_mentions, 37);
|
||||
@@ -861,6 +904,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);
|
||||
@@ -883,6 +927,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);
|
||||
@@ -908,7 +953,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,
|
||||
@@ -916,6 +961,7 @@ mod tests {
|
||||
previous_events.clone(),
|
||||
&[ev1.clone(), ev2.clone()],
|
||||
&mut read_receipts,
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
// It did find the receipt event (ev1).
|
||||
@@ -934,6 +980,7 @@ mod tests {
|
||||
previous_events,
|
||||
&[new_event],
|
||||
&mut read_receipts,
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
// Only the new event should be added.
|
||||
@@ -1000,6 +1047,7 @@ mod tests {
|
||||
all_events.clone(),
|
||||
&[],
|
||||
&mut read_receipts,
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
assert!(
|
||||
@@ -1021,6 +1069,7 @@ mod tests {
|
||||
head_events.clone(),
|
||||
&tail_events,
|
||||
&mut read_receipts,
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
assert!(
|
||||
@@ -1065,6 +1114,7 @@ mod tests {
|
||||
events,
|
||||
&[], // no new events
|
||||
&mut read_receipts,
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
// Then there are no unread events,
|
||||
@@ -1102,6 +1152,7 @@ mod tests {
|
||||
events,
|
||||
&[ev0], // duplicate event!
|
||||
&mut read_receipts,
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
// All events are unread, and there's no pending receipt.
|
||||
@@ -1536,6 +1587,7 @@ mod tests {
|
||||
Vec::new(),
|
||||
&events,
|
||||
&mut read_receipts,
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
// Only the last two events sent by Bob count as unread.
|
||||
@@ -1547,4 +1599,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 { with_subscriptions: false },
|
||||
);
|
||||
receipts.process_event(
|
||||
&make_event(own_alice, event_id!("$some_other_thread_root")),
|
||||
own_alice,
|
||||
ThreadingSupport::Enabled { with_subscriptions: false },
|
||||
);
|
||||
|
||||
receipts.process_event(
|
||||
&make_event(bob, event_id!("$some_thread_root")),
|
||||
own_alice,
|
||||
ThreadingSupport::Enabled { with_subscriptions: false },
|
||||
);
|
||||
receipts.process_event(
|
||||
&make_event(bob, event_id!("$some_other_thread_root")),
|
||||
own_alice,
|
||||
ThreadingSupport::Enabled { with_subscriptions: false },
|
||||
);
|
||||
|
||||
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 { with_subscriptions: false },
|
||||
);
|
||||
|
||||
assert_eq!(receipts.num_unread, 1);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,17 +17,18 @@ use std::{
|
||||
mem,
|
||||
};
|
||||
|
||||
use matrix_sdk_common::timer;
|
||||
use ruma::{
|
||||
RoomId,
|
||||
events::{
|
||||
direct::OwnedDirectUserIdentifier, AnyGlobalAccountDataEvent, GlobalAccountDataEventType,
|
||||
AnyGlobalAccountDataEvent, GlobalAccountDataEventType, direct::OwnedDirectUserIdentifier,
|
||||
},
|
||||
serde::Raw,
|
||||
RoomId,
|
||||
};
|
||||
use tracing::{debug, instrument, trace, warn};
|
||||
|
||||
use super::super::Context;
|
||||
use crate::{store::BaseStateStore, RoomInfo, StateChanges};
|
||||
use crate::{RoomInfo, StateChanges, store::BaseStateStore};
|
||||
|
||||
/// Create the [`Global`] account data processor.
|
||||
pub fn global(events: &[Raw<AnyGlobalAccountDataEvent>]) -> Global {
|
||||
@@ -43,6 +44,8 @@ pub struct Global {
|
||||
impl Global {
|
||||
/// Creates a new processor for global account data.
|
||||
fn process(events: &[Raw<AnyGlobalAccountDataEvent>]) -> Self {
|
||||
let _timer = timer!(tracing::Level::TRACE, "Global::process (global account data)");
|
||||
|
||||
let mut raw_by_type = BTreeMap::new();
|
||||
let mut parsed_events = Vec::new();
|
||||
|
||||
@@ -102,10 +105,10 @@ impl Global {
|
||||
|
||||
// Update the direct targets of rooms if they changed.
|
||||
for (room_id, new_direct_targets) in new_dms {
|
||||
if let Some(old_direct_targets) = old_dms.remove(&room_id) {
|
||||
if old_direct_targets == new_direct_targets {
|
||||
continue;
|
||||
}
|
||||
if let Some(old_direct_targets) = old_dms.remove(&room_id)
|
||||
&& old_direct_targets == new_direct_targets
|
||||
{
|
||||
continue;
|
||||
}
|
||||
trace!(?room_id, targets = ?new_direct_targets, "Marking room as direct room");
|
||||
map_info(room_id, state_changes, state_store, |info| {
|
||||
@@ -125,6 +128,8 @@ impl Global {
|
||||
|
||||
/// Applies the processed data to the state changes and the state store.
|
||||
pub async fn apply(mut self, context: &mut Context, state_store: &BaseStateStore) {
|
||||
let _timer = timer!(tracing::Level::TRACE, "Global::apply (global account data)");
|
||||
|
||||
// Fill in the content of `changes.account_data`.
|
||||
mem::swap(&mut context.state_changes.account_data, &mut self.raw_by_type);
|
||||
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
mod global;
|
||||
mod room;
|
||||
|
||||
pub use global::{global, Global};
|
||||
pub use global::{Global, global};
|
||||
pub use room::for_room;
|
||||
|
||||
@@ -13,20 +13,20 @@
|
||||
// limitations under the License.
|
||||
|
||||
use ruma::{
|
||||
events::{marked_unread::MarkedUnreadEventContent, AnyRoomAccountDataEvent},
|
||||
serde::Raw,
|
||||
RoomId,
|
||||
events::{AnyRoomAccountDataEvent, marked_unread::MarkedUnreadEventContent},
|
||||
serde::Raw,
|
||||
};
|
||||
use tracing::{instrument, warn};
|
||||
|
||||
use super::super::{Context, RoomInfoNotableUpdates};
|
||||
use crate::{
|
||||
room::AccountDataSource, store::BaseStateStore, RoomInfo, RoomInfoNotableUpdateReasons,
|
||||
StateChanges,
|
||||
RoomInfo, RoomInfoNotableUpdateReasons, StateChanges, room::AccountDataSource,
|
||||
store::BaseStateStore,
|
||||
};
|
||||
|
||||
#[instrument(skip_all, fields(?room_id))]
|
||||
pub async fn for_room(
|
||||
pub fn for_room(
|
||||
context: &mut Context,
|
||||
room_id: &RoomId,
|
||||
events: &[Raw<AnyRoomAccountDataEvent>],
|
||||
|
||||
@@ -13,22 +13,25 @@
|
||||
// limitations under the License.
|
||||
|
||||
use eyeball::SharedObservable;
|
||||
use matrix_sdk_common::timer;
|
||||
use ruma::{
|
||||
events::{ignored_user_list::IgnoredUserListEvent, GlobalAccountDataEventType},
|
||||
events::{GlobalAccountDataEventType, ignored_user_list::IgnoredUserListEvent},
|
||||
serde::Raw,
|
||||
};
|
||||
use tracing::{error, instrument, trace};
|
||||
|
||||
use super::Context;
|
||||
use crate::{
|
||||
store::{BaseStateStore, StateStoreExt as _},
|
||||
Result,
|
||||
store::{BaseStateStore, StateStoreExt as _},
|
||||
};
|
||||
|
||||
/// 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<()> {
|
||||
let _timer = timer!(tracing::Level::TRACE, "_method");
|
||||
|
||||
save_changes(&context, state_store, None).await?;
|
||||
broadcast_room_info_notable_updates(&context, state_store);
|
||||
|
||||
@@ -44,6 +47,8 @@ pub async fn save_and_apply(
|
||||
ignore_user_list_changes: &SharedObservable<Vec<String>>,
|
||||
sync_token: Option<String>,
|
||||
) -> Result<()> {
|
||||
let _timer = timer!(tracing::Level::TRACE, "_method");
|
||||
|
||||
trace!("ready to submit changes to store");
|
||||
|
||||
let previous_ignored_user_list =
|
||||
@@ -80,7 +85,7 @@ fn apply_changes(
|
||||
if let Some(event) =
|
||||
context.state_changes.account_data.get(&GlobalAccountDataEventType::IgnoredUserList)
|
||||
{
|
||||
match event.deserialize_as::<IgnoredUserListEvent>() {
|
||||
match event.deserialize_as_unchecked::<IgnoredUserListEvent>() {
|
||||
Ok(event) => {
|
||||
let user_ids: Vec<String> =
|
||||
event.content.ignored_users.keys().map(|id| id.to_string()).collect();
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use matrix_sdk_crypto::{DecryptionSettings, RoomEventDecryptionResult};
|
||||
use ruma::{events::AnySyncTimelineEvent, serde::Raw, RoomId};
|
||||
use matrix_sdk_crypto::RoomEventDecryptionResult;
|
||||
use ruma::{RoomId, events::AnySyncTimelineEvent, serde::Raw};
|
||||
|
||||
use super::{super::verification, E2EE};
|
||||
use crate::Result;
|
||||
@@ -33,11 +33,11 @@ pub async fn sync_timeline_event(
|
||||
) -> Result<Option<TimelineEvent>> {
|
||||
let Some(olm) = e2ee.olm_machine else { return Ok(None) };
|
||||
|
||||
let decryption_settings =
|
||||
DecryptionSettings { sender_device_trust_requirement: e2ee.decryption_trust_requirement };
|
||||
|
||||
Ok(Some(
|
||||
match olm.try_decrypt_room_event(event.cast_ref(), room_id, &decryption_settings).await? {
|
||||
match olm
|
||||
.try_decrypt_room_event(event.cast_ref_unchecked(), room_id, e2ee.decryption_settings)
|
||||
.await?
|
||||
{
|
||||
RoomEventDecryptionResult::Decrypted(decrypted) => {
|
||||
// Note: the push actions are set by the caller.
|
||||
let timeline_event = TimelineEvent::from_decrypted(decrypted, None);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk_crypto::{OlmMachine, TrustRequirement};
|
||||
use matrix_sdk_crypto::{DecryptionSettings, OlmMachine};
|
||||
|
||||
pub mod decrypt;
|
||||
pub mod to_device;
|
||||
@@ -22,16 +22,16 @@ pub mod tracked_users;
|
||||
#[derive(Clone)]
|
||||
pub struct E2EE<'a> {
|
||||
pub olm_machine: Option<&'a OlmMachine>,
|
||||
pub decryption_trust_requirement: TrustRequirement,
|
||||
pub decryption_settings: &'a DecryptionSettings,
|
||||
pub verification_is_allowed: bool,
|
||||
}
|
||||
|
||||
impl<'a> E2EE<'a> {
|
||||
pub fn new(
|
||||
olm_machine: Option<&'a OlmMachine>,
|
||||
decryption_trust_requirement: TrustRequirement,
|
||||
decryption_settings: &'a DecryptionSettings,
|
||||
verification_is_allowed: bool,
|
||||
) -> Self {
|
||||
Self { olm_machine, decryption_trust_requirement, verification_is_allowed }
|
||||
Self { olm_machine, decryption_settings, verification_is_allowed }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,17 @@
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use matrix_sdk_crypto::{store::RoomKeyInfo, EncryptionSyncChanges, OlmMachine};
|
||||
use matrix_sdk_common::deserialized_responses::{
|
||||
ProcessedToDeviceEvent, ToDeviceUnableToDecryptInfo, ToDeviceUnableToDecryptReason,
|
||||
};
|
||||
use matrix_sdk_crypto::{
|
||||
DecryptionSettings, EncryptionSyncChanges, OlmMachine, store::types::RoomKeyInfo,
|
||||
};
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::{v3, v5, DeviceLists},
|
||||
OneTimeKeyAlgorithm, UInt,
|
||||
api::client::sync::sync_events::{DeviceLists, v3, v5},
|
||||
events::AnyToDeviceEvent,
|
||||
serde::Raw,
|
||||
OneTimeKeyAlgorithm, UInt,
|
||||
};
|
||||
|
||||
use crate::Result;
|
||||
@@ -33,6 +38,7 @@ pub async fn from_msc4186(
|
||||
to_device: Option<&v5::response::ToDevice>,
|
||||
e2ee: &v5::response::E2EE,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
decryption_settings: &DecryptionSettings,
|
||||
) -> Result<Output> {
|
||||
process(
|
||||
olm_machine,
|
||||
@@ -41,6 +47,7 @@ pub async fn from_msc4186(
|
||||
&e2ee.device_one_time_keys_count,
|
||||
e2ee.device_unused_fallback_key_types.as_deref(),
|
||||
to_device.as_ref().map(|to_device| to_device.next_batch.clone()),
|
||||
decryption_settings,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -53,6 +60,7 @@ pub async fn from_msc4186(
|
||||
pub async fn from_sync_v2(
|
||||
response: &v3::Response,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
decryption_settings: &DecryptionSettings,
|
||||
) -> Result<Output> {
|
||||
process(
|
||||
olm_machine,
|
||||
@@ -61,6 +69,7 @@ pub async fn from_sync_v2(
|
||||
&response.device_one_time_keys_count,
|
||||
response.device_unused_fallback_key_types.as_deref(),
|
||||
Some(response.next_batch.clone()),
|
||||
decryption_settings,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -76,6 +85,7 @@ async fn process(
|
||||
one_time_keys_counts: &BTreeMap<OneTimeKeyAlgorithm, UInt>,
|
||||
unused_fallback_keys: Option<&[OneTimeKeyAlgorithm]>,
|
||||
next_batch_token: Option<String>,
|
||||
decryption_settings: &DecryptionSettings,
|
||||
) -> Result<Output> {
|
||||
let encryption_sync_changes = EncryptionSyncChanges {
|
||||
to_device_events,
|
||||
@@ -91,30 +101,42 @@ async fn process(
|
||||
// This makes sure that we have the decryption keys for the room
|
||||
// events at hand.
|
||||
let (events, room_key_updates) =
|
||||
olm_machine.receive_sync_changes(encryption_sync_changes).await?;
|
||||
olm_machine.receive_sync_changes(encryption_sync_changes, decryption_settings).await?;
|
||||
|
||||
let events = events
|
||||
.iter()
|
||||
// TODO: There is loss of information here, after calling `to_raw` it is not
|
||||
// possible to make the difference between a successfully decrypted event and a plain
|
||||
// text event. This information needs to be propagated to top layer at some point if
|
||||
// clients relies on custom encrypted to device events.
|
||||
.map(|p| p.to_raw())
|
||||
.collect();
|
||||
|
||||
Output { decrypted_to_device_events: events, room_key_updates: Some(room_key_updates) }
|
||||
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 {
|
||||
encrypted_event: raw,
|
||||
utd_info: ToDeviceUnableToDecryptInfo {
|
||||
reason: ToDeviceUnableToDecryptReason::NoOlmMachine,
|
||||
},
|
||||
}
|
||||
} 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>>,
|
||||
}
|
||||
|
||||
@@ -14,10 +14,11 @@
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use matrix_sdk_common::timer;
|
||||
use matrix_sdk_crypto::OlmMachine;
|
||||
use ruma::{OwnedUserId, RoomId};
|
||||
|
||||
use crate::{store::BaseStateStore, EncryptionState, Result, RoomMemberships};
|
||||
use crate::{EncryptionState, Result, RoomMemberships, store::BaseStateStore};
|
||||
|
||||
/// Update tracked users, if the room is encrypted.
|
||||
pub async fn update(
|
||||
@@ -25,12 +26,11 @@ pub async fn update(
|
||||
room_encryption_state: EncryptionState,
|
||||
user_ids_to_track: &BTreeSet<OwnedUserId>,
|
||||
) -> Result<()> {
|
||||
if room_encryption_state.is_encrypted() {
|
||||
if let Some(olm) = olm_machine {
|
||||
if !user_ids_to_track.is_empty() {
|
||||
olm.update_tracked_users(user_ids_to_track.iter().map(AsRef::as_ref)).await?
|
||||
}
|
||||
}
|
||||
if room_encryption_state.is_encrypted()
|
||||
&& let Some(olm) = olm_machine
|
||||
&& !user_ids_to_track.is_empty()
|
||||
{
|
||||
olm.update_tracked_users(user_ids_to_track.iter().map(AsRef::as_ref)).await?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -46,19 +46,21 @@ pub async fn update_or_set_if_room_is_newly_encrypted(
|
||||
room_id: &RoomId,
|
||||
state_store: &BaseStateStore,
|
||||
) -> Result<()> {
|
||||
if new_room_encryption_state.is_encrypted() {
|
||||
if let Some(olm) = olm_machine {
|
||||
if !previous_room_encryption_state.is_encrypted() {
|
||||
// The room turned on encryption in this sync, we need
|
||||
// to also get all the existing users and mark them for
|
||||
// tracking.
|
||||
let user_ids = state_store.get_user_ids(room_id, RoomMemberships::ACTIVE).await?;
|
||||
olm.update_tracked_users(user_ids.iter().map(AsRef::as_ref)).await?
|
||||
}
|
||||
let _timer = timer!(tracing::Level::TRACE, "update_or_set_if_room_is_newly_encrypted");
|
||||
|
||||
if !user_ids_to_track.is_empty() {
|
||||
olm.update_tracked_users(user_ids_to_track.iter().map(AsRef::as_ref)).await?;
|
||||
}
|
||||
if new_room_encryption_state.is_encrypted()
|
||||
&& let Some(olm) = olm_machine
|
||||
{
|
||||
if !previous_room_encryption_state.is_encrypted() {
|
||||
// The room turned on encryption in this sync, we need
|
||||
// to also get all the existing users and mark them for
|
||||
// tracking.
|
||||
let user_ids = state_store.get_user_ids(room_id, RoomMemberships::ACTIVE).await?;
|
||||
olm.update_tracked_users(user_ids.iter().map(AsRef::as_ref)).await?
|
||||
}
|
||||
|
||||
if !user_ids_to_track.is_empty() {
|
||||
olm.update_tracked_users(user_ids_to_track.iter().map(AsRef::as_ref)).await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use ruma::{events::AnySyncEphemeralRoomEvent, serde::Raw, RoomId};
|
||||
use ruma::{RoomId, events::AnySyncEphemeralRoomEvent, serde::Raw};
|
||||
use tracing::info;
|
||||
|
||||
use super::Context;
|
||||
|
||||
@@ -13,13 +13,13 @@
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use matrix_sdk_crypto::{DecryptionSettings, RoomEventDecryptionResult};
|
||||
use ruma::{events::AnySyncTimelineEvent, serde::Raw, RoomId};
|
||||
use matrix_sdk_crypto::RoomEventDecryptionResult;
|
||||
use ruma::{RoomId, events::AnySyncTimelineEvent, serde::Raw};
|
||||
|
||||
use super::{e2ee::E2EE, verification, Context};
|
||||
use super::{Context, e2ee::E2EE, verification};
|
||||
use crate::{
|
||||
latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent},
|
||||
Result, Room,
|
||||
latest_event::{LatestEvent, PossibleLatestEvent, is_suitable_for_latest_event},
|
||||
};
|
||||
|
||||
/// Decrypt any [`Room::latest_encrypted_events`] for a particular set of
|
||||
@@ -108,13 +108,10 @@ async fn decrypt_sync_room_event(
|
||||
e2ee: &E2EE<'_>,
|
||||
room_id: &RoomId,
|
||||
) -> Result<TimelineEvent> {
|
||||
let decryption_settings =
|
||||
DecryptionSettings { sender_device_trust_requirement: e2ee.decryption_trust_requirement };
|
||||
|
||||
let event = match e2ee
|
||||
.olm_machine
|
||||
.expect("An `OlmMachine` is expected")
|
||||
.try_decrypt_room_event(event.cast_ref(), room_id, &decryption_settings)
|
||||
.try_decrypt_room_event(event.cast_ref_unchecked(), room_id, e2ee.decryption_settings)
|
||||
.await?
|
||||
{
|
||||
RoomEventDecryptionResult::Decrypted(decrypted) => {
|
||||
@@ -140,11 +137,11 @@ async fn decrypt_sync_room_event(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use matrix_sdk_test::{
|
||||
async_test, event_factory::EventFactory, JoinedRoomBuilder, SyncResponseBuilder,
|
||||
JoinedRoomBuilder, SyncResponseBuilder, async_test, event_factory::EventFactory,
|
||||
};
|
||||
use ruma::{event_id, events::room::member::MembershipState, room_id, user_id};
|
||||
|
||||
use super::{decrypt_from_rooms, Context, E2EE};
|
||||
use super::{Context, E2EE, decrypt_from_rooms};
|
||||
use crate::{room::RoomInfoNotableUpdateReasons, test_utils::logged_in_base_client};
|
||||
|
||||
#[async_test]
|
||||
@@ -184,7 +181,7 @@ mod tests {
|
||||
vec![room.clone()],
|
||||
E2EE::new(
|
||||
client.olm_machine().await.as_ref(),
|
||||
client.decryption_trust_requirement,
|
||||
&client.decryption_settings,
|
||||
client.handle_verification_events,
|
||||
),
|
||||
)
|
||||
@@ -195,11 +192,13 @@ mod tests {
|
||||
assert!(room.latest_encrypted_events().is_empty());
|
||||
assert!(room.latest_event().is_none());
|
||||
assert!(context.state_changes.room_infos.is_empty());
|
||||
assert!(!context
|
||||
.room_info_notable_updates
|
||||
.get(room_id)
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT));
|
||||
assert!(
|
||||
!context
|
||||
.room_info_notable_updates
|
||||
.get(room_id)
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use ruma::{
|
||||
OwnedRoomId,
|
||||
push::{Action, PushConditionRoomCtx, Ruleset},
|
||||
serde::Raw,
|
||||
OwnedRoomId, RoomId,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -43,25 +43,21 @@ impl<'a> Notification<'a> {
|
||||
|
||||
fn push_notification(
|
||||
&mut self,
|
||||
room_id: &RoomId,
|
||||
room_id: OwnedRoomId,
|
||||
actions: Vec<Action>,
|
||||
event: RawAnySyncOrStrippedTimelineEvent,
|
||||
) {
|
||||
self.notifications
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.push(sync::Notification { actions, event });
|
||||
self.notifications.entry(room_id).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`).
|
||||
/// `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>(
|
||||
pub async fn push_notification_from_event_if<E, P>(
|
||||
&mut self,
|
||||
room_id: &RoomId,
|
||||
push_condition_room_ctx: &PushConditionRoomCtx,
|
||||
event: &Raw<E>,
|
||||
predicate: P,
|
||||
@@ -70,10 +66,14 @@ impl<'a> Notification<'a> {
|
||||
Raw<E>: Into<RawAnySyncOrStrippedTimelineEvent>,
|
||||
P: Fn(&Action) -> bool,
|
||||
{
|
||||
let actions = self.push_rules.get_actions(event, push_condition_room_ctx);
|
||||
let actions = self.push_rules.get_actions(event, push_condition_room_ctx).await;
|
||||
|
||||
if actions.iter().any(predicate) {
|
||||
self.push_notification(room_id, actions.to_owned(), event.clone().into());
|
||||
self.push_notification(
|
||||
push_condition_room_ctx.room_id.clone(),
|
||||
actions.to_owned(),
|
||||
event.clone().into(),
|
||||
);
|
||||
}
|
||||
|
||||
actions
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
// limitations under the License.
|
||||
|
||||
use ruma::{
|
||||
events::{
|
||||
room::member::{MembershipState, RoomMemberEventContent},
|
||||
SyncStateEvent,
|
||||
},
|
||||
RoomId,
|
||||
events::{
|
||||
SyncStateEvent,
|
||||
room::member::{MembershipState, RoomMemberEventContent},
|
||||
},
|
||||
};
|
||||
|
||||
use super::Context;
|
||||
|
||||
@@ -12,10 +12,12 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk_common::timer;
|
||||
|
||||
use super::super::Context;
|
||||
use crate::{
|
||||
room::UpdatedRoomDisplayName, store::BaseStateStore, sync::RoomUpdates,
|
||||
RoomInfoNotableUpdateReasons,
|
||||
RoomInfoNotableUpdateReasons, room::UpdatedRoomDisplayName, store::BaseStateStore,
|
||||
sync::RoomUpdates,
|
||||
};
|
||||
|
||||
pub async fn update_for_rooms(
|
||||
@@ -23,6 +25,8 @@ pub async fn update_for_rooms(
|
||||
room_updates: &RoomUpdates,
|
||||
state_store: &BaseStateStore,
|
||||
) {
|
||||
let _timer = timer!(tracing::Level::TRACE, "display_name::update_for_rooms");
|
||||
|
||||
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`.
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
use ruma::RoomId;
|
||||
use tokio::sync::broadcast::Sender;
|
||||
|
||||
use crate::{store::ambiguity_map::AmbiguityCache, RequestedRequiredStates, RoomInfoNotableUpdate};
|
||||
use crate::{RequestedRequiredStates, RoomInfoNotableUpdate, store::ambiguity_map::AmbiguityCache};
|
||||
|
||||
pub mod display_name;
|
||||
pub mod msc4186;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user